@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
367 lines (339 loc) • 11.6 kB
text/typescript
import { Menu, MenuItemConstructorOptions, app, shell } from 'electron';
import * as path from 'node:path';
import { isDev } from '@/const/env';
import type { 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?: any): Menu {
let template: MenuItemConstructorOptions[];
switch (type) {
case 'chat': {
template = this.getChatContextMenuTemplate(data);
break;
}
case 'editor': {
template = this.getEditorContextMenuTemplate(data);
break;
}
default: {
template = this.getDefaultContextMenuTemplate();
}
}
return Menu.buildFromTemplate(template);
}
buildTrayMenu(): Menu {
const template = this.getTrayMenuTemplate();
this.trayMenu = Menu.buildFromTemplate(template);
return this.trayMenu;
}
refresh(options?: MenuOptions): void {
// 重建应用菜单
this.buildAndSetAppMenu(options);
// 如果托盘菜单存在,也重建它(如果需要动态更新)
// this.trayMenu = this.buildTrayMenu();
// 需要考虑如何更新现有托盘图标的菜单
}
// --- 私有方法:定义菜单模板和逻辑 ---
private getAppMenuTemplate(options?: MenuOptions): MenuItemConstructorOptions[] {
const appName = app.getName();
const showDev = isDev || options?.showDevItems;
// 创建命名空间翻译函数
const t = this.app.i18n.ns('menu');
// 添加调试日志
// console.log('[MacOSMenu] 菜单渲染, i18n实例:', !!this.app.i18n);
const template: MenuItemConstructorOptions[] = [
{
label: appName,
submenu: [
{
label: t('macOS.about', { appName }),
role: 'about',
},
{
click: () => {
this.app.updaterManager.checkForUpdates({ manual: true });
},
label: t('common.checkUpdates'),
},
{ type: 'separator' },
{
accelerator: 'Command+,',
click: () => {
this.app.browserManager.showSettingsWindow();
},
label: t('macOS.preferences'),
},
{ type: 'separator' },
{
label: t('macOS.services'),
role: 'services',
submenu: [],
},
{ type: 'separator' },
{
accelerator: 'Command+H',
label: t('macOS.hide', { appName }),
role: 'hide',
},
{
accelerator: 'Command+Alt+H',
label: t('macOS.hideOthers'),
role: 'hideOthers',
},
{
label: t('macOS.unhide'),
role: 'unhide',
},
{ type: 'separator' },
{
accelerator: 'Command+Q',
label: t('file.quit'),
role: 'quit',
},
],
},
{
label: t('file.title'),
submenu: [
{
accelerator: 'Command+W',
label: t('window.close'),
role: 'close',
},
],
},
{
label: t('edit.title'),
submenu: [
{ accelerator: 'Command+Z', label: t('edit.undo'), role: 'undo' },
{ accelerator: 'Shift+Command+Z', label: t('edit.redo'), role: 'redo' },
{ type: 'separator' },
{ accelerator: 'Command+X', label: t('edit.cut'), role: 'cut' },
{ accelerator: 'Command+C', label: t('edit.copy'), role: 'copy' },
{ accelerator: 'Command+V', 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' },
{ accelerator: 'Command+A', 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' },
{ accelerator: 'Command+0', label: t('view.resetZoom'), role: 'resetZoom' },
{ accelerator: 'Command+Plus', label: t('view.zoomIn'), role: 'zoomIn' },
{ accelerator: 'Command+-', label: t('view.zoomOut'), role: 'zoomOut' },
{ type: 'separator' },
{ accelerator: 'F11', label: t('view.toggleFullscreen'), role: 'togglefullscreen' },
],
},
{
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.log(`[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: '打开日志目录',
},
{
click: () => {
const userDataPath = app.getPath('userData');
console.log(`[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: '配置目录',
},
],
},
];
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' },
{
click: () => {
const userDataPath = app.getPath('userData');
shell.openPath(userDataPath).catch((err) => {
console.error(`[Menu] Error opening path ${userDataPath}:`, err);
});
},
label: '用户配置目录',
},
{
click: () => {
// @ts-expect-error cache 目录好像暂时不在类型定义里
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: '更新缓存目录',
},
{
click: () => {
this.app.storeManager.openInEditor();
},
label: '打开 Settings 配置文件',
},
{ type: 'separator' },
{
label: '自动更新测试模拟',
submenu: [
{
click: () => {
this.app.updaterManager.simulateUpdateAvailable();
},
label: '模拟启动后台自动下载更新(3s 下完)',
},
{
click: () => {
this.app.updaterManager.simulateDownloadProgress();
},
label: '模拟下载进度',
},
{
click: () => {
this.app.updaterManager.simulateUpdateDownloaded();
},
label: '模拟下载完成',
},
],
},
],
});
}
return template;
}
private getDefaultContextMenuTemplate(): MenuItemConstructorOptions[] {
const t = this.app.i18n.ns('menu');
return [
{ label: t('edit.cut'), role: 'cut' },
{ label: t('edit.copy'), role: 'copy' },
{ label: t('edit.paste'), role: 'paste' },
{ label: t('edit.selectAll'), role: 'selectAll' },
{ type: 'separator' },
];
}
private getChatContextMenuTemplate(data?: any): MenuItemConstructorOptions[] {
const t = this.app.i18n.ns('menu');
const commonT = this.app.i18n.ns('common');
const items: MenuItemConstructorOptions[] = [
{ 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' },
];
if (data?.messageId) {
items.push(
{ type: 'separator' },
{
click: () => {
console.log('尝试删除消息:', data.messageId);
// 调用 MessageService (假设存在)
// const messageService = this.app.getService(MessageService);
// messageService?.deleteMessage(data.messageId);
},
label: commonT('actions.delete'),
},
);
}
return items;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private getEditorContextMenuTemplate(_data?: any): MenuItemConstructorOptions[] {
const t = this.app.i18n.ns('menu');
// 编辑器特定的上下文菜单
return [
{ label: t('edit.cut'), role: 'cut' },
{ label: t('edit.copy'), role: 'copy' },
{ label: t('edit.paste'), role: 'paste' },
{ type: 'separator' },
{ label: t('edit.undo'), role: 'undo' },
{ label: t('edit.redo'), role: 'redo' },
{ type: 'separator' },
{ label: t('edit.selectAll'), role: 'selectAll' },
];
}
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: () => this.app.browserManager.retrieveByIdentifier('settings').show(),
label: t('file.preferences'),
},
{ type: 'separator' },
{ label: t('tray.quit'), role: 'quit' },
];
}
}