mirror of
https://github.com/qier222/YesPlayMusic.git
synced 2024-11-24 21:09:23 +08:00
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:
parent
3cbb8d9b25
commit
d716bb8cde
BIN
public/img/icons/pause.png
Normal file
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
BIN
public/img/icons/unlike.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 932 B |
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
6
src/utils/platform.js
Normal 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;
|
Loading…
Reference in New Issue
Block a user