Implement local track upload

This commit is contained in:
2026-01-14 00:28:19 +05:00
parent df9fe970ab
commit 5fd026c026
2 changed files with 129 additions and 13 deletions

View File

@ -4,11 +4,13 @@ import { Motion } from "motion-v";
import { ref } from "vue";
import { X } from "lucide-vue-next";
import Button from "../button/Button.vue";
import Progress from "../progress/Progress.vue";
interface FileUploadProps {
class?: HTMLAttributes["class"];
accept?: string;
modelValue?: File[];
ongoingUploads?: Map<string, { file: File; progress: number }>;
}
const props = defineProps<FileUploadProps>();
@ -49,12 +51,43 @@ function handleDrop(e: DragEvent) {
const droppedFiles = e.dataTransfer?.files ? Array.from(e.dataTransfer.files) : [];
if (droppedFiles.length) handleFileChange(droppedFiles);
}
function removeFile(event: MouseEvent, index: number) {
files.value.splice(index, 1);
event.stopPropagation();
emit("update:modelValue", files.value);
}
function getUploadProgress(file: File): number {
if (!props.ongoingUploads) return 0;
for (const [_, upload] of props.ongoingUploads) {
if (
upload.file.name === file.name &&
upload.file.size === file.size &&
upload.file.lastModified === file.lastModified
) {
return upload.progress;
}
}
return 0;
}
function isFileUploading(file: File): boolean {
if (!props.ongoingUploads) return false;
for (const [_, upload] of props.ongoingUploads) {
if (
upload.file.name === file.name &&
upload.file.size === file.size &&
upload.file.lastModified === file.lastModified
) {
return true;
}
}
return false;
}
watch(() => props.modelValue, (newVal) => {
if (newVal) {
files.value = newVal;
@ -68,7 +101,7 @@ watch(() => props.modelValue, (newVal) => {
@drop.prevent="handleDrop" @mouseover="handleEnter" @mouseleave="handleLeave">
<div class="group/file relative block w-full cursor-pointer overflow-hidden rounded-lg p-10 bg-muted space-y-4"
@click="handleClick">
<input ref="fileInputRef" type="file" class="hidden" :accept="accept" @change="onFileChange" />
<input ref="fileInputRef" type="file" class="hidden" :accept="accept" @change="onFileChange" multiple />
<!-- Grid pattern -->
<div
@ -76,33 +109,46 @@ watch(() => props.modelValue, (newVal) => {
<slot />
</div>
<div class="relative mx-auto mt-10 w-full max-w-xl space-y-4">
<div class="relative mx-auto w-full max-w-xl space-y-8">
<Motion v-for="(file, idx) in files" :key="`file-${idx}`" :initial="{ opacity: 0, scaleX: 0 }"
:animate="{ opacity: 1, scaleX: 1 }"
class="relative z-40 mx-auto flex w-full flex-col items-start justify-start overflow-hidden rounded-md p-4 shadow-sm md:h-24 text-muted-foreground bg-background">
class="relative z-40 mx-auto flex w-full flex-col items-start justify-start overflow-hidden rounded-md p-4 shadow-sm text-muted-foreground bg-background gap-2">
<div class="flex w-full items-center justify-between gap-4">
<Motion as="p" :initial="{ opacity: 0 }" :animate="{ opacity: 1 }"
class="max-w-xs truncate text-base">
{{ file.name }}
</Motion>
<Motion as="p" :initial="{ opacity: 0 }" :animate="{ opacity: 1 }"
class="shadow-input w-fit shrink-0 rounded-lg px-2 py-1 text-sm">
{{ (file.size / (1024 * 1024)).toFixed(2) }} MB
</Motion>
<div class="flex items-center gap-2">
<Motion as="p" :initial="{ opacity: 0 }" :animate="{ opacity: 1 }"
class="shadow-input w-fit shrink-0 rounded-lg px-2 py-1 text-sm">
{{ (file.size / (1024 * 1024)).toFixed(2) }} MB
</Motion>
<Motion as="div" :initial="{ opacity: 0 }" :animate="{ opacity: 1 }"
v-if="isFileUploading(file)">
<span class="text-xs px-2 py-1 rounded bg-blue-100 text-blue-800">
Uploading...
</span>
</Motion>
</div>
</div>
<div
class="mt-2 flex w-full flex-col items-start justify-between text-sm text-muted-foreground md:flex-row md:items-center">
class="flex w-full flex-col items-start justify-between text-sm text-muted-foreground md:flex-row md:items-center">
<Motion as="p" :initial="{ opacity: 0 }" :animate="{ opacity: 1 }"
class="rounded-md bg-muted px-1.5 py-1 text-sm">
{{ file.type || "unknown type" }}
</Motion>
<Motion as="div" :initial="{ opacity: 0 }" :animate="{ opacity: 1 }">
<Button variant="ghost" @click="(e: MouseEvent) => removeFile(e, idx)">
<Motion as="div" :initial="{ opacity: 0 }" :animate="{ opacity: 1 }"
v-if="!isFileUploading(file)">
<Button variant="ghost" @click="(e: MouseEvent) => removeFile(e, idx)"
:disabled="isFileUploading(file)">
<X />
</Button>
</Motion>
</div>
<div class="w-full" v-if="isFileUploading(file)">
<Progress :modelValue="getUploadProgress(file)" class="w-full" />
</div>
</Motion>
<template v-if="!files.length">