Compare commits

...

22 Commits

Author SHA1 Message Date
imsyy
15b500806a feat: 完善自动登录 2024-12-12 11:37:29 +08:00
imsyy
9accf5d27d 🐞 fix: 修复音量调节 2024-12-11 17:57:56 +08:00
imsyy
191ab29a44 feat: 完善部分配置及页面 2024-12-11 17:12:59 +08:00
imsyy
a4d4cd5f70 feat: 支持软件内登录 2024-12-11 10:38:15 +08:00
imsyy
3b07f7346f feat: 新增副歌时间展示 2024-12-10 17:45:12 +08:00
imsyy
fbf261f80b Merge branch 'dev' of github.com:imsyy/SPlayer into dev 2024-12-02 16:02:13 +08:00
imsyy
432fa18299 🌈 style: 修改部分播放样式 #290 2024-12-02 16:02:02 +08:00
底层用户
b2dcec840b Merge pull request #301 from serious-snow/dev
refactor(music): 优化音乐播放逻辑和数据处理
2024-12-02 16:00:11 +08:00
imsyy
6f93d65f02 🔧 build: change version 2024-12-02 11:48:18 +08:00
imsyy
852b901353 🐞 fix: 修复歌单歌曲数量异常 #303 2024-12-02 11:38:21 +08:00
imsyy
71a282157d 🐞 fix: 修复下载路径分隔符 #302 2024-12-02 10:04:06 +08:00
wangjian
49f462cdf5 refactor(music): 优化音乐播放逻辑和数据处理
-改进 setNextPlaySong 方法,处理空播放列表的情况
-优化重复歌曲移除逻辑,确保插入位置正确
- 调整音乐数据重置方式,使用对象展开运算符
-增加播放器加载状态检查,避免未准备好时操作
-改进歌曲播放逻辑,处理添加到播放列表后立即播放的情况- 优化播放索引切换,考虑当前播放状态
-调整播放列表移除歌曲逻辑,优化用户体验
- 将初始 playIndex 设置为 -1,表示未播放状态
2024-11-29 14:28:22 +08:00
imsyy
94c0ca70e1 🐞 fix: 修复 snap 无法正常启动 #299 2024-11-27 14:30:24 +08:00
imsyy
813637762c 🔧 build: update dependency 2024-11-05 10:33:09 +08:00
imsyy
eb39b85a8b 🐞 fix: 修复启动失败 2024-11-05 09:49:39 +08:00
底层用户
201fd8d687 Merge pull request #288 from jcfun/dev
fix🐛: 修复了歌词翻译和音译开关不生效的bug
2024-11-04 18:17:53 +08:00
底层用户
02116d8f0f Merge branch 'dev' into dev 2024-11-04 18:16:49 +08:00
jcfun
551a190edf fix🐛: 修复了歌词翻译和音译开关不生效的bug 2024-11-04 12:29:27 +08:00
底层用户
f5015a4028 Merge pull request #279 from FrzMtrsprt/lyric-pos
🦄 refactor: 初始化桌面歌词至屏幕下方
2024-10-21 09:09:33 +08:00
FrzMtrsprt
ea55616bd6 🦄 refactor: 初始化桌面歌词至屏幕下方 2024-10-20 17:17:31 -04:00
imsyy
2b8eb93404 🐞 fix: fix some bugs #275 2024-10-18 15:50:24 +08:00
imsyy
18113d94e9 🐞 fix: 修复 UID 登录模式部分功能异常 2024-10-16 16:19:33 +08:00
60 changed files with 2309 additions and 1722 deletions

View File

@@ -20,7 +20,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20.x" node-version: "22.x"
# 复制环境变量文件 # 复制环境变量文件
- name: Copy .env.example - name: Copy .env.example
run: | run: |

View File

