merge: integrate upstream main with local Makelore changes
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron';
|
||||
import { registerHostApiProxyHandlers } from './ipc/host-api-proxy';
|
||||
import { registerTranscriptExportHandler } from './ipc/transcript-export';
|
||||
import { registerConversationLinkContextMenuHandler } from './ipc/conversation-link-context-menu';
|
||||
import { applyProxySettings } from './proxy';
|
||||
import { syncLaunchAtStartupSettingFromStore } from './launch-at-startup';
|
||||
import { getAllSettings, getSetting, resetSettings, setSetting, type AppSettings } from '../utils/store';
|
||||
@@ -240,6 +241,7 @@ export function registerIpcHandlers(
|
||||
): void {
|
||||
registerHostApiProxyHandlers(hostApiContext);
|
||||
registerTranscriptExportHandler(mainWindow);
|
||||
registerConversationLinkContextMenuHandler(mainWindow);
|
||||
const rendererLeases = new Set<string>();
|
||||
const releaseRendererLeases = (): void => {
|
||||
if (rendererLeases.size === 0) return;
|
||||
|
||||
134
electron/main/ipc/conversation-link-context-menu.ts
Normal file
134
electron/main/ipc/conversation-link-context-menu.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
clipboard,
|
||||
ipcMain,
|
||||
Menu,
|
||||
shell,
|
||||
type MenuItemConstructorOptions,
|
||||
} from 'electron';
|
||||
|
||||
const MAX_LINK_LENGTH = 4_096;
|
||||
const LOCAL_WEB_FILE_PATTERN = /\.(?:html?|xhtml)$/i;
|
||||
|
||||
export type ConversationLinkContextMenuRequest =
|
||||
| { kind: 'external'; target: string }
|
||||
| { kind: 'local-web'; target: string };
|
||||
|
||||
export interface ConversationLinkMenuActions {
|
||||
platform: NodeJS.Platform;
|
||||
homePath: string;
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
openPath: (path: string) => Promise<string>;
|
||||
showItemInFolder: (path: string) => void;
|
||||
writeText: (value: string) => void;
|
||||
}
|
||||
|
||||
function isBoundedSingleLine(value: string): boolean {
|
||||
return value.length > 0
|
||||
&& value.length <= MAX_LINK_LENGTH
|
||||
&& !/[\0\r\n]/.test(value);
|
||||
}
|
||||
|
||||
function safeExternalTarget(value: unknown): string | null {
|
||||
const candidate = typeof value === 'string' ? value.trim() : '';
|
||||
if (!isBoundedSingleLine(candidate)) return null;
|
||||
|
||||
try {
|
||||
const url = new URL(candidate);
|
||||
if ((url.protocol !== 'https:' && url.protocol !== 'http:')
|
||||
|| url.username
|
||||
|| url.password) {
|
||||
return null;
|
||||
}
|
||||
return url.toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveHomeRelativePath(value: string, homePath: string): string {
|
||||
if (value === '~') return homePath;
|
||||
const separator = homePath.includes('\\') ? '\\' : '/';
|
||||
const suffix = value.slice(2).replace(/[\\/]/g, separator);
|
||||
return `${homePath.replace(/[\\/]$/, '')}${separator}${suffix}`;
|
||||
}
|
||||
|
||||
function safeLocalWebTarget(value: unknown, homePath: string): string | null {
|
||||
const candidate = typeof value === 'string' ? value.trim() : '';
|
||||
if (!isBoundedSingleLine(candidate) || !LOCAL_WEB_FILE_PATTERN.test(candidate)) return null;
|
||||
|
||||
if (candidate === '~' || /^~[\\/]/.test(candidate)) {
|
||||
return resolveHomeRelativePath(candidate, homePath);
|
||||
}
|
||||
|
||||
const absolute = candidate.startsWith('/')
|
||||
|| /^[A-Za-z]:[\\/]/.test(candidate)
|
||||
|| /^\\\\[^\\]+\\[^\\]+/.test(candidate);
|
||||
return absolute ? candidate : null;
|
||||
}
|
||||
|
||||
function ignoreRejectedAction(action: Promise<unknown>): void {
|
||||
void action.catch(() => undefined);
|
||||
}
|
||||
|
||||
export function createConversationLinkMenuTemplate(
|
||||
request: ConversationLinkContextMenuRequest,
|
||||
actions: ConversationLinkMenuActions,
|
||||
): MenuItemConstructorOptions[] {
|
||||
if (request?.kind === 'external') {
|
||||
const target = safeExternalTarget(request.target);
|
||||
if (!target) throw new Error('Unsupported external URL');
|
||||
|
||||
return [
|
||||
{
|
||||
label: '用默认浏览器打开',
|
||||
click: () => ignoreRejectedAction(actions.openExternal(target)),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: '复制链接地址',
|
||||
click: () => actions.writeText(target),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (request?.kind === 'local-web') {
|
||||
const target = safeLocalWebTarget(request.target, actions.homePath);
|
||||
if (!target) throw new Error('Unsupported local web file');
|
||||
|
||||
return [
|
||||
{
|
||||
label: '用默认浏览器打开',
|
||||
click: () => ignoreRejectedAction(actions.openPath(target)),
|
||||
},
|
||||
{
|
||||
label: actions.platform === 'darwin' ? '在访达中显示' : '在文件夹中显示',
|
||||
click: () => actions.showItemInFolder(target),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: '复制地址',
|
||||
click: () => actions.writeText(target),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
throw new Error('Unsupported link context menu request');
|
||||
}
|
||||
|
||||
export function registerConversationLinkContextMenuHandler(mainWindow: BrowserWindow): void {
|
||||
ipcMain.handle('shell:showLinkContextMenu', (event, request: ConversationLinkContextMenuRequest) => {
|
||||
const template = createConversationLinkMenuTemplate(request, {
|
||||
platform: process.platform,
|
||||
homePath: app.getPath('home'),
|
||||
openExternal: (url) => shell.openExternal(url),
|
||||
openPath: (path) => shell.openPath(path),
|
||||
showItemInFolder: (path) => shell.showItemInFolder(path),
|
||||
writeText: (value) => clipboard.writeText(value),
|
||||
});
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
const ownerWindow = BrowserWindow.fromWebContents(event.sender) ?? mainWindow;
|
||||
menu.popup({ window: ownerWindow });
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user