1
0
Fork 0
mirror of https://github.com/anyproto/anytype-ts.git synced 2025-06-07 21:47:02 +09:00
anytype-ts/electron/js/menu.js
Andrew Simachev f6ad0d984c
code review
2025-06-06 11:31:19 +02:00

567 lines
17 KiB
JavaScript

const { app, shell, Menu, Tray } = require('electron');
const { is } = require('electron-util');
const fs = require('fs');
const path = require('path');
const ConfigManager = require('./config.js');
const Util = require('./util.js');
const Separator = { type: 'separator' };
const DEFAULT_SHORTCUTS = {
createObject: [ 'CmdOrCtrl', 'N' ],
undo: [ 'CmdOrCtrl', 'Z' ],
redo: [ 'CmdOrCtrl', 'Shift', 'Z' ],
selectAll: [ 'CmdOrCtrl', 'A' ],
searchText: [ 'CmdOrCtrl', 'F' ],
print: [ 'CmdOrCtrl', 'P' ],
newWindow: [ 'CmdOrCtrl', 'Shift', 'N' ],
zoomIn: [ 'CmdOrCtrl', '=' ],
zoomOut: [ 'CmdOrCtrl', '-' ],
zoomReset: [ 'CmdOrCtrl', '0' ],
toggleFullscreen: [ 'CmdOrCtrl', 'Shift', 'F' ],
shortcut: [ 'Ctrl', 'Space' ],
};
class MenuManager {
win = null;
menu = null;
tray = null;
store = null;
setWindow (win) {
this.win = win;
};
initShortcuts () {
this.shortcuts = this.store.get('shortcuts') || {};
};
getAccelerator (id) {
let keys = this.shortcuts[id];
if (undefined === keys) {
return (DEFAULT_SHORTCUTS[id] || []).join('+');
};
keys = keys || [];
const ret = [];
for (const key of keys) {
if (key == 'ctrl') {
ret.push('CmdOrCtrl');
} else
if (key == 'shift') {
ret.push('Shift');
} else
if (key == 'alt') {
ret.push('Alt');
} else {
ret.push(key.toUpperCase());
};
};
return ret.join('+');
};
initMenu () {
this.initShortcuts();
const { config } = ConfigManager;
const Api = require('./api.js');
const WindowManager = require('./window.js');
const UpdateManager = require('./update.js');
const isAllowedUpdate = UpdateManager.isAllowed();
config.debug = config.debug || {};
config.flagsMw = config.flagsMw || {};
const menuParam = [
{
label: 'Anytype',
submenu: [
{ label: Util.translate('electronMenuAbout'), click: () => Util.send(this.win, 'popup', 'about', {}, true) },
Separator,
{ role: 'hide', label: Util.translate('electronMenuHide') },
{ role: 'hideothers', label: Util.translate('electronMenuHideOthers') },
{ role: 'unhide', label: Util.translate('electronMenuUnhide') },
{ type: 'separator', visible: isAllowedUpdate },
{ label: Util.translate('electronMenuCheckUpdates'), click: () => Api.updateCheck(this.win), visible: isAllowedUpdate },
Separator,
{ label: Util.translate('commonSettings'), submenu: this.menuSettings() },
Separator,
{ label: Util.translate('electronMenuQuit'), accelerator: 'CmdOrCtrl+Q', click: () => Api.exit(this.win, false) },
]
},
{
role: 'fileMenu', label: Util.translate('electronMenuFile'),
submenu: [
{ label: Util.translate('commonNewObject'), accelerator: this.getAccelerator('createObject'), click: () => Util.send(this.win, 'commandGlobal', 'createObject') },
{ label: Util.translate('commonNewSpace'), click: () => Util.send(this.win, 'commandGlobal', 'createSpace') },
Separator,
{ label: Util.translate('electronMenuImport'), click: () => this.openSettings('importIndex') },
{ label: Util.translate('electronMenuExport'), click: () => this.openSettings('exportIndex') },
{ label: Util.translate('electronMenuSaveAs'), click: () => Util.send(this.win, 'commandGlobal', 'save') },
Separator,
{
label: Util.translate('electronMenuOpen'), submenu: [
{ label: Util.translate('electronMenuWorkDirectory'), click: () => shell.openPath(Util.userPath()) },
{ label: Util.translate('electronMenuDataDirectory'), click: () => shell.openPath(Util.dataPath()) },
{ label: Util.translate('electronMenuConfigDirectory'), click: () => shell.openPath(Util.defaultUserDataPath()) },
{ label: Util.translate('electronMenuLogsDirectory'), click: () => shell.openPath(Util.logPath()) },
{
label: Util.translate('electronMenuCustomCss'),
click: () => {
const fp = path.join(Util.userPath(), 'custom.css');
if (!fs.existsSync(fp)) {
fs.writeFileSync(fp, '');
};
shell.openPath(fp);
},
},
]
},
Separator,
{
label: Util.translate('electronMenuApplyCustomCss'), type: 'checkbox', checked: !config.disableCss,
click: () => {
config.disableCss = !config.disableCss;
Api.setConfig(this.win, { disableCss: config.disableCss }, () => {
WindowManager.reloadAll();
});
},
},
Separator,
{ role: 'close', label: Util.translate('electronMenuClose') },
]
},
{
label: Util.translate('electronMenuEdit'),
submenu: [
{
label: Util.translate('electronMenuUndo'), accelerator: this.getAccelerator('undo'),
click: () => {
if (this.win) {
this.win.webContents.undo();
Util.send(this.win, 'commandGlobal', 'undo');
};
}
},
{
label: Util.translate('electronMenuRedo'), accelerator: this.getAccelerator('redo'),
click: () => {
if (this.win) {
this.win.webContents.redo();
Util.send(this.win, 'commandGlobal', 'redo');
};
}
},
Separator,
{ label: Util.translate('electronMenuCopy'), role: 'copy' },
{ label: Util.translate('electronMenuCut'), role: 'cut' },
{ label: Util.translate('electronMenuPaste'), role: 'paste' },
{
label: Util.translate('electronMenuPastePlain'), accelerator: 'CmdOrCtrl+Shift+V',
click: () => {
if (is.macos) {
Util.send(this.win, 'commandEditor', 'pastePlain');
};
},
},
Separator,
{
label: Util.translate('electronMenuSelectAll'), accelerator: this.getAccelerator('selectAll'),
click: () => {
if (this.win) {
this.win.webContents.selectAll();
Util.send(this.win, 'commandEditor', 'selectAll');
};
}
},
{ label: Util.translate('electronMenuSearch'), accelerator: this.getAccelerator('searchText'), click: () => Util.send(this.win, 'commandGlobal', 'search') },
Separator,
{ label: Util.translate('electronMenuPrint'), accelerator: this.getAccelerator('print'), click: () => Util.send(this.win, 'commandGlobal', 'print') },
]
},
{
role: 'windowMenu', label: Util.translate('electronMenuWindow'),
submenu: [
{ label: Util.translate('electronMenuNewWindow'), accelerator: this.getAccelerator('newWindow'), click: () => WindowManager.createMain({ isChild: true }) },
Separator,
{ role: 'minimize', label: Util.translate('electronMenuMinimise') },
{ label: Util.translate('electronMenuZoomIn'), accelerator: this.getAccelerator('zoomIn'), click: () => Api.setZoom(this.win, this.win.webContents.getZoomLevel() + 1) },
{ label: Util.translate('electronMenuZoomOut'), accelerator: this.getAccelerator('zoomOut'), click: () => Api.setZoom(this.win, this.win.webContents.getZoomLevel() - 1) },
{ label: Util.translate('electronMenuZoomDefault'), accelerator: this.getAccelerator('zoomReset'), click: () => Api.setZoom(this.win, 0) },
{
label: Util.translate('electronMenuFullscreen'), accelerator: this.getAccelerator('toggleFullscreen'), type: 'checkbox', checked: this.win.isFullScreen(),
click: () => this.win.setFullScreen(!this.win.isFullScreen())
},
{ label: Util.translate('electronMenuReload'), accelerator: 'CmdOrCtrl+R', click: () => this.win.reload() }
]
},
{
label: Util.translate('electronMenuHelp'),
submenu: [
{
label: `${Util.translate('electronMenuReleaseNotes')} (${app.getVersion()})`,
click: () => Util.send(this.win, 'popup', 'help', { data: { document: 'whatsNew' } })
},
{
label: Util.translate('electronMenuShortcuts'), accelerator: this.getAccelerator('shortcut'),
click: () => Util.send(this.win, 'commandGlobal', 'shortcut')
},
Separator,
{ label: Util.translate('electronMenuGallery'), click: () => Util.send(this.win, 'commandGlobal', 'gallery') },
{ label: Util.translate('electronMenuCommunity'), click: () => Util.send(this.win, 'commandGlobal', 'community') },
{ label: Util.translate('electronMenuTutorial'), click: () => Util.send(this.win, 'commandGlobal', 'tutorial') },
{ label: Util.translate('electronMenuContact'), click: () => Util.send(this.win, 'commandGlobal', 'contact') },
{ label: Util.translate('electronMenuTech'), click: () => Util.send(this.win, 'commandGlobal', 'tech') },
Separator,
{ label: Util.translate('electronMenuTerms'), click: () => Util.send(this.win, 'commandGlobal', 'terms') },
{ label: Util.translate('electronMenuPrivacy'), click: () => Util.send(this.win, 'commandGlobal', 'privacy') },
]
},
];
const flags = {
ui: Util.translate('electronMenuFlagInterface'),
hiddenObject: Util.translate('electronMenuFlagHidden'),
analytics: Util.translate('electronMenuFlagAnalytics'),
};
const flagsMw = {
request: Util.translate('electronMenuFlagMwRequest'),
event: Util.translate('electronMenuFlagMwEvent'),
sync: Util.translate('electronMenuFlagMwSync'),
file: Util.translate('electronMenuFlagMwFile'),
time: Util.translate('electronMenuFlagMwTime'),
json: Util.translate('electronMenuFlagMwJson'),
};
const flagMenu = [];
const flagMwMenu = [];
for (const i in flags) {
flagMenu.push({
label: flags[i], type: 'checkbox', checked: config.debug[i],
click: () => {
config.debug[i] = !config.debug[i];
Api.setConfig(this.win, { debug: config.debug });
if ([ 'hiddenObject' ].includes(i)) {
this.win.reload();
};
}
});
};
for (const i in flagsMw) {
flagMwMenu.push({
label: flagsMw[i], type: 'checkbox', checked: config.flagsMw[i],
click: () => {
config.flagsMw[i] = !config.flagsMw[i];
Api.setConfig(this.win, config);
}
});
};
flagMenu.push(Separator);
flagMenu.push({
label: Util.translate('electronMenuFlagMw'),
submenu: flagMwMenu,
});
menuParam.push({
label: Util.translate('electronMenuDebug'),
submenu: [
{ label: Util.translate('electronMenuFlags'), submenu: flagMenu },
Separator,
{ label: Util.translate('electronMenuDebugSpace'), click: () => Util.send(this.win, 'commandGlobal', 'debugSpace') },
{ label: Util.translate('electronMenuDebugObject'), click: () => Util.send(this.win, 'commandGlobal', 'debugTree') },
{ label: Util.translate('electronMenuDebugProcess'), click: () => Util.send(this.win, 'commandGlobal', 'debugProcess') },
{ label: Util.translate('electronMenuDebugStat'), click: () => Util.send(this.win, 'commandGlobal', 'debugStat') },
{ label: Util.translate('electronMenuDebugReconcile'), click: () => Util.send(this.win, 'commandGlobal', 'debugReconcile') },
{ label: Util.translate('electronMenuDebugNet'), click: () => Util.send(this.win, 'commandGlobal', 'debugNet') },
{ label: Util.translate('electronMenuDebugLog'), click: () => Util.send(this.win, 'commandGlobal', 'debugLog') },
{ label: Util.translate('electronMenuDebugProfiler'), click: () => Util.send(this.win, 'commandGlobal', 'debugProfiler') },
Separator,
{ label: Util.translate('electronMenuDevTools'), accelerator: 'Alt+CmdOrCtrl+I', click: () => this.win.toggleDevTools() },
]
});
const channels = ConfigManager.getChannels().map(it => {
it.click = () => {
if (!UpdateManager.isUpdating) {
Util.send(this.win, 'commandGlobal', 'releaseChannel', it.id);
};
};
return it;
});
if (channels.length > 1) {
menuParam.push({ label: Util.translate('electronMenuVersion'), submenu: channels });
};
const menuSudo = {
label: 'Sudo',
submenu: [
Separator,
{
label: 'Experimental features', type: 'checkbox', checked: config.experimental,
click: () => {
Api.setConfig(this.win, { experimental: !config.experimental });
this.win.reload();
}
},
Separator,
{
label: 'Test payments', type: 'checkbox', checked: config.testPayment,
click: () => {
Api.setConfig(this.win, { testPayment: !config.testPayment });
this.win.reload();
}
},
Separator,
{ label: 'Export templates', click: () => Util.send(this.win, 'commandGlobal', 'exportTemplates') },
{ label: 'Export objects', click: () => Util.send(this.win, 'commandGlobal', 'exportObjects') },
{ label: 'Export localstore', click: () => Util.send(this.win, 'commandGlobal', 'exportLocalstore') },
Separator,
{ label: 'Reset onboarding', click: () => Util.send(this.win, 'commandGlobal', 'resetOnboarding') },
{ label: 'Read all messages', click: () => Util.send(this.win, 'commandGlobal', 'readAllMessages') },
Separator,
{ label: 'Relaunch', click: () => Api.exit(this.win, true) },
]
};
if (config.sudo) {
menuParam.push(menuSudo);
};
this.menu = Menu.buildFromTemplate(menuParam);
Menu.setApplicationMenu(this.menu);
};
initTray () {
const { config } = ConfigManager;
const WindowManager = require('./window.js');
const Api = require('./api.js');
const UpdateManager = require('./update.js');
const isAllowedUpdate = UpdateManager.isAllowed();
this.destroy();
if (config.hideTray) {
return;
};
const icon = this.getTrayIcon();
this.tray = new Tray (icon);
this.tray.setToolTip('Anytype');
this.tray.setContextMenu(Menu.buildFromTemplate([
{ label: Util.translate('electronMenuOpenApp'), click: () => this.winShow() },
Separator,
{ label: Util.translate('electronMenuNewWindow'), accelerator: this.getAccelerator('newWindow'), click: () => WindowManager.createMain({ isChild: true }) },
Separator,
{ label: Util.translate('electronMenuCheckUpdates'), click: () => { this.winShow(); Api.updateCheck(this.win); }, visible: isAllowedUpdate },
{ label: Util.translate('commonSettings'), submenu: this.menuSettings() },
Separator,
{ label: Util.translate('electronMenuQuit'), click: () => { this.winHide(); Api.exit(this.win, '', false); } },
]));
// Force on top and focus because in some case Electron fail with this.winShow()
this.tray.on('double-click', () => {
if (this.win && !this.win.isDestroyed()) {
this.win.setAlwaysOnTop(true);
this.winShow();
this.win.setAlwaysOnTop(false);
};
});
};
winShow () {
if (this.win && !this.win.isDestroyed()) {
this.win.show();
};
};
winHide () {
if (this.win && !this.win.isDestroyed()) {
this.win.hide();
};
};
menuSettings () {
const { config } = ConfigManager;
const Api = require('./api.js');
const Locale = require('../../dist/lib/json/locale.json');
const lang = Util.getLang();
const langMenu = [];
for (const key of Util.enabledLangs()) {
langMenu.push({
label: Locale[key], type: 'checkbox', checked: key == lang,
click: () => Util.send(this.win, 'commandGlobal', 'interfaceLang', key)
});
};
return [
{
label: Util.translate('electronMenuAccountSettings'), click: () => {
this.winShow();
this.openSettings('');
}
},
{
label: Util.translate('electronMenuSpaceSettings'), click: () => {
this.winShow();
this.openSettings('spaceIndex');
}
},
Separator,
{
label: Util.translate('electronMenuImport'), click: () => {
this.winShow();
this.openSettings('importIndex');
}
},
{
label: Util.translate('electronMenuExport'), click: () => {
this.winShow();
this.openSettings('exportIndex');
}
},
{ label: Util.translate('electronMenuLanguage'), submenu: langMenu },
Separator,
{
label: Util.translate('electronMenuShowTray'), type: 'checkbox', checked: !config.hideTray, click: () => {
Api.setConfig(this.win, { hideTray: !config.hideTray });
this.initTray();
}
},
(is.windows || is.linux) ? {
label: Util.translate('electronMenuShowMenu'), type: 'checkbox', checked: config.showMenuBar, click: () => {
const { config } = ConfigManager;
Api.setMenuBarVisibility(this.win, !config.showMenuBar);
this.initTray();
}
} : null,
Separator,
{
label: Util.translate('commonNewObject'), accelerator:this.getAccelerator('createObject'), click: () => {
this.winShow();
Util.send(this.win, 'commandGlobal', 'createObject');
}
},
].filter(it => it);
};
openSettings (page) {
const Api = require('./api.js');
if (Api.isPinChecked) {
Util.send(this.win, 'route', '/main/settings/' + page);
};
};
updateTrayIcon () {
if (this.tray && this.tray.setImage) {
const icon = this.getTrayIcon();
if (icon) {
this.tray.setImage(icon);
};
};
};
getTrayIcon () {
let icon = '';
if (is.windows) {
icon = path.join('icons', '32x32.png');
} else
if (is.linux) {
const env = process.env.ORIGINAL_XDG_CURRENT_DESKTOP;
const panelAlwaysDark = env.includes('GNOME') || (env == 'Unity'); // for GNOME shell env, including ubuntu -- the panel is always dark
if (panelAlwaysDark) {
icon = 'iconTrayWhite.png';
} else
if (Util.getTheme() == 'dark') {
icon = 'iconTrayWhite.png';
} else {
icon = 'iconTrayBlack.png';
};
} else
if (is.macos) {
icon = `iconTrayTemplate.png`;
};
return icon ? path.join(Util.imagePath(), icon) : '';
};
destroy () {
if (this.tray && !this.tray.isDestroyed()) {
this.tray.destroy();
this.tray = null;
};
};
};
module.exports = new MenuManager();