mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 19:37:35 +08:00
Compare commits
20 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
146af3aeba | ||
|
|
b2ddb9f4e2 | ||
|
|
201186bab2 | ||
|
|
bfcd59daca | ||
|
|
d5c3843c3f | ||
|
|
d3f307eac5 | ||
|
|
675a52b8d1 | ||
|
|
aee90e9c4e | ||
|
|
eb39b81d8d | ||
|
|
436df47104 | ||
|
|
e04e5e34c6 | ||
|
|
b57d685c03 | ||
|
|
6684172592 | ||
|
|
0257e74ff0 | ||
|
|
16c8865651 | ||
|
|
1a61aa2458 | ||
|
|
e543f07d8e | ||
|
|
96a0495a88 | ||
|
|
02befcd8a4 | ||
|
|
1edceeebdd |
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -21,6 +21,9 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
# 安装 pnpm
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
@@ -31,10 +34,10 @@ jobs:
|
||||
}
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
run: pnpm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App
|
||||
run: npm run build:win
|
||||
run: pnpm run build:win
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 清理不必要的构建产物
|
||||
|
||||
25
.github/workflows/release.yml
vendored
25
.github/workflows/release.yml
vendored
@@ -21,7 +21,10 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
node-version: "22.x"
|
||||
# 安装 pnpm
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
@@ -32,10 +35,10 @@ jobs:
|
||||
}
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
run: pnpm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for Windows
|
||||
run: npm run build:win || true
|
||||
run: pnpm run build:win || true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 上传构建产物
|
||||
@@ -66,6 +69,9 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
# 安装 pnpm
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
@@ -76,10 +82,10 @@ jobs:
|
||||
fi
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
run: pnpm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for macOS
|
||||
run: npm run build:mac || true
|
||||
run: pnpm run build:mac || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
@@ -111,6 +117,9 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
# 安装 pnpm
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
# 更新 Ubuntu 软件源
|
||||
- name: Ubuntu Update with sudo
|
||||
run: sudo apt-get update
|
||||
@@ -135,16 +144,18 @@ jobs:
|
||||
fi
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
run: pnpm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for Linux
|
||||
run: npm run build:linux || true
|
||||
run: pnpm run build:linux || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
|
||||
# 上传 Snap 包到 Snapcraft 商店
|
||||
- name: Publish Snap to Snap Store
|
||||
run: snapcraft upload dist/*.snap --release stable
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
|
||||
# 上传构建产物
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@@ -23,8 +23,16 @@ COPY --from=builder /app/out/renderer /usr/share/nginx/html
|
||||
|
||||
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
RUN apk add --no-cache npm
|
||||
COPY --from=builder /app/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
RUN npm install -g NeteaseCloudMusicApi
|
||||
RUN apk add --no-cache npm python3 youtube-dl \
|
||||
&& npm install -g @unblockneteasemusic/server NeteaseCloudMusicApi \
|
||||
&& wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp \
|
||||
&& chmod +x /usr/local/bin/yt-dlp \
|
||||
&& chmod +x /docker-entrypoint.sh
|
||||
|
||||
CMD nginx && npx NeteaseCloudMusicApi
|
||||
ENV NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["npx", "NeteaseCloudMusicApi"]
|
||||
@@ -1,6 +1,12 @@
|
||||
# SPlayer
|
||||
|
||||
> 一个简约的音乐播放器
|
||||
> A simple music player
|
||||
|
||||

|
||||

|
||||
[](https://github.com/imsyy/SPlayer/actions/workflows/release.yml)
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export default {
|
||||
"createGlobalState": true,
|
||||
"createInjectionState": true,
|
||||
"createReactiveFn": true,
|
||||
"createRef": true,
|
||||
"createReusableTemplate": true,
|
||||
"createSharedComposable": true,
|
||||
"createTemplatePromise": true,
|
||||
@@ -63,6 +64,7 @@ export default {
|
||||
"onBeforeUpdate": true,
|
||||
"onClickOutside": true,
|
||||
"onDeactivated": true,
|
||||
"onElementRemoval": true,
|
||||
"onErrorCaptured": true,
|
||||
"onKeyStroke": true,
|
||||
"onLongPress": true,
|
||||
@@ -145,6 +147,7 @@ export default {
|
||||
"useCloned": true,
|
||||
"useColorMode": true,
|
||||
"useConfirmDialog": true,
|
||||
"useCountdown": true,
|
||||
"useCounter": true,
|
||||
"useCssModule": true,
|
||||
"useCssVar": true,
|
||||
@@ -229,12 +232,14 @@ export default {
|
||||
"usePreferredDark": true,
|
||||
"usePreferredLanguages": true,
|
||||
"usePreferredReducedMotion": true,
|
||||
"usePreferredReducedTransparency": true,
|
||||
"usePrevious": true,
|
||||
"useRafFn": true,
|
||||
"useRefHistory": true,
|
||||
"useResizeObserver": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useSSRWidth": true,
|
||||
"useScreenOrientation": true,
|
||||
"useScreenSafeArea": true,
|
||||
"useScriptTag": true,
|
||||
|
||||
5
auto-imports.d.ts
vendored
5
auto-imports.d.ts
vendored
@@ -21,6 +21,7 @@ declare global {
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createRef: typeof import('@vueuse/core')['createRef']
|
||||
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||
@@ -55,6 +56,7 @@ declare global {
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||
@@ -137,6 +139,7 @@ declare global {
|
||||
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
const useCountdown: typeof import('@vueuse/core')['useCountdown']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
@@ -221,12 +224,14 @@ declare global {
|
||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
|
||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
|
||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||
|
||||
3
components.d.ts
vendored
3
components.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
@@ -91,11 +92,9 @@ declare module 'vue' {
|
||||
NOl: typeof import('naive-ui')['NOl']
|
||||
NP: typeof import('naive-ui')['NP']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NResult: typeof import('naive-ui')['NResult']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
|
||||
@@ -10,3 +10,21 @@ services:
|
||||
ports:
|
||||
- 25884:25884
|
||||
restart: always
|
||||
environment:
|
||||
# 所有变量都不是必填项
|
||||
# 网易云服务端 IP, 可在宿主机通过 ping music.163.com 获得
|
||||
- NETEASE_SERVER_IP=220.197.30.65
|
||||
# UnblockNeteaseMusic 使用的音源, 支持列表见 https://github.com/UnblockNeteaseMusic/server?tab=readme-ov-file#%E9%9F%B3%E6%BA%90%E6%B8%85%E5%8D%95
|
||||
- UNBLOCK_SOURCES=kugou kuwo bilibili
|
||||
# 可添加 UnblockNeteaseMusic 支持的任何环境变量, 支持列表见 https://github.com/UnblockNeteaseMusic/server?tab=readme-ov-file#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F
|
||||
- ENABLE_FLAC=false
|
||||
- ENABLE_HTTPDNS=false
|
||||
- BLOCK_ADS=true
|
||||
- DISABLE_UPGRADE_CHECK=false
|
||||
- DEVELOPMENT=false
|
||||
- FOLLOW_SOURCE_ORDER=true
|
||||
- JSON_LOG=false
|
||||
- NO_CACHE=false
|
||||
- SELECT_MAX_BR=true
|
||||
- LOG_LEVEL=info
|
||||
- SEARCH_ALBUM=true
|
||||
|
||||
29
docker-entrypoint.sh
Normal file
29
docker-entrypoint.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# start unblock service in the background
|
||||
npx unblockneteasemusic -p 80:443 -s -f ${NETEASE_SERVER_IP:-220.197.30.65} -o ${UNBLOCK_SOURCES:-kugou kuwo bilibili} 2>&1 &
|
||||
|
||||
# point the neteasemusic address to the unblock service
|
||||
if ! grep -q "music.163.com" /etc/hosts; then
|
||||
echo "127.0.0.1 music.163.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface.music.163.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface.music.163.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface3.music.163.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface3.music.163.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface.music.163.com.163jiasu.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface.music.163.com.163jiasu.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface3.music.163.com.163jiasu.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface3.music.163.com.163jiasu.com" >> /etc/hosts
|
||||
fi
|
||||
|
||||
# start the nginx daemon
|
||||
nginx
|
||||
|
||||
# start the main process
|
||||
exec "$@"
|
||||
@@ -1,9 +1,9 @@
|
||||
import { app, shell, BrowserWindow, BrowserWindowConstructorOptions } from "electron";
|
||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { electronApp } from "@electron-toolkit/utils";
|
||||
import { join } from "path";
|
||||
import { release, type } from "os";
|
||||
import { isDev, isMac, appName } from "./utils";
|
||||
import { registerAllShortcuts, unregisterShortcuts } from "./shortcut";
|
||||
import { unregisterShortcuts } from "./shortcut";
|
||||
import { initTray, MainTray } from "./tray";
|
||||
import { initThumbar, Thumbar } from "./thumbar";
|
||||
import { type StoreType, initStore } from "./store";
|
||||
@@ -75,8 +75,6 @@ class MainProcess {
|
||||
this.thumbar,
|
||||
this.store,
|
||||
);
|
||||
// 注册快捷键
|
||||
registerAllShortcuts(this.mainWindow!);
|
||||
});
|
||||
}
|
||||
// 创建窗口
|
||||
@@ -218,11 +216,6 @@ class MainProcess {
|
||||
this.showWindow();
|
||||
});
|
||||
|
||||
// 开发环境控制台
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
|
||||
// 自定义协议
|
||||
app.on("open-url", (_, url) => {
|
||||
console.log("Received custom protocol URL:", url);
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Thumbar } from "./thumbar";
|
||||
import { StoreType } from "./store";
|
||||
import { isDev, getFileID, getFileMD5 } from "./utils";
|
||||
import { isShortcutRegistered, registerShortcut, unregisterShortcuts } from "./shortcut";
|
||||
import { join, basename, resolve } from "path";
|
||||
import { join, basename, resolve, relative, isAbsolute } from "path";
|
||||
import { download } from "electron-dl";
|
||||
import { checkUpdate, startDownloadUpdate } from "./update";
|
||||
import fs from "fs/promises";
|
||||
@@ -173,8 +173,11 @@ const initWinIpcMain = (
|
||||
// 遍历音乐文件
|
||||
ipcMain.handle("get-music-files", async (_, dirPath: string) => {
|
||||
try {
|
||||
// 规范化路径
|
||||
const filePath = resolve(dirPath).replace(/\\/g, "/");
|
||||
console.info(`📂 Fetching music files from: ${filePath}`);
|
||||
// 查找指定目录下的所有音乐文件
|
||||
const musicFiles = await fg("**/*.{mp3,wav,flac}", { cwd: dirPath });
|
||||
const musicFiles = await fg("**/*.{mp3,wav,flac}", { cwd: filePath });
|
||||
// 解析元信息
|
||||
const metadataPromises = musicFiles.map(async (file) => {
|
||||
const filePath = join(dirPath, file);
|
||||
@@ -214,18 +217,19 @@ const initWinIpcMain = (
|
||||
// 获取音乐元信息
|
||||
ipcMain.handle("get-music-metadata", async (_, path: string) => {
|
||||
try {
|
||||
const { common, format } = await parseFile(path);
|
||||
const filePath = resolve(path).replace(/\\/g, "/");
|
||||
const { common, format } = await parseFile(filePath);
|
||||
return {
|
||||
// 文件名称
|
||||
fileName: basename(path),
|
||||
fileName: basename(filePath),
|
||||
// 文件大小
|
||||
fileSize: (await fs.stat(path)).size / (1024 * 1024),
|
||||
fileSize: (await fs.stat(filePath)).size / (1024 * 1024),
|
||||
// 元信息
|
||||
common,
|
||||
// 音质信息
|
||||
format,
|
||||
// md5
|
||||
md5: await getFileMD5(path),
|
||||
md5: await getFileMD5(filePath),
|
||||
};
|
||||
} catch (error) {
|
||||
log.error("❌ Error fetching music metadata:", error);
|
||||
@@ -236,24 +240,19 @@ const initWinIpcMain = (
|
||||
// 获取音乐歌词
|
||||
ipcMain.handle("get-music-lyric", async (_, path: string): Promise<string> => {
|
||||
try {
|
||||
const { common, native } = await parseFile(path);
|
||||
const filePath = resolve(path).replace(/\\/g, "/");
|
||||
const { common } = await parseFile(filePath);
|
||||
const lyric = common?.lyrics;
|
||||
if (lyric && lyric.length > 0) return String(lyric[0]);
|
||||
// 如果歌词数据不存在,尝试读取同名的 lrc 文件
|
||||
else {
|
||||
// 尝试读取 UNSYNCEDLYRICS
|
||||
const nativeTags = native["ID3v2.3"] || native["ID3v2.4"];
|
||||
const usltTag = nativeTags?.find((tag) => tag.id === "USLT");
|
||||
if (usltTag) return String(usltTag.value.text);
|
||||
// 如果歌词数据不存在,尝试读取同名的 lrc 文件
|
||||
else {
|
||||
const lrcFilePath = path.replace(/\.[^.]+$/, ".lrc");
|
||||
try {
|
||||
await fs.access(lrcFilePath);
|
||||
const lrcData = await fs.readFile(lrcFilePath, "utf-8");
|
||||
return lrcData || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
const lrcFilePath = filePath.replace(/\.[^.]+$/, ".lrc");
|
||||
try {
|
||||
await fs.access(lrcFilePath);
|
||||
const lrcData = await fs.readFile(lrcFilePath, "utf-8");
|
||||
return lrcData || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -623,6 +622,16 @@ const initLyricIpcMain = (
|
||||
lyricWin.setIgnoreMouseEvents(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否是子文件夹
|
||||
ipcMain.handle("check-if-subfolder", (_, localFilesPath: string[], selectedDir: string) => {
|
||||
const resolvedSelectedDir = resolve(selectedDir);
|
||||
const allPaths = localFilesPath.map((p) => resolve(p));
|
||||
return allPaths.some((existingPath) => {
|
||||
const relativePath = relative(existingPath, resolvedSelectedDir);
|
||||
return relativePath && !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// tray
|
||||
|
||||
@@ -1,123 +1,72 @@
|
||||
import {
|
||||
BrowserWindow,
|
||||
MenuItemConstructorOptions,
|
||||
Menu,
|
||||
session,
|
||||
dialog,
|
||||
ipcMain,
|
||||
} from "electron";
|
||||
import { BrowserWindow, session } from "electron";
|
||||
import icon from "../../public/icons/favicon.png?asset";
|
||||
import { join } from "path";
|
||||
|
||||
const openLoginWin = (mainWin: BrowserWindow) => {
|
||||
const loginSession = session.fromPartition("login-win");
|
||||
const openLoginWin = async (mainWin: BrowserWindow) => {
|
||||
let loginTimer: NodeJS.Timeout;
|
||||
const loginSession = session.fromPartition("persist:login");
|
||||
// 清除 Cookie
|
||||
await loginSession.clearStorageData({
|
||||
storages: ["cookies", "localstorage"],
|
||||
});
|
||||
const loginWin = new BrowserWindow({
|
||||
parent: mainWin,
|
||||
title: "登录网易云音乐",
|
||||
title: "登录网易云音乐( 若遇到无响应请关闭后重试 )",
|
||||
width: 1280,
|
||||
height: 800,
|
||||
center: true,
|
||||
modal: true,
|
||||
autoHideMenuBar: true,
|
||||
icon,
|
||||
// resizable: false,
|
||||
// movable: false,
|
||||
// minimizable: false,
|
||||
// maximizable: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
partition: "login-win",
|
||||
session: loginSession,
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
preload: join(__dirname, "../preload/index.mjs"),
|
||||
},
|
||||
});
|
||||
|
||||
// 打开网易云
|
||||
loginWin.loadURL("https://music.163.com/#/my/");
|
||||
loginWin.loadURL("https://music.163.com/#/login/");
|
||||
|
||||
// 阻止新窗口创建
|
||||
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: "未查找到登录信息,请重试",
|
||||
// 检查是否登录
|
||||
const checkLogin = async () => {
|
||||
try {
|
||||
loginWin.webContents.executeJavaScript(
|
||||
"document.title = '登录网易云音乐( 若遇到无响应请关闭后重试 )'",
|
||||
);
|
||||
// 是否登录?判断 MUSIC_U
|
||||
const MUSIC_U = await loginSession.cookies.get({
|
||||
name: "MUSIC_U",
|
||||
});
|
||||
return;
|
||||
if (MUSIC_U && MUSIC_U?.length > 0) {
|
||||
if (loginTimer) clearInterval(loginTimer);
|
||||
const value = `MUSIC_U=${MUSIC_U[0].value};`;
|
||||
// 发送回主进程
|
||||
mainWin?.webContents.send("send-cookies", value);
|
||||
loginWin.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
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);
|
||||
// 循环检查
|
||||
loginWin.webContents.once("did-finish-load", () => {
|
||||
loginWin.show();
|
||||
loginTimer = setInterval(checkLogin, 1000);
|
||||
loginWin.on("closed", () => {
|
||||
clearInterval(loginTimer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default openLoginWin;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BrowserWindow, globalShortcut } from "electron";
|
||||
import { isDev } from "./utils";
|
||||
import { globalShortcut } from "electron";
|
||||
import log from "../main/logger";
|
||||
|
||||
// 注册快捷键并检查
|
||||
@@ -29,15 +28,3 @@ export const unregisterShortcuts = () => {
|
||||
globalShortcut.unregisterAll();
|
||||
log.info("🚫 All shortcuts unregistered.");
|
||||
};
|
||||
|
||||
// 注册所有快捷键
|
||||
export const registerAllShortcuts = (win: BrowserWindow) => {
|
||||
// 开启控制台
|
||||
registerShortcut("CmdOrCtrl+Shift+I", () => {
|
||||
win.webContents.openDevTools({
|
||||
title: "SPlayer DevTools",
|
||||
// 客户端分离
|
||||
mode: isDev ? "right" : "detach",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
nativeImage,
|
||||
nativeTheme,
|
||||
} from "electron";
|
||||
import { isWin, isLinux, isDev, appName } from "./utils";
|
||||
import { isWin, appName } from "./utils";
|
||||
import { join } from "path";
|
||||
import log from "./logger";
|
||||
|
||||
@@ -269,12 +269,8 @@ class CreateTray implements MainTray {
|
||||
|
||||
export const initTray = (win: BrowserWindow, lyricWin: BrowserWindow) => {
|
||||
try {
|
||||
// 若为 MacOS
|
||||
if (isWin || isLinux || isDev) {
|
||||
log.info("🚀 Tray Process Startup");
|
||||
return new CreateTray(win, lyricWin);
|
||||
}
|
||||
return null;
|
||||
log.info("🚀 Tray Process Startup");
|
||||
return new CreateTray(win, lyricWin);
|
||||
} catch (error) {
|
||||
log.error("❌ Tray Process Error", error);
|
||||
return null;
|
||||
|
||||
@@ -46,7 +46,7 @@ const initAppServer = async () => {
|
||||
server.register(initNcmAPI, { prefix: "/api" });
|
||||
server.register(initUnblockAPI, { prefix: "/api" });
|
||||
// 启动端口
|
||||
const port = Number(import.meta.env["VITE_SERVER_PORT"] || 25884);
|
||||
const port = Number(process.env["VITE_SERVER_PORT"] || 25884);
|
||||
await server.listen({ port });
|
||||
log.info(`🌐 Starting AppServer on port ${port}`);
|
||||
return server;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
||||
import { pathCase } from "change-case";
|
||||
import NeteaseCloudMusicApi from "NeteaseCloudMusicApi";
|
||||
import NeteaseCloudMusicApi from "@neteaseapireborn/api";
|
||||
import log from "../../main/logger";
|
||||
|
||||
// 获取数据
|
||||
@@ -33,12 +33,12 @@ const initNcmAPI = async (fastify: FastifyInstance) => {
|
||||
// 主信息
|
||||
fastify.get("/netease", (_, reply) => {
|
||||
reply.send({
|
||||
name: "NeteaseCloudMusicApi",
|
||||
version: "4.20.0",
|
||||
description: "网易云音乐 Node.js API service",
|
||||
author: "@binaryify",
|
||||
name: "@neteaseapireborn/api",
|
||||
version: "4.29.2",
|
||||
description: "网易云音乐 API Enhanced",
|
||||
author: "@MoeFurina",
|
||||
license: "MIT",
|
||||
url: "https://gitlab.com/Binaryify/neteasecloudmusicapi",
|
||||
url: "https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
22
nginx.conf
22
nginx.conf
@@ -15,6 +15,22 @@ server {
|
||||
rewrite ^(.*)$ /index.html last;
|
||||
}
|
||||
|
||||
location /api/netease/song/url/v1 {
|
||||
proxy_buffers 16 64k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Host $remote_addr;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://localhost:3000/song/url/v1;
|
||||
|
||||
sub_filter '"url":"https://music.163.com' '"url":"/music/unblock';
|
||||
sub_filter_types application/json;
|
||||
sub_filter_once off;
|
||||
}
|
||||
|
||||
location /api/netease/ {
|
||||
proxy_buffers 16 64k;
|
||||
proxy_buffer_size 128k;
|
||||
@@ -26,4 +42,10 @@ server {
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://localhost:3000/;
|
||||
}
|
||||
|
||||
location /music/unblock/ {
|
||||
proxy_pass https://music.163.com/;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
}
|
||||
|
||||
97
package.json
97
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"productName": "SPlayer",
|
||||
"version": "3.0.0-beta.1",
|
||||
"version": "3.0.0-beta.2",
|
||||
"description": "A minimalist music player",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "imsyy",
|
||||
@@ -40,21 +40,21 @@
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@imsyy/color-utils": "^1.0.2",
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@pixi/app": "^7.4.2",
|
||||
"@pixi/core": "^7.4.2",
|
||||
"@pixi/display": "^7.4.2",
|
||||
"@pixi/filter-blur": "^7.4.2",
|
||||
"@neteaseapireborn/api": "^4.29.2",
|
||||
"@pixi/app": "^7.4.3",
|
||||
"@pixi/core": "^7.4.3",
|
||||
"@pixi/display": "^7.4.3",
|
||||
"@pixi/filter-blur": "^7.4.3",
|
||||
"@pixi/filter-bulge-pinch": "^5.1.1",
|
||||
"@pixi/filter-color-matrix": "^7.4.2",
|
||||
"@pixi/sprite": "^7.4.2",
|
||||
"@vueuse/core": "^12.0.0",
|
||||
"NeteaseCloudMusicApi": "^4.25.0",
|
||||
"axios": "^1.7.9",
|
||||
"@pixi/filter-color-matrix": "^7.4.3",
|
||||
"@pixi/sprite": "^7.4.3",
|
||||
"@vueuse/core": "^12.8.2",
|
||||
"axios": "^1.11.0",
|
||||
"change-case": "^5.4.4",
|
||||
"dayjs": "^1.11.13",
|
||||
"electron-dl": "^4.0.0",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"electron-updater": "^6.6.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"font-list": "^1.5.1",
|
||||
"get-port": "^7.1.0",
|
||||
@@ -65,59 +65,66 @@
|
||||
"jss-preset-default": "^10.10.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^14.1.4",
|
||||
"marked": "^15.0.8",
|
||||
"md5": "^2.3.0",
|
||||
"music-metadata": "7.14.0",
|
||||
"pinia": "^2.3.0",
|
||||
"pinia-plugin-persistedstate": "^4.1.3",
|
||||
"music-metadata": "10.5.1",
|
||||
"pinia": "^2.3.1",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"plyr": "^3.7.8",
|
||||
"vue-virt-list": "^1.5.5"
|
||||
"vue-virt-list": "^1.5.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@fastify/cookie": "^9.4.0",
|
||||
"@fastify/http-proxy": "^9.5.0",
|
||||
"@fastify/multipart": "^8.3.0",
|
||||
"@fastify/static": "^7.0.4",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/http-proxy": "^11.1.2",
|
||||
"@fastify/multipart": "^9.0.3",
|
||||
"@fastify/static": "^8.1.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/howler": "^2.2.12",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^22.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||
"@typescript-eslint/parser": "^8.18.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@types/node": "^22.14.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
||||
"@typescript-eslint/parser": "^8.30.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"ajv": "^8.17.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"electron": "^30.5.1",
|
||||
"electron-builder": "^25.1.8",
|
||||
"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.29.0",
|
||||
"naive-ui": "^2.40.3",
|
||||
"node-taglib-sharp": "^5.2.3",
|
||||
"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",
|
||||
"vite": "^5.4.11",
|
||||
"electron": "^35.1.5",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-log": "^5.3.4",
|
||||
"electron-vite": "^3.1.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"fast-glob": "^3.3.3",
|
||||
"fastify": "^5.3.1",
|
||||
"naive-ui": "^2.42.0",
|
||||
"node-taglib-sharp": "^6.0.1",
|
||||
"prettier": "^3.5.3",
|
||||
"sass": "^1.86.3",
|
||||
"terser": "^5.39.0",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-auto-import": "^0.19.0",
|
||||
"unplugin-vue-components": "^28.5.0",
|
||||
"vite": "^5.4.18",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-wasm": "^3.3.0",
|
||||
"vue": "^3.5.13",
|
||||
"vite-plugin-wasm": "^3.4.1",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-tsc": "2.0.29"
|
||||
"vue-tsc": "^3.0.5"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"dmg-builder": "25.1.8",
|
||||
"electron-builder-squirrel-windows": "25.1.8"
|
||||
}
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"core-js",
|
||||
"electron",
|
||||
"esbuild",
|
||||
"vue-demi"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
4045
pnpm-lock.yaml
generated
4045
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,10 @@
|
||||
"id": 3136952023,
|
||||
"name": "私人雷达"
|
||||
},
|
||||
{
|
||||
"id": 8402996200,
|
||||
"name": "会员雷达"
|
||||
},
|
||||
{
|
||||
"id": 5320167908,
|
||||
"name": "时光雷达"
|
||||
|
||||
@@ -176,11 +176,20 @@ const menuOptions = computed<MenuOption[] | MenuGroupOption[]>(() => {
|
||||
children: [...likedPlaylist.value],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
: [
|
||||
{
|
||||
key: "local",
|
||||
link: "local",
|
||||
label: "本地歌曲",
|
||||
show: isElectron,
|
||||
icon: renderIcon("FolderMusic"),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// 生成歌单列表
|
||||
const renderPlaylist = (playlist: CoverType[], showCover: boolean) => {
|
||||
if (!isLogin()) return [];
|
||||
return playlist.map((playlist) => ({
|
||||
key: playlist.id,
|
||||
label: () =>
|
||||
|
||||
@@ -363,7 +363,6 @@ const getListData = async (id: number): Promise<SongType[]> => {
|
||||
&:hover {
|
||||
background-color: rgba(var(--primary), 0.12);
|
||||
.cover {
|
||||
border-radius: 16px 16px 0 0;
|
||||
.cover-img {
|
||||
transform: scale(1.1);
|
||||
filter: brightness(0.8);
|
||||
|
||||
@@ -38,7 +38,7 @@ const openWeb = () => {
|
||||
window.$dialog.info({
|
||||
title: "使用前告知",
|
||||
content:
|
||||
"请知悉,该功能仍旧无法确保账号的安全性!请自行决定是否使用!如遇打开窗口后页面出现白屏或者无法点击等情况,请关闭后再试。在登录完成后,请点击菜单栏中的 “登录完成” 按钮以完成登录( 通常位于窗口的左上角,macOS 位于顶部的全局菜单栏中 )",
|
||||
"请知悉,该功能仍旧无法确保账号的安全性!请自行决定是否使用!如遇打开窗口后页面出现白屏或者无法点击等情况,请关闭后重试",
|
||||
positiveText: "我已了解",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () => window.electron.ipcRenderer.send("open-login-web"),
|
||||
|
||||
@@ -200,8 +200,8 @@ const getSongInfo = async () => {
|
||||
name: common.title || "",
|
||||
artist: common.artist || "",
|
||||
album: common.album || "",
|
||||
alia: common.comment?.[0] || "",
|
||||
lyric: common.lyrics?.[0] || "",
|
||||
alia: (common.comment?.[0] as string) || "",
|
||||
lyric: (common.lyrics?.[0] as unknown as string) || "",
|
||||
type: format.codec,
|
||||
duration: format.duration ? Number(format.duration.toFixed(2)) : 0,
|
||||
size: fileSize,
|
||||
@@ -212,7 +212,7 @@ const getSongInfo = async () => {
|
||||
// 获取封面
|
||||
const coverBuff = common.picture?.[0]?.data || "";
|
||||
const coverType = common.picture?.[0]?.format || "";
|
||||
if (coverBuff) coverData.value = blob.createBlobURL(coverBuff, coverType, path);
|
||||
if (coverBuff) coverData.value = blob.createBlobURL(coverBuff as Buffer, coverType, path);
|
||||
};
|
||||
|
||||
// 在线匹配
|
||||
|
||||
@@ -324,6 +324,7 @@ onBeforeUnmount(() => {
|
||||
padding: 10px 16px;
|
||||
transform: scale(0.86);
|
||||
transform-origin: left center;
|
||||
will-change: filter, opacity, transform;
|
||||
transition:
|
||||
filter 0.35s,
|
||||
opacity 0.35s,
|
||||
|
||||
@@ -60,6 +60,7 @@ const { start: dynamicCoverStart, stop: dynamicCoverStop } = useTimeoutFn(
|
||||
const getDynamicCover = async () => {
|
||||
if (
|
||||
isLogin() !== 1 ||
|
||||
musicStore.playSong.path ||
|
||||
!musicStore.playSong.id ||
|
||||
!settingStore.dynamicCover ||
|
||||
settingStore.playerType !== "cover"
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
/>
|
||||
</Transition>
|
||||
<!-- 默认内容 -->
|
||||
<SearchDefault @to-search="toSearch" />
|
||||
<SearchDefault v-if="settingStore.useOnlineService" @to-search="toSearch" />
|
||||
<!-- 搜索结果 -->
|
||||
<SearchSuggest @to-search="toSearch" />
|
||||
<!-- 右键菜单 -->
|
||||
@@ -37,7 +37,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStatusStore, useDataStore } from "@/stores";
|
||||
import { useStatusStore, useDataStore, useSettingStore } from "@/stores";
|
||||
import { searchDefault } from "@/api/search";
|
||||
import SearchInpMenu from "@/components/Menu/SearchInpMenu.vue";
|
||||
import player from "@/utils/player";
|
||||
@@ -47,13 +47,16 @@ import { formatSongsList } from "@/utils/format";
|
||||
const router = useRouter();
|
||||
const dataStore = useDataStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
// 右键菜单
|
||||
const searchInpMenuRef = ref<InstanceType<typeof SearchInpMenu> | null>(null);
|
||||
|
||||
// 搜索框数据
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null);
|
||||
const searchPlaceholder = ref<string>("搜索音乐 / 视频");
|
||||
const searchPlaceholder = ref<string>(
|
||||
settingStore.useOnlineService ? "搜索音乐 / 视频" : "搜索本地音乐",
|
||||
);
|
||||
const searchRealkeyword = ref<string>("");
|
||||
|
||||
// 搜索框输入限制
|
||||
@@ -94,14 +97,23 @@ const updatePlaceholder = async () => {
|
||||
|
||||
// 前往搜索
|
||||
const toSearch = async (key: any, type: string = "keyword") => {
|
||||
// 关闭搜索框
|
||||
statusStore.searchFocus = false;
|
||||
searchInputRef.value?.blur();
|
||||
// 未输入内容且不存在推荐
|
||||
if (!key && searchPlaceholder.value === "搜索音乐 / 视频") return;
|
||||
if (!key && searchPlaceholder.value !== "搜索音乐 / 视频" && searchRealkeyword.value) {
|
||||
key = searchRealkeyword.value?.trim();
|
||||
}
|
||||
// 关闭搜索框
|
||||
statusStore.searchFocus = false;
|
||||
searchInputRef.value?.blur();
|
||||
// 本地搜索
|
||||
if (!settingStore.useOnlineService) {
|
||||
// 跳转本地搜索页面
|
||||
router.push({
|
||||
name: "search",
|
||||
query: { keyword: key },
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 更新推荐
|
||||
updatePlaceholder();
|
||||
// 前往搜索
|
||||
@@ -143,9 +155,10 @@ const toSearch = async (key: any, type: string = "keyword") => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updatePlaceholder();
|
||||
// 每分钟更新
|
||||
useIntervalFn(updatePlaceholder, 60 * 1000);
|
||||
if (settingStore.useOnlineService) {
|
||||
useIntervalFn(updatePlaceholder, 60 * 1000, { immediate: true });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -27,7 +27,11 @@
|
||||
<!-- 搜索建议 -->
|
||||
<Transition name="fade" mode="out-in" @after-leave="calcSearchSuggestHeights">
|
||||
<div
|
||||
v-if="Object.keys(searchSuggestData)?.length && searchSuggestData?.order"
|
||||
v-if="
|
||||
Object.keys(searchSuggestData)?.length &&
|
||||
searchSuggestData?.order &&
|
||||
settingStore.useOnlineService
|
||||
"
|
||||
ref="searchSuggestRef"
|
||||
class="all-suggest"
|
||||
>
|
||||
@@ -60,13 +64,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { searchSuggest } from "@/api/search";
|
||||
import { useStatusStore } from "@/stores";
|
||||
import { useStatusStore, useSettingStore } from "@/stores";
|
||||
|
||||
const emit = defineEmits<{
|
||||
toSearch: [key: number | string, type: string];
|
||||
}>();
|
||||
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
// 搜索建议数据
|
||||
const searchSuggestData = ref<any>({});
|
||||
@@ -125,7 +130,7 @@ const calcSearchSuggestHeights = () => {
|
||||
watchDebounced(
|
||||
() => statusStore.searchInputValue,
|
||||
(val) => {
|
||||
if (!val || val === "") return;
|
||||
if (!val || val === "" || !settingStore.useOnlineService) return;
|
||||
getSearchSuggest(val);
|
||||
},
|
||||
{ debounce: 300 },
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</n-tag>
|
||||
<n-text :depth="3" class="time">{{ newVersion?.time }}</n-text>
|
||||
</n-flex>
|
||||
<div class="markdown-body" v-html="newVersion?.changelog" />
|
||||
<div class="markdown-body" v-html="newVersion?.changelog" @click="jumpLink" />
|
||||
</n-card>
|
||||
</n-collapse-transition>
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
</n-tag>
|
||||
<n-text :depth="3" class="time">{{ item?.time }}</n-text>
|
||||
</n-flex>
|
||||
<div class="markdown-body" v-html="item?.changelog" />
|
||||
<div class="markdown-body" v-html="item?.changelog" @click="jumpLink" />
|
||||
</n-card>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
@@ -126,6 +126,16 @@ const checkUpdate = debounce(
|
||||
{ leading: true, trailing: false },
|
||||
);
|
||||
|
||||
// 链接跳转
|
||||
const jumpLink = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName !== "A") {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
openLink((target as HTMLAnchorElement).href);
|
||||
};
|
||||
|
||||
// 获取更新日志
|
||||
const getUpdateData = async () => (updateData.value = await getUpdateLog());
|
||||
|
||||
|
||||
@@ -142,13 +142,7 @@
|
||||
<n-text class="name">在线服务</n-text>
|
||||
<n-text class="tip" :depth="3">是否开启软件的在线服务</n-text>
|
||||
</div>
|
||||
<n-switch
|
||||
class="set"
|
||||
:disabled="true"
|
||||
:value="useOnlineService"
|
||||
:round="false"
|
||||
@update:value="modeChange"
|
||||
/>
|
||||
<n-switch class="set" :value="useOnlineService" :round="false" @update:value="modeChange" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
@@ -256,12 +250,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SelectOption } from "naive-ui";
|
||||
import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
|
||||
import { isElectron } from "@/utils/helper";
|
||||
import { useDataStore, useMusicStore, useSettingStore, useStatusStore } from "@/stores";
|
||||
import { isDev, isElectron } from "@/utils/helper";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import themeColor from "@/assets/data/themeColor.json";
|
||||
import player from "@/utils/player";
|
||||
|
||||
const dataStore = useDataStore();
|
||||
const musicStore = useMusicStore();
|
||||
const settingStore = useSettingStore();
|
||||
const statusStore = useStatusStore();
|
||||
@@ -319,26 +314,40 @@ const modeChange = (val: boolean) => {
|
||||
if (val) {
|
||||
window.$dialog.warning({
|
||||
title: "开启在线服务",
|
||||
content: "确定开启软件的在线服务?更改将在重启后生效!",
|
||||
content: "确定开启软件的在线服务?更改将在热重载后生效!",
|
||||
positiveText: "开启",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () => {
|
||||
useOnlineService.value = true;
|
||||
settingStore.useOnlineService = true;
|
||||
// 清理播放数据
|
||||
dataStore.$reset();
|
||||
musicStore.$reset();
|
||||
// 清空本地数据
|
||||
localStorage.removeItem("data-store");
|
||||
localStorage.removeItem("music-store");
|
||||
// 热重载
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
window.$dialog.warning({
|
||||
title: "关闭在线服务",
|
||||
content:
|
||||
"确定关闭软件的在线服务?将关闭包括搜索、登录、在线音乐播放等在内的全部在线服务,软件将会变为本地播放器!更改将在软件重启后生效!",
|
||||
"确定关闭软件的在线服务?将关闭包括搜索、登录、在线音乐播放等在内的全部在线服务,并且将会退出登录状态,软件将会变为本地播放器!更改将在重启后生效!",
|
||||
positiveText: "关闭",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () => {
|
||||
useOnlineService.value = false;
|
||||
settingStore.useOnlineService = false;
|
||||
// 清理播放数据
|
||||
dataStore.$reset();
|
||||
musicStore.$reset();
|
||||
// 清空本地数据
|
||||
localStorage.removeItem("data-store");
|
||||
localStorage.removeItem("music-store");
|
||||
// 重启
|
||||
window.electron.ipcRenderer.send("win-reload");
|
||||
if (!isDev) window.electron.ipcRenderer.send("win-reload");
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
useOnlineService.value = true;
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
</div>
|
||||
<n-switch class="set" v-model:value="settingStore.showLocalCover" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">显示本地默认歌曲目录</n-text>
|
||||
</div>
|
||||
<n-switch class="set" v-model:value="settingStore.showDefaultLocalPath" :round="false" />
|
||||
</n-card>
|
||||
<n-card class="set-item" id="local-list-choose" content-style="flex-direction: column">
|
||||
<n-flex justify="space-between">
|
||||
<div class="label">
|
||||
|
||||
@@ -73,13 +73,6 @@
|
||||
</div>
|
||||
<n-switch v-model:value="settingStore.useSongUnlock" 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-switch v-model:value="settingStore.scrobbleSong" class="set" :round="false" />
|
||||
</n-card>
|
||||
<n-card v-if="isElectron" class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">音频输出设备</n-text>
|
||||
@@ -189,7 +182,13 @@
|
||||
<n-card class="set-item">
|
||||
<div class="label">
|
||||
<n-text class="name">音乐频谱</n-text>
|
||||
<n-text class="tip" :depth="3">开启音乐频谱会极大影响性能,如遇问题请关闭</n-text>
|
||||
<n-text class="tip" :depth="3">
|
||||
{{
|
||||
isElectron
|
||||
? "开启音乐频谱会影响性能或音频输出切换等功能,如遇问题请关闭"
|
||||
: "开启可能会造成无法播放或其他问题,如遇任何问题请关闭"
|
||||
}}
|
||||
</n-text>
|
||||
</div>
|
||||
<n-switch
|
||||
class="set"
|
||||
@@ -253,30 +252,45 @@ const songLevelData = {
|
||||
value: "higher",
|
||||
},
|
||||
exhigh: {
|
||||
label: "极高 HQ",
|
||||
tip: "近 CD 品质的细节体验,最高 320kbps",
|
||||
label: "极高 (HQ)",
|
||||
tip: "近CD品质的细节体验,最高320kbps",
|
||||
value: "exhigh",
|
||||
},
|
||||
lossless: {
|
||||
label: "无损 SQ",
|
||||
tip: "高保真无损音质,最高 48kHz/16bit",
|
||||
label: "无损 (SQ)",
|
||||
tip: "高保真无损音质,最高48kHz/16bit",
|
||||
value: "lossless",
|
||||
},
|
||||
hires: {
|
||||
label: "高清臻音 Spatial Audio",
|
||||
tip: "环绕声体验,声音听感增强,96kHz/24bit",
|
||||
label: "高解析度无损 (Hi-Res)",
|
||||
tip: "更饱满清晰的高解析度音质,最高192kHz/24bit",
|
||||
value: "hires",
|
||||
},
|
||||
jyeffect: {
|
||||
label: "高清臻音 (Spatial Audio)",
|
||||
tip: "声音听感增强,96kHz/24bit",
|
||||
value: "jyeffect",
|
||||
},
|
||||
jymaster: {
|
||||
label: "超清母带 Master",
|
||||
label: "超清母带 (Master)",
|
||||
tip: "还原音频细节,192kHz/24bit",
|
||||
value: "jymaster",
|
||||
},
|
||||
sky: {
|
||||
label: "沉浸环绕声 Surround Audio",
|
||||
tip: "沉浸式体验,最高 5.1 声道",
|
||||
label: "沉浸环绕声 (Surround Audio)",
|
||||
tip: "沉浸式空间环绕音感,最高5.1声道",
|
||||
value: "sky",
|
||||
},
|
||||
vivid: {
|
||||
label: "臻音全景声 (Audio Vivid)",
|
||||
tip: "极致沉浸三维空间音频,最高7.1.4声道",
|
||||
value: "vivid",
|
||||
},
|
||||
dolby: {
|
||||
label: "杜比全景声 (Dolby Atmos)",
|
||||
tip: "杜比全景声音乐,沉浸式聆听体验",
|
||||
value: "dolby",
|
||||
},
|
||||
};
|
||||
|
||||
// 获取全部输出设备
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</Transition>
|
||||
<!-- 真实图片 -->
|
||||
<img
|
||||
v-if="src"
|
||||
v-if="imgSrc"
|
||||
ref="imgRef"
|
||||
:src="imgSrc"
|
||||
:key="imgSrc"
|
||||
@@ -69,10 +69,27 @@ const imageError = (e: Event) => {
|
||||
};
|
||||
|
||||
// 可视状态变化
|
||||
watchOnce(isCanLook, (show) => {
|
||||
emit("update:show", show);
|
||||
if (show) imgSrc.value = props.src;
|
||||
});
|
||||
watch(
|
||||
isCanLook,
|
||||
(show) => {
|
||||
emit("update:show", show);
|
||||
if (show) imgSrc.value = props.src;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 监听 src 变化
|
||||
watch(
|
||||
() => props.src,
|
||||
(val) => {
|
||||
isLoaded.value = false;
|
||||
if (isCanLook.value) {
|
||||
imgSrc.value = val;
|
||||
} else {
|
||||
imgSrc.value = undefined;
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { formatCategoryList } from "@/utils/format";
|
||||
|
||||
interface ListState {
|
||||
playList: SongType[];
|
||||
originalPlayList: SongType[];
|
||||
historyList: SongType[];
|
||||
cloudPlayList: SongType[];
|
||||
searchHistory: string[];
|
||||
@@ -54,6 +55,8 @@ export const useDataStore = defineStore("data", {
|
||||
state: (): ListState => ({
|
||||
// 播放列表
|
||||
playList: [],
|
||||
// 原始播放列表
|
||||
originalPlayList: [],
|
||||
// 播放历史
|
||||
historyList: [],
|
||||
// 搜索历史
|
||||
@@ -157,6 +160,29 @@ export const useDataStore = defineStore("data", {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
// 保存原始播放列表
|
||||
async setOriginalPlayList(data: SongType[]): Promise<void> {
|
||||
const snapshot = cloneDeep(data);
|
||||
this.originalPlayList = snapshot;
|
||||
await musicDB.setItem("originalPlayList", snapshot);
|
||||
},
|
||||
// 获取原始播放列表
|
||||
async getOriginalPlayList(): Promise<SongType[] | null> {
|
||||
if (Array.isArray(this.originalPlayList) && this.originalPlayList.length > 0) {
|
||||
return this.originalPlayList;
|
||||
}
|
||||
const data = (await musicDB.getItem("originalPlayList")) as SongType[] | null;
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
this.originalPlayList = data;
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// 清除原始播放列表
|
||||
async clearOriginalPlayList(): Promise<void> {
|
||||
this.originalPlayList = [];
|
||||
await musicDB.setItem("originalPlayList", []);
|
||||
},
|
||||
// 新增下一首播放歌曲
|
||||
async setNextPlaySong(song: SongType, index: number): Promise<number> {
|
||||
// 若为空,则直接添加
|
||||
|
||||
@@ -86,6 +86,7 @@ interface SettingState {
|
||||
dynamicCover: boolean;
|
||||
useKeepAlive: boolean;
|
||||
excludeKeywords: string[];
|
||||
showDefaultLocalPath: boolean;
|
||||
}
|
||||
|
||||
export const useSettingStore = defineStore("setting", {
|
||||
@@ -148,6 +149,7 @@ export const useSettingStore = defineStore("setting", {
|
||||
excludeKeywords: keywords, // 排除歌词关键字
|
||||
// 本地
|
||||
localFilesPath: [],
|
||||
showDefaultLocalPath: true, // 显示默认本地路径
|
||||
localSeparators: ["/", "&"],
|
||||
showLocalCover: true,
|
||||
// 下载
|
||||
|
||||
@@ -29,6 +29,7 @@ import { radioSub } from "@/api/radio";
|
||||
*/
|
||||
export const isLogin = (): 0 | 1 | 2 => {
|
||||
const dataStore = useDataStore();
|
||||
if (!dataStore.userLoginStatus) return 0;
|
||||
if (dataStore.loginType === "uid") return 2;
|
||||
return getCookie("MUSIC_U") ? 1 : 0;
|
||||
};
|
||||
|
||||
@@ -273,9 +273,11 @@ export const changeLocalPath = async (delIndex?: number) => {
|
||||
// 检查是否为子文件夹
|
||||
const defaultMusicPath = await window.electron.ipcRenderer.invoke("get-default-dir", "music");
|
||||
const allPath = [defaultMusicPath, ...settingStore.localFilesPath];
|
||||
const isSubfolder = allPath.some((existingPath) => {
|
||||
return selectedDir.startsWith(existingPath);
|
||||
});
|
||||
const isSubfolder = await window.electron.ipcRenderer.invoke(
|
||||
"check-if-subfolder",
|
||||
allPath,
|
||||
selectedDir,
|
||||
);
|
||||
if (!isSubfolder) {
|
||||
settingStore.localFilesPath.push(selectedDir);
|
||||
} else {
|
||||
|
||||
@@ -12,7 +12,6 @@ import { heartRateList } from "@/api/playlist";
|
||||
import { formatSongsList } from "./format";
|
||||
import { isLogin } from "./auth";
|
||||
import { openUserLogin } from "./modal";
|
||||
import { scrobble } from "@/api/user";
|
||||
import { personalFm, personalFmToTrash } from "@/api/rec";
|
||||
import blob from "./blob";
|
||||
|
||||
@@ -41,6 +40,17 @@ class Player {
|
||||
// 初始化媒体会话
|
||||
this.initMediaSession();
|
||||
}
|
||||
/**
|
||||
* 洗牌数组(Fisher-Yates)
|
||||
*/
|
||||
private shuffleArray<T>(arr: T[]): T[] {
|
||||
const copy = arr.slice();
|
||||
for (let i = copy.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[copy[i], copy[j]] = [copy[j], copy[i]];
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
@@ -168,12 +178,22 @@ class Player {
|
||||
const keyWord = songData.name + "-" + artist;
|
||||
if (!songId || !keyWord) return null;
|
||||
// 尝试解锁
|
||||
const [neteaseUrl, kuwoUrl] = await Promise.all([
|
||||
const results = await Promise.allSettled([
|
||||
unlockSongUrl(songId, keyWord, "netease"),
|
||||
unlockSongUrl(songId, keyWord, "kuwo"),
|
||||
]);
|
||||
if (neteaseUrl.code === 200 && neteaseUrl.url !== "") return neteaseUrl.url;
|
||||
if (kuwoUrl.code === 200 && kuwoUrl.url !== "") return kuwoUrl.url;
|
||||
// 解析结果
|
||||
const [neteaseRes, kuwoRes] = results;
|
||||
if (
|
||||
neteaseRes.status === "fulfilled" &&
|
||||
neteaseRes.value.code === 200 &&
|
||||
neteaseRes.value.url
|
||||
) {
|
||||
return neteaseRes.value.url;
|
||||
}
|
||||
if (kuwoRes.status === "fulfilled" && kuwoRes.value.code === 200 && kuwoRes.value.url) {
|
||||
return kuwoRes.value.url;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error in getUnlockSongUrl", error);
|
||||
@@ -241,14 +261,17 @@ class Player {
|
||||
// 获取数据
|
||||
const dataStore = useDataStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
const playSongData = this.getPlaySongData();
|
||||
// 获取配置
|
||||
const { seek } = options;
|
||||
// 初次加载
|
||||
this.player.once("load", () => {
|
||||
// 允许跨域
|
||||
const audioDom = this.getAudioDom();
|
||||
audioDom.crossOrigin = "anonymous";
|
||||
if (settingStore.showSpectrums) {
|
||||
const audioDom = this.getAudioDom();
|
||||
audioDom.crossOrigin = "anonymous";
|
||||
}
|
||||
// 恢复进度( 需距离本曲结束大于 2 秒 )
|
||||
if (seek && statusStore.duration - statusStore.currentTime > 2) this.setSeek(seek);
|
||||
// 更新状态
|
||||
@@ -587,10 +610,11 @@ class Player {
|
||||
return;
|
||||
}
|
||||
this.player.play();
|
||||
statusStore.playStatus = true;
|
||||
// 淡入
|
||||
await new Promise<void>((resolve) => {
|
||||
this.player.once("play", () => {
|
||||
// 在淡入开始时立即设置播放状态
|
||||
statusStore.playStatus = true;
|
||||
this.player.fade(0, statusStore.playVolume, this.getFadeTime());
|
||||
resolve();
|
||||
});
|
||||
@@ -608,12 +632,14 @@ class Player {
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即设置播放状态
|
||||
if (changeStatus) statusStore.playStatus = false;
|
||||
|
||||
// 淡出
|
||||
await new Promise<void>((resolve) => {
|
||||
this.player.fade(statusStore.playVolume, 0, this.getFadeTime());
|
||||
this.player.once("fade", () => {
|
||||
this.player.pause();
|
||||
if (changeStatus) statusStore.playStatus = false;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
@@ -644,8 +670,6 @@ class Player {
|
||||
const playListLength = playList.length;
|
||||
// 播放列表是否为空
|
||||
if (playListLength === 0) throw new Error("Play list is empty");
|
||||
// 打卡
|
||||
this.scrobbleSong();
|
||||
// 若为私人FM
|
||||
if (statusStore.personalFmMode) {
|
||||
await this.initPersonalFM(true);
|
||||
@@ -657,19 +681,15 @@ class Player {
|
||||
this.setSeek(0);
|
||||
await this.play();
|
||||
}
|
||||
// 列表循环或处于心动模式
|
||||
if (playSongMode === "repeat" || playHeartbeatMode || playSong.type === "radio") {
|
||||
// 列表循环或处于心动模式或随机模式
|
||||
if (
|
||||
playSongMode === "repeat" ||
|
||||
playSongMode === "shuffle" ||
|
||||
playHeartbeatMode ||
|
||||
playSong.type === "radio"
|
||||
) {
|
||||
statusStore.playIndex += type === "next" ? 1 : -1;
|
||||
}
|
||||
// 随机播放
|
||||
else if (playSongMode === "shuffle") {
|
||||
let newIndex: number;
|
||||
// 确保不会随机到同一首
|
||||
do {
|
||||
newIndex = Math.floor(Math.random() * playListLength);
|
||||
} while (newIndex === statusStore.playIndex);
|
||||
statusStore.playIndex = newIndex;
|
||||
}
|
||||
// 单曲循环
|
||||
else if (playSongMode === "repeat-once") {
|
||||
statusStore.lyricIndex = -1;
|
||||
@@ -698,28 +718,65 @@ class Player {
|
||||
* 切换播放模式
|
||||
* @param mode 播放模式 repeat / repeat-once / shuffle
|
||||
*/
|
||||
togglePlayMode(mode: PlayModeType | false) {
|
||||
async togglePlayMode(mode: PlayModeType | false) {
|
||||
const statusStore = useStatusStore();
|
||||
const dataStore = useDataStore();
|
||||
const musicStore = useMusicStore();
|
||||
// 退出心动模式
|
||||
if (statusStore.playHeartbeatMode) this.toggleHeartMode(false);
|
||||
// 若传入了指定模式
|
||||
// 计算目标模式
|
||||
let targetMode: PlayModeType;
|
||||
if (mode) {
|
||||
statusStore.playSongMode = mode;
|
||||
targetMode = mode;
|
||||
} else {
|
||||
switch (statusStore.playSongMode) {
|
||||
case "repeat":
|
||||
statusStore.playSongMode = "repeat-once";
|
||||
targetMode = "repeat-once";
|
||||
break;
|
||||
case "shuffle":
|
||||
statusStore.playSongMode = "repeat";
|
||||
targetMode = "repeat";
|
||||
break;
|
||||
case "repeat-once":
|
||||
statusStore.playSongMode = "shuffle";
|
||||
targetMode = "shuffle";
|
||||
break;
|
||||
default:
|
||||
statusStore.playSongMode = "repeat";
|
||||
targetMode = "repeat";
|
||||
}
|
||||
}
|
||||
// 进入随机模式:保存原顺序并打乱当前歌单
|
||||
if (targetMode === "shuffle" && statusStore.playSongMode !== "shuffle") {
|
||||
const currentList = dataStore.playList;
|
||||
if (currentList && currentList.length > 1) {
|
||||
const currentSongId = musicStore.playSong?.id;
|
||||
await dataStore.setOriginalPlayList(currentList);
|
||||
const shuffled = this.shuffleArray(currentList);
|
||||
await dataStore.setPlayList(shuffled);
|
||||
if (currentSongId) {
|
||||
const newIndex = shuffled.findIndex((s: any) => s?.id === currentSongId);
|
||||
if (newIndex !== -1) useStatusStore().playIndex = newIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 离开随机模式:恢复到原顺序
|
||||
if (
|
||||
statusStore.playSongMode === "shuffle" &&
|
||||
(targetMode === "repeat" || targetMode === "repeat-once")
|
||||
) {
|
||||
const original = await dataStore.getOriginalPlayList();
|
||||
if (original && original.length) {
|
||||
const currentSongId = musicStore.playSong?.id;
|
||||
await dataStore.setPlayList(original);
|
||||
if (currentSongId) {
|
||||
const origIndex = original.findIndex((s: any) => s?.id === currentSongId);
|
||||
useStatusStore().playIndex = origIndex !== -1 ? origIndex : 0;
|
||||
} else {
|
||||
useStatusStore().playIndex = 0;
|
||||
}
|
||||
await dataStore.clearOriginalPlayList();
|
||||
}
|
||||
}
|
||||
// 应用模式
|
||||
statusStore.playSongMode = targetMode;
|
||||
this.playModeSyncIpc();
|
||||
}
|
||||
/**
|
||||
@@ -850,11 +907,19 @@ class Player {
|
||||
const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
// 获取配置
|
||||
const { showTip, scrobble, play } = options;
|
||||
// 打卡
|
||||
if (scrobble) this.scrobbleSong();
|
||||
const { showTip, play } = options;
|
||||
|
||||
// 处理随机播放模式
|
||||
let processedData = cloneDeep(data);
|
||||
if (statusStore.playSongMode === "shuffle") {
|
||||
// 保存原始播放列表
|
||||
await dataStore.setOriginalPlayList(cloneDeep(data));
|
||||
// 随机排序
|
||||
processedData = this.shuffleArray(processedData);
|
||||
}
|
||||
|
||||
// 更新列表
|
||||
await dataStore.setPlayList(cloneDeep(data));
|
||||
await dataStore.setPlayList(processedData);
|
||||
// 关闭特殊模式
|
||||
if (statusStore.playHeartbeatMode) this.toggleHeartMode(false);
|
||||
if (statusStore.personalFmMode) statusStore.personalFmMode = false;
|
||||
@@ -864,15 +929,15 @@ class Player {
|
||||
if (musicStore.playSong.id === song.id) {
|
||||
if (play) await this.play();
|
||||
} else {
|
||||
// 查找索引
|
||||
statusStore.playIndex = data.findIndex((item) => item.id === song.id);
|
||||
// 查找索引(在处理后的列表中查找)
|
||||
statusStore.playIndex = processedData.findIndex((item) => item.id === song.id);
|
||||
// 播放
|
||||
await this.pause(false);
|
||||
await this.initPlayer();
|
||||
}
|
||||
} else {
|
||||
statusStore.playIndex =
|
||||
statusStore.playSongMode === "shuffle" ? Math.floor(Math.random() * data.length) : 0;
|
||||
statusStore.playSongMode === "shuffle" ? Math.floor(Math.random() * processedData.length) : 0;
|
||||
// 播放
|
||||
await this.pause(false);
|
||||
await this.initPlayer();
|
||||
@@ -1106,29 +1171,6 @@ class Player {
|
||||
this.message?.destroy();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 听歌打卡
|
||||
*/
|
||||
async scrobbleSong() {
|
||||
const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
try {
|
||||
if (!isLogin()) return;
|
||||
if (!settingStore.scrobbleSong) return;
|
||||
// 获取所需数据
|
||||
const playSongData = this.getPlaySongData();
|
||||
if (!playSongData) return;
|
||||
const { id, name } = playSongData;
|
||||
const sourceid = musicStore.playPlaylistId;
|
||||
const time = statusStore.duration;
|
||||
// 网易云打卡
|
||||
console.log("打卡:", id, name, sourceid, time);
|
||||
await scrobble(id, sourceid, time);
|
||||
} catch (error) {
|
||||
console.error("Failed to scrobble song:", error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 初始化私人FM
|
||||
* @param playNext 是否播放下一首
|
||||
|
||||
@@ -106,7 +106,14 @@
|
||||
<template #prefix>
|
||||
<SvgIcon :size="20" name="FolderMusic" />
|
||||
</template>
|
||||
<n-thing :title="defaultMusicPath" description="系统默认音乐文件夹,无法更改" />
|
||||
<template #suffix>
|
||||
<n-switch
|
||||
v-model:value="settingStore.showDefaultLocalPath"
|
||||
:round="false"
|
||||
class="set"
|
||||
/>
|
||||
</template>
|
||||
<n-thing :title="defaultMusicPath" description="系统默认音乐文件夹" />
|
||||
</n-list-item>
|
||||
<n-list-item v-for="(item, index) in settingStore.localFilesPath" :key="index">
|
||||
<template #prefix>
|
||||
@@ -176,7 +183,10 @@ const listData = computed<SongType[]>(() => {
|
||||
// 获取音乐文件夹
|
||||
const getMusicFolder = async (): Promise<string[]> => {
|
||||
defaultMusicPath.value = await window.electron.ipcRenderer.invoke("get-default-dir", "music");
|
||||
return [defaultMusicPath.value, ...settingStore.localFilesPath];
|
||||
return [
|
||||
settingStore.showDefaultLocalPath ? defaultMusicPath.value : "",
|
||||
...settingStore.localFilesPath,
|
||||
];
|
||||
};
|
||||
|
||||
// 全部音乐大小
|
||||
@@ -262,7 +272,7 @@ localEventBus.on(() => getAllLocalMusic());
|
||||
|
||||
// 本地目录变化
|
||||
watch(
|
||||
() => settingStore.localFilesPath,
|
||||
() => [settingStore.localFilesPath, settingStore.showDefaultLocalPath],
|
||||
async () => await getAllLocalMusic(),
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"electron/main/utils.ts",
|
||||
"electron/main/index.d.ts",
|
||||
"electron/preload/index.d.ts",
|
||||
"electron/preload/index.ts"
|
||||
"electron/preload/index.ts",
|
||||
"dist/lastfm.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
"src/**/*",
|
||||
"src/**/*.vue",
|
||||
"electron/main/index.d.ts",
|
||||
"electron/preload/index.d.ts"
|
||||
"electron/preload/index.d.ts",
|
||||
"dist/lastfm.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"maxNodeModuleJsDepth": 2,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
|
||||
Reference in New Issue
Block a user