feat(electron): app menu item and hotkey for creating new page (#2267)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
128
apps/electron/layers/main/src/application-menu.ts
Normal file
128
apps/electron/layers/main/src/application-menu.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { app, Menu } from 'electron';
|
||||||
|
|
||||||
|
import { isMacOS } from '../../utils';
|
||||||
|
import { subjects } from './events';
|
||||||
|
|
||||||
|
// Unique id for menuitems
|
||||||
|
const MENUITEM_NEW_PAGE = 'affine:new-page';
|
||||||
|
|
||||||
|
export function createApplicationMenu() {
|
||||||
|
const isMac = isMacOS();
|
||||||
|
|
||||||
|
// Electron menu cannot be modified
|
||||||
|
// You have to copy the complete default menu template event if you want to add a single custom item
|
||||||
|
// See https://www.electronjs.org/docs/latest/api/menu#examples
|
||||||
|
const template = [
|
||||||
|
// { role: 'appMenu' }
|
||||||
|
...(isMac
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: app.name,
|
||||||
|
submenu: [
|
||||||
|
{ role: 'about' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'services' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'hide' },
|
||||||
|
{ role: 'hideOthers' },
|
||||||
|
{ role: 'unhide' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'quit' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
// { role: 'fileMenu' }
|
||||||
|
{
|
||||||
|
label: 'File',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
id: MENUITEM_NEW_PAGE,
|
||||||
|
label: 'New Page',
|
||||||
|
accelerator: isMac ? 'Cmd+N' : 'Ctrl+N',
|
||||||
|
click: () => {
|
||||||
|
subjects.applicationMenu.newPageAction.next();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
isMac ? { role: 'close' } : { role: 'quit' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// { role: 'editMenu' }
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'undo' },
|
||||||
|
{ role: 'redo' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'cut' },
|
||||||
|
{ role: 'copy' },
|
||||||
|
{ role: 'paste' },
|
||||||
|
...(isMac
|
||||||
|
? [
|
||||||
|
{ role: 'pasteAndMatchStyle' },
|
||||||
|
{ role: 'delete' },
|
||||||
|
{ role: 'selectAll' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Speech',
|
||||||
|
submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// { role: 'viewMenu' }
|
||||||
|
{
|
||||||
|
label: 'View',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'reload' },
|
||||||
|
{ role: 'forceReload' },
|
||||||
|
{ role: 'toggleDevTools' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'resetZoom' },
|
||||||
|
{ role: 'zoomIn' },
|
||||||
|
{ role: 'zoomOut' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'togglefullscreen' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// { role: 'windowMenu' }
|
||||||
|
{
|
||||||
|
label: 'Window',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'minimize' },
|
||||||
|
{ role: 'zoom' },
|
||||||
|
...(isMac
|
||||||
|
? [
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'front' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'window' },
|
||||||
|
]
|
||||||
|
: [{ role: 'close' }]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'help',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Learn More',
|
||||||
|
click: async () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { shell } = require('electron');
|
||||||
|
await shell.openExternal('https://affine.pro/');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// @ts-ignore The snippet is copied from Electron official docs.
|
||||||
|
// It's working as expected. No idea why it contains type errors.
|
||||||
|
// Just ignore for now.
|
||||||
|
const menu = Menu.buildFromTemplate(template);
|
||||||
|
Menu.setApplicationMenu(menu);
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
22
apps/electron/layers/main/src/events/application-menu.ts
Normal file
22
apps/electron/layers/main/src/events/application-menu.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
import type { MainEventListener } from './type';
|
||||||
|
|
||||||
|
export const applicationMenuSubjects = {
|
||||||
|
newPageAction: new Subject<void>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Events triggered by application menu
|
||||||
|
*/
|
||||||
|
export const applicationMenuEvents = {
|
||||||
|
/**
|
||||||
|
* File -> New Page
|
||||||
|
*/
|
||||||
|
onNewPageAction: (fn: () => void) => {
|
||||||
|
const sub = applicationMenuSubjects.newPageAction.subscribe(fn);
|
||||||
|
return () => {
|
||||||
|
sub.unsubscribe();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} satisfies Record<string, MainEventListener>;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
export * from './register';
|
export * from './register';
|
||||||
|
|
||||||
|
import { applicationMenuSubjects } from './application-menu';
|
||||||
import { dbSubjects } from './db';
|
import { dbSubjects } from './db';
|
||||||
|
|
||||||
export const subjects = {
|
export const subjects = {
|
||||||
db: dbSubjects,
|
db: dbSubjects,
|
||||||
|
applicationMenu: applicationMenuSubjects,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { app, BrowserWindow } from 'electron';
|
import { app, BrowserWindow } from 'electron';
|
||||||
|
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
|
import { applicationMenuEvents } from './application-menu';
|
||||||
import { dbEvents } from './db';
|
import { dbEvents } from './db';
|
||||||
import { updaterEvents } from './updater';
|
import { updaterEvents } from './updater';
|
||||||
|
|
||||||
export const allEvents = {
|
export const allEvents = {
|
||||||
db: dbEvents,
|
db: dbEvents,
|
||||||
updater: updaterEvents,
|
updater: updaterEvents,
|
||||||
|
applicationMenu: applicationMenuEvents,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getActiveWindows() {
|
function getActiveWindows() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import './security-restrictions';
|
|||||||
|
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
|
|
||||||
|
import { createApplicationMenu } from './application-menu';
|
||||||
import { registerEvents } from './events';
|
import { registerEvents } from './events';
|
||||||
import { registerHandlers } from './handlers';
|
import { registerHandlers } from './handlers';
|
||||||
import { registerUpdater } from './handlers/updater';
|
import { registerUpdater } from './handlers/updater';
|
||||||
@@ -57,6 +58,7 @@ app
|
|||||||
.then(registerHandlers)
|
.then(registerHandlers)
|
||||||
.then(registerEvents)
|
.then(registerEvents)
|
||||||
.then(restoreOrCreateWindow)
|
.then(restoreOrCreateWindow)
|
||||||
|
.then(createApplicationMenu)
|
||||||
.then(registerUpdater)
|
.then(registerUpdater)
|
||||||
.catch(e => console.error('Failed create window:', e));
|
.catch(e => console.error('Failed create window:', e));
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -89,6 +89,14 @@ export const RootAppSidebar = ({
|
|||||||
const page = await createPage();
|
const page = await createPage();
|
||||||
openPage(page.id);
|
openPage(page.id);
|
||||||
}, [createPage, openPage]);
|
}, [createPage, openPage]);
|
||||||
|
|
||||||
|
// Listen to the "New Page" action from the menu
|
||||||
|
useEffect(() => {
|
||||||
|
if (environment.isDesktop) {
|
||||||
|
return window.events?.applicationMenu.onNewPageAction(onClickNewPage);
|
||||||
|
}
|
||||||
|
}, [onClickNewPage]);
|
||||||
|
|
||||||
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
|
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (environment.isDesktop && typeof sidebarOpen === 'boolean') {
|
if (environment.isDesktop && typeof sidebarOpen === 'boolean') {
|
||||||
|
|||||||
Reference in New Issue
Block a user