mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 09:37:28 +00:00
* 💄 style(topic): darken project group folder label in sidebar Previous `type='secondary'` on the group title was too faint against the sidebar background; promote the text to default color for better legibility and keep the folder icon at tertiary so it stays subtle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(topic): use colorTextSecondary for project group title Text's `type='secondary'` resolves to a lighter token than `colorTextSecondary`; apply `colorTextSecondary` directly so the title lands at the intended shade (darker than before, lighter than default). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(electron): show blue unread dot on tab when agent has unread badge Mirror the sidebar agent unread badge on the corresponding browser-like tab as a subtle blue dot, so unread completions are visible even when the sidebar is out of view. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(electron): forward proxy env vars to spawned agent CLI The main-process undici dispatcher set by ProxyDispatcherManager only covers in-process requests — child processes like claude-code CLI never saw the user's proxy config. Extract a shared `buildProxyEnv` so any CLI spawn can merge HTTP(S)_PROXY / ALL_PROXY / NO_PROXY into its env. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(electron): close active tab on Cmd+W when multiple tabs are open Cmd/Ctrl+W now closes the focused tab first and only closes the window when a single tab (or none) remains. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(electron): add Cmd+T shortcut to open a new tab Reuses the active tab's plugin context to create a same-type tab, mirroring the TabBar + button behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(electron): use container color for active tab background Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(electron): update Close menu item expectations for smart Cmd+W Tests now assert the CmdOrCtrl+W accelerator and click handler instead of the legacy role: 'close'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(electron): drop const/store import from HeterogeneousAgentCtr The controller previously pulled defaultProxySettings from @/const/store, which chain-loads @/modules/updater/configs and electron-is — that breaks any unit test that mocks `electron` without a full app shim. Make buildProxyEnv accept undefined and read the store value directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
688 lines
22 KiB
TypeScript
688 lines
22 KiB
TypeScript
import * as path from 'node:path';
|
|
|
|
import type { MenuItemConstructorOptions } from 'electron';
|
|
import { app, BrowserWindow, clipboard, Menu, shell } from 'electron';
|
|
|
|
import { isDev } from '@/const/env';
|
|
import NotificationCtr from '@/controllers/NotificationCtr';
|
|
import SystemController from '@/controllers/SystemCtr';
|
|
|
|
import type { ContextMenuData, IMenuPlatform, MenuOptions } from '../types';
|
|
import { BaseMenuPlatform } from './BaseMenuPlatform';
|
|
|
|
export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|
private appMenu: Menu | null = null;
|
|
private trayMenu: Menu | null = null;
|
|
|
|
buildAndSetAppMenu(options?: MenuOptions): Menu {
|
|
const template = this.getAppMenuTemplate(options);
|
|
|
|
this.appMenu = Menu.buildFromTemplate(template);
|
|
|
|
Menu.setApplicationMenu(this.appMenu);
|
|
|
|
return this.appMenu;
|
|
}
|
|
|
|
buildContextMenu(type: string, data?: ContextMenuData): Menu {
|
|
let template: MenuItemConstructorOptions[];
|
|
switch (type) {
|
|
case 'chat': {
|
|
template = this.getChatContextMenuTemplate(data);
|
|
break;
|
|
}
|
|
case 'editor': {
|
|
template = this.getEditorContextMenuTemplate(data);
|
|
break;
|
|
}
|
|
default: {
|
|
template = this.getDefaultContextMenuTemplate(data);
|
|
}
|
|
}
|
|
return Menu.buildFromTemplate(template);
|
|
}
|
|
|
|
buildTrayMenu(): Menu {
|
|
const template = this.getTrayMenuTemplate();
|
|
this.trayMenu = Menu.buildFromTemplate(template);
|
|
return this.trayMenu;
|
|
}
|
|
|
|
refresh(options?: MenuOptions): void {
|
|
// Rebuild Application menu
|
|
this.buildAndSetAppMenu(options);
|
|
// If tray menu exists, rebuild it as well (if dynamic update is needed)
|
|
// this.trayMenu = this.buildTrayMenu();
|
|
// Need to consider how to update the menu for existing tray icons
|
|
}
|
|
|
|
// --- Private methods: define menu templates and logic ---
|
|
|
|
private getAppMenuTemplate(options?: MenuOptions): MenuItemConstructorOptions[] {
|
|
const appName = app.getName();
|
|
const showDev = isDev || options?.showDevItems;
|
|
// Create namespaced translation function
|
|
const t = this.app.i18n.ns('menu');
|
|
|
|
// Add debug logging
|
|
// console.log('[MacOSMenu] Menu rendering, i18n instance:', !!this.app.i18n);
|
|
|
|
const template: MenuItemConstructorOptions[] = [
|
|
{
|
|
label: appName,
|
|
submenu: [
|
|
{
|
|
click: async () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.show();
|
|
mainWindow.broadcast('navigate', { path: '/settings/about' });
|
|
},
|
|
label: t('macOS.about', { appName }),
|
|
},
|
|
this.getUpdateMenuItem(t),
|
|
{ type: 'separator' },
|
|
{
|
|
accelerator: 'Command+,',
|
|
click: async () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.show();
|
|
mainWindow.broadcast('navigate', { path: '/settings' });
|
|
},
|
|
label: t('macOS.preferences'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: t('macOS.services'),
|
|
role: 'services',
|
|
submenu: [],
|
|
},
|
|
{ type: 'separator' },
|
|
{ label: t('macOS.hide', { appName }), role: 'hide' },
|
|
{ label: t('macOS.hideOthers'), role: 'hideOthers' },
|
|
{ label: t('macOS.unhide'), role: 'unhide' },
|
|
{ type: 'separator' },
|
|
{ label: t('file.quit'), role: 'quit' },
|
|
],
|
|
},
|
|
{
|
|
label: t('file.title'),
|
|
submenu: [
|
|
{
|
|
accelerator: 'Command+N',
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.show();
|
|
mainWindow.broadcast('createNewTopic');
|
|
},
|
|
label: t('file.newTopic'),
|
|
},
|
|
{
|
|
accelerator: 'Command+T',
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.show();
|
|
mainWindow.broadcast('createNewTab');
|
|
},
|
|
label: t('file.newTab'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
accelerator: 'Alt+Command+A',
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.show();
|
|
mainWindow.broadcast('createNewAgent');
|
|
},
|
|
label: t('file.newAgent'),
|
|
},
|
|
{
|
|
accelerator: 'Alt+Command+G',
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.show();
|
|
mainWindow.broadcast('createNewAgentGroup');
|
|
},
|
|
label: t('file.newAgentGroup'),
|
|
},
|
|
{
|
|
accelerator: 'Alt+Command+P',
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.show();
|
|
mainWindow.broadcast('createNewPage');
|
|
},
|
|
label: t('file.newPage'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
accelerator: 'CmdOrCtrl+W',
|
|
click: () => {
|
|
const focused = BrowserWindow.getFocusedWindow();
|
|
if (!focused) return;
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
if (focused === mainWindow.browserWindow) {
|
|
mainWindow.broadcast('closeCurrentTabOrWindow');
|
|
} else {
|
|
focused.close();
|
|
}
|
|
},
|
|
label: t('window.close'),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: t('edit.title'),
|
|
submenu: [
|
|
{ label: t('edit.undo'), role: 'undo' },
|
|
{ label: t('edit.redo'), role: 'redo' },
|
|
{ type: 'separator' },
|
|
{ label: t('edit.cut'), role: 'cut' },
|
|
{ label: t('edit.copy'), role: 'copy' },
|
|
{ label: t('edit.paste'), role: 'paste' },
|
|
{ type: 'separator' },
|
|
{
|
|
label: t('edit.speech'),
|
|
submenu: [
|
|
{ label: t('edit.startSpeaking'), role: 'startSpeaking' },
|
|
{ label: t('edit.stopSpeaking'), role: 'stopSpeaking' },
|
|
],
|
|
},
|
|
{ type: 'separator' },
|
|
{ label: t('edit.selectAll'), role: 'selectAll' },
|
|
],
|
|
},
|
|
{
|
|
label: t('view.title'),
|
|
submenu: [
|
|
{ label: t('view.reload'), role: 'reload' },
|
|
{ label: t('view.forceReload'), role: 'forceReload' },
|
|
{ accelerator: 'F12', label: t('dev.devTools'), role: 'toggleDevTools' },
|
|
{ type: 'separator' },
|
|
{ label: t('view.resetZoom'), role: 'resetZoom' },
|
|
{ label: t('view.zoomIn'), role: 'zoomIn' },
|
|
{ label: t('view.zoomOut'), role: 'zoomOut' },
|
|
{ type: 'separator' },
|
|
{ accelerator: 'F11', label: t('view.toggleFullscreen'), role: 'togglefullscreen' },
|
|
],
|
|
},
|
|
{
|
|
label: t('history.title'),
|
|
submenu: [
|
|
{
|
|
accelerator: 'Command+[',
|
|
acceleratorWorksWhenHidden: true,
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.broadcast('historyGoBack');
|
|
},
|
|
label: t('history.back'),
|
|
},
|
|
{
|
|
accelerator: 'Command+]',
|
|
acceleratorWorksWhenHidden: true,
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.broadcast('historyGoForward');
|
|
},
|
|
label: t('history.forward'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
accelerator: 'Shift+Command+H',
|
|
acceleratorWorksWhenHidden: true,
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.broadcast('navigate', { path: '/' });
|
|
},
|
|
label: t('history.home'),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: t('window.title'),
|
|
role: 'windowMenu',
|
|
},
|
|
{
|
|
label: t('help.title'),
|
|
role: 'help',
|
|
submenu: [
|
|
{
|
|
click: async () => {
|
|
await shell.openExternal('https://lobehub.com');
|
|
},
|
|
label: t('help.visitWebsite'),
|
|
},
|
|
{
|
|
click: async () => {
|
|
await shell.openExternal('https://github.com/lobehub/lobe-chat');
|
|
},
|
|
label: t('help.githubRepo'),
|
|
},
|
|
{
|
|
click: async () => {
|
|
await shell.openExternal('https://github.com/lobehub/lobe-chat/issues/new/choose');
|
|
},
|
|
label: t('help.reportIssue'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
click: () => {
|
|
const logsPath = app.getPath('logs');
|
|
console.info(`[Menu] Opening logs directory: ${logsPath}`);
|
|
shell.openPath(logsPath).catch((err) => {
|
|
console.error(`[Menu] Error opening path ${logsPath}:`, err);
|
|
// Optionally show an error dialog to the user
|
|
});
|
|
},
|
|
label: t('help.openLogsDir'),
|
|
},
|
|
{
|
|
click: () => {
|
|
const userDataPath = app.getPath('userData');
|
|
console.info(`[Menu] Opening user data directory: ${userDataPath}`);
|
|
shell.openPath(userDataPath).catch((err) => {
|
|
console.error(`[Menu] Error opening path ${userDataPath}:`, err);
|
|
// Optionally show an error dialog to the user
|
|
});
|
|
},
|
|
label: t('help.openConfigDir'),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
if (showDev) {
|
|
template.push({
|
|
label: t('dev.title'),
|
|
submenu: [
|
|
{
|
|
click: () => {
|
|
this.app.browserManager.retrieveByIdentifier('devtools').show();
|
|
},
|
|
label: t('dev.devPanel'),
|
|
},
|
|
{
|
|
click: () => {
|
|
this.app.menuManager.rebuildAppMenu();
|
|
},
|
|
label: t('dev.refreshMenu'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: t('dev.permissions.title'),
|
|
submenu: [
|
|
{
|
|
click: () => {
|
|
const notificationCtr = this.app.getController(NotificationCtr);
|
|
void notificationCtr.requestNotificationPermission();
|
|
},
|
|
label: t('dev.permissions.notification.request'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
click: () => {
|
|
const systemCtr = this.app.getController(SystemController);
|
|
void systemCtr.requestAccessibilityAccess();
|
|
},
|
|
label: t('dev.permissions.accessibility.request'),
|
|
},
|
|
{
|
|
click: () => {
|
|
const systemCtr = this.app.getController(SystemController);
|
|
void systemCtr.requestMicrophoneAccess();
|
|
},
|
|
label: t('dev.permissions.microphone.request'),
|
|
},
|
|
{
|
|
click: () => {
|
|
const systemCtr = this.app.getController(SystemController);
|
|
void systemCtr.requestScreenAccess();
|
|
},
|
|
label: t('dev.permissions.screen.request'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
click: () => {
|
|
const systemCtr = this.app.getController(SystemController);
|
|
void systemCtr.promptFullDiskAccessIfNotGranted();
|
|
},
|
|
label: t('dev.permissions.fullDisk.request'),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
click: () => {
|
|
const userDataPath = app.getPath('userData');
|
|
shell.openPath(userDataPath).catch((err) => {
|
|
console.error(`[Menu] Error opening path ${userDataPath}:`, err);
|
|
});
|
|
},
|
|
label: t('dev.openUserDataDir'),
|
|
},
|
|
{
|
|
click: () => {
|
|
// @ts-expect-error cache directory seems to be temporarily missing from type definitions
|
|
const cachePath = app.getPath('cache');
|
|
|
|
const updaterCachePath = path.join(cachePath, `${app.getName()}-updater`);
|
|
shell.openPath(updaterCachePath).catch((err) => {
|
|
console.error(`[Menu] Error opening path ${updaterCachePath}:`, err);
|
|
});
|
|
},
|
|
label: t('dev.openUpdaterCacheDir'),
|
|
},
|
|
{
|
|
click: () => {
|
|
this.app.storeManager.openInEditor();
|
|
},
|
|
label: t('dev.openSettingsFile'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: t('dev.updaterSimulation'),
|
|
submenu: [
|
|
{
|
|
click: () => {
|
|
this.app.updaterManager.simulateUpdateAvailable();
|
|
},
|
|
label: t('dev.simulateAutoDownload'),
|
|
},
|
|
{
|
|
click: () => {
|
|
this.app.updaterManager.simulateDownloadProgress();
|
|
},
|
|
label: t('dev.simulateDownloadProgress'),
|
|
},
|
|
{
|
|
click: () => {
|
|
this.app.updaterManager.simulateUpdateDownloaded();
|
|
},
|
|
label: t('dev.simulateDownloadComplete'),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
return template;
|
|
}
|
|
|
|
private getUpdateMenuItem(t: (key: string, opts?: any) => string): MenuItemConstructorOptions {
|
|
const { stage } = this.app.updaterManager.getUpdaterState();
|
|
|
|
switch (stage) {
|
|
case 'checking': {
|
|
return { enabled: false, label: t('common.checkingUpdates') };
|
|
}
|
|
case 'downloading': {
|
|
return { enabled: false, label: t('common.downloadingUpdate') };
|
|
}
|
|
case 'downloaded': {
|
|
return {
|
|
click: () => this.app.updaterManager.installNow(),
|
|
label: t('common.restartToUpdate'),
|
|
};
|
|
}
|
|
case 'latest': {
|
|
return { enabled: false, label: t('common.isLatestVersion') };
|
|
}
|
|
default: {
|
|
return {
|
|
click: () => this.app.updaterManager.checkForUpdates({ manual: true }),
|
|
label: t('common.checkUpdates'),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
private getDefaultContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
|
const t = this.app.i18n.ns('menu');
|
|
const hasText = Boolean(data?.selectionText?.trim());
|
|
const hasLink = Boolean(data?.linkURL);
|
|
const hasImage = data?.mediaType === 'image' && Boolean(data?.srcURL);
|
|
|
|
const template: MenuItemConstructorOptions[] = [];
|
|
|
|
// Look Up (macOS only) - only when text is selected
|
|
if (hasText) {
|
|
template.push({
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.webContents.showDefinitionForSelection();
|
|
},
|
|
label: t('edit.lookUp'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Search with Google - only when text is selected
|
|
if (hasText) {
|
|
template.push({
|
|
click: () => {
|
|
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
|
shell.openExternal(searchUrl);
|
|
},
|
|
label: t('context.searchWithGoogle'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Link actions
|
|
if (hasLink) {
|
|
template.push({
|
|
click: () => shell.openExternal(data!.linkURL!),
|
|
label: t('context.openLink'),
|
|
});
|
|
template.push({
|
|
click: () => clipboard.writeText(data!.linkURL!),
|
|
label: t('context.copyLink'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Image actions
|
|
if (hasImage) {
|
|
template.push({
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.webContents.downloadURL(data!.srcURL!);
|
|
},
|
|
label: t('context.saveImage'),
|
|
});
|
|
template.push({
|
|
click: () => {
|
|
clipboard.writeText(data!.srcURL!);
|
|
},
|
|
label: t('context.copyImageAddress'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Standard edit actions
|
|
template.push(
|
|
{ label: t('edit.cut'), role: 'cut' },
|
|
{ label: t('edit.copy'), role: 'copy' },
|
|
{ label: t('edit.paste'), role: 'paste' },
|
|
{ label: t('edit.selectAll'), role: 'selectAll' },
|
|
);
|
|
|
|
// Inspect Element in dev mode
|
|
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
|
template.push({ type: 'separator' });
|
|
template.push({
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
|
},
|
|
label: t('context.inspectElement'),
|
|
});
|
|
}
|
|
|
|
return template;
|
|
}
|
|
|
|
private getChatContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
|
const t = this.app.i18n.ns('menu');
|
|
const hasText = Boolean(data?.selectionText?.trim());
|
|
const hasLink = Boolean(data?.linkURL);
|
|
const hasImage = data?.mediaType === 'image' && Boolean(data?.srcURL);
|
|
|
|
const template: MenuItemConstructorOptions[] = [];
|
|
|
|
// Look Up (macOS only) - only when text is selected
|
|
if (hasText) {
|
|
template.push({
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.webContents.showDefinitionForSelection();
|
|
},
|
|
label: t('edit.lookUp'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Search with Google - only when text is selected
|
|
if (hasText) {
|
|
template.push({
|
|
click: () => {
|
|
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
|
shell.openExternal(searchUrl);
|
|
},
|
|
label: t('context.searchWithGoogle'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Link actions
|
|
if (hasLink) {
|
|
template.push({
|
|
click: () => shell.openExternal(data!.linkURL!),
|
|
label: t('context.openLink'),
|
|
});
|
|
template.push({
|
|
click: () => clipboard.writeText(data!.linkURL!),
|
|
label: t('context.copyLink'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Image actions
|
|
if (hasImage) {
|
|
template.push({
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.webContents.downloadURL(data!.srcURL!);
|
|
},
|
|
label: t('context.saveImage'),
|
|
});
|
|
template.push({
|
|
click: () => {
|
|
clipboard.writeText(data!.srcURL!);
|
|
},
|
|
label: t('context.copyImageAddress'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Standard edit actions for chat (copy/paste focused)
|
|
template.push(
|
|
{ label: t('edit.copy'), role: 'copy' },
|
|
{ label: t('edit.paste'), role: 'paste' },
|
|
{ type: 'separator' },
|
|
{ label: t('edit.selectAll'), role: 'selectAll' },
|
|
);
|
|
|
|
// Inspect Element in dev mode
|
|
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
|
template.push({ type: 'separator' });
|
|
template.push({
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
|
},
|
|
label: t('context.inspectElement'),
|
|
});
|
|
}
|
|
|
|
return template;
|
|
}
|
|
|
|
private getEditorContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
|
const t = this.app.i18n.ns('menu');
|
|
const hasText = Boolean(data?.selectionText?.trim());
|
|
|
|
const template: MenuItemConstructorOptions[] = [];
|
|
|
|
// Look Up (macOS only) - only when text is selected
|
|
if (hasText) {
|
|
template.push({
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.webContents.showDefinitionForSelection();
|
|
},
|
|
label: t('edit.lookUp'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Search with Google - only when text is selected
|
|
if (hasText) {
|
|
template.push({
|
|
click: () => {
|
|
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
|
shell.openExternal(searchUrl);
|
|
},
|
|
label: t('context.searchWithGoogle'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Standard edit actions for editor (full edit capabilities)
|
|
template.push(
|
|
{ label: t('edit.cut'), role: 'cut' },
|
|
{ label: t('edit.copy'), role: 'copy' },
|
|
{ label: t('edit.paste'), role: 'paste' },
|
|
{ type: 'separator' },
|
|
{ label: t('edit.selectAll'), role: 'selectAll' },
|
|
{ type: 'separator' },
|
|
{ label: t('edit.delete'), role: 'delete' },
|
|
);
|
|
|
|
// Inspect Element in dev mode
|
|
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
|
template.push({ type: 'separator' });
|
|
template.push({
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
|
},
|
|
label: t('context.inspectElement'),
|
|
});
|
|
}
|
|
|
|
return template;
|
|
}
|
|
|
|
private getTrayMenuTemplate(): MenuItemConstructorOptions[] {
|
|
const t = this.app.i18n.ns('menu');
|
|
const appName = app.getName();
|
|
|
|
return [
|
|
{
|
|
click: () => this.app.browserManager.showMainWindow(),
|
|
label: t('tray.show', { appName }),
|
|
},
|
|
{
|
|
click: async () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.show();
|
|
mainWindow.broadcast('navigate', { path: '/settings' });
|
|
},
|
|
label: t('file.preferences'),
|
|
},
|
|
{ type: 'separator' },
|
|
{ label: t('tray.quit'), role: 'quit' },
|
|
];
|
|
}
|
|
}
|