From d716bb8cdeb2e2c9ec93293b862082a87d456210 Mon Sep 17 00:00:00 2001 From: memorydream <34763046+memorydream@users.noreply.github.com> Date: Mon, 17 Jan 2022 23:23:21 +0800 Subject: [PATCH] feat: refactor tray (#1227) * feat: support set tray icon tooltip info * fix: name * refactor tray impl and add tray playing state change * fix: linux impl * add pause icon * add tray like state * fix * fix: linux impl * better pause icon --- public/img/icons/pause.png | Bin 0 -> 953 bytes public/img/icons/unlike.png | Bin 0 -> 932 bytes src/background.js | 28 +++--- src/electron/ipcMain.js | 139 +++++++++++++++------------- src/electron/tray.js | 180 +++++++++++++++++++++++++++--------- src/utils/Player.js | 39 ++++++-- src/utils/platform.js | 6 ++ 7 files changed, 266 insertions(+), 126 deletions(-) create mode 100644 public/img/icons/pause.png create mode 100644 public/img/icons/unlike.png create mode 100644 src/utils/platform.js diff --git a/public/img/icons/pause.png b/public/img/icons/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..509d7382b31bb035ea816d305247e5ad28b160dc GIT binary patch literal 953 zcmV;q14jIbP)_;n*qKa;#p>Khj^YiwdGiX_lSpB zQO*;e5Jx#3koZ>UipOt)3m$(7=GpWD;1@lmLJKP`%!=F~o+OSKdYLuOBW@B>&6N!0 zQQBj$kwy{;NKjD5CUiK6Q8sC2?S1jOZy6=>F#1}kfRm$uB6*{vmZ_^sStDJgrK2L| z{(qjfHa+Ea4<&%^SJ(X<2ZH-Rt?IhJr>+)=)sGuwjVDT`pIFU?P#Qw%hHCTrM|FXH1Mahr@A#Wujazd$<<@y!uT!!QQq2?2}6a*KE_9*>U@ zwApN8-oOp|Fcyo&MzNo@TCF$4g+MqQt|5NZb$yi}j{?F%GMT)mGnU6|G#YE5JcL4_ zD{_Ug!2zkfN1>idrLLmUsOUT~8mIRj b{p0xtjMff*aXD5p00000NkvXXu0mjfGd-`s literal 0 HcmV?d00001 diff --git a/public/img/icons/unlike.png b/public/img/icons/unlike.png new file mode 100644 index 0000000000000000000000000000000000000000..a0afa24600ff68f90eeacc9a4d23bd15c9ced3d6 GIT binary patch literal 932 zcmV;V16%xwP)IpX|ADT8tAZdXf{2qpz^_G0Jhv&dNPEJ~d3f)65BHu6_|rNw?4AT*m=4dRlG18^ zP3n7x4}%CJh&h?r)>3vchpzGSNYxY%anY-LUB?(Gs5S$9{lqiO;tugVaeB+K1n&_K zvyxmOJ|T{AIw0|_&=rs01Q$L263la%1;8(QO2sx-+L)ENK|DzuHS`K=o=4myq?#*P z%A>T#U?YPRl8~UFf=%df5T|U?%sTrL4c{`#G#*FBU3dS6}la~$Z~2kJG~{XKQv`WX;-0j|xKzf5zQ{U*KE(xS&eWCyss zX=%y4r)R_ zgd~TeCZ|*kO9ycf1T_PpAPBdhwK-^M2?|<@7N;5%B7|(QhK59Ob;!a+2_k+VNnoOO zuJt~aAyE%5_rH73Iq$stAIJT3@xKLcc%E;I--&@hK(5tlQ?e}IrBbP9>W4z1 z5k*m^jYi{rI-OQ}IjRG%*K4ZR>xXa?2B40R1VK0;cZfCY8wfp4J%i9%CX;z02VLvQ zWReH@ga8JEVWLnd7$r&awA<|<$N{ZZJ5M#8PB#hCFdQBe2l$WWcDrZocKeIZ=aWBA zq*DdI-#?C!%_lQQL`f|Bk(P%U)xm@m|pTcn-k7uUQXe|aUV8UaIGWMUn%nW{5Hd| zOf%>cU`;rf$Iv#h$MwanY&Lt%dMpnVV4y*n1P1kg)q4lr^zPBY_m`^x0000 { } }; -const isWindows = process.platform === 'win32'; -const isMac = process.platform === 'darwin'; -const isLinux = process.platform === 'linux'; -const isDevelopment = process.env.NODE_ENV === 'development'; - class Background { constructor() { this.window = null; - this.tray = null; + this.ypmTrayImpl = null; this.store = new Store({ windowWidth: { width: { type: 'number', default: 1440 }, @@ -324,8 +327,14 @@ class Background { }); this.handleWindowEvents(); + // create tray + if (isCreateTray) { + this.trayEventEmitter = new EventEmitter(); + this.ypmTrayImpl = createTray(this.window, this.trayEventEmitter); + } + // init ipcMain - initIpcMain(this.window, this.store); + initIpcMain(this.window, this.store, this.trayEventEmitter); // set proxy const proxyRules = this.store.get('proxy'); @@ -341,11 +350,6 @@ class Background { // create menu createMenu(this.window, this.store); - // create tray - if (isWindows || isLinux || isDevelopment) { - this.tray = createTray(this.window); - } - // create dock menu for macOS const createdDockMenu = createDockMenu(this.window); if (createDockMenu && app.dock) app.dock.setMenu(createdDockMenu); diff --git a/src/electron/ipcMain.js b/src/electron/ipcMain.js index 64e5168..9d0d42e 100644 --- a/src/electron/ipcMain.js +++ b/src/electron/ipcMain.js @@ -4,12 +4,73 @@ import { registerGlobalShortcut } from '@/electron/globalShortcut'; import cloneDeep from 'lodash/cloneDeep'; import shortcuts from '@/utils/shortcuts'; import { createMenu } from './menu'; +import { isCreateTray, isMac } from '@/utils/platform'; const clc = require('cli-color'); const log = text => { console.log(`${clc.blueBright('[ipcMain.js]')} ${text}`); }; +const exitAsk = (e, win) => { + e.preventDefault(); //阻止默认行为 + dialog + .showMessageBox({ + type: 'info', + title: 'Information', + cancelId: 2, + defaultId: 0, + message: '确定要关闭吗?', + buttons: ['最小化', '直接退出'], + }) + .then(result => { + if (result.response == 0) { + e.preventDefault(); //阻止默认行为 + win.minimize(); //调用 最小化实例方法 + } else if (result.response == 1) { + win = null; + //app.quit(); + app.exit(); //exit()直接关闭客户端,不会执行quit(); + } + }) + .catch(err => { + log(err); + }); +}; + +const exitAskWithoutMac = (e, win) => { + e.preventDefault(); //阻止默认行为 + dialog + .showMessageBox({ + type: 'info', + title: 'Information', + cancelId: 2, + defaultId: 0, + message: '确定要关闭吗?', + buttons: ['最小化到托盘', '直接退出'], + checkboxLabel: '记住我的选择', + }) + .then(result => { + if (result.checkboxChecked && result.response !== 2) { + win.webContents.send( + 'rememberCloseAppOption', + result.response === 0 ? 'minimizeToTray' : 'exit' + ); + } + + if (result.response === 0) { + e.preventDefault(); //阻止默认行为 + win.hide(); //调用 最小化实例方法 + } else if (result.response === 1) { + win = null; + //app.quit(); + app.exit(); //exit()直接关闭客户端,不会执行quit(); + } + }) + .catch(err => { + log(err); + }); +}; + const client = require('discord-rich-presence')('818936529484906596'); /** @@ -58,7 +119,7 @@ function parseSourceStringToList(sourceString) { return sourceString.split(',').map(s => s.trim()); } -export function initIpcMain(win, store) { +export function initIpcMain(win, store, trayEventEmitter) { ipcMain.handle('unblock-music', async (_, track, source) => { // 兼容 unblockneteasemusic 所使用的 api 字段 track.alias = track.alia || []; @@ -102,9 +163,9 @@ export function initIpcMain(win, store) { }); ipcMain.on('close', e => { - if (process.platform === 'darwin') { + if (isMac) { win.hide(); - exitAsk(e); + exitAsk(e, win); } else { let closeOpt = store.get('settings.closeAppOption'); if (closeOpt === 'exit') { @@ -115,7 +176,7 @@ export function initIpcMain(win, store) { e.preventDefault(); win.hide(); } else { - exitAskWithoutMac(e); + exitAskWithoutMac(e, win); } } }); @@ -214,63 +275,15 @@ export function initIpcMain(win, store) { registerGlobalShortcut(win, store); }); - const exitAsk = e => { - e.preventDefault(); //阻止默认行为 - dialog - .showMessageBox({ - type: 'info', - title: 'Information', - cancelId: 2, - defaultId: 0, - message: '确定要关闭吗?', - buttons: ['最小化', '直接退出'], - }) - .then(result => { - if (result.response == 0) { - e.preventDefault(); //阻止默认行为 - win.minimize(); //调用 最小化实例方法 - } else if (result.response == 1) { - win = null; - //app.quit(); - app.exit(); //exit()直接关闭客户端,不会执行quit(); - } - }) - .catch(err => { - log(err); - }); - }; - - const exitAskWithoutMac = e => { - e.preventDefault(); //阻止默认行为 - dialog - .showMessageBox({ - type: 'info', - title: 'Information', - cancelId: 2, - defaultId: 0, - message: '确定要关闭吗?', - buttons: ['最小化到托盘', '直接退出'], - checkboxLabel: '记住我的选择', - }) - .then(result => { - if (result.checkboxChecked && result.response !== 2) { - win.webContents.send( - 'rememberCloseAppOption', - result.response === 0 ? 'minimizeToTray' : 'exit' - ); - } - - if (result.response === 0) { - e.preventDefault(); //阻止默认行为 - win.hide(); //调用 最小化实例方法 - } else if (result.response === 1) { - win = null; - //app.quit(); - app.exit(); //exit()直接关闭客户端,不会执行quit(); - } - }) - .catch(err => { - log(err); - }); - }; + if (isCreateTray) { + ipcMain.on('updateTrayTooltip', (_, title) => { + trayEventEmitter.emit('updateTooltip', title); + }); + ipcMain.on('updateTrayPlayState', (_, isPlaying) => { + trayEventEmitter.emit('updatePlayState', isPlaying); + }); + ipcMain.on('updateTrayLikeState', (_, isLiked) => { + trayEventEmitter.emit('updateLikeState', isLiked); + }); + } } diff --git a/src/electron/tray.js b/src/electron/tray.js index b0ec291..6afa580 100644 --- a/src/electron/tray.js +++ b/src/electron/tray.js @@ -1,40 +1,30 @@ /* global __static */ import path from 'path'; import { app, nativeImage, Tray, Menu } from 'electron'; +import { isLinux } from '@/utils/platform'; -export function createTray(win) { - let icon = nativeImage - .createFromPath(path.join(__static, 'img/icons/menu@88.png')) - .resize({ - height: 20, - width: 20, - }); - - let contextMenu = Menu.buildFromTemplate([ - //setContextMenu破坏了预期的click行为 - //在linux下,鼠标左右键都会呼出contextMenu - //所以此处单独为linux添加一个 显示主面板 选项 - ...(process.platform === 'linux' - ? [ - { - label: '显示主面板', - click: () => { - win.show(); - }, - }, - { - type: 'separator', - }, - ] - : []), +function createMenuTemplate(win) { + return [ { - label: '播放/暂停', + label: '播放', icon: nativeImage.createFromPath( path.join(__static, 'img/icons/play.png') ), click: () => { win.webContents.send('play'); }, + id: 'play', + }, + { + label: '暂停', + icon: nativeImage.createFromPath( + path.join(__static, 'img/icons/pause.png') + ), + click: () => { + win.webContents.send('play'); + }, + id: 'pause', + visible: false, }, { label: '上一首', @@ -75,6 +65,19 @@ export function createTray(win) { click: () => { win.webContents.send('like'); }, + id: 'like', + }, + { + label: '取消喜欢', + icon: nativeImage.createFromPath( + path.join(__static, 'img/icons/unlike.png') + ), + accelerator: 'CmdOrCtrl+L', + click: () => { + win.webContents.send('like'); + }, + id: 'unlike', + visible: false, }, { label: '退出', @@ -86,28 +89,117 @@ export function createTray(win) { app.exit(); }, }, - ]); - let tray = new Tray(icon); - tray.setToolTip('YesPlayMusic'); + ]; +} + +// linux下托盘的实现方式比较迷惑 +// right-click无法在linux下使用 +// click在默认行为下会弹出一个contextMenu,里面的唯一选项才会调用click事件 +// setContextMenu应该是目前唯一能在linux下使用托盘菜单api +// 但是无法区分鼠标左右键 +class YPMTrayLinuxImpl { + constructor(tray, win, emitter) { + this.tray = tray; + this.win = win; + this.emitter = emitter; + this.template = undefined; + this.initTemplate(); + this.contextMenu = Menu.buildFromTemplate(this.template); + + this.tray.setContextMenu(this.contextMenu); + this.handleEvents(); + } - if (process.platform === 'linux') { - //linux下托盘的实现方式比较迷惑 - //right-click无法在linux下使用 - //click在默认行为下会弹出一个contextMenu,里面的唯一选项才会调用click事件 - //setContextMenu应该是目前唯一能在linux下使用托盘菜单api - //但是无法区分鼠标左右键 - - tray.setContextMenu(contextMenu); - } else { - //windows and macos - tray.on('click', () => { - win.show(); + initTemplate() { + //在linux下,鼠标左右键都会呼出contextMenu + //所以此处单独为linux添加一个 显示主面板 选项 + this.template = [ + { + label: '显示主面板', + click: () => { + this.win.show(); + }, + }, + { + type: 'separator', + }, + ].concat(createMenuTemplate(this.win)); + } + + handleEvents() { + this.emitter.on('updateTooltip', title => this.tray.setToolTip(title)); + this.emitter.on('updatePlayState', isPlaying => { + this.contextMenu.getMenuItemById('play').visible = !isPlaying; + this.contextMenu.getMenuItemById('pause').visible = isPlaying; + this.tray.setContextMenu(this.contextMenu); + }); + this.emitter.on('updateLikeState', isLiked => { + this.contextMenu.getMenuItemById('like').visible = !isLiked; + this.contextMenu.getMenuItemById('unlike').visible = isLiked; + this.tray.setContextMenu(this.contextMenu); + }); + } +} + +class YPMTrayWindowsImpl { + constructor(tray, win, emitter) { + this.tray = tray; + this.win = win; + this.emitter = emitter; + this.template = createMenuTemplate(win); + this.contextMenu = Menu.buildFromTemplate(this.template); + + this.isPlaying = false; + this.curDisplayPlaying = false; + + this.isLiked = false; + this.curDisplayLiked = false; + + this.handleEvents(); + } + + handleEvents() { + this.tray.on('click', () => { + this.win.show(); }); - tray.on('right-click', () => { - tray.popUpContextMenu(contextMenu); + this.tray.on('right-click', () => { + if (this.isPlaying !== this.curDisplayPlaying) { + this.curDisplayPlaying = this.isPlaying; + this.contextMenu.getMenuItemById('play').visible = !this.isPlaying; + this.contextMenu.getMenuItemById('pause').visible = this.isPlaying; + } + + if (this.isLiked !== this.curDisplayLiked) { + this.curDisplayLiked = this.isLiked; + this.contextMenu.getMenuItemById('like').visible = !this.isLiked; + this.contextMenu.getMenuItemById('unlike').visible = this.isLiked; + } + + this.tray.popUpContextMenu(this.contextMenu); }); + + this.emitter.on('updateTooltip', title => this.tray.setToolTip(title)); + this.emitter.on( + 'updatePlayState', + isPlaying => (this.isPlaying = isPlaying) + ); + this.emitter.on('updateLikeState', isLiked => (this.isLiked = isLiked)); } +} + +export function createTray(win, eventEmitter) { + let icon = nativeImage + .createFromPath(path.join(__static, 'img/icons/menu@88.png')) + .resize({ + height: 20, + width: 20, + }); + + let tray = new Tray(icon); + tray.setToolTip('YesPlayMusic'); - return tray; + return isLinux + ? new YPMTrayLinuxImpl(tray, win, eventEmitter) + : new YPMTrayWindowsImpl(tray, win, eventEmitter); } diff --git a/src/utils/Player.js b/src/utils/Player.js index 5bb7cd9..8054f69 100644 --- a/src/utils/Player.js +++ b/src/utils/Player.js @@ -9,6 +9,7 @@ import { personalFM, fmTrash } from '@/api/others'; import store from '@/store'; import { isAccountLoggedIn } from '@/utils/auth'; import { trackUpdateNowPlaying, trackScrobble } from '@/api/lastfm'; +import { isCreateTray } from '@/utils/platform'; const electron = process.env.IS_ELECTRON === true ? window.require('electron') : null; @@ -20,6 +21,21 @@ const excludeSaveKeys = [ '_personalFMNextLoading', ]; +function setTitle(track) { + document.title = track + ? `${track.name} · ${track.ar[0].name} - YesPlayMusic` + : 'YesPlayMusic'; + if (isCreateTray) { + ipcRenderer.send('updateTrayTooltip', document.title); + } +} + +function setTrayLikeState(isLiked) { + if (isCreateTray) { + ipcRenderer.send('updateTrayLikeState', isLiked); + } +} + export default class { constructor() { // 播放器状态 @@ -194,6 +210,12 @@ export default class { }); } } + _setPlaying(isPlaying) { + this._playing = isPlaying; + if (isCreateTray) { + ipcRenderer.send('updateTrayPlayState', this._playing); + } + } _setIntervals() { // 同步播放进度 // TODO: 如果 _progress 在别的地方被改变了,这个定时器会覆盖之前改变的值,是bug @@ -284,8 +306,9 @@ export default class { if (autoplay) { this.play(); if (this._currentTrack.name) { - document.title = `${this._currentTrack.name} · ${this._currentTrack.ar[0].name} - YesPlayMusic`; + setTitle(this._currentTrack); } + setTrayLikeState(store.state.liked.songs.includes(this.currentTrack.id)); } this.setOutputDevice(); this._howler.once('end', () => { @@ -539,7 +562,7 @@ export default class { const [trackID, index] = this._getNextTrack(); if (trackID === undefined) { this._howler?.stop(); - this._playing = false; + this._setPlaying(false); return false; } this.current = index; @@ -593,16 +616,16 @@ export default class { pause() { this._howler?.pause(); - this._playing = false; - document.title = 'YesPlayMusic'; + this._setPlaying(false); + setTitle(null); this._pauseDiscordPresence(this._currentTrack); } play() { if (this._howler?.playing()) return; this._howler?.play(); - this._playing = true; + this._setPlaying(true); if (this._currentTrack.name) { - document.title = `${this._currentTrack.name} · ${this._currentTrack.ar[0].name} - YesPlayMusic`; + setTitle(this._currentTrack); } this._playDiscordPresence(this._currentTrack, this.seek()); if (store.state.lastfm.key !== undefined) { @@ -737,10 +760,12 @@ export default class { sendSelfToIpcMain() { if (process.env.IS_ELECTRON !== true) return false; + let liked = store.state.liked.songs.includes(this.currentTrack.id); ipcRenderer.send('player', { playing: this.playing, - likedCurrentTrack: store.state.liked.songs.includes(this.currentTrack.id), + likedCurrentTrack: liked, }); + setTrayLikeState(liked); } switchRepeatMode() { diff --git a/src/utils/platform.js b/src/utils/platform.js new file mode 100644 index 0000000..1d803a4 --- /dev/null +++ b/src/utils/platform.js @@ -0,0 +1,6 @@ +export const isWindows = process.platform === 'win32'; +export const isMac = process.platform === 'darwin'; +export const isLinux = process.platform === 'linux'; +export const isDevelopment = process.env.NODE_ENV === 'development'; + +export const isCreateTray = isWindows || isLinux || isDevelopment;