bookwiz.io / lib / hooks / useFileOperations.ts
useFileOperations.ts
Raw
import { useState, useCallback } from 'react'

export interface UseFileOperationsReturn {
  // File inclusion states
  includedFiles: string[]
  includedFileNames: Map<string, string>
  isDragOver: boolean

  // File inclusion functions
  addIncludedFiles: (files: string[]) => void
  removeIncludedFile: (fileIdentifier: string) => void
  setIncludedFileNames: React.Dispatch<React.SetStateAction<Map<string, string>>>
  getDisplayName: (fileIdentifier: string) => string

  // Drag and drop handlers
  handleDragOver: (e: React.DragEvent) => void
  handleDragEnter: (e: React.DragEvent) => void
  handleDragLeave: (e: React.DragEvent) => void
  handleDrop: (e: React.DragEvent) => void
}

export function useFileOperations(): UseFileOperationsReturn {
  // File inclusion states
  const [includedFiles, setIncludedFiles] = useState<string[]>([])
  const [includedFileNames, setIncludedFileNames] = useState<Map<string, string>>(new Map())
  const [isDragOver, setIsDragOver] = useState(false)

  // Helper function to get display name for included files
  const getDisplayName = useCallback((fileIdentifier: string): string => {
    // If we have a stored name for this identifier, use it
    const storedName = includedFileNames.get(fileIdentifier)
    if (storedName) return storedName
    
    // If it looks like a UUID, show "Loading..." while we fetch the name
    if (fileIdentifier.match(/^[a-f0-9-]{36}$/)) {
      return "Loading..."
    }
    
    // Otherwise it's probably a file name, return as-is
    return fileIdentifier
  }, [includedFileNames])

  // File inclusion functions
  const addIncludedFiles = useCallback((files: string[]) => {
    setIncludedFiles(prev => {
      const uniqueFiles = files.filter(file => !prev.includes(file))
      return [...prev, ...uniqueFiles]
    })
  }, [])

  const removeIncludedFile = useCallback((fileIdentifier: string) => {
    setIncludedFiles(prev => prev.filter(file => file !== fileIdentifier))
    setIncludedFileNames(prev => {
      const newMap = new Map(prev)
      newMap.delete(fileIdentifier)
      return newMap
    })
  }, [])

  // Drag and drop handlers
  const handleDragOver = useCallback((e: React.DragEvent) => {
    e.preventDefault()
    e.stopPropagation()
    if (!isDragOver) {
      setIsDragOver(true)
    }
  }, [isDragOver])

  const handleDragEnter = useCallback((e: React.DragEvent) => {
    e.preventDefault()
    e.stopPropagation()
    setIsDragOver(true)
  }, [])

  const handleDragLeave = useCallback((e: React.DragEvent) => {
    e.preventDefault()
    e.stopPropagation()
    // Only set isDragOver to false if we're leaving the drop zone entirely
    if (!e.currentTarget.contains(e.relatedTarget as Node)) {
      setIsDragOver(false)
    }
  }, [])

  const handleDrop = useCallback((e: React.DragEvent) => {
    e.preventDefault()
    setIsDragOver(false)
    
    const files = Array.from(e.dataTransfer.files)
    const items = Array.from(e.dataTransfer.items)
    console.log('Dropped files:', files)
    console.log('Dropped items:', items)
    
    // Handle files if available
    if (files.length > 0) {
      const newFiles = files.map(file => {
        // Use the full path if available (webkitRelativePath), otherwise use name
        const fileName = file.webkitRelativePath || file.name
        console.log('Processing file:', fileName, file)
        return fileName
      })
      
      console.log('New files to add:', newFiles)
      addIncludedFiles(newFiles)
    } else {
      // Handle items when files array is empty
      const newFiles: string[] = []
      
      // Check if we have JSON data (prioritize over text/plain)
      const hasJsonData = items.some(item => item.kind === 'string' && item.type === 'application/json')
      
      items.forEach(item => {
        console.log('Processing item:', item)
        if (item.kind === 'file') {
          const entry = item.webkitGetAsEntry()
          if (entry) {
            console.log('Entry:', entry.name, entry.isDirectory ? 'directory' : 'file')
            newFiles.push(entry.name)
          } else {
            // Fallback: try to get file directly
            const file = item.getAsFile()
            if (file) {
              console.log('Got file from item:', file.name)
              newFiles.push(file.name)
            }
          }
        } else if (item.kind === 'string' && (item.type === 'application/json' || !hasJsonData)) {
          // Handle JSON data from file explorer drag (async)
          item.getAsString((text) => {
            try {
              const fileData = JSON.parse(text)
              console.log('Got file data from JSON:', fileData)
              if (fileData.id && fileData.type === 'file') {
                // Use file ID when available (dragged from internal file explorer)
                setIncludedFiles(prev => {
                  if (!prev.includes(fileData.id)) {
                    // Store the name for this ID
                    setIncludedFileNames(prevNames => new Map(prevNames).set(fileData.id, fileData.name))
                    
                    const updated = [...prev, fileData.id]
                    console.log('Updated included files from JSON (ID):', updated)
                    return updated
                  }
                  return prev
                })
              } else if (fileData.name) {
                // Fallback to name if no ID
                setIncludedFiles(prev => {
                  if (!prev.includes(fileData.name)) {
                    const updated = [...prev, fileData.name]
                    console.log('Updated included files from JSON (name):', updated)
                    return updated
                  }
                  return prev
                })
              }
            } catch (e) {
              console.error('Failed to parse JSON drag data:', e)
            }
          })
        }
      })
      
      // Only process synchronous files here
      if (newFiles.length > 0) {
        addIncludedFiles(newFiles)
      }
    }
  }, [addIncludedFiles])



  return {
    // File inclusion states
    includedFiles,
    includedFileNames,
    isDragOver,

    // File inclusion functions
    addIncludedFiles,
    removeIncludedFile,
    setIncludedFileNames,
    getDisplayName,

    // Drag and drop handlers
    handleDragOver,
    handleDragEnter,
    handleDragLeave,
    handleDrop,
  }
}