feat(desktop): port v0.11.4-v0.15.2 from Tauri to Electron + Discord-parity audio (v0.16.0)

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>
This commit is contained in:
byGalax
2026-05-06 23:35:01 +02:00
parent 72d02e385f
commit 825160ee46
504 changed files with 14217 additions and 14366 deletions
+154
View File
@@ -0,0 +1,154 @@
// 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);