@@ -6,13 +6,13 @@ on:
push: push:
tags: tags:
- v* - v*
workflow_dispatch:
jobs: jobs:
# Windows # Windows
build-windows: build-windows:
name: Build for Windows name: Build for Windows
runs-on: windows-latest runs-on: windows-latest
timeout-minutes: 30
steps: steps:
# 检出 Git 仓库 # 检出 Git 仓库
- name: Check out git repository - name: Check out git repository
@@ -50,7 +50,6 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
with: with:
draft: false
files: dist/*.* files: dist/*.*
env: env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
@@ -58,7 +57,6 @@ jobs:
build-macos: build-macos:
name: Build for macOS name: Build for macOS
runs-on: macos-latest runs-on: macos-latest
timeout-minutes: 30
steps: steps:
# 检出 Git 仓库 # 检出 Git 仓库
- name: Check out git repository - name: Check out git repository
@@ -97,7 +95,6 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
with: with:
draft: false
files: dist/*.* files: dist/*.*
env: env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
@@ -105,7 +102,6 @@ jobs:
build-linux: build-linux:
name: Build for Linux name: Build for Linux
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
timeout-minutes: 30
steps: steps:
# 检出 Git 仓库 # 检出 Git 仓库
- name: Check out git repository - name: Check out git repository
@@ -124,6 +120,11 @@ jobs:
sudo apt-get install --no-install-recommends -y rpm && sudo apt-get install --no-install-recommends -y rpm &&
sudo apt-get install --no-install-recommends -y libarchive-tools && sudo apt-get install --no-install-recommends -y libarchive-tools &&
sudo apt-get install --no-install-recommends -y libopenjp2-tools sudo apt-get install --no-install-recommends -y libopenjp2-tools
# 安装 Snapcraft
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v2
with:
snapcraft_token: ${{ secrets.SNAPCRAFT_TOKEN }}
# 复制环境变量文件 # 复制环境变量文件
- name: Copy .env.example - name: Copy .env.example
run: | run: |
@@ -141,6 +142,11 @@ jobs:
shell: bash shell: bash
env: env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
# 上传 Snap 包到 Snapcraft 商店
- name: Publish Snap to Snap Store
run: snapcraft upload dist/*.snap --release stable
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
# 上传构建产物 # 上传构建产物
- name: Upload Linux artifact - name: Upload Linux artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -153,7 +159,6 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
with: with:
draft: false
files: dist/*.* files: dist/*.*
env: env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}

2
.gitignore vendored
View File

@@ -28,3 +28,5 @@ components.d.ts
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.env.development
.env.production

View File

@@ -117,6 +117,10 @@
[Dev Workflow](https://github.com/imsyy/SPlayer/actions/workflows/build.yml) [Dev Workflow](https://github.com/imsyy/SPlayer/actions/workflows/build.yml)
## Snap Store
[![Get it from the Snap Store](https://snapcraft.io/en/dark/install.svg)](https://snapcraft.io/splayer)
## ⚙️ Docker 部署 ## ⚙️ Docker 部署
> 安装及配置 `Docker` 将不在此处说明,请自行解决 > 安装及配置 `Docker` 将不在此处说明,请自行解决

2
components.d.ts vendored
View File

@@ -17,6 +17,7 @@ declare module 'vue' {
CoverMenu: typeof import('./src/components/Menu/CoverMenu.vue')['default'] CoverMenu: typeof import('./src/components/Menu/CoverMenu.vue')['default']
CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default'] CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default']
DownloadSong: typeof import('./src/components/Modal/DownloadSong.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'] FullPlayer: typeof import('./src/components/Player/FullPlayer.vue')['default']
GeneralSetting: typeof import('./src/components/Setting/GeneralSetting.vue')['default'] GeneralSetting: typeof import('./src/components/Setting/GeneralSetting.vue')['default']
JumpArtist: typeof import('./src/components/Modal/JumpArtist.vue')['default'] JumpArtist: typeof import('./src/components/Modal/JumpArtist.vue')['default']
@@ -54,6 +55,7 @@ declare module 'vue' {
NDrawer: typeof import('naive-ui')['NDrawer'] NDrawer: typeof import('naive-ui')['NDrawer']
NDrawerContent: typeof import('naive-ui')['NDrawerContent'] NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDropdown: typeof import('naive-ui')['NDropdown'] NDropdown: typeof import('naive-ui')['NDropdown']
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
NEllipsis: typeof import('naive-ui')['NEllipsis'] NEllipsis: typeof import('naive-ui')['NEllipsis']
NEmpty: typeof import('naive-ui')['NEmpty'] NEmpty: typeof import('naive-ui')['NEmpty']
NFlex: typeof import('naive-ui')['NFlex'] NFlex: typeof import('naive-ui')['NFlex']

View File

@@ -84,7 +84,7 @@ dmg:
# Linux 平台配置 # Linux 平台配置
linux: linux:
# 可执行文件名 # 可执行文件名
executableName: SPlayer executableName: splayer
# 应用程序的图标文件路径 # 应用程序的图标文件路径
icon: public/icons/favicon-512x512.png icon: public/icons/favicon-512x512.png
# 构建类型 # 构建类型
@@ -93,6 +93,7 @@ linux:
- AppImage - AppImage
- deb - deb
- rpm - rpm
- snap
- tar.gz - tar.gz
# 维护者信息 # 维护者信息
maintainer: imsyy.top maintainer: imsyy.top

View File

@@ -6,10 +6,11 @@ import { isDev, isMac, appName } from "./utils";
import { registerAllShortcuts, unregisterShortcuts } from "./shortcut"; import { registerAllShortcuts, unregisterShortcuts } from "./shortcut";
import { initTray, MainTray } from "./tray"; import { initTray, MainTray } from "./tray";
import { initThumbar, Thumbar } from "./thumbar"; import { initThumbar, Thumbar } from "./thumbar";
import { type StoreType, initStore } from "./store";
import Store from "electron-store";
import initAppServer from "../server"; import initAppServer from "../server";
import initIpcMain from "./ipcMain"; import initIpcMain from "./ipcMain";
import log from "./logger"; import log from "./logger";
import store from "./store";
// icon // icon
import icon from "../../public/icons/favicon.png?asset"; import icon from "../../public/icons/favicon.png?asset";
@@ -29,6 +30,8 @@ class MainProcess {
mainWindow: BrowserWindow | null = null; mainWindow: BrowserWindow | null = null;
lyricWindow: BrowserWindow | null = null; lyricWindow: BrowserWindow | null = null;
loadingWindow: BrowserWindow | null = null; loadingWindow: BrowserWindow | null = null;
// store
store: Store<StoreType> | null = null;
// 托盘 // 托盘
mainTray: MainTray | null = null; mainTray: MainTray | null = null;
// 工具栏 // 工具栏
@@ -38,7 +41,7 @@ class MainProcess {
constructor() { constructor() {
log.info("🚀 Main process startup"); log.info("🚀 Main process startup");
// 禁用 Windows 7 的 GPU 加速功能 // 禁用 Windows 7 的 GPU 加速功能
if (release().startsWith("6.1") && type() == 'Windows_NT') app.disableHardwareAcceleration(); if (release().startsWith("6.1") && type() == "Windows_NT") app.disableHardwareAcceleration();
// 单例锁 // 单例锁
if (!app.requestSingleInstanceLock()) { if (!app.requestSingleInstanceLock()) {
log.error("❌ There is already a program running and this process is terminated"); log.error("❌ There is already a program running and this process is terminated");
@@ -46,10 +49,12 @@ class MainProcess {
process.exit(0); process.exit(0);
} else this.showWindow(); } else this.showWindow();
// 准备就绪 // 准备就绪
app.whenReady().then(async () => { app.on("ready", async () => {
log.info("🚀 Application Process Startup"); log.info("🚀 Application Process Startup");
// 设置应用程序名称 // 设置应用程序名称
electronApp.setAppUserModelId(app.getName()); electronApp.setAppUserModelId("com.imsyy.splayer");
// 初始化 store
this.store = initStore();
// 启动主服务进程 // 启动主服务进程
await initAppServer(); await initAppServer();
// 启动进程 // 启动进程
@@ -68,7 +73,7 @@ class MainProcess {
this.loadingWindow, this.loadingWindow,
this.mainTray, this.mainTray,
this.thumbar, this.thumbar,
store, this.store,
); );
// 注册快捷键 // 注册快捷键
registerAllShortcuts(this.mainWindow!); registerAllShortcuts(this.mainWindow!);
@@ -111,8 +116,8 @@ class MainProcess {
createMainWindow() { createMainWindow() {
// 窗口配置项 // 窗口配置项
const options: BrowserWindowConstructorOptions = { const options: BrowserWindowConstructorOptions = {
width: store.get("window").width, width: this.store?.get("window").width,
height: store.get("window").height, height: this.store?.get("window").height,
minHeight: 800, minHeight: 800,
minWidth: 1280, minWidth: 1280,
// 菜单栏 // 菜单栏
@@ -132,8 +137,8 @@ class MainProcess {
} }
// 配置网络代理 // 配置网络代理
if (store.get("proxy")) { if (this.store?.get("proxy")) {
this.mainWindow.webContents.session.setProxy({ proxyRules: store.get("proxy") }); this.mainWindow.webContents.session.setProxy({ proxyRules: this.store?.get("proxy") });
} }
// 窗口打开处理程序 // 窗口打开处理程序
@@ -162,15 +167,15 @@ class MainProcess {
createLyricsWindow() { createLyricsWindow() {
// 初始化窗口 // 初始化窗口
this.lyricWindow = this.createWindow({ this.lyricWindow = this.createWindow({
width: store.get("lyric").width || 800, width: this.store?.get("lyric").width || 800,
height: store.get("lyric").height || 180, height: this.store?.get("lyric").height || 180,
minWidth: 440, minWidth: 440,
minHeight: 120, minHeight: 120,
maxWidth: 1600, maxWidth: 1600,
maxHeight: 300, maxHeight: 300,
// 窗口位置 // 窗口位置
x: store.get("lyric").x, x: this.store?.get("lyric").x,
y: store.get("lyric").y, y: this.store?.get("lyric").y,
transparent: true, transparent: true,
backgroundColor: "rgba(0, 0, 0, 0)", backgroundColor: "rgba(0, 0, 0, 0)",
alwaysOnTop: true, alwaysOnTop: true,
@@ -236,6 +241,10 @@ class MainProcess {
} }
// 窗口事件 // 窗口事件
handleWindowEvents() { handleWindowEvents() {
this.mainWindow?.on("ready-to-show", () => {
if (!this.mainWindow) return;
this.thumbar = initThumbar(this.mainWindow);
});
this.mainWindow?.on("show", () => { this.mainWindow?.on("show", () => {
// this.mainWindow?.webContents.send("lyricsScroll"); // this.mainWindow?.webContents.send("lyricsScroll");
}); });
@@ -257,7 +266,7 @@ class MainProcess {
const bounds = this.lyricWindow?.getBounds(); const bounds = this.lyricWindow?.getBounds();
if (bounds) { if (bounds) {
const { width, height } = bounds; const { width, height } = bounds;
store.set("lyric", { ...store.get("lyric"), width, height }); this.store?.set("lyric", { ...this.store?.get("lyric"), width, height });
} }
}); });
@@ -275,7 +284,7 @@ class MainProcess {
saveBounds() { saveBounds() {
if (this.mainWindow?.isFullScreen()) return; if (this.mainWindow?.isFullScreen()) return;
const bounds = this.mainWindow?.getBounds(); const bounds = this.mainWindow?.getBounds();
if (bounds) store.set("window", bounds); if (bounds) this.store?.set("window", bounds);
} }
// 显示窗口 // 显示窗口
showWindow() { showWindow() {

View File

@@ -24,6 +24,7 @@ import fs from "fs/promises";
import log from "../main/logger"; import log from "../main/logger";
import Store from "electron-store"; import Store from "electron-store";
import fg from "fast-glob"; import fg from "fast-glob";
import openLoginWin from "./loginWin";
// 注册 ipcMain // 注册 ipcMain
const initIpcMain = ( const initIpcMain = (
@@ -535,6 +536,9 @@ const initWinIpcMain = (
// 开始下载更新 // 开始下载更新
ipcMain.on("start-download-update", () => startDownloadUpdate()); ipcMain.on("start-download-update", () => startDownloadUpdate());
// 新建窗口
ipcMain.on("open-login-web", () => openLoginWin(win!));
}; };
// lyric // lyric
@@ -666,6 +670,10 @@ const initTrayIpcMain = (
// thumbar // thumbar
const initThumbarIpcMain = (thumbar: Thumbar | null): void => { const initThumbarIpcMain = (thumbar: Thumbar | null): void => {
if (!thumbar) return; if (!thumbar) return;
// 更新工具栏
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
thumbar?.updateThumbar(playStatus);
});
}; };
// store // store

123
electron/main/loginWin.ts Normal file
View File

@@ -0,0 +1,123 @@
import {
BrowserWindow,
MenuItemConstructorOptions,
Menu,
session,
dialog,
ipcMain,
} from "electron";
import icon from "../../public/icons/favicon.png?asset";
const openLoginWin = (mainWin: BrowserWindow) => {
const loginSession = session.fromPartition("login-win");
const loginWin = new BrowserWindow({
parent: mainWin,
title: "登录网易云音乐",
width: 1280,
height: 800,
center: true,
modal: true,
icon,
// resizable: false,
// movable: false,
// minimizable: false,
// maximizable: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
partition: "login-win",
},
});
// 打开网易云
loginWin.loadURL("https://music.163.com/#/my/");
// 阻止新窗口创建
loginWin.webContents.setWindowOpenHandler(() => {
return { action: "deny" };
});
// 登录完成
const loginFinish = async () => {
if (!loginWin) return;
// 获取 Cookie
const cookies = await loginWin.webContents.session.cookies.get({ name: "MUSIC_U" });
if (!cookies?.[0]?.value) {
dialog.showMessageBox({
type: "info",
title: "登录失败",
message: "未查找到登录信息,请重试",
});
return;
}
const value = `MUSIC_U=${cookies[0].value};`;
// 发送回主进程
mainWin?.webContents.send("send-cookies", value);
await loginSession?.clearStorageData();
loginWin.close();
};
// 页面注入
// loginWin.webContents.once("did-finish-load", () => {
// const script = `
// const style = document.createElement('style');
// style.innerHTML = \`
// .login-btn {
// position: fixed;
// left: 0;
// bottom: 0;
// width: 100%;
// height: 80px;
// display: flex;
// align-items: center;
// justify-content: center;
// background-color: #242424;
// z-index: 99999;
// }
// .login-btn span {
// color: white;
// margin-right: 20px;
// }
// .login-btn button {
// border: none;
// outline: none;
// background-color: #c20c0c;
// border-radius: 25px;
// color: white;
// height: 40px;
// padding: 0 20px;
// cursor: pointer;
// }
// \`;
// document.head.appendChild(style);
// const div = document.createElement('div');
// div.className = 'login-btn';
// div.innerHTML = \`
// <span>请在登录成功后点击</span>
// <button>登录完成</button>
// \`;
// div.querySelector('button').addEventListener('click', () => {
// window.electron.ipcRenderer.send("login-success");
// });
// document.body.appendChild(div);
// `;
// loginWin.webContents.executeJavaScript(script);
// });
// 监听事件
ipcMain.on("login-success", loginFinish);
// 菜单栏
const menuTemplate: MenuItemConstructorOptions[] = [
{
label: "登录完成",
click: loginFinish,
},
];
const menu = Menu.buildFromTemplate(menuTemplate);
loginWin.setMenu(menu);
};
export default openLoginWin;

View File

@@ -1,4 +1,5 @@
import Store from "electron-store"; import Store from "electron-store";
import { screen } from "electron";
import log from "./logger"; import log from "./logger";
log.info("🌱 Store init"); log.info("🌱 Store init");
@@ -24,23 +25,23 @@ export interface StoreType {
} }
// 初始化仓库 // 初始化仓库
const store = new Store<StoreType>({ export const initStore = () => {
defaults: { return new Store<StoreType>({
window: { defaults: {
width: 1280, window: {
height: 800, width: 1280,
height: 800,
},
lyric: {
fontSize: 30,
mainColor: "#fff",
shadowColor: "rgba(0, 0, 0, 0.5)",
x: screen.getPrimaryDisplay().workAreaSize.width / 2 - 400,
y: screen.getPrimaryDisplay().workAreaSize.height - 90,
width: 800,
height: 180,
},
proxy: "",
}, },
lyric: { });
fontSize: 30, };
mainColor: "#fff",
shadowColor: "rgba(0, 0, 0, 0.5)",
x: 0,
y: 0,
width: 800,
height: 180,
},
proxy: "",
},
});
export default store;

View File

@@ -14,6 +14,7 @@ type ThumbarMap = Map<ThumbarKeys, ThumbarButton>;
export interface Thumbar { export interface Thumbar {
clearThumbar(): void; clearThumbar(): void;
updateThumbar(playing: boolean, clean?: boolean): void;
} }
// 工具栏图标 // 工具栏图标
@@ -32,12 +33,12 @@ const createThumbarButtons = (win: BrowserWindow): ThumbarMap => {
.set(ThumbarKeys.Prev, { .set(ThumbarKeys.Prev, {
tooltip: "上一曲", tooltip: "上一曲",
icon: thumbarIcon("prev"), icon: thumbarIcon("prev"),
click: () => win.webContents.send("play-prev"), click: () => win.webContents.send("playPrev"),
}) })
.set(ThumbarKeys.Next, { .set(ThumbarKeys.Next, {
tooltip: "下一曲", tooltip: "下一曲",
icon: thumbarIcon("next"), icon: thumbarIcon("next"),
click: () => win.webContents.send("play-next"), click: () => win.webContents.send("playNext"),
}) })
.set(ThumbarKeys.Play, { .set(ThumbarKeys.Play, {
tooltip: "播放", tooltip: "播放",
@@ -47,7 +48,7 @@ const createThumbarButtons = (win: BrowserWindow): ThumbarMap => {
.set(ThumbarKeys.Pause, { .set(ThumbarKeys.Pause, {
tooltip: "暂停", tooltip: "暂停",
icon: thumbarIcon("pause"), icon: thumbarIcon("pause"),
click: () => win.webContents.send("play-pause"), click: () => win.webContents.send("pause"),
}); });
}; };
@@ -75,7 +76,7 @@ class createThumbar implements Thumbar {
this.updateThumbar(); this.updateThumbar();
} }
// 更新工具栏 // 更新工具栏
private updateThumbar(playing: boolean = false, clean: boolean = false) { updateThumbar(playing: boolean = false, clean: boolean = false) {
if (clean) return this.clearThumbar(); if (clean) return this.clearThumbar();
this._win.setThumbarButtons([this._prev, playing ? this._pause : this._play, this._next]); this._win.setThumbarButtons([this._prev, playing ? this._pause : this._play, this._next]);
} }

14
electron/server/port.ts Normal file
View File

@@ -0,0 +1,14 @@
import getPort from "get-port";
// 默认端口
let webPort: number;
let servePort: number;
const getSafePort = async () => {
if (webPort && servePort) return { webPort, servePort };
webPort = await getPort({ port: 14558 });
servePort = await getPort({ port: 25884 });
return { webPort, servePort };
};
export default getSafePort;

View File

@@ -1,7 +1,7 @@
{ {
"name": "splayer", "name": "splayer",
"productName": "SPlayer", "productName": "SPlayer",
"version": "3.0.0-alpha.4", "version": "3.0.0-beta.1",
"description": "A minimalist music player", "description": "A minimalist music player",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "imsyy", "author": "imsyy",
@@ -47,29 +47,31 @@
"@pixi/filter-bulge-pinch": "^5.1.1", "@pixi/filter-bulge-pinch": "^5.1.1",
"@pixi/filter-color-matrix": "^7.4.2", "@pixi/filter-color-matrix": "^7.4.2",
"@pixi/sprite": "^7.4.2", "@pixi/sprite": "^7.4.2",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^12.0.0",
"NeteaseCloudMusicApi": "^4.23.1", "NeteaseCloudMusicApi": "^4.25.0",
"axios": "^1.7.7", "axios": "^1.7.9",
"change-case": "^5.4.4", "change-case": "^5.4.4",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"electron-dl": "^3.5.2", "electron-dl": "^4.0.0",
"electron-store": "^8.2.0", "electron-store": "^8.2.0",
"electron-updater": "^6.3.9", "electron-updater": "^6.3.9",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"font-list": "^1.5.1", "font-list": "^1.5.1",
"github-markdown-css": "^5.7.0", "get-port": "^7.1.0",
"github-markdown-css": "^5.8.1",
"howler": "^2.2.4", "howler": "^2.2.4",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jss": "^10.10.0", "jss": "^10.10.0",
"jss-preset-default": "^10.10.0", "jss-preset-default": "^10.10.0",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"marked": "^14.1.2", "marked": "^14.1.4",
"md5": "^2.3.0",
"music-metadata": "7.14.0", "music-metadata": "7.14.0",
"pinia": "^2.2.4", "pinia": "^2.3.0",
"pinia-plugin-persistedstate": "^3.2.3", "pinia-plugin-persistedstate": "^4.1.3",
"plyr": "^3.7.8", "plyr": "^3.7.8",
"vue-virt-list": "^1.5.2" "vue-virt-list": "^1.5.5"
}, },
"devDependencies": { "devDependencies": {
"@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/preload": "^3.0.1",
@@ -82,33 +84,40 @@
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/howler": "^2.2.12", "@types/howler": "^2.2.12",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^22.7.5", "@types/md5": "^2.3.5",
"@typescript-eslint/eslint-plugin": "^8.8.1", "@types/node": "^22.10.1",
"@typescript-eslint/parser": "^8.8.1", "@typescript-eslint/eslint-plugin": "^8.18.0",
"@vitejs/plugin-vue": "^5.1.4", "@typescript-eslint/parser": "^8.18.0",
"@vitejs/plugin-vue": "^5.2.1",
"ajv": "^8.17.1", "ajv": "^8.17.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"electron": "^28.3.3", "electron": "^30.5.1",
"electron-builder": "^25.1.7", "electron-builder": "^25.1.8",
"electron-log": "^5.2.0", "electron-log": "^5.2.4",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"eslint": "^9.12.0", "eslint": "^9.16.0",
"eslint-plugin-vue": "^9.29.0", "eslint-plugin-vue": "^9.32.0",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"fastify": "^4.28.1", "fastify": "^4.29.0",
"naive-ui": "^2.40.1", "naive-ui": "^2.40.3",
"node-taglib-sharp": "^5.2.3", "node-taglib-sharp": "^5.2.3",
"prettier": "^3.3.3", "prettier": "^3.4.2",
"sass": "^1.79.5", "sass": "^1.82.0",
"terser": "^5.34.1", "terser": "^5.37.0",
"typescript": "^5.6.3", "typescript": "5.6.2",
"unplugin-auto-import": "^0.18.3", "unplugin-auto-import": "^0.18.6",
"unplugin-vue-components": "^0.27.4", "unplugin-vue-components": "^0.27.5",
"vite": "^5.4.8", "vite": "^5.4.11",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-wasm": "^3.3.0", "vite-plugin-wasm": "^3.3.0",
"vue": "3.5.10", "vue": "^3.5.13",
"vue-router": "^4.4.5", "vue-router": "^4.5.0",
"vue-tsc": "^2.1.6" "vue-tsc": "2.0.29"
},
"pnpm": {
"overrides": {
"dmg-builder": "25.1.8",
"electron-builder-squirrel-windows": "25.1.8"
}
} }
} }

2912
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/icons/logo-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -55,9 +55,10 @@
<!-- 路由页面 --> <!-- 路由页面 -->
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in"> <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" /> <component :is="Component" class="router-view" />
</KeepAlive> </KeepAlive>
<component v-else :is="Component" class="router-view" />
</Transition> </Transition>
</RouterView> </RouterView>
<!-- 回顶 --> <!-- 回顶 -->

View File

@@ -114,10 +114,20 @@ export const matchSong = (
* 歌曲动态封面 * 歌曲动态封面
* @param {number} id - 歌曲 id * @param {number} id - 歌曲 id
*/ */
export const songDynamicCover = (id: number) => { export const songDynamicCover = (id: number) => {
return request({ return request({
url: "/song/dynamic/cover", url: "/song/dynamic/cover",
params: { id }, params: { id },
}); });
}; };
/**
* 副歌时间
* @param {number} id - 歌曲 id
*/
export const songChorus = (id: number) => {
return request({
url: "/song/chorus",
params: { id },
});
};

View File

@@ -98,7 +98,7 @@
<n-text class="ar"> {{ song.artists || "未知艺术家" }} </n-text> <n-text class="ar"> {{ song.artists || "未知艺术家" }} </n-text>
</div> </div>
<!-- 别名 --> <!-- 别名 -->
<n-text v-if="song.alia" class="alia" depth="3">{{ song.alia }}</n-text> <n-text v-if="song.alia" class="alia text-hidden" depth="3">{{ song.alia }}</n-text>
</div> </div>
</div> </div>
<!-- 专辑 --> <!-- 专辑 -->
@@ -204,10 +204,10 @@ const localCover = async (show: boolean) => {
border-radius: 12px; border-radius: 12px;
border: 2px solid rgba(var(--primary), 0.12); border: 2px solid rgba(var(--primary), 0.12);
background-color: var(--surface-container-hex); background-color: var(--surface-container-hex);
transition: // transition:
transform 0.1s, // transform 0.1s,
background-color 0.3s var(--n-bezier), // background-color 0.3s var(--n-bezier),
border-color 0.3s var(--n-bezier); // border-color 0.3s var(--n-bezier);
&.play { &.play {
border-color: rgba(var(--primary), 0.58); border-color: rgba(var(--primary), 0.58);
background-color: rgba(var(--primary), 0.28); background-color: rgba(var(--primary), 0.28);
@@ -253,6 +253,9 @@ const localCover = async (show: boolean) => {
transition: transition:
opacity 0.3s, opacity 0.3s,
transform 0.3s; transform 0.3s;
:deep(.svg-container) {
color: var(--primary-hex);
}
} }
.status, .status,
.play { .play {

View File

@@ -122,7 +122,7 @@ const menuOptions = computed<MenuOption[] | MenuGroupOption[]>(() => {
key: "cloud", key: "cloud",
link: "cloud", link: "cloud",
label: "我的云盘", label: "我的云盘",
show: isElectron && dataStore.loginType !== "uid", show: isLogin() === 1,
icon: renderIcon("Cloud"), icon: renderIcon("Cloud"),
}, },
{ {

View File

@@ -281,9 +281,11 @@ onBeforeUnmount(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.song-list { .song-list {
height: 100%; height: 100%;
border-radius: 12px 0 0 12px;
overflow: hidden;
.song-card { .song-card {
padding-bottom: 12px; padding-bottom: 12px;
padding-right: 4px; // padding-right: 4px;
} }
// 悬浮顶栏 // 悬浮顶栏
.list-header { .list-header {
@@ -293,8 +295,8 @@ onBeforeUnmount(() => {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 8px 18px 8px 12px; padding: 8px 12px;
margin-right: 4px; // margin-right: 4px;
border: 1px solid transparent; border: 1px solid transparent;
background-color: var(--background-hex); background-color: var(--background-hex);
.n-text { .n-text {

View File

@@ -27,7 +27,7 @@ import {
openPlaylistAdd, openPlaylistAdd,
openSongInfoEditor, openSongInfoEditor,
} from "@/utils/modal"; } from "@/utils/modal";
import { deleteSongs } from "@/utils/auth"; import { deleteSongs, isLogin } from "@/utils/auth";
import { songUrl } from "@/api/song"; import { songUrl } from "@/api/song";
import player from "@/utils/player"; import player from "@/utils/player";
@@ -64,6 +64,7 @@ const openDropdown = (
const isHasMv = !!song?.mv && song.mv !== 0; const isHasMv = !!song?.mv && song.mv !== 0;
const isCloud = router.currentRoute.value.name === "cloud"; const isCloud = router.currentRoute.value.name === "cloud";
const isLocal = !!song?.path; const isLocal = !!song?.path;
const isLoginNormal = isLogin() === 1;
// 是否当前播放 // 是否当前播放
const isCurrent = statusStore.playIndex === index; const isCurrent = statusStore.playIndex === index;
// 是否为用户歌单 // 是否为用户歌单
@@ -169,7 +170,7 @@ const openDropdown = (
{ {
key: "cloud-import", key: "cloud-import",
label: "导入至云盘", label: "导入至云盘",
show: !isCloud && type === "song" && !isLocal, show: !isCloud && isLoginNormal && type === "song" && !isLocal,
props: { props: {
onClick: () => importSongToCloud(song), onClick: () => importSongToCloud(song),
}, },
@@ -178,7 +179,7 @@ const openDropdown = (
{ {
key: "delete", key: "delete",
label: "从歌单中删除", label: "从歌单中删除",
show: isUserPlaylist && !isCloud, show: isUserPlaylist && isLoginNormal && !isCloud,
props: { props: {
onClick: () => deleteSongs(playListId!, [song.id], () => emit("removeSong", [song.id])), onClick: () => deleteSongs(playListId!, [song.id], () => emit("removeSong", [song.id])),
}, },

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="create-playlist"> <div class="create-playlist">
<n-tabs v-model:value="playlistType" type="segment" animated> <n-tabs v-model:value="playlistType" type="segment" animated>
<n-tab-pane name="online" tab="在线歌单"> <n-tab-pane :disabled="isLogin() !== 1" name="online" tab="在线歌单">
<n-form ref="onlineFormRef" :model="onlineFormData" :rules="onlineFormRules"> <n-form ref="onlineFormRef" :model="onlineFormData" :rules="onlineFormRules">
<n-form-item label="歌单名称" path="name"> <n-form-item label="歌单名称" path="name">
<n-input v-model:value="onlineFormData.name" placeholder="请输入歌单名称" /> <n-input v-model:value="onlineFormData.name" placeholder="请输入歌单名称" />
@@ -28,7 +28,7 @@ import { useDataStore } from "@/stores";
import { textRule } from "@/utils/rules"; import { textRule } from "@/utils/rules";
import { debounce } from "lodash-es"; import { debounce } from "lodash-es";
import { createPlaylist } from "@/api/playlist"; import { createPlaylist } from "@/api/playlist";
import { updateUserLikePlaylist } from "@/utils/auth"; import { isLogin, updateUserLikePlaylist } from "@/utils/auth";
const emit = defineEmits<{ close: [] }>(); const emit = defineEmits<{ close: [] }>();
@@ -42,7 +42,7 @@ interface OnlineFormType {
const dataStore = useDataStore(); const dataStore = useDataStore();
// 歌单类别 // 歌单类别
const playlistType = ref<"online" | "local">("online"); const playlistType = ref<"online" | "local">(isLogin() === 1 ? "online" : "local");
// 在线歌单数据 // 在线歌单数据
const onlineFormRef = ref<FormInst | null>(null); const onlineFormRef = ref<FormInst | null>(null);

View File

@@ -31,7 +31,7 @@
</n-flex> </n-flex>
</n-radio-group> </n-radio-group>
</n-collapse-item> </n-collapse-item>
<n-collapse-item v-if="isElectron" title="下载路径" name="path"> <n-collapse-item v-if="isElectron" title="本次下载路径" name="path">
<n-input-group> <n-input-group>
<n-input :value="downloadPath || '未配置下载目录'" disabled> <n-input :value="downloadPath || '未配置下载目录'" disabled>
<template #prefix> <template #prefix>
@@ -137,7 +137,7 @@ const changeDownloadPath = async () => {
const download = async () => { const download = async () => {
if (!songData.value) return; if (!songData.value) return;
loading.value = true; loading.value = true;
downloadPath.value = settingStore.downloadPath; if (settingStore.downloadPath) downloadPath.value = settingStore.downloadPath;
try { try {
// 获取下载链接 // 获取下载链接
const result = await songDownloadUrl(props.id, songLevelChoosed.value); const result = await songDownloadUrl(props.id, songLevelChoosed.value);
@@ -184,7 +184,7 @@ const electronDownload = async (url: string, songName: string, fileType: string)
} }
// 下载歌曲 // 下载歌曲
const config = { const config = {
fileName: songName, fileName: songName.replace(/[/:*?"<>|]/g, "&"),
fileType, fileType,
path: downloadPath.value, path: downloadPath.value,
downloadMeta, downloadMeta,

View 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>

View File

@@ -6,7 +6,7 @@
</template> </template>
可在官方的 可在官方的
<n-a href="https://music.163.com/" target="_blank">网页端</n-a> <n-a href="https://music.163.com/" target="_blank">网页端</n-a>
和客户端的控制台中获取只需要 Cookie 中的 <code>MUSIC_U</code> 字段即可例如 或点击下方的自动获取只需要 Cookie 中的 <code>MUSIC_U</code> 字段即可例如
<code>MUSIC_U=00C7...;</code><br />请注意必须以 <code>;</code> 结束 <code>MUSIC_U=00C7...;</code><br />请注意必须以 <code>;</code> 结束
</n-alert> </n-alert>
<n-input <n-input
@@ -15,12 +15,16 @@
type="textarea" type="textarea"
placeholder="请输入 Cookie" placeholder="请输入 Cookie"
/> />
<n-button type="primary" @click="login">登录</n-button> <n-flex class="menu">
<n-button v-if="isElectron" type="primary" @click="openWeb">自动获取</n-button>
<n-button type="primary" @click="login">登录</n-button>
</n-flex>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { LoginType } from "@/types/main"; import type { LoginType } from "@/types/main";
import { isElectron } from "@/utils/helper";
const emit = defineEmits<{ const emit = defineEmits<{
close: []; close: [];
@@ -29,12 +33,32 @@ const emit = defineEmits<{
const cookie = ref<string>(); const cookie = ref<string>();
// 开启窗口
const openWeb = () => {
window.$dialog.info({
title: "使用前告知",
content:
"请知悉,该功能仍旧无法确保账号的安全性!请自行决定是否使用!如遇打开窗口后页面出现白屏或者无法点击等情况,请关闭后再试。在登录完成后,请点击菜单栏中的 “登录完成” 按钮以完成登录( 通常位于窗口的左上角macOS 位于顶部的全局菜单栏中 ",
positiveText: "我已了解",
negativeText: "取消",
onPositiveClick: () => window.electron.ipcRenderer.send("open-login-web"),
});
};
// Cookie 登录 // Cookie 登录
const login = async () => { const login = async () => {
if (!cookie.value) { if (!cookie.value) {
window.$message.warning("请输入 Cookie"); window.$message.warning("请输入 Cookie");
return; return;
} }
cookie.value = cookie.value.trim();
console.log(cookie.value.endsWith(";"));
// 是否为有效 Cookie
if (!cookie.value.includes("MUSIC_U") || !cookie.value.endsWith(";")) {
window.$message.warning("请输入有效的 Cookie");
return;
}
// 写入 Cookie // 写入 Cookie
try { try {
window.$message.success("登录成功"); window.$message.success("登录成功");
@@ -53,6 +77,16 @@ const login = async () => {
console.error("Cookie 登录出错:", error); console.error("Cookie 登录出错:", error);
} }
}; };
onMounted(() => {
if (isElectron) {
window.electron.ipcRenderer.on("send-cookies", (_, value) => {
if (!value) return;
cookie.value = value;
login();
});
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -73,5 +107,13 @@ const login = async () => {
margin: 4px 0; margin: 4px 0;
font-family: auto; font-family: auto;
} }
.menu {
margin-top: 20px;
.n-button {
width: auto;
flex: 1;
margin: 0;
}
}
} }
</style> </style>

View File

@@ -57,7 +57,7 @@ import { useDataStore } from "@/stores";
import { coverLoaded } from "@/utils/helper"; import { coverLoaded } from "@/utils/helper";
import { playlistTracks } from "@/api/playlist"; import { playlistTracks } from "@/api/playlist";
import { debounce } from "lodash-es"; import { debounce } from "lodash-es";
import { updateUserLikePlaylist, updateUserLikeSongs } from "@/utils/auth"; import { isLogin, updateUserLikePlaylist, updateUserLikeSongs } from "@/utils/auth";
import { openCreatePlaylist } from "@/utils/modal"; import { openCreatePlaylist } from "@/utils/modal";
const props = defineProps<{ const props = defineProps<{
@@ -86,6 +86,10 @@ const onlinePlaylists = computed(() => {
// 添加到歌单 // 添加到歌单
const addPlaylist = debounce( const addPlaylist = debounce(
async (id: number, index: number) => { async (id: number, index: number) => {
if (isLogin() === 2) {
window.$message.warning("该登录模式暂不支持该操作");
return;
}
loadingMsg.value = window.$message.loading("正在添加歌曲至歌单", { duration: 0 }); loadingMsg.value = window.$message.loading("正在添加歌曲至歌单", { duration: 0 });
const ids = props.data.map((item) => item.id).filter((item) => item !== 0); const ids = props.data.map((item) => item.id).filter((item) => item !== 0);
const result = await playlistTracks(id, ids); const result = await playlistTracks(id, ids);

View File

@@ -2,7 +2,7 @@
<div <div
v-show="statusStore.showFullPlayer" v-show="statusStore.showFullPlayer"
:style="{ :style="{
'--main-color': mainColor, '--main-color': statusStore.mainColor,
cursor: statusStore.playerMetaShow ? 'auto' : 'none', cursor: statusStore.playerMetaShow ? 'auto' : 'none',
}" }"
class="full-player" class="full-player"
@@ -66,16 +66,7 @@
<!-- 封面 --> <!-- 封面 -->
<PlayerCover /> <PlayerCover />
<!-- 数据 --> <!-- 数据 -->
<PlayerData <PlayerData :center="playerDataCenter" :theme="statusStore.mainColor" />
v-if="settingStore.playerType === 'cover' || !musicStore.isHasLrc || isShowComment"
:center="
statusStore.pureLyricMode ||
musicStore.playSong.type === 'radio' ||
!musicStore.isHasLrc ||
isShowComment
"
:theme="mainColor"
/>
</div> </div>
<Transition name="fade" mode="out-in"> <Transition name="fade" mode="out-in">
<!-- 评论 --> <!-- 评论 -->
@@ -83,14 +74,14 @@
<!-- 歌词 --> <!-- 歌词 -->
<div v-else-if="musicStore.isHasLrc" class="content-right"> <div v-else-if="musicStore.isHasLrc" class="content-right">
<!-- 数据 --> <!-- 数据 -->
<PlayerData <!-- <PlayerData
v-if=" v-if="
(statusStore.pureLyricMode && musicStore.isHasLrc) || (statusStore.pureLyricMode && musicStore.isHasLrc) ||
(settingStore.playerType === 'record' && musicStore.isHasLrc) (settingStore.playerType === 'record' && musicStore.isHasLrc)
" "
:center="statusStore.pureLyricMode" :center="statusStore.pureLyricMode"
:theme="mainColor" :theme="mainColor"
/> /> -->
<!-- 歌词 --> <!-- 歌词 -->
<MainAMLyric v-if="settingStore.useAMLyrics" /> <MainAMLyric v-if="settingStore.useAMLyrics" />
<MainLyric v-else /> <MainLyric v-else />
@@ -103,7 +94,7 @@
<!-- 音乐频谱 --> <!-- 音乐频谱 -->
<PlayerSpectrum <PlayerSpectrum
v-if="settingStore.showSpectrums" 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" :show="!statusStore.playerMetaShow"
:height="60" :height="60"
/> />
@@ -120,6 +111,11 @@ const musicStore = useMusicStore();
const statusStore = useStatusStore(); const statusStore = useStatusStore();
const settingStore = useSettingStore(); const settingStore = useSettingStore();
// 是否显示评论
const isShowComment = computed<boolean>(
() => !musicStore.playSong.path && statusStore.showPlayerComment,
);
// 主内容 key // 主内容 key
const playerContentKey = computed(() => { const playerContentKey = computed(() => {
return ` return `
@@ -129,8 +125,15 @@ const playerContentKey = computed(() => {
${isShowComment.value}`; ${isShowComment.value}`;
}); });
// 是否显示评论 // 数据是否居中
const isShowComment = computed(() => !musicStore.playSong.path && statusStore.showPlayerComment); const playerDataCenter = computed<boolean>(
() =>
!musicStore.isHasLrc ||
statusStore.pureLyricMode ||
settingStore.playerType === "record" ||
musicStore.playSong.type === "radio" ||
isShowComment.value,
);
// 当前实时歌词 // 当前实时歌词
const instantLyrics = computed(() => { const instantLyrics = computed(() => {
@@ -138,14 +141,7 @@ const instantLyrics = computed(() => {
const content = isYrc const content = isYrc
? musicStore.songLyric.yrcData[statusStore.lyricIndex] ? musicStore.songLyric.yrcData[statusStore.lyricIndex]
: musicStore.songLyric.lrcData[statusStore.lyricIndex]; : musicStore.songLyric.lrcData[statusStore.lyricIndex];
return { content: content?.content, tran: content?.tran }; 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}`;
}); });
// 隐藏播放元素 // 隐藏播放元素

View File

@@ -50,9 +50,8 @@ const { pause: pauseSeek, resume: resumeSeek } = useRafFn(() => {
// 歌词主色 // 歌词主色
const mainColor = computed(() => { const mainColor = computed(() => {
const mainColor = statusStore.songCoverTheme?.main; if (!statusStore.mainColor) return "rgb(239, 239, 239)";
if (!mainColor) return "rgb(239, 239, 239)"; return `rgb(${statusStore.mainColor})`;
return `rgb(${mainColor.r}, ${mainColor.g}, ${mainColor.b})`;
}); });
// 当前歌词 // 当前歌词

View File

@@ -67,9 +67,9 @@
</div> </div>
</div> </div>
<!-- 翻译 --> <!-- 翻译 -->
<span v-if="item.tran" class="tran">{{ item.tran }}</span> <span v-if="item.tran && settingStore.showTran" class="tran">{{ item.tran }}</span>
<!-- 音译 --> <!-- 音译 -->
<span v-if="item.roma" class="roma">{{ item.roma }}</span> <span v-if="item.roma && settingStore.showRoma" class="roma">{{ item.roma }}</span>
<!-- 倒计时 --> <!-- 倒计时 -->
<div <div
v-if=" v-if="
@@ -115,9 +115,9 @@
<!-- 歌词 --> <!-- 歌词 -->
<span class="content">{{ item.content }}</span> <span class="content">{{ item.content }}</span>
<!-- 翻译 --> <!-- 翻译 -->
<span v-if="item.tran" class="tran">{{ item.tran }}</span> <span v-if="item.tran && settingStore.showTran" class="tran">{{ item.tran }}</span>
<!-- 音译 --> <!-- 音译 -->
<span v-if="item.roma" class="roma">{{ item.roma }}</span> <span v-if="item.roma && settingStore.showRoma" class="roma">{{ item.roma }}</span>
</div> </div>
<div class="placeholder" /> <div class="placeholder" />
</template> </template>
@@ -571,8 +571,10 @@ onBeforeUnmount(() => {
} }
} }
} }
&.record,
&.pure { &.pure {
:deep(.n-scrollbar-content) {
padding: 0 80px;
}
.lyric-content { .lyric-content {
.placeholder { .placeholder {
&:first-child { &:first-child {
@@ -588,11 +590,6 @@ onBeforeUnmount(() => {
} }
} }
} }
&.pure {
:deep(.n-scrollbar-content) {
padding: 0 80px;
}
}
&:hover { &:hover {
.lrc-line { .lrc-line {
filter: blur(0) !important; filter: blur(0) !important;

View File

@@ -3,7 +3,7 @@
<n-drawer <n-drawer
v-model:show="statusStore.playListShow" v-model:show="statusStore.playListShow"
:class="{ 'full-player': statusStore.showFullPlayer }" :class="{ 'full-player': statusStore.showFullPlayer }"
:style="{ '--main-color': mainColor }" :style="{ '--main-color': statusStore.mainColor }"
:auto-focus="false" :auto-focus="false"
id="main-playlist" id="main-playlist"
style="width: 400px" style="width: 400px"
@@ -124,13 +124,6 @@ const statusStore = useStatusStore();
const playListRef = ref<VirtualListInst | null>(null); 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(() => { const playListData = computed(() => {
return dataStore.playList.map((item, index) => { return dataStore.playList.map((item, index) => {
@@ -290,6 +283,9 @@ const scrollToItem = (index: number, behavior: "smooth" | "auto" = "smooth") =>
&.on { &.on {
border-color: rgb(var(--main-color)); border-color: rgb(var(--main-color));
} }
&:hover {
border-color: rgb(var(--main-color));
}
.num { .num {
color: rgba(var(--main-color), 0.52); color: rgba(var(--main-color), 0.52);
} }

View File

@@ -15,6 +15,11 @@
:max="100" :max="100"
:tooltip="false" :tooltip="false"
:keyboard="false" :keyboard="false"
:marks="
statusStore.chorus && statusStore.progress <= statusStore.chorus
? { [statusStore.chorus]: '' }
: undefined
"
class="player-slider" class="player-slider"
@dragstart="player.pause(false)" @dragstart="player.pause(false)"
@dragend="sliderDragend" @dragend="sliderDragend"
@@ -168,17 +173,17 @@
@select="(mode) => player.togglePlayMode(mode)" @select="(mode) => player.togglePlayMode(mode)"
> >
<div class="menu-icon" @click.stop="player.togglePlayMode(false)"> <div class="menu-icon" @click.stop="player.togglePlayMode(false)">
<SvgIcon :name="playModeIcon" /> <SvgIcon :name="statusStore.playModeIcon" />
</div> </div>
</n-dropdown> </n-dropdown>
<!-- 音量调节 --> <!-- 音量调节 -->
<n-popover :show-arrow="false" :style="{ padding: 0 }"> <n-popover :show-arrow="false" :style="{ padding: 0 }">
<template #trigger> <template #trigger>
<div class="menu-icon" @click.stop="player.toggleMute" @wheel="changeVolume"> <div class="menu-icon" @click.stop="player.toggleMute" @wheel="player.setVolume">
<SvgIcon :name="playVolumeIcon" /> <SvgIcon :name="statusStore.playVolumeIcon" />
</div> </div>
</template> </template>
<div class="volume-change" @wheel="changeVolume"> <div class="volume-change" @wheel="player.setVolume">
<n-slider <n-slider
v-model:value="statusStore.playVolume" v-model:value="statusStore.playVolume"
:tooltip="false" :tooltip="false"
@@ -188,7 +193,7 @@
vertical vertical
@update:value="(val) => player.setVolume(val)" @update:value="(val) => player.setVolume(val)"
/> />
<n-text class="slider-num">{{ playVolumePercentage }}%</n-text> <n-text class="slider-num">{{ statusStore.playVolumePercent }}%</n-text>
</div> </div>
</n-popover> </n-popover>
<!-- 播放列表 --> <!-- 播放列表 -->
@@ -214,7 +219,7 @@
import type { DropdownOption } from "naive-ui"; import type { DropdownOption } from "naive-ui";
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores"; import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
import { secondsToTime, calculateCurrentTime } from "@/utils/time"; 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 { toLikeSong } from "@/utils/auth";
import { openDownloadSong, openJumpArtist, openPlaylistAdd } from "@/utils/modal"; import { openDownloadSong, openJumpArtist, openPlaylistAdd } from "@/utils/modal";
import player from "@/utils/player"; import player from "@/utils/player";
@@ -277,6 +282,20 @@ const songMoreOptions = computed<DropdownOption[]>(() => {
props: { onClick: () => openDownloadSong(musicStore.playSong) }, props: { onClick: () => openDownloadSong(musicStore.playSong) },
icon: renderIcon("Download"), icon: renderIcon("Download"),
}, },
{
key: "comment",
label: "查看评论",
show: !isLocal,
props: {
onClick: () => {
statusStore.$patch({
showFullPlayer: true,
showPlayerComment: true,
});
},
},
icon: renderIcon("Message"),
},
]; ];
}); });
@@ -289,43 +308,6 @@ const sliderDragend = () => {
player.play(); 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 isShowLyrics = computed(() => {
const isHasLrc = musicStore.isHasLrc; const isHasLrc = musicStore.isHasLrc;
@@ -344,14 +326,10 @@ const instantLyrics = computed(() => {
const content = isYrc const content = isYrc
? musicStore.songLyric.yrcData[statusStore.lyricIndex] ? musicStore.songLyric.yrcData[statusStore.lyricIndex]
: musicStore.songLyric.lrcData[statusStore.lyricIndex]; : musicStore.songLyric.lrcData[statusStore.lyricIndex];
return content?.tran ? `${content?.content} ${content?.tran} ` : content?.content; return content?.tran && settingStore.showTran
? `${content?.content} ${content?.tran} `
: content?.content;
}); });
// 音量条鼠标滚动
const changeVolume = (e: WheelEvent) => {
const deltaY = e.deltaY;
player.setVolume(deltaY > 0 ? "down" : "up");
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -378,6 +356,7 @@ const changeVolume = (e: WheelEvent) => {
height: 16px; height: 16px;
top: -8px; top: -8px;
left: 0; left: 0;
margin: 0;
--n-rail-height: 3px; --n-rail-height: 3px;
--n-handle-size: 14px; --n-handle-size: 14px;
} }
@@ -553,6 +532,8 @@ const changeVolume = (e: WheelEvent) => {
font-size: 12px; font-size: 12px;
margin-right: 8px; margin-right: 8px;
.n-text { .n-text {
color: var(--primary-hex);
opacity: 0.8;
&:nth-of-type(1) { &:nth-of-type(1) {
&::after { &::after {
content: "/"; content: "/";
@@ -573,6 +554,7 @@ const changeVolume = (e: WheelEvent) => {
cursor: pointer; cursor: pointer;
.n-icon { .n-icon {
font-size: 22px; font-size: 22px;
color: var(--primary-hex);
} }
&:hover { &:hover {
transform: scale(1.1); transform: scale(1.1);

View File

@@ -195,6 +195,7 @@ onMounted(player.initPersonalFM);
--n-width: 46px; --n-width: 46px;
--n-height: 46px; --n-height: 46px;
.n-icon { .n-icon {
color: var(--primary-hex);
transition: opacity 0.1s ease-in-out; transition: opacity 0.1s ease-in-out;
} }
} }
@@ -205,6 +206,7 @@ onMounted(player.initPersonalFM);
width: 38px; width: 38px;
height: 38px; height: 38px;
border-radius: 50%; border-radius: 50%;
color: var(--primary-hex);
transition: transition:
background-color 0.3s, background-color 0.3s,
transform 0.3s; transform 0.3s;

View File

@@ -129,6 +129,9 @@ onMounted(() => {
:deep(.n-scrollbar-content) { :deep(.n-scrollbar-content) {
padding-right: 60px; padding-right: 60px;
} }
:deep(.n-skeleton) {
background-color: rgba(var(--main-color), 0.08);
}
.comment-list { .comment-list {
margin: 0 auto; margin: 0 auto;
} }

View File

@@ -94,8 +94,28 @@
</div> </div>
<!-- 播放模式 --> <!-- 播放模式 -->
<div class="menu-icon" @click.stop="player.togglePlayMode(false)"> <div class="menu-icon" @click.stop="player.togglePlayMode(false)">
<SvgIcon :name="playModeIcon" /> <SvgIcon :name="statusStore.playModeIcon" />
</div> </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 <div
v-if="!statusStore.personalFmMode" v-if="!statusStore.personalFmMode"
@@ -121,18 +141,6 @@ const dataStore = useDataStore();
const musicStore = useMusicStore(); const musicStore = useMusicStore();
const statusStore = useStatusStore(); 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 sliderDragend = () => {
const seek = calculateCurrentTime(statusStore.progress, statusStore.duration); const seek = calculateCurrentTime(statusStore.progress, statusStore.duration);
@@ -259,11 +267,6 @@ const sliderDragend = () => {
margin: 6px 8px; margin: 6px 8px;
--n-handle-size: 12px; --n-handle-size: 12px;
--n-rail-height: 4px; --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 { span {
opacity: 0.6; 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> </style>

View File

@@ -59,7 +59,7 @@ const { start: dynamicCoverStart, stop: dynamicCoverStop } = useTimeoutFn(
// 获取动态封面 // 获取动态封面
const getDynamicCover = async () => { const getDynamicCover = async () => {
if ( if (
!isLogin() || isLogin() !== 1 ||
!musicStore.playSong.id || !musicStore.playSong.id ||
!settingStore.dynamicCover || !settingStore.dynamicCover ||
settingStore.playerType !== "cover" settingStore.playerType !== "cover"

View File

@@ -27,7 +27,12 @@
<div v-if="musicStore.playSong.type !== 'radio'" class="artists"> <div v-if="musicStore.playSong.type !== 'radio'" class="artists">
<SvgIcon :depth="3" name="Artist" size="20" /> <SvgIcon :depth="3" name="Artist" size="20" />
<div v-if="Array.isArray(musicStore.playSong.artists)" class="ar-list"> <div v-if="Array.isArray(musicStore.playSong.artists)" class="ar-list">
<span v-for="ar in musicStore.playSong.artists" :key="ar.id" class="ar"> <span
v-for="ar in musicStore.playSong.artists"
:key="ar.id"
class="ar"
@click="jumpPage({ name: 'artist', query: { id: ar.id } })"
>
{{ ar.name }} {{ ar.name }}
</span> </span>
</div> </div>
@@ -44,16 +49,23 @@
<!-- 专辑 --> <!-- 专辑 -->
<div v-if="musicStore.playSong.type !== 'radio'" class="album"> <div v-if="musicStore.playSong.type !== 'radio'" class="album">
<SvgIcon :depth="3" name="Album" size="20" /> <SvgIcon :depth="3" name="Album" size="20" />
<span class="name-text text-hidden"> <span
{{ v-if="isObject(musicStore.playSong.album)"
typeof musicStore.playSong.album === "string" class="name-text text-hidden"
? musicStore.playSong.album || "未知专辑" @click="jumpPage({ name: 'album', query: { id: musicStore.playSong.album.id } })"
: musicStore.playSong.album?.name || "未知专辑" >
}} {{ musicStore.playSong.album?.name || "未知专辑" }}
</span>
<span v-else class="name-text text-hidden">
{{ musicStore.playSong.album || "未知专辑" }}
</span> </span>
</div> </div>
<!-- 电台 --> <!-- 电台 -->
<div v-if="musicStore.playSong.type === 'radio'" class="dj"> <div
v-if="musicStore.playSong.type === 'radio'"
class="dj"
@click="jumpPage({ name: 'dj', query: { id: musicStore.playSong.dj?.id } })"
>
<SvgIcon :depth="3" name="Podcast" size="20" /> <SvgIcon :depth="3" name="Podcast" size="20" />
<span class="name-text text-hidden">{{ musicStore.playSong.dj?.name || "播客电台" }}</span> <span class="name-text text-hidden">{{ musicStore.playSong.dj?.name || "播客电台" }}</span>
</div> </div>
@@ -61,16 +73,32 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { RouteLocationRaw } from "vue-router";
import { useMusicStore, useStatusStore, useSettingStore } from "@/stores"; import { useMusicStore, useStatusStore, useSettingStore } from "@/stores";
import { debounce, isObject } from "lodash-es";
defineProps<{ defineProps<{
center?: boolean; center?: boolean;
theme?: string; theme?: string;
}>(); }>();
const router = useRouter();
const musicStore = useMusicStore(); const musicStore = useMusicStore();
const statusStore = useStatusStore(); const statusStore = useStatusStore();
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const jumpPage = debounce(
(go: RouteLocationRaw) => {
if (!go) return;
statusStore.showFullPlayer = false;
router.push(go);
},
300,
{
leading: true,
trailing: false,
},
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -10,7 +10,15 @@
{{ packageJson.version }} {{ packageJson.version }}
</n-tag> </n-tag>
</n-flex> </n-flex>
<n-button type="primary" strong secondary @click="checkUpdate"> 检查更新 </n-button> <n-button
:loading="statusStore.updateCheck"
type="primary"
strong
secondary
@click="checkUpdate"
>
{{ statusStore.updateCheck ? "检查更新中" : "检查更新" }}
</n-button>
</n-card> </n-card>
<n-collapse-transition :show="!!updateData"> <n-collapse-transition :show="!!updateData">
<n-card class="set-item update-data"> <n-card class="set-item update-data">
@@ -73,8 +81,11 @@
import type { UpdateLogType } from "@/types/main"; import type { UpdateLogType } from "@/types/main";
import { getUpdateLog, isElectron, openLink } from "@/utils/helper"; import { getUpdateLog, isElectron, openLink } from "@/utils/helper";
import { debounce } from "lodash-es"; import { debounce } from "lodash-es";
import { useStatusStore } from "@/stores";
import packageJson from "@/../package.json"; import packageJson from "@/../package.json";
const statusStore = useStatusStore();
// 社区数据 // 社区数据
const communityData = [ const communityData = [
{ {
@@ -102,18 +113,18 @@ const oldVersion = computed<UpdateLogType[]>(() => {
}); });
// 检查更新 // 检查更新
const checkUpdate = debounce(() => { const checkUpdate = debounce(
if (!isElectron) { () => {
window.open(packageJson.github + "/releases", "_blank"); if (!isElectron) {
return; window.open(packageJson.github + "/releases", "_blank");
} return;
window.$notification.info({ }
title: "检查更新", statusStore.updateCheck = true;
content: "正在检查更新,请稍后...", window.electron.ipcRenderer.send("check-update", true);
duration: 3000, },
}); 300,
window.electron.ipcRenderer.send("check-update", true); { leading: true, trailing: false },
}, 300); );
// 获取更新日志 // 获取更新日志
const getUpdateData = async () => (updateData.value = await getUpdateLog()); const getUpdateData = async () => (updateData.value = await getUpdateLog());

View File

@@ -95,6 +95,13 @@
</div> </div>
<n-switch class="set" v-model:value="settingStore.menuShowCover" :round="false" /> <n-switch class="set" v-model:value="settingStore.menuShowCover" :round="false" />
</n-card> </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"> <n-card class="set-item">
<div class="label"> <div class="label">
<n-text class="name">页面切换动画</n-text> <n-text class="name">页面切换动画</n-text>
@@ -135,7 +142,13 @@
<n-text class="name">在线服务</n-text> <n-text class="name">在线服务</n-text>
<n-text class="tip" :depth="3">是否开启软件的在线服务</n-text> <n-text class="tip" :depth="3">是否开启软件的在线服务</n-text>
</div> </div>
<n-switch class="set" :value="useOnlineService" :round="false" @update:value="modeChange" /> <n-switch
class="set"
:disabled="true"
:value="useOnlineService"
:round="false"
@update:value="modeChange"
/>
</n-card> </n-card>
<n-card class="set-item"> <n-card class="set-item">
<div class="label"> <div class="label">

View File

@@ -232,6 +232,13 @@
</div> </div>
<n-switch v-model:value="settingStore.lyricsBlur" class="set" :round="false" /> <n-switch v-model:value="settingStore.lyricsBlur" class="set" :round="false" />
</n-card> </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>
<div class="set-list"> <div class="set-list">
<n-h3 prefix="bar"> Apple Music-like Lyrics </n-h3> <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 { cloneDeep, isEqual } from "lodash-es";
import { isElectron } from "@/utils/helper"; import { isElectron } from "@/utils/helper";
import player from "@/utils/player"; import player from "@/utils/player";
import { openLyricExclude } from "@/utils/modal";
const statusStore = useStatusStore(); const statusStore = useStatusStore();
const settingStore = useSettingStore(); const settingStore = useSettingStore();

View File

@@ -181,7 +181,7 @@
</div> </div>
<n-switch <n-switch
v-model:value="settingStore.dynamicCover" v-model:value="settingStore.dynamicCover"
:disabled="!isLogin()" :disabled="isLogin() !== 1"
:round="false" :round="false"
class="set" class="set"
/> />

View File

@@ -88,8 +88,8 @@ watchOnce(isCanLook, (show) => {
} }
.loading { .loading {
position: absolute; position: absolute;
top: 0; // top: 0;
left: 0; // left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 0; z-index: 0;

View File

@@ -50,8 +50,7 @@ const userDB = localforage.createInstance({
storeName: "user", storeName: "user",
}); });
export const useDataStore = defineStore({ export const useDataStore = defineStore("data", {
id: "data",
state: (): ListState => ({ state: (): ListState => ({
// 播放列表 // 播放列表
playList: [], playList: [],
@@ -160,15 +159,23 @@ export const useDataStore = defineStore({
}, },
// 新增下一首播放歌曲 // 新增下一首播放歌曲
async setNextPlaySong(song: SongType, index: number): Promise<number> { async setNextPlaySong(song: SongType, index: number): Promise<number> {
// 移除重复的歌曲(如果存在) // 若为空,则直接添加
const playList = this.playList.filter((item) => item.id !== song.id); if (this.playList.length === 0) {
this.playList = [song];
await musicDB.setItem("playList", cloneDeep(this.playList));
return 0;
}
// 在当前播放位置之后插入歌曲 // 在当前播放位置之后插入歌曲
const indexAdd = index + 1; const indexAdd = index + 1;
playList.splice(indexAdd, 0, song); this.playList.splice(indexAdd, 0, song);
// 移除重复的歌曲(如果存在)
const playList = this.playList.filter((item, idx) => idx === indexAdd || item.id !== song.id);
// 更新本地存储 // 更新本地存储
this.playList = playList; this.playList = playList;
await musicDB.setItem("playList", cloneDeep(playList)); await musicDB.setItem("playList", cloneDeep(playList));
return indexAdd; // 返回刚刚插入的歌曲索引
return playList.findIndex((item) => item.id === song.id);
}, },
// 更改播放历史 // 更改播放历史
async setHistory(song: SongType) { async setHistory(song: SongType) {
@@ -296,6 +303,6 @@ export const useDataStore = defineStore({
persist: { persist: {
key: "data-store", key: "data-store",
storage: localStorage, storage: localStorage,
paths: ["userLoginStatus", "loginType", "userData", "searchHistory", "catData"], pick: ["userLoginStatus", "loginType", "userData", "searchHistory", "catData"],
}, },
}); });

View File

@@ -34,8 +34,7 @@ const defaultMusicData: SongType = {
type: "song", type: "song",
}; };
export const useMusicStore = defineStore({ export const useMusicStore = defineStore("music", {
id: "music",
state: (): MusicState => ({ state: (): MusicState => ({
// 当前播放歌曲 // 当前播放歌曲
playSong: { ...defaultMusicData }, playSong: { ...defaultMusicData },
@@ -86,7 +85,7 @@ export const useMusicStore = defineStore({
actions: { actions: {
// 恢复默认音乐数据 // 恢复默认音乐数据
resetMusicData() { resetMusicData() {
this.playSong = defaultMusicData; this.playSong = { ...defaultMusicData };
this.songLyric = { this.songLyric = {
lrcData: [], lrcData: [],
yrcData: [], yrcData: [],

View File

@@ -1,4 +1,5 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { keywords } from "@/assets/data/exclude";
interface SettingState { interface SettingState {
themeMode: "light" | "dark" | "auto"; themeMode: "light" | "dark" | "auto";
@@ -83,10 +84,11 @@ interface SettingState {
fullPlayerCache: boolean; fullPlayerCache: boolean;
scrobbleSong: boolean; scrobbleSong: boolean;
dynamicCover: boolean; dynamicCover: boolean;
useKeepAlive: boolean;
excludeKeywords: string[];
} }
export const useSettingStore = defineStore({ export const useSettingStore = defineStore("setting", {
id: "setting",
state: (): SettingState => ({ state: (): SettingState => ({
// 个性化 // 个性化
themeMode: "auto", // 明暗模式 themeMode: "auto", // 明暗模式
@@ -108,6 +110,7 @@ export const useSettingStore = defineStore({
checkUpdateOnStart: true, // 启动时检查更新 checkUpdateOnStart: true, // 启动时检查更新
preventSleep: false, // 是否禁止休眠 preventSleep: false, // 是否禁止休眠
fullPlayerCache: false, // 全屏播放器缓存 fullPlayerCache: false, // 全屏播放器缓存
useKeepAlive: true, // 使用 keep-alive
// 播放 // 播放
songLevel: "exhigh", // 音质 songLevel: "exhigh", // 音质
playDevice: "default", // 播放设备 playDevice: "default", // 播放设备
@@ -142,6 +145,7 @@ export const useSettingStore = defineStore({
lyricsBlur: false, // 歌词模糊 lyricsBlur: false, // 歌词模糊
lyricsScrollPosition: "start", // 歌词滚动位置 lyricsScrollPosition: "start", // 歌词滚动位置
lrcMousePause: false, // 鼠标悬停暂停 lrcMousePause: false, // 鼠标悬停暂停
excludeKeywords: keywords, // 排除歌词关键字
// 本地 // 本地
localFilesPath: [], localFilesPath: [],
localSeparators: ["/", "&"], localSeparators: ["/", "&"],

View File

@@ -21,8 +21,7 @@ interface ShortcutStore {
}; };
} }
export const useShortcutStore = defineStore({ export const useShortcutStore = defineStore("shortcut", {
id: "shortcut",
state: (): ShortcutStore => ({ state: (): ShortcutStore => ({
// 全局快捷键开启 // 全局快捷键开启
globalOpen: true, globalOpen: true,

View File

@@ -29,6 +29,7 @@ interface StatusState {
lyricIndex: number; lyricIndex: number;
currentTime: number; currentTime: number;
duration: number; duration: number;
chorus: number;
progress: number; progress: number;
currentTimeOffset: number; currentTimeOffset: number;
playUblock: boolean; playUblock: boolean;
@@ -37,10 +38,10 @@ interface StatusState {
showDesktopLyric: boolean; showDesktopLyric: boolean;
showPlayerComment: boolean; showPlayerComment: boolean;
personalFmMode: boolean; personalFmMode: boolean;
updateCheck: boolean;
} }
export const useStatusStore = defineStore({ export const useStatusStore = defineStore("status", {
id: "status",
state: (): StatusState => ({ state: (): StatusState => ({
// 菜单折叠状态 // 菜单折叠状态
menuCollapsed: false, menuCollapsed: false,
@@ -65,6 +66,8 @@ export const useStatusStore = defineStore({
currentTime: 0, currentTime: 0,
duration: 0, duration: 0,
progress: 0, progress: 0,
// 副歌时间
chorus: 0,
// 进度偏移 // 进度偏移
currentTimeOffset: 0, currentTimeOffset: 0,
// 封面主题 // 封面主题
@@ -74,7 +77,7 @@ export const useStatusStore = defineStore({
// 音乐频谱数据 // 音乐频谱数据
spectrumsData: [], spectrumsData: [],
// 当前播放索引 // 当前播放索引
playIndex: 0, playIndex: -1,
// 歌词播放索引 // 歌词播放索引
lyricIndex: -1, lyricIndex: -1,
// 默认倍速 // 默认倍速
@@ -97,14 +100,49 @@ export const useStatusStore = defineStore({
showDesktopLyric: false, showDesktopLyric: false,
// 播放器评论 // 播放器评论
showPlayerComment: false, showPlayerComment: false,
// 更新检查
updateCheck: 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: {}, actions: {},
// 持久化 // 持久化
persist: { persist: {
key: "status-store", key: "status-store",
storage: localStorage, storage: localStorage,
paths: [ pick: [
"menuCollapsed", "menuCollapsed",
"currentTime", "currentTime",
"duration", "duration",

View File

@@ -23,7 +23,10 @@ import { likePlaylist, playlistTracks } from "@/api/playlist";
import { likeArtist } from "@/api/artist"; import { likeArtist } from "@/api/artist";
import { radioSub } from "@/api/radio"; import { radioSub } from "@/api/radio";
// 是否登录 /**
* 用户是否登录
* @returns 0 - 未登录 / 1 - 正常登录 / 2 - UID 登录
*/
export const isLogin = (): 0 | 1 | 2 => { export const isLogin = (): 0 | 1 | 2 => {
const dataStore = useDataStore(); const dataStore = useDataStore();
if (dataStore.loginType === "uid") return 2; if (dataStore.loginType === "uid") return 2;
@@ -33,7 +36,6 @@ export const isLogin = (): 0 | 1 | 2 => {
// 退出登录 // 退出登录
export const toLogout = async () => { export const toLogout = async () => {
const dataStore = useDataStore(); const dataStore = useDataStore();
// 退出登录
await logout(); await logout();
// 去除 cookie // 去除 cookie
removeCookie("MUSIC_U"); removeCookie("MUSIC_U");

View File

@@ -1,6 +1,6 @@
import type { SongType, CoverType, ArtistType, CommentType, MetaData, CatType } from "@/types/main"; import type { SongType, CoverType, ArtistType, CommentType, MetaData, CatType } from "@/types/main";
import { msToTime } from "./time"; import { msToTime } from "./time";
import { isArray } from "lodash-es"; import { flatMap, isArray, uniqBy } from "lodash-es";
type CoverDataType = { type CoverDataType = {
cover: string; cover: string;
@@ -81,7 +81,10 @@ export const formatCoverList = (data: any[]): CoverType[] => {
const creator = isArray(item.creator) ? item.creator[0] : item.creator; const creator = isArray(item.creator) ? item.creator[0] : item.creator;
// 获取歌手信息 // 获取歌手信息
const artists = (): string | MetaData[] => { const artists = (): string | MetaData[] => {
const artistData = [item.artist, item.artists, item.ar].flat().filter(Boolean); const artistData = uniqBy(
flatMap([item.artist, item.artists, item.ar]).filter(Boolean),
"id",
);
if (artistData.length === 0) return ""; if (artistData.length === 0) return "";
return artistData.map((artist) => ({ return artistData.map((artist) => ({
id: artist?.id, id: artist?.id,

View File

@@ -1,8 +1,14 @@
import { isElectron } from "./helper"; import { isElectron } from "./helper";
import { openUpdateApp } from "./modal"; import { openUpdateApp } from "./modal";
import { useMusicStore, useDataStore } from "@/stores"; import { useMusicStore, useDataStore, useStatusStore } from "@/stores";
import player from "./player";
import { toLikeSong } from "./auth"; import { toLikeSong } from "./auth";
import player from "./player";
// 关闭更新状态
const closeUpdateStatus = () => {
const statusStore = useStatusStore();
statusStore.updateCheck = false;
};
// 全局 IPC 事件 // 全局 IPC 事件
const initIpc = () => { const initIpc = () => {
@@ -35,13 +41,18 @@ const initIpc = () => {
window.electron.ipcRenderer.on("closeDesktopLyric", () => player.toggleDesktopLyric()); window.electron.ipcRenderer.on("closeDesktopLyric", () => player.toggleDesktopLyric());
// 无更新 // 无更新
window.electron.ipcRenderer.on("update-not-available", () => { window.electron.ipcRenderer.on("update-not-available", () => {
closeUpdateStatus();
window.$message.success("当前已是最新版本"); window.$message.success("当前已是最新版本");
}); });
// 有更新 // 有更新
window.electron.ipcRenderer.on("update-available", (_, info) => openUpdateApp(info)); window.electron.ipcRenderer.on("update-available", (_, info) => {
closeUpdateStatus();
openUpdateApp(info);
});
// 更新错误 // 更新错误
window.electron.ipcRenderer.on("update-error", (_, error) => { window.electron.ipcRenderer.on("update-error", (_, error) => {
console.error("Error updating:", error); console.error("Error updating:", error);
closeUpdateStatus();
window.$message.error("更新过程出现错误"); window.$message.error("更新过程出现错误");
}); });
} catch (error) { } catch (error) {

View File

@@ -1,9 +1,14 @@
import { LyricLine, parseLrc, parseYrc } from "@applemusic-like-lyrics/lyric"; import { LyricLine, parseLrc, parseYrc } from "@applemusic-like-lyrics/lyric";
import { keywords } from "@/assets/data/exclude";
import type { LyricType } from "@/types/main"; import type { LyricType } from "@/types/main";
import { useMusicStore } from "@/stores"; import { useMusicStore, useSettingStore } from "@/stores";
import { msToS } from "./time"; import { msToS } from "./time";
// 歌词排除内容
const getExcludeKeywords = () => {
const settingStore = useSettingStore();
return settingStore.excludeKeywords;
};
// 恢复默认 // 恢复默认
export const resetSongLyric = () => { export const resetSongLyric = () => {
const musicStore = useMusicStore(); const musicStore = useMusicStore();
@@ -77,7 +82,7 @@ export const parseLrcData = (lrcData: LyricLine[]): LyricType[] => {
const time = msToS(words[0].startTime); const time = msToS(words[0].startTime);
const content = words[0].word.trim(); 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 null;
} }
return { return {
@@ -113,7 +118,7 @@ export const parseYrcData = (yrcData: LyricLine[]): LyricType[] => {
.map((word) => word.content + (word.endsWithSpace ? " " : "")) .map((word) => word.content + (word.endsWithSpace ? " " : ""))
.join(""); .join("");
// 排除内容 // 排除内容
if (!contentStr || keywords.some((keyword) => contentStr.includes(keyword))) { if (!contentStr || getExcludeKeywords().some((keyword) => contentStr.includes(keyword))) {
return null; return null;
} }
return { return {

View File

@@ -16,6 +16,7 @@ import UpdatePlaylist from "@/components/Modal/UpdatePlaylist.vue";
import DownloadSong from "@/components/Modal/DownloadSong.vue"; import DownloadSong from "@/components/Modal/DownloadSong.vue";
import MainSetting from "@/components/Setting/MainSetting.vue"; import MainSetting from "@/components/Setting/MainSetting.vue";
import UpdateApp from "@/components/Modal/UpdateApp.vue"; import UpdateApp from "@/components/Modal/UpdateApp.vue";
import ExcludeKeywords from "@/components/Modal/ExcludeKeywords.vue";
// 用户协议 // 用户协议
export const openUserAgreement = () => { export const openUserAgreement = () => {
@@ -101,7 +102,7 @@ export const openSongInfoEditor = (song: SongType) => {
// 添加到歌单 // 添加到歌单
export const openPlaylistAdd = (data: SongType[], isLocal: boolean) => { export const openPlaylistAdd = (data: SongType[], isLocal: boolean) => {
if (!data.length) return window.$message.warning("请正确选择歌曲"); if (!data.length) return window.$message.warning("请正确选择歌曲");
if (!isLogin()) return openUserLogin(); if (!isLogin() && !isLocal) return openUserLogin();
const modal = window.$modal.create({ const modal = window.$modal.create({
preset: "card", preset: "card",
transformOrigin: "center", transformOrigin: "center",
@@ -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);
},
});
};

View File

@@ -4,7 +4,7 @@ import { Howl, Howler } from "howler";
import { cloneDeep } from "lodash-es"; import { cloneDeep } from "lodash-es";
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores"; import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
import { parsedLyricsData, resetSongLyric, parseLocalLyric } from "./lyric"; import { parsedLyricsData, resetSongLyric, parseLocalLyric } from "./lyric";
import { songUrl, unlockSongUrl, songLyric } from "@/api/song"; import { songUrl, unlockSongUrl, songLyric, songChorus } from "@/api/song";
import { getCoverColorData } from "@/utils/color"; import { getCoverColorData } from "@/utils/color";
import { calculateProgress } from "./time"; import { calculateProgress } from "./time";
import { isElectron, isDev } from "./helper"; import { isElectron, isDev } from "./helper";
@@ -52,6 +52,7 @@ class Player {
currentTime: 0, currentTime: 0,
duration: 0, duration: 0,
progress: 0, progress: 0,
chorus: 0,
currentTimeOffset: 0, currentTimeOffset: 0,
lyricIndex: -1, lyricIndex: -1,
playStatus: false, playStatus: false,
@@ -113,12 +114,7 @@ class Player {
// 歌词跨界处理 // 歌词跨界处理
const lyricIndex = index === -1 ? lyrics.length - 1 : index - 1; const lyricIndex = index === -1 ? lyrics.length - 1 : index - 1;
// 更新状态 // 更新状态
statusStore.$patch({ statusStore.$patch({ currentTime, duration, progress, lyricIndex });
currentTime,
duration,
progress,
lyricIndex,
});
// 客户端事件 // 客户端事件
if (isElectron) { if (isElectron) {
// 歌词变化 // 歌词变化
@@ -217,9 +213,11 @@ class Player {
if (!settingStore.showSpectrums) this.toggleOutputDevice(); if (!settingStore.showSpectrums) this.toggleOutputDevice();
// 自动播放 // 自动播放
if (autoPlay) this.play(); if (autoPlay) this.play();
// 获取歌词数据 - 非电台和本地 // 获取歌曲附加信息 - 非电台和本地
if (type !== "radio" && !path) this.getLyricData(id); if (type !== "radio" && !path) {
else resetSongLyric(); this.getLyricData(id);
this.getChorus(id);
} else resetSongLyric();
// 定时获取状态 // 定时获取状态
if (!this.playerInterval) this.handlePlayStatus(); if (!this.playerInterval) this.handlePlayStatus();
// 新增播放历史 // 新增播放历史
@@ -276,7 +274,7 @@ class Player {
}); });
// 暂停 // 暂停
this.player.on("pause", () => { this.player.on("pause", () => {
window.document.title = "SPlayer"; if (!isElectron) window.document.title = "SPlayer";
// ipc // ipc
if (isElectron) window.electron.ipcRenderer.send("play-status-change", false); if (isElectron) window.electron.ipcRenderer.send("play-status-change", false);
console.log("⏸️ song pause:", playSongData); console.log("⏸️ song pause:", playSongData);
@@ -402,6 +400,22 @@ class Player {
const lyricRes = await songLyric(id); const lyricRes = await songLyric(id);
parsedLyricsData(lyricRes); parsedLyricsData(lyricRes);
} }
/**
* 获取副歌时间
* @param id 歌曲id
*/
private async getChorus(id: number) {
const statusStore = useStatusStore();
const result = await songChorus(id);
if (result?.code !== 200 || result?.chorus?.length === 0) {
statusStore.chorus = 0;
return;
}
// 计算并保存
const chorus = result?.chorus?.[0]?.startTime;
const time = ((chorus / 1000 / statusStore.duration) * 100).toFixed(2);
statusStore.chorus = Number(time);
}
/** /**
* 播放错误 * 播放错误
* 在播放错误时,播放下一首 * 在播放错误时,播放下一首
@@ -588,6 +602,12 @@ class Player {
*/ */
async pause(changeStatus: boolean = true) { async pause(changeStatus: boolean = true) {
const statusStore = useStatusStore(); const statusStore = useStatusStore();
// 播放器未加载完成
if (this.player.state() !== "loaded") {
return;
}
// 淡出 // 淡出
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
this.player.fade(statusStore.playVolume, 0, this.getFadeTime()); this.player.fade(statusStore.playVolume, 0, this.getFadeTime());
@@ -738,20 +758,28 @@ class Player {
} }
/** /**
* 设置播放音量 * 设置播放音量
* @param volume 音量 * @param actions 音量
*/ */
setVolume(volume: number | "up" | "down") { setVolume(actions: number | "up" | "down" | WheelEvent) {
const statusStore = useStatusStore(); const statusStore = useStatusStore();
const increment = 0.05;
// 直接设置 // 直接设置
if (typeof volume === "number") { if (typeof actions === "number") {
volume = Math.max(0, Math.min(volume, 1)); actions = Math.max(0, Math.min(actions, 1));
} else { }
const increment = 0.05; // 分类调节
else if (actions === "up" || actions === "down") {
statusStore.playVolume = Math.max( statusStore.playVolume = Math.max(
0, 0,
Math.min(statusStore.playVolume + (volume === "up" ? increment : -increment), 1), Math.min(statusStore.playVolume + (actions === "up" ? increment : -increment), 1),
); );
} }
// 鼠标滚轮
else {
const deltaY = actions.deltaY;
const volumeChange = deltaY > 0 ? -increment : increment;
statusStore.playVolume = Math.max(0, Math.min(statusStore.playVolume + volumeChange, 1));
}
// 调整音量 // 调整音量
this.player.volume(statusStore.playVolume); this.player.volume(statusStore.playVolume);
} }
@@ -873,15 +901,16 @@ class Player {
// 尝试添加 // 尝试添加
const songIndex = await dataStore.setNextPlaySong(song, statusStore.playIndex); const songIndex = await dataStore.setNextPlaySong(song, statusStore.playIndex);
// 播放歌曲 // 播放歌曲
if (!songIndex) return; if (songIndex < 0) return;
if (play) this.togglePlayIndex(songIndex); if (play) this.togglePlayIndex(songIndex, true);
else window.$message.success("已添加至下一首播放"); else window.$message.success("已添加至下一首播放");
} }
/** /**
* 切换播放索引 * 切换播放索引
* @param index 播放索引 * @param index 播放索引
* @param play 是否立即播放
*/ */
async togglePlayIndex(index: number) { async togglePlayIndex(index: number, play: boolean = false) {
const dataStore = useDataStore(); const dataStore = useDataStore();
const statusStore = useStatusStore(); const statusStore = useStatusStore();
// 获取数据 // 获取数据
@@ -889,7 +918,7 @@ class Player {
// 若超出播放列表 // 若超出播放列表
if (index >= playList.length) return; if (index >= playList.length) return;
// 相同 // 相同
if (statusStore.playIndex === index) { if (!play && statusStore.playIndex === index) {
this.play(); this.play();
return; return;
} }
@@ -915,6 +944,8 @@ class Player {
this.cleanPlayList(); this.cleanPlayList();
return; return;
} }
// 是否为当前播放歌曲
const isCurrentPlay = statusStore.playIndex === index;
// 深拷贝,防止影响原数据 // 深拷贝,防止影响原数据
const newPlaylist = cloneDeep(playList); const newPlaylist = cloneDeep(playList);
// 若将移除最后一首 // 若将移除最后一首
@@ -929,7 +960,7 @@ class Player {
newPlaylist.splice(index, 1); newPlaylist.splice(index, 1);
dataStore.setPlayList(newPlaylist); dataStore.setPlayList(newPlaylist);
// 若为当前播放 // 若为当前播放
if (statusStore.playIndex === index) { if (isCurrentPlay) {
this.initPlayer(statusStore.playStatus); this.initPlayer(statusStore.playStatus);
} }
} }
@@ -949,6 +980,7 @@ class Player {
showFullPlayer: false, showFullPlayer: false,
playHeartbeatMode: false, playHeartbeatMode: false,
personalFmMode: false, personalFmMode: false,
playIndex: -1,
}); });
musicStore.resetMusicData(); musicStore.resetMusicData();
dataStore.setPlayList([]); dataStore.setPlayList([]);
@@ -1028,8 +1060,12 @@ class Player {
window.$message.success("已退出心动模式"); window.$message.success("已退出心动模式");
return; return;
} }
if (!isLogin()) { if (isLogin() !== 1) {
openUserLogin(true); if (isLogin() === 0) {
openUserLogin(true);
} else {
window.$message.warning("该登录模式暂不支持该操作");
}
return; return;
} }
if (statusStore.playHeartbeatMode) { if (statusStore.playHeartbeatMode) {

View File

@@ -129,7 +129,7 @@
<!-- 路由 --> <!-- 路由 -->
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in"> <Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
<KeepAlive> <KeepAlive v-if="settingStore.useKeepAlive">
<component <component
ref="componentRef" ref="componentRef"
:is="Component" :is="Component"
@@ -138,6 +138,14 @@
@scroll="listScroll" @scroll="listScroll"
/> />
</KeepAlive> </KeepAlive>
<component
v-else
ref="componentRef"
:is="Component"
:id="artistId"
class="router-view"
@scroll="listScroll"
/>
</Transition> </Transition>
</RouterView> </RouterView>
</div> </div>

View File

@@ -18,9 +18,10 @@
<!-- 路由 --> <!-- 路由 -->
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in"> <Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
<KeepAlive> <KeepAlive v-if="settingStore.useKeepAlive">
<component :is="Component" class="router-view" /> <component :is="Component" class="router-view" />
</KeepAlive> </KeepAlive>
<component v-else :is="Component" class="router-view" />
</Transition> </Transition>
</RouterView> </RouterView>
</div> </div>

View File

@@ -31,7 +31,12 @@
</n-grid> </n-grid>
<!-- 公共推荐 --> <!-- 公共推荐 -->
<div v-for="(item, index) in recData" :key="index" class="rec-public"> <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-h3 prefix="bar">
<n-text>{{ item.name }}</n-text> <n-text>{{ item.name }}</n-text>
<SvgIcon v-if="item.path" :size="26" name="Right" /> <SvgIcon v-if="item.path" :size="26" name="Right" />
@@ -194,7 +199,10 @@ const getAllRecData = async () => {
} }
}; };
onActivated(getAllRecData); onMounted(() => {
getAllRecData();
onActivated(getAllRecData);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -31,9 +31,10 @@
<!-- 路由 --> <!-- 路由 -->
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in"> <Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
<KeepAlive> <KeepAlive v-if="settingStore.useKeepAlive">
<component :is="Component" class="router-view" /> <component :is="Component" class="router-view" />
</KeepAlive> </KeepAlive>
<component v-else :is="Component" class="router-view" />
</Transition> </Transition>
</RouterView> </RouterView>
</div> </div>

View File

@@ -49,7 +49,11 @@
<n-flex class="meta"> <n-flex class="meta">
<div class="item"> <div class="item">
<SvgIcon name="Person" :depth="3" /> <SvgIcon name="Person" :depth="3" />
<div v-if="Array.isArray(albumDetailData.artists)" class="artists text-hidden"> <div
v-if="Array.isArray(albumDetailData.artists)"
class="artists text-hidden"
@click="openJumpArtist(albumDetailData.artists)"
>
<n-text <n-text
v-for="(ar, arIndex) in albumDetailData.artists" v-for="(ar, arIndex) in albumDetailData.artists"
:key="arIndex" :key="arIndex"
@@ -58,7 +62,11 @@
{{ ar.name || "未知艺术家" }} {{ ar.name || "未知艺术家" }}
</n-text> </n-text>
</div> </div>
<div v-else class="artists text-hidden"> <div
v-else
class="artists text-hidden"
@click="openJumpArtist(albumDetailData.artists || '')"
>
<n-text class="ar"> {{ albumDetailData.artists || "未知艺术家" }} </n-text> <n-text class="ar"> {{ albumDetailData.artists || "未知艺术家" }} </n-text>
</div> </div>
</div> </div>
@@ -169,6 +177,7 @@ import { renderToolbar } from "@/utils/meta";
import { useDataStore, useStatusStore } from "@/stores"; import { useDataStore, useStatusStore } from "@/stores";
import { debounce } from "lodash-es"; import { debounce } from "lodash-es";
import { formatTimestamp } from "@/utils/time"; import { formatTimestamp } from "@/utils/time";
import { openJumpArtist } from "@/utils/modal";
import player from "@/utils/player"; import player from "@/utils/player";
const router = useRouter(); const router = useRouter();

View File

@@ -215,7 +215,12 @@
import type { CoverType, SongType } from "@/types/main"; import type { CoverType, SongType } from "@/types/main";
import type { DropdownOption, MessageReactive } from "naive-ui"; import type { DropdownOption, MessageReactive } from "naive-ui";
import { songDetail } from "@/api/song"; 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 { formatCoverList, formatSongsList } from "@/utils/format";
import { coverLoaded, formatNumber, fuzzySearch, renderIcon } from "@/utils/helper"; import { coverLoaded, formatNumber, fuzzySearch, renderIcon } from "@/utils/helper";
import { renderToolbar } from "@/utils/meta"; import { renderToolbar } from "@/utils/meta";
@@ -283,6 +288,7 @@ const moreOptions = computed<DropdownOption[]>(() => [
label: "公开隐私歌单", label: "公开隐私歌单",
key: "privacy", key: "privacy",
show: playlistDetailData.value?.privacy === 10, show: playlistDetailData.value?.privacy === 10,
props: { onClick: openPrivacy },
icon: renderIcon("ListLockOpen"), icon: renderIcon("ListLockOpen"),
}, },
{ {
@@ -355,21 +361,24 @@ const handleLocalPlaylist = (id: number) => {
// 获取在线歌单 // 获取在线歌单
const handleOnlinePlaylist = async (id: number, getList: boolean, refresh: boolean) => { const handleOnlinePlaylist = async (id: number, getList: boolean, refresh: boolean) => {
console.log(id, getList, refresh);
// 获取歌单详情 // 获取歌单详情
const detail = await playlistDetail(id); const detail = await playlistDetail(id);
playlistDetailData.value = formatCoverList(detail.playlist)[0]; playlistDetailData.value = formatCoverList(detail.playlist)[0];
const count = playlistDetailData.value?.count || 0;
// 不需要获取列表或无歌曲 // 不需要获取列表或无歌曲
if (!getList || playlistDetailData.value.count === 0) { if (!getList || count === 0) {
loading.value = false; loading.value = false;
return; return;
} }
// 如果已登录且歌曲数量少于 800直接加载所有歌曲 // 如果已登录且歌曲数量少于 800直接加载所有歌曲
if (isLogin() === 1 && (playlistDetailData.value?.count as number) < 800) { if (isLogin() === 1 && count === detail.privileges?.length && count < 800) {
const ids: number[] = detail.privileges.map((song: any) => song.id as number); const ids = detail.privileges.map((song: any) => song.id as number);
const result = await songDetail(ids); const result = await songDetail(ids);
playlistData.value = formatSongsList(result.songs); playlistData.value = formatSongsList(result.songs);
} else { } else {
await getPlaylistAllSongs(id, playlistDetailData.value.count || 0, refresh); await getPlaylistAllSongs(id, count, refresh);
} }
loading.value = false; loading.value = false;
}; };
@@ -424,7 +433,6 @@ const loadingMsgShow = (show: boolean = true, count?: number) => {
closable: true, closable: true,
}); });
} else { } else {
loading.value = false;
loadingMsg.value?.destroy(); loadingMsg.value?.destroy();
loadingMsg.value = null; loadingMsg.value = null;
} }
@@ -481,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) => { onBeforeRouteUpdate((to) => {
const id = Number(to.query.id as string); const id = Number(to.query.id as string);
if (id) { if (id) {

View File

@@ -82,9 +82,10 @@
<!-- 路由 --> <!-- 路由 -->
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in"> <Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
<KeepAlive> <KeepAlive v-if="settingStore.useKeepAlive">
<component :is="Component" :data="listData" :loading="loading" class="router-view" /> <component :is="Component" :data="listData" :loading="loading" class="router-view" />
</KeepAlive> </KeepAlive>
<component v-else :is="Component" :data="listData" :loading="loading" class="router-view" />
</Transition> </Transition>
</RouterView> </RouterView>
<!-- 目录管理 --> <!-- 目录管理 -->

View File

@@ -16,16 +16,17 @@
<!-- 路由 --> <!-- 路由 -->
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in"> <Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
<KeepAlive> <KeepAlive v-if="settingStore.useKeepAlive">
<component :is="Component" :keyword="searchKeyword" class="router-view" /> <component :is="Component" :keyword="searchKeyword" class="router-view" />
</KeepAlive> </KeepAlive>
<component v-else :is="Component" :keyword="searchKeyword" class="router-view" />
</Transition> </Transition>
</RouterView> </RouterView>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useSettingStore } from '@/stores'; import { useSettingStore } from "@/stores";
const router = useRouter(); const router = useRouter();
const settingStore = useSettingStore(); const settingStore = useSettingStore();