Merge pull request #107 from DevloperAmanSingh/feature/progress-bar
Feature: Add progress indicator for large file imports
This commit is contained in:
@ -17,27 +17,28 @@ export function MediaPanel() {
|
|||||||
const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore();
|
const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [mediaFilter, setMediaFilter] = useState("all");
|
const [mediaFilter, setMediaFilter] = useState("all");
|
||||||
|
|
||||||
const processFiles = async (files: FileList | File[]) => {
|
const processFiles = async (files: FileList | File[]) => {
|
||||||
// If no files, do nothing
|
if (!files || files.length === 0) return;
|
||||||
if (!files?.length) return;
|
|
||||||
|
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
|
setProgress(0);
|
||||||
try {
|
try {
|
||||||
// Process files (extract metadata, generate thumbnails, etc.)
|
// Process files (extract metadata, generate thumbnails, etc.)
|
||||||
const items = await processMediaFiles(files);
|
const processedItems = await processMediaFiles(files, (p) =>
|
||||||
|
setProgress(p)
|
||||||
|
);
|
||||||
// Add each processed media item to the store
|
// Add each processed media item to the store
|
||||||
items.forEach((item) => {
|
processedItems.forEach((item) => addMediaItem(item));
|
||||||
addMediaItem(item);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Show error if processing fails
|
// Show error toast if processing fails
|
||||||
console.error("File processing failed:", error);
|
console.error("Error processing files:", error);
|
||||||
toast.error("Failed to process files");
|
toast.error("Failed to process files");
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
|
setProgress(0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -241,15 +242,12 @@ export function MediaPanel() {
|
|||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Upload className="h-4 w-4 animate-spin" />
|
<Upload className="h-4 w-4 animate-spin" />
|
||||||
<span className="hidden md:inline ml-2">Processing...</span>
|
<span className="hidden md:inline ml-2">{progress}%</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
<span
|
<span className="hidden sm:inline ml-2" aria-label="Add file">
|
||||||
className="hidden sm:inline ml-2"
|
|
||||||
aria-label="Add file"
|
|
||||||
>
|
|
||||||
Add
|
Add
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
|
@ -69,6 +69,7 @@ export function Timeline() {
|
|||||||
} = usePlaybackStore();
|
} = usePlaybackStore();
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
const [zoomLevel, setZoomLevel] = useState(1);
|
const [zoomLevel, setZoomLevel] = useState(1);
|
||||||
const dragCounterRef = useRef(0);
|
const dragCounterRef = useRef(0);
|
||||||
const timelineRef = useRef<HTMLDivElement>(null);
|
const timelineRef = useRef<HTMLDivElement>(null);
|
||||||
@ -334,8 +335,12 @@ export function Timeline() {
|
|||||||
} else if (e.dataTransfer.files?.length > 0) {
|
} else if (e.dataTransfer.files?.length > 0) {
|
||||||
// Handle file drops by creating new tracks
|
// Handle file drops by creating new tracks
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
|
setProgress(0);
|
||||||
try {
|
try {
|
||||||
const processedItems = await processMediaFiles(e.dataTransfer.files);
|
const processedItems = await processMediaFiles(
|
||||||
|
e.dataTransfer.files,
|
||||||
|
(p) => setProgress(p)
|
||||||
|
);
|
||||||
for (const processedItem of processedItems) {
|
for (const processedItem of processedItems) {
|
||||||
addMediaItem(processedItem);
|
addMediaItem(processedItem);
|
||||||
const currentMediaItems = useMediaStore.getState().mediaItems;
|
const currentMediaItems = useMediaStore.getState().mediaItems;
|
||||||
@ -363,6 +368,7 @@ export function Timeline() {
|
|||||||
toast.error("Failed to process dropped files");
|
toast.error("Failed to process dropped files");
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
|
setProgress(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -594,10 +600,11 @@ export function Timeline() {
|
|||||||
<div className="w-px h-6 bg-border mx-1" />
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
|
|
||||||
{/* Time Display */}
|
{/* Time Display */}
|
||||||
<div className="text-xs text-muted-foreground font-mono px-2"
|
<div
|
||||||
style={{ minWidth: '18ch', textAlign: 'center' }}
|
className="text-xs text-muted-foreground font-mono px-2"
|
||||||
>
|
style={{ minWidth: "18ch", textAlign: "center" }}
|
||||||
{currentTime.toFixed(1)}s / {duration.toFixed(1)}s
|
>
|
||||||
|
{currentTime.toFixed(1)}s / {duration.toFixed(1)}s
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Test Clip Button - for debugging */}
|
{/* Test Clip Button - for debugging */}
|
||||||
@ -946,14 +953,12 @@ export function Timeline() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isDragOver && (
|
{isDragOver && (
|
||||||
<div
|
<div className="absolute inset-0 z-20 flex items-center justify-center pointer-events-none backdrop-blur-lg">
|
||||||
className="absolute left-0 right-0 border-2 border-dashed border-accent flex items-center justify-center text-muted-foreground"
|
<div>
|
||||||
style={{
|
{isProcessing
|
||||||
top: `${tracks.length * 60}px`,
|
? `Processing ${progress}%`
|
||||||
height: "60px",
|
: "Drop media here to add to timeline"}
|
||||||
}}
|
</div>
|
||||||
>
|
|
||||||
<div>Drop media here to add a new track</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1630,18 +1635,18 @@ function TimelineTrackContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mediaItem.type === "audio") {
|
if (mediaItem.type === "audio") {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex items-center gap-2">
|
<div className="w-full h-full flex items-center gap-2">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<AudioWaveform
|
<AudioWaveform
|
||||||
audioUrl={mediaItem.url}
|
audioUrl={mediaItem.url}
|
||||||
height={24}
|
height={24}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback for videos without thumbnails
|
// Fallback for videos without thumbnails
|
||||||
return (
|
return (
|
||||||
|
@ -11,11 +11,15 @@ import {
|
|||||||
export interface ProcessedMediaItem extends Omit<MediaItem, "id"> {}
|
export interface ProcessedMediaItem extends Omit<MediaItem, "id"> {}
|
||||||
|
|
||||||
export async function processMediaFiles(
|
export async function processMediaFiles(
|
||||||
files: FileList | File[]
|
files: FileList | File[],
|
||||||
|
onProgress?: (progress: number) => void
|
||||||
): Promise<ProcessedMediaItem[]> {
|
): Promise<ProcessedMediaItem[]> {
|
||||||
const fileArray = Array.from(files);
|
const fileArray = Array.from(files);
|
||||||
const processedItems: ProcessedMediaItem[] = [];
|
const processedItems: ProcessedMediaItem[] = [];
|
||||||
|
|
||||||
|
const total = fileArray.length;
|
||||||
|
let completed = 0;
|
||||||
|
|
||||||
for (const file of fileArray) {
|
for (const file of fileArray) {
|
||||||
const fileType = getFileType(file);
|
const fileType = getFileType(file);
|
||||||
|
|
||||||
@ -57,6 +61,15 @@ export async function processMediaFiles(
|
|||||||
duration,
|
duration,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Yield back to the event loop to keep the UI responsive
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
completed += 1;
|
||||||
|
if (onProgress) {
|
||||||
|
const percent = Math.round((completed / total) * 100);
|
||||||
|
onProgress(percent);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing file:", file.name, error);
|
console.error("Error processing file:", file.name, error);
|
||||||
toast.error(`Failed to process ${file.name}`);
|
toast.error(`Failed to process ${file.name}`);
|
||||||
|
Reference in New Issue
Block a user