194 lines
6.8 KiB
Vue
194 lines
6.8 KiB
Vue
<template>
|
|
<Frame margin="none" class="px-3 py-4 flex items-center gap-2 cursor-pointer" @click="openDialog">
|
|
<div>
|
|
<ListMusicIcon :size="40" v-if="hasLoaded" />
|
|
<CassetteTape :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">
|
|
{{ trackCount }} track(s)
|
|
</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>
|
|
<RefreshCw class="mr-2 h-4 w-4" />
|
|
Retry
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem class="text-destructive">
|
|
<Trash2 class="mr-2 h-4 w-4" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
<Dialog :open="isDialogOpen" @update:open="toggleDialog">
|
|
<DialogContent class="max-w-3xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Playlist Upload Details</DialogTitle>
|
|
<DialogDescription>
|
|
Detailed information about this playlist upload
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div class="grid gap-4 py-4">
|
|
<div class="space-y-4">
|
|
<h3 class="text-lg font-semibold">Basic 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>Status</Label>
|
|
<div class="flex items-center gap-2">
|
|
<div :class="`h-2 w-2 rounded-full ${getStatusColor()}`" />
|
|
<span class="text-sm">{{ getStatusText() }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label>Track Count</Label>
|
|
<p class="text-sm">{{ trackCount }} tracks</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="ytdlnStdout" class="space-y-4">
|
|
<div class="flex justify-between items-center">
|
|
<h3 class="text-lg font-semibold">yt-dlp Output</h3>
|
|
<Button variant="outline" size="sm" @click="copyToClipboard(ytdlnStdout)">
|
|
<Copy class="mr-2 h-4 w-4" />
|
|
Copy
|
|
</Button>
|
|
</div>
|
|
<div class="bg-muted rounded-md p-4">
|
|
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-60">{{ ytdlnStdout }}</pre>
|
|
</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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" @click="isDialogOpen = false">Close</Button>
|
|
<Button @click="handleRetry">
|
|
<RefreshCw class="mr-2 h-4 w-4" />
|
|
Retry Upload
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</Frame>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {
|
|
CassetteTape,
|
|
Copy,
|
|
EllipsisVertical,
|
|
Eye,
|
|
FileQuestionMark,
|
|
ListMusicIcon,
|
|
RefreshCw,
|
|
Trash2
|
|
} from 'lucide-vue-next';
|
|
import { ref } from 'vue';
|
|
import type { PlaylistProgressAllOfStatus } from '~/composeables/api/models';
|
|
|
|
interface Props {
|
|
title: string
|
|
trackCount?: number
|
|
ytdlnStdout: string
|
|
status: PlaylistProgressAllOfStatus
|
|
progress?: number
|
|
error?: string
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
progress: 0
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
retry: []
|
|
}>();
|
|
|
|
const isDialogOpen = ref(false);
|
|
|
|
const hasLoaded = props.trackCount;
|
|
const hasProgress = props.progress !== undefined && props.progress > 0;
|
|
const hasError = props.error;
|
|
|
|
const openDialog = () => {
|
|
isDialogOpen.value = true;
|
|
};
|
|
|
|
const toggleDialog = (value: boolean) => {
|
|
isDialogOpen.value = value;
|
|
}
|
|
|
|
const getStatusColor = () => {
|
|
if (hasError) return 'bg-destructive';
|
|
if (props.status === 'FINISHED') return 'bg-green-500';
|
|
if (props.status === 'LOADING') return 'bg-blue-500';
|
|
if (hasProgress) return 'bg-amber-500';
|
|
return 'bg-gray-500';
|
|
};
|
|
|
|
const getStatusText = () => {
|
|
if (hasError) return 'Error';
|
|
if (props.status === 'FINISHED') return 'Completed';
|
|
if (props.status === 'LOADING') return 'Loading';
|
|
if (hasProgress) return 'In Progress';
|
|
return 'Pending';
|
|
};
|
|
|
|
const copyToClipboard = async (text: string) => {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
console.log('Copied to clipboard');
|
|
} catch (err) {
|
|
console.error('Failed to copy:', err);
|
|
}
|
|
};
|
|
|
|
const handleRetry = () => {
|
|
emit('retry');
|
|
isDialogOpen.value = false;
|
|
};
|
|
</script>
|