mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-21 21:31:12 +02:00
Three capability upgrades to the embedded browser pane: - Screen share: getDisplayMedia() in the browser partition now shows a Chrome-style source picker (screens/windows with thumbnails, optional system audio for screens) instead of silently capturing the primary screen. Requests are denied when the pane is hidden. The app's own session keeps its auto-loopback handler for meeting transcription. - Notifications: the browser partition grants the notifications permission so sites (WhatsApp Web, Gmail, ...) can post native OS notifications while loaded. Background Web Push remains unavailable. - Extensions: electron-chrome-extensions wired to the browser session. Unpacked extensions load from ~/.rowboat/extensions/<name>/; tab lifecycle is mirrored into chrome.tabs, and a <browser-action-list> toolbar hosts action icons/popups next to the address bar. NOTE: the library is GPL-3.0/Patron dual-licensed — licensing decision required before shipping in a release build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
import { contextBridge, ipcRenderer, webFrame, webUtils } from 'electron';
|
|
import { injectBrowserAction } from 'electron-chrome-extensions/browser-action';
|
|
import { ipc as ipcShared } from '@x/shared';
|
|
|
|
// Expose the <browser-action-list> custom element (extension action icons +
|
|
// popups for the embedded browser pane). App documents only — this preload
|
|
// is attached solely to the app window, but guard against it ever being
|
|
// reused for remote content.
|
|
if (location.protocol === 'app:' || location.origin === 'http://localhost:5173') {
|
|
try {
|
|
injectBrowserAction();
|
|
} catch (error) {
|
|
console.error('[preload] injectBrowserAction failed:', error);
|
|
}
|
|
}
|
|
|
|
type InvokeChannels = ipcShared.InvokeChannels;
|
|
type IPCChannels = ipcShared.IPCChannels;
|
|
type SendChannels = ipcShared.SendChannels;
|
|
const { validateRequest } = ipcShared;
|
|
|
|
const ipc = {
|
|
/**
|
|
* Invoke a channel that expects a response (request/response pattern)
|
|
* Only channels with non-null responses can be invoked
|
|
*/
|
|
invoke<K extends InvokeChannels>(
|
|
channel: K,
|
|
args: IPCChannels[K]['req']
|
|
): Promise<IPCChannels[K]['res']> {
|
|
// Runtime validation of request payload
|
|
const validatedArgs = validateRequest(channel, args);
|
|
return ipcRenderer.invoke(channel, validatedArgs);
|
|
},
|
|
|
|
/**
|
|
* Send a message to a channel without expecting a response (fire-and-forget)
|
|
* Only channels with null responses can be sent
|
|
*/
|
|
send<K extends SendChannels>(
|
|
channel: K,
|
|
args: IPCChannels[K]['req']
|
|
): void {
|
|
// Runtime validation of request payload
|
|
const validatedArgs = validateRequest(channel, args);
|
|
ipcRenderer.send(channel, validatedArgs);
|
|
},
|
|
|
|
/**
|
|
* Listen to a send channel event
|
|
* Returns a cleanup function to remove the listener
|
|
*/
|
|
on<K extends SendChannels>(
|
|
channel: K,
|
|
handler: (event: IPCChannels[K]['req']) => void
|
|
): () => void {
|
|
const listener = (_event: unknown, data: IPCChannels[K]['req']) => {
|
|
handler(data);
|
|
};
|
|
ipcRenderer.on(channel, listener);
|
|
return () => {
|
|
ipcRenderer.removeListener(channel, listener);
|
|
};
|
|
},
|
|
};
|
|
|
|
contextBridge.exposeInMainWorld('ipc', ipc);
|
|
|
|
contextBridge.exposeInMainWorld('electronUtils', {
|
|
getPathForFile: (file: File) => webUtils.getPathForFile(file),
|
|
getZoomFactor: () => webFrame.getZoomFactor(),
|
|
});
|