825160ee46
Chronological port of every Tauri release commit (v0.11.4 -> v0.15.2)
into the Electron rebuild, plus Discord-style audio handling that goes
beyond the Tauri original.
Highlights:
- All 17 Tauri release commits ported (audio fixes, custom notification
sound, Discord-style chat UX, profile banner, changelog page,
Discord-parity call UX, screen-share echo + re-watch UX, audio-loop
fix).
- Native napi-rs audio-loopback addon with WASAPI process-loopback:
* EXCLUDE_TARGET_PROCESS_TREE for full-screen shares -> peers
never hear themselves echoed back through the capture.
* INCLUDE_TARGET_PROCESS_TREE for window shares -> only the
picked window's audio is captured, not the whole OS mixer
(Discord parity).
* HWND -> PID resolution via Win32 GetWindowThreadProcessId.
- Discord-style screen-source picker (thumbnail grid, screens vs
apps tabs, live-refreshing thumbnails).
- Hash routing fix for packaged builds (file:// can't resolve
BrowserRouter paths).
- Tauri sources removed (apps/desktop/src-tauri).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
155 lines
7.5 KiB
TypeScript
155 lines
7.5 KiB
TypeScript
// Preload bridge. Exposes a single `window.electronAPI` object to the
|
|
// renderer that mirrors the CHANNELS surface from ipc-types.ts. Each
|
|
// CHANNEL becomes a typed method that calls `ipcRenderer.invoke`; the
|
|
// three event channels (shortcut fired/released + updater progress) get
|
|
// `on*` subscribers that return an unsubscribe function.
|
|
//
|
|
// contextIsolation is ON so the renderer never sees `ipcRenderer` or
|
|
// Node directly — only this curated surface.
|
|
|
|
import { contextBridge, ipcRenderer } from 'electron';
|
|
|
|
import {
|
|
CHANNELS,
|
|
ELECTRON_RUNTIME_MARKER,
|
|
type AudioLoopbackChunk,
|
|
type AudioLoopbackStartResult,
|
|
type FsPath,
|
|
type FsRenameArgs,
|
|
type FsWriteArgs,
|
|
type NotifyArgs,
|
|
type ScreenSource,
|
|
type SecureStoreHandle,
|
|
type SecureStoreOpenArgs,
|
|
type ShortcutEvent,
|
|
type ShortcutRegisterArgs,
|
|
type SqlExecuteResult,
|
|
type SqlSelectResult,
|
|
type UpdateCheckResult,
|
|
type UpdateProgress,
|
|
} from './ipc-types';
|
|
|
|
type Unsubscribe = () => void;
|
|
|
|
function on<T>(channel: string, cb: (payload: T) => void): Unsubscribe {
|
|
const handler = (_evt: Electron.IpcRendererEvent, payload: T): void => cb(payload);
|
|
ipcRenderer.on(channel, handler);
|
|
return () => ipcRenderer.removeListener(channel, handler);
|
|
}
|
|
|
|
const api = {
|
|
platform: ELECTRON_RUNTIME_MARKER,
|
|
osPlatform: process.platform as NodeJS.Platform,
|
|
appVersion: process.env.npm_package_version ?? '0.0.0',
|
|
|
|
// Screen sources ---------------------------------------------------------
|
|
getScreenSources: (): Promise<ScreenSource[]> => ipcRenderer.invoke(CHANNELS.SCREEN_GET_SOURCES),
|
|
getScreenThumbnail: (sourceId: string): Promise<string | null> =>
|
|
ipcRenderer.invoke(CHANNELS.SCREEN_GET_THUMBNAIL, sourceId),
|
|
/** Tell main which source the user picked in our Discord-style modal,
|
|
* BEFORE calling getDisplayMedia. The display-media handler reads this
|
|
* and routes the matching desktopCapturer Source into LiveKit. Pass
|
|
* null on cancel/error to clear the pending state. */
|
|
setPendingShareSource: (sourceId: string | null): Promise<void> =>
|
|
ipcRenderer.invoke(CHANNELS.SCREEN_SET_PENDING_SOURCE, sourceId),
|
|
|
|
// Audio loopback ---------------------------------------------------------
|
|
resolveLoopbackSource: (): Promise<{ sourceId: string } | null> =>
|
|
ipcRenderer.invoke(CHANNELS.AUDIO_LOOPBACK_RESOLVE_SOURCE),
|
|
|
|
/** Native WASAPI process-loopback (Windows only). Captures all system
|
|
* audio EXCEPT our own PID tree so peers don't hear themselves echoed
|
|
* back. Throws on platforms where the addon isn't available — caller
|
|
* is expected to fall back to the renderer-driven getUserMedia path
|
|
* (see lib/screenAudio.ts). */
|
|
audioLoopback: {
|
|
start: (): Promise<AudioLoopbackStartResult> =>
|
|
ipcRenderer.invoke(CHANNELS.AUDIO_LOOPBACK_START),
|
|
/** Window-share variant: capture only the picked window's process
|
|
* tree (INCLUDE_TARGET_PROCESS_TREE). hwnd is the decimal HWND
|
|
* parsed from desktopCapturer's `window:<HWND>:0` source id. */
|
|
startForWindow: (hwnd: number): Promise<AudioLoopbackStartResult> =>
|
|
ipcRenderer.invoke(CHANNELS.AUDIO_LOOPBACK_START_FOR_WINDOW, { hwnd }),
|
|
stop: (captureId: number): Promise<void> =>
|
|
ipcRenderer.invoke(CHANNELS.AUDIO_LOOPBACK_STOP, captureId),
|
|
onChunk: (cb: (payload: AudioLoopbackChunk) => void): Unsubscribe =>
|
|
on<AudioLoopbackChunk>(CHANNELS.AUDIO_LOOPBACK_CHUNK, cb),
|
|
},
|
|
|
|
// Shortcuts --------------------------------------------------------------
|
|
registerShortcut: (args: ShortcutRegisterArgs): Promise<boolean> =>
|
|
ipcRenderer.invoke(CHANNELS.SHORTCUT_REGISTER, args),
|
|
unregisterShortcut: (id: string): Promise<void> =>
|
|
ipcRenderer.invoke(CHANNELS.SHORTCUT_UNREGISTER, id),
|
|
isShortcutRegistered: (id: string): Promise<boolean> =>
|
|
ipcRenderer.invoke(CHANNELS.SHORTCUT_IS_REGISTERED, id),
|
|
onShortcutFired: (cb: (evt: ShortcutEvent) => void): Unsubscribe =>
|
|
on<ShortcutEvent>(CHANNELS.SHORTCUT_EVT_FIRED, cb),
|
|
onShortcutReleased: (cb: (evt: ShortcutEvent) => void): Unsubscribe =>
|
|
on<ShortcutEvent>(CHANNELS.SHORTCUT_EVT_RELEASED, cb),
|
|
|
|
// Notifications ----------------------------------------------------------
|
|
notify: (args: NotifyArgs): Promise<void> => ipcRenderer.invoke(CHANNELS.NOTIFY_SHOW, args),
|
|
getNotificationPermission: (): Promise<'granted' | 'denied' | 'default'> =>
|
|
ipcRenderer.invoke(CHANNELS.NOTIFY_PERMISSION),
|
|
|
|
// Tray -------------------------------------------------------------------
|
|
setTrayUnread: (count: number): Promise<void> => ipcRenderer.invoke(CHANNELS.TRAY_UNREAD, count),
|
|
|
|
// Secure store -----------------------------------------------------------
|
|
secureStoreOpen: (args: SecureStoreOpenArgs): Promise<SecureStoreHandle> =>
|
|
ipcRenderer.invoke(CHANNELS.SECURE_STORE_OPEN, args),
|
|
secureStoreGet: (handle: string, key: string): Promise<string | null> =>
|
|
ipcRenderer.invoke(CHANNELS.SECURE_STORE_GET, handle, key),
|
|
secureStoreSet: (handle: string, key: string, value: string): Promise<void> =>
|
|
ipcRenderer.invoke(CHANNELS.SECURE_STORE_SET, handle, key, value),
|
|
secureStoreRemove: (handle: string, key: string): Promise<void> =>
|
|
ipcRenderer.invoke(CHANNELS.SECURE_STORE_REMOVE, handle, key),
|
|
secureStoreClose: (handle: string): Promise<void> =>
|
|
ipcRenderer.invoke(CHANNELS.SECURE_STORE_CLOSE, handle),
|
|
|
|
// Filesystem -------------------------------------------------------------
|
|
fsRead: (p: FsPath): Promise<string | null> => ipcRenderer.invoke(CHANNELS.FS_READ, p),
|
|
fsWrite: (args: FsWriteArgs): Promise<void> => ipcRenderer.invoke(CHANNELS.FS_WRITE, args),
|
|
fsExists: (p: FsPath): Promise<boolean> => ipcRenderer.invoke(CHANNELS.FS_EXISTS, p),
|
|
fsMkdir: (p: FsPath): Promise<void> => ipcRenderer.invoke(CHANNELS.FS_MKDIR, p),
|
|
fsRename: (args: FsRenameArgs): Promise<void> => ipcRenderer.invoke(CHANNELS.FS_RENAME, args),
|
|
fsRemove: (p: FsPath): Promise<void> => ipcRenderer.invoke(CHANNELS.FS_REMOVE, p),
|
|
fsAppLocalDataDir: (): Promise<string> => ipcRenderer.invoke(CHANNELS.FS_APP_LOCAL_DATA_DIR),
|
|
|
|
// SQL --------------------------------------------------------------------
|
|
sqlLoad: (args: { name: string }): Promise<string> =>
|
|
ipcRenderer.invoke(CHANNELS.SQL_LOAD, args),
|
|
sqlExecute: (args: {
|
|
handle: string;
|
|
query: string;
|
|
bindings?: unknown[];
|
|
}): Promise<SqlExecuteResult> => ipcRenderer.invoke(CHANNELS.SQL_EXECUTE, args),
|
|
sqlSelect: (args: {
|
|
handle: string;
|
|
query: string;
|
|
bindings?: unknown[];
|
|
}): Promise<SqlSelectResult> => ipcRenderer.invoke(CHANNELS.SQL_SELECT, args),
|
|
sqlClose: (handle: string): Promise<void> => ipcRenderer.invoke(CHANNELS.SQL_CLOSE, handle),
|
|
|
|
// Updater ----------------------------------------------------------------
|
|
checkForUpdate: (): Promise<UpdateCheckResult> => ipcRenderer.invoke(CHANNELS.UPDATER_CHECK),
|
|
downloadInstallUpdate: (): Promise<void> =>
|
|
ipcRenderer.invoke(CHANNELS.UPDATER_DOWNLOAD_INSTALL),
|
|
onUpdaterProgress: (cb: (p: UpdateProgress) => void): Unsubscribe =>
|
|
on<UpdateProgress>(CHANNELS.UPDATER_EVT_PROGRESS, cb),
|
|
|
|
// Autostart --------------------------------------------------------------
|
|
isAutoStartEnabled: (): Promise<boolean> => ipcRenderer.invoke(CHANNELS.AUTOSTART_IS_ENABLED),
|
|
setAutoStart: (enabled: boolean): Promise<void> =>
|
|
ipcRenderer.invoke(CHANNELS.AUTOSTART_SET, enabled),
|
|
|
|
// Window fullscreen ------------------------------------------------------
|
|
setFullscreen: (enabled: boolean): Promise<void> =>
|
|
ipcRenderer.invoke(CHANNELS.WINDOW_SET_FULLSCREEN, enabled),
|
|
} as const;
|
|
|
|
export type ElectronAPI = typeof api;
|
|
|
|
contextBridge.exposeInMainWorld('electronAPI', api);
|