mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-17 06:00:20 +02:00
feat: macOS menu enhancement (#1903)
This commit is contained in:
+99
-8
@@ -29,7 +29,7 @@ import packageJson from '../../package.json';
|
||||
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
|
||||
import { shutdownServer } from './features/core/remote';
|
||||
import { store } from './features/core/settings';
|
||||
import MenuBuilder from './menu';
|
||||
import MenuBuilder, { MenuPlaybackState } from './menu';
|
||||
import {
|
||||
autoUpdaterLogInterface,
|
||||
createLog,
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
} from './utils';
|
||||
import './features';
|
||||
|
||||
import { PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||
import { PlayerRepeat, PlayerStatus, PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||
|
||||
const ALPHA_UPDATER_CONFIG: {
|
||||
bucket: string;
|
||||
@@ -277,6 +277,13 @@ let tray: null | Tray = null;
|
||||
let exitFromTray = false;
|
||||
let forceQuit = false;
|
||||
let powerSaveBlockerId: null | number = null;
|
||||
let menuBuilder: MenuBuilder | null = null;
|
||||
let currentPlaybackStatus: PlayerStatus = PlayerStatus.PAUSED;
|
||||
let currentPrivateMode = false;
|
||||
let currentRepeatMode: PlayerRepeat = PlayerRepeat.NONE;
|
||||
let currentSidebarCollapsed = false;
|
||||
let currentShuffleEnabled = false;
|
||||
let playbackMenuAccelerators: MenuPlaybackState['accelerators'] = {};
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
import('source-map-support').then((sourceMapSupport) => {
|
||||
@@ -333,6 +340,23 @@ export const getMainWindow = () => {
|
||||
return mainWindow;
|
||||
};
|
||||
|
||||
const rebuildMainMenu = () => {
|
||||
if (!menuBuilder || !mainWindow) return;
|
||||
|
||||
menuBuilder.buildMenu({
|
||||
accelerators: playbackMenuAccelerators,
|
||||
playbackStatus: currentPlaybackStatus,
|
||||
privateMode: currentPrivateMode,
|
||||
repeatMode: currentRepeatMode,
|
||||
shuffleEnabled: currentShuffleEnabled,
|
||||
sidebarCollapsed: currentSidebarCollapsed,
|
||||
});
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
Menu.setApplicationMenu(null);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendToastToRenderer = ({
|
||||
message,
|
||||
type,
|
||||
@@ -699,12 +723,8 @@ async function createWindow(first = true): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
const menuBuilder = new MenuBuilder(mainWindow);
|
||||
menuBuilder.buildMenu();
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
Menu.setApplicationMenu(null);
|
||||
}
|
||||
menuBuilder = new MenuBuilder(mainWindow);
|
||||
rebuildMainMenu();
|
||||
|
||||
// Open URLs in the user's browser
|
||||
mainWindow.webContents.setWindowOpenHandler((edata) => {
|
||||
@@ -782,6 +802,17 @@ enum BindingActions {
|
||||
VOLUME_UP = 'volumeUp',
|
||||
}
|
||||
|
||||
const getMenuAccelerator = (
|
||||
data: Record<BindingActions, { allowGlobal: boolean; hotkey: string; isGlobal: boolean }>,
|
||||
action: BindingActions,
|
||||
) => {
|
||||
const hotkey = data[action]?.hotkey;
|
||||
|
||||
if (!hotkey) return undefined;
|
||||
|
||||
return hotkeyToElectronAccelerator(hotkey);
|
||||
};
|
||||
|
||||
const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
|
||||
[BindingActions.GLOBAL_SEARCH]: () => {},
|
||||
[BindingActions.LOCAL_SEARCH]: () => {},
|
||||
@@ -835,6 +866,26 @@ ipcMain.on(
|
||||
}
|
||||
}
|
||||
|
||||
playbackMenuAccelerators = {
|
||||
next: getMenuAccelerator(data, BindingActions.NEXT),
|
||||
playPause:
|
||||
getMenuAccelerator(data, BindingActions.PLAY_PAUSE) ||
|
||||
getMenuAccelerator(data, BindingActions.PLAY) ||
|
||||
getMenuAccelerator(data, BindingActions.PAUSE),
|
||||
previous: getMenuAccelerator(data, BindingActions.PREVIOUS),
|
||||
repeat: getMenuAccelerator(data, BindingActions.TOGGLE_REPEAT),
|
||||
seekBackward: getMenuAccelerator(data, BindingActions.SKIP_BACKWARD),
|
||||
seekForward: getMenuAccelerator(data, BindingActions.SKIP_FORWARD),
|
||||
shuffle: getMenuAccelerator(data, BindingActions.SHUFFLE),
|
||||
stop: getMenuAccelerator(data, BindingActions.STOP),
|
||||
volumeDown: getMenuAccelerator(data, BindingActions.VOLUME_DOWN),
|
||||
volumeUp: getMenuAccelerator(data, BindingActions.VOLUME_UP),
|
||||
};
|
||||
|
||||
if (isMacOS()) {
|
||||
rebuildMainMenu();
|
||||
}
|
||||
|
||||
const globalMediaKeysEnabled = store.get('global_media_hotkeys', true) as boolean;
|
||||
|
||||
if (globalMediaKeysEnabled) {
|
||||
@@ -975,3 +1026,43 @@ if (!ipcMain.eventNames().includes('open-application-directory')) {
|
||||
shell.openPath(userDataPath);
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.on('update-playback', (_event, status: PlayerStatus) => {
|
||||
currentPlaybackStatus = status;
|
||||
|
||||
if (!isMacOS()) return;
|
||||
|
||||
rebuildMainMenu();
|
||||
});
|
||||
|
||||
ipcMain.on('update-repeat', (_event, repeat: PlayerRepeat) => {
|
||||
currentRepeatMode = repeat;
|
||||
|
||||
if (!isMacOS()) return;
|
||||
|
||||
rebuildMainMenu();
|
||||
});
|
||||
|
||||
ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
|
||||
currentShuffleEnabled = shuffle;
|
||||
|
||||
if (!isMacOS()) return;
|
||||
|
||||
rebuildMainMenu();
|
||||
});
|
||||
|
||||
ipcMain.on('update-private-mode', (_event, privateMode: boolean) => {
|
||||
currentPrivateMode = privateMode;
|
||||
|
||||
if (!isMacOS()) return;
|
||||
|
||||
rebuildMainMenu();
|
||||
});
|
||||
|
||||
ipcMain.on('update-sidebar-collapsed', (_event, collapsedSidebar: boolean) => {
|
||||
currentSidebarCollapsed = collapsedSidebar;
|
||||
|
||||
if (!isMacOS()) return;
|
||||
|
||||
rebuildMainMenu();
|
||||
});
|
||||
|
||||
+190
-4
@@ -1,18 +1,53 @@
|
||||
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, shell } from 'electron';
|
||||
|
||||
import packageJson from '../../package.json';
|
||||
|
||||
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export type MenuPlaybackState = {
|
||||
accelerators?: {
|
||||
next?: string;
|
||||
playPause?: string;
|
||||
previous?: string;
|
||||
repeat?: string;
|
||||
seekBackward?: string;
|
||||
seekForward?: string;
|
||||
shuffle?: string;
|
||||
stop?: string;
|
||||
volumeDown?: string;
|
||||
volumeUp?: string;
|
||||
};
|
||||
playbackStatus?: PlayerStatus;
|
||||
privateMode?: boolean;
|
||||
repeatMode?: PlayerRepeat;
|
||||
shuffleEnabled?: boolean;
|
||||
sidebarCollapsed?: boolean;
|
||||
};
|
||||
|
||||
interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
|
||||
selector?: string;
|
||||
submenu?: DarwinMenuItemConstructorOptions[] | Menu;
|
||||
}
|
||||
|
||||
export default class MenuBuilder {
|
||||
developmentEnvironmentSetup = false;
|
||||
mainWindow: BrowserWindow;
|
||||
|
||||
constructor(mainWindow: BrowserWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
buildDarwinTemplate(): MenuItemConstructorOptions[] {
|
||||
buildDarwinTemplate({
|
||||
accelerators,
|
||||
playbackStatus = PlayerStatus.PAUSED,
|
||||
privateMode = false,
|
||||
repeatMode = PlayerRepeat.NONE,
|
||||
shuffleEnabled = false,
|
||||
sidebarCollapsed = false,
|
||||
}: MenuPlaybackState = {}): MenuItemConstructorOptions[] {
|
||||
const isPlaying = playbackStatus === PlayerStatus.PLAYING;
|
||||
const isRepeatEnabled = repeatMode !== PlayerRepeat.NONE;
|
||||
|
||||
const subMenuAbout: DarwinMenuItemConstructorOptions = {
|
||||
label: 'Electron',
|
||||
submenu: [
|
||||
@@ -29,6 +64,21 @@ export default class MenuBuilder {
|
||||
label: 'Settings',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-open-manage-servers');
|
||||
},
|
||||
label: 'Manage servers',
|
||||
},
|
||||
{
|
||||
checked: privateMode,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-toggle-private-mode');
|
||||
},
|
||||
label: 'Private session',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: 'Services', submenu: [] },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
@@ -71,6 +121,22 @@ export default class MenuBuilder {
|
||||
const subMenuViewDev: MenuItemConstructorOptions = {
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{
|
||||
accelerator: 'Command+K',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-open-command-palette');
|
||||
},
|
||||
label: 'Command Palette...',
|
||||
},
|
||||
{
|
||||
checked: sidebarCollapsed,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-toggle-sidebar');
|
||||
},
|
||||
label: 'Collapse sidebar',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
accelerator: 'Command+R',
|
||||
click: () => {
|
||||
@@ -97,6 +163,22 @@ export default class MenuBuilder {
|
||||
const subMenuViewProd: MenuItemConstructorOptions = {
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{
|
||||
accelerator: 'Command+K',
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-open-command-palette');
|
||||
},
|
||||
label: 'Command Palette...',
|
||||
},
|
||||
{
|
||||
checked: sidebarCollapsed,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-toggle-sidebar');
|
||||
},
|
||||
label: 'Collapse sidebar',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
accelerator: 'Ctrl+Command+F',
|
||||
click: () => {
|
||||
@@ -119,6 +201,89 @@ export default class MenuBuilder {
|
||||
{ label: 'Bring All to Front', selector: 'arrangeInFront:' },
|
||||
],
|
||||
};
|
||||
const subMenuPlayback: MenuItemConstructorOptions = {
|
||||
label: 'Playback',
|
||||
submenu: [
|
||||
{
|
||||
accelerator: accelerators?.playPause,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-play-pause');
|
||||
},
|
||||
label: isPlaying ? 'Pause' : 'Play',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
accelerator: accelerators?.next,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-next');
|
||||
},
|
||||
label: 'Next',
|
||||
},
|
||||
{
|
||||
accelerator: accelerators?.previous,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-previous');
|
||||
},
|
||||
label: 'Previous',
|
||||
},
|
||||
{
|
||||
accelerator: accelerators?.seekForward,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-skip-forward');
|
||||
},
|
||||
label: 'Seek Forward',
|
||||
},
|
||||
{
|
||||
accelerator: accelerators?.seekBackward,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-skip-backward');
|
||||
},
|
||||
label: 'Seek Backforward',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
accelerator: accelerators?.shuffle,
|
||||
checked: shuffleEnabled,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-toggle-shuffle');
|
||||
},
|
||||
label: 'Shuffle',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
accelerator: accelerators?.repeat,
|
||||
checked: isRepeatEnabled,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-toggle-repeat');
|
||||
},
|
||||
label: 'Repeat',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
accelerator: accelerators?.stop,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-stop');
|
||||
},
|
||||
label: 'Stop',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
accelerator: accelerators?.volumeUp,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-volume-up');
|
||||
},
|
||||
label: 'Volume Up',
|
||||
},
|
||||
{
|
||||
accelerator: accelerators?.volumeDown,
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-player-volume-down');
|
||||
},
|
||||
label: 'Volume Down',
|
||||
},
|
||||
],
|
||||
};
|
||||
const subMenuHelp: MenuItemConstructorOptions = {
|
||||
label: 'Help',
|
||||
submenu: [
|
||||
@@ -148,6 +313,13 @@ export default class MenuBuilder {
|
||||
},
|
||||
label: 'Search Issues',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
click: () => {
|
||||
this.mainWindow.webContents.send('renderer-open-release-notes');
|
||||
},
|
||||
label: 'Version ' + packageJson.version,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -156,7 +328,14 @@ export default class MenuBuilder {
|
||||
? subMenuViewDev
|
||||
: subMenuViewProd;
|
||||
|
||||
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
|
||||
return [
|
||||
subMenuAbout,
|
||||
subMenuEdit,
|
||||
subMenuView,
|
||||
subMenuPlayback,
|
||||
subMenuWindow,
|
||||
subMenuHelp,
|
||||
];
|
||||
}
|
||||
|
||||
buildDefaultTemplate(): MenuItemConstructorOptions[] {
|
||||
@@ -262,14 +441,14 @@ export default class MenuBuilder {
|
||||
return templateDefault;
|
||||
}
|
||||
|
||||
buildMenu(): Menu {
|
||||
buildMenu(playbackState: MenuPlaybackState = {}): Menu {
|
||||
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
||||
this.setupDevelopmentEnvironment();
|
||||
}
|
||||
|
||||
const template =
|
||||
process.platform === 'darwin'
|
||||
? this.buildDarwinTemplate()
|
||||
? this.buildDarwinTemplate(playbackState)
|
||||
: this.buildDefaultTemplate();
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
@@ -279,6 +458,13 @@ export default class MenuBuilder {
|
||||
}
|
||||
|
||||
setupDevelopmentEnvironment(): void {
|
||||
// buildMenu can run multiple times as menu state updates; attach this once.
|
||||
if (this.developmentEnvironmentSetup) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.developmentEnvironmentSetup = true;
|
||||
|
||||
this.mainWindow.webContents.on('context-menu', (_, props) => {
|
||||
const { x, y } = props;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user