Refactor Sidebar and enhance playlist functionality; update main.js for session restoration; add toggleLike feature in authStore; improve AdminPage and LandingPage UI; integrate liked tracks in LibraryPage and PlaylistPage; optimize track playback logic.

This commit is contained in:
2026-05-26 12:37:41 +07:00
parent 9fdd78e737
commit b2f3e9661e
14 changed files with 433 additions and 269 deletions
+37 -30
View File
@@ -9,13 +9,15 @@
</div>
<div v-else class="grid grid-cols-4 gap-6">
<div v-for="track in likedTracks" :key="track._id"
@click="playTrack(track)"
@click="playTrack(track, true)"
class="group cursor-pointer bg-white rounded-2xl border-2 border-black overflow-hidden hover:scale-[1.02] transition-transform"
>
<div class="aspect-square bg-gray-300 relative overflow-hidden">
<img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" />
<div class="absolute bottom-2 right-2 w-10 h-10 bg-noctune-yellow rounded-full flex items-center justify-center border-2 border-black opacity-0 group-hover:opacity-100 transition-opacity">
<svg class="w-5 h-5 ml-0.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
<div class="absolute bottom-2 right-2 w-10 h-10 rounded-full flex items-center justify-center border-2 border-black opacity-0 group-hover:opacity-100 transition-opacity"
:class="isCurrentTrack(track) ? 'bg-white' : 'bg-noctune-yellow'">
<svg v-if="isCurrentTrack(track) && playerStore.isPlaying" class="w-5 h-5 text-black" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
<svg v-else class="w-5 h-5 ml-0.5 text-black" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
</div>
</div>
<div class="p-3">
@@ -41,20 +43,23 @@
</div>
<div class="divide-y divide-gray-200">
<div v-for="(track, index) in allTracks" :key="track._id"
@click="playTrack(track)"
@click="playTrack(track, false, index)"
class="px-6 py-3 flex items-center hover:bg-gray-50 transition-colors cursor-pointer"
>
<div class="flex-1 flex items-center gap-3">
<span class="text-sm text-gray-400 w-6">{{ index + 1 }}</span>
<span class="text-sm text-gray-400 w-6">
<svg v-if="isCurrentTrack(track) && playerStore.isPlaying" class="w-4 h-4 text-noctune-teal inline" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
<span v-else>{{ index + 1 }}</span>
</span>
<div class="w-10 h-10 rounded-lg flex-shrink-0 overflow-hidden bg-gray-300 border border-black">
<img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" />
</div>
<span class="font-medium text-black text-sm truncate">{{ track.title }}</span>
<span class="font-medium text-sm truncate" :class="isCurrentTrack(track) ? 'text-noctune-teal' : 'text-black'">{{ track.title }}</span>
</div>
<span class="w-40 text-center text-gray-600 text-sm truncate">{{ track.artist }}</span>
<span class="w-32 text-center text-gray-600 text-sm truncate">{{ track.album }}</span>
<button @click.stop="toggleLike(track)" class="w-12 flex justify-center text-gray-400 hover:text-red-400 transition-colors">
<svg class="w-5 h-5" :class="track._isLiked ? 'fill-red-400 text-red-400' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/></svg>
<button @click.stop="toggleLike(track)" class="w-12 flex justify-center transition-colors">
<svg class="w-5 h-5" :class="isLiked(track) ? 'fill-red-400 text-red-400' : 'text-gray-400 hover:text-red-400'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/></svg>
</button>
</div>
</div>
@@ -67,10 +72,12 @@
import { ref, onMounted } from 'vue'
import { usePlayerStore } from '../stores/playerStore.js'
import { useQueueStore } from '../stores/queueStore.js'
import { useAuthStore } from '../stores/authStore.js'
import api from '../lib/api.js'
const playerStore = usePlayerStore()
const queueStore = useQueueStore()
const authStore = useAuthStore()
const likedTracks = ref([])
const allTracks = ref([])
@@ -82,26 +89,31 @@ function formatUrl(url) {
return url.startsWith('http') ? url : `http://localhost:3001${url}`
}
function playTrack(track) {
const tracks = allTracks.value.length ? allTracks.value : likedTracks.value
const idx = tracks.findIndex((t) => t._id === track._id)
queueStore.setQueue(tracks, idx >= 0 ? idx : 0)
function isCurrentTrack(track) {
return playerStore.currentTrack?._id === track._id
}
function isLiked(track) {
return authStore.likedTrackIds?.has(track._id)
}
function playTrack(track, fromLiked = false, idx) {
if (isCurrentTrack(track)) {
playerStore.togglePlay()
return
}
const source = fromLiked ? likedTracks.value : allTracks.value
const i = idx ?? source.findIndex((t) => t._id === track._id)
queueStore.setQueue(source, i)
playerStore.setTrack(track)
playerStore.play()
}
async function toggleLike(track) {
try {
if (track._isLiked) {
await api.delete(`/tracks/${track._id}/like`)
track._isLiked = false
likedTracks.value = likedTracks.value.filter((t) => t._id !== track._id)
} else {
await api.post(`/tracks/${track._id}/like`)
track._isLiked = true
}
} catch {
// ignore
const wasLiked = isLiked(track)
await authStore.toggleLike(track._id)
if (wasLiked) {
likedTracks.value = likedTracks.value.filter((t) => t._id !== track._id)
}
}
@@ -109,8 +121,7 @@ onMounted(async () => {
try {
const { data: liked } = await api.get('/tracks/liked')
if (liked.success) {
likedTracks.value = liked.data.likedTracks || []
likedTracks.value.forEach((t) => (t._isLiked = true))
likedTracks.value = liked.data || []
}
} catch {
// ignore
@@ -120,11 +131,7 @@ onMounted(async () => {
try {
const { data: all } = await api.get('/tracks')
if (all.success) {
allTracks.value = all.data
const likedIds = new Set(likedTracks.value.map((t) => t._id))
allTracks.value.forEach((t) => (t._isLiked = likedIds.has(t._id)))
}
if (all.success) allTracks.value = all.data
} catch {
// ignore
} finally {