From 96868188e6c0681b41d2d5c48578c8655b6aac93 Mon Sep 17 00:00:00 2001 From: cbaucom Date: Thu, 11 Jun 2026 09:44:01 -0400 Subject: [PATCH 1/3] Add project-store import module for dropped media files. Resolve duplicate filenames with numeric suffixes and skip unsupported types before writing. Co-authored-by: Cursor --- specs/issue-16-drag-drop-import.md | 28 ++++++++ src/project-store/import-media.test.ts | 97 ++++++++++++++++++++++++++ src/project-store/import-media.ts | 77 ++++++++++++++++++++ src/project-store/index.ts | 2 + src/project-store/media-loader.ts | 47 +++++++------ 5 files changed, 230 insertions(+), 21 deletions(-) create mode 100644 specs/issue-16-drag-drop-import.md create mode 100644 src/project-store/import-media.test.ts create mode 100644 src/project-store/import-media.ts diff --git a/specs/issue-16-drag-drop-import.md b/specs/issue-16-drag-drop-import.md new file mode 100644 index 0000000..25da63e --- /dev/null +++ b/specs/issue-16-drag-drop-import.md @@ -0,0 +1,28 @@ +# Issue 16 — Drag-and-drop import to folder + +## Goal + +Drop media files onto the app window → copy into the open project folder → append to the storyboard grid. + +## Pure module (`src/project-store/import-media.ts`) + +- `resolveUniqueFilename(existingFilenames, desiredFilename)` — suffix `-1`, `-2`, … before extension when name collides. +- `importDroppedMediaFiles(dirHandle, files, existingFilenames)` — validate type first (no writes for unsupported), write blob, return `{ imported: MediaSlide[], skipped: { filename, reason }[] }`. +- Reuse `createMediaSlideFromFile` extracted from `media-loader.ts`. + +## Editor shell (thin) + +- `useProject.importDroppedFiles(files)` — list folder filenames, call import module, append slides, set dismissible notice for skipped types. +- `DropImportLayer` — `dragover`/`drop` on window when `folderOpen`; ignores non-file drops. + +## Tests + +In-memory dir fake (blob writable). No live FS/network. Vertical TDD on acceptance criteria. + +## Acceptance mapping + +| Criterion | Verification | +| --- | --- | +| Files in folder + end of grid | import test + playwright smoke | +| Duplicate suffixed, both playable | import test | +| Unsupported skipped + notice, no partial writes | import test + UI notice | diff --git a/src/project-store/import-media.test.ts b/src/project-store/import-media.test.ts new file mode 100644 index 0000000..b5268d4 --- /dev/null +++ b/src/project-store/import-media.test.ts @@ -0,0 +1,97 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { importDroppedMediaFiles, resolveUniqueFilename } from './import-media' + +class FakeWritable { + public written: Blob | null = null + async write(data: Blob) { this.written = data } + async close() {} +} + +class FakeFileHandle { + public writable = new FakeWritable() + async createWritable() { return this.writable } +} + +class FakeDirHandle { + public files = new Map() + + async getFileHandle(name: string, opts?: { create?: boolean }) { + if (!this.files.has(name) && opts?.create) { + this.files.set(name, new FakeFileHandle()) + } + const handle = this.files.get(name) + if (!handle) throw new Error(`File not found: ${name}`) + return handle + } +} + +beforeEach(() => { + vi.stubGlobal('URL', { + ...URL, + createObjectURL: vi.fn(() => 'blob:mock-url'), + revokeObjectURL: vi.fn(), + }) +}) + +afterEach(() => { + vi.restoreAllMocks() +}) + +describe('resolveUniqueFilename', () => { + it('returns the desired filename when it is unused', () => { + const existing = new Set(['other.jpg']) + expect(resolveUniqueFilename(existing, 'photo.jpg')).toBe('photo.jpg') + }) + + it('suffixes duplicate filenames before the extension', () => { + const existing = new Set(['photo.jpg']) + expect(resolveUniqueFilename(existing, 'photo.jpg')).toBe('photo-1.jpg') + }) + + it('increments the suffix until the filename is free', () => { + const existing = new Set(['photo.jpg', 'photo-1.jpg', 'photo-2.jpg']) + expect(resolveUniqueFilename(existing, 'photo.jpg')).toBe('photo-3.jpg') + }) +}) + +describe('importDroppedMediaFiles', () => { + it('copies supported media into the folder and returns slides for them', async () => { + const dir = new FakeDirHandle() + const file = new File(['image'], 'sunset.jpg', { type: 'image/jpeg' }) + + const result = await importDroppedMediaFiles(dir, [file], new Set()) + + expect(result.skipped).toEqual([]) + expect(result.imported).toHaveLength(1) + expect(result.imported[0]?.filename).toBe('sunset.jpg') + expect(dir.files.has('sunset.jpg')).toBe(true) + expect(dir.files.get('sunset.jpg')!.writable.written).toBeInstanceOf(Blob) + }) + + it('saves duplicate names under a suffixed filename', async () => { + const dir = new FakeDirHandle() + dir.files.set('photo.jpg', new FakeFileHandle()) + const file = new File(['image'], 'photo.jpg', { type: 'image/jpeg' }) + + const result = await importDroppedMediaFiles(dir, [file], new Set(['photo.jpg'])) + + expect(result.imported).toHaveLength(1) + expect(result.imported[0]?.filename).toBe('photo-1.jpg') + expect(dir.files.has('photo-1.jpg')).toBe(true) + expect(dir.files.has('photo.jpg')).toBe(true) + }) + + it('skips unsupported types without writing partial files', async () => { + const dir = new FakeDirHandle() + const supported = new File(['image'], 'good.jpg', { type: 'image/jpeg' }) + const unsupported = new File(['text'], 'notes.txt', { type: 'text/plain' }) + + const result = await importDroppedMediaFiles(dir, [unsupported, supported], new Set()) + + expect(result.imported).toHaveLength(1) + expect(result.imported[0]?.filename).toBe('good.jpg') + expect(result.skipped).toEqual([{ filename: 'notes.txt', reason: 'unsupported-type' }]) + expect(dir.files.has('good.jpg')).toBe(true) + expect(dir.files.has('notes.txt')).toBe(false) + }) +}) diff --git a/src/project-store/import-media.ts b/src/project-store/import-media.ts new file mode 100644 index 0000000..73391f8 --- /dev/null +++ b/src/project-store/import-media.ts @@ -0,0 +1,77 @@ +import { isSupportedMedia } from '../timeline-core' +import type { MediaSlide } from '../timeline-core/types' +import { createMediaSlideFromFile } from './media-loader' + +interface WritableHandle { + close(): Promise + write(data: Blob): Promise +} + +interface ImportFileHandle { + createWritable(): Promise +} + +export interface ImportDirHandle { + getFileHandle(name: string, options?: { create?: boolean }): Promise +} + +export type SkippedImport = { + filename: string + reason: 'unsupported-type' +} + +export type ImportDroppedMediaResult = { + imported: MediaSlide[] + skipped: SkippedImport[] +} + +export function resolveUniqueFilename( + existingFilenames: ReadonlySet, + desiredFilename: string, +): string { + if (!existingFilenames.has(desiredFilename)) return desiredFilename + + const dotIndex = desiredFilename.lastIndexOf('.') + const base = dotIndex > 0 ? desiredFilename.slice(0, dotIndex) : desiredFilename + const extension = dotIndex > 0 ? desiredFilename.slice(dotIndex) : '' + + let counter = 1 + while (true) { + const candidate = `${base}-${counter}${extension}` + if (!existingFilenames.has(candidate)) return candidate + counter += 1 + } +} + +export async function importDroppedMediaFiles( + dirHandle: ImportDirHandle, + files: File[], + existingFilenames: ReadonlySet, +): Promise { + const imported: MediaSlide[] = [] + const skipped: SkippedImport[] = [] + const reservedFilenames = new Set(existingFilenames) + + for (const file of files) { + if (!isSupportedMedia(file.name)) { + skipped.push({ filename: file.name, reason: 'unsupported-type' }) + continue + } + + const filename = resolveUniqueFilename(reservedFilenames, file.name) + reservedFilenames.add(filename) + + const fileHandle = await dirHandle.getFileHandle(filename, { create: true }) + const writable = await fileHandle.createWritable() + await writable.write(file) + await writable.close() + + const namedFile = new File([file], filename, { + lastModified: file.lastModified, + type: file.type, + }) + imported.push(await createMediaSlideFromFile(namedFile)) + } + + return { imported, skipped } +} diff --git a/src/project-store/index.ts b/src/project-store/index.ts index 7f7c5c1..9eeeefc 100644 --- a/src/project-store/index.ts +++ b/src/project-store/index.ts @@ -1,5 +1,7 @@ export { enumerateAudioTracks, revokeAudioBlobUrls } from './audio-loader' export type { AudioTrack } from './audio-loader' +export { importDroppedMediaFiles, resolveUniqueFilename } from './import-media' +export type { ImportDirHandle, ImportDroppedMediaResult, SkippedImport } from './import-media' export { createFreshProject, openProject, saveProject } from './project-store' export type { DirHandle, OpenResult } from './project-store' export { addRecentProject, listRecentProjects, removeRecentProject, requestHandlePermission } from './recent-projects' diff --git a/src/project-store/media-loader.ts b/src/project-store/media-loader.ts index 2edcb69..ae877d7 100644 --- a/src/project-store/media-loader.ts +++ b/src/project-store/media-loader.ts @@ -25,6 +25,25 @@ async function getVideoDurationFrames(file: File): Promise { } } +export async function createMediaSlideFromFile(file: File): Promise { + const filename = file.name + const type = getMediaType(filename) + const blobUrl = URL.createObjectURL(file) + const durationInFrames = + type === 'video' + ? await getVideoDurationFrames(file) + : IMAGE_DURATION_FRAMES + + return { + blobUrl, + durationInFrames, + excluded: false, + filename, + id: `${filename}-${file.lastModified}`, + type, + } +} + export async function enumerateFolder( dirHandle: FileSystemDirectoryHandle, ): Promise { @@ -37,36 +56,22 @@ export async function enumerateFolder( files.push(file) } - const sorted = sortByFilename(files.map((f) => f.name)) - const filesByName = new Map(files.map((f) => [f.name, f])) + const sorted = sortByFilename(files.map((file) => file.name)) + const filesByName = new Map(files.map((file) => [file.name, file])) const createdUrls: string[] = [] try { const slides: MediaSlide[] = [] for (const filename of sorted) { const file = filesByName.get(filename)! - const type = getMediaType(filename) - const blobUrl = URL.createObjectURL(file) - createdUrls.push(blobUrl) - - const durationInFrames = - type === 'video' - ? await getVideoDurationFrames(file) - : IMAGE_DURATION_FRAMES - - slides.push({ - id: `${filename}-${file.lastModified}`, - filename, - type, - blobUrl, - durationInFrames, - excluded: false, - }) + const slide = await createMediaSlideFromFile(file) + createdUrls.push(slide.blobUrl) + slides.push(slide) } return slides - } catch (e) { + } catch (error) { createdUrls.forEach((url) => URL.revokeObjectURL(url)) - throw e + throw error } } From 95eab2cdd523ab2245414cb2078dc3ce537d5116 Mon Sep 17 00:00:00 2001 From: cbaucom Date: Thu, 11 Jun 2026 09:44:06 -0400 Subject: [PATCH 2/3] Wire drag-and-drop import into the editor shell. Append imported slides to the storyboard and show a dismissible notice for skipped file types. Co-authored-by: Cursor --- src/editor-shell/App.tsx | 12 +++++++ src/editor-shell/DropImportLayer.tsx | 33 +++++++++++++++++++ src/editor-shell/useProject.ts | 48 ++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 src/editor-shell/DropImportLayer.tsx diff --git a/src/editor-shell/App.tsx b/src/editor-shell/App.tsx index 64e4d27..155fcd5 100644 --- a/src/editor-shell/App.tsx +++ b/src/editor-shell/App.tsx @@ -13,6 +13,7 @@ import type { GlobalSettings, SlideOverrides, ThemeName } from '../timeline-core import { applyTheme } from '../timeline-core' import { plan } from '../sequence-planner' import { AppHeader } from './AppHeader' +import { DropImportLayer } from './DropImportLayer' import { EditorLayout } from './EditorLayout' import { EditorSidebar } from './EditorSidebar' import { EmptyState } from './EmptyState' @@ -40,6 +41,7 @@ export function App() { error, corruptError, folderOpen, + importNotice, recentProjects, } = project @@ -110,6 +112,7 @@ export function App() { return (
+ )} + {importNotice ? ( +
+ {importNotice} + +
+ ) : null} + {loading && (
Loading media… diff --git a/src/editor-shell/DropImportLayer.tsx b/src/editor-shell/DropImportLayer.tsx new file mode 100644 index 0000000..81f7756 --- /dev/null +++ b/src/editor-shell/DropImportLayer.tsx @@ -0,0 +1,33 @@ +import { useEffect } from 'react' + +type Props = { + enabled: boolean + onDropFiles: (files: File[]) => void +} + +export function DropImportLayer({ enabled, onDropFiles }: Props) { + useEffect(() => { + if (!enabled) return + + function handleDragOver(event: DragEvent) { + if (!event.dataTransfer?.types.includes('Files')) return + event.preventDefault() + } + + function handleDrop(event: DragEvent) { + if (!event.dataTransfer?.types.includes('Files')) return + event.preventDefault() + const files = [...event.dataTransfer.files] + if (files.length > 0) onDropFiles(files) + } + + window.addEventListener('dragover', handleDragOver) + window.addEventListener('drop', handleDrop) + return () => { + window.removeEventListener('dragover', handleDragOver) + window.removeEventListener('drop', handleDrop) + } + }, [enabled, onDropFiles]) + + return null +} diff --git a/src/editor-shell/useProject.ts b/src/editor-shell/useProject.ts index b4477bd..133898b 100644 --- a/src/editor-shell/useProject.ts +++ b/src/editor-shell/useProject.ts @@ -6,6 +6,7 @@ import type { GlobalSettings, ThemeName } from '../timeline-core' import { addRecentProject, enumerateAudioTracks, + importDroppedMediaFiles, listRecentProjects, openProject, requestHandlePermission, @@ -23,6 +24,14 @@ import { FPS } from './PlayerPane' const AUTOSAVE_DELAY = 2000 +async function listFolderFilenames(handle: FileSystemDirectoryHandle): Promise> { + const names = new Set() + for await (const entry of handle.values()) { + if (entry.kind === 'file') names.add(entry.name) + } + return names +} + type Options = { onFolderLoaded?: () => void } @@ -38,6 +47,7 @@ export function useProject({ onFolderLoaded }: Options = {}) { const [error, setError] = useState(null) const [corruptError, setCorruptError] = useState(null) const [folderOpen, setFolderOpen] = useState(false) + const [importNotice, setImportNotice] = useState(null) const [recentProjects, setRecentProjects] = useState([]) const dirHandleRef = useRef(null) @@ -203,6 +213,41 @@ export function useProject({ onFolderLoaded }: Options = {}) { setCorruptError(null) }, []) + const dismissImportNotice = useCallback(() => { + setImportNotice(null) + }, []) + + const importDroppedFiles = useCallback(async (files: File[]) => { + const handle = dirHandleRef.current + if (!handle || files.length === 0) return + + try { + const existingFilenames = await listFolderFilenames(handle) + const { imported, skipped } = await importDroppedMediaFiles(handle, files, existingFilenames) + + if (imported.length > 0) { + pendingRevokeRef.current = latestSlidesRef.current + setSlides((previousSlides) => [ + ...previousSlides, + ...imported.map((slide) => ( + slide.type === 'image' + ? { ...slide, durationInFrames: Math.round(globalSettings.imageDurationSecs * FPS) } + : slide + )), + ]) + } + + if (skipped.length > 0) { + const names = skipped.map((entry) => entry.filename).join(', ') + setImportNotice(`Skipped unsupported file type${skipped.length > 1 ? 's' : ''}: ${names}`) + } else { + setImportNotice(null) + } + } catch (error) { + setError(error instanceof Error ? error.message : 'Failed to import files') + } + }, [globalSettings]) + return { audioTracks, globalSettings, @@ -215,7 +260,10 @@ export function useProject({ onFolderLoaded }: Options = {}) { loading, error, corruptError, + dismissImportNotice, folderOpen, + importDroppedFiles, + importNotice, recentProjects, pickFolder, refresh, From ae294502eb5bde98cbe6fbd99de0f9d3df49c54f Mon Sep 17 00:00:00 2001 From: cbaucom Date: Thu, 11 Jun 2026 09:54:11 -0400 Subject: [PATCH 3/3] Add playwright smoke script for drag-and-drop import. Verifies appended slides, duplicate suffixing, and unsupported-file notice. Co-authored-by: Cursor --- issue-16-drag-drop-import.png | Bin 0 -> 50969 bytes scripts/playwright-issue-16-smoke.mjs | 129 ++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 issue-16-drag-drop-import.png create mode 100644 scripts/playwright-issue-16-smoke.mjs diff --git a/issue-16-drag-drop-import.png b/issue-16-drag-drop-import.png new file mode 100644 index 0000000000000000000000000000000000000000..7d22bbde823123e686474c66dae6e9cf9336bdf7 GIT binary patch literal 50969 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 Ho8bQeDE{nX literal 0 HcmV?d00001 diff --git a/scripts/playwright-issue-16-smoke.mjs b/scripts/playwright-issue-16-smoke.mjs new file mode 100644 index 0000000..f1cec37 --- /dev/null +++ b/scripts/playwright-issue-16-smoke.mjs @@ -0,0 +1,129 @@ +import fs from 'node:fs' +import path from 'node:path' +import { spawnSync } from 'node:child_process' + +function run(args) { + const result = spawnSync('npx', ['playwright-cli', ...args], { + cwd: process.cwd(), + encoding: 'utf8', + maxBuffer: 20 * 1024 * 1024, + }) + if (result.stdout) process.stdout.write(result.stdout) + if (result.stderr) process.stderr.write(result.stderr) + if (result.error) console.error(result.error) + return result.status ?? 1 +} + +const demo = path.resolve('test-fixtures/demo') +const names = ['photo.jpg', 'photo2.jpg', 'theme.wav'] +const payload = Object.fromEntries( + names.map((name) => [name, fs.readFileSync(path.join(demo, name)).toString('base64')]), +) +payload['slideshow.json'] = Buffer.from(JSON.stringify({ + globalSettings: { + fitMode: 'cover', + imageDurationSecs: 3, + kenBurns: true, + transitionType: 'cut', + }, + schemaVersion: 1, + slides: [ + { + durationInFrames: 90, + excluded: false, + filename: 'photo.jpg', + id: 'photo.jpg-1781145972852', + type: 'image', + }, + { + durationInFrames: 90, + excluded: false, + filename: 'photo2.jpg', + id: 'photo2.jpg-1781145972852', + type: 'image', + }, + ], +}, null, 2)).toString('base64') + +const code = `async page => { + const payload = ${JSON.stringify(payload)}; + await page.goto('http://localhost:5173/'); + await page.evaluate((files) => { + function decodeBase64(base64) { + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index); + return bytes; + } + class MockFileHandle { + constructor(name, base64) { + this.kind = 'file'; + this.name = name; + this._base64 = base64; + } + async getFile() { + const bytes = decodeBase64(this._base64); + const type = this.name.endsWith('.jpg') ? 'image/jpeg' + : this.name.endsWith('.wav') ? 'audio/wav' + : 'application/json'; + return new File([bytes], this.name, { type, lastModified: 1781145972852 }); + } + async createWritable() { + const handle = this; + return { + write: async (data) => { + const buffer = data instanceof Blob ? await data.arrayBuffer() : data; + handle._base64 = btoa(String.fromCharCode(...new Uint8Array(buffer))); + }, + close: async () => {}, + }; + } + } + class MockDirHandle { + constructor(files) { + this.name = 'demo'; + this._files = files; + } + async getFileHandle(name, options) { + const handle = this._files.get(name); + if (handle) return handle; + if (options?.create) { + const created = new MockFileHandle(name, ''); + this._files.set(name, created); + return created; + } + throw new DOMException('NotFoundError'); + } + async *values() { + for (const handle of this._files.values()) yield handle; + } + } + const handles = new Map( + Object.entries(files).map(([name, base64]) => [name, new MockFileHandle(name, base64)]), + ); + window.showDirectoryPicker = async () => new MockDirHandle(handles); + }, payload); + await page.getByRole('button', { name: 'Open Folder' }).click(); + await page.getByText('photo.jpg').first().waitFor(); + await page.getByText('photo2.jpg').first().waitFor(); + + await page.evaluate(async () => { + const transfer = new DataTransfer(); + transfer.items.add(new File(['new image'], 'imported.jpg', { type: 'image/jpeg', lastModified: 1781145972852 })); + transfer.items.add(new File(['duplicate'], 'photo.jpg', { type: 'image/jpeg', lastModified: 1781145972852 })); + transfer.items.add(new File(['text'], 'notes.txt', { type: 'text/plain', lastModified: 1781145972852 })); + window.dispatchEvent(new DragEvent('drop', { bubbles: true, dataTransfer: transfer })); + }); + + await page.getByText('imported.jpg').first().waitFor(); + await page.getByText('photo-1.jpg').first().waitFor(); + await page.getByText('Skipped unsupported file type: notes.txt').waitFor(); + await page.screenshot({ path: 'issue-16-drag-drop-import.png', fullPage: true }); +}` + +let status = run(['open', 'http://localhost:5173/', '--browser=chrome']) +if (status !== 0) process.exit(status) + +status = run(['run-code', code]) +run(['close']) +process.exit(status)