diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 595024e..2a12efb 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -28,7 +28,6 @@ export default defineConfig(({ command, mode }) => { input: { index: resolve(__dirname, "electron/main/index.ts"), lyric: resolve(__dirname, "web/lyric.html"), - loading: resolve(__dirname, "web/loading.html"), }, }, }, @@ -101,6 +100,7 @@ export default defineConfig(({ command, mode }) => { rollupOptions: { input: { index: resolve(__dirname, "index.html"), + loading: resolve(__dirname, "web/loading/index.html"), }, output: { manualChunks: { diff --git a/electron/main/index.ts b/electron/main/index.ts index 3b8a3f1..206940f 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1,204 +1,63 @@ -import { app, shell, BrowserWindow, BrowserWindowConstructorOptions } from "electron"; +import { app, BrowserWindow } from "electron"; import { electronApp } from "@electron-toolkit/utils"; -import { join } from "path"; import { release, type } from "os"; -import { isDev, isMac, appName, isLinux } from "./utils"; +import { isMac } from "./utils/config"; import { unregisterShortcuts } from "./shortcut"; import { initTray, MainTray } from "./tray"; -import { initThumbar, Thumbar } from "./thumbar"; -import { type StoreType, initStore } from "./store"; -import Store from "electron-store"; +import { processLog } from "./logger"; import initAppServer from "../server"; -import initIpcMain from "./ipcMain"; -import log from "./logger"; -// icon -import icon from "../../public/icons/favicon.png?asset"; +import { initSingleLock } from "./utils/single-lock"; +import loadWindow from "./windows/load-window"; +import mainWindow from "./windows/main-window"; +import lyricWindow from "./windows/lyric-window"; +import initIpc from "./ipc"; // 屏蔽报错 process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true"; -// 模拟打包 -Object.defineProperty(app, "isPackaged", { - get() { - return true; - }, -}); - // 主进程 class MainProcess { // 窗口 mainWindow: BrowserWindow | null = null; lyricWindow: BrowserWindow | null = null; - loadingWindow: BrowserWindow | null = null; - // store - store: Store | null = null; + loadWindow: BrowserWindow | null = null; // 托盘 mainTray: MainTray | null = null; - // 工具栏 - thumbar: Thumbar | null = null; // 是否退出 isQuit: boolean = false; constructor() { - log.info("🚀 Main process startup"); + processLog.info("🚀 Main process startup"); + // 程序单例锁 + initSingleLock(); // 禁用 Windows 7 的 GPU 加速功能 if (release().startsWith("6.1") && type() == "Windows_NT") app.disableHardwareAcceleration(); - // 单例锁 - if (!app.requestSingleInstanceLock()) { - log.error("❌ There is already a program running and this process is terminated"); - app.quit(); - process.exit(0); - } else this.showWindow(); - // 准备就绪 - app.on("ready", async () => { - log.info("🚀 Application Process Startup"); + // 监听应用事件 + this.handleAppEvents(); + // Electron 初始化完成后 + // 某些API只有在此事件发生后才能使用 + app.whenReady().then(async () => { + processLog.info("🚀 Application Process Startup"); // 设置应用程序名称 electronApp.setAppUserModelId("com.imsyy.splayer"); - // 初始化 store - this.store = initStore(); // 启动主服务进程 await initAppServer(); - // 启动进程 - this.createLoadingWindow(); - this.createMainWindow(); - this.createLyricsWindow(); - this.handleAppEvents(); - this.handleWindowEvents(); + // 启动窗口 + this.loadWindow = loadWindow.create(); + this.mainWindow = mainWindow.create(); + this.lyricWindow = lyricWindow.create(); // 注册其他服务 this.mainTray = initTray(this.mainWindow!, this.lyricWindow!); - this.thumbar = initThumbar(this.mainWindow!); - // 注册主进程事件 - initIpcMain( - this.mainWindow, - this.lyricWindow, - this.loadingWindow, - this.mainTray, - this.thumbar, - this.store, - ); + // 注册 IPC 通信 + initIpc(); }); } - // 创建窗口 - createWindow(options: BrowserWindowConstructorOptions = {}): BrowserWindow { - const defaultOptions: BrowserWindowConstructorOptions = { - title: appName, - width: 1280, - height: 720, - frame: false, - center: true, - // 图标 - icon, - webPreferences: { - preload: join(__dirname, "../preload/index.mjs"), - // 禁用渲染器沙盒 - sandbox: false, - // 禁用同源策略 - webSecurity: false, - // 允许 HTTP - allowRunningInsecureContent: true, - // 禁用拼写检查 - spellcheck: false, - // 启用 Node.js - nodeIntegration: true, - nodeIntegrationInWorker: true, - // 启用上下文隔离 - contextIsolation: false, - }, - }; - // 合并参数 - options = Object.assign(defaultOptions, options); - // 创建窗口 - const win = new BrowserWindow(options); - return win; - } - // 创建主窗口 - createMainWindow() { - // 窗口配置项 - const options: BrowserWindowConstructorOptions = { - width: this.store?.get("window").width, - height: this.store?.get("window").height, - minHeight: 600, - minWidth: 800, - // 菜单栏 - titleBarStyle: "customButtonsOnHover", - // 立即显示窗口 - show: false, - }; - // 初始化窗口 - this.mainWindow = this.createWindow(options); - - // 渲染路径 - if (isDev && process.env["ELECTRON_RENDERER_URL"]) { - this.mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]); - } else { - const port = Number(import.meta.env["VITE_SERVER_PORT"] || 25884); - this.mainWindow.loadURL(`http://127.0.0.1:${port}`); - } - - // 配置网络代理 - if (this.store?.get("proxy")) { - this.mainWindow.webContents.session.setProxy({ proxyRules: this.store?.get("proxy") }); - } - - // 窗口打开处理程序 - this.mainWindow.webContents.setWindowOpenHandler((details) => { - const { url } = details; - if (url.startsWith("https://") || url.startsWith("http://")) { - shell.openExternal(url); - } - return { action: "deny" }; - }); - } - // 创建加载窗口 - createLoadingWindow() { - // 初始化窗口 - this.loadingWindow = this.createWindow({ - width: 800, - height: 560, - maxWidth: 800, - maxHeight: 560, - resizable: false, - }); - // 渲染路径 - this.loadingWindow.loadFile(join(__dirname, "../main/web/loading.html")); - } - // 创建桌面歌词窗口 - createLyricsWindow() { - // 初始化窗口 - this.lyricWindow = this.createWindow({ - width: this.store?.get("lyric").width || 800, - height: this.store?.get("lyric").height || 180, - minWidth: 440, - minHeight: 120, - maxWidth: 1600, - maxHeight: 300, - // 窗口位置 - x: this.store?.get("lyric").x, - y: this.store?.get("lyric").y, - transparent: true, - backgroundColor: "rgba(0, 0, 0, 0)", - alwaysOnTop: true, - resizable: true, - movable: true, - // 不在任务栏显示 - skipTaskbar: true, - // 窗口不能最小化 - minimizable: false, - // 窗口不能最大化 - maximizable: false, - // 窗口不能进入全屏状态 - fullscreenable: false, - show: false, - }); - // 渲染路径 - this.lyricWindow.loadFile(join(__dirname, "../main/web/lyric.html")); - } // 应用程序事件 handleAppEvents() { // 窗口被关闭时 app.on("window-all-closed", () => { if (!isMac) app.quit(); this.mainWindow = null; - this.loadingWindow = null; + this.loadWindow = null; }); // 应用被激活 @@ -206,19 +65,12 @@ class MainProcess { const allWindows = BrowserWindow.getAllWindows(); if (allWindows.length) { allWindows[0].focus(); - } else { - this.createMainWindow(); } }); - // 新增 session - app.on("second-instance", () => { - this.showWindow(); - }); - // 自定义协议 app.on("open-url", (_, url) => { - console.log("Received custom protocol URL:", url); + processLog.log("Received custom protocol URL:", url); }); // 将要退出 @@ -232,82 +84,6 @@ class MainProcess { this.isQuit = true; }); } - // 窗口事件 - handleWindowEvents() { - this.mainWindow?.on("ready-to-show", () => { - if (!this.mainWindow) return; - this.thumbar = initThumbar(this.mainWindow); - }); - this.mainWindow?.on("show", () => { - // this.mainWindow?.webContents.send("lyricsScroll"); - }); - this.mainWindow?.on("focus", () => { - this.saveBounds(); - }); - // 移动、缩放、最大化、取消最大化 - this.mainWindow?.on("resized", () => { - // 若处于全屏则不保存 - if (this.mainWindow?.isFullScreen()) return; - this.saveBounds(); - }); - this.mainWindow?.on("moved", () => { - this.saveBounds(); - }); - this.mainWindow?.on("maximize", () => { - this.saveBounds(); - }); - this.mainWindow?.on("unmaximize", () => { - this.saveBounds(); - }) - - // Linux 无法使用 resized 和 moved - if (isLinux) { - this.mainWindow?.on("resize", () => { - // 若处于全屏则不保存 - if (this.mainWindow?.isFullScreen()) return; - this.saveBounds(); - }) - this.mainWindow?.on("move", () => { - this.saveBounds(); - }); - } - - // 歌词窗口缩放 - this.lyricWindow?.on("resized", () => { - const bounds = this.lyricWindow?.getBounds(); - if (bounds) { - const { width, height } = bounds; - this.store?.set("lyric", { ...this.store?.get("lyric"), width, height }); - } - }); - - // 窗口关闭 - this.mainWindow?.on("close", (event) => { - event.preventDefault(); - if (this.isQuit) { - app.exit(); - } else { - this.mainWindow?.hide(); - } - }); - } - // 更新窗口大小 - saveBounds() { - if (this.mainWindow?.isFullScreen()) return; - const bounds: any = this.mainWindow?.getBounds(); - if (bounds) { - bounds.maximized = this.mainWindow?.isMaximized(); - this.store?.set("window", bounds); - } - } - // 显示窗口 - showWindow() { - if (this.mainWindow) { - this.mainWindow.show(); - if (this.mainWindow.isMinimized()) this.mainWindow.restore(); - this.mainWindow.focus(); - } - } } export default new MainProcess(); diff --git a/electron/main/ipc/index.ts b/electron/main/ipc/index.ts new file mode 100644 index 0000000..60ad24b --- /dev/null +++ b/electron/main/ipc/index.ts @@ -0,0 +1,27 @@ +import initFileIpc from "./ipc-file"; +import initLyricIpc from "./ipc-lyric"; +import initShortcutIpc from "./ipc-shortcut"; +import initStoreIpc from "./ipc-store"; +import initSystemIpc from "./ipc-system"; +import initThumbarIpc from "./ipc-thumbar"; +import initTrayIpc from "./ipc-tray"; +import initUpdateIpc from "./ipc-update"; +import initWindowsIpc from "./ipc-window"; + +/** + * 初始化全部 IPC 通信 + * @returns void + */ +const initIpc = (): void => { + initSystemIpc(); + initWindowsIpc(); + initUpdateIpc(); + initFileIpc(); + initTrayIpc(); + initLyricIpc(); + initStoreIpc(); + initThumbarIpc(); + initShortcutIpc(); +}; + +export default initIpc; diff --git a/electron/main/ipc/ipc-file.ts b/electron/main/ipc/ipc-file.ts new file mode 100644 index 0000000..14bc4a9 --- /dev/null +++ b/electron/main/ipc/ipc-file.ts @@ -0,0 +1,376 @@ +import { app, BrowserWindow, dialog, ipcMain, shell } from "electron"; +import { basename, join, resolve } from "path"; +import { access, readFile, stat, unlink, writeFile } from "fs/promises"; +import { parseFile } from "music-metadata"; +import { getFileID, getFileMD5, metaDataLyricsArrayToLrc } from "../utils/helper"; +import { File, Picture, Id3v2Settings } from "node-taglib-sharp"; +import { ipcLog } from "../logger"; +import FastGlob from "fast-glob"; +import { download } from "electron-dl"; + +/** + * 文件相关 IPC + */ +const initFileIpc = (): void => { + // 默认文件夹 + ipcMain.handle( + "get-default-dir", + (_event, type: "documents" | "downloads" | "pictures" | "music" | "videos"): string => { + return app.getPath(type); + }, + ); + + // 遍历音乐文件 + 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 FastGlob("**/*.{mp3,wav,flac}", { cwd: filePath }); + // 解析元信息 + const metadataPromises = musicFiles.map(async (file) => { + const filePath = join(dirPath, file); + // 处理元信息 + const { common, format } = await parseFile(filePath); + // 获取文件大小 + const { size } = await stat(filePath); + // 判断音质等级 + let quality: string; + if ((format.sampleRate || 0) >= 96000 || (format.bitsPerSample || 0) > 16) { + quality = "Hi-Res"; + } else if ((format.sampleRate || 0) >= 44100) { + quality = "HQ"; + } else { + quality = "SQ"; + } + return { + id: getFileID(filePath), + name: common.title || basename(filePath), + artists: common.artists?.[0] || common.artist, + album: common.album || "", + alia: common.comment?.[0]?.text || "", + duration: (format?.duration ?? 0) * 1000, + size: (size / (1024 * 1024)).toFixed(2), + path: filePath, + quality, + }; + }); + const metadataArray = await Promise.all(metadataPromises); + return metadataArray; + } catch (error) { + ipcLog.error("❌ Error fetching music metadata:", error); + throw error; + } + }); + + // 获取音乐元信息 + ipcMain.handle("get-music-metadata", async (_, path: string) => { + try { + const filePath = resolve(path).replace(/\\/g, "/"); + const { common, format } = await parseFile(filePath); + return { + // 文件名称 + fileName: basename(filePath), + // 文件大小 + fileSize: (await stat(filePath)).size / (1024 * 1024), + // 元信息 + common, + // 歌词 + lyric: + metaDataLyricsArrayToLrc(common?.lyrics?.[0]?.syncText || []) || + common?.lyrics?.[0]?.text || + "", + // 音质信息 + format, + // md5 + md5: await getFileMD5(filePath), + }; + } catch (error) { + ipcLog.error("❌ Error fetching music metadata:", error); + throw error; + } + }); + + // 修改音乐元信息 + ipcMain.handle("set-music-metadata", async (_, path: string, metadata: any) => { + try { + const { name, artist, album, alia, lyric, cover } = metadata; + // 规范化路径 + const songPath = resolve(path); + const coverPath = cover ? resolve(cover) : null; + // 读取歌曲文件 + const songFile = File.createFromPath(songPath); + // 读取封面文件 + const songCover = coverPath ? Picture.fromPath(coverPath) : null; + // 保存元数据 + Id3v2Settings.forceDefaultVersion = true; + Id3v2Settings.defaultVersion = 3; + songFile.tag.title = name || "未知曲目"; + songFile.tag.performers = [artist || "未知艺术家"]; + songFile.tag.album = album || "未知专辑"; + songFile.tag.albumArtists = [artist || "未知艺术家"]; + songFile.tag.lyrics = lyric || ""; + songFile.tag.description = alia || ""; + songFile.tag.comment = alia || ""; + if (songCover) songFile.tag.pictures = [songCover]; + // 保存元信息 + songFile.save(); + songFile.dispose(); + return true; + } catch (error) { + ipcLog.error("❌ Error setting music metadata:", error); + throw error; + } + }); + + // 获取音乐歌词 + ipcMain.handle( + "get-music-lyric", + async ( + _, + path: string, + ): Promise<{ + lyric: string; + format: "lrc" | "ttml"; + }> => { + try { + const filePath = resolve(path).replace(/\\/g, "/"); + const { common } = await parseFile(filePath); + + // 尝试获取同名的歌词文件 + const filePathWithoutExt = filePath.replace(/\.[^.]+$/, ""); + for (const ext of ["ttml", "lrc"] as const) { + const lyricPath = `${filePathWithoutExt}.${ext}`; + ipcLog.info("lyricPath", lyricPath); + try { + await access(lyricPath); + const lyric = await readFile(lyricPath, "utf-8"); + if (lyric && lyric != "") return { lyric, format: ext }; + } catch { + /* empty */ + } + } + + // 尝试获取元数据 + const lyric = common?.lyrics?.[0]?.syncText; + if (lyric && lyric.length > 0) { + return { lyric: metaDataLyricsArrayToLrc(lyric), format: "lrc" }; + } else if (common?.lyrics?.[0]?.text) { + return { lyric: common?.lyrics?.[0]?.text, format: "lrc" }; + } + + // 没有歌词 + return { lyric: "", format: "lrc" }; + } catch (error) { + ipcLog.error("❌ Error fetching music lyric:", error); + throw error; + } + }, + ); + + // 获取音乐封面 + ipcMain.handle( + "get-music-cover", + async (_, path: string): Promise<{ data: Buffer; format: string } | null> => { + try { + const { common } = await parseFile(path); + // 获取封面数据 + const picture = common.picture?.[0]; + if (picture) { + return { data: Buffer.from(picture.data), format: picture.format }; + } else { + const coverFilePath = path.replace(/\.[^.]+$/, ".jpg"); + try { + await access(coverFilePath); + const coverData = await readFile(coverFilePath); + return { data: coverData, format: "image/jpeg" }; + } catch { + return null; + } + } + } catch (error) { + console.error("❌ Error fetching music cover:", error); + throw error; + } + }, + ); + + // 读取本地歌词 + ipcMain.handle( + "read-local-lyric", + async (_, lyricDir: string, id: number, ext: string): Promise => { + const lyricPath = join(lyricDir, `${id}.${ext}`); + try { + await access(lyricPath); + const lyric = await readFile(lyricPath, "utf-8"); + if (lyric) return lyric; + } catch { + /* empty */ + } + return ""; + }, + ); + + // 删除文件 + ipcMain.handle("delete-file", async (_, path: string) => { + try { + // 规范化路径 + const resolvedPath = resolve(path); + // 检查文件是否存在 + try { + await access(resolvedPath); + } catch { + throw new Error("❌ File not found"); + } + // 删除文件 + await unlink(resolvedPath); + return true; + } catch (error) { + ipcLog.error("❌ File delete error", error); + return false; + } + }); + + // 打开文件夹 + ipcMain.on("open-folder", async (_, path: string) => { + try { + // 规范化路径 + const resolvedPath = resolve(path); + // 检查文件夹是否存在 + try { + await access(resolvedPath); + } catch { + throw new Error("❌ Folder not found"); + } + // 打开文件夹 + shell.showItemInFolder(resolvedPath); + } catch (error) { + ipcLog.error("❌ Folder open error", error); + throw error; + } + }); + + // 图片选择窗口 + ipcMain.handle("choose-image", async () => { + try { + const { filePaths } = await dialog.showOpenDialog({ + properties: ["openFile"], + filters: [{ name: "Images", extensions: ["jpg", "jpeg", "png"] }], + }); + if (!filePaths || filePaths.length === 0) return null; + return filePaths[0]; + } catch (error) { + ipcLog.error("❌ Image choose error", error); + return null; + } + }); + + // 路径选择窗口 + ipcMain.handle("choose-path", async () => { + try { + const { filePaths } = await dialog.showOpenDialog({ + title: "选择文件夹", + defaultPath: app.getPath("downloads"), + properties: ["openDirectory", "createDirectory"], + buttonLabel: "选择文件夹", + }); + if (!filePaths || filePaths.length === 0) return null; + return filePaths[0]; + } catch (error) { + ipcLog.error("❌ Path choose error", error); + return null; + } + }); + + // 下载文件 + ipcMain.handle( + "download-file", + async ( + event, + url: string, + options: { + fileName: string; + fileType: string; + path: string; + downloadMeta?: boolean; + downloadCover?: boolean; + downloadLyric?: boolean; + saveMetaFile?: boolean; + lyric?: string; + songData?: any; + } = { + fileName: "未知文件名", + fileType: "mp3", + path: app.getPath("downloads"), + }, + ): Promise => { + try { + // 获取窗口 + const win = BrowserWindow.fromWebContents(event.sender); + if (!win) return false; + // 获取配置 + const { + fileName, + fileType, + path, + lyric, + downloadMeta, + downloadCover, + downloadLyric, + saveMetaFile, + songData, + } = options; + // 规范化路径 + const downloadPath = resolve(path); + // 检查文件夹是否存在 + try { + await access(downloadPath); + } catch { + throw new Error("❌ Folder not found"); + } + // 下载文件 + const songDownload = await download(win, url, { + directory: downloadPath, + filename: `${fileName}.${fileType}`, + }); + if (!downloadMeta || !songData?.cover) return true; + // 下载封面 + const coverUrl = songData?.coverSize?.l || songData.cover; + const coverDownload = await download(win, coverUrl, { + directory: downloadPath, + filename: `${fileName}.jpg`, + }); + // 读取歌曲文件 + const songFile = File.createFromPath(songDownload.getSavePath()); + // 生成图片信息 + const songCover = Picture.fromPath(coverDownload.getSavePath()); + // 保存修改后的元数据 + Id3v2Settings.forceDefaultVersion = true; + Id3v2Settings.defaultVersion = 3; + songFile.tag.title = songData?.name || "未知曲目"; + songFile.tag.album = songData?.album?.name || "未知专辑"; + songFile.tag.performers = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"]; + songFile.tag.albumArtists = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"]; + if (lyric && downloadLyric) songFile.tag.lyrics = lyric; + if (songCover && downloadCover) songFile.tag.pictures = [songCover]; + // 保存元信息 + songFile.save(); + songFile.dispose(); + // 创建同名歌词文件 + if (lyric && saveMetaFile && downloadLyric) { + const lrcPath = join(downloadPath, `${fileName}.lrc`); + await writeFile(lrcPath, lyric, "utf-8"); + } + // 是否删除封面 + if (!saveMetaFile || !downloadCover) await unlink(coverDownload.getSavePath()); + return true; + } catch (error) { + ipcLog.error("❌ Error downloading file:", error); + return false; + } + }, + ); +}; + +export default initFileIpc; diff --git a/electron/main/ipc/ipc-lyric.ts b/electron/main/ipc/ipc-lyric.ts new file mode 100644 index 0000000..e264465 --- /dev/null +++ b/electron/main/ipc/ipc-lyric.ts @@ -0,0 +1,111 @@ +import { ipcMain, screen } from "electron"; +import lyricWindow from "../windows/lyric-window"; +import { useStore } from "../store"; +import mainWindow from "../windows/main-window"; +import { isAbsolute, relative, resolve } from "path"; + +/** + * 歌词相关 IPC + */ +const initLyricIpc = (): void => { + const store = useStore(); + const mainWin = mainWindow.getWin(); + const lyricWin = lyricWindow.getWin(); + + // 切换桌面歌词 + ipcMain.on("change-desktop-lyric", (_event, val: boolean) => { + if (val) { + lyricWin?.show(); + lyricWin?.setAlwaysOnTop(true, "screen-saver"); + } else lyricWin?.hide(); + }); + + // 音乐名称更改 + ipcMain.on("play-song-change", (_, title) => { + if (!title) return; + lyricWin?.webContents.send("play-song-change", title); + }); + + // 音乐歌词更改 + ipcMain.on("play-lyric-change", (_, lyricData) => { + if (!lyricData) return; + lyricWin?.webContents.send("play-lyric-change", lyricData); + }); + + // 获取窗口位置 + ipcMain.handle("get-window-bounds", () => { + return lyricWin?.getBounds(); + }); + + // 获取屏幕尺寸 + ipcMain.handle("get-screen-size", () => { + const { width, height } = screen.getPrimaryDisplay().workAreaSize; + return { width, height }; + }); + + // 移动窗口 + ipcMain.on("move-window", (_, x, y, width, height) => { + lyricWin?.setBounds({ x, y, width, height }); + // 保存配置 + store.set("lyric", { ...store.get("lyric"), x, y, width, height }); + // 保持置顶 + lyricWin?.setAlwaysOnTop(true, "screen-saver"); + }); + + // 更新高度 + ipcMain.on("update-window-height", (_, height) => { + if (!lyricWin) return; + const { width } = lyricWin.getBounds(); + // 更新窗口高度 + lyricWin.setBounds({ width, height }); + }); + + // 获取配置 + ipcMain.handle("get-desktop-lyric-option", () => { + return store.get("lyric"); + }); + + // 保存配置 + ipcMain.on("set-desktop-lyric-option", (_, option, callback: boolean = false) => { + store.set("lyric", option); + // 触发窗口更新 + if (callback && lyricWin) { + lyricWin.webContents.send("desktop-lyric-option-change", option); + } + mainWin?.webContents.send("desktop-lyric-option-change", option); + }); + + // 发送主程序事件 + ipcMain.on("send-main-event", (_, name, val) => { + mainWin?.webContents.send(name, val); + }); + + // 关闭桌面歌词 + ipcMain.on("closeDesktopLyric", () => { + lyricWin?.hide(); + mainWin?.webContents.send("closeDesktopLyric"); + }); + + // 锁定/解锁桌面歌词 + ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => { + if (!lyricWin) return; + // 是否穿透 + if (isLock) { + lyricWin.setIgnoreMouseEvents(true, { forward: true }); + } else { + 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); + }); + }); +}; + +export default initLyricIpc; diff --git a/electron/main/ipc/ipc-shortcut.ts b/electron/main/ipc/ipc-shortcut.ts new file mode 100644 index 0000000..439dcfd --- /dev/null +++ b/electron/main/ipc/ipc-shortcut.ts @@ -0,0 +1,37 @@ +import { ipcMain } from "electron"; +import { isShortcutRegistered, registerShortcut, unregisterShortcuts } from "../shortcut"; +import mainWindow from "../windows/main-window"; + +/** + * 初始化快捷键 IPC 主进程 + * @returns void + */ +const initShortcutIpc = (): void => { + const mainWin = mainWindow.getWin(); + + // 快捷键是否被注册 + ipcMain.handle("is-shortcut-registered", (_, shortcut: string) => isShortcutRegistered(shortcut)); + + // 注册快捷键 + ipcMain.handle("register-all-shortcut", (_, allShortcuts: any): string[] | false => { + if (!mainWin || !allShortcuts) return false; + // 卸载所有快捷键 + unregisterShortcuts(); + // 注册快捷键 + const failedShortcuts: string[] = []; + for (const key in allShortcuts) { + const shortcut = allShortcuts[key].globalShortcut; + if (!shortcut) continue; + // 快捷键回调 + const callback = () => mainWin.webContents.send(key); + const isSuccess = registerShortcut(shortcut, callback); + if (!isSuccess) failedShortcuts.push(shortcut); + } + return failedShortcuts; + }); + + // 卸载所有快捷键 + ipcMain.on("unregister-all-shortcut", () => unregisterShortcuts()); +}; + +export default initShortcutIpc; diff --git a/electron/main/ipc/ipc-store.ts b/electron/main/ipc/ipc-store.ts new file mode 100644 index 0000000..fa7fdbf --- /dev/null +++ b/electron/main/ipc/ipc-store.ts @@ -0,0 +1,11 @@ +import { useStore } from "../store"; + +/** + * 初始化 store IPC 主进程 + */ +const initStoreIpc = (): void => { + const store = useStore(); + if (!store) return; +}; + +export default initStoreIpc; diff --git a/electron/main/ipc/ipc-system.ts b/electron/main/ipc/ipc-system.ts new file mode 100644 index 0000000..571dea5 --- /dev/null +++ b/electron/main/ipc/ipc-system.ts @@ -0,0 +1,95 @@ +import { ipcMain, net, powerSaveBlocker, session } from "electron"; +import { ipcLog } from "../logger"; +import { getFonts } from "font-list"; +import { useStore } from "../store"; +import mainWindow from "../windows/main-window"; + +/** + * 初始化系统 IPC 通信 + * @returns void + */ +const initSystemIpc = (): void => { + const store = useStore(); + const mainWin = mainWindow.getWin(); + + /** 阻止系统息屏 ID */ + let preventId: number | null = null; + + // 是否阻止系统息屏 + ipcMain.on("prevent-sleep", (_event, val: boolean) => { + if (val) { + preventId = powerSaveBlocker.start("prevent-display-sleep"); + ipcLog.info("⏾ System sleep prevention started"); + } else { + if (preventId !== null) { + powerSaveBlocker.stop(preventId); + ipcLog.info("✅ System sleep prevention stopped"); + } + } + }); + + // 获取系统全部字体 + ipcMain.handle("get-all-fonts", async () => { + try { + const fonts = await getFonts(); + return fonts; + } catch (error) { + ipcLog.error(`❌ Failed to get all system fonts: ${error}`); + return []; + } + }); + + // 取消代理 + ipcMain.on("remove-proxy", () => { + store.set("proxy", ""); + mainWin?.webContents.session.setProxy({ proxyRules: "" }); + ipcLog.info("✅ Remove proxy successfully"); + }); + + // 配置网络代理 + ipcMain.on("set-proxy", (_, config) => { + const proxyRules = `${config.protocol}://${config.server}:${config.port}`; + store.set("proxy", proxyRules); + mainWin?.webContents.session.setProxy({ proxyRules }); + ipcLog.info("✅ Set proxy successfully:", proxyRules); + }); + + // 代理测试 + ipcMain.handle("test-proxy", async (_, config) => { + const proxyRules = `${config.protocol}://${config.server}:${config.port}`; + try { + // 设置代理 + const ses = session.defaultSession; + await ses.setProxy({ proxyRules }); + // 测试请求 + const request = net.request({ url: "https://www.baidu.com" }); + return new Promise((resolve) => { + request.on("response", (response) => { + if (response.statusCode === 200) { + ipcLog.info("✅ Proxy test successful"); + resolve(true); + } else { + ipcLog.error(`❌ Proxy test failed with status code: ${response.statusCode}`); + resolve(false); + } + }); + request.on("error", (error) => { + ipcLog.error("❌ Error testing proxy:", error); + resolve(false); + }); + request.end(); + }); + } catch (error) { + ipcLog.error("❌ Error testing proxy:", error); + return false; + } + }); + + // 重置全部设置 + ipcMain.on("reset-setting", () => { + store.reset(); + ipcLog.info("✅ Reset setting successfully"); + }); +}; + +export default initSystemIpc; diff --git a/electron/main/ipc/ipc-thumbar.ts b/electron/main/ipc/ipc-thumbar.ts new file mode 100644 index 0000000..4a01d52 --- /dev/null +++ b/electron/main/ipc/ipc-thumbar.ts @@ -0,0 +1,15 @@ +import { ipcMain } from "electron"; +import { getThumbar } from "../thumbar"; + +const initThumbarIpc = (): void => { + // 更新工具栏 + ipcMain.on("play-status-change", (_, playStatus: boolean) => { + const thumbar = getThumbar(); + if (!thumbar) { + return; + } + thumbar.updateThumbar(playStatus); + }); +}; + +export default initThumbarIpc; diff --git a/electron/main/ipc/ipc-tray.ts b/electron/main/ipc/ipc-tray.ts new file mode 100644 index 0000000..74702f4 --- /dev/null +++ b/electron/main/ipc/ipc-tray.ts @@ -0,0 +1,47 @@ +import { ipcMain } from "electron"; +import { getMainTray } from "../tray"; +import lyricWindow from "../windows/lyric-window"; + +/** + * 托盘 IPC + */ +const initTrayIpc = (): void => { + const tray = getMainTray(); + const lyricWin = lyricWindow.getWin(); + + // 音乐播放状态更改 + ipcMain.on("play-status-change", (_, playStatus: boolean) => { + tray?.setPlayState(playStatus ? "play" : "pause"); + lyricWin?.webContents.send("play-status-change", playStatus); + }); + + // 音乐名称更改 + ipcMain.on("play-song-change", (_, title) => { + if (!title) return; + // 更改标题 + tray?.setTitle(title); + tray?.setPlayName(title); + }); + + // 播放模式切换 + ipcMain.on("play-mode-change", (_, mode) => { + tray?.setPlayMode(mode); + }); + + // 喜欢状态切换 + ipcMain.on("like-status-change", (_, likeStatus: boolean) => { + tray?.setLikeState(likeStatus); + }); + + // 桌面歌词开关 + ipcMain.on("change-desktop-lyric", (_, val: boolean) => { + tray?.setDesktopLyricShow(val); + }); + + // 锁定/解锁桌面歌词 + ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => { + tray?.setDesktopLyricLock(isLock); + }); +}; + +export default initTrayIpc; diff --git a/electron/main/ipc/ipc-update.ts b/electron/main/ipc/ipc-update.ts new file mode 100644 index 0000000..cd7b1bd --- /dev/null +++ b/electron/main/ipc/ipc-update.ts @@ -0,0 +1,15 @@ +import { ipcMain } from "electron"; +import { checkUpdate, startDownloadUpdate } from "../update"; +import mainWindow from "../windows/main-window"; + +const initUpdateIpc = () => { + const mainWin = mainWindow.getWin(); + + // 检查更新 + ipcMain.on("check-update", (_event, showTip) => checkUpdate(mainWin!, showTip)); + + // 开始下载更新 + ipcMain.on("start-download-update", () => startDownloadUpdate()); +}; + +export default initUpdateIpc; diff --git a/electron/main/ipc/ipc-window.ts b/electron/main/ipc/ipc-window.ts new file mode 100644 index 0000000..cdd63b6 --- /dev/null +++ b/electron/main/ipc/ipc-window.ts @@ -0,0 +1,112 @@ +import { app, ipcMain } from "electron"; +import { useStore } from "../store"; +import { isDev } from "../utils/config"; +import { initThumbar } from "../thumbar"; +import mainWindow from "../windows/main-window"; +import loadWindow from "../windows/load-window"; +import loginWindow from "../windows/login-window"; + +/** + * 窗口 IPC 通信 + * @returns void + */ +const initWindowsIpc = (): void => { + // 相关窗口 + const mainWin = mainWindow.getWin(); + const loadWin = loadWindow.getWin(); + // store + const store = useStore(); + + // 当前窗口状态 + ipcMain.on("win-state", (event) => { + event.returnValue = mainWin?.isMaximized(); + }); + + // 加载完成 + ipcMain.on("win-loaded", () => { + if (loadWin && !loadWin.isDestroyed()) loadWin.destroy(); + const isMaximized = store.get("window")?.maximized; + if (isMaximized) mainWin?.maximize(); + mainWin?.show(); + mainWin?.focus(); + // 初始化缩略图工具栏 + if (mainWin) initThumbar(mainWin); + }); + + // 最小化 + ipcMain.on("win-min", (event) => { + event.preventDefault(); + mainWin?.minimize(); + }); + + // 最大化 + ipcMain.on("win-max", () => { + mainWin?.maximize(); + }); + + // 还原 + ipcMain.on("win-restore", () => { + mainWin?.restore(); + }); + + // 关闭 + ipcMain.on("win-close", (event) => { + event.preventDefault(); + mainWin?.close(); + app.quit(); + }); + + // 隐藏 + ipcMain.on("win-hide", () => { + mainWin?.hide(); + }); + + // 显示 + ipcMain.on("win-show", () => { + mainWin?.show(); + }); + + // 重启 + ipcMain.on("win-reload", () => { + app.quit(); + app.relaunch(); + }); + + // 显示进度 + ipcMain.on("set-bar", (_event, val: number | "none" | "indeterminate" | "error" | "paused") => { + switch (val) { + case "none": + mainWin?.setProgressBar(-1); + break; + case "indeterminate": + mainWin?.setProgressBar(2, { mode: "indeterminate" }); + break; + case "error": + mainWin?.setProgressBar(1, { mode: "error" }); + break; + case "paused": + mainWin?.setProgressBar(1, { mode: "paused" }); + break; + default: + if (typeof val === "number") { + mainWin?.setProgressBar(val / 100); + } else { + mainWin?.setProgressBar(-1); + } + break; + } + }); + + // 开启控制台 + ipcMain.on("open-dev-tools", () => { + mainWin?.webContents.openDevTools({ + title: "SPlayer DevTools", + mode: isDev ? "right" : "detach", + }); + }); + + // 开启登录窗口 + ipcMain.on("open-login-web", () => loginWindow.create(mainWin!)); +}; + +export default initWindowsIpc; diff --git a/electron/main/ipcMain.ts b/electron/main/ipcMain.ts deleted file mode 100644 index ca84a82..0000000 --- a/electron/main/ipcMain.ts +++ /dev/null @@ -1,765 +0,0 @@ -import { - app, - ipcMain, - BrowserWindow, - powerSaveBlocker, - screen, - shell, - dialog, - net, - session, -} from "electron"; -import { File, Picture, Id3v2Settings } from "node-taglib-sharp"; -import { parseFile } from "music-metadata"; -import { getFonts } from "font-list"; -import { MainTray } from "./tray"; -import { Thumbar } from "./thumbar"; -import { StoreType } from "./store"; -import { isDev, getFileID, getFileMD5, metaDataLyricsArrayToLrc } from "./utils"; -import { isShortcutRegistered, registerShortcut, unregisterShortcuts } from "./shortcut"; -import { join, basename, resolve, relative, isAbsolute } from "path"; -import { download } from "electron-dl"; -import { checkUpdate, startDownloadUpdate } from "./update"; -import fs from "fs/promises"; -import log from "../main/logger"; -import Store from "electron-store"; -import fg from "fast-glob"; -import openLoginWin from "./loginWin"; -import path from "node:path"; - -// 注册 ipcMain -const initIpcMain = ( - win: BrowserWindow | null, - lyricWin: BrowserWindow | null, - loadingWin: BrowserWindow | null, - tray: MainTray | null, - thumbar: Thumbar | null, - store: Store, -) => { - initWinIpcMain(win, loadingWin, lyricWin, store); - initLyricIpcMain(lyricWin, win, store); - initTrayIpcMain(tray, win, lyricWin); - initThumbarIpcMain(thumbar); - initStoreIpcMain(store); - initOtherIpcMain(win); -}; - -// win -const initWinIpcMain = ( - win: BrowserWindow | null, - loadingWin: BrowserWindow | null, - lyricWin: BrowserWindow | null, - store: Store, -) => { - let preventId: number | null = null; - - // 当前窗口状态 - ipcMain.on("win-state", (ev) => { - ev.returnValue = win?.isMaximized(); - }); - - // 加载完成 - ipcMain.on("win-loaded", () => { - if (loadingWin && !loadingWin.isDestroyed()) loadingWin.close(); - win?.show(); - win?.focus(); - const isMaximized = store?.get("window")?.maximized; - if (isMaximized) win?.maximize(); - }); - - // 最小化 - ipcMain.on("win-min", (ev) => { - ev.preventDefault(); - win?.minimize(); - }); - // 最大化 - ipcMain.on("win-max", () => { - win?.maximize(); - }); - // 还原 - ipcMain.on("win-restore", () => { - win?.restore(); - }); - // 关闭 - ipcMain.on("win-close", (ev) => { - ev.preventDefault(); - win?.close(); - app.quit(); - }); - // 隐藏 - ipcMain.on("win-hide", () => { - win?.hide(); - }); - // 显示 - ipcMain.on("win-show", () => { - win?.show(); - }); - // 重启 - ipcMain.on("win-reload", () => { - app.quit(); - app.relaunch(); - }); - - // 显示进度 - ipcMain.on("set-bar", (_, val: number | "none" | "indeterminate" | "error" | "paused") => { - switch (val) { - case "none": - win?.setProgressBar(-1); - break; - case "indeterminate": - win?.setProgressBar(2, { mode: "indeterminate" }); - break; - case "error": - win?.setProgressBar(1, { mode: "error" }); - break; - case "paused": - win?.setProgressBar(1, { mode: "paused" }); - break; - default: - if (typeof val === "number") { - win?.setProgressBar(val / 100); - } else { - win?.setProgressBar(-1); - } - break; - } - }); - - // 开启控制台 - ipcMain.on("open-dev-tools", () => { - win?.webContents.openDevTools({ - title: "SPlayer DevTools", - mode: isDev ? "right" : "detach", - }); - }); - - // 获取系统全部字体 - ipcMain.handle("get-all-fonts", async () => { - try { - const fonts = await getFonts(); - return fonts; - } catch (error) { - log.error(`❌ Failed to get all system fonts: ${error}`); - return []; - } - }); - - // 切换桌面歌词 - ipcMain.on("change-desktop-lyric", (_, val: boolean) => { - if (val) { - lyricWin?.show(); - lyricWin?.setAlwaysOnTop(true, "screen-saver"); - } else lyricWin?.hide(); - }); - - // 是否阻止系统息屏 - ipcMain.on("prevent-sleep", (_, val: boolean) => { - if (val) { - preventId = powerSaveBlocker.start("prevent-display-sleep"); - log.info("⏾ System sleep prevention started"); - } else { - if (preventId !== null) { - powerSaveBlocker.stop(preventId); - log.info("✅ System sleep prevention stopped"); - } - } - }); - - // 默认文件夹 - ipcMain.handle( - "get-default-dir", - (_, type: "documents" | "downloads" | "pictures" | "music" | "videos"): string => { - return app.getPath(type); - }, - ); - - // 遍历音乐文件 - 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: filePath }); - // 解析元信息 - const metadataPromises = musicFiles.map(async (file) => { - const filePath = join(dirPath, file); - // 处理元信息 - const { common, format } = await parseFile(filePath); - // 获取文件大小 - const { size } = await fs.stat(filePath); - // 判断音质等级 - let quality: string; - if ((format.sampleRate || 0) >= 96000 || (format.bitsPerSample || 0) > 16) { - quality = "Hi-Res"; - } else if ((format.sampleRate || 0) >= 44100) { - quality = "HQ"; - } else { - quality = "SQ"; - } - return { - id: getFileID(filePath), - name: common.title || basename(filePath), - artists: common.artists?.[0] || common.artist, - album: common.album || "", - alia: common.comment?.[0]?.text || "", - duration: (format?.duration ?? 0) * 1000, - size: (size / (1024 * 1024)).toFixed(2), - path: filePath, - quality, - }; - }); - const metadataArray = await Promise.all(metadataPromises); - return metadataArray; - } catch (error) { - log.error("❌ Error fetching music metadata:", error); - throw error; - } - }); - - // 获取音乐元信息 - ipcMain.handle("get-music-metadata", async (_, path: string) => { - try { - const filePath = resolve(path).replace(/\\/g, "/"); - const { common, format } = await parseFile(filePath); - return { - // 文件名称 - fileName: basename(filePath), - // 文件大小 - fileSize: (await fs.stat(filePath)).size / (1024 * 1024), - // 元信息 - common, - // 歌词 - lyric: - metaDataLyricsArrayToLrc(common?.lyrics?.[0]?.syncText || []) || - common?.lyrics?.[0]?.text || - "", - // 音质信息 - format, - // md5 - md5: await getFileMD5(filePath), - }; - } catch (error) { - log.error("❌ Error fetching music metadata:", error); - throw error; - } - }); - - // 获取音乐歌词 - ipcMain.handle( - "get-music-lyric", - async ( - _, - path: string, - ): Promise<{ - lyric: string; - format: "lrc" | "ttml"; - }> => { - try { - const filePath = resolve(path).replace(/\\/g, "/"); - const { common } = await parseFile(filePath); - - // 尝试获取同名的歌词文件 - const filePathWithoutExt = filePath.replace(/\.[^.]+$/, ""); - for (const ext of ["ttml", "lrc"] as const) { - const lyricPath = `${filePathWithoutExt}.${ext}`; - console.log("lyricPath", lyricPath); - try { - await fs.access(lyricPath); - const lyric = await fs.readFile(lyricPath, "utf-8"); - if (lyric && lyric != "") return { lyric, format: ext }; - } catch { - /* empty */ - } - } - - // 尝试获取元数据 - const lyric = common?.lyrics?.[0]?.syncText; - if (lyric && lyric.length > 0) { - return { lyric: metaDataLyricsArrayToLrc(lyric), format: "lrc" }; - } else if (common?.lyrics?.[0]?.text) { - return { lyric: common?.lyrics?.[0]?.text, format: "lrc" }; - } - - // 没有歌词 - return { lyric: "", format: "lrc" }; - } catch (error) { - log.error("❌ Error fetching music lyric:", error); - throw error; - } - }, - ); - - // 获取音乐封面 - ipcMain.handle( - "get-music-cover", - async (_, path: string): Promise<{ data: Buffer; format: string } | null> => { - try { - const { common } = await parseFile(path); - // 获取封面数据 - const picture = common.picture?.[0]; - if (picture) { - return { data: Buffer.from(picture.data), format: picture.format }; - } else { - const coverFilePath = path.replace(/\.[^.]+$/, ".jpg"); - try { - await fs.access(coverFilePath); - const coverData = await fs.readFile(coverFilePath); - return { data: coverData, format: "image/jpeg" }; - } catch { - return null; - } - } - } catch (error) { - console.error("❌ Error fetching music cover:", error); - throw error; - } - }, - ); - - // 读取本地歌词 - ipcMain.handle( - "read-local-lyric", - async (_, lyricDir: string, id: number, ext: string): Promise => { - const lyricPath = path.join(lyricDir, `${id}.${ext}`); - try { - await fs.access(lyricPath); - const lyric = await fs.readFile(lyricPath, "utf-8"); - if (lyric) return lyric; - } catch { - /* empty */ - } - return ""; - }, - ); - - // 删除文件 - ipcMain.handle("delete-file", async (_, path: string) => { - try { - // 规范化路径 - const resolvedPath = resolve(path); - // 检查文件是否存在 - try { - await fs.access(resolvedPath); - } catch { - throw new Error("❌ File not found"); - } - // 删除文件 - await fs.unlink(resolvedPath); - return true; - } catch (error) { - log.error("❌ File delete error", error); - return false; - } - }); - - // 打开文件夹 - ipcMain.on("open-folder", async (_, path: string) => { - try { - // 规范化路径 - const resolvedPath = resolve(path); - // 检查文件夹是否存在 - try { - await fs.access(resolvedPath); - } catch { - throw new Error("❌ Folder not found"); - } - // 打开文件夹 - shell.showItemInFolder(resolvedPath); - } catch (error) { - log.error("❌ Folder open error", error); - throw error; - } - }); - - // 图片选择窗口 - ipcMain.handle("choose-image", async () => { - try { - const { filePaths } = await dialog.showOpenDialog({ - properties: ["openFile"], - filters: [{ name: "Images", extensions: ["jpg", "jpeg", "png"] }], - }); - if (!filePaths || filePaths.length === 0) return null; - return filePaths[0]; - } catch (error) { - log.error("❌ Image choose error", error); - return null; - } - }); - - // 路径选择窗口 - ipcMain.handle("choose-path", async () => { - try { - const { filePaths } = await dialog.showOpenDialog({ - title: "选择文件夹", - defaultPath: app.getPath("downloads"), - properties: ["openDirectory", "createDirectory"], - buttonLabel: "选择文件夹", - }); - if (!filePaths || filePaths.length === 0) return null; - return filePaths[0]; - } catch (error) { - log.error("❌ Path choose error", error); - return null; - } - }); - - // 修改音乐元信息 - ipcMain.handle("set-music-metadata", async (_, path: string, metadata: any) => { - try { - const { name, artist, album, alia, lyric, cover } = metadata; - // 规范化路径 - const songPath = resolve(path); - const coverPath = cover ? resolve(cover) : null; - // 读取歌曲文件 - const songFile = File.createFromPath(songPath); - // 读取封面文件 - const songCover = coverPath ? Picture.fromPath(coverPath) : null; - // 保存元数据 - Id3v2Settings.forceDefaultVersion = true; - Id3v2Settings.defaultVersion = 3; - songFile.tag.title = name || "未知曲目"; - songFile.tag.performers = [artist || "未知艺术家"]; - songFile.tag.album = album || "未知专辑"; - songFile.tag.albumArtists = [artist || "未知艺术家"]; - songFile.tag.lyrics = lyric || ""; - songFile.tag.description = alia || ""; - songFile.tag.comment = alia || ""; - if (songCover) songFile.tag.pictures = [songCover]; - // 保存元信息 - songFile.save(); - songFile.dispose(); - return true; - } catch (error) { - log.error("❌ Error setting music metadata:", error); - throw error; - } - }); - - // 下载文件 - ipcMain.handle( - "download-file", - async ( - _, - url: string, - options: { - fileName: string; - fileType: string; - path: string; - downloadMeta?: boolean; - downloadCover?: boolean; - downloadLyric?: boolean; - saveMetaFile?: boolean; - lyric?: string; - songData?: any; - } = { - fileName: "未知文件名", - fileType: "mp3", - path: app.getPath("downloads"), - }, - ): Promise => { - try { - if (!win) return false; - // 获取配置 - const { - fileName, - fileType, - path, - lyric, - downloadMeta, - downloadCover, - downloadLyric, - saveMetaFile, - songData, - } = options; - // 规范化路径 - const downloadPath = resolve(path); - // 检查文件夹是否存在 - try { - await fs.access(downloadPath); - } catch { - throw new Error("❌ Folder not found"); - } - // 下载文件 - const songDownload = await download(win, url, { - directory: downloadPath, - filename: `${fileName}.${fileType}`, - }); - if (!downloadMeta || !songData?.cover) return true; - // 下载封面 - const coverUrl = songData?.coverSize?.l || songData.cover; - const coverDownload = await download(win, coverUrl, { - directory: downloadPath, - filename: `${fileName}.jpg`, - }); - // 读取歌曲文件 - const songFile = File.createFromPath(songDownload.getSavePath()); - // 生成图片信息 - const songCover = Picture.fromPath(coverDownload.getSavePath()); - // 保存修改后的元数据 - Id3v2Settings.forceDefaultVersion = true; - Id3v2Settings.defaultVersion = 3; - songFile.tag.title = songData?.name || "未知曲目"; - songFile.tag.album = songData?.album?.name || "未知专辑"; - songFile.tag.performers = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"]; - songFile.tag.albumArtists = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"]; - if (lyric && downloadLyric) songFile.tag.lyrics = lyric; - if (songCover && downloadCover) songFile.tag.pictures = [songCover]; - // 保存元信息 - songFile.save(); - songFile.dispose(); - // 创建同名歌词文件 - if (lyric && saveMetaFile && downloadLyric) { - const lrcPath = join(downloadPath, `${fileName}.lrc`); - await fs.writeFile(lrcPath, lyric, "utf-8"); - } - // 是否删除封面 - if (!saveMetaFile || !downloadCover) await fs.unlink(coverDownload.getSavePath()); - return true; - } catch (error) { - log.error("❌ Error downloading file:", error); - return false; - } - }, - ); - - // 取消代理 - ipcMain.on("remove-proxy", () => { - store.set("proxy", ""); - win?.webContents.session.setProxy({ proxyRules: "" }); - log.info("✅ Remove proxy successfully"); - }); - - // 配置网络代理 - ipcMain.on("set-proxy", (_, config) => { - const proxyRules = `${config.protocol}://${config.server}:${config.port}`; - store.set("proxy", proxyRules); - win?.webContents.session.setProxy({ proxyRules }); - log.info("✅ Set proxy successfully:", proxyRules); - }); - - // 代理测试 - ipcMain.handle("test-proxy", async (_, config) => { - const proxyRules = `${config.protocol}://${config.server}:${config.port}`; - try { - // 设置代理 - const ses = session.defaultSession; - await ses.setProxy({ proxyRules }); - // 测试请求 - const request = net.request({ url: "https://www.baidu.com" }); - return new Promise((resolve) => { - request.on("response", (response) => { - if (response.statusCode === 200) { - log.info("✅ Proxy test successful"); - resolve(true); - } else { - log.error(`❌ Proxy test failed with status code: ${response.statusCode}`); - resolve(false); - } - }); - request.on("error", (error) => { - log.error("❌ Error testing proxy:", error); - resolve(false); - }); - request.end(); - }); - } catch (error) { - log.error("❌ Error testing proxy:", error); - return false; - } - }); - - // 重置全部设置 - ipcMain.on("reset-setting", () => { - store.reset(); - log.info("✅ Reset setting successfully"); - }); - - // 检查更新 - ipcMain.on("check-update", (_, showTip) => checkUpdate(win!, showTip)); - - // 开始下载更新 - ipcMain.on("start-download-update", () => startDownloadUpdate()); - - // 新建窗口 - ipcMain.on("open-login-web", () => openLoginWin(win!)); -}; - -// lyric -const initLyricIpcMain = ( - lyricWin: BrowserWindow | null, - mainWin: BrowserWindow | null, - store: Store, -): void => { - // 音乐名称更改 - ipcMain.on("play-song-change", (_, title) => { - if (!title) return; - lyricWin?.webContents.send("play-song-change", title); - }); - - // 音乐歌词更改 - ipcMain.on("play-lyric-change", (_, lyricData) => { - if (!lyricData) return; - lyricWin?.webContents.send("play-lyric-change", lyricData); - }); - - // 获取窗口位置 - ipcMain.handle("get-window-bounds", () => { - return lyricWin?.getBounds(); - }); - - // 获取屏幕尺寸 - ipcMain.handle("get-screen-size", () => { - const { width, height } = screen.getPrimaryDisplay().workAreaSize; - return { width, height }; - }); - - // 移动窗口 - ipcMain.on("move-window", (_, x, y, width, height) => { - lyricWin?.setBounds({ x, y, width, height }); - // 保存配置 - store.set("lyric", { ...store.get("lyric"), x, y, width, height }); - // 保持置顶 - lyricWin?.setAlwaysOnTop(true, "screen-saver"); - }); - - // 更新高度 - ipcMain.on("update-window-height", (_, height) => { - if (!lyricWin) return; - const { width } = lyricWin.getBounds(); - // 更新窗口高度 - lyricWin.setBounds({ width, height }); - }); - - // 获取配置 - ipcMain.handle("get-desktop-lyric-option", () => { - return store.get("lyric"); - }); - - // 保存配置 - ipcMain.on("set-desktop-lyric-option", (_, option, callback: boolean = false) => { - store.set("lyric", option); - // 触发窗口更新 - if (callback && lyricWin) { - lyricWin.webContents.send("desktop-lyric-option-change", option); - } - mainWin?.webContents.send("desktop-lyric-option-change", option); - }); - - // 发送主程序事件 - ipcMain.on("send-main-event", (_, name, val) => { - mainWin?.webContents.send(name, val); - }); - - // 关闭桌面歌词 - ipcMain.on("closeDesktopLyric", () => { - lyricWin?.hide(); - mainWin?.webContents.send("closeDesktopLyric"); - }); - - // 锁定/解锁桌面歌词 - ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => { - if (!lyricWin) return; - // 是否穿透 - if (isLock) { - lyricWin.setIgnoreMouseEvents(true, { forward: true }); - } else { - 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 -const initTrayIpcMain = ( - tray: MainTray | null, - win: BrowserWindow | null, - lyricWin: BrowserWindow | null, -): void => { - // 音乐播放状态更改 - ipcMain.on("play-status-change", (_, playStatus: boolean) => { - tray?.setPlayState(playStatus ? "play" : "pause"); - lyricWin?.webContents.send("play-status-change", playStatus); - }); - - // 音乐名称更改 - ipcMain.on("play-song-change", (_, title) => { - if (!title) return; - // 更改标题 - win?.setTitle(title); - tray?.setTitle(title); - tray?.setPlayName(title); - }); - - // 播放模式切换 - ipcMain.on("play-mode-change", (_, mode) => { - tray?.setPlayMode(mode); - }); - - // 喜欢状态切换 - ipcMain.on("like-status-change", (_, likeStatus: boolean) => { - tray?.setLikeState(likeStatus); - }); - - // 桌面歌词开关 - ipcMain.on("change-desktop-lyric", (_, val: boolean) => { - tray?.setDesktopLyricShow(val); - }); - - // 锁定/解锁桌面歌词 - ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => { - tray?.setDesktopLyricLock(isLock); - }); -}; - -// thumbar -const initThumbarIpcMain = (thumbar: Thumbar | null): void => { - if (!thumbar) return; - // 更新工具栏 - ipcMain.on("play-status-change", (_, playStatus: boolean) => { - thumbar?.updateThumbar(playStatus); - }); -}; - -// store -const initStoreIpcMain = (store: Store): void => { - if (!store) return; -}; - -// other -const initOtherIpcMain = (mainWin: BrowserWindow | null): void => { - // 快捷键是否被注册 - ipcMain.handle("is-shortcut-registered", (_, shortcut: string) => isShortcutRegistered(shortcut)); - - // 注册快捷键 - ipcMain.handle("register-all-shortcut", (_, allShortcuts: any): string[] | false => { - if (!mainWin || !allShortcuts) return false; - // 卸载所有快捷键 - unregisterShortcuts(); - // 注册快捷键 - const failedShortcuts: string[] = []; - for (const key in allShortcuts) { - const shortcut = allShortcuts[key].globalShortcut; - if (!shortcut) continue; - // 快捷键回调 - const callback = () => mainWin.webContents.send(key); - const isSuccess = registerShortcut(shortcut, callback); - if (!isSuccess) failedShortcuts.push(shortcut); - } - return failedShortcuts; - }); - - // 卸载所有快捷键 - ipcMain.on("unregister-all-shortcut", () => unregisterShortcuts()); -}; - -export default initIpcMain; diff --git a/electron/main/logger.ts b/electron/main/logger.ts deleted file mode 100644 index 9f3e425..0000000 --- a/electron/main/logger.ts +++ /dev/null @@ -1,31 +0,0 @@ -// 日志输出 -import { join } from "path"; -import { app } from "electron"; -import { isDev } from "./utils"; -import log from "electron-log"; - -// 绑定事件 -Object.assign(console, log.functions); - -// 日志配置 -log.transports.file.level = "info"; -log.transports.file.maxSize = 2 * 1024 * 1024; // 2M -if (log.transports.ipc) log.transports.ipc.level = false; - -// 控制台输出 -log.transports.console.useStyles = true; - -// 文件输出 -log.transports.file.format = "{y}-{m}-{d} {h}:{i}:{s}:{ms} {text}"; - -// 本地输出 -if (!isDev) { - log.transports.file.resolvePathFn = () => - join(app.getPath("documents"), "/SPlayer/SPlayer-log.txt"); -} else { - log.transports.file.level = false; -} - -log.info("📃 logger initialized"); - -export default log; diff --git a/electron/main/logger/index.ts b/electron/main/logger/index.ts new file mode 100644 index 0000000..e7490af --- /dev/null +++ b/electron/main/logger/index.ts @@ -0,0 +1,47 @@ +// 日志输出 +import { existsSync, mkdirSync } from "fs"; +import { join } from "path"; +import { app } from "electron"; +import log from "electron-log"; + +// 日志文件路径 +const logDir = join(app.getPath("logs")); + +// 是否存在日志目录 +if (!existsSync(logDir)) mkdirSync(logDir); + +// 获取日期 - YYYY-MM-DD +const dateString = new Date().toISOString().slice(0, 10); +const logFilePath = join(logDir, `${dateString}.log`); + +// 配置日志系统 +log.transports.console.useStyles = true; // 颜色输出 +log.transports.file.level = "info"; // 仅记录 info 及以上级别 +log.transports.file.resolvePathFn = (): string => logFilePath; // 日志文件路径 +log.transports.file.maxSize = 2 * 1024 * 1024; // 文件最大 2MB + +// 日志格式化 +// log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}] [{level}] [{scope}] {text}"; + +// 绑定默认事件 +const defaultLog = log.scope("default"); +console.log = defaultLog.log; +console.info = defaultLog.info; +console.warn = defaultLog.warn; +console.error = defaultLog.error; + +// 分作用域导出 +export { defaultLog }; +export const ipcLog = log.scope("ipc"); +export const trayLog = log.scope("tray"); +export const thumbarLog = log.scope("thumbar"); +export const storeLog = log.scope("store"); +export const updateLog = log.scope("update"); +export const systemLog = log.scope("system"); +export const configLog = log.scope("config"); +export const windowsLog = log.scope("windows"); +export const processLog = log.scope("process"); +export const preloadLog = log.scope("preload"); +export const rendererLog = log.scope("renderer"); +export const shortcutLog = log.scope("shortcut"); +export const serverLog = log.scope("server"); diff --git a/electron/main/loginWin.ts b/electron/main/loginWin.ts deleted file mode 100644 index f5e94b6..0000000 --- a/electron/main/loginWin.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { BrowserWindow, session } from "electron"; -import icon from "../../public/icons/favicon.png?asset"; -import { join } from "path"; - -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: "登录网易云音乐( 若遇到无响应请关闭后重试 )", - width: 1280, - height: 800, - center: true, - autoHideMenuBar: true, - icon, - // resizable: false, - // movable: false, - // minimizable: false, - // maximizable: false, - webPreferences: { - session: loginSession, - sandbox: false, - webSecurity: false, - preload: join(__dirname, "../preload/index.mjs"), - }, - }); - - // 打开网易云 - loginWin.loadURL("https://music.163.com/#/login/"); - - // 阻止新窗口创建 - loginWin.webContents.setWindowOpenHandler(() => { - return { action: "deny" }; - }); - - // 检查是否登录 - const checkLogin = async () => { - try { - loginWin.webContents.executeJavaScript( - "document.title = '登录网易云音乐( 若遇到无响应请关闭后重试 )'", - ); - // 是否登录?判断 MUSIC_U - const MUSIC_U = await loginSession.cookies.get({ - name: "MUSIC_U", - }); - 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); - } - }; - - // 循环检查 - loginWin.webContents.once("did-finish-load", () => { - loginWin.show(); - loginTimer = setInterval(checkLogin, 1000); - loginWin.on("closed", () => { - clearInterval(loginTimer); - }); - }); -}; - -export default openLoginWin; diff --git a/electron/main/shortcut.ts b/electron/main/shortcut/index.ts similarity index 66% rename from electron/main/shortcut.ts rename to electron/main/shortcut/index.ts index e35058b..89cc1ed 100644 --- a/electron/main/shortcut.ts +++ b/electron/main/shortcut/index.ts @@ -1,19 +1,19 @@ import { globalShortcut } from "electron"; -import log from "../main/logger"; +import { shortcutLog } from "../logger"; // 注册快捷键并检查 export const registerShortcut = (shortcut: string, callback: () => void): boolean => { try { const success = globalShortcut.register(shortcut, callback); if (!success) { - log.error(`❌ Failed to register shortcut: ${shortcut}`); + shortcutLog.error(`❌ Failed to register shortcut: ${shortcut}`); return false; } else { - log.info(`✅ Shortcut registered: ${shortcut}`); + shortcutLog.info(`✅ Shortcut registered: ${shortcut}`); return true; } } catch (error) { - log.error(`ℹ️ Error registering shortcut ${shortcut}:`, error); + shortcutLog.error(`ℹ️ Error registering shortcut ${shortcut}:`, error); return false; } }; @@ -26,5 +26,5 @@ export const isShortcutRegistered = (shortcut: string): boolean => { // 卸载所有快捷键 export const unregisterShortcuts = () => { globalShortcut.unregisterAll(); - log.info("🚫 All shortcuts unregistered."); + shortcutLog.info("🚫 All shortcuts unregistered."); }; diff --git a/electron/main/store.ts b/electron/main/store/index.ts similarity index 84% rename from electron/main/store.ts rename to electron/main/store/index.ts index ee7a018..c41d988 100644 --- a/electron/main/store.ts +++ b/electron/main/store/index.ts @@ -1,8 +1,8 @@ -import Store from "electron-store"; import { screen } from "electron"; -import log from "./logger"; +import { storeLog } from "../logger"; +import Store from "electron-store"; -log.info("🌱 Store init"); +storeLog.info("🌱 Store init"); export interface StoreType { window: { @@ -25,8 +25,11 @@ export interface StoreType { proxy: string; } -// 初始化仓库 -export const initStore = () => { +/** + * 使用 Store + * @returns Store + */ +export const useStore = () => { return new Store({ defaults: { window: { diff --git a/electron/main/thumbar.ts b/electron/main/thumbar/index.ts similarity index 83% rename from electron/main/thumbar.ts rename to electron/main/thumbar/index.ts index 80c199f..d7a6590 100644 --- a/electron/main/thumbar.ts +++ b/electron/main/thumbar/index.ts @@ -1,7 +1,7 @@ import { BrowserWindow, nativeImage, nativeTheme, ThumbarButton } from "electron"; import { join } from "path"; -import { isWin } from "./utils"; -import log from "./logger"; +import { isWin } from "../utils/config"; +import { thumbarLog } from "../logger"; enum ThumbarKeys { Play = "play", @@ -17,6 +17,9 @@ export interface Thumbar { updateThumbar(playing: boolean, clean?: boolean): void; } +// 缩略图单例 +let thumbar: Thumbar | null = null; + // 工具栏图标 const thumbarIcon = (filename: string) => { // 是否为暗色 @@ -86,14 +89,26 @@ class createThumbar implements Thumbar { } } +/** + * 初始化缩略图工具栏 + * @param win 窗口 + * @returns 缩略图工具栏 + */ export const initThumbar = (win: BrowserWindow) => { try { // 若非 Win if (!isWin) return null; - log.info("🚀 ThumbarButtons Startup"); - return new createThumbar(win); + thumbarLog.info("🚀 ThumbarButtons Startup"); + thumbar = new createThumbar(win); + return thumbar; } catch (error) { - log.error("❌ ThumbarButtons Error", error); + thumbarLog.error("❌ ThumbarButtons Error", error); throw error; } }; + +/** + * 获取缩略图工具栏 + * @returns 缩略图工具栏 + */ +export const getThumbar = () => thumbar; diff --git a/electron/main/tray.ts b/electron/main/tray/index.ts similarity index 85% rename from electron/main/tray.ts rename to electron/main/tray/index.ts index 6c86ccf..89a0b2b 100644 --- a/electron/main/tray.ts +++ b/electron/main/tray/index.ts @@ -7,9 +7,9 @@ import { nativeImage, nativeTheme, } from "electron"; -import { isWin, appName } from "./utils"; +import { isWin, appName } from "../utils/config"; import { join } from "path"; -import log from "./logger"; +import { trayLog } from "../logger"; // 播放模式 type PlayMode = "repeat" | "repeat-once" | "shuffle"; @@ -34,6 +34,9 @@ export interface MainTray { destroyTray(): void; } +// 托盘单例 +let mainTrayInstance: MainTray | null = null; + // 托盘图标 const trayIcon = (filename: string) => { // const rootPath = isDev @@ -219,11 +222,19 @@ class CreateTray implements MainTray { }); } // 设置标题 + /** + * 设置标题 + * @param title 标题 + */ setTitle(title: string) { + this._win.setTitle(title); this._tray.setTitle(title); this._tray.setToolTip(title); } - // 设置播放名称 + /** + * 设置播放名称 + * @param name 播放名称 + */ setPlayName(name: string) { // 超长处理 if (name.length > 20) name = name.slice(0, 20) + "..."; @@ -231,48 +242,80 @@ class CreateTray implements MainTray { // 更新菜单 this.initTrayMenu(); } - // 设置播放状态 + /** + * 设置播放状态 + * @param state 播放状态 + */ setPlayState(state: PlayState) { playState = state; // 更新菜单 this.initTrayMenu(); } - // 设置播放模式 + /** + * 设置播放模式 + * @param mode 播放模式 + */ setPlayMode(mode: PlayMode) { playMode = mode; // 更新菜单 this.initTrayMenu(); } - // 设置喜欢状态 + /** + * 设置喜欢状态 + * @param like 喜欢状态 + */ setLikeState(like: boolean) { likeSong = like; // 更新菜单 this.initTrayMenu(); } - // 桌面歌词开关 + /** + * 桌面歌词开关 + * @param show 桌面歌词开关状态 + */ setDesktopLyricShow(show: boolean) { desktopLyricShow = show; // 更新菜单 this.initTrayMenu(); } - // 锁定桌面歌词 + /** + * 锁定桌面歌词 + * @param lock 锁定桌面歌词状态 + */ setDesktopLyricLock(lock: boolean) { desktopLyricLock = lock; // 更新菜单 this.initTrayMenu(); } - // 销毁托盘 + /** + * 销毁托盘 + */ destroyTray() { this._tray.destroy(); } } +/** + * 初始化托盘 + * @param win 主窗口 + * @param lyricWin 歌词窗口 + * @returns 托盘实例 + */ export const initTray = (win: BrowserWindow, lyricWin: BrowserWindow) => { try { - log.info("🚀 Tray Process Startup"); - return new CreateTray(win, lyricWin); + trayLog.info("🚀 Tray Process Startup"); + const tray = new CreateTray(win, lyricWin); + // 保存单例实例 + mainTrayInstance = tray; + return tray; } catch (error) { - log.error("❌ Tray Process Error", error); + trayLog.error("❌ Tray Process Error", error); return null; } }; + +/** + * 获取托盘实例 + * @returns 托盘实例 + */ +export const getMainTray = (): MainTray | null => mainTrayInstance; diff --git a/electron/main/update.ts b/electron/main/update/index.ts similarity index 73% rename from electron/main/update.ts rename to electron/main/update/index.ts index 11991be..92c9227 100644 --- a/electron/main/update.ts +++ b/electron/main/update/index.ts @@ -1,10 +1,18 @@ -import { type BrowserWindow } from "electron"; +import { app, type BrowserWindow } from "electron"; +import { updateLog } from "../logger"; import electronUpdater from "electron-updater"; -import log from "./logger"; +import { isDev } from "../utils/config"; // import const { autoUpdater } = electronUpdater; +// 开发环境启用 +if (isDev) { + Object.defineProperty(app, "isPackaged", { + get: () => true, + }); +} + // 更新源 autoUpdater.setFeedURL({ provider: "github", @@ -28,19 +36,19 @@ const initUpdaterListeners = (win: BrowserWindow) => { // 当有新版本可用时 autoUpdater.on("update-available", (info) => { win.webContents.send("update-available", info); - log.info(`🚀 New version available: ${info.version}`); + updateLog.info(`🚀 New version available: ${info.version}`); }); // 更新下载进度 autoUpdater.on("download-progress", (progress) => { win.webContents.send("download-progress", progress); - log.info(`🚀 Downloading: ${progress.percent}%`); + updateLog.info(`🚀 Downloading: ${progress.percent}%`); }); // 当下载完成时 autoUpdater.on("update-downloaded", (info) => { win.webContents.send("update-downloaded", info); - log.info(`🚀 Update downloaded: ${info.version}`); + updateLog.info(`🚀 Update downloaded: ${info.version}`); // 安装更新 autoUpdater.quitAndInstall(); }); @@ -48,13 +56,13 @@ const initUpdaterListeners = (win: BrowserWindow) => { // 当没有新版本时 autoUpdater.on("update-not-available", (info) => { if (isShowTip) win.webContents.send("update-not-available", info); - log.info(`✅ No new version available: ${info.version}`); + updateLog.info(`✅ No new version available: ${info.version}`); }); // 更新错误 autoUpdater.on("error", (err) => { win.webContents.send("update-error", err); - log.error(`❌ Update error: ${err.message}`); + updateLog.error(`❌ Update error: ${err.message}`); }); isInit = true; diff --git a/electron/main/utils/config.ts b/electron/main/utils/config.ts new file mode 100644 index 0000000..b0d5d62 --- /dev/null +++ b/electron/main/utils/config.ts @@ -0,0 +1,51 @@ +import { is } from "@electron-toolkit/utils"; +import { app } from "electron"; + +/** + * 是否为开发环境 + * @returns boolean + */ +export const isDev = is.dev; + +/** 是否为 Windows 系统 */ +export const isWin = process.platform === "win32"; +/** 是否为 macOS 系统 */ +export const isMac = process.platform === "darwin"; +/** 是否为 Linux 系统 */ +export const isLinux = process.platform === "linux"; + +/** + * 软件版本 + * @returns string + */ +export const appVersion = app.getVersion(); + +/** + * 程序名称 + * @returns string + */ +export const appName = app.getName() || "SPlayer"; + +/** + * 服务器端口 + * @returns number + */ +export const port = Number(import.meta.env["VITE_SERVER_PORT"] || 25884); + +/** + * 主窗口加载地址 + * @returns string + */ +export const mainWinUrl = + isDev && process.env["ELECTRON_RENDERER_URL"] + ? process.env["ELECTRON_RENDERER_URL"] + : `http://localhost:${port}`; + +/** + * 加载窗口地址 + * @returns string + */ +export const loadWinUrl = + isDev && process.env["ELECTRON_RENDERER_URL"] + ? `${process.env["ELECTRON_RENDERER_URL"]}/web/loading/index.html` + : `http://localhost:${port}/web/loading/index.html`; diff --git a/electron/main/utils.ts b/electron/main/utils/helper.ts similarity index 69% rename from electron/main/utils.ts rename to electron/main/utils/helper.ts index 6e29854..c0b7236 100644 --- a/electron/main/utils.ts +++ b/electron/main/utils/helper.ts @@ -1,21 +1,14 @@ -import { app } from "electron"; -import { is } from "@electron-toolkit/utils"; -import fs from "fs/promises"; -import crypto from "crypto"; +import { createHash } from "crypto"; +import { readFile } from "fs/promises"; -// 系统判断 -export const isDev = is.dev; -export const isWin = process.platform === "win32"; -export const isMac = process.platform === "darwin"; -export const isLinux = process.platform === "linux"; - -// 程序名称 -export const appName = app.getName() || "SPlayer"; - -// 生成唯一ID +/** + * 生成文件唯一ID + * @param filePath 文件路径 + * @returns 唯一ID + */ export const getFileID = (filePath: string): number => { // SHA-256 - const hash = crypto.createHash("sha256"); + const hash = createHash("sha256"); hash.update(filePath); const digest = hash.digest("hex"); // 将哈希值的前 16 位转换为十进制数字 @@ -23,10 +16,14 @@ export const getFileID = (filePath: string): number => { return Number(uniqueId.toString().padStart(16, "0")); }; -// 生成文件 MD5 +/** + * 生成文件 MD5 + * @param path 文件路径 + * @returns MD5值 + */ export const getFileMD5 = async (path: string): Promise => { - const data = await fs.readFile(path); - const hash = crypto.createHash("md5"); + const data = await readFile(path); + const hash = createHash("md5"); hash.update(data); return hash.digest("hex"); }; diff --git a/electron/main/utils/single-lock.ts b/electron/main/utils/single-lock.ts new file mode 100644 index 0000000..2a987e6 --- /dev/null +++ b/electron/main/utils/single-lock.ts @@ -0,0 +1,25 @@ +import { app } from "electron"; +import { systemLog } from "../logger"; +import mainWindow from "../windows/main-window"; + +/** + * 初始化单实例锁 + * @returns 如果当前实例获得了锁,返回 true;否则返回 false + */ +export const initSingleLock = (): boolean => { + const gotTheLock = app.requestSingleInstanceLock(); + // 如果未获得锁,退出当前实例 + if (!gotTheLock) { + app.quit(); + systemLog.warn("❌ 已有一个实例正在运行"); + return false; + } + // 当第二个实例启动时触发 + else { + app.on("second-instance", () => { + systemLog.warn("❌ 第二个实例将要启动"); + mainWindow.getWin()?.show(); + }); + } + return true; +}; diff --git a/electron/main/windows/index.ts b/electron/main/windows/index.ts new file mode 100644 index 0000000..49cc924 --- /dev/null +++ b/electron/main/windows/index.ts @@ -0,0 +1,45 @@ +import { BrowserWindow, BrowserWindowConstructorOptions } from "electron"; +import { windowsLog } from "../logger"; +import { appName } from "../utils/config"; +import { join } from "path"; +import icon from "../../../public/icons/favicon.png?asset"; + +export const createWindow = ( + options: BrowserWindowConstructorOptions = {}, +): BrowserWindow | null => { + try { + const defaultOptions: BrowserWindowConstructorOptions = { + title: appName, + width: 1280, + height: 720, + frame: false, // 创建后是否显示窗口 + center: true, // 窗口居中 + icon, // 窗口图标 + autoHideMenuBar: true, // 隐藏菜单栏 + webPreferences: { + preload: join(__dirname, "../preload/index.mjs"), + // 禁用渲染器沙盒 + sandbox: false, + // 禁用同源策略 + webSecurity: false, + // 允许 HTTP + allowRunningInsecureContent: true, + // 禁用拼写检查 + spellcheck: false, + // 启用 Node.js + nodeIntegration: true, + nodeIntegrationInWorker: true, + // 启用上下文隔离 + contextIsolation: false, + }, + }; + // 合并参数 + options = Object.assign(defaultOptions, options); + // 创建窗口 + const win = new BrowserWindow(options); + return win; + } catch (error) { + windowsLog.error(error); + return null; + } +}; diff --git a/electron/main/windows/load-window.ts b/electron/main/windows/load-window.ts new file mode 100644 index 0000000..39659db --- /dev/null +++ b/electron/main/windows/load-window.ts @@ -0,0 +1,60 @@ +import { BrowserWindow } from "electron"; +import { createWindow } from "./index"; +import { loadWinUrl } from "../utils/config"; + +class LoadWindow { + private win: BrowserWindow | null = null; + private winURL: string; + constructor() { + this.winURL = loadWinUrl; + } + /** + * 主窗口事件 + * @returns void + */ + private event(): void { + if (!this.win) return; + // 准备好显示 + this.win.on("ready-to-show", () => { + this.win?.show(); + }); + } + /** + * 创建窗口 + * @returns BrowserWindow | null + */ + create(): BrowserWindow | null { + this.win = createWindow({ + width: 800, + height: 560, + maxWidth: 800, + maxHeight: 560, + resizable: false, + alwaysOnTop: true, + // 不在任务栏显示 + skipTaskbar: true, + // 窗口不能最小化 + minimizable: false, + // 窗口不能最大化 + maximizable: false, + // 窗口不能进入全屏状态 + fullscreenable: false, + show: false, + }); + if (!this.win) return null; + // 加载地址 + this.win.loadURL(this.winURL); + // 窗口事件 + this.event(); + return this.win; + } + /** + * 获取窗口 + * @returns BrowserWindow | null + */ + getWin(): BrowserWindow | null { + return this.win; + } +} + +export default new LoadWindow(); diff --git a/electron/main/windows/login-window.ts b/electron/main/windows/login-window.ts new file mode 100644 index 0000000..75c7a9c --- /dev/null +++ b/electron/main/windows/login-window.ts @@ -0,0 +1,100 @@ +import { app, BrowserWindow, session } from "electron"; +import { createWindow } from "./index"; +import { join } from "path"; + +class LoginWindow { + private win: BrowserWindow | null = null; + private loginTimer: NodeJS.Timeout | null = null; + private loginSession: Electron.Session | null = null; + + constructor() {} + + private getLoginSession(): Electron.Session { + if (!this.loginSession) { + this.loginSession = session.fromPartition("persist:login"); + } + return this.loginSession; + } + // 事件绑定 + private event(mainWin: BrowserWindow): void { + if (!this.win) return; + // 阻止新窗口创建 + this.win.webContents.setWindowOpenHandler(() => ({ action: "deny" })); + // 加载完成后显示并开始轮询登录状态 + this.win.webContents.once("did-finish-load", () => { + this.win?.show(); + this.loginTimer = setInterval(() => this.checkLogin(mainWin), 1000); + this.win?.on("closed", () => { + if (this.loginTimer) clearInterval(this.loginTimer); + this.loginTimer = null; + }); + }); + } + + // 检查是否已登录 + private async checkLogin(mainWin: BrowserWindow) { + if (!this.win) return; + try { + this.win.webContents.executeJavaScript( + "document.title = '登录网易云音乐( 若遇到无响应请关闭后重试 )'", + ); + // 判断 MUSIC_U + const MUSIC_U = await this.getLoginSession().cookies.get({ name: "MUSIC_U" }); + if (MUSIC_U && MUSIC_U.length > 0) { + if (this.loginTimer) clearInterval(this.loginTimer); + this.loginTimer = null; + const value = `MUSIC_U=${MUSIC_U[0].value};`; + // 发送回主进程 + mainWin?.webContents.send("send-cookies", value); + this.win.destroy(); + this.win = null; + } + } catch (error) { + console.error(error); + } + } + + // 创建登录窗口 + async create(mainWin: BrowserWindow): Promise { + await app.whenReady(); + const loginSession = this.getLoginSession(); + // 清理登录会话存储 + await loginSession.clearStorageData({ + storages: ["cookies", "localstorage"], + }); + + this.win = createWindow({ + parent: mainWin, + title: "登录网易云音乐( 若遇到无响应请关闭后重试 )", + width: 1280, + height: 800, + center: true, + autoHideMenuBar: true, + webPreferences: { + preload: join(__dirname, "../preload/index.mjs"), + sandbox: false, + webSecurity: false, + allowRunningInsecureContent: true, + spellcheck: false, + nodeIntegration: true, + nodeIntegrationInWorker: true, + contextIsolation: false, + session: loginSession, + }, + }); + + if (!this.win) return null; + // 加载登录地址 + this.win.loadURL("https://music.163.com/#/login/"); + // 绑定事件 + this.event(mainWin); + return this.win; + } + + // 获取窗口 + getWin(): BrowserWindow | null { + return this.win; + } +} + +export default new LoginWindow(); diff --git a/electron/main/windows/lyric-window.ts b/electron/main/windows/lyric-window.ts new file mode 100644 index 0000000..fa46feb --- /dev/null +++ b/electron/main/windows/lyric-window.ts @@ -0,0 +1,73 @@ +import { BrowserWindow } from "electron"; +import { createWindow } from "./index"; +import { useStore } from "../store"; +import { join } from "path"; + +class LyricWindow { + private win: BrowserWindow | null = null; + constructor() {} + /** + * 主窗口事件 + * @returns void + */ + private event(): void { + if (!this.win) return; + // 歌词窗口缩放 + this.win?.on("resized", () => { + const store = useStore(); + const bounds = this.win?.getBounds(); + if (bounds) { + const { width, height } = bounds; + store.set("lyric", { ...store.get("lyric"), width, height }); + } + }); + } + /** + * 创建主窗口 + * @returns BrowserWindow | null + */ + create(): BrowserWindow | null { + const store = useStore(); + const { width, height, x, y } = store.get("lyric"); + this.win = createWindow({ + width: width || 800, + height: height || 180, + minWidth: 440, + minHeight: 120, + maxWidth: 1600, + maxHeight: 300, + // 窗口位置 + x, + y, + transparent: true, + backgroundColor: "rgba(0, 0, 0, 0)", + alwaysOnTop: true, + resizable: true, + movable: true, + show: false, + // 不在任务栏显示 + skipTaskbar: true, + // 窗口不能最小化 + minimizable: false, + // 窗口不能最大化 + maximizable: false, + // 窗口不能进入全屏状态 + fullscreenable: false, + }); + if (!this.win) return null; + // 加载地址 + this.win.loadFile(join(__dirname, "../main/web/lyric.html")); + // 窗口事件 + this.event(); + return this.win; + } + /** + * 获取窗口 + * @returns BrowserWindow | null + */ + getWin(): BrowserWindow | null { + return this.win; + } +} + +export default new LyricWindow(); diff --git a/electron/main/windows/main-window.ts b/electron/main/windows/main-window.ts new file mode 100644 index 0000000..a9ac8ce --- /dev/null +++ b/electron/main/windows/main-window.ts @@ -0,0 +1,126 @@ +import { BrowserWindow, shell } from "electron"; +import { createWindow } from "./index"; +import { mainWinUrl } from "../utils/config"; +import { useStore } from "../store"; +import { isLinux } from "../utils/config"; + +class MainWindow { + private win: BrowserWindow | null = null; + private winURL: string; + constructor() { + this.winURL = mainWinUrl; + } + /** + * 保存窗口大小和状态 + */ + private saveBounds() { + if (this.win?.isFullScreen()) return; + const store = useStore(); + const bounds = this.win?.getBounds(); + if (bounds) { + const maximized = this.win?.isMaximized(); + store.set("window", { ...bounds, maximized }); + } + } + /** + * 主窗口事件 + * @returns void + */ + private event(): void { + if (!this.win) return; + const store = useStore(); + // 配置网络代理 + if (store.get("proxy")) { + this.win.webContents.session.setProxy({ proxyRules: store.get("proxy") }); + } + + // 窗口打开处理程序 + this.win.webContents.setWindowOpenHandler((details) => { + const { url } = details; + if (url.startsWith("https://") || url.startsWith("http://")) { + shell.openExternal(url); + } + return { action: "deny" }; + }); + // 窗口显示时 + this.win?.on("show", () => { + // this.mainWindow?.webContents.send("lyricsScroll"); + }); + // 窗口获得焦点时 + this.win?.on("focus", () => { + this.saveBounds(); + }); + // 窗口大小改变时 + this.win?.on("resized", () => { + // 若处于全屏则不保存 + if (this.win?.isFullScreen()) return; + this.saveBounds(); + }); + // 窗口位置改变时 + this.win?.on("moved", () => { + this.saveBounds(); + }); + // 窗口最大化时 + this.win?.on("maximize", () => { + this.saveBounds(); + this.win?.webContents.send("win-state-change", true); + }); + // 窗口取消最大化时 + this.win?.on("unmaximize", () => { + this.saveBounds(); + this.win?.webContents.send("win-state-change", false); + }); + // Linux 无法使用 resized 和 moved + if (isLinux) { + this.win?.on("resize", () => { + // 若处于全屏则不保存 + if (this.win?.isFullScreen()) return; + this.saveBounds(); + }); + this.win?.on("move", () => { + this.saveBounds(); + }); + } + } + /** + * 创建窗口 + * @returns BrowserWindow | null + */ + create(): BrowserWindow | null { + const store = useStore(); + const { width, height } = store.get("window"); + this.win = createWindow({ + // 菜单栏 + titleBarStyle: "customButtonsOnHover", + width, + height, + minHeight: 600, + minWidth: 800, + show: false, + }); + if (!this.win) return null; + // 加载地址 + this.win.loadURL(this.winURL); + // 窗口事件 + this.event(); + return this.win; + } + /** + * 获取窗口 + * @returns BrowserWindow | null + */ + getWin(): BrowserWindow | null { + return this.win; + } + /** + * 显示主窗口 + */ + showWindow() { + if (this.win) { + this.win.show(); + if (this.win.isMinimized()) this.win.restore(); + this.win.focus(); + } + } +} +export default new MainWindow(); diff --git a/electron/server/index.ts b/electron/server/index.ts index 87ffdac..9fe6d58 100644 --- a/electron/server/index.ts +++ b/electron/server/index.ts @@ -1,12 +1,12 @@ import { join } from "path"; -import { isDev } from "../main/utils"; +import { isDev } from "../main/utils/config"; +import { serverLog } from "../main/logger"; import initNcmAPI from "./netease"; import initUnblockAPI from "./unblock"; import fastifyCookie from "@fastify/cookie"; import fastifyMultipart from "@fastify/multipart"; import fastifyStatic from "@fastify/static"; import fastify from "fastify"; -import log from "../main/logger"; const initAppServer = async () => { try { @@ -21,7 +21,7 @@ const initAppServer = async () => { server.register(fastifyMultipart); // 生产环境启用静态文件 if (!isDev) { - log.info("📂 Serving static files from /renderer"); + serverLog.info("📂 Serving static files from /renderer"); server.register(fastifyStatic, { root: join(__dirname, "../renderer"), }); @@ -50,10 +50,10 @@ const initAppServer = async () => { // 启动端口 const port = Number(process.env["VITE_SERVER_PORT"] || 25884); await server.listen({ port }); - log.info(`🌐 Starting AppServer on port ${port}`); + serverLog.info(`🌐 Starting AppServer on port ${port}`); return server; } catch (error) { - log.error("🚫 AppServer failed to start"); + serverLog.error("🚫 AppServer failed to start"); throw error; } }; diff --git a/electron/server/netease/index.ts b/electron/server/netease/index.ts index 734645f..04c121e 100644 --- a/electron/server/netease/index.ts +++ b/electron/server/netease/index.ts @@ -1,7 +1,7 @@ import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; import { pathCase } from "change-case"; +import { serverLog } from "../../main/logger"; import NeteaseCloudMusicApi from "@neteasecloudmusicapienhanced/api"; -import log from "../../main/logger"; // 获取数据 const getHandler = (name: string, neteaseApi: (params: any) => any) => { @@ -9,7 +9,7 @@ const getHandler = (name: string, neteaseApi: (params: any) => any) => { req: FastifyRequest<{ Querystring: { [key: string]: string } }>, reply: FastifyReply, ) => { - log.info("🌐 Request NcmAPI:", name); + serverLog.log("🌐 Request NcmAPI:", name); // 获取 NcmAPI 数据 try { const result = await neteaseApi({ @@ -19,7 +19,7 @@ const getHandler = (name: string, neteaseApi: (params: any) => any) => { }); return reply.send(result.body); } catch (error: any) { - log.error("❌ NcmAPI Error:", error); + serverLog.error("❌ NcmAPI Error:", error); if ([400, 301].includes(error.status)) { return reply.status(error.status).send(error.body); } @@ -60,7 +60,7 @@ const initNcmAPI = async (fastify: FastifyInstance) => { } }); - log.info("🌐 Register NcmAPI successfully"); + serverLog.info("🌐 Register NcmAPI successfully"); }; export default initNcmAPI; diff --git a/electron/server/unblock/index.ts b/electron/server/unblock/index.ts index e1bce02..9df3f58 100644 --- a/electron/server/unblock/index.ts +++ b/electron/server/unblock/index.ts @@ -1,7 +1,7 @@ import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; import { SongUrlResult } from "./unblock"; +import { serverLog } from "../../main/logger"; import getKuwoSongUrl from "./kuwo"; -import log from "../../main/logger"; import axios from "axios"; /** @@ -17,10 +17,10 @@ const getNeteaseSongUrl = async (id: number | string): Promise => params: { types: "url", id }, }); const songUrl = result.data.url; - log.info("🔗 NeteaseSongUrl URL:", songUrl); + serverLog.log("🔗 NeteaseSongUrl URL:", songUrl); return { code: 200, url: songUrl }; } catch (error) { - log.error("❌ Get NeteaseSongUrl Error:", error); + serverLog.error("❌ Get NeteaseSongUrl Error:", error); return { code: 404, url: null }; } }; @@ -62,7 +62,7 @@ const UnblockAPI = async (fastify: FastifyInstance) => { }, ); - log.info("🌐 Register UnblockAPI successfully"); + serverLog.info("🌐 Register UnblockAPI successfully"); }; export default UnblockAPI; diff --git a/electron/server/unblock/kuwo.ts b/electron/server/unblock/kuwo.ts index 0caee66..df73364 100644 --- a/electron/server/unblock/kuwo.ts +++ b/electron/server/unblock/kuwo.ts @@ -1,6 +1,6 @@ import { encryptQuery } from "./kwDES"; import { SongUrlResult } from "./unblock"; -import log from "../../main/logger"; +import { serverLog } from "../../main/logger"; import axios from "axios"; // 获取酷我音乐歌曲 ID @@ -26,7 +26,7 @@ const getKuwoSongId = async (keyword: string): Promise => { if (songName && !songName?.includes(originalName[0])) return null; return songId.slice("MUSIC_".length); } catch (error) { - log.error("❌ Get KuwoSongId Error:", error); + serverLog.error("❌ Get KuwoSongId Error:", error); return null; } }; @@ -53,12 +53,12 @@ const getKuwoSongUrl = async (keyword: string): Promise => { }); if (result.data) { const urlMatch = result.data.match(/http[^\s$"]+/)[0]; - log.info("🔗 KuwoSong URL:", urlMatch); + serverLog.log("🔗 KuwoSong URL:", urlMatch); return { code: 200, url: urlMatch }; } return { code: 404, url: null }; } catch (error) { - log.error("❌ Get KuwoSong URL Error:", error); + serverLog.error("❌ Get KuwoSong URL Error:", error); return { code: 404, url: null }; } }; diff --git a/package.json b/package.json index b566fea..661ee66 100644 --- a/package.json +++ b/package.json @@ -34,13 +34,13 @@ }, "dependencies": { "@applemusic-like-lyrics/core": "^0.1.3", - "@applemusic-like-lyrics/lyric": "^0.2.4", + "@applemusic-like-lyrics/lyric": "^0.3.0", "@applemusic-like-lyrics/vue": "^0.1.5", "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", "@imsyy/color-utils": "^1.0.2", "@material/material-color-utilities": "^0.3.0", - "@neteasecloudmusicapienhanced/api": "^4.29.11", + "@neteasecloudmusicapienhanced/api": "^4.29.12", "@pixi/app": "^7.4.3", "@pixi/core": "^7.4.3", "@pixi/display": "^7.4.3", @@ -53,7 +53,7 @@ "change-case": "^5.4.4", "dayjs": "^1.11.18", "electron-dl": "^4.0.0", - "electron-store": "^8.2.0", + "electron-store": "^11.0.2", "electron-updater": "^6.6.2", "file-saver": "^2.0.5", "font-list": "^2.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c3fce1..2db178c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,8 @@ importers: specifier: ^0.1.3 version: 0.1.3(@pixi/app@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3)))(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))(@pixi/filter-blur@7.4.3(@pixi/core@7.4.3))(@pixi/filter-bulge-pinch@5.1.1(@pixi/core@7.4.3))(@pixi/filter-color-matrix@7.4.3(@pixi/core@7.4.3))(@pixi/sprite@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3)))(jss-preset-default@10.10.0)(jss@10.10.0) '@applemusic-like-lyrics/lyric': - specifier: ^0.2.4 - version: 0.2.4 + specifier: ^0.3.0 + version: 0.3.0 '@applemusic-like-lyrics/vue': specifier: ^0.1.5 version: 0.1.5(@pixi/app@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3)))(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))(@pixi/filter-blur@7.4.3(@pixi/core@7.4.3))(@pixi/filter-bulge-pinch@5.1.1(@pixi/core@7.4.3))(@pixi/filter-color-matrix@7.4.3(@pixi/core@7.4.3))(@pixi/sprite@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3)))(jss-preset-default@10.10.0)(jss@10.10.0)(vue@3.5.22(typescript@5.9.3)) @@ -34,8 +34,8 @@ importers: specifier: ^0.3.0 version: 0.3.0 '@neteasecloudmusicapienhanced/api': - specifier: ^4.29.11 - version: 4.29.11 + specifier: ^4.29.12 + version: 4.29.12 '@pixi/app': specifier: ^7.4.3 version: 7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3)) @@ -73,8 +73,8 @@ importers: specifier: ^4.0.0 version: 4.0.0 electron-store: - specifier: ^8.2.0 - version: 8.2.0 + specifier: ^11.0.2 + version: 11.0.2 electron-updater: specifier: ^6.6.2 version: 6.6.2 @@ -260,8 +260,8 @@ packages: jss: '*' jss-preset-default: '*' - '@applemusic-like-lyrics/lyric@0.2.4': - resolution: {integrity: sha512-B1N7dMEwEIlgtyqLGVt7EbXJBQLCbv5J/N0YjkgWHu5qM1vMhKSGOXzrBuMDusmr4Z6tfO5iLfOYQUtoSXZyeg==} + '@applemusic-like-lyrics/lyric@0.3.0': + resolution: {integrity: sha512-ZGyBheZZfqjQmGnEUciNKCARwkqIP39ONZirJE+NOQjQ47TYlZz4tlLBRH/uRfq5qviYyJ1S9Q+pZxlYoyHWVw==} '@applemusic-like-lyrics/vue@0.1.5': resolution: {integrity: sha512-FE8XtfoScmSwg61XFdNjdYBBQ8RvT10V01hFv38sMsOPUxLQn7rVUafx3NCwUDwGbAYPrC4Azn5wUpeZPdKFOg==} @@ -871,8 +871,8 @@ packages: '@material/material-color-utilities@0.3.0': resolution: {integrity: sha512-ztmtTd6xwnuh2/xu+Vb01btgV8SQWYCaK56CkRK8gEkWe5TuDyBcYJ0wgkMRn+2VcE9KUmhvkz+N9GHrqw/C0g==} - '@neteasecloudmusicapienhanced/api@4.29.11': - resolution: {integrity: sha512-6yajC4cDH74+xpI5OadFXqIeA1TgmAi67QLRv3JbPet9JIWN6DXAagCz0QJUbo0bUPIdH1CimyU/P+eEyxP5nA==} + '@neteasecloudmusicapienhanced/api@4.29.12': + resolution: {integrity: sha512-oIT+XgjMN/m0e1C7c+KUKee9AMjAziJY28aZ2m4yAAJyMkAwgcMHpNs9nQ91U8FRHGwX4TNXoWGX1Tczx484TA==} engines: {node: '>=12'} hasBin: true @@ -1458,14 +1458,6 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -1562,9 +1554,8 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} - atomically@1.7.0: - resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} - engines: {node: '>=10.12.0'} + atomically@2.0.3: + resolution: {integrity: sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==} avvio@9.1.0: resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} @@ -1802,9 +1793,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - conf@10.2.0: - resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==} - engines: {node: '>=12'} + conf@15.0.2: + resolution: {integrity: sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw==} + engines: {node: '>=20'} confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -1909,9 +1900,9 @@ packages: dayjs@1.11.18: resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} - debounce-fn@4.0.0: - resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==} - engines: {node: '>=10'} + debounce-fn@6.0.0: + resolution: {integrity: sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==} + engines: {node: '>=18'} debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -2000,9 +1991,9 @@ packages: os: [darwin] hasBin: true - dot-prop@6.0.1: - resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} - engines: {node: '>=10'} + dot-prop@10.1.0: + resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==} + engines: {node: '>=20'} dotenv-expand@11.0.7: resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} @@ -2012,6 +2003,10 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2052,8 +2047,9 @@ packages: electron-publish@26.0.11: resolution: {integrity: sha512-a8QRH0rAPIWH9WyyS5LbNvW9Ark6qe63/LqDB7vu2JXYpi0Gma5Q60Dh4tmTqhOBQt0xsrzD8qE7C+D7j+B24A==} - electron-store@8.2.0: - resolution: {integrity: sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw==} + electron-store@11.0.2: + resolution: {integrity: sha512-4VkNRdN+BImL2KcCi41WvAYbh6zLX5AUTi4so68yPqiItjbgTjqpEnGAqasgnG+lB6GuAyUltKwVopp6Uv+gwQ==} + engines: {node: '>=20'} electron-to-chromium@1.5.235: resolution: {integrity: sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==} @@ -2105,6 +2101,10 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + err-code@2.0.3: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} @@ -2365,10 +2365,6 @@ packages: resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==} engines: {node: '>=20'} - find-up@3.0.0: - resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} - engines: {node: '>=6'} - find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -2715,10 +2711,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - is-plain-obj@1.1.0: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} @@ -2795,8 +2787,8 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-schema-typed@7.0.3: - resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==} + json-schema-typed@8.0.1: + resolution: {integrity: sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==} json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2891,10 +2883,6 @@ packages: localforage@1.10.0: resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} - locate-path@3.0.0: - resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} - engines: {node: '>=6'} - locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -3014,9 +3002,9 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - mimic-fn@3.1.0: - resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} - engines: {node: '>=8'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} mimic-response@1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} @@ -3240,10 +3228,6 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-locate@3.0.0: - resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} - engines: {node: '>=6'} - p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -3282,10 +3266,6 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-exists@3.0.0: - resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} - engines: {node: '>=4'} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3394,10 +3374,6 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - pkg-up@3.1.0: - resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} - engines: {node: '>=8'} - plist@3.1.0: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} @@ -3849,6 +3825,9 @@ packages: resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} engines: {node: '>=10'} + stubborn-fs@1.2.5: + resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} + sumchecker@3.0.1: resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} engines: {node: '>= 8.0'} @@ -3869,6 +3848,10 @@ packages: resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==} engines: {node: '>=0.10.0'} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} @@ -3955,9 +3938,9 @@ packages: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} - type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} + type-fest@5.1.0: + resolution: {integrity: sha512-wQ531tuWvB6oK+pchHIu5lHe5f5wpSCqB8Kf4dWQRbOYc9HTge7JL0G4Qd44bh6QuJCccIzL3bugb8GI0MwHrg==} + engines: {node: '>=20'} type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} @@ -4202,6 +4185,9 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + when-exit@2.1.4: + resolution: {integrity: sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==} + which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} @@ -4313,7 +4299,7 @@ snapshots: jss: 10.10.0 jss-preset-default: 10.10.0 - '@applemusic-like-lyrics/lyric@0.2.4': {} + '@applemusic-like-lyrics/lyric@0.3.0': {} '@applemusic-like-lyrics/vue@0.1.5(@pixi/app@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3)))(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))(@pixi/filter-blur@7.4.3(@pixi/core@7.4.3))(@pixi/filter-bulge-pinch@5.1.1(@pixi/core@7.4.3))(@pixi/filter-color-matrix@7.4.3(@pixi/core@7.4.3))(@pixi/sprite@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3)))(jss-preset-default@10.10.0)(jss@10.10.0)(vue@3.5.22(typescript@5.9.3))': dependencies: @@ -4943,12 +4929,12 @@ snapshots: '@material/material-color-utilities@0.3.0': {} - '@neteasecloudmusicapienhanced/api@4.29.11': + '@neteasecloudmusicapienhanced/api@4.29.12': dependencies: '@unblockneteasemusic/server': 0.28.0 axios: 1.12.2 crypto-js: 4.2.0 - dotenv: 16.6.1 + dotenv: 17.2.3 express: 5.1.0 express-fileupload: 1.5.2 md5: 2.3.0 @@ -5542,10 +5528,6 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ajv-formats@2.1.1(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 - ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -5663,7 +5645,10 @@ snapshots: atomic-sleep@1.0.0: {} - atomically@1.7.0: {} + atomically@2.0.3: + dependencies: + stubborn-fs: 1.2.5 + when-exit: 2.1.4 avvio@9.1.0: dependencies: @@ -5957,18 +5942,17 @@ snapshots: concat-map@0.0.1: {} - conf@10.2.0: + conf@15.0.2: dependencies: ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - atomically: 1.7.0 - debounce-fn: 4.0.0 - dot-prop: 6.0.1 - env-paths: 2.2.1 - json-schema-typed: 7.0.3 - onetime: 5.1.2 - pkg-up: 3.1.0 + ajv-formats: 3.0.1(ajv@8.17.1) + atomically: 2.0.3 + debounce-fn: 6.0.0 + dot-prop: 10.1.0 + env-paths: 3.0.0 + json-schema-typed: 8.0.1 semver: 7.7.3 + uint8array-extras: 1.5.0 confbox@0.1.8: {} @@ -6058,9 +6042,9 @@ snapshots: dayjs@1.11.18: {} - debounce-fn@4.0.0: + debounce-fn@6.0.0: dependencies: - mimic-fn: 3.1.0 + mimic-function: 5.0.1 debug@4.4.3: dependencies: @@ -6154,9 +6138,9 @@ snapshots: verror: 1.10.1 optional: true - dot-prop@6.0.1: + dot-prop@10.1.0: dependencies: - is-obj: 2.0.0 + type-fest: 5.1.0 dotenv-expand@11.0.7: dependencies: @@ -6164,6 +6148,8 @@ snapshots: dotenv@16.6.1: {} + dotenv@17.2.3: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6235,10 +6221,10 @@ snapshots: transitivePeerDependencies: - supports-color - electron-store@8.2.0: + electron-store@11.0.2: dependencies: - conf: 10.2.0 - type-fest: 2.19.0 + conf: 15.0.2 + type-fest: 5.1.0 electron-to-chromium@1.5.235: {} @@ -6306,6 +6292,8 @@ snapshots: env-paths@2.2.1: {} + env-paths@3.0.0: {} + err-code@2.0.3: {} es-define-property@1.0.1: {} @@ -6652,10 +6640,6 @@ snapshots: fast-querystring: 1.1.2 safe-regex2: 5.0.0 - find-up@3.0.0: - dependencies: - locate-path: 3.0.0 - find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -7016,8 +7000,6 @@ snapshots: is-number@7.0.0: {} - is-obj@2.0.0: {} - is-plain-obj@1.1.0: {} is-promise@4.0.0: {} @@ -7074,7 +7056,7 @@ snapshots: json-schema-traverse@1.0.0: {} - json-schema-typed@7.0.3: {} + json-schema-typed@8.0.1: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -7224,11 +7206,6 @@ snapshots: dependencies: lie: 3.1.1 - locate-path@3.0.0: - dependencies: - p-locate: 3.0.0 - path-exists: 3.0.0 - locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -7338,7 +7315,7 @@ snapshots: mimic-fn@2.1.0: {} - mimic-fn@3.1.0: {} + mimic-function@5.0.1: {} mimic-response@1.0.1: {} @@ -7592,10 +7569,6 @@ snapshots: dependencies: yocto-queue: 0.1.0 - p-locate@3.0.0: - dependencies: - p-limit: 2.3.0 - p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -7638,8 +7611,6 @@ snapshots: path-browserify@1.0.1: {} - path-exists@3.0.0: {} - path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -7756,10 +7727,6 @@ snapshots: exsolve: 1.0.7 pathe: 2.0.3 - pkg-up@3.1.0: - dependencies: - find-up: 3.0.0 - plist@3.1.0: dependencies: '@xmldom/xmldom': 0.8.11 @@ -8258,6 +8225,8 @@ snapshots: '@tokenizer/token': 0.3.0 peek-readable: 4.1.0 + stubborn-fs@1.2.5: {} + sumchecker@3.0.1: dependencies: debug: 4.4.3 @@ -8278,6 +8247,8 @@ snapshots: symbol-observable@1.2.0: {} + tagged-tag@1.0.0: {} + tar@6.2.1: dependencies: chownr: 2.0.0 @@ -8367,7 +8338,9 @@ snapshots: type-fest@0.13.1: optional: true - type-fest@2.19.0: {} + type-fest@5.1.0: + dependencies: + tagged-tag: 1.0.0 type-is@2.0.1: dependencies: @@ -8594,6 +8567,8 @@ snapshots: webpack-virtual-modules@0.6.2: {} + when-exit@2.1.4: {} + which-module@2.0.1: {} which@2.0.2: diff --git a/src/components/Layout/Nav.vue b/src/components/Layout/Nav.vue index 4f356b2..c002a4f 100644 --- a/src/components/Layout/Nav.vue +++ b/src/components/Layout/Nav.vue @@ -110,10 +110,8 @@ const min = () => window.electron.ipcRenderer.send("win-min"); // 最大化或还原 const maxOrRes = () => { if (window.electron.ipcRenderer.sendSync("win-state")) { - isMax.value = false; window.electron.ipcRenderer.send("win-restore"); } else { - isMax.value = true; window.electron.ipcRenderer.send("win-max"); } }; @@ -198,9 +196,15 @@ const setSelect = (key: string) => { }; onMounted(() => { - // 获取窗口状态 + // 获取窗口状态并监听主进程的状态变更 if (isElectron) { isMax.value = window.electron.ipcRenderer.sendSync("win-state"); + window.electron.ipcRenderer.on( + "win-state-change", + (_event, value: boolean) => { + isMax.value = value; + }, + ); } }); diff --git a/src/utils/player.ts b/src/utils/player.ts index 54c4939..01b973c 100644 --- a/src/utils/player.ts +++ b/src/utils/player.ts @@ -290,7 +290,9 @@ class Player { if (currentSessionId !== this.playSessionId) return; if (!isElectron) window.document.title = "SPlayer"; // 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); }); // 结束 diff --git a/tsconfig.node.json b/tsconfig.node.json index a544f24..8e78fba 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -4,8 +4,8 @@ "electron.vite.config.*", "electron/**/*", "electron/main/index.ts", - "electron/main/logger.ts", - "electron/main/store.ts", + "electron/main/logger/index.ts", + "electron/main/store/index.ts", "electron/main/utils.ts", "electron/main/index.d.ts", "electron/preload/index.d.ts", diff --git a/web/loading.html b/web/loading.html deleted file mode 100644 index 1cd7aa2..0000000 --- a/web/loading.html +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - SPlayer - - - - -
- -
- -
-
- SPlayer · Copyright © IMSYY -
- - - - diff --git a/web/loading/index.html b/web/loading/index.html new file mode 100644 index 0000000..287b3eb --- /dev/null +++ b/web/loading/index.html @@ -0,0 +1,54 @@ + + + + + + + + SPlayer + + + + +
+ +
+ +
+
+ SPlayer · Copyright © IMSYY +
+ + + + + \ No newline at end of file diff --git a/web/loading/style.css b/web/loading/style.css new file mode 100644 index 0000000..98b406d --- /dev/null +++ b/web/loading/style.css @@ -0,0 +1,115 @@ +:root { + --bg-color: #fff; + --font-coloe: #000; + --primary: #f55e55; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg-color: #161616; + --font-coloe: #fff; + } +} + +* { + margin: 0; + padding: 0; + user-select: none; + box-sizing: border-box; + -webkit-user-drag: none; +} + +body { + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: var(--bg-color); +} + +main { + display: flex; + flex-direction: column; + align-items: center; +} + +footer { + position: absolute; + bottom: 20px; + font-size: 14px; + color: var(--primary); + opacity: 0.6; +} + +.logo { + width: 120px; + height: 120px; + display: flex; + align-items: center; + justify-content: center; +} + +.loader, +.loader:before, +.loader:after { + border-radius: 50%; + width: 2.5em; + height: 2.5em; + animation-fill-mode: both; + animation: bblFadInOut 1.8s infinite ease-in-out; +} + +.loader { + color: var(--primary); + font-size: 6px; + position: relative; + text-indent: -9999em; + transform: translateZ(0); + animation-delay: -0.16s; + margin: 40px 0; +} + +.loader:before, +.loader:after { + content: ""; + position: absolute; + top: 0; +} + +.loader:before { + left: -3.5em; + animation-delay: -0.32s; +} + +.loader:after { + left: 3.5em; +} + +#startApp { + opacity: 0; + margin-top: 10px; + outline: none; + border: 2px solid var(--primary); + height: 36px; + width: 90px; + font-size: 14px; + border-radius: 12px; + color: var(--primary); + font-weight: bold; + background-color: transparent; + cursor: pointer; + transition: opacity 0.3s; +} + +@keyframes bblFadInOut { + 0%, + 80%, + 100% { + box-shadow: 0 2.5em 0 -1.3em; + } + + 40% { + box-shadow: 0 2.5em 0 0; + } +}