264 lines
10 KiB
Vue
264 lines
10 KiB
Vue
<template>
|
|
<Frame margin="none" class="px-3 py-4 flex items-center gap-2 cursor-pointer" @click="openDialog">
|
|
<div>
|
|
<Disc3 :size="40" v-if="hasLoaded" />
|
|
<AudioWaveform :size="40" v-else-if="hasProgress" />
|
|
<FileQuestionMark :size="40" v-else-if="hasError" />
|
|
</div>
|
|
<div class="w-full">
|
|
<div class="flex flex-row items-center gap-1">
|
|
<p class="font-medium">
|
|
{{ title }}
|
|
</p>
|
|
</div>
|
|
<div class="flex flex-row" v-if="hasLoaded">
|
|
<p class="text-sm text-muted-foreground">
|
|
{{ size }}
|
|
</p>
|
|
<Dot />
|
|
<p class="text-sm text-muted-foreground">
|
|
{{ format }}
|
|
</p>
|
|
<Dot />
|
|
<p class="text-sm text-muted-foreground">
|
|
{{ type }}
|
|
</p>
|
|
</div>
|
|
<div class="flex flex-row items-center gap-2" v-if="hasProgress">
|
|
<p class="text-sm text-muted-foreground">
|
|
{{ progress }}%
|
|
</p>
|
|
<Progress :modelValue="progress" />
|
|
</div>
|
|
<div class="flex flex-row" v-if="hasError">
|
|
<p class="text-sm text-destructive-foreground">
|
|
{{ error }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger as-child>
|
|
<Button variant="ghost">
|
|
<EllipsisVertical :size="40" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent class="w-56" align="start">
|
|
<DropdownMenuItem @click="openDialog">
|
|
<Eye class="mr-2 h-4 w-4" />
|
|
View Details
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
<Download class="mr-2 h-4 w-4" />
|
|
Download
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem class="text-destructive">
|
|
<Trash2 class="mr-2 h-4 w-4" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
<Dialog v-model:show="isDialogOpen">
|
|
<DialogContent class="max-w-3xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Track Upload Details</DialogTitle>
|
|
<DialogDescription>
|
|
Detailed information about this track upload
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div class="grid gap-4 py-4">
|
|
<div class="space-y-4">
|
|
<h3 class="text-lg font-semibold">Track Information</h3>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-2">
|
|
<Label>Title</Label>
|
|
<p class="text-sm">{{ title }}</p>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label>Format</Label>
|
|
<p class="text-sm">{{ format }}</p>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label>Size</Label>
|
|
<p class="text-sm">{{ size }}</p>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label>Type</Label>
|
|
<p class="text-sm">{{ type }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="trackProgressData" class="space-y-4">
|
|
<h3 class="text-lg font-semibold">Upload Details</h3>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-2">
|
|
<Label>Progress</Label>
|
|
<div class="flex items-center gap-2">
|
|
<p class="text-sm">{{ progress }}%</p>
|
|
<Progress :modelValue="progress" class="w-24" />
|
|
</div>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label>Timestamp</Label>
|
|
<p class="text-sm">{{ formatTimestamp(trackProgressData.timestamp) }}</p>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label>User ID</Label>
|
|
<p class="text-sm">{{ trackProgressData.userId }}</p>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label>Track Source ID</Label>
|
|
<p class="text-sm">{{ trackProgressData.trackSourceId }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="trackProgressData.title || trackProgressData.format" class="space-y-2">
|
|
<Label>Additional Information</Label>
|
|
<div class="bg-muted rounded-md p-3 space-y-1">
|
|
<div v-if="trackProgressData.title" class="flex justify-between">
|
|
<span class="text-sm font-medium">Original Title:</span>
|
|
<span class="text-sm">{{ trackProgressData.title }}</span>
|
|
</div>
|
|
<div v-if="trackProgressData.format" class="flex justify-between">
|
|
<span class="text-sm font-medium">Source Format:</span>
|
|
<span class="text-sm">{{ trackProgressData.format }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<h3 class="text-lg font-semibold">Status</h3>
|
|
<div class="flex items-center gap-3 p-4 rounded-lg border">
|
|
<div :class="`h-3 w-3 rounded-full ${getStatusColor()}`" />
|
|
<div>
|
|
<p class="font-medium">{{ getStatusText() }}</p>
|
|
<p v-if="progress" class="text-sm text-muted-foreground">
|
|
Upload progress: {{ progress }}%
|
|
</p>
|
|
<p v-if="trackProgressData?.timestamp" class="text-sm text-muted-foreground">
|
|
Last updated: {{ formatTimestamp(trackProgressData.timestamp) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="hasError" class="space-y-4">
|
|
<h3 class="text-lg font-semibold text-destructive">Error Details</h3>
|
|
<div class="bg-destructive/10 rounded-md p-4">
|
|
<p class="text-sm text-destructive">{{ error }}</p>
|
|
<Button v-if="hasError" variant="outline" size="sm" class="mt-2" @click="handleRetry">
|
|
<RefreshCw class="mr-2 h-4 w-4" />
|
|
Retry Upload
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" @click="isDialogOpen = false">Close</Button>
|
|
<Button variant="secondary" @click="downloadTrack">
|
|
<Download class="mr-2 h-4 w-4" />
|
|
Download Track
|
|
</Button>
|
|
<Button @click="openInPlayer">
|
|
<Play class="mr-2 h-4 w-4" />
|
|
Open in Player
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</Frame>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {
|
|
AudioWaveform,
|
|
Disc3,
|
|
Dot,
|
|
Download,
|
|
EllipsisVertical,
|
|
Eye,
|
|
FileQuestionMark,
|
|
Play,
|
|
RefreshCw,
|
|
Trash2
|
|
} from 'lucide-vue-next';
|
|
import { ref } from 'vue';
|
|
|
|
interface Props {
|
|
title: string
|
|
size?: string
|
|
format?: string
|
|
type?: string
|
|
progress?: number
|
|
error?: string
|
|
trackProgressData?: {
|
|
playlistId?: number
|
|
trackSourceId: number
|
|
userId: number
|
|
timestamp: number
|
|
title: string
|
|
format: string
|
|
}
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
progress: 0
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
retry: []
|
|
download: []
|
|
play: []
|
|
}>();
|
|
|
|
const isDialogOpen = ref(false);
|
|
|
|
const hasLoaded = props.size && props.format && props.type;
|
|
const hasProgress = props.progress !== undefined && props.progress > 0;
|
|
const hasError = props.error;
|
|
|
|
const openDialog = () => {
|
|
isDialogOpen.value = true;
|
|
};
|
|
|
|
const getStatusColor = () => {
|
|
if (hasError) return 'bg-destructive';
|
|
if (hasLoaded && props.progress === 100) return 'bg-green-500';
|
|
if (hasProgress) return 'bg-blue-500';
|
|
return 'bg-gray-500';
|
|
};
|
|
|
|
const getStatusText = () => {
|
|
if (hasError) return 'Error';
|
|
if (hasLoaded && props.progress === 100) return 'Upload Complete';
|
|
if (hasProgress) return 'Uploading';
|
|
return 'Pending';
|
|
};
|
|
|
|
const formatTimestamp = (timestamp?: number) => {
|
|
if (!timestamp) return 'N/A';
|
|
return new Date(timestamp).toLocaleString();
|
|
};
|
|
|
|
const handleRetry = () => {
|
|
emit('retry');
|
|
isDialogOpen.value = false;
|
|
};
|
|
|
|
const downloadTrack = () => {
|
|
emit('download');
|
|
isDialogOpen.value = false;
|
|
};
|
|
|
|
const openInPlayer = () => {
|
|
emit('play');
|
|
isDialogOpen.value = false;
|
|
};
|
|
</script>
|