Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
/**
* Hook for managing context menu state.
*/
// eslint-disable-next-line react-refresh/only-export-components
export function useContextMenu() {
const [menu, setMenu] = useState<{ x: number; y: number; items: ContextMenuItem[] } | null>(null)

Expand All @@ -179,6 +180,7 @@ export function useContextMenu() {
/**
* Common context menu items for nodes.
*/
// eslint-disable-next-line react-refresh/only-export-components
export function getNodeContextMenuItems(options: {
onCopy?: () => void
onCut?: () => void
Expand Down Expand Up @@ -264,6 +266,7 @@ export function getNodeContextMenuItems(options: {
/**
* Common context menu items for canvas.
*/
// eslint-disable-next-line react-refresh/only-export-components
export function getCanvasContextMenuItems(options: {
onPaste?: () => void
onSelectAll?: () => void
Expand Down
1 change: 1 addition & 0 deletions src/components/ImageModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export function ImageModal() {
/**
* Helper function to show the image modal from anywhere.
*/
// eslint-disable-next-line react-refresh/only-export-components
export function showImageModal(url: string, title?: string, metadata?: Record<string, unknown>) {
const event = new CustomEvent<ImageModalData>('show-image-modal', {
detail: { url, title, metadata },
Expand Down
1 change: 1 addition & 0 deletions src/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
/**
* Hook to access settings from anywhere in the app.
*/
// eslint-disable-next-line react-refresh/only-export-components
export function useSettings(): Settings {
const [settings, setSettings] = useState<Settings>(DEFAULT_SETTINGS)

Expand Down
1 change: 1 addition & 0 deletions src/context/ExecutionContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export function ExecutionProvider({ children }: ExecutionProviderProps) {
* Hook to access execution state from any component.
* Must be used within an ExecutionProvider.
*/
// eslint-disable-next-line react-refresh/only-export-components
export function useExecutionContext(): ExecutionState {
const context = useContext(ExecutionContext)

Expand Down
1 change: 1 addition & 0 deletions src/context/GraphContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function GraphProvider({ children }: GraphProviderProps) {
* Hook to access graph state from any component.
* Must be used within a GraphProvider.
*/
// eslint-disable-next-line react-refresh/only-export-components
export function useGraphContext(): GraphState {
const context = useContext(GraphContext)

Expand Down
1 change: 1 addition & 0 deletions src/context/ImageHistoryContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export function ImageHistoryProvider({ children }: ImageHistoryProviderProps) {
* Hook to access image history state from any component.
* Must be used within an ImageHistoryProvider.
*/
// eslint-disable-next-line react-refresh/only-export-components
export function useImageHistory(): ImageHistoryState {
const context = useContext(ImageHistoryContext)

Expand Down
127 changes: 67 additions & 60 deletions src/hooks/useClipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,69 +15,16 @@ interface ClipboardOptions {
export function useClipboard({ graph, canvas, liteGraph, onNodeCreated }: ClipboardOptions) {
const isHandling = useRef(false)

/**
* Handle paste from clipboard.
* Supports images, text, and workflow JSON.
*/
const handlePaste = useCallback(async (e?: ClipboardEvent) => {
if (!graph || !canvas || !liteGraph || isHandling.current) return

isHandling.current = true

try {
// Get clipboard items
const items = e?.clipboardData?.items || await getClipboardItems()
if (!items) return

// Check for images first
for (const item of items) {
// Handle both DataTransferItem (from paste event) and ClipboardItem (from Clipboard API)
const itemType = 'type' in item ? (item as DataTransferItem).type : (item as ClipboardItem).types[0]
if (itemType?.startsWith('image/')) {
e?.preventDefault()
await handleImagePaste(item as unknown as ClipboardItem)
return
}
}

// Check for text (could be JSON workflow or prompt)
for (const item of items) {
const itemType = 'type' in item ? (item as DataTransferItem).type : (item as ClipboardItem).types[0]
if (itemType === 'text/plain') {
const text = e?.clipboardData
? e.clipboardData.getData('text/plain')
: await (item as ClipboardItem).getType('text/plain').then(blob => blob.text())

if (text) {
e?.preventDefault()
await handleTextPaste(text)
return
}
}
}
} catch (error) {
console.error('Paste error:', error)
} finally {
isHandling.current = false
}
}, [graph, canvas, liteGraph])

/**
* Get clipboard items using Clipboard API.
*/
async function getClipboardItems(): Promise<ClipboardItems | null> {
try {
return await navigator.clipboard.read()
} catch {
return null
}
}
// Store refs to latest props to avoid stale closures
const propsRef = useRef({ graph, canvas, liteGraph, onNodeCreated })
propsRef.current = { graph, canvas, liteGraph, onNodeCreated }

/**
* Handle pasting an image from clipboard.
* Creates an ImageSource node and uploads the image.
*/
async function handleImagePaste(item: ClipboardItem) {
const handleImagePaste = useCallback(async (item: ClipboardItem) => {
const { graph, canvas, liteGraph, onNodeCreated } = propsRef.current
if (!graph || !canvas || !liteGraph) return

const imageType = item.types[0]
Expand Down Expand Up @@ -119,13 +66,14 @@ export function useClipboard({ graph, canvas, liteGraph, onNodeCreated }: Clipbo
canvas.setDirty(true, true)
canvas.selectNode(node)
onNodeCreated?.(node.id)
}
}, [])

/**
* Handle pasting text from clipboard.
* Could be a workflow JSON, URL, or prompt text.
*/
async function handleTextPaste(text: string) {
const handleTextPaste = useCallback(async (text: string) => {
const { graph, canvas, liteGraph, onNodeCreated } = propsRef.current
if (!graph || !canvas || !liteGraph) return

const trimmed = text.trim()
Expand Down Expand Up @@ -192,6 +140,65 @@ export function useClipboard({ graph, canvas, liteGraph, onNodeCreated }: Clipbo
canvas.selectNode(node)
onNodeCreated?.(node.id)
}
}, [])

/**
* Handle paste from clipboard.
* Supports images, text, and workflow JSON.
*/
const handlePaste = useCallback(async (e?: ClipboardEvent) => {
const { graph, canvas, liteGraph } = propsRef.current
if (!graph || !canvas || !liteGraph || isHandling.current) return

isHandling.current = true

try {
// Get clipboard items
const items = e?.clipboardData?.items || await getClipboardItems()
if (!items) return

// Check for images first
for (const item of items) {
// Handle both DataTransferItem (from paste event) and ClipboardItem (from Clipboard API)
const itemType = 'type' in item ? (item as DataTransferItem).type : (item as ClipboardItem).types[0]
if (itemType?.startsWith('image/')) {
e?.preventDefault()
await handleImagePaste(item as unknown as ClipboardItem)
return
}
}

// Check for text (could be JSON workflow or prompt)
for (const item of items) {
const itemType = 'type' in item ? (item as DataTransferItem).type : (item as ClipboardItem).types[0]
if (itemType === 'text/plain') {
const text = e?.clipboardData
? e.clipboardData.getData('text/plain')
: await (item as ClipboardItem).getType('text/plain').then(blob => blob.text())

if (text) {
e?.preventDefault()
await handleTextPaste(text)
return
}
}
}
} catch (error) {
console.error('Paste error:', error)
} finally {
isHandling.current = false
}
}, [handleImagePaste, handleTextPaste])

/**
* Get clipboard items using Clipboard API.
*/
async function getClipboardItems(): Promise<ClipboardItems | null> {
try {
return await navigator.clipboard.read()
} catch {
return null
}
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/hooks/useKeyboardShortcuts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useCallback, useRef } from 'react'
import { useEffect, useCallback, useRef, useMemo } from 'react'
import type { LGraph, LGraphCanvas } from 'litegraph.js'
import { NODE_MODE } from '../nodes/base/BaseNode'

Expand Down Expand Up @@ -83,8 +83,8 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions) {
onRedo?.()
}, [graph, canvas, onRedo])

// Define shortcuts
const shortcuts: ShortcutAction[] = [
// Define shortcuts - memoized to prevent useEffect re-registration
const shortcuts: ShortcutAction[] = useMemo(() => [
{
key: 's',
ctrl: true,
Expand Down Expand Up @@ -244,7 +244,7 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions) {
action: () => onTemplates?.(),
description: 'Prompt templates',
},
]
], [canvas, graph, handleRedo, handleUndo, onLoad, onNew, onSave, onTemplates])

// Handle keydown events
useEffect(() => {
Expand Down