Files
frontend/app/pages/edit.vue
2026-01-05 22:24:42 +05:00

215 lines
7.7 KiB
Vue

<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { InputWithIcon } from '@/components/ui/input'
import { Outline } from '@/components/ui/outline'
import { SidebarTrigger } from '@/components/ui/sidebar'
import { useMutation, useQueryClient } from '@tanstack/vue-query'
import { Search } from 'lucide-vue-next'
import { VueDraggableNext } from 'vue-draggable-next'
import MusicCard from '~/components/internal/musiccard/MusicCard.vue'
import type { TrackResponse } from '~/composeables/api/models'
import { bulkReorder, getGetPlaylistTracksQueryKey, useGetPlaylistTracks } from '~/composeables/api/track-controller/track-controller'
import { useCurrentPlaylistStore } from '~/stores/use-current-playlist-store'
const searchValue = ref('')
const queryClient = useQueryClient();
const currentPlaylistStore = useCurrentPlaylistStore();
const { data: playlistTracks, isLoading, error, refetch } = useGetPlaylistTracks(
computed(() => currentPlaylistStore.id),
{
query: {
enabled: computed(() => currentPlaylistStore.id !== -1)
}
}
)
const { mutate: reorderTracks } = useMutation({
mutationFn: ({ playlistId, trackIds }: { playlistId: number; trackIds: number[] }) =>
bulkReorder(playlistId, { trackIds }),
onMutate: async ({ playlistId, trackIds }) => {
await queryClient.cancelQueries({ queryKey: getGetPlaylistTracksQueryKey(playlistId) });
const previousTracks = queryClient.getQueryData(getGetPlaylistTracksQueryKey(playlistId));
queryClient.setQueryData(getGetPlaylistTracksQueryKey(playlistId), (old: any) => {
if (!old) return old;
return {
...old,
data: trackIds.map(id => old.data.find((t: any) => t.trackId === id || t.id === id)),
};
});
return { previousTracks };
},
onError: (err, { playlistId }, context) => {
console.log(err)
if (context?.previousTracks) {
queryClient.setQueryData(getGetPlaylistTracksQueryKey(playlistId), context.previousTracks);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: getGetPlaylistTracksQueryKey(currentPlaylistStore.id) });
},
});
interface MappedTrack {
id: number;
title: string;
author: string;
authorLabel: string;
badges: string[];
imageUrl: string | undefined;
date: string;
}
const mappedTracks = ref<MappedTrack[]>([])
watchEffect(() => {
if (!playlistTracks.value) return []
const tracks = playlistTracks.value.data
const result = tracks
.map((track: TrackResponse, index: number) => ({
id: track.trackId!,
title: track.title || 'Unknown Track',
author: track.artist || 'Unknown Artist',
authorLabel: "Artist",
badges: ['mp3'], // TODO: badges
imageUrl: getDefaultImage(index), // TODO: imageUrl
date: formatDate(''), // TODO: createdAt
}));
mappedTracks.value = result;
})
const getDefaultImage = (index: number) => {
const defaultImages = [
"https://github.com/yavuzceliker/sample-images/blob/main/docs/image-1.jpg?raw=true",
"https://github.com/yavuzceliker/sample-images/blob/main/docs/image-2.jpg?raw=true",
"https://github.com/yavuzceliker/sample-images/blob/main/docs/image-3.jpg?raw=true",
"https://github.com/yavuzceliker/sample-images/blob/main/docs/image-5.jpg?raw=true"
]
return defaultImages[index % defaultImages.length]
}
const formatDate = (dateString: string) => {
if (!dateString) return 'recently added'
const date = new Date(dateString)
const now = new Date()
const diffTime = Math.abs(now.getTime() - date.getTime())
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
if (diffDays === 1) return 'yesterday'
if (diffDays < 7) return `${diffDays} days ago`
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`
return `${Math.floor(diffDays / 365)} years ago`
}
const filteredTracks = computed(() => {
if (!searchValue.value) return mappedTracks.value
return mappedTracks.value.filter(track =>
track.title.toLowerCase().includes(searchValue.value.toLowerCase()) ||
track.author.toLowerCase().includes(searchValue.value.toLowerCase()) ||
track.badges.some(badge => badge.toLowerCase().includes(searchValue.value.toLowerCase()))
)
});
function onTrackOrderChange() {
const trackIds = mappedTracks.value?.map(t => t.id);
reorderTracks({
playlistId: currentPlaylistStore.id,
trackIds,
});
}
watch(() => currentPlaylistStore.id, (newId) => {
if (newId !== -1) {
refetch()
}
})
</script>
<template>
<div class="flex-1">
<NuxtLayout name="default">
<template #header>
<Outline side="bottom" padding="dense" class="w-full flex">
<div class="flex gap-8 w-full items-center">
<SidebarTrigger :size="5" />
<h2 class="scroll-m-20 text-3xl font-semibold tracking-tight transition-colors first:mt-0">
Tracks
</h2>
</div>
<Button>
Export
</Button>
</Outline>
</template>
<div class="flex flex-col w-full p-4 gap-4 h-full">
<InputWithIcon v-model="searchValue" :icon="Search" placeholder="Search..." type="search" icon-size="5"
id="user-search" class="w-full" />
<div>
<div v-if="isLoading" class="flex justify-center items-center py-8">
<p>Loading tracks...</p>
</div>
<div v-else-if="error" class="flex justify-center items-center py-8">
<p class="text-red-500">Error loading tracks: {{ error }}</p>
</div>
<div v-else-if="!filteredTracks.length" class="flex justify-center items-center py-8">
<p v-if="searchValue">No tracks found matching "{{ searchValue }}"</p>
<p v-else>No tracks in this playlist</p>
</div>
<VueDraggableNext v-model="mappedTracks" group="tracks" @change="onTrackOrderChange" item-key="id"
class="space-y-2" v-else>
<div v-for="track in mappedTracks" :key="track.id">
<MusicCard :key="track.id" :title="track.title" :author="track.author"
:authorLabel="track.authorLabel" :badges="track.badges" :imageUrl="track.imageUrl"
:date="track.date" />
</div>
</VueDraggableNext>
</div>
</div>
<template #sidebar>
<Outline padding="none" class="h-full" side="left">
<Outline padding="dense" side="bottom">
<p class="leading-7 not-first:mt-6 font-semibold">
Metadata editor
</p>
</Outline>
</Outline>
</template>
</NuxtLayout>
</div>
</template>
<style>
.list-move {
transition: 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.list-enter-active,
.list-leave-active {
transition: 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(-20px);
}
.list-leave-active {
position: absolute;
pointer-events: none;
}
</style>