From 374a7a837dfb069d7c4b535d388ba5bdbe1ef4a7 Mon Sep 17 00:00:00 2001 From: alger Date: Mon, 31 Mar 2025 23:05:19 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20mac=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=9F=B3=E4=B9=90=E6=8E=A7=E5=88=B6=E5=9B=BE=E6=A0=87=20,=20?= =?UTF-8?q?=E6=89=98=E7=9B=98=E8=8F=9C=E5=8D=95=E9=A1=B9=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=92=AD=E6=94=BE=E7=8A=B6=E6=80=81=E5=92=8C=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E6=AD=8C=E6=9B=B2=E4=BF=A1=E6=81=AF=E7=9A=84=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat #105 --- resources/icons/next.png | Bin 0 -> 269 bytes resources/icons/pause.png | Bin 0 -> 141 bytes resources/icons/play.png | Bin 0 -> 251 bytes resources/icons/prev.png | Bin 0 -> 289 bytes src/i18n/lang/en-US/common.ts | 7 +- src/i18n/lang/zh-CN/common.ts | 7 +- src/main/index.ts | 14 +- src/main/modules/tray.ts | 428 ++++++++++++++++++++++++--- src/preload/index.d.ts | 1 + src/preload/index.ts | 1 + src/renderer/App.vue | 2 + src/renderer/hooks/MusicHook.ts | 2 + src/renderer/store/modules/player.ts | 7 +- 13 files changed, 416 insertions(+), 53 deletions(-) create mode 100644 resources/icons/next.png create mode 100644 resources/icons/pause.png create mode 100644 resources/icons/play.png create mode 100644 resources/icons/prev.png diff --git a/resources/icons/next.png b/resources/icons/next.png new file mode 100644 index 0000000000000000000000000000000000000000..ffa74da057bddcd75ad1b19d4c24c63e9384b66b GIT binary patch literal 269 zcmV+o0rLKdP)Px#$4Nv%R7gwR)jNs;K@^4IuX9(U`%%ONhy$}36E$!Lf{B4^5LYn4zzCvxKzAqut10J-iQ z-I8kF1_aO^Ul!5 zkdv)xIJ96J;}nOM%qq4w!|P+3!t6pjg77B7u2(pt^&h*A(2Yx}@0S@@e?-<5xu7~3 TfZJ{h00000NkvXXu0mjfO%iS- literal 0 HcmV?d00001 diff --git a/resources/icons/pause.png b/resources/icons/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..a8a702fe67fc136807d91888eef8efa8f17e6872 GIT binary patch literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|+&x_!Lp;2b zQx+H|EZO*zXR(+7PhySZ7x`7Q0}LL8nrAU;h`2X*B&5mkF1o02C^m-o@1~A~K%X8a mCs#wmvPx#wMj%lR7gwh)WHgYK@^7J$0b_Wl3aku-B_^T4n)bqy(mSIENp4YFDcCY%!*mh zyzk66=bVbntCE@THv}zAuqhK~Ohjkw(Z(oM5-TXUGc;3PlPU z`jzMtLEs+IL;soNQ$fL9qK?(0=v#uoJ)wr_z2w`1D&{!^*&@pN$=S*@G+GD49F3N3 zRP3(9ZX9##)G7*b1n@Q$9`?udK^gAt%wO;}z6 literal 0 HcmV?d00001 diff --git a/resources/icons/prev.png b/resources/icons/prev.png new file mode 100644 index 0000000000000000000000000000000000000000..fdd84e05041115327a6de015cd3322d2d1080e44 GIT binary patch literal 289 zcmV++0p9+JP)Px#+et)0R7gwh)WJ#uQ544UpSu=D*eZmAUO`{LMH`{5%Me^d&k|9BTT$qd@>N_2 zE^LGmZZkACv=EXsDnif`=D7arnSSMQJ&ZWfV_ZMx n;rByc7sU~^t0n({S_{7c2gD*7hb|$r00000NkvXXu0mjf@~L&D literal 0 HcmV?d00001 diff --git a/src/i18n/lang/en-US/common.ts b/src/i18n/lang/en-US/common.ts index b2cb933..e6bb675 100644 --- a/src/i18n/lang/en-US/common.ts +++ b/src/i18n/lang/en-US/common.ts @@ -41,7 +41,12 @@ export default { songCount: '{count} songs', tray: { show: 'Show', - quit: 'Quit' + quit: 'Quit', + playPause: 'Play/Pause', + prev: 'Previous', + next: 'Next', + pause: 'Pause', + play: 'Play' }, language: 'Language' }; diff --git a/src/i18n/lang/zh-CN/common.ts b/src/i18n/lang/zh-CN/common.ts index 5f61650..2d57b40 100644 --- a/src/i18n/lang/zh-CN/common.ts +++ b/src/i18n/lang/zh-CN/common.ts @@ -41,6 +41,11 @@ export default { language: '语言', tray: { show: '显示', - quit: '退出' + quit: '退出', + playPause: '播放/暂停', + prev: '上一首', + next: '下一首', + pause: '暂停', + play: '播放' } }; diff --git a/src/main/index.ts b/src/main/index.ts index 0a4d111..dfb5584 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -9,7 +9,7 @@ import { initializeConfig } from './modules/config'; import { initializeFileManager } from './modules/fileManager'; import { initializeFonts } from './modules/fonts'; import { initializeShortcuts, registerShortcuts } from './modules/shortcuts'; -import { initializeTray, updateTrayMenu } from './modules/tray'; +import { initializeTray, updateCurrentSong, updatePlayState, updateTrayMenu } from './modules/tray'; import { setupUpdateHandlers } from './modules/update'; import { createMainWindow, initializeWindowManager } from './modules/window'; import { startMusicApi } from './server'; @@ -109,11 +109,21 @@ if (!isSingleInstance) { // 更新主进程的语言设置 i18n.global.locale = locale; // 更新托盘菜单 - updateTrayMenu(); + updateTrayMenu(mainWindow); // 通知所有窗口语言已更改 mainWindow?.webContents.send('language-changed', locale); }); + // 监听播放状态变化 + ipcMain.on('update-play-state', (_, playing: boolean) => { + updatePlayState(playing); + }); + + // 监听当前歌曲变化 + ipcMain.on('update-current-song', (_, song: any) => { + updateCurrentSong(song); + }); + // 所有窗口关闭时的处理 app.on('window-all-closed', () => { if (process.platform !== 'darwin') { diff --git a/src/main/modules/tray.ts b/src/main/modules/tray.ts index 0b78789..593f21d 100644 --- a/src/main/modules/tray.ts +++ b/src/main/modules/tray.ts @@ -1,82 +1,418 @@ -import { app, BrowserWindow, Menu, nativeImage, Tray } from 'electron'; +import { + app, + BrowserWindow, + Menu, + MenuItem, + MenuItemConstructorOptions, + nativeImage, + Tray +} from 'electron'; import { join } from 'path'; import type { Language } from '../../i18n/main'; import i18n from '../../i18n/main'; +// 歌曲信息接口定义 +interface SongInfo { + name: string; + song: { + artists: Array<{ name: string; [key: string]: any }>; + [key: string]: any; + }; + [key: string]: any; +} + let tray: Tray | null = null; +// 为macOS状态栏添加控制图标 +let playPauseTray: Tray | null = null; +let prevTray: Tray | null = null; +let nextTray: Tray | null = null; +let songTitleTray: Tray | null = null; + +let isPlaying = false; +let currentSong: SongInfo | null = null; const LANGUAGES: { label: string; value: Language }[] = [ { label: '简体中文', value: 'zh-CN' }, { label: 'English', value: 'en-US' } ]; +// 更新播放状态 +export function updatePlayState(playing: boolean) { + isPlaying = playing; + if (tray) { + updateTrayMenu(BrowserWindow.getAllWindows()[0]); + } + // 更新播放/暂停图标 + updateStatusBarTray(); +} + +// 获取艺术家名称字符串 +function getArtistString(song: SongInfo | null): string { + if (!song || !song.song || !song.song.artists) return ''; + return song.song.artists.map((item) => item.name).join(' / '); +} + +// 获取歌曲完整标题(歌曲名 - 艺术家) +function getSongTitle(song: SongInfo | null): string { + if (!song) return '未播放'; + const artistStr = getArtistString(song); + return artistStr ? `${song.name} - ${artistStr}` : song.name; +} + +// 更新当前播放的音乐信息 +export function updateCurrentSong(song: SongInfo | null) { + currentSong = song; + if (tray) { + updateTrayMenu(BrowserWindow.getAllWindows()[0]); + } + // 更新状态栏歌曲信息 + updateStatusBarTray(); +} + +// 确保 macOS 状态栏图标能正确显示 +function getProperIconSize() { + // macOS 状态栏通常高度为22像素 + const height = 18; + const width = 18; + return { width, height }; +} + +// 更新macOS状态栏图标 +function updateStatusBarTray() { + if (process.platform !== 'darwin') return; + + const iconSize = getProperIconSize(); + + // 更新歌曲标题显示 + if (songTitleTray) { + if (currentSong) { + // 限制歌曲名显示长度,添加作者名 + const songName = currentSong.name.slice(0, 10); + let title = songName; + const artistStr = getArtistString(currentSong); + // 如果有艺术家名称,添加到标题中 + if (artistStr) { + title = `${songName} - ${artistStr.slice(0, 6)}${artistStr.length > 6 ? '..' : ''}`; + } + + // 设置标题和提示 + songTitleTray.setTitle(title, { + fontType: 'monospacedDigit' // 使用等宽字体以确保更好的可读性 + }); + + // 完整信息放在tooltip中 + const fullTitle = getSongTitle(currentSong); + songTitleTray.setToolTip(fullTitle); + console.log('更新状态栏歌曲显示:', title, '完整信息:', fullTitle); + } else { + songTitleTray.setTitle('未播放', { + fontType: 'monospacedDigit' + }); + songTitleTray.setToolTip('未播放'); + console.log('更新状态栏歌曲显示: 未播放'); + } + } + + // 更新播放/暂停图标 + if (playPauseTray) { + // 使用PNG图标替代文本 + const iconPath = join( + app.getAppPath(), + 'resources/icons', + isPlaying ? 'pause.png' : 'play.png' + ); + const icon = nativeImage.createFromPath(iconPath).resize(iconSize); + icon.setTemplateImage(true); // 设置为模板图片,适合macOS深色/浅色模式 + playPauseTray.setImage(icon); + playPauseTray.setToolTip( + isPlaying ? i18n.global.t('common.tray.pause') : i18n.global.t('common.tray.play') + ); + } +} + // 导出更新菜单的函数 -export function updateTrayMenu() { +export function updateTrayMenu(mainWindow: BrowserWindow) { if (!tray) return; - // 创建一个上下文菜单 - const contextMenu = Menu.buildFromTemplate([ - { - label: i18n.global.t('common.tray.show'), - click: () => { - BrowserWindow.getAllWindows()[0]?.show(); - } - }, - { type: 'separator' }, - { - label: i18n.global.t('common.language'), - submenu: LANGUAGES.map(({ label, value }) => ({ + // 如果是macOS,设置TouchBar + if (process.platform === 'darwin') { + // macOS 上使用直接的控制按钮 + const menu = new Menu(); + + // 当前播放的音乐信息 + if (currentSong) { + menu.append( + new MenuItem({ + label: getSongTitle(currentSong), + enabled: false, + type: 'normal' + }) + ); + menu.append(new MenuItem({ type: 'separator' })); + } + + // 上一首、播放/暂停、下一首的菜单项 + // 在macOS上临时使用文本菜单项替代图标,确保基本功能正常 + menu.append( + new MenuItem({ + label: i18n.global.t('common.tray.prev'), + type: 'normal', + click: () => { + mainWindow.webContents.send('global-shortcut', 'prevPlay'); + } + }) + ); + + menu.append( + new MenuItem({ + label: i18n.global.t(isPlaying ? 'common.tray.pause' : 'common.tray.play'), + type: 'normal', + click: () => { + mainWindow.webContents.send('global-shortcut', 'togglePlay'); + } + }) + ); + + menu.append( + new MenuItem({ + label: i18n.global.t('common.tray.next'), + type: 'normal', + click: () => { + mainWindow.webContents.send('global-shortcut', 'nextPlay'); + } + }) + ); + + // 分隔符 + menu.append(new MenuItem({ type: 'separator' })); + + // 显示主窗口 + menu.append( + new MenuItem({ + label: i18n.global.t('common.tray.show'), + type: 'normal', + click: () => { + mainWindow.show(); + } + }) + ); + + // 语言切换子菜单 + const languageSubmenu = Menu.buildFromTemplate( + LANGUAGES.map(({ label, value }) => ({ label, type: 'radio', checked: i18n.global.locale === value, click: () => { - // 更新主进程的语言设置 i18n.global.locale = value; - // 更新托盘菜单 - updateTrayMenu(); - // 直接通知主窗口 - const windows = BrowserWindow.getAllWindows(); - for (const win of windows) { - win.webContents.send('language-changed', value); - console.log('向窗口发送语言变更事件:', value); - } + updateTrayMenu(mainWindow); + mainWindow.webContents.send('language-changed', value); } })) - }, - { type: 'separator' }, - { - label: i18n.global.t('common.tray.quit'), - click: () => { - app.quit(); - } - } - ]); + ); - // 设置系统托盘图标的上下文菜单 - tray.setContextMenu(contextMenu); + menu.append( + new MenuItem({ + label: i18n.global.t('common.language'), + type: 'submenu', + submenu: languageSubmenu + }) + ); + + // 退出按钮 + menu.append( + new MenuItem({ + label: i18n.global.t('common.tray.quit'), + type: 'normal', + click: () => { + app.quit(); + } + }) + ); + + tray.setContextMenu(menu); + } else { + // Windows 和 Linux 使用原来的菜单样式 + const menuTemplate: MenuItemConstructorOptions[] = [ + // 当前播放的音乐信息 + ...((currentSong + ? [ + { + label: getSongTitle(currentSong), + enabled: false, + type: 'normal' + }, + { type: 'separator' } + ] + : []) as MenuItemConstructorOptions[]), + { + label: i18n.global.t('common.tray.show'), + type: 'normal', + click: () => { + mainWindow.show(); + } + }, + { type: 'separator' }, + { + label: i18n.global.t('common.tray.prev'), + type: 'normal', + click: () => { + mainWindow.webContents.send('global-shortcut', 'prevPlay'); + } + }, + { + label: i18n.global.t(isPlaying ? 'common.tray.pause' : 'common.tray.play'), + type: 'normal', + click: () => { + mainWindow.webContents.send('global-shortcut', 'togglePlay'); + } + }, + { + label: i18n.global.t('common.tray.next'), + type: 'normal', + click: () => { + mainWindow.webContents.send('global-shortcut', 'nextPlay'); + } + }, + { type: 'separator' }, + { + label: i18n.global.t('common.language'), + type: 'submenu', + submenu: LANGUAGES.map(({ label, value }) => ({ + label, + type: 'radio', + checked: i18n.global.locale === value, + click: () => { + i18n.global.locale = value; + updateTrayMenu(mainWindow); + mainWindow.webContents.send('language-changed', value); + } + })) + }, + { type: 'separator' }, + { + label: i18n.global.t('common.tray.quit'), + type: 'normal', + click: () => { + app.quit(); + } + } + ]; + + const contextMenu = Menu.buildFromTemplate(menuTemplate); + tray.setContextMenu(contextMenu); + } +} + +// 初始化状态栏Tray +function initializeStatusBarTray(mainWindow: BrowserWindow) { + if (process.platform !== 'darwin') return; + + const iconSize = getProperIconSize(); + + // 创建下一首按钮(调整顺序,先创建下一首按钮) + const nextIcon = nativeImage + .createFromPath(join(app.getAppPath(), 'resources/icons', 'next.png')) + .resize(iconSize); + nextIcon.setTemplateImage(true); // 设置为模板图片,适合macOS深色/浅色模式 + nextTray = new Tray(nextIcon); + nextTray.setToolTip(i18n.global.t('common.tray.next')); + nextTray.on('click', () => { + mainWindow.webContents.send('global-shortcut', 'nextPlay'); + }); + + // 创建播放/暂停按钮 + const playPauseIcon = nativeImage + .createFromPath(join(app.getAppPath(), 'resources/icons', isPlaying ? 'pause.png' : 'play.png')) + .resize(iconSize); + playPauseIcon.setTemplateImage(true); // 设置为模板图片,适合macOS深色/浅色模式 + playPauseTray = new Tray(playPauseIcon); + playPauseTray.setToolTip( + isPlaying ? i18n.global.t('common.tray.pause') : i18n.global.t('common.tray.play') + ); + playPauseTray.on('click', () => { + mainWindow.webContents.send('global-shortcut', 'togglePlay'); + }); + + // 创建上一首按钮(调整顺序,最后创建上一首按钮) + const prevIcon = nativeImage + .createFromPath(join(app.getAppPath(), 'resources/icons', 'prev.png')) + .resize(iconSize); + prevIcon.setTemplateImage(true); // 设置为模板图片,适合macOS深色/浅色模式 + prevTray = new Tray(prevIcon); + prevTray.setToolTip(i18n.global.t('common.tray.prev')); + prevTray.on('click', () => { + mainWindow.webContents.send('global-shortcut', 'prevPlay'); + }); + + // 创建歌曲信息显示 - 需要使用特殊处理 + const titleIcon = nativeImage + .createFromPath(join(app.getAppPath(), 'resources/icons', 'note.png')) + .resize({ width: 16, height: 16 }); + titleIcon.setTemplateImage(true); + songTitleTray = new Tray(titleIcon); + + // 初始化显示文本 + const initialText = getSongTitle(currentSong); + + // 在macOS上,特别设置title来显示文本,确保它能正确显示 + songTitleTray.setTitle(initialText, { + fontType: 'monospacedDigit' // 使用等宽字体以确保更好的可读性 + }); + + songTitleTray.setToolTip(initialText); + songTitleTray.on('click', () => { + mainWindow.show(); + }); + + // 强制更新一次所有图标 + updateStatusBarTray(); + + // 打印调试信息 + console.log('状态栏初始化完成,歌曲显示标题:', initialText); } /** * 初始化系统托盘 */ export function initializeTray(iconPath: string, mainWindow: BrowserWindow) { + // 根据平台选择合适的图标 + const iconSize = process.platform === 'darwin' ? 18 : 16; + const iconFile = process.platform === 'darwin' ? 'icon_16x16.png' : 'icon_16x16.png'; + const trayIcon = nativeImage - .createFromPath(join(iconPath, 'icon_16x16.png')) - .resize({ width: 16, height: 16 }); + .createFromPath(join(iconPath, iconFile)) + .resize({ width: iconSize, height: iconSize }); + tray = new Tray(trayIcon); - // 初始化菜单 - updateTrayMenu(); + // 设置托盘图标的提示文字 + tray.setToolTip('Alger Music Player'); - // 当系统托盘图标被点击时,切换窗口的显示/隐藏 - tray.on('click', () => { - if (mainWindow.isVisible()) { - mainWindow.hide(); - } else { - mainWindow.show(); - } - }); + // 初始化菜单 + updateTrayMenu(mainWindow); + + // 初始化状态栏控制按钮 (macOS) + initializeStatusBarTray(mainWindow); + + // 在 macOS 上,点击图标时显示菜单 + if (process.platform === 'darwin') { + tray.on('click', () => { + if (tray) { + tray.popUpContextMenu(); + } + }); + } else { + // 在其他平台上,点击图标时切换窗口显示状态 + tray.on('click', () => { + if (mainWindow.isVisible()) { + mainWindow.hide(); + } else { + mainWindow.show(); + } + }); + } return tray; } diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 93d0300..ec889bc 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -20,6 +20,7 @@ declare global { removeDownloadListeners: () => void; onLanguageChanged: (callback: (locale: string) => void) => void; invoke: (channel: string, ...args: any[]) => Promise; + sendSong: (data: any) => void; }; $message: any; } diff --git a/src/preload/index.ts b/src/preload/index.ts index be1f737..206eb55 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -11,6 +11,7 @@ const api = { restart: () => ipcRenderer.send('restart'), openLyric: () => ipcRenderer.send('open-lyric'), sendLyric: (data) => ipcRenderer.send('send-lyric', data), + sendSong: (data) => ipcRenderer.send('update-current-song', data), unblockMusic: (id) => ipcRenderer.invoke('unblock-music', id), // 歌词窗口关闭事件 onLyricWindowClosed: (callback: () => void) => { diff --git a/src/renderer/App.vue b/src/renderer/App.vue index 02e9970..b8f272c 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -11,6 +11,7 @@ diff --git a/src/renderer/hooks/MusicHook.ts b/src/renderer/hooks/MusicHook.ts index 90b4b96..460e66b 100644 --- a/src/renderer/hooks/MusicHook.ts +++ b/src/renderer/hooks/MusicHook.ts @@ -1,3 +1,4 @@ +import { cloneDeep } from 'lodash'; import { createDiscreteApi } from 'naive-ui'; import { computed, nextTick, onUnmounted, ref, watch } from 'vue'; @@ -394,6 +395,7 @@ const setupAudioListeners = () => { // 监听播放 audioService.on('play', () => { playerStore.setPlayMusic(true); + window.api.sendSong(cloneDeep(playerStore.playMusic)); clearInterval(); interval = window.setInterval(() => { try { diff --git a/src/renderer/store/modules/player.ts b/src/renderer/store/modules/player.ts index a72f1f2..31b0385 100644 --- a/src/renderer/store/modules/player.ts +++ b/src/renderer/store/modules/player.ts @@ -353,14 +353,15 @@ export const usePlayerStore = defineStore('player', () => { const setIsPlay = (value: boolean) => { isPlay.value = value; + play.value = value; localStorage.setItem('isPlaying', value.toString()); + // 通知主进程播放状态变化 + window.electron?.ipcRenderer.send('update-play-state', value); }; const setPlayMusic = async (value: boolean | SongResult) => { if (typeof value === 'boolean') { - play.value = value; - isPlay.value = value; - localStorage.setItem('isPlaying', value.toString()); + setIsPlay(value); } else { await handlePlayMusic(value); play.value = true;