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:
parent
377a15cdad
commit
65e3520ecf
|
@ -12,7 +12,7 @@ const router = useRouter()
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-screen h-screen overflow-hidden bg-[#191919]">
|
<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="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">
|
<div class="flex justify-between align-center h-[2.625rem] items-center">
|
||||||
<ul class="flex gap-4" v-if="(() => {
|
<ul class="flex gap-4" v-if="(() => {
|
||||||
|
|
|
@ -9,11 +9,13 @@ import App from './App.vue'
|
||||||
import HomePage from './pages/Home.vue'
|
import HomePage from './pages/Home.vue'
|
||||||
import AlbumDetailView from './pages/AlbumDetail.vue'
|
import AlbumDetailView from './pages/AlbumDetail.vue'
|
||||||
import Playroom from './pages/Playroom.vue'
|
import Playroom from './pages/Playroom.vue'
|
||||||
|
import Library from './pages/Library.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', component: HomePage },
|
{ path: '/', component: HomePage },
|
||||||
{ path: '/albums/:albumId', component: AlbumDetailView },
|
{ path: '/albums/:albumId', component: AlbumDetailView },
|
||||||
{ path: '/playroom', component: Playroom }
|
{ path: '/playroom', component: Playroom },
|
||||||
|
{ path: '/library', component: Library }
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|
97
src/pages/Library.vue
Normal file
97
src/pages/Library.vue
Normal 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>
|
|
@ -21,7 +21,7 @@ export const useFavourites = defineStore('favourites', () => {
|
||||||
const detectAvailableAPIs = () => {
|
const detectAvailableAPIs = () => {
|
||||||
// 检查原生 chrome API
|
// 检查原生 chrome API
|
||||||
try {
|
try {
|
||||||
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
|
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
|
||||||
storageType.value = 'chrome'
|
storageType.value = 'chrome'
|
||||||
return 'chrome'
|
return 'chrome'
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ export const useFavourites = defineStore('favourites', () => {
|
||||||
|
|
||||||
// 检查 window.chrome
|
// 检查 window.chrome
|
||||||
try {
|
try {
|
||||||
if (window.chrome && window.chrome.storage && window.chrome.storage.sync) {
|
if (window.chrome && window.chrome.storage && window.chrome.storage.local) {
|
||||||
storageType.value = 'chrome'
|
storageType.value = 'chrome'
|
||||||
return 'chrome'
|
return 'chrome'
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ export const useFavourites = defineStore('favourites', () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'chrome':
|
case 'chrome':
|
||||||
return await new Promise((resolve) => {
|
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) {
|
if (api) {
|
||||||
api.get({ [key]: defaultValue }, (result) => {
|
api.get({ [key]: defaultValue }, (result) => {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
|
@ -100,7 +100,7 @@ export const useFavourites = defineStore('favourites', () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'chrome':
|
case 'chrome':
|
||||||
return await new Promise<void>((resolve, reject) => {
|
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) {
|
if (api) {
|
||||||
api.set({ [key]: value }, () => {
|
api.set({ [key]: value }, () => {
|
||||||
if (chrome.runtime.lastError) {
|
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 getFavourites = async () => {
|
||||||
const result = await getStoredValue('favourites', defaultFavourites)
|
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 () => {
|
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 () => {
|
const initializeFavourites = async () => {
|
||||||
try {
|
try {
|
||||||
const savedFavourites = await getFavourites()
|
const savedFavourites = await getFavourites()
|
||||||
// 确保设置的是有效数组
|
// 确保设置的是有效且规范化的数组
|
||||||
favourites.value = Array.isArray(savedFavourites) ? savedFavourites : []
|
favourites.value = Array.isArray(savedFavourites) ? savedFavourites : []
|
||||||
isLoaded.value = true
|
isLoaded.value = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user