mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 03:14:57 +08:00
✨ feat: 完善部分配置及页面
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -17,6 +17,7 @@ declare module 'vue' {
|
||||
CoverMenu: typeof import('./src/components/Menu/CoverMenu.vue')['default']
|
||||
CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default']
|
||||
DownloadSong: typeof import('./src/components/Modal/DownloadSong.vue')['default']
|
||||
ExcludeKeywords: typeof import('./src/components/Modal/ExcludeKeywords.vue')['default']
|
||||
FullPlayer: typeof import('./src/components/Player/FullPlayer.vue')['default']
|
||||
GeneralSetting: typeof import('./src/components/Setting/GeneralSetting.vue')['default']
|
||||
JumpArtist: typeof import('./src/components/Modal/JumpArtist.vue')['default']
|
||||
@@ -54,6 +55,7 @@ declare module 'vue' {
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
|
||||
24
package.json
24
package.json
@@ -47,12 +47,12 @@
|
||||
"@pixi/filter-bulge-pinch": "^5.1.1",
|
||||
"@pixi/filter-color-matrix": "^7.4.2",
|
||||
"@pixi/sprite": "^7.4.2",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/core": "^12.0.0",
|
||||
"NeteaseCloudMusicApi": "^4.25.0",
|
||||
"axios": "^1.7.8",
|
||||
"axios": "^1.7.9",
|
||||
"change-case": "^5.4.4",
|
||||
"dayjs": "^1.11.13",
|
||||
"electron-dl": "^3.5.2",
|
||||
"electron-dl": "^4.0.0",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -67,8 +67,8 @@
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^14.1.4",
|
||||
"music-metadata": "7.14.0",
|
||||
"pinia": "^2.2.8",
|
||||
"pinia-plugin-persistedstate": "^3.2.3",
|
||||
"pinia": "^2.3.0",
|
||||
"pinia-plugin-persistedstate": "^4.1.3",
|
||||
"plyr": "^3.7.8",
|
||||
"vue-virt-list": "^1.5.5"
|
||||
},
|
||||
@@ -84,24 +84,24 @@
|
||||
"@types/howler": "^2.2.12",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.16.0",
|
||||
"@typescript-eslint/parser": "^8.16.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||
"@typescript-eslint/parser": "^8.18.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"ajv": "^8.17.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"electron": "^30.5.1",
|
||||
"electron-builder": "^25.1.8",
|
||||
"electron-log": "^5.2.3",
|
||||
"electron-log": "^5.2.4",
|
||||
"electron-vite": "^2.3.0",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fastify": "^4.28.1",
|
||||
"fastify": "^4.29.0",
|
||||
"naive-ui": "^2.40.3",
|
||||
"node-taglib-sharp": "^5.2.3",
|
||||
"prettier": "^3.4.1",
|
||||
"sass": "^1.81.0",
|
||||
"terser": "^5.36.0",
|
||||
"prettier": "^3.4.2",
|
||||
"sass": "^1.82.0",
|
||||
"terser": "^5.37.0",
|
||||
"typescript": "5.6.2",
|
||||
"unplugin-auto-import": "^0.18.6",
|
||||
"unplugin-vue-components": "^0.27.5",
|
||||
|
||||
1360
pnpm-lock.yaml
generated
1360
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -55,9 +55,10 @@
|
||||
<!-- 路由页面 -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
|
||||
<KeepAlive :max="20" :exclude="['layout']">
|
||||
<KeepAlive v-if="settingStore.useKeepAlive" :max="20" :exclude="['layout']">
|
||||
<component :is="Component" class="router-view" />
|
||||
</KeepAlive>
|
||||
<component v-else :is="Component" class="router-view" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
<!-- 回顶 -->
|
||||
|
||||
20
src/components/Modal/ExcludeKeywords.vue
Normal file
20
src/components/Modal/ExcludeKeywords.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="exclude">
|
||||
<n-alert :show-icon="false">请勿添加过多,以免影响歌词的正常显示</n-alert>
|
||||
<n-dynamic-tags v-model:value="settingStore.excludeKeywords" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from "@/stores";
|
||||
|
||||
const settingStore = useSettingStore();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.exclude {
|
||||
.n-alert {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -44,6 +44,11 @@ const login = async () => {
|
||||
window.$message.warning("请输入 Cookie");
|
||||
return;
|
||||
}
|
||||
// 是否为有效 Cookie
|
||||
if (!cookie.value.includes("MUSIC_U") || cookie.value.endsWith(";")) {
|
||||
window.$message.warning("请输入有效的 Cookie");
|
||||
return;
|
||||
}
|
||||
// 写入 Cookie
|
||||
try {
|
||||
window.$message.success("登录成功");
|
||||
@@ -66,8 +71,6 @@ const login = async () => {
|
||||
onMounted(() => {
|
||||
if (isElectron) {
|
||||
window.electron.ipcRenderer.on("send-cookies", (_, value) => {
|
||||
console.log(typeof value);
|
||||
|
||||
if (!value) return;
|
||||
cookie.value = value;
|
||||
login();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
v-show="statusStore.showFullPlayer"
|
||||
:style="{
|
||||
'--main-color': mainColor,
|
||||
'--main-color': statusStore.mainColor,
|
||||
cursor: statusStore.playerMetaShow ? 'auto' : 'none',
|
||||
}"
|
||||
class="full-player"
|
||||
@@ -66,7 +66,7 @@
|
||||
<!-- 封面 -->
|
||||
<PlayerCover />
|
||||
<!-- 数据 -->
|
||||
<PlayerData :center="playerDataCenter" :theme="mainColor" />
|
||||
<PlayerData :center="playerDataCenter" :theme="statusStore.mainColor" />
|
||||
</div>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<!-- 评论 -->
|
||||
@@ -94,7 +94,7 @@
|
||||
<!-- 音乐频谱 -->
|
||||
<PlayerSpectrum
|
||||
v-if="settingStore.showSpectrums"
|
||||
:color="mainColor ? `rgb(${mainColor})` : 'rgb(239 239 239)'"
|
||||
:color="statusStore.mainColor ? `rgb(${statusStore.mainColor})` : 'rgb(239 239 239)'"
|
||||
:show="!statusStore.playerMetaShow"
|
||||
:height="60"
|
||||
/>
|
||||
@@ -144,13 +144,6 @@ const instantLyrics = computed(() => {
|
||||
return { content: content?.content, tran: settingStore.showTran && content?.tran };
|
||||
});
|
||||
|
||||
// 播放器主色
|
||||
const mainColor = computed(() => {
|
||||
const mainColor = statusStore.songCoverTheme?.main;
|
||||
if (!mainColor) return "239, 239, 239";
|
||||
return `${mainColor.r}, ${mainColor.g}, ${mainColor.b}`;
|
||||
});
|
||||
|
||||
// 隐藏播放元素
|
||||
const {
|
||||
isPending,
|
||||
|
||||
@@ -50,9 +50,8 @@ const { pause: pauseSeek, resume: resumeSeek } = useRafFn(() => {
|
||||
|
||||
// 歌词主色
|
||||
const mainColor = computed(() => {
|
||||
const mainColor = statusStore.songCoverTheme?.main;
|
||||
if (!mainColor) return "rgb(239, 239, 239)";
|
||||
return `rgb(${mainColor.r}, ${mainColor.g}, ${mainColor.b})`;
|
||||
if (!statusStore.mainColor) return "rgb(239, 239, 239)";
|
||||
return `rgb(${statusStore.mainColor})`;
|
||||
});
|
||||
|
||||
// 当前歌词
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<n-drawer
|
||||
v-model:show="statusStore.playListShow"
|
||||
:class="{ 'full-player': statusStore.showFullPlayer }"
|
||||
:style="{ '--main-color': mainColor }"
|
||||
:style="{ '--main-color': statusStore.mainColor }"
|
||||
:auto-focus="false"
|
||||
id="main-playlist"
|
||||
style="width: 400px"
|
||||
@@ -124,13 +124,6 @@ const statusStore = useStatusStore();
|
||||
|
||||
const playListRef = ref<VirtualListInst | null>(null);
|
||||
|
||||
// 列表主色
|
||||
const mainColor = computed(() => {
|
||||
const mainColor = statusStore.songCoverTheme?.main;
|
||||
if (!mainColor) return "239, 239, 239";
|
||||
return `${mainColor.r}, ${mainColor.g}, ${mainColor.b}`;
|
||||
});
|
||||
|
||||
// 播放列表数据
|
||||
const playListData = computed(() => {
|
||||
return dataStore.playList.map((item, index) => {
|
||||
@@ -290,6 +283,9 @@ const scrollToItem = (index: number, behavior: "smooth" | "auto" = "smooth") =>
|
||||
&.on {
|
||||
border-color: rgb(var(--main-color));
|
||||
}
|
||||
&:hover {
|
||||
border-color: rgb(var(--main-color));
|
||||
}
|
||||
.num {
|
||||
color: rgba(var(--main-color), 0.52);
|
||||
}
|
||||
|
||||
@@ -173,17 +173,17 @@
|
||||
@select="(mode) => player.togglePlayMode(mode)"
|
||||
>
|
||||
<div class="menu-icon" @click.stop="player.togglePlayMode(false)">
|
||||
<SvgIcon :name="playModeIcon" />
|
||||
<SvgIcon :name="statusStore.playModeIcon" />
|
||||
</div>
|
||||
</n-dropdown>
|
||||
<!-- 音量调节 -->
|
||||
<n-popover :show-arrow="false" :style="{ padding: 0 }">
|
||||
<template #trigger>
|
||||
<div class="menu-icon" @click.stop="player.toggleMute" @wheel="changeVolume">
|
||||
<SvgIcon :name="playVolumeIcon" />
|
||||
<div class="menu-icon" @click.stop="player.toggleMute" @wheel="player.setVolume">
|
||||
<SvgIcon :name="statusStore.playVolumeIcon" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="volume-change" @wheel="changeVolume">
|
||||
<div class="volume-change" @wheel="player.setVolume">
|
||||
<n-slider
|
||||
v-model:value="statusStore.playVolume"
|
||||
:tooltip="false"
|
||||
@@ -193,7 +193,7 @@
|
||||
vertical
|
||||
@update:value="(val) => player.setVolume(val)"
|
||||
/>
|
||||
<n-text class="slider-num">{{ playVolumePercentage }}%</n-text>
|
||||
<n-text class="slider-num">{{ statusStore.playVolumePercent }}%</n-text>
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 播放列表 -->
|
||||
@@ -219,7 +219,7 @@
|
||||
import type { DropdownOption } from "naive-ui";
|
||||
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
|
||||
import { secondsToTime, calculateCurrentTime } from "@/utils/time";
|
||||
import { renderIcon, isElectron } from "@/utils/helper";
|
||||
import { renderIcon, isElectron, coverLoaded } from "@/utils/helper";
|
||||
import { toLikeSong } from "@/utils/auth";
|
||||
import { openDownloadSong, openJumpArtist, openPlaylistAdd } from "@/utils/modal";
|
||||
import player from "@/utils/player";
|
||||
@@ -282,6 +282,20 @@ const songMoreOptions = computed<DropdownOption[]>(() => {
|
||||
props: { onClick: () => openDownloadSong(musicStore.playSong) },
|
||||
icon: renderIcon("Download"),
|
||||
},
|
||||
{
|
||||
key: "comment",
|
||||
label: "查看评论",
|
||||
show: !isLocal,
|
||||
props: {
|
||||
onClick: () => {
|
||||
statusStore.$patch({
|
||||
showFullPlayer: true,
|
||||
showPlayerComment: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
icon: renderIcon("Message"),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@@ -294,43 +308,6 @@ const sliderDragend = () => {
|
||||
player.play();
|
||||
};
|
||||
|
||||
// 封面加载完成
|
||||
const coverLoaded = (e: Event) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (target && target.nodeType === Node.ELEMENT_NODE) {
|
||||
target.style.opacity = "1";
|
||||
}
|
||||
};
|
||||
|
||||
// 当前音量百分比
|
||||
const playVolumePercentage = computed(() => {
|
||||
return Math.round(statusStore.playVolume * 100);
|
||||
});
|
||||
|
||||
// 当前音量图标
|
||||
const playVolumeIcon = computed(() => {
|
||||
const volume = statusStore.playVolume;
|
||||
return volume === 0
|
||||
? "VolumeOff"
|
||||
: volume < 0.4
|
||||
? "VolumeMute"
|
||||
: volume < 0.7
|
||||
? "VolumeDown"
|
||||
: "VolumeUp";
|
||||
});
|
||||
|
||||
// 当前播放模式图标
|
||||
const playModeIcon = computed(() => {
|
||||
const mode = statusStore.playSongMode;
|
||||
return statusStore.playHeartbeatMode
|
||||
? "HeartBit"
|
||||
: mode === "repeat"
|
||||
? "Repeat"
|
||||
: mode === "repeat-once"
|
||||
? "RepeatSong"
|
||||
: "Shuffle";
|
||||
});
|
||||
|
||||
// 是否展示歌词
|
||||
const isShowLyrics = computed(() => {
|
||||
const isHasLrc = musicStore.isHasLrc;
|
||||
@@ -353,12 +330,6 @@ const instantLyrics = computed(() => {
|
||||
? `${content?.content}( ${content?.tran} )`
|
||||
: content?.content;
|
||||
});
|
||||
|
||||
// 音量条鼠标滚动
|
||||
const changeVolume = (e: WheelEvent) => {
|
||||
const deltaY = e.deltaY;
|
||||
player.setVolume(deltaY > 0 ? "down" : "up");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -129,6 +129,9 @@ onMounted(() => {
|
||||
:deep(.n-scrollbar-content) {
|
||||
padding-right: 60px;
|
||||
}
|
||||
:deep(.n-skeleton) {
|
||||
background-color: rgba(var(--main-color), 0.08);
|
||||
}
|
||||
.comment-list {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -94,8 +94,28 @@
|
||||
</div>
|
||||
<!-- 播放模式 -->
|
||||
<div class="menu-icon" @click.stop="player.togglePlayMode(false)">
|
||||
<SvgIcon :name="playModeIcon" />
|
||||
<SvgIcon :name="statusStore.playModeIcon" />
|
||||
</div>
|
||||
<!-- 音量调节 -->
|
||||
<n-popover :show-arrow="false" :style="{ '--main-color': statusStore.mainColor }" raw>
|
||||
<template #trigger>
|
||||
<div class="menu-icon" @click.stop="player.toggleMute" @wheel="player.setVolume">
|
||||
<SvgIcon :name="statusStore.playVolumeIcon" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="volume-change" @wheel="player.setVolume">
|
||||
<n-slider
|
||||
v-model:value="statusStore.playVolume"
|
||||
:tooltip="false"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
vertical
|
||||
@update:value="(val) => player.setVolume(val)"
|
||||
/>
|
||||
<n-text class="slider-num">{{ statusStore.playVolumePercent }}%</n-text>
|
||||
</div>
|
||||
</n-popover>
|
||||
<!-- 播放列表 -->
|
||||
<div
|
||||
v-if="!statusStore.personalFmMode"
|
||||
@@ -121,18 +141,6 @@ const dataStore = useDataStore();
|
||||
const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
|
||||
// 当前播放模式图标
|
||||
const playModeIcon = computed(() => {
|
||||
const mode = statusStore.playSongMode;
|
||||
return statusStore.playHeartbeatMode
|
||||
? "HeartBit"
|
||||
: mode === "repeat"
|
||||
? "Repeat"
|
||||
: mode === "repeat-once"
|
||||
? "RepeatSong"
|
||||
: "Shuffle";
|
||||
});
|
||||
|
||||
// 进度条拖拽结束
|
||||
const sliderDragend = () => {
|
||||
const seek = calculateCurrentTime(statusStore.progress, statusStore.duration);
|
||||
@@ -259,11 +267,6 @@ const sliderDragend = () => {
|
||||
margin: 6px 8px;
|
||||
--n-handle-size: 12px;
|
||||
--n-rail-height: 4px;
|
||||
--n-rail-color: rgba(var(--main-color), 0.14);
|
||||
--n-rail-color-hover: rgba(var(--main-color), 0.3);
|
||||
--n-fill-color: rgb(var(--main-color));
|
||||
--n-handle-color: rgb(var(--main-color));
|
||||
--n-fill-color-hover: rgb(var(--main-color));
|
||||
}
|
||||
span {
|
||||
opacity: 0.6;
|
||||
@@ -277,4 +280,28 @@ const sliderDragend = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// volume
|
||||
.volume-change {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 64px;
|
||||
height: 200px;
|
||||
padding: 12px 16px;
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(var(--main-color), 0.14);
|
||||
.slider-num {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: rgb(var(--main-color));
|
||||
}
|
||||
}
|
||||
// slider
|
||||
.n-slider {
|
||||
--n-rail-color: rgba(var(--main-color), 0.14);
|
||||
--n-rail-color-hover: rgba(var(--main-color), 0.3);
|
||||
--n-fill-color: rgb(var(--main-color));
|
||||
--n-handle-color: rgb(var(--main-color));
|
||||
--n-fill-color-hover: rgb(var(--main-color));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -95,6 +95,13 @@
|
||||
</div>
|
||||
<n-switch class="set" v-model:value="settingStore.menuShowCover" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">开启页面缓存</n-text>
|
||||
<n-text class="tip" :depth="3">是否开启部分页面的缓存,这将会增加内存占用</n-text>
|
||||
</div>
|
||||
<n-switch class="set" v-model:value="settingStore.useKeepAlive" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">页面切换动画</n-text>
|
||||
|
||||
@@ -232,6 +232,13 @@
|
||||
</div>
|
||||
<n-switch v-model:value="settingStore.lyricsBlur" class="set" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">歌词排除内容</n-text>
|
||||
<n-text class="tip" :depth="3"> 歌词中包含的关键词将不会显示 </n-text>
|
||||
</div>
|
||||
<n-button type="primary" strong secondary @click="openLyricExclude">配置</n-button>
|
||||
</n-card>
|
||||
</div>
|
||||
<div class="set-list">
|
||||
<n-h3 prefix="bar"> Apple Music-like Lyrics </n-h3>
|
||||
@@ -328,6 +335,7 @@ import { useSettingStore, useStatusStore } from "@/stores";
|
||||
import { cloneDeep, isEqual } from "lodash-es";
|
||||
import { isElectron } from "@/utils/helper";
|
||||
import player from "@/utils/player";
|
||||
import { openLyricExclude } from "@/utils/modal";
|
||||
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
@@ -50,8 +50,7 @@ const userDB = localforage.createInstance({
|
||||
storeName: "user",
|
||||
});
|
||||
|
||||
export const useDataStore = defineStore({
|
||||
id: "data",
|
||||
export const useDataStore = defineStore("data", {
|
||||
state: (): ListState => ({
|
||||
// 播放列表
|
||||
playList: [],
|
||||
@@ -168,15 +167,15 @@ export const useDataStore = defineStore({
|
||||
}
|
||||
|
||||
// 在当前播放位置之后插入歌曲
|
||||
const indexAdd = index + 1
|
||||
this.playList.splice(indexAdd, 0, song)
|
||||
const indexAdd = index + 1;
|
||||
this.playList.splice(indexAdd, 0, song);
|
||||
// 移除重复的歌曲(如果存在)
|
||||
const playList = this.playList.filter((item,idx) => idx === indexAdd || item.id !== song.id);
|
||||
const playList = this.playList.filter((item, idx) => idx === indexAdd || item.id !== song.id);
|
||||
// 更新本地存储
|
||||
this.playList = playList;
|
||||
await musicDB.setItem("playList", cloneDeep(playList));
|
||||
// 返回刚刚插入的歌曲索引
|
||||
return playList.findIndex(item => item.id === song.id);
|
||||
return playList.findIndex((item) => item.id === song.id);
|
||||
},
|
||||
// 更改播放历史
|
||||
async setHistory(song: SongType) {
|
||||
@@ -304,6 +303,6 @@ export const useDataStore = defineStore({
|
||||
persist: {
|
||||
key: "data-store",
|
||||
storage: localStorage,
|
||||
paths: ["userLoginStatus", "loginType", "userData", "searchHistory", "catData"],
|
||||
pick: ["userLoginStatus", "loginType", "userData", "searchHistory", "catData"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,8 +34,7 @@ const defaultMusicData: SongType = {
|
||||
type: "song",
|
||||
};
|
||||
|
||||
export const useMusicStore = defineStore({
|
||||
id: "music",
|
||||
export const useMusicStore = defineStore("music", {
|
||||
state: (): MusicState => ({
|
||||
// 当前播放歌曲
|
||||
playSong: { ...defaultMusicData },
|
||||
@@ -86,7 +85,7 @@ export const useMusicStore = defineStore({
|
||||
actions: {
|
||||
// 恢复默认音乐数据
|
||||
resetMusicData() {
|
||||
this.playSong = {...defaultMusicData};
|
||||
this.playSong = { ...defaultMusicData };
|
||||
this.songLyric = {
|
||||
lrcData: [],
|
||||
yrcData: [],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { keywords } from "@/assets/data/exclude";
|
||||
|
||||
interface SettingState {
|
||||
themeMode: "light" | "dark" | "auto";
|
||||
@@ -83,10 +84,11 @@ interface SettingState {
|
||||
fullPlayerCache: boolean;
|
||||
scrobbleSong: boolean;
|
||||
dynamicCover: boolean;
|
||||
useKeepAlive: boolean;
|
||||
excludeKeywords: string[];
|
||||
}
|
||||
|
||||
export const useSettingStore = defineStore({
|
||||
id: "setting",
|
||||
export const useSettingStore = defineStore("setting", {
|
||||
state: (): SettingState => ({
|
||||
// 个性化
|
||||
themeMode: "auto", // 明暗模式
|
||||
@@ -108,6 +110,7 @@ export const useSettingStore = defineStore({
|
||||
checkUpdateOnStart: true, // 启动时检查更新
|
||||
preventSleep: false, // 是否禁止休眠
|
||||
fullPlayerCache: false, // 全屏播放器缓存
|
||||
useKeepAlive: true, // 使用 keep-alive
|
||||
// 播放
|
||||
songLevel: "exhigh", // 音质
|
||||
playDevice: "default", // 播放设备
|
||||
@@ -142,6 +145,7 @@ export const useSettingStore = defineStore({
|
||||
lyricsBlur: false, // 歌词模糊
|
||||
lyricsScrollPosition: "start", // 歌词滚动位置
|
||||
lrcMousePause: false, // 鼠标悬停暂停
|
||||
excludeKeywords: keywords, // 排除歌词关键字
|
||||
// 本地
|
||||
localFilesPath: [],
|
||||
localSeparators: ["/", "&"],
|
||||
|
||||
@@ -21,8 +21,7 @@ interface ShortcutStore {
|
||||
};
|
||||
}
|
||||
|
||||
export const useShortcutStore = defineStore({
|
||||
id: "shortcut",
|
||||
export const useShortcutStore = defineStore("shortcut", {
|
||||
state: (): ShortcutStore => ({
|
||||
// 全局快捷键开启
|
||||
globalOpen: true,
|
||||
|
||||
@@ -40,8 +40,7 @@ interface StatusState {
|
||||
personalFmMode: boolean;
|
||||
}
|
||||
|
||||
export const useStatusStore = defineStore({
|
||||
id: "status",
|
||||
export const useStatusStore = defineStore("status", {
|
||||
state: (): StatusState => ({
|
||||
// 菜单折叠状态
|
||||
menuCollapsed: false,
|
||||
@@ -101,13 +100,46 @@ export const useStatusStore = defineStore({
|
||||
// 播放器评论
|
||||
showPlayerComment: false,
|
||||
}),
|
||||
getters: {},
|
||||
getters: {
|
||||
// 播放音量图标
|
||||
playVolumeIcon(state) {
|
||||
const volume = state.playVolume;
|
||||
return volume === 0
|
||||
? "VolumeOff"
|
||||
: volume < 0.4
|
||||
? "VolumeMute"
|
||||
: volume < 0.7
|
||||
? "VolumeDown"
|
||||
: "VolumeUp";
|
||||
},
|
||||
// 播放模式图标
|
||||
playModeIcon(state) {
|
||||
const mode = state.playSongMode;
|
||||
return state.playHeartbeatMode
|
||||
? "HeartBit"
|
||||
: mode === "repeat"
|
||||
? "Repeat"
|
||||
: mode === "repeat-once"
|
||||
? "RepeatSong"
|
||||
: "Shuffle";
|
||||
},
|
||||
// 音量百分比
|
||||
playVolumePercent(state) {
|
||||
return Math.round(state.playVolume * 100);
|
||||
},
|
||||
// 播放器主色
|
||||
mainColor(state) {
|
||||
const mainColor = state.songCoverTheme?.main;
|
||||
if (!mainColor) return "239, 239, 239";
|
||||
return `${mainColor.r}, ${mainColor.g}, ${mainColor.b}`;
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
// 持久化
|
||||
persist: {
|
||||
key: "status-store",
|
||||
storage: localStorage,
|
||||
paths: [
|
||||
pick: [
|
||||
"menuCollapsed",
|
||||
"currentTime",
|
||||
"duration",
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { LyricLine, parseLrc, parseYrc } from "@applemusic-like-lyrics/lyric";
|
||||
import { keywords } from "@/assets/data/exclude";
|
||||
import type { LyricType } from "@/types/main";
|
||||
import { useMusicStore } from "@/stores";
|
||||
import { useMusicStore, useSettingStore } from "@/stores";
|
||||
import { msToS } from "./time";
|
||||
|
||||
// 歌词排除内容
|
||||
const getExcludeKeywords = () => {
|
||||
const settingStore = useSettingStore();
|
||||
return settingStore.excludeKeywords;
|
||||
};
|
||||
|
||||
// 恢复默认
|
||||
export const resetSongLyric = () => {
|
||||
const musicStore = useMusicStore();
|
||||
@@ -77,7 +82,7 @@ export const parseLrcData = (lrcData: LyricLine[]): LyricType[] => {
|
||||
const time = msToS(words[0].startTime);
|
||||
const content = words[0].word.trim();
|
||||
// 排除内容
|
||||
if (!content || keywords.some((keyword) => content.includes(keyword))) {
|
||||
if (!content || getExcludeKeywords().some((keyword) => content.includes(keyword))) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
@@ -113,7 +118,7 @@ export const parseYrcData = (yrcData: LyricLine[]): LyricType[] => {
|
||||
.map((word) => word.content + (word.endsWithSpace ? " " : ""))
|
||||
.join("");
|
||||
// 排除内容
|
||||
if (!contentStr || keywords.some((keyword) => contentStr.includes(keyword))) {
|
||||
if (!contentStr || getExcludeKeywords().some((keyword) => contentStr.includes(keyword))) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -16,6 +16,7 @@ import UpdatePlaylist from "@/components/Modal/UpdatePlaylist.vue";
|
||||
import DownloadSong from "@/components/Modal/DownloadSong.vue";
|
||||
import MainSetting from "@/components/Setting/MainSetting.vue";
|
||||
import UpdateApp from "@/components/Modal/UpdateApp.vue";
|
||||
import ExcludeKeywords from "@/components/Modal/ExcludeKeywords.vue";
|
||||
|
||||
// 用户协议
|
||||
export const openUserAgreement = () => {
|
||||
@@ -233,3 +234,17 @@ export const openUpdateApp = (data: UpdateInfoType) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 歌词排除内容
|
||||
export const openLyricExclude = () => {
|
||||
window.$modal.create({
|
||||
preset: "card",
|
||||
transformOrigin: "center",
|
||||
autoFocus: false,
|
||||
style: { width: "600px" },
|
||||
title: "歌词排除内容",
|
||||
content: () => {
|
||||
return h(ExcludeKeywords);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -758,15 +758,17 @@ class Player {
|
||||
}
|
||||
/**
|
||||
* 设置播放音量
|
||||
* @param volume 音量
|
||||
* @param actions 音量
|
||||
*/
|
||||
setVolume(volume: number | "up" | "down") {
|
||||
setVolume(actions: number | WheelEvent) {
|
||||
const statusStore = useStatusStore();
|
||||
// 直接设置
|
||||
if (typeof volume === "number") {
|
||||
volume = Math.max(0, Math.min(volume, 1));
|
||||
if (typeof actions === "number") {
|
||||
actions = Math.max(0, Math.min(actions, 1));
|
||||
} else {
|
||||
const increment = 0.05;
|
||||
const deltaY = actions.deltaY;
|
||||
const volume = deltaY > 0 ? "down" : "up";
|
||||
statusStore.playVolume = Math.max(
|
||||
0,
|
||||
Math.min(statusStore.playVolume + (volume === "up" ? increment : -increment), 1),
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
<!-- 路由 -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
|
||||
<KeepAlive>
|
||||
<KeepAlive v-if="settingStore.useKeepAlive">
|
||||
<component
|
||||
ref="componentRef"
|
||||
:is="Component"
|
||||
@@ -138,6 +138,14 @@
|
||||
@scroll="listScroll"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component
|
||||
v-else
|
||||
ref="componentRef"
|
||||
:is="Component"
|
||||
:id="artistId"
|
||||
class="router-view"
|
||||
@scroll="listScroll"
|
||||
/>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
<!-- 路由 -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
|
||||
<KeepAlive>
|
||||
<KeepAlive v-if="settingStore.useKeepAlive">
|
||||
<component :is="Component" class="router-view" />
|
||||
</KeepAlive>
|
||||
<component v-else :is="Component" class="router-view" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,12 @@
|
||||
</n-grid>
|
||||
<!-- 公共推荐 -->
|
||||
<div v-for="(item, index) in recData" :key="index" class="rec-public">
|
||||
<n-flex class="title" align="center" justify="space-between">
|
||||
<n-flex
|
||||
class="title"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
@click="router.push({ path: item.path ?? undefined })"
|
||||
>
|
||||
<n-h3 prefix="bar">
|
||||
<n-text>{{ item.name }}</n-text>
|
||||
<SvgIcon v-if="item.path" :size="26" name="Right" />
|
||||
@@ -194,7 +199,10 @@ const getAllRecData = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
onActivated(getAllRecData);
|
||||
onMounted(() => {
|
||||
getAllRecData();
|
||||
onActivated(getAllRecData);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -31,9 +31,10 @@
|
||||
<!-- 路由 -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
|
||||
<KeepAlive>
|
||||
<KeepAlive v-if="settingStore.useKeepAlive">
|
||||
<component :is="Component" class="router-view" />
|
||||
</KeepAlive>
|
||||
<component v-else :is="Component" class="router-view" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
|
||||
@@ -215,7 +215,12 @@
|
||||
import type { CoverType, SongType } from "@/types/main";
|
||||
import type { DropdownOption, MessageReactive } from "naive-ui";
|
||||
import { songDetail } from "@/api/song";
|
||||
import { playlistDetail, playlistAllSongs, deletePlaylist } from "@/api/playlist";
|
||||
import {
|
||||
playlistDetail,
|
||||
playlistAllSongs,
|
||||
deletePlaylist,
|
||||
updatePlaylistPrivacy,
|
||||
} from "@/api/playlist";
|
||||
import { formatCoverList, formatSongsList } from "@/utils/format";
|
||||
import { coverLoaded, formatNumber, fuzzySearch, renderIcon } from "@/utils/helper";
|
||||
import { renderToolbar } from "@/utils/meta";
|
||||
@@ -283,6 +288,7 @@ const moreOptions = computed<DropdownOption[]>(() => [
|
||||
label: "公开隐私歌单",
|
||||
key: "privacy",
|
||||
show: playlistDetailData.value?.privacy === 10,
|
||||
props: { onClick: openPrivacy },
|
||||
icon: renderIcon("ListLockOpen"),
|
||||
},
|
||||
{
|
||||
@@ -483,6 +489,23 @@ const updatePlaylist = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// 公开隐私歌单
|
||||
const openPrivacy = async () => {
|
||||
if (playlistDetailData.value?.privacy !== 10) return;
|
||||
window.$dialog.warning({
|
||||
title: "公开隐私歌单",
|
||||
content: "确认公开这个歌单?该操作无法撤销!",
|
||||
positiveText: "公开",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
const result = await updatePlaylistPrivacy(playlistId.value);
|
||||
if (result.code !== 200) return;
|
||||
if (playlistDetailData.value) playlistDetailData.value.privacy = 0;
|
||||
window.$message.success("歌单公开成功");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onBeforeRouteUpdate((to) => {
|
||||
const id = Number(to.query.id as string);
|
||||
if (id) {
|
||||
|
||||
@@ -82,9 +82,10 @@
|
||||
<!-- 路由 -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
|
||||
<KeepAlive>
|
||||
<KeepAlive v-if="settingStore.useKeepAlive">
|
||||
<component :is="Component" :data="listData" :loading="loading" class="router-view" />
|
||||
</KeepAlive>
|
||||
<component v-else :is="Component" :data="listData" :loading="loading" class="router-view" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
<!-- 目录管理 -->
|
||||
|
||||
@@ -16,16 +16,17 @@
|
||||
<!-- 路由 -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
|
||||
<KeepAlive>
|
||||
<KeepAlive v-if="settingStore.useKeepAlive">
|
||||
<component :is="Component" :keyword="searchKeyword" class="router-view" />
|
||||
</KeepAlive>
|
||||
<component v-else :is="Component" :keyword="searchKeyword" class="router-view" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from '@/stores';
|
||||
import { useSettingStore } from "@/stores";
|
||||
|
||||
const router = useRouter();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
Reference in New Issue
Block a user