Skip to content
Open
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
8 changes: 3 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

144 changes: 86 additions & 58 deletions src/components/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import { MAX_FILE_SIZE, WARNING_FILE_SIZE } from "@/lib/types";

interface Props {
onFileSelect: (file: File | null) => void;
onMultipleFileSelect?: (files: FileList | null) => void;
currentFile: File | null;
fileError: string;
duration: number;
}

export default function FileUpload({
onFileSelect,
onMultipleFileSelect,
currentFile,
fileError,
duration,
Expand Down Expand Up @@ -114,44 +116,60 @@ export default function FileUpload({

// ── Drop zone (inner) handler ─────────────────────────
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragging(false);
const file = e.dataTransfer.files?.[0];
if (file) handleFile(file);
};
e.preventDefault();
setDragging(false);

const files = e.dataTransfer.files;

if (!files?.length) return;

if (files.length === 1 && files[0]) {
handleFile(files[0]);
}

onMultipleFileSelect?.(files);
};

// ── File info (shown after upload) ───────────────────
const FileInfo = () => (
<div className="px-4 py-3 bg-[var(--surface)] border border-[var(--border)] rounded-[var(--radius)] shadow-[var(--shadow)]">
<div className="flex flex-col lg:flex-row lg:items-center gap-3">
<div className="flex items-start gap-3 flex-1 min-w-0">
<div className="hidden lg:flex items-center justify-center w-9 h-9 rounded-lg bg-[var(--surface)] border border-[var(--border)] shrink-0">
<Film size={16} className="text-film-600" />
const FileInfo = () => (
<div className="px-4 py-3 bg-[var(--surface)] border border-[var(--border)] rounded-[var(--radius)] shadow-[var(--shadow)]">
<div className="flex flex-col lg:flex-row lg:items-center gap-3">
<div className="flex items-start gap-3 flex-1 min-w-0">
<div className="hidden lg:flex items-center justify-center w-9 h-9 rounded-lg bg-[var(--surface)] border border-[var(--border)] shrink-0">
<Film size={16} className="text-film-600" />
</div>

<Film
size={18}
className="lg:hidden text-film-600 shrink-0 mt-0.5"
/>

<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 mb-0.5">
<p className="text-sm font-semibold text-[var(--text)] truncate max-w-[320px] xl:max-w-[420px]">
{currentFile?.name}
</p>

{currentFile && (
<span className="px-2 py-0.5 bg-[var(--accent-muted)] text-[var(--text)] font-bold tracking-wider rounded text-[10px] uppercase shrink-0">
{currentFile.name.includes(".")
? currentFile.name.split(".").pop()
: "VIDEO"}
</span>
)}
</div>
<Film size={18} className="lg:hidden text-film-600 shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 mb-0.5">
<p className="text-sm font-semibold text-[var(--text)] truncate max-w-[320px] xl:max-w-[420px]">
{currentFile?.name}
</p>
{currentFile && (
<span className="px-2 py-0.5 bg-[var(--accent-muted)] text-[var(--text)] font-bold tracking-wider rounded text-[10px] uppercase shrink-0">
{currentFile.name.includes(".")
? currentFile.name.split(".").pop()
: "VIDEO"}
</span>
)}
</div>
<div className="text-xs text-[var(--muted)] mt-1 space-y-0.5">
<p>{formatBytes(currentFile?.size ?? 0)}</p>
<p>
{duration > 0
? `Duration: ${formatDuration(duration)}`
: "Loading duration..."}
</p>
</div>

<div className="text-xs text-[var(--muted)] mt-1 space-y-0.5">
<p>{formatBytes(currentFile?.size ?? 0)}</p>

<p>
{duration > 0
? `Duration: ${formatDuration(duration)}`
: "Loading duration..."}
</p>
</div>
</div>
</div>

<button
type="button"
Expand All @@ -163,28 +181,28 @@ export default function FileUpload({
</button>
</div>

<p className="text-xs text-[var(--muted)] mt-3 break-words">
Supports: MP4, MOV, AVI, MKV, WebM, and most video formats
</p>

{fileError && (
<p className="text-xs text-[var(--error)] mt-2 font-medium">{fileError}</p>
)}

<input
ref={inputRef}
type="file"
accept="video/*"
className="hidden"
onChange={(e) => {
const f = e.target.files?.[0];
if (f) handleFile(f);
}}
/>
</div>
);

// ── Drop zone (inner) ─────────────────────────────────
<p className="text-xs text-[var(--muted)] mt-3 break-words">
Supports: MP4, MOV, AVI, MKV, WebM, and most video formats
</p>

{fileError && (
<p className="text-xs text-[var(--error)] mt-2 font-medium">
{fileError}
</p>
)}

<input
ref={inputRef}
type="file"
multiple
accept="video/*"
className="hidden"
onChange={(e) => {
onMultipleFileSelect?.(e.target.files)
}}
/>
</div>
);
const DropZone = () => (
<div
id="upload-zone"
Expand Down Expand Up @@ -246,14 +264,24 @@ export default function FileUpload({
)}
</div>

<p className="text-xs text-[var(--muted)] text-center">
<p className="text-xs text-gray-500 text-center">
Supports: MP4, MOV, AVI, MKV, WebM, and most video formats up to 2GB
</p>

{fileError && (
<p className="text-sm text-[var(--error)] text-center">{fileError}</p>
)}
</div>
<input
ref={inputRef}
type="file"
multiple
accept="video/*"
className="hidden"
onChange={(e) => {
onMultipleFileSelect?.(e.target.files)
}}
/>
</div>
);

return (
Expand Down Expand Up @@ -315,4 +343,4 @@ export default function FileUpload({
</div>
</>
);
}
}
1 change: 1 addition & 0 deletions src/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function ThemeToggle() {
);
}

const isDark = theme === "dark";
return (
<button
type="button"
Expand Down
109 changes: 107 additions & 2 deletions src/components/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ function KeyboardShortcutsPanel() {

export default function VideoEditor() {
const {
file, duration, recipe, status, progress,
file, setFile, clips, setClips, handleMultipleFilesSelect, duration, recipe, status, progress,
result, error, exportStartedAt, updateRecipe,
handleFileSelect, fileError, handleExport, cancelExport, reset, resetSettings,
videoRef,
Expand Down Expand Up @@ -411,7 +411,111 @@ return () => {

<div className="space-y-4 min-w-0">
<div className="bg-[var(--surface)] rounded-xl p-3 border border-[var(--border)] animate-fade-in">
<FileUpload onFileSelect={handleFileSelect} currentFile={file} fileError={fileError} duration={duration} />
<FileUpload onFileSelect={handleFileSelect} onMultipleFileSelect={handleMultipleFilesSelect} currentFile={file} fileError={fileError} duration={duration} />

{clips.length > 0 && (
<div className="mt-4 space-y-2">
<h3 className="text-sm font-semibold text-[var(--text)]">
Selected Clips
</h3>


{clips.map((clip, index) => (
<div
key={index}
className={cn(
"rounded-lg border p-3 flex items-center justify-between transition-colors",
file === clip
? "border-film-500 bg-film-50"
: "border-[var(--border)] bg-[var(--bg)]"
)}
>
{/* Clickable preview area */}
<button
type="button"
onClick={() => handleFileSelect(clip)}
className="flex-1 cursor-pointer truncate text-left"
>
<span className="text-sm truncate block">
{index + 1}. {clip.name}
</span>

{file === clip && (
<p className="text-xs text-film-500 mt-1">
Previewing
</p>
)}
</button>

{/* Action buttons */}
<div className="flex items-center gap-2 ml-3 shrink-0">
{/* Move Up */}
<button
type="button"
disabled={index === 0}
onClick={(e) => {
e.stopPropagation()

const updated = [...clips]

const temp = updated[index]!
updated[index] = updated[index - 1]!
updated[index - 1] = temp
setClips(updated)
}}
className="px-2 py-1 rounded border border-[var(--border)] text-xs disabled:opacity-40 hover:border-film-400 transition-colors"
>
</button>

{/* Move Down */}
<button
type="button"
disabled={index === clips.length - 1}
onClick={(e) => {
e.stopPropagation()

const updated = [...clips]
const temp = updated[index]!
updated[index] = updated[index + 1]!
updated[index + 1] = temp

setClips(updated)
}}
className="px-2 py-1 rounded border border-[var(--border)] text-xs disabled:opacity-40 hover:border-film-400 transition-colors"
>
</button>

{/* Remove */}
<button
type="button"
onClick={(e) => {
e.stopPropagation()

const updated = clips.filter((_, i) => i !== index)

setClips(updated)

// If removed clip is current preview
if (clip === file) {
if (updated.length > 0) {
handleFileSelect(updated[0]!)
} else {
reset()
}
}
}}
className="px-2 py-1 rounded border border-red-400 text-xs text-red-500 hover:bg-red-50 transition-colors"
>
Remove
</button>
</div>
</div>
))}

</div>
)}

{!file && (
<div className="text-center text-[var(--muted)] py-6">
Expand All @@ -423,6 +527,7 @@ return () => {
{file && (
<div className="mt-4 animate-fade-in">
<VideoPreview
key={`${file?.name}-${file?.size}`}
file={file}
recipe={recipe}
videoRef={videoRef}
Expand Down
Loading