feat(Library): add Library page and integrate favourites functionality

refactor(useFavourites): switch from chrome.storage.sync to chrome.storage.local and normalize favourites data
This commit is contained in:
Astrian Zheng 2025-05-28 09:46:43 +10:00
parent 377a15cdad
commit 65e3520ecf
Signed by: Astrian
SSH Key Fingerprint: SHA256:rVnhx3DAKjujCwWE13aDl7uV6+9U1MvydLkNRXJrBiA
4 changed files with 155 additions and 11 deletions

View File

@ -12,7 +12,7 @@ const router = useRouter()
<template>
<div class="w-screen h-screen overflow-hidden bg-[#191919]">
<div class="flex flex-col w-full h-full overflow-y-auto pb-24">
<div class="flex flex-col w-full h-full overflow-y-auto">
<div class="py-8 px-4 md:px-8 w-screen bg-gradient-to-b from-[#00000080] to-transparent z-20 absolute top-0">
<div class="flex justify-between align-center h-[2.625rem] items-center">
<ul class="flex gap-4" v-if="(() => {

View File

@ -9,11 +9,13 @@ import App from './App.vue'
import HomePage from './pages/Home.vue'
import AlbumDetailView from './pages/AlbumDetail.vue'
import Playroom from './pages/Playroom.vue'
import Library from './pages/Library.vue'
const routes = [
{ path: '/', component: HomePage },
{ path: '/albums/:albumId', component: AlbumDetailView },
{ path: '/playroom', component: Playroom }
{ path: '/playroom', component: Playroom },
{ path: '/library', component: Library }
]
const router = createRouter({

97
src/pages/Library.vue Normal file
View File

@ -0,0 +1,97 @@
<script setup lang="ts">
import StarFilledIcon from '../assets/icons/starfilled.vue'
import PlayIcon from '../assets/icons/play.vue'
import ShuffleIcon from '../assets/icons/shuffle.vue'
import { useFavourites } from '../stores/useFavourites'
import { ref } from 'vue'
import { artistsOrganize } from '../utils'
const favourites = useFavourites()
const currentList = ref<'favourites' | number>('favourites')
</script>
<template>
<div class="flex h-screen overflow-y-auto gap-8 select-none">
<div class="w-96 sticky top-0 pl-8">
<ul class="pt-26 flex flex-col gap-2">
<li>
<button
class="flex gap-2 items-center w-full hover:bg-neutral-600/40 active:bg-neutral-700/50 transition-all rounded-md px-2 py-2"
:class="currentList === 'favourites' ? 'bg-neutral-600/20' : ''" @click="currentList = 'favourites'">
<div class="w-12 h-12 relative text-white bg-neutral-600 rounded-md shadow-md">
<StarFilledIcon :size="6" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
</div>
<div class="flex flex-col text-left">
<div class="text-white text-xl">我的星标歌曲</div>
<div class="text-white/50 text-sm">{{ favourites.favouritesCount }} 首歌曲</div>
</div>
</button>
</li>
<li>
<div class="w-full">
<div class="text-white/50 text-center">
<div class="text-lg">自定义歌单功能尚未就绪</div>
<div class="text-md">随时回来看看</div>
</div>
</div>
</li>
</ul>
</div>
<div class="flex-1 mt-26">
<div class="flex gap-4">
<div class="w-72 h-72 rounded-md overflow-hidden shadow-2xl bg-neutral-800/20 relative">
<img :src="favourites.favourites[0]?.album?.coverUrl"
v-if="favourites.favouritesCount > 0 && favourites.favouritesCount < 4" />
<div v-else-if="favourites.favouritesCount >= 4" class="grid grid-cols-2 grid-rows-2">
<img :src="favourites.favourites[0]?.album?.coverUrl" class="w-full h-full object-cover" />
<img :src="favourites.favourites[1]?.album?.coverUrl" class="w-full h-full object-cover" />
<img :src="favourites.favourites[2]?.album?.coverUrl" class="w-full h-full object-cover" />
<img :src="favourites.favourites[3]?.album?.coverUrl" class="w-full h-full object-cover" />
</div>
<div
class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-neutral-800/20 to-sky-800/70 backdrop-grayscale-100">
<StarFilledIcon class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white" :size="32" />
</div>
</div>
<div class="flex flex-col justify-between">
<div v-if="currentList === 'favourites'" class="flex flex-col gap-2">
<div class="text-white text-4xl font-medium">我的星标歌曲</div>
<div class="text-white/50 text-lg">{{ favourites.favouritesCount }} 首歌曲</div>
</div>
<div class="flex gap-2">
<button
class="bg-sky-500/20 hover:bg-sky-500/30 active:bg-sky-600/30 active:shadow-inner backdrop-blur-3xl border border-[#ffffff39] rounded-full w-56 h-10 text-base text-white flex justify-center items-center gap-2 transition-all"
@click="">
<PlayIcon :size="4" />
<div>播放歌单</div>
</button>
<button
class="text-white w-10 h-10 bg-neutral-800/80 border border-[#ffffff39] backdrop-blur-3xl rounded-full flex justify-center items-center hover:bg-neutral-700/80 transition-all"
@click="">
<ShuffleIcon :size="4" />
</button>
</div>
</div>
</div>
<div class="flex flex-col gap-2 mt-4 mr-8">
<button v-for="item in favourites.favourites.slice().reverse()" :key="item.song.cid"
class="text-left flex items-center p-2 hover:bg-neutral-400/10 odd:bg-neutral-400/5 rounded-md transition-all">
<img :src="item.album?.coverUrl" class="w-12 h-12 rounded-md object-cover inline-block mr-2" />
<div>
<div class="text-white text-base font-medium">{{ item.song.name }}</div>
<div class="text-white/50 text-sm">{{ item.album?.name }} - {{ artistsOrganize(item.song.artists ?? []) }}
</div>
</div>
</button>
</div>
</div>
</div>
</template>

View File

@ -21,7 +21,7 @@ export const useFavourites = defineStore('favourites', () => {
const detectAvailableAPIs = () => {
// 检查原生 chrome API
try {
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
storageType.value = 'chrome'
return 'chrome'
}
@ -31,7 +31,7 @@ export const useFavourites = defineStore('favourites', () => {
// 检查 window.chrome
try {
if (window.chrome && window.chrome.storage && window.chrome.storage.sync) {
if (window.chrome && window.chrome.storage && window.chrome.storage.local) {
storageType.value = 'chrome'
return 'chrome'
}
@ -64,7 +64,7 @@ export const useFavourites = defineStore('favourites', () => {
switch (type) {
case 'chrome':
return await new Promise((resolve) => {
const api = chrome?.storage?.sync || window.chrome?.storage?.sync
const api = chrome?.storage?.local || window.chrome?.storage?.local
if (api) {
api.get({ [key]: defaultValue }, (result) => {
if (chrome.runtime.lastError) {
@ -100,7 +100,7 @@ export const useFavourites = defineStore('favourites', () => {
switch (type) {
case 'chrome':
return await new Promise<void>((resolve, reject) => {
const api = chrome?.storage?.sync || window.chrome?.storage?.sync
const api = chrome?.storage?.local || window.chrome?.storage?.local
if (api) {
api.set({ [key]: value }, () => {
if (chrome.runtime.lastError) {
@ -127,17 +127,62 @@ export const useFavourites = defineStore('favourites', () => {
}
}
// 数据验证和规范化函数
const normalizeFavourites = (data: any[]): QueueItem[] => {
if (!Array.isArray(data)) return []
return data.map(item => {
if (!item || !item.song) return null
// 规范化 Song 对象
const song: Song = {
cid: item.song.cid || '',
name: item.song.name || '',
albumCid: item.song.albumCid,
sourceUrl: item.song.sourceUrl,
lyricUrl: item.song.lyricUrl,
mvUrl: item.song.mvUrl,
mvCoverUrl: item.song.mvCoverUrl,
// 确保 artistes 和 artists 是数组
artistes: Array.isArray(item.song.artistes) ? item.song.artistes :
typeof item.song.artistes === 'object' ? Object.values(item.song.artistes) :
[],
artists: Array.isArray(item.song.artists) ? item.song.artists :
typeof item.song.artists === 'object' ? Object.values(item.song.artists) :
[]
}
// 规范化 Album 对象(如果存在)
const album = item.album ? {
cid: item.album.cid || '',
name: item.album.name || '',
intro: item.album.intro,
belong: item.album.belong,
coverUrl: item.album.coverUrl || '',
coverDeUrl: item.album.coverDeUrl,
artistes: Array.isArray(item.album.artistes) ? item.album.artistes :
typeof item.album.artistes === 'object' ? Object.values(item.album.artistes) :
[],
songs: item.album.songs
} : undefined
return { song, album }
}).filter(Boolean) as QueueItem[]
}
// 获取收藏列表
const getFavourites = async () => {
const result = await getStoredValue('favourites', defaultFavourites)
// 确保返回的是数组
return Array.isArray(result) ? result : defaultFavourites
// 确保返回的是数组并进行数据规范化
const normalizedResult = Array.isArray(result) ? normalizeFavourites(result) : defaultFavourites
return normalizedResult
}
// 保存收藏列表
const saveFavourites = async () => {
// 确保保存的是数组
await setStoredValue('favourites', [...favourites.value])
// 确保保存的是规范化的数组
const normalizedFavourites = normalizeFavourites([...favourites.value])
await setStoredValue('favourites', normalizedFavourites)
}
// 检查歌曲是否已收藏
@ -209,7 +254,7 @@ export const useFavourites = defineStore('favourites', () => {
const initializeFavourites = async () => {
try {
const savedFavourites = await getFavourites()
// 确保设置的是有效数组
// 确保设置的是有效且规范化的数组
favourites.value = Array.isArray(savedFavourites) ? savedFavourites : []
isLoaded.value = true
} catch (error) {