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
This commit is contained in:
memorydream 2022-01-17 23:23:21 +08:00 committed by GitHub
parent 3cbb8d9b25
commit d716bb8cde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 269 additions and 129 deletions

BIN
public/img/icons/pause.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

BIN
public/img/icons/unlike.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

View File

@ -8,6 +8,13 @@ import {
globalShortcut, globalShortcut,
nativeTheme, nativeTheme,
} from 'electron'; } from 'electron';
import {
isWindows,
isMac,
isLinux,
isDevelopment,
isCreateTray,
} from '@/utils/platform';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'; import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import { startNeteaseMusicApi } from './electron/services'; import { startNeteaseMusicApi } from './electron/services';
import { initIpcMain } from './electron/ipcMain.js'; import { initIpcMain } from './electron/ipcMain.js';
@ -18,6 +25,7 @@ import { createDockMenu } from './electron/dockMenu';
import { registerGlobalShortcut } from './electron/globalShortcut'; import { registerGlobalShortcut } from './electron/globalShortcut';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'; import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
import { EventEmitter } from 'events';
import express from 'express'; import express from 'express';
import expressProxy from 'express-http-proxy'; import expressProxy from 'express-http-proxy';
import Store from 'electron-store'; import Store from 'electron-store';
@ -69,15 +77,10 @@ const closeOnLinux = (e, win, store) => {
} }
}; };
const isWindows = process.platform === 'win32';
const isMac = process.platform === 'darwin';
const isLinux = process.platform === 'linux';
const isDevelopment = process.env.NODE_ENV === 'development';
class Background { class Background {
constructor() { constructor() {
this.window = null; this.window = null;
this.tray = null; this.ypmTrayImpl = null;
this.store = new Store({ this.store = new Store({
windowWidth: { windowWidth: {
width: { type: 'number', default: 1440 }, width: { type: 'number', default: 1440 },
@ -324,8 +327,14 @@ class Background {
}); });
this.handleWindowEvents(); this.handleWindowEvents();
// create tray
if (isCreateTray) {
this.trayEventEmitter = new EventEmitter();
this.ypmTrayImpl = createTray(this.window, this.trayEventEmitter);
}
// init ipcMain // init ipcMain
initIpcMain(this.window, this.store); initIpcMain(this.window, this.store, this.trayEventEmitter);
// set proxy // set proxy
const proxyRules = this.store.get('proxy'); const proxyRules = this.store.get('proxy');
@ -341,11 +350,6 @@ class Background {
// create menu // create menu
createMenu(this.window, this.store); createMenu(this.window, this.store);
// create tray
if (isWindows || isLinux || isDevelopment) {
this.tray = createTray(this.window);
}
// create dock menu for macOS // create dock menu for macOS
const createdDockMenu = createDockMenu(this.window); const createdDockMenu = createDockMenu(this.window);
if (createDockMenu && app.dock) app.dock.setMenu(createdDockMenu); if (createDockMenu && app.dock) app.dock.setMenu(createdDockMenu);

View File

@ -4,12 +4,73 @@ import { registerGlobalShortcut } from '@/electron/globalShortcut';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import shortcuts from '@/utils/shortcuts'; import shortcuts from '@/utils/shortcuts';
import { createMenu } from './menu'; import { createMenu } from './menu';
import { isCreateTray, isMac } from '@/utils/platform';
const clc = require('cli-color'); const clc = require('cli-color');
const log = text => { const log = text => {
console.log(`${clc.blueBright('[ipcMain.js]')} ${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'); const client = require('discord-rich-presence')('818936529484906596');
/** /**
@ -58,7 +119,7 @@ function parseSourceStringToList(sourceString) {
return sourceString.split(',').map(s => s.trim()); 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) => { ipcMain.handle('unblock-music', async (_, track, source) => {
// 兼容 unblockneteasemusic 所使用的 api 字段 // 兼容 unblockneteasemusic 所使用的 api 字段
track.alias = track.alia || []; track.alias = track.alia || [];
@ -102,9 +163,9 @@ export function initIpcMain(win, store) {
}); });
ipcMain.on('close', e => { ipcMain.on('close', e => {
if (process.platform === 'darwin') { if (isMac) {
win.hide(); win.hide();
exitAsk(e); exitAsk(e, win);
} else { } else {
let closeOpt = store.get('settings.closeAppOption'); let closeOpt = store.get('settings.closeAppOption');
if (closeOpt === 'exit') { if (closeOpt === 'exit') {
@ -115,7 +176,7 @@ export function initIpcMain(win, store) {
e.preventDefault(); e.preventDefault();
win.hide(); win.hide();
} else { } else {
exitAskWithoutMac(e); exitAskWithoutMac(e, win);
} }
} }
}); });
@ -214,63 +275,15 @@ export function initIpcMain(win, store) {
registerGlobalShortcut(win, store); registerGlobalShortcut(win, store);
}); });
const exitAsk = e => { if (isCreateTray) {
e.preventDefault(); //阻止默认行为 ipcMain.on('updateTrayTooltip', (_, title) => {
dialog trayEventEmitter.emit('updateTooltip', title);
.showMessageBox({ });
type: 'info', ipcMain.on('updateTrayPlayState', (_, isPlaying) => {
title: 'Information', trayEventEmitter.emit('updatePlayState', isPlaying);
cancelId: 2, });
defaultId: 0, ipcMain.on('updateTrayLikeState', (_, isLiked) => {
message: '确定要关闭吗?', trayEventEmitter.emit('updateLikeState', isLiked);
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);
});
};
} }

View File

@ -1,40 +1,30 @@
/* global __static */ /* global __static */
import path from 'path'; import path from 'path';
import { app, nativeImage, Tray, Menu } from 'electron'; import { app, nativeImage, Tray, Menu } from 'electron';
import { isLinux } from '@/utils/platform';
export function createTray(win) { function createMenuTemplate(win) {
let icon = nativeImage return [
.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',
},
]
: []),
{ {
label: '播放/暂停', label: '播放',
icon: nativeImage.createFromPath( icon: nativeImage.createFromPath(
path.join(__static, 'img/icons/play.png') path.join(__static, 'img/icons/play.png')
), ),
click: () => { click: () => {
win.webContents.send('play'); 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: '上一首', label: '上一首',
@ -75,6 +65,19 @@ export function createTray(win) {
click: () => { click: () => {
win.webContents.send('like'); 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: '退出', label: '退出',
@ -86,28 +89,117 @@ export function createTray(win) {
app.exit(); app.exit();
}, },
}, },
]); ];
}
// 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();
}
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();
});
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); let tray = new Tray(icon);
tray.setToolTip('YesPlayMusic'); tray.setToolTip('YesPlayMusic');
if (process.platform === 'linux') { return isLinux
//linux下托盘的实现方式比较迷惑 ? new YPMTrayLinuxImpl(tray, win, eventEmitter)
//right-click无法在linux下使用 : new YPMTrayWindowsImpl(tray, win, eventEmitter);
//click在默认行为下会弹出一个contextMenu里面的唯一选项才会调用click事件
//setContextMenu应该是目前唯一能在linux下使用托盘菜单api
//但是无法区分鼠标左右键
tray.setContextMenu(contextMenu);
} else {
//windows and macos
tray.on('click', () => {
win.show();
});
tray.on('right-click', () => {
tray.popUpContextMenu(contextMenu);
});
}
return tray;
} }

View File

@ -9,6 +9,7 @@ import { personalFM, fmTrash } from '@/api/others';
import store from '@/store'; import store from '@/store';
import { isAccountLoggedIn } from '@/utils/auth'; import { isAccountLoggedIn } from '@/utils/auth';
import { trackUpdateNowPlaying, trackScrobble } from '@/api/lastfm'; import { trackUpdateNowPlaying, trackScrobble } from '@/api/lastfm';
import { isCreateTray } from '@/utils/platform';
const electron = const electron =
process.env.IS_ELECTRON === true ? window.require('electron') : null; process.env.IS_ELECTRON === true ? window.require('electron') : null;
@ -20,6 +21,21 @@ const excludeSaveKeys = [
'_personalFMNextLoading', '_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 { export default class {
constructor() { constructor() {
// 播放器状态 // 播放器状态
@ -194,6 +210,12 @@ export default class {
}); });
} }
} }
_setPlaying(isPlaying) {
this._playing = isPlaying;
if (isCreateTray) {
ipcRenderer.send('updateTrayPlayState', this._playing);
}
}
_setIntervals() { _setIntervals() {
// 同步播放进度 // 同步播放进度
// TODO: 如果 _progress 在别的地方被改变了这个定时器会覆盖之前改变的值是bug // TODO: 如果 _progress 在别的地方被改变了这个定时器会覆盖之前改变的值是bug
@ -284,8 +306,9 @@ export default class {
if (autoplay) { if (autoplay) {
this.play(); this.play();
if (this._currentTrack.name) { 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.setOutputDevice();
this._howler.once('end', () => { this._howler.once('end', () => {
@ -539,7 +562,7 @@ export default class {
const [trackID, index] = this._getNextTrack(); const [trackID, index] = this._getNextTrack();
if (trackID === undefined) { if (trackID === undefined) {
this._howler?.stop(); this._howler?.stop();
this._playing = false; this._setPlaying(false);
return false; return false;
} }
this.current = index; this.current = index;
@ -593,16 +616,16 @@ export default class {
pause() { pause() {
this._howler?.pause(); this._howler?.pause();
this._playing = false; this._setPlaying(false);
document.title = 'YesPlayMusic'; setTitle(null);
this._pauseDiscordPresence(this._currentTrack); this._pauseDiscordPresence(this._currentTrack);
} }
play() { play() {
if (this._howler?.playing()) return; if (this._howler?.playing()) return;
this._howler?.play(); this._howler?.play();
this._playing = true; this._setPlaying(true);
if (this._currentTrack.name) { if (this._currentTrack.name) {
document.title = `${this._currentTrack.name} · ${this._currentTrack.ar[0].name} - YesPlayMusic`; setTitle(this._currentTrack);
} }
this._playDiscordPresence(this._currentTrack, this.seek()); this._playDiscordPresence(this._currentTrack, this.seek());
if (store.state.lastfm.key !== undefined) { if (store.state.lastfm.key !== undefined) {
@ -737,10 +760,12 @@ export default class {
sendSelfToIpcMain() { sendSelfToIpcMain() {
if (process.env.IS_ELECTRON !== true) return false; if (process.env.IS_ELECTRON !== true) return false;
let liked = store.state.liked.songs.includes(this.currentTrack.id);
ipcRenderer.send('player', { ipcRenderer.send('player', {
playing: this.playing, playing: this.playing,
likedCurrentTrack: store.state.liked.songs.includes(this.currentTrack.id), likedCurrentTrack: liked,
}); });
setTrayLikeState(liked);
} }
switchRepeatMode() { switchRepeatMode() {

6
src/utils/platform.js Normal file
View File

@ -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;