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 { 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>
</> </>

View File

@ -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,8 +600,9 @@ 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>
@ -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>

View File

@ -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}`);