Merge pull request #107 from DevloperAmanSingh/feature/progress-bar

Feature: Add progress indicator for large file imports
This commit is contained in:
iza
2025-06-25 19:29:46 +03:00
committed by GitHub
3 changed files with 55 additions and 39 deletions

View File

@ -17,27 +17,28 @@ export function MediaPanel() {
const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore();
const fileInputRef = useRef<HTMLInputElement>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [progress, setProgress] = useState(0);
const [searchQuery, setSearchQuery] = useState("");
const [mediaFilter, setMediaFilter] = useState("all");
const processFiles = async (files: FileList | File[]) => {
// If no files, do nothing
if (!files?.length) return;
if (!files || files.length === 0) return;
setIsProcessing(true);
setProgress(0);
try {
// 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
items.forEach((item) => {
addMediaItem(item);
});
processedItems.forEach((item) => addMediaItem(item));
} catch (error) {
// Show error if processing fails
console.error("File processing failed:", error);
// Show error toast if processing fails
console.error("Error processing files:", error);
toast.error("Failed to process files");
} finally {
setIsProcessing(false);
setProgress(0);
}
};
@ -241,15 +242,12 @@ export function MediaPanel() {
{isProcessing ? (
<>
<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" />
<span
className="hidden sm:inline ml-2"
aria-label="Add file"
>
<span className="hidden sm:inline ml-2" aria-label="Add file">
Add
</span>
</>

View File

@ -69,6 +69,7 @@ export function Timeline() {
} = usePlaybackStore();
const [isDragOver, setIsDragOver] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [progress, setProgress] = useState(0);
const [zoomLevel, setZoomLevel] = useState(1);
const dragCounterRef = useRef(0);
const timelineRef = useRef<HTMLDivElement>(null);
@ -334,8 +335,12 @@ export function Timeline() {
} else if (e.dataTransfer.files?.length > 0) {
// Handle file drops by creating new tracks
setIsProcessing(true);
setProgress(0);
try {
const processedItems = await processMediaFiles(e.dataTransfer.files);
const processedItems = await processMediaFiles(
e.dataTransfer.files,
(p) => setProgress(p)
);
for (const processedItem of processedItems) {
addMediaItem(processedItem);
const currentMediaItems = useMediaStore.getState().mediaItems;
@ -363,6 +368,7 @@ export function Timeline() {
toast.error("Failed to process dropped files");
} finally {
setIsProcessing(false);
setProgress(0);
}
}
};
@ -594,10 +600,11 @@ export function Timeline() {
<div className="w-px h-6 bg-border mx-1" />
{/* Time Display */}
<div className="text-xs text-muted-foreground font-mono px-2"
style={{ minWidth: '18ch', textAlign: 'center' }}
>
{currentTime.toFixed(1)}s / {duration.toFixed(1)}s
<div
className="text-xs text-muted-foreground font-mono px-2"
style={{ minWidth: "18ch", textAlign: "center" }}
>
{currentTime.toFixed(1)}s / {duration.toFixed(1)}s
</div>
{/* Test Clip Button - for debugging */}
@ -946,14 +953,12 @@ export function Timeline() {
</>
)}
{isDragOver && (
<div
className="absolute left-0 right-0 border-2 border-dashed border-accent flex items-center justify-center text-muted-foreground"
style={{
top: `${tracks.length * 60}px`,
height: "60px",
}}
>
<div>Drop media here to add a new track</div>
<div className="absolute inset-0 z-20 flex items-center justify-center pointer-events-none backdrop-blur-lg">
<div>
{isProcessing
? `Processing ${progress}%`
: "Drop media here to add to timeline"}
</div>
</div>
)}
</div>
@ -1630,18 +1635,18 @@ function TimelineTrackContent({
}
if (mediaItem.type === "audio") {
return (
<div className="w-full h-full flex items-center gap-2">
<div className="flex-1 min-w-0">
<AudioWaveform
audioUrl={mediaItem.url}
height={24}
className="w-full"
/>
return (
<div className="w-full h-full flex items-center gap-2">
<div className="flex-1 min-w-0">
<AudioWaveform
audioUrl={mediaItem.url}
height={24}
className="w-full"
/>
</div>
</div>
</div>
);
}
);
}
// Fallback for videos without thumbnails
return (

View File

@ -11,11 +11,15 @@ import {
export interface ProcessedMediaItem extends Omit<MediaItem, "id"> {}
export async function processMediaFiles(
files: FileList | File[]
files: FileList | File[],
onProgress?: (progress: number) => void
): Promise<ProcessedMediaItem[]> {
const fileArray = Array.from(files);
const processedItems: ProcessedMediaItem[] = [];
const total = fileArray.length;
let completed = 0;
for (const file of fileArray) {
const fileType = getFileType(file);
@ -57,6 +61,15 @@ export async function processMediaFiles(
duration,
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) {
console.error("Error processing file:", file.name, error);
toast.error(`Failed to process ${file.name}`);