Format imports, partial implementation of import upload entries
This commit is contained in:
@ -1,20 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import PlaylistsNotFound from '@/components/internal/playlists/select/PlaylistsNotFound.vue';
|
||||
import Frame from '@/components/ui/frame/Frame.vue';
|
||||
import Select from '@/components/ui/select/Select.vue';
|
||||
import SelectContent from '@/components/ui/select/SelectContent.vue';
|
||||
import SelectCustomTrigger from '@/components/ui/select/SelectCustomTrigger.vue';
|
||||
import SelectGroup from '@/components/ui/select/SelectGroup.vue';
|
||||
import SelectItem from '@/components/ui/select/SelectItem.vue';
|
||||
import SelectLabel from '@/components/ui/select/SelectLabel.vue';
|
||||
import SelectSeparator from '@/components/ui/select/SelectSeparator.vue';
|
||||
import { useSidebar } from '@/components/ui/sidebar';
|
||||
import Skeleton from '@/components/ui/skeleton/Skeleton.vue';
|
||||
import { usePlaylists } from '@/composeables/api/playlist-controller/playlist-controller';
|
||||
import { ChevronsUpDown, Music4, Plus } from 'lucide-vue-next';
|
||||
import Button from '../ui/button/Button.vue';
|
||||
import PlaylistCreateDialog from './playlists/select/PlaylistCreateDialog.vue';
|
||||
import { useCurrentPlaylistStore } from '~/stores/use-current-playlist-store';
|
||||
import { useSidebar } from '../ui/sidebar';
|
||||
|
||||
const {
|
||||
open: sidebarOpen,
|
||||
@ -68,7 +58,7 @@ watch(data, (value) => {
|
||||
<SelectContent align="start" side="right" :sideOffset="4">
|
||||
<div class="w-full">
|
||||
<div v-if="isLoading">
|
||||
<UiSpinner />
|
||||
<Spinner />
|
||||
</div>
|
||||
<div v-else-if="isError">
|
||||
<SelectLabel>{{ error }}</SelectLabel>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<Frame margin="none" class="px-3 py-4 flex items-center gap-2">
|
||||
<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-if="hasProgress" />
|
||||
<FileQuestionMark :size="40" v-if="hasError" />
|
||||
<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">
|
||||
@ -24,7 +24,7 @@
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ progress }}%
|
||||
</p>
|
||||
<UiProgress :modelValue="progress" />
|
||||
<Progress :modelValue="progress" />
|
||||
</div>
|
||||
<div class="flex flex-row" v-if="hasError">
|
||||
<p class="text-sm text-destructive-foreground">
|
||||
@ -35,48 +35,110 @@
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<UiButton variant="ghost">
|
||||
<Button variant="ghost">
|
||||
<EllipsisVertical :size="40" />
|
||||
</UiButton>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-56" align="start">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Keyboard shortcuts
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<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">
|
||||
<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 class="space-y-2">
|
||||
<Label>Type</Label>
|
||||
<p class="text-sm">{{ type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="playlistProgressData?.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(playlistProgressData.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">{{
|
||||
playlistProgressData.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 lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import Frame from '@/components/ui/frame/Frame.vue';
|
||||
import { CassetteTape, Dot, EllipsisVertical, FileQuestionMark, ListMusicIcon } from 'lucide-vue-next';
|
||||
CassetteTape,
|
||||
Copy,
|
||||
Dot,
|
||||
EllipsisVertical,
|
||||
Eye,
|
||||
FileQuestionMark,
|
||||
ListMusicIcon,
|
||||
RefreshCw,
|
||||
Trash2
|
||||
} from 'lucide-vue-next';
|
||||
import { ref } from 'vue';
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
@ -84,13 +146,68 @@ interface Props {
|
||||
type?: string
|
||||
progress?: number
|
||||
error?: string
|
||||
playlistProgressData?: {
|
||||
playlistId: number
|
||||
trackSourceId: number
|
||||
userId: number
|
||||
timestamp: number
|
||||
ytdlnStdout: string
|
||||
overallProgress: number
|
||||
status: 'LOADING' | 'FINISHED'
|
||||
}
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
progress: 0
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
retry: []
|
||||
}>();
|
||||
|
||||
const isDialogOpen = ref(false);
|
||||
|
||||
const hasLoaded = props.trackCount && props.type;
|
||||
const hasProgress = props.progress;
|
||||
const hasProgress = props.progress !== undefined && props.progress > 0;
|
||||
const hasError = props.error;
|
||||
|
||||
const openDialog = () => {
|
||||
isDialogOpen.value = true;
|
||||
console.log(isDialogOpen.value)
|
||||
};
|
||||
|
||||
const getStatusColor = () => {
|
||||
if (hasError) return 'bg-destructive';
|
||||
if (props.playlistProgressData?.status === 'FINISHED') return 'bg-green-500';
|
||||
if (props.playlistProgressData?.status === 'LOADING') return 'bg-blue-500';
|
||||
if (hasProgress) return 'bg-amber-500';
|
||||
return 'bg-gray-500';
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
if (hasError) return 'Error';
|
||||
if (props.playlistProgressData?.status === 'FINISHED') return 'Completed';
|
||||
if (props.playlistProgressData?.status === 'LOADING') return 'Loading';
|
||||
if (hasProgress) return 'In Progress';
|
||||
return 'Pending';
|
||||
};
|
||||
|
||||
const formatTimestamp = (timestamp?: number) => {
|
||||
if (!timestamp) return 'N/A';
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<Frame margin="none" class="px-3 py-4 flex items-center gap-2">
|
||||
<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-if="hasProgress" />
|
||||
<FileQuestionMark :size="40" v-if="hasError" />
|
||||
<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">
|
||||
@ -28,7 +28,7 @@
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ progress }}%
|
||||
</p>
|
||||
<UiProgress :modelValue="progress" />
|
||||
<Progress :modelValue="progress" />
|
||||
</div>
|
||||
<div class="flex flex-row" v-if="hasError">
|
||||
<p class="text-sm text-destructive-foreground">
|
||||
@ -39,44 +39,156 @@
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<UiButton variant="ghost">
|
||||
<Button variant="ghost">
|
||||
<EllipsisVertical :size="40" />
|
||||
</UiButton>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-56" align="start">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Keyboard shortcuts
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<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 {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import Frame from '@/components/ui/frame/Frame.vue';
|
||||
import { AudioWaveform, Disc3, Dot, EllipsisVertical, FileQuestionMark } from 'lucide-vue-next';
|
||||
AudioWaveform,
|
||||
Disc3,
|
||||
Dot,
|
||||
Download,
|
||||
EllipsisVertical,
|
||||
Eye,
|
||||
FileQuestionMark,
|
||||
Play,
|
||||
RefreshCw,
|
||||
Trash2
|
||||
} from 'lucide-vue-next';
|
||||
import { ref } from 'vue';
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
@ -85,13 +197,67 @@ interface Props {
|
||||
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;
|
||||
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>
|
||||
|
||||
@ -34,10 +34,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import Draggable from '@/components/icon/Draggable.vue'
|
||||
import Frame from '@/components/ui/frame/Frame.vue'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
|
||||
@ -3,21 +3,10 @@ import { toTypedSchema } from "@vee-validate/zod";
|
||||
import { useForm } from "vee-validate";
|
||||
import * as z from "zod";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import { toast } from "vue-sonner";
|
||||
import Button from '~/components/ui/button/Button.vue';
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||
import Input from '~/components/ui/input/Input.vue';
|
||||
import { getPlaylistsQueryKey, useCreatePlaylist } from "~/composeables/api/playlist-controller/playlist-controller";
|
||||
import { FormField } from "@/components/ui/form";
|
||||
|
||||
const formSchema = toTypedSchema(z.object({
|
||||
playlistName: z.string().min(2).max(50).default(''),
|
||||
|
||||
@ -1,11 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import Button from '@/components/ui/button/Button.vue';
|
||||
import Empty from '@/components/ui/empty/Empty.vue';
|
||||
import EmptyContent from '@/components/ui/empty/EmptyContent.vue';
|
||||
import EmptyDescription from '@/components/ui/empty/EmptyDescription.vue';
|
||||
import EmptyHeader from '@/components/ui/empty/EmptyHeader.vue';
|
||||
import EmptyMedia from '@/components/ui/empty/EmptyMedia.vue';
|
||||
import EmptyTitle from '@/components/ui/empty/EmptyTitle.vue';
|
||||
import { FileMusicIcon } from 'lucide-vue-next';
|
||||
import PlaylistCreateDialog from './PlaylistCreateDialog.vue';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user