diff --git a/dist-electron/main.js b/dist-electron/main.js index 6a78272..0c12c18 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -1,7 +1,13 @@ -import { ipcMain, BrowserWindow, app, Menu } from "electron"; +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { app, ipcMain, BrowserWindow, Menu } from "electron"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import path from "node:path"; +import os from "node:os"; +import net from "node:net"; +import { spawn } from "node:child_process"; createRequire(import.meta.url); const __dirname$1 = path.dirname(fileURLToPath(import.meta.url)); process.env.APP_ROOT = path.join(__dirname$1, ".."); @@ -10,24 +16,145 @@ const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron"); const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist"); process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, "public") : RENDERER_DIST; let win; +class MpvController { + // 用于处理 JSON 数据粘包 + constructor() { + __publicField(this, "process", null); + __publicField(this, "socket", null); + __publicField(this, "socketPath"); + __publicField(this, "buffer", ""); + const socketName = `mpv-socket-${Date.now()}`; + this.socketPath = process.platform === "win32" ? `\\\\.\\pipe\\${socketName}` : path.join(os.tmpdir(), socketName); + } + start(binaryPath) { + console.log(`[MPV] Starting from: ${binaryPath}`); + console.log(`[MPV] IPC Socket: ${this.socketPath}`); + this.process = spawn(binaryPath, [ + "--idle", + // 空闲时不退出 + "--no-video", + // 纯音频模式 + "--keep-open=yes", + // 播放结束不退出 + `--input-ipc-server=${this.socketPath}` + // 指定 IPC 监听地址 + ]); + this.process.on("error", (err) => { + console.error("[MPV] Process Error:", err); + }); + this.process.on("exit", (code) => { + var _a; + console.log(`[MPV] Process exited with code ${code}`); + (_a = this.socket) == null ? void 0 : _a.destroy(); + }); + this.connectSocket(); + } + connectSocket(retries = 10) { + setTimeout(() => { + const socket = net.createConnection(this.socketPath); + socket.on("connect", () => { + console.log("[MPV] IPC Connected!"); + this.socket = socket; + this.setupObservers(); + }); + socket.on("data", (data) => this.handleData(data)); + socket.on("error", (err) => { + if (retries > 0) { + this.connectSocket(retries - 1); + } else { + console.error("[MPV] Failed to connect to IPC socket:", err); + } + }); + }, 500); + } + // 处理接收到的数据 (解决 TCP 数据包粘连或截断问题) + handleData(data) { + this.buffer += data.toString(); + const lines = this.buffer.split("\n"); + this.buffer = lines.pop() || ""; + lines.forEach((line) => { + if (!line.trim()) return; + try { + const message = JSON.parse(line); + console.log(message); + this.handleMessage(message); + } catch (e) { + console.error("[MPV] JSON Parse Error:", e); + } + }); + } + // 处理解析后的 JSON 消息 + handleMessage(msg) { + if (!win) return; + if (msg.event === "property-change") { + switch (msg.name) { + case "time-pos": + win.webContents.send("mpv-time-update", msg.data); + break; + case "duration": + win.webContents.send("mpv-duration", msg.data); + break; + case "pause": + win.webContents.send("mpv-play-state", !msg.data); + break; + } + } + if (msg.event === "end-file") { + win.webContents.send("mpv-ended"); + win.webContents.send("mpv-play-state", false); + } + } + // 初始化监听属性 + setupObservers() { + this.send(["observe_property", 1, "time-pos"]); + this.send(["observe_property", 2, "duration"]); + this.send(["observe_property", 3, "pause"]); + } + // 发送命令给 MPV + send(command) { + if (!this.socket || this.socket.destroyed) return; + const payload = JSON.stringify({ command }); + this.socket.write(payload + "\n"); + } + // === 业务方法 === + load(url, autoPlay) { + if (autoPlay) { + this.send(["set_property", "pause", false]); + this.send(["loadfile", url, "replace"]); + } else { + this.send(["set_property", "pause", true]); + this.send(["loadfile", url, "replace"]); + } + } + play() { + this.send(["set_property", "pause", false]); + } + pause() { + this.send(["set_property", "pause", true]); + } + seek(time) { + this.send(["seek", time, "absolute"]); + } + setVolume(volume) { + this.send(["set_property", "volume", volume]); + } +} +const mpv = new MpvController(); +const mpvExecutablePath = app.isPackaged ? path.join(process.resourcesPath, "core", "mpv.exe") : path.join(process.env.APP_ROOT, "core", "mpv.exe"); function createWindow() { win = new BrowserWindow({ - // 🔧 禁用原生标题栏(无边框) frame: false, - // 设置窗口最小大小 minWidth: 950, minHeight: 700, - // 设置初始窗口大小 width: 1e3, height: 800, icon: path.join(process.env.VITE_PUBLIC, "electron-vite.svg"), webPreferences: { - preload: path.join(__dirname$1, "preload.mjs") + preload: path.join(__dirname$1, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true } }); - if (process.env.NODE_ENV === "development") { - win.webContents.openDevTools({ mode: "right" }); - } win.webContents.on("did-finish-load", () => { win == null ? void 0 : win.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); }); @@ -38,22 +165,17 @@ function createWindow() { } } ipcMain.on("window-minimize", (event) => { - const win2 = BrowserWindow.fromWebContents(event.sender); - win2 == null ? void 0 : win2.minimize(); -}); -ipcMain.on("window-maximize", () => { - if (win == null ? void 0 : win.isMaximized()) { - win.unmaximize(); - } else { - win == null ? void 0 : win.maximize(); - } -}); -ipcMain.on("window-close", () => { - win == null ? void 0 : win.close(); -}); -ipcMain.handle("window-is-maximized", () => { - return (win == null ? void 0 : win.isMaximized()) || false; + var _a; + return (_a = BrowserWindow.fromWebContents(event.sender)) == null ? void 0 : _a.minimize(); }); +ipcMain.on("window-maximize", () => (win == null ? void 0 : win.isMaximized()) ? win.unmaximize() : win == null ? void 0 : win.maximize()); +ipcMain.on("window-close", () => win == null ? void 0 : win.close()); +ipcMain.handle("window-is-maximized", () => (win == null ? void 0 : win.isMaximized()) || false); +ipcMain.on("mpv-load", (_, url, autoPlay = true) => mpv.load(url, autoPlay)); +ipcMain.on("mpv-play", () => mpv.play()); +ipcMain.on("mpv-pause", () => mpv.pause()); +ipcMain.on("mpv-seek", (_, time) => mpv.seek(time)); +ipcMain.on("mpv-volume", (_, volume) => mpv.setVolume(volume)); app.on("window-all-closed", () => { if (process.platform !== "darwin") { app.quit(); @@ -67,6 +189,7 @@ app.on("activate", () => { }); app.whenReady().then(() => { Menu.setApplicationMenu(null); + mpv.start(mpvExecutablePath); createWindow(); }); export { diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs index 4c44b9e..4454d43 100644 --- a/dist-electron/preload.mjs +++ b/dist-electron/preload.mjs @@ -1,8 +1,20 @@ "use strict"; const electron = require("electron"); electron.contextBridge.exposeInMainWorld("electronAPI", { + // 窗口控制 minimizeWindow: () => electron.ipcRenderer.send("window-minimize"), maximizeWindow: () => electron.ipcRenderer.send("window-maximize"), closeWindow: () => electron.ipcRenderer.send("window-close"), - isMaximized: () => electron.ipcRenderer.invoke("window-is-maximized") + isMaximized: () => electron.ipcRenderer.invoke("window-is-maximized"), + // MPV 控制 (Renderer -> Main) + mpvLoad: (url, autoPlay = true) => electron.ipcRenderer.send("mpv-load", url, autoPlay), + mpvPlay: () => electron.ipcRenderer.send("mpv-play"), + mpvPause: () => electron.ipcRenderer.send("mpv-pause"), + mpvSeek: (time) => electron.ipcRenderer.send("mpv-seek", time), + mpvSetVolume: (volume) => electron.ipcRenderer.send("mpv-volume", volume), + // MPV 事件 (Main -> Renderer) + onMpvTimeUpdate: (callback) => electron.ipcRenderer.on("mpv-time-update", (_, time) => callback(time)), + onMpvDuration: (callback) => electron.ipcRenderer.on("mpv-duration", (_, duration) => callback(duration)), + onMpvPlayState: (callback) => electron.ipcRenderer.on("mpv-play-state", (_, isPlaying) => callback(isPlaying)), + onMpvEnded: (callback) => electron.ipcRenderer.on("mpv-ended", () => callback()) }); diff --git a/electron/main.ts b/electron/main.ts index fb6572e..a798760 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -2,22 +2,15 @@ import { app, BrowserWindow, Menu, ipcMain } from 'electron' import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' import path from 'node:path' +import os from 'node:os' +import net from 'node:net' +import { spawn, type ChildProcess } from 'node:child_process' const require = createRequire(import.meta.url) const __dirname = path.dirname(fileURLToPath(import.meta.url)) -// The built directory structure -// -// ├─┬─┬ dist -// │ │ └── index.html -// │ │ -// │ ├─┬ dist-electron -// │ │ ├── main.js -// │ │ └── preload.mjs -// │ process.env.APP_ROOT = path.join(__dirname, '..') -// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'] export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron') export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') @@ -26,76 +19,219 @@ process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, let win: BrowserWindow | null -function createWindow() { - win = new BrowserWindow({ - // 🔧 禁用原生标题栏(无边框) - frame: false, - // 设置窗口最小大小 - minWidth: 950, - minHeight: 700, - // 设置初始窗口大小 - width: 1000, - height: 800, - icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'), - webPreferences: { - preload: path.join(__dirname, 'preload.mjs'), - }, - - }) - if (process.env.NODE_ENV === 'development') { - win.webContents.openDevTools({ mode: 'right' }); // 可选:'undocked', 'bottom', 'right' - } - // Test active push message to Renderer-process. - win.webContents.on('did-finish-load', () => { - win?.webContents.send('main-process-message', new Date().toLocaleString()) - }) +class MpvController { + private process: ChildProcess | null = null; + private socket: net.Socket | null = null; + private socketPath: string; + private buffer: string = ''; - if (VITE_DEV_SERVER_URL) { - win.loadURL(VITE_DEV_SERVER_URL) - } else { - win.loadFile(path.join(RENDERER_DIST, 'index.html')) - } + constructor() { + const socketName = `mpv-socket-${Date.now()}`; + this.socketPath = process.platform === 'win32' + ? `\\\\.\\pipe\\${socketName}` + : path.join(os.tmpdir(), socketName); + } + + start(binaryPath: string) { + console.log(`[MPV] Starting from: ${binaryPath}`); + console.log(`[MPV] IPC Socket: ${this.socketPath}`); + + // 启动 MPV 进程 + this.process = spawn(binaryPath, [ + '--idle', // 空闲时不退出 + '--no-video', // 纯音频模式 + '--keep-open=yes', // 播放结束不退出 + `--input-ipc-server=${this.socketPath}` + ]); + + this.process.on('error', (err) => { + console.error('[MPV] Process Error:', err); + }); + + this.process.on('exit', (code) => { + console.log(`[MPV] Process exited with code ${code}`); + this.socket?.destroy(); + }); + + this.connectSocket(); + } + + private connectSocket(retries = 10) { + setTimeout(() => { + const socket = net.createConnection(this.socketPath); + + socket.on('connect', () => { + console.log('[MPV] IPC Connected!'); + this.socket = socket; + this.setupObservers(); + }); + + socket.on('data', (data) => this.handleData(data)); + + socket.on('error', (err) => { + if (retries > 0) { + // MPV 还没准备好,继续重试 + this.connectSocket(retries - 1); + } else { + console.error('[MPV] Failed to connect to IPC socket:', err); + } + }); + }, 500); // 500ms 重试间隔 + } + + // 处理接收到的数据 + private handleData(data: Buffer) { + this.buffer += data.toString(); + + // MPV 的消息以 \n 分隔 + const lines = this.buffer.split('\n'); + // 最后一个元素可能是不完整的行,留到下一次处理 + this.buffer = lines.pop() || ''; + + lines.forEach(line => { + if (!line.trim()) return; + try { + const message = JSON.parse(line); + console.log(message); + this.handleMessage(message); + } catch (e) { + console.error('[MPV] JSON Parse Error:', e); + } + }); + } + + // 处理解析后的 JSON 消息 + private handleMessage(msg: any) { + if (!win) return; + + // 处理属性变更事件 + if (msg.event === 'property-change') { + switch (msg.name) { + case 'time-pos': + win.webContents.send('mpv-time-update', msg.data); + break; + case 'duration': + win.webContents.send('mpv-duration', msg.data); + break; + case 'pause': + win.webContents.send('mpv-play-state', !msg.data); // data=true 意味着暂停 + break; + } + } + + // 处理播放结束事件 + if (msg.event === 'end-file') { + win.webContents.send('mpv-ended'); + win.webContents.send('mpv-play-state', false); + } + } + + // 初始化监听属性 + private setupObservers() { + this.send(['observe_property', 1, 'time-pos']); + this.send(['observe_property', 2, 'duration']); + this.send(['observe_property', 3, 'pause']); + } + + // 发送命令给 MPV + send(command: any[]) { + if (!this.socket || this.socket.destroyed) return; + + const payload = JSON.stringify({ command }); + this.socket.write(payload + '\n'); + } + + // === 业务方法 === + + load(url: string, autoPlay: boolean) { + if (autoPlay) { + // 确保取消暂停 (防止之前是暂停状态) + this.send(['set_property', 'pause', false]); + // 加载文件 + this.send(['loadfile', url, 'replace']); + } else { + // 先设置为暂停 (这样后续加载的文件会继承这个暂停状态) + this.send(['set_property', 'pause', true]); + // 加载文件 + this.send(['loadfile', url, 'replace']); + } + } + + play() { this.send(['set_property', 'pause', false]); } + + pause() { this.send(['set_property', 'pause', true]); } + + seek(time: number) { this.send(['seek', time, 'absolute']); } + + setVolume(volume: number) { this.send(['set_property', 'volume', volume]); } } -ipcMain.on('window-minimize', (event) => { - const win = BrowserWindow.fromWebContents(event.sender) - win?.minimize() -}) +const mpv = new MpvController(); -ipcMain.on('window-maximize', () => { - if (win?.isMaximized()) { - win.unmaximize() +// === Electron 窗口逻辑 === + +const mpvExecutablePath = app.isPackaged + ? path.join(process.resourcesPath, 'core', 'mpv.exe') + : path.join(process.env.APP_ROOT, 'core', 'mpv.exe'); + +function createWindow() { + win = new BrowserWindow({ + frame: false, + minWidth: 950, + minHeight: 700, + width: 1000, + height: 800, + icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'), + webPreferences: { + preload: path.join(__dirname, 'preload.mjs'), + nodeIntegration: false, + contextIsolation: true, + }, + }) + + win.webContents.on('did-finish-load', () => { + win?.webContents.send('main-process-message', new Date().toLocaleString()) + }) + + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL) } else { - win?.maximize() + win.loadFile(path.join(RENDERER_DIST, 'index.html')) + } +} + +// === IPC 监听 === + +ipcMain.on('window-minimize', (event) => BrowserWindow.fromWebContents(event.sender)?.minimize()) +ipcMain.on('window-maximize', () => win?.isMaximized() ? win.unmaximize() : win?.maximize()) +ipcMain.on('window-close', () => win?.close()) +ipcMain.handle('window-is-maximized', () => win?.isMaximized() || false) + +// MPV 控制指令 +ipcMain.on('mpv-load', (_, url, autoPlay = true) => mpv.load(url, autoPlay)) +ipcMain.on('mpv-play', () => mpv.play()) +ipcMain.on('mpv-pause', () => mpv.pause()) +ipcMain.on('mpv-seek', (_, time) => mpv.seek(time)) +ipcMain.on('mpv-volume', (_, volume) => mpv.setVolume(volume)) + + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + win = null } }) -ipcMain.on('window-close', () => { - win?.close() -}) - -ipcMain.handle('window-is-maximized', () => { - return win?.isMaximized() || false -}) - -// Quit when all windows are closed, except on macOS. -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit() - win = null - } -}) - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow() - } + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } }) - app.whenReady().then(() => { - Menu.setApplicationMenu(null) - - createWindow() + Menu.setApplicationMenu(null) + // 启动 MPV + mpv.start(mpvExecutablePath); + createWindow() }) \ No newline at end of file diff --git a/electron/preload.ts b/electron/preload.ts index 3b1bb41..a339652 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,8 +1,29 @@ import { contextBridge, ipcRenderer } from 'electron' contextBridge.exposeInMainWorld('electronAPI', { + // 窗口控制 minimizeWindow: () => ipcRenderer.send('window-minimize'), maximizeWindow: () => ipcRenderer.send('window-maximize'), closeWindow: () => ipcRenderer.send('window-close'), - isMaximized: () => ipcRenderer.invoke('window-is-maximized') + isMaximized: () => ipcRenderer.invoke('window-is-maximized'), + + // MPV 控制 (Renderer -> Main) + mpvLoad: (url: string,autoPlay: boolean = true) => ipcRenderer.send('mpv-load', url, autoPlay), + mpvPlay: () => ipcRenderer.send('mpv-play'), + mpvPause: () => ipcRenderer.send('mpv-pause'), + mpvSeek: (time: number) => ipcRenderer.send('mpv-seek', time), + mpvSetVolume: (volume: number) => ipcRenderer.send('mpv-volume', volume), + + // MPV 事件 (Main -> Renderer) + onMpvTimeUpdate: (callback: (time: number) => void) => + ipcRenderer.on('mpv-time-update', (_, time) => callback(time)), + + onMpvDuration: (callback: (duration: number) => void) => + ipcRenderer.on('mpv-duration', (_, duration) => callback(duration)), + + onMpvPlayState: (callback: (isPlaying: boolean) => void) => + ipcRenderer.on('mpv-play-state', (_, isPlaying) => callback(isPlaying)), + + onMpvEnded: (callback: () => void) => + ipcRenderer.on('mpv-ended', () => callback()), }) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0c19ab1..58bf341 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "element-plus": "^2.13.0", "jss": "^10.10.0", "jss-preset-default": "^10.10.0", + "node-mpv": "^1.5.0", "pinia": "^3.0.4", "tdesign-vue-next": "^1.17.7", "vue": "^3.4.21", @@ -2669,6 +2670,12 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz", @@ -2837,6 +2844,12 @@ "node": ">=8" } }, + "node_modules/browser-fingerprint": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/browser-fingerprint/-/browser-fingerprint-0.0.1.tgz", + "integrity": "sha512-b8SXP7yOlzLUJXF8WUvIjmbJzkJC0X6OHe7J9a/SHqEBC7a9Eglag6AANSTJz82h5U582kuxm/5TPudnD68EPA==", + "license": "MIT" + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", @@ -3304,6 +3317,13 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", @@ -3382,6 +3402,18 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/cuid": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/cuid/-/cuid-1.3.8.tgz", + "integrity": "sha512-MoL67ZZuBetDMxzrZtO+Iq1ATajFACQCP52QRinBgd3yTjYdv54mJO8ibUrh06fojKCoX5P2i7KkEatm4VTIOQ==", + "deprecated": "Cuid and other k-sortable and non-cryptographic ids (Ulid, ObjectId, KSUID, all UUIDs) are all insecure. Use @paralleldrive/cuid2 instead.", + "license": "MIT", + "dependencies": { + "browser-fingerprint": "0.0.1", + "core-js": "^1.1.1", + "node-fingerprint": "0.0.2" + } + }, "node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", @@ -5471,6 +5503,22 @@ "license": "MIT", "optional": true }, + "node_modules/node-fingerprint": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/node-fingerprint/-/node-fingerprint-0.0.2.tgz", + "integrity": "sha512-vPFfTD5EBJieQ4SI3v61fWxlV1kav3m9Dbejd6CjWhOJn8s+XMxpOOosCNAyIrUQ/jJOlPndfrZ0lSw4+RgwcA==", + "license": "MIT" + }, + "node_modules/node-mpv": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/node-mpv/-/node-mpv-1.5.0.tgz", + "integrity": "sha512-kvLo+PcWHZ/Sg7t9XeFDi5KJrNOL9XJOEljCEh5wBNOHiE6Wa/txwIsYWKmNaIFuncbEhgjnoOavE4T5YBNV8Q==", + "dependencies": { + "cuid": "^1.3.8", + "lodash": ">= 4.0.0", + "promise": "^7.1.1" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5717,6 +5765,15 @@ "node": ">=0.4.0" } }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmmirror.com/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/promise-retry/-/promise-retry-2.0.1.tgz", diff --git a/package.json b/package.json index 52a44a4..0c35513 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "element-plus": "^2.13.0", "jss": "^10.10.0", "jss-preset-default": "^10.10.0", + "node-mpv": "^1.5.0", "pinia": "^3.0.4", "tdesign-vue-next": "^1.17.7", "vue": "^3.4.21", diff --git a/src/App.vue b/src/App.vue index ffda61d..1da6d0b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -56,6 +56,9 @@ const store = usePlayerStore(); const router = useRouter(); const route = useRoute(); +// Register IPC +store.init() + // 跟踪路由历史,判断是否可以返回和前进 const canGoBack = ref(true); // 返回按钮始终可用,让浏览器处理 const canGoForward = ref(false); diff --git a/src/components/layout/PlayerBar.vue b/src/components/layout/PlayerBar.vue index 65bc192..9e0df9d 100644 --- a/src/components/layout/PlayerBar.vue +++ b/src/components/layout/PlayerBar.vue @@ -1,21 +1,49 @@ + +/* --- Vue 动画 --- */ +.fade-slide-enter-active, +.fade-slide-leave-active { + transition: all 0.3s ease; +} +.fade-slide-enter-from { + opacity: 0; + transform: translateY(5px); +} +.fade-slide-leave-to { + opacity: 0; + transform: translateY(-5px); +} + \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 9151ccb..d1ac18e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,7 +16,6 @@ const router = createRouter({ routes: [ { path: '/', component: HomeView }, { path: '/playlist/:id', component: PlaylistDetailView }, - // 可扩展其他路由 ] }) diff --git a/src/stores/playerStore.ts b/src/stores/playerStore.ts index 72beb71..8e7b6bd 100644 --- a/src/stores/playerStore.ts +++ b/src/stores/playerStore.ts @@ -6,8 +6,8 @@ export const usePlayerStore = defineStore('player', () => { id: 1, title: "Example Track", artist: "AI Artist", - cover: "http://p2.music.126.net/W3VMsSEjTdvhz7h3a0oxTg==/17782401556325576.jpg?param=130y130", - url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", + cover: "http://p2.music.126.net/h2vun-h_uGBYzGvQoLKiBw==/109951165966921437.jpg?param=130y130", + url: "https://m704.music.126.net/20260115210245/494fafc7ecc89da85365b1e6533cdb30/jdyyaac/obj/w5rDlsOJwrLDjj7CmsOj/32280537391/40ea/84dd/db94/451cfc92afa4a12926f40b1183eca3cd.m4a?vuutv=yqFO4JPFSDRDeqVLCjH3fPvuLTHnPPNLPMIBbHWfYTZOmqP5/RFh7UnxA2sG1X9+MLdjYsrkG5vUIUjV6t+y1pniceMN5lePyr33C0D1Aho=&authSecret=0000019bc1a9710615420a3283920006&cdntag=bWFyaz1vc193ZWIscXVhbGl0eV9leGhpZ2g", duration: 0 }); @@ -18,91 +18,148 @@ export const usePlayerStore = defineStore('player', () => { const showSettings = ref(false); const darkMode = ref(false); const themeColors = ref({ primary: '#6366f1', secondary: '#a855f7' }); - - // 新增:用于顶部进度条的最深色 const progressColor = ref('#ffffff'); - const audio = new Audio(currentSong.value.url); - audio.volume = volume.value / 100; + // 标记是否已初始化监听器,防止重复绑定 + let isInitialized = false; - // 颜色提取函数(轻量版,不依赖外部库) + // --- 初始化函数:在组件挂载时调用一次 --- + function init() { + if (isInitialized) return; + + // 监听时间更新 + window.electronAPI.onMpvTimeUpdate((time) => { + currentTime.value = time; + }); + + // 监听时长更新 (MPV 加载完元数据后会发送) + window.electronAPI.onMpvDuration((duration) => { + currentSong.value.duration = duration; + }); + + // 监听播放状态 (用于同步 MPV 内部状态和 UI) + window.electronAPI.onMpvPlayState((playing) => { + isPlaying.value = playing; + }); + + // 监听结束 + window.electronAPI.onMpvEnded(() => { + isPlaying.value = false; + currentTime.value = 0; + // 这里可以添加自动播放下一首的逻辑 + }); + + // 初始化音量 + window.electronAPI.mpvSetVolume(volume.value); + + // 加载初始歌曲 + loadCurrentSong(false); + + isInitialized = true; + } + + + function loadCurrentSong(autoPlay:boolean=true) { + if(currentSong.value.url) { + window.electronAPI.mpvLoad(currentSong.value.url,autoPlay); + } + } + + function togglePlay() { + if (isPlaying.value) { + window.electronAPI.mpvPause(); + } else { + window.electronAPI.mpvPlay(); + } + isPlaying.value = !isPlaying.value; + } + + function seek(time: number) { + window.electronAPI.mpvSeek(time); + currentTime.value = time; // 立即更新 UI 防止跳变 + } + + // 监听音量变化 + watch(volume, (newVol) => { + window.electronAPI.mpvSetVolume(newVol); + }); + + // 监听歌曲 URL 变化 (切歌) + watch(() => currentSong.value.url, () => { + loadCurrentSong(true); + }); + + // 监听封面变化提取颜色 (保持原有逻辑不变) + watch(() => currentSong.value.cover, () => { + extractColors(); + }, { immediate: true }); + + // --- 颜色提取逻辑 (保持不变) --- function extractColors() { const img = new Image(); img.crossOrigin = 'Anonymous'; - img.src = currentSong.value.cover + '?t=' + Date.now(); // 避免缓存 + img.src = currentSong.value.cover + '?t=' + Date.now(); img.onload = () => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return; - // 缩小图片以提高性能 const scale = 0.1; canvas.width = Math.floor(img.width * scale); canvas.height = Math.floor(img.height * scale); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; - // 收集所有像素颜色 - const colors: { r: number; g: number; b: number; brightness: number }[] = []; - + const colors: { r: number; g: number; b: number; brightness: number }[] = []; + for (let i = 0; i < imageData.length; i += 4) { const r = imageData[i]; const g = imageData[i + 1]; const b = imageData[i + 2]; const brightness = (r + g + b) / 3; - colors.push({ r, g, b, brightness }); } - - // 计算颜色频率(简单实现) + const colorFrequency: Record = {}; colors.forEach(color => { - // 将颜色量化为 16 位色,减少颜色数量 const key = `${Math.floor(color.r / 16)}${Math.floor(color.g / 16)}${Math.floor(color.b / 16)}`; colorFrequency[key] = (colorFrequency[key] || 0) + 1; }); - - // 转换回 RGB 并按频率排序 + const sortedColors = Object.entries(colorFrequency) - .map(([key, count]) => { - const r = parseInt(key[0], 16) * 16; - const g = parseInt(key[1], 16) * 16; - const b = parseInt(key[2], 16) * 16; - return { r, g, b, count }; - }) - .sort((a, b) => b.count - a.count); - - // 提取主色调和辅助色调 + .map(([key, count]) => { + const r = parseInt(key[0], 16) * 16; + const g = parseInt(key[1], 16) * 16; + const b = parseInt(key[2], 16) * 16; + return { r, g, b, count }; + }) + .sort((a, b) => b.count - a.count); + if (sortedColors.length > 0) { - // 主色调:频率最高的颜色 const primary = sortedColors[0]; const primaryColor = `rgb(${primary.r}, ${primary.g}, ${primary.b})`; - - // 辅助色调:选择与主色调亮度差异较大的颜色 + let secondary = sortedColors[1] || sortedColors[0]; let maxBrightnessDiff = Math.abs(primary.brightness - secondary.brightness); - - // 在频率较高的颜色中寻找最合适的辅助色 + for (let i = 1; i < Math.min(10, sortedColors.length); i++) { const currentBrightness = (sortedColors[i].r + sortedColors[i].g + sortedColors[i].b) / 3; const brightnessDiff = Math.abs(primary.brightness - currentBrightness); - + if (brightnessDiff > maxBrightnessDiff) { maxBrightnessDiff = brightnessDiff; secondary = sortedColors[i]; } } - + const secondaryColor = `rgb(${secondary.r}, ${secondary.g}, ${secondary.b})`; - - // 更新主题颜色 + themeColors.value = { primary: primaryColor, secondary: secondaryColor }; - - // 提取进度条颜色(使用主色调的深色版本) + const darkPrimary = { r: Math.floor(primary.r * 0.7), g: Math.floor(primary.g * 0.7), @@ -110,59 +167,22 @@ export const usePlayerStore = defineStore('player', () => { }; progressColor.value = `rgb(${darkPrimary.r}, ${darkPrimary.g}, ${darkPrimary.b})`; } else { - // 默认颜色 themeColors.value = { primary: '#6366f1', secondary: '#a855f7' }; progressColor.value = '#ffffff'; } }; } - // 监听歌曲切换时重新提取颜色 - watch(() => currentSong.value.cover, () => { - extractColors(); - console.log("!!") - }, { immediate: true }); - - audio.addEventListener('loadedmetadata', () => { - currentSong.value.duration = audio.duration; - }); - - audio.addEventListener('timeupdate', () => { - currentTime.value = audio.currentTime; - }); - - audio.addEventListener('ended', () => { - isPlaying.value = false; - currentTime.value = 0; - }); - - function togglePlay() { - if (isPlaying.value) { - audio.pause(); - } else { - audio.play().catch(e => console.warn("播放失败,需用户交互:", e)); - } - isPlaying.value = !isPlaying.value; - } - - function seek(time: number) { - audio.currentTime = time; - } - function toggleSettings() { console.log("!!");showSettings.value = !showSettings.value; } - - watch(volume, (newVol) => { - audio.volume = newVol / 100; - }); - const progressPercentage = computed(() => - currentSong.value.duration ? (currentTime.value / currentSong.value.duration) * 100 : 0 + currentSong.value.duration ? (currentTime.value / currentSong.value.duration) * 100 : 0 ); return { currentSong, isPlaying, currentTime, volume, showPlaylist, themeColors, progressPercentage, progressColor, togglePlay, seek, extractColors, - showSettings, toggleSettings, - darkMode + showSettings, + darkMode, + init }; -}); +}); \ No newline at end of file diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts new file mode 100644 index 0000000..767825c --- /dev/null +++ b/src/types/electron.d.ts @@ -0,0 +1,24 @@ +export interface IElectronAPI { + minimizeWindow: () => void; + maximizeWindow: () => void; + closeWindow: () => void; + isMaximized: () => Promise; + + mpvLoad: (url: string,autoPlay?: boolean) => void; + mpvPlay: () => void; + mpvPause: () => void; + mpvResume: () => void; + mpvSeek: (time: number) => void; + mpvSetVolume: (volume: number) => void; + + onMpvTimeUpdate: (callback: (time: number) => void) => void; + onMpvDuration: (callback: (duration: number) => void) => void; + onMpvPlayState: (callback: (isPlaying: boolean) => void) => void; + onMpvEnded: (callback: () => void) => void; +} + +declare global { + interface Window { + electronAPI: IElectronAPI + } +} \ No newline at end of file