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,
nativeTheme,
} from 'electron';
import {
isWindows,
isMac,
isLinux,
isDevelopment,
isCreateTray,
} from '@/utils/platform';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import { startNeteaseMusicApi } from './electron/services';
import { initIpcMain } from './electron/ipcMain.js';
@ -18,6 +25,7 @@ import { createDockMenu } from './electron/dockMenu';
import { registerGlobalShortcut } from './electron/globalShortcut';
import { autoUpdater } from 'electron-updater';
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
import { EventEmitter } from 'events';
import express from 'express';
import expressProxy from 'express-http-proxy';
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 {
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);

View File

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

View File

@ -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();
},
},
]);
];
}
// 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);
tray.setToolTip('YesPlayMusic');
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();
});
tray.on('right-click', () => {
tray.popUpContextMenu(contextMenu);
});
}
return tray;
return isLinux
? new YPMTrayLinuxImpl(tray, win, eventEmitter)
: new YPMTrayWindowsImpl(tray, win, eventEmitter);
}

View File

@ -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() {

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;