Реалізація Drag-and-Drop файлів із системи у десктоп-застосунок

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Drag-and-Drop файлів із системи у десктоп-застосунок
Середня
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація Drag-and-Drop файлів із системи в десктоп-приложение

Перетаскування файлів з файлового менеджера в вікно приложения — базовий сценарій для більшості продуктивних інструментів. У Electron це реалізується через стандартні drag-and-drop события браузера (renderer) плюс опціональну нативну обробку (main). У Tauri — через tauri-plugin-drag-drop.

Базовий обробник в Renderer

HTML5 Drag-and-Drop API працює в Electron renderer як в звичайному браузері. Головна відмінність: при перетаскуванні файлів з ОС, e.dataTransfer.files містить File-об'єкти з нативними шляхами до файлів.

// src/hooks/useFileDrop.ts
import { useRef, useState, useCallback } from 'react'

export interface DroppedFile {
  name: string
  path: string
  size: number
  type: string
  lastModified: number
}

interface UseFileDropOptions {
  accept?: string[]
  maxFiles?: number
  maxSizeBytes?: number
  onDrop: (files: DroppedFile[]) => void
  onError?: (error: string) => void
}

export function useFileDrop(options: UseFileDropOptions) {
  const [isDragging, setIsDragging] = useState(false)
  const dragCounter = useRef(0)

  const validateFile = useCallback((file: File): string | null => {
    if (options.accept && options.accept.length > 0) {
      const ext = '.' + file.name.split('.').pop()?.toLowerCase()
      if (!options.accept.includes(ext)) {
        return `Формат ${ext} не підтримується`
      }
    }
    if (options.maxSizeBytes && file.size > options.maxSizeBytes) {
      const mb = (options.maxSizeBytes / 1024 / 1024).toFixed(1)
      return `Файл перевищує ${mb} МБ`
    }
    return null
  }, [options.accept, options.maxSizeBytes])

  const handleDragEnter = useCallback((e: DragEvent) => {
    e.preventDefault()
    dragCounter.current++
    setIsDragging(true)
  }, [])

  const handleDragLeave = useCallback((e: DragEvent) => {
    e.preventDefault()
    dragCounter.current--
    if (dragCounter.current === 0) {
      setIsDragging(false)
    }
  }, [])

  const handleDrop = useCallback((e: DragEvent) => {
    e.preventDefault()
    setIsDragging(false)
    dragCounter.current = 0

    const files = Array.from(e.dataTransfer?.files ?? [])

    if (options.maxFiles && files.length > options.maxFiles) {
      options.onError?.(`Максимум ${options.maxFiles} файлів`)
      return
    }

    const valid: DroppedFile[] = []
    for (const file of files) {
      const error = validateFile(file)
      if (error) {
        options.onError?.(error)
        continue
      }
      valid.push({
        name: file.name,
        path: (file as any).path ?? '',
        size: file.size,
        type: file.type,
        lastModified: file.lastModified,
      })
    }

    if (valid.length > 0) {
      options.onDrop(valid)
    }
  }, [validateFile, options])

  return {
    isDragging,
    dropProps: {
      onDragEnter: handleDragEnter,
      onDragLeave: handleDragLeave,
      onDragOver: (e: DragEvent) => {
        e.preventDefault()
        e.dataTransfer.dropEffect = 'copy'
      },
      onDrop: handleDrop,
    },
  }
}

Компонент DropZone

// src/components/DropZone.tsx
interface DropZoneProps {
  accept?: string[]
  maxFiles?: number
  maxSizeMb?: number
  onFilesDropped: (files: DroppedFile[]) => void
}

export function DropZone({ accept, maxFiles, maxSizeMb = 100, onFilesDropped }: DropZoneProps) {
  const { isDragging, dropProps } = useFileDrop({
    accept,
    maxFiles,
    maxSizeBytes: maxSizeMb * 1024 * 1024,
    onDrop: onFilesDropped,
  })

  return (
    <div
      {...dropProps}
      className={`relative p-8 border-2 border-dashed rounded-lg transition-all
        ${isDragging ? 'border-blue-500 bg-blue-50' : 'border-gray-300'}`}
    >
      {isDragging && (
        <div className="absolute inset-0 flex items-center justify-center bg-blue-500/10">
          <p className="text-blue-700 font-medium">Відпустіть файли тут</p>
        </div>
      )}
    </div>
  )
}

Обробка файлів через IPC

// preload/index.ts
contextBridge.exposeInMainWorld('fileSystem', {
  readFile: (filePath: string) =>
    ipcRenderer.invoke('fs:read-file', filePath),
  copyToWorkspace: (sourcePath: string, destName: string) =>
    ipcRenderer.invoke('fs:copy-to-workspace', sourcePath, destName),
})
// main/ipc-handlers/file-system.ts
const workspacePath = path.join(app.getPath('userData'), 'workspace')

ipcMain.handle('fs:read-file', async (_event, filePath: string) => {
  try {
    const resolved = path.resolve(filePath)
    const buffer = await fs.readFile(resolved)
    return { ok: true, data: buffer.toString('base64'), size: buffer.length }
  } catch (e: any) {
    return { ok: false, error: e.message }
  }
})

ipcMain.handle('fs:copy-to-workspace', async (_event, sourcePath: string, destName: string) => {
  try {
    await fs.mkdir(workspacePath, { recursive: true })
    const safeName = path.basename(destName).replace(/[^a-zA-Z0-9._-]/g, '_')
    const destPath = path.join(workspacePath, safeName)
    await fs.copyFile(sourcePath, destPath)
    return { ok: true, destPath }
  } catch (e: any) {
    return { ok: false, error: e.message }
  }
})

Перетаскування директорій

Коли користувач перетягує папку, e.dataTransfer.files містить її як один File. Використовуйте webkitGetAsEntry() для доступу до вмісту:

// src/utils/directory-reader.ts
export async function readDroppedItems(dataTransfer: DataTransfer): Promise<FileEntry[]> {
  const entries: FileSystemEntry[] = []

  for (let i = 0; i < dataTransfer.items.length; i++) {
    const entry = dataTransfer.items[i].webkitGetAsEntry()
    if (entry) entries.push(entry)
  }

  const files: FileEntry[] = []
  await Promise.all(entries.map(entry => traverseEntry(entry, '', files)))
  return files
}

async function traverseEntry(
  entry: FileSystemEntry,
  relativePath: string,
  result: FileEntry[]
): Promise<void> {
  if (entry.isFile) {
    const file = await getFile(entry as FileSystemFileEntry)
    result.push({
      path: (file as any).path ?? '',
      relativePath: relativePath + file.name,
      file,
    })
  } else if (entry.isDirectory) {
    const reader = (entry as FileSystemDirectoryEntry).createReader()
    const entries = await readAllEntries(reader)
    await Promise.all(
      entries.map(e => traverseEntry(e, relativePath + entry.name + '/', result))
    )
  }
}

Базова DropZone з валідацією: 4–6 годин. З підтримкою папок, прогресом, IPC в main, безпечною роботою з шляхами та тестами: 2–3 робочих дні.