| 1 | import type { |
| 2 | DesktopBridge, |
| 3 | DesktopPreviewOpenLinkEvent, |
| 4 | DesktopPreviewPointerEvent, |
| 5 | DesktopPreviewRecordingInputEvent, |
| 6 | DesktopPreviewRecordingFrame, |
| 7 | DesktopPreviewTabState, |
| 8 | DesktopSnapShotEvent, |
| 9 | } from "@t3tools/contracts"; |
| 10 | import { exposeClerkBridge } from "@clerk/electron/preload"; |
| 11 | import { contextBridge, ipcRenderer, webFrame, webUtils } from "electron"; |
| 12 | |
| 13 | import * as IpcChannels from "./ipc/channels.ts"; |
| 14 | import { mergeLegacyLocalStorage } from "./legacyLocalStorageMerge.ts"; |
| 15 | |
| 16 | const SNAP_SHOT_EVENT_TYPES = new Set([ |
| 17 | "requested", |
| 18 | "started", |
| 19 | "ready", |
| 20 | "failed", |
| 21 | "shortcut-changed", |
| 22 | ]); |
| 23 | function isSnapShotEvent(value: unknown): value is DesktopSnapShotEvent { |
| 24 | if (typeof value !== "object" || value === null) return false; |
| 25 | const { type, id } = value as { type?: unknown; id?: unknown }; |
| 26 | return ( |
| 27 | typeof type === "string" && |
| 28 | SNAP_SHOT_EVENT_TYPES.has(type) && |
| 29 | (id === undefined || typeof id === "string") |
| 30 | ); |
| 31 | } |
| 32 | |
| 33 | exposeClerkBridge({ passkeys: true }); |
| 34 | |
| 35 | // Runs before any app script reads localStorage. See DesktopLegacyLocalStorage. |
| 36 | try { |
| 37 | const legacyItems: unknown = ipcRenderer.sendSync(IpcChannels.TAKE_LEGACY_LOCAL_STORAGE_CHANNEL); |
| 38 | if (typeof legacyItems === "object" && legacyItems !== null) { |
| 39 | if (mergeLegacyLocalStorage(window.localStorage, legacyItems as Record<string, string>)) { |
| 40 | void ipcRenderer.invoke(IpcChannels.COMPLETE_LEGACY_LOCAL_STORAGE_CHANNEL); |
| 41 | } |
| 42 | } |
| 43 | } catch { |
| 44 | // Best effort: the app still starts on the V2 profile's own storage. |
| 45 | } |
| 46 | |
| 47 | // oxlint-disable-next-line t3code/no-global-process-runtime -- Electron exposes the client platform in its sandboxed preload process. |
| 48 | const clientPlatform = process.platform; |
| 49 | |
| 50 | if (clientPlatform === "darwin") { |
| 51 | // Native window buttons do not scale with Chromium zoom. Keep their reserved |
| 52 | // space in native points, including when a zoomed page is reloaded. |
| 53 | const syncWindowControlInset = () => { |
| 54 | document.documentElement.style.setProperty( |
| 55 | "--desktop-window-controls-inset", |
| 56 | `${90 / webFrame.getZoomFactor()}px`, |
| 57 | ); |
| 58 | }; |
| 59 | window.addEventListener("DOMContentLoaded", syncWindowControlInset, { once: true }); |
| 60 | window.addEventListener("resize", syncWindowControlInset); |
| 61 | } |
| 62 | |
| 63 | function unwrapEnsureSshEnvironmentResult(result: unknown) { |
| 64 | if ( |
| 65 | typeof result === "object" && |
| 66 | result !== null && |
| 67 | "type" in result && |
| 68 | result.type === IpcChannels.SSH_PASSWORD_PROMPT_CANCELLED_RESULT |
| 69 | ) { |
| 70 | const message = |
| 71 | "message" in result && typeof result.message === "string" |
| 72 | ? result.message |
| 73 | : "SSH authentication cancelled."; |
| 74 | throw new Error(message); |
| 75 | } |
| 76 | return result as Awaited<ReturnType<DesktopBridge["ensureSshEnvironment"]>>; |
| 77 | } |
| 78 | |
| 79 | contextBridge.exposeInMainWorld("desktopBridge", { |
| 80 | supportsLocalTerminal: (input) => |
| 81 | ipcRenderer.sendSync(`${IpcChannels.DESKTOP_TERMINAL_CHANNEL}:available`, input) === true, |
| 82 | openLocalTerminal: (input, onMessage, onClose) => { |
| 83 | const { port1, port2 } = new MessageChannel(); |
| 84 | let closed = false; |
| 85 | const close = () => { |
| 86 | if (closed) return; |
| 87 | closed = true; |
| 88 | port1.postMessage(null); |
| 89 | port1.close(); |
| 90 | onClose(); |
| 91 | }; |
| 92 | port1.addEventListener("message", ({ data }) => { |
| 93 | if (data?.type === "message" && typeof data.data === "string") onMessage(data.data); |
| 94 | else if (data?.type === "close") close(); |
| 95 | }); |
| 96 | ipcRenderer.postMessage(IpcChannels.DESKTOP_TERMINAL_CHANNEL, input, [port2]); |
| 97 | port1.start(); |
| 98 | return { |
| 99 | send: (data) => { |
| 100 | if (!closed) port1.postMessage(data); |
| 101 | }, |
| 102 | close, |
| 103 | }; |
| 104 | }, |
| 105 | getAppBranding: () => { |
| 106 | const result = ipcRenderer.sendSync(IpcChannels.GET_APP_BRANDING_CHANNEL); |
| 107 | if (typeof result !== "object" || result === null) { |
| 108 | return null; |
| 109 | } |
| 110 | return result as ReturnType<DesktopBridge["getAppBranding"]>; |
| 111 | }, |
| 112 | getPathForFile: (file: File) => webUtils.getPathForFile(file), |
| 113 | getClientPlatform: () => clientPlatform, |
| 114 | setNotificationBadge: (badge) => |
| 115 | ipcRenderer.invoke(IpcChannels.SET_NOTIFICATION_BADGE_CHANNEL, badge), |
| 116 | onNotificationBadgeClear: (listener) => { |
| 117 | const handler = () => listener(); |
| 118 | ipcRenderer.on(IpcChannels.SET_NOTIFICATION_BADGE_CHANNEL, handler); |
| 119 | return () => ipcRenderer.removeListener(IpcChannels.SET_NOTIFICATION_BADGE_CHANNEL, handler); |
| 120 | }, |
| 121 | onTrackpadScrollEnd: (listener) => { |
| 122 | const handler = () => listener(); |
| 123 | ipcRenderer.on(IpcChannels.TRACKPAD_SCROLL_END_CHANNEL, handler); |
| 124 | return () => ipcRenderer.removeListener(IpcChannels.TRACKPAD_SCROLL_END_CHANNEL, handler); |
| 125 | }, |
| 126 | getSystemLocale: () => { |
| 127 | const result = ipcRenderer.sendSync(IpcChannels.GET_SYSTEM_LOCALE_CHANNEL); |
| 128 | return typeof result === "string" ? result : null; |
| 129 | }, |
| 130 | getLocalEnvironmentBootstraps: () => { |
| 131 | const result = ipcRenderer.sendSync(IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL); |
| 132 | if (!Array.isArray(result)) { |
| 133 | return []; |
| 134 | } |
| 135 | return result as ReturnType<DesktopBridge["getLocalEnvironmentBootstraps"]>; |
| 136 | }, |
| 137 | getLocalEnvironmentBearerToken: () => |
| 138 | ipcRenderer.invoke(IpcChannels.GET_LOCAL_ENVIRONMENT_BEARER_TOKEN_CHANNEL), |
| 139 | getLocalEnvironmentEnabled: () => |
| 140 | ipcRenderer.sendSync(IpcChannels.GET_LOCAL_ENVIRONMENT_ENABLED_CHANNEL) !== false, |
| 141 | setLocalEnvironmentEnabled: (enabled) => |
| 142 | ipcRenderer.invoke(IpcChannels.SET_LOCAL_ENVIRONMENT_ENABLED_CHANNEL, enabled), |
| 143 | getClientSettings: () => ipcRenderer.invoke(IpcChannels.GET_CLIENT_SETTINGS_CHANNEL), |
| 144 | setClientSettings: (settings) => |
| 145 | ipcRenderer.invoke(IpcChannels.SET_CLIENT_SETTINGS_CHANNEL, settings), |
| 146 | requestSnapShotPermissions: (includeAccessibility) => |
| 147 | ipcRenderer.invoke(IpcChannels.REQUEST_SNAP_SHOT_PERMISSIONS_CHANNEL, includeAccessibility), |
| 148 | getSnapShotState: () => ipcRenderer.invoke(IpcChannels.GET_SNAP_SHOT_STATE_CHANNEL), |
| 149 | setupSnapShot: (action) => ipcRenderer.invoke(IpcChannels.SETUP_SNAP_SHOT_CHANNEL, action), |
| 150 | previewSnapShotConfig: (request) => |
| 151 | ipcRenderer.invoke(IpcChannels.PREVIEW_SNAP_SHOT_CONFIG_CHANNEL, request), |
| 152 | applySnapShotConfig: (id) => ipcRenderer.invoke(IpcChannels.APPLY_SNAP_SHOT_CONFIG_CHANNEL, id), |
| 153 | checkSnapShotShortcut: (shortcut) => |
| 154 | ipcRenderer.invoke(IpcChannels.CHECK_SNAP_SHOT_SHORTCUT_CHANNEL, shortcut), |
| 155 | setSnapShotShortcutSuppressed: (suppressed) => |
| 156 | ipcRenderer.invoke(IpcChannels.SET_SNAP_SHOT_SHORTCUT_SUPPRESSED_CHANNEL, suppressed), |
| 157 | listPendingSnapShots: () => ipcRenderer.invoke(IpcChannels.LIST_PENDING_SNAP_SHOTS_CHANNEL), |
| 158 | readSnapShot: (id) => ipcRenderer.invoke(IpcChannels.READ_SNAP_SHOT_CHANNEL, id), |
| 159 | setSnapShotAnimationDestination: (destination) => |
| 160 | ipcRenderer.invoke(IpcChannels.SET_SNAP_SHOT_ANIMATION_DESTINATION_CHANNEL, destination), |
| 161 | dismissSnapShotAnimation: (id) => |
| 162 | ipcRenderer.invoke(IpcChannels.DISMISS_SNAP_SHOT_ANIMATION_CHANNEL, id), |
| 163 | acknowledgeSnapShot: (id) => ipcRenderer.invoke(IpcChannels.ACKNOWLEDGE_SNAP_SHOT_CHANNEL, id), |
| 164 | beginHostedAuth: (server) => ipcRenderer.invoke(IpcChannels.BEGIN_HOSTED_AUTH_CHANNEL, server), |
| 165 | getHostedAuthState: () => ipcRenderer.invoke(IpcChannels.GET_HOSTED_AUTH_STATE_CHANNEL), |
| 166 | getHostedAccessToken: (server) => |
| 167 | ipcRenderer.invoke(IpcChannels.GET_HOSTED_ACCESS_TOKEN_CHANNEL, server), |
| 168 | cancelHostedAuth: () => ipcRenderer.invoke(IpcChannels.CANCEL_HOSTED_AUTH_CHANNEL), |
| 169 | disconnectHostedAuth: () => ipcRenderer.invoke(IpcChannels.DISCONNECT_HOSTED_AUTH_CHANNEL), |
| 170 | getConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.GET_CONNECTION_CATALOG_CHANNEL), |
| 171 | setConnectionCatalog: (catalog) => |
| 172 | ipcRenderer.invoke(IpcChannels.SET_CONNECTION_CATALOG_CHANNEL, catalog), |
| 173 | clearConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.CLEAR_CONNECTION_CATALOG_CHANNEL), |
| 174 | discoverSshHosts: () => ipcRenderer.invoke(IpcChannels.DISCOVER_SSH_HOSTS_CHANNEL), |
| 175 | resolveSshHost: (alias) => ipcRenderer.invoke(IpcChannels.RESOLVE_SSH_HOST_CHANNEL, alias), |
| 176 | ensureSshEnvironment: async (target, options) => |
| 177 | unwrapEnsureSshEnvironmentResult( |
| 178 | await ipcRenderer.invoke(IpcChannels.ENSURE_SSH_ENVIRONMENT_CHANNEL, { |
| 179 | target, |
| 180 | ...(options === undefined ? {} : { options }), |
| 181 | }), |
| 182 | ), |
| 183 | disconnectSshEnvironment: (target) => |
| 184 | ipcRenderer.invoke(IpcChannels.DISCONNECT_SSH_ENVIRONMENT_CHANNEL, target), |
| 185 | fetchSshEnvironmentDescriptor: (httpBaseUrl) => |
| 186 | ipcRenderer.invoke(IpcChannels.FETCH_SSH_ENVIRONMENT_DESCRIPTOR_CHANNEL, { httpBaseUrl }), |
| 187 | bootstrapSshBearerSession: (httpBaseUrl, credential) => |
| 188 | ipcRenderer.invoke(IpcChannels.BOOTSTRAP_SSH_BEARER_SESSION_CHANNEL, { |
| 189 | httpBaseUrl, |
| 190 | credential, |
| 191 | }), |
| 192 | fetchSshSessionState: (httpBaseUrl, bearerToken) => |
| 193 | ipcRenderer.invoke(IpcChannels.FETCH_SSH_SESSION_STATE_CHANNEL, { httpBaseUrl, bearerToken }), |
| 194 | issueSshWebSocketTicket: (httpBaseUrl, bearerToken) => |
| 195 | ipcRenderer.invoke(IpcChannels.ISSUE_SSH_WEBSOCKET_TOKEN_CHANNEL, { httpBaseUrl, bearerToken }), |
| 196 | onSshPasswordPrompt: (listener) => { |
| 197 | const wrappedListener = (_event: Electron.IpcRendererEvent, request: unknown) => { |
| 198 | if (typeof request !== "object" || request === null) return; |
| 199 | listener(request as Parameters<typeof listener>[0]); |
| 200 | }; |
| 201 | |
| 202 | ipcRenderer.on(IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL, wrappedListener); |
| 203 | return () => { |
| 204 | ipcRenderer.removeListener(IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL, wrappedListener); |
| 205 | }; |
| 206 | }, |
| 207 | resolveSshPasswordPrompt: (requestId, password) => |
| 208 | ipcRenderer.invoke(IpcChannels.RESOLVE_SSH_PASSWORD_PROMPT_CHANNEL, { requestId, password }), |
| 209 | getServerExposureState: () => ipcRenderer.invoke(IpcChannels.GET_SERVER_EXPOSURE_STATE_CHANNEL), |
| 210 | setServerExposureMode: (mode) => |
| 211 | ipcRenderer.invoke(IpcChannels.SET_SERVER_EXPOSURE_MODE_CHANNEL, mode), |
| 212 | setTailscaleServeEnabled: (input) => |
| 213 | ipcRenderer.invoke(IpcChannels.SET_TAILSCALE_SERVE_ENABLED_CHANNEL, input), |
| 214 | getAdvertisedEndpoints: () => ipcRenderer.invoke(IpcChannels.GET_ADVERTISED_ENDPOINTS_CHANNEL), |
| 215 | getWslState: () => ipcRenderer.invoke(IpcChannels.GET_WSL_STATE_CHANNEL), |
| 216 | setWslBackendEnabled: (enabled) => |
| 217 | ipcRenderer.invoke(IpcChannels.SET_WSL_BACKEND_ENABLED_CHANNEL, enabled), |
| 218 | setWslDistro: (distro) => ipcRenderer.invoke(IpcChannels.SET_WSL_DISTRO_CHANNEL, distro), |
| 219 | setWslOnly: (enabled) => ipcRenderer.invoke(IpcChannels.SET_WSL_ONLY_CHANNEL, enabled), |
| 220 | pickFolder: (options) => ipcRenderer.invoke(IpcChannels.PICK_FOLDER_CHANNEL, options), |
| 221 | pickProjectFavicon: (initialPath) => |
| 222 | ipcRenderer.invoke(IpcChannels.PICK_PROJECT_FAVICON_CHANNEL, initialPath), |
| 223 | pickThemeFiles: () => ipcRenderer.invoke(IpcChannels.PICK_THEME_FILES_CHANNEL, undefined), |
| 224 | setTheme: (theme) => ipcRenderer.invoke(IpcChannels.SET_THEME_CHANNEL, theme), |
| 225 | showContextMenu: (items, position) => |
| 226 | ipcRenderer.invoke(IpcChannels.CONTEXT_MENU_CHANNEL, { |
| 227 | items, |
| 228 | ...(position === undefined ? {} : { position }), |
| 229 | }), |
| 230 | receiveProviderAuthCallback: (url: string) => |
| 231 | ipcRenderer.invoke(IpcChannels.RECEIVE_PROVIDER_AUTH_CALLBACK_CHANNEL, url), |
| 232 | cancelProviderAuthCallback: (url: string) => |
| 233 | ipcRenderer.invoke(IpcChannels.CANCEL_PROVIDER_AUTH_CALLBACK_CHANNEL, url), |
| 234 | openExternal: (url: string) => ipcRenderer.invoke(IpcChannels.OPEN_EXTERNAL_CHANNEL, url), |
| 235 | ...(clientPlatform === "darwin" |
| 236 | ? { |
| 237 | openVideo: (input: { url: string; name: string }) => |
| 238 | ipcRenderer.invoke(IpcChannels.OPEN_VIDEO_CHANNEL, input), |
| 239 | } |
| 240 | : {}), |
| 241 | checkSystemPermission: (pane: string) => |
| 242 | ipcRenderer.invoke(IpcChannels.CHECK_SYSTEM_PERMISSION_CHANNEL, pane), |
| 243 | openSystemSettings: (pane: string) => |
| 244 | ipcRenderer.invoke(IpcChannels.OPEN_SYSTEM_SETTINGS_CHANNEL, pane), |
| 245 | probeRemoteEditors: () => ipcRenderer.invoke(IpcChannels.PROBE_REMOTE_EDITORS_CHANNEL, undefined), |
| 246 | pasteAsText: () => ipcRenderer.invoke(IpcChannels.PASTE_AS_TEXT_CHANNEL, undefined), |
| 247 | onMenuAction: (listener) => { |
| 248 | const wrappedListener = (_event: Electron.IpcRendererEvent, action: unknown) => { |
| 249 | if (typeof action !== "string") return; |
| 250 | listener(action); |
| 251 | }; |
| 252 | |
| 253 | ipcRenderer.on(IpcChannels.MENU_ACTION_CHANNEL, wrappedListener); |
| 254 | return () => { |
| 255 | ipcRenderer.removeListener(IpcChannels.MENU_ACTION_CHANNEL, wrappedListener); |
| 256 | }; |
| 257 | }, |
| 258 | onSnapShotEvent: (listener) => { |
| 259 | const wrappedListener = (_event: Electron.IpcRendererEvent, event: unknown) => { |
| 260 | if (!isSnapShotEvent(event)) return; |
| 261 | listener(event); |
| 262 | }; |
| 263 | |
| 264 | ipcRenderer.on(IpcChannels.SNAP_SHOT_EVENT_CHANNEL, wrappedListener); |
| 265 | return () => { |
| 266 | ipcRenderer.removeListener(IpcChannels.SNAP_SHOT_EVENT_CHANNEL, wrappedListener); |
| 267 | }; |
| 268 | }, |
| 269 | onQuitShortcut: (listener) => { |
| 270 | const wrappedListener = (_event: Electron.IpcRendererEvent, hint: unknown) => { |
| 271 | if (typeof hint !== "object" || hint === null || !("state" in hint)) return; |
| 272 | if (hint.state === "up") { |
| 273 | listener({ state: "up" }); |
| 274 | return; |
| 275 | } |
| 276 | if ( |
| 277 | hint.state === "down" && |
| 278 | "mode" in hint && |
| 279 | (hint.mode === "hold" || hint.mode === "double-click") |
| 280 | ) { |
| 281 | listener({ state: "down", mode: hint.mode }); |
| 282 | } |
| 283 | }; |
| 284 | |
| 285 | ipcRenderer.on(IpcChannels.QUIT_SHORTCUT_CHANNEL, wrappedListener); |
| 286 | return () => { |
| 287 | ipcRenderer.removeListener(IpcChannels.QUIT_SHORTCUT_CHANNEL, wrappedListener); |
| 288 | }; |
| 289 | }, |
| 290 | getWindowFullscreenState: () => |
| 291 | ipcRenderer.sendSync(IpcChannels.GET_WINDOW_FULLSCREEN_STATE_CHANNEL) === true, |
| 292 | onWindowFullscreenStateChange: (listener) => { |
| 293 | const wrappedListener = (_event: Electron.IpcRendererEvent, fullscreen: unknown) => { |
| 294 | if (typeof fullscreen !== "boolean") return; |
| 295 | listener(fullscreen); |
| 296 | }; |
| 297 | |
| 298 | ipcRenderer.on(IpcChannels.WINDOW_FULLSCREEN_STATE_CHANNEL, wrappedListener); |
| 299 | return () => { |
| 300 | ipcRenderer.removeListener(IpcChannels.WINDOW_FULLSCREEN_STATE_CHANNEL, wrappedListener); |
| 301 | }; |
| 302 | }, |
| 303 | getUpdateState: () => ipcRenderer.invoke(IpcChannels.UPDATE_GET_STATE_CHANNEL), |
| 304 | setUpdateChannel: (channel) => |
| 305 | ipcRenderer.invoke(IpcChannels.UPDATE_SET_CHANNEL_CHANNEL, channel), |
| 306 | checkForUpdate: () => ipcRenderer.invoke(IpcChannels.UPDATE_CHECK_CHANNEL), |
| 307 | downloadUpdate: () => ipcRenderer.invoke(IpcChannels.UPDATE_DOWNLOAD_CHANNEL), |
| 308 | installUpdate: () => ipcRenderer.invoke(IpcChannels.UPDATE_INSTALL_CHANNEL), |
| 309 | cliCommand: { |
| 310 | getState: () => ipcRenderer.invoke(IpcChannels.CLI_COMMAND_GET_STATE_CHANNEL), |
| 311 | install: () => ipcRenderer.invoke(IpcChannels.CLI_COMMAND_INSTALL_CHANNEL), |
| 312 | uninstall: () => ipcRenderer.invoke(IpcChannels.CLI_COMMAND_UNINSTALL_CHANNEL), |
| 313 | }, |
| 314 | onUpdateState: (listener) => { |
| 315 | const wrappedListener = (_event: Electron.IpcRendererEvent, state: unknown) => { |
| 316 | if (typeof state !== "object" || state === null) return; |
| 317 | listener(state as Parameters<typeof listener>[0]); |
| 318 | }; |
| 319 | |
| 320 | ipcRenderer.on(IpcChannels.UPDATE_STATE_CHANNEL, wrappedListener); |
| 321 | return () => { |
| 322 | ipcRenderer.removeListener(IpcChannels.UPDATE_STATE_CHANNEL, wrappedListener); |
| 323 | }; |
| 324 | }, |
| 325 | webLinks: { |
| 326 | setReady: (ready) => ipcRenderer.invoke(IpcChannels.WEB_LINK_READY_CHANNEL, ready), |
| 327 | onOpen: (listener) => { |
| 328 | const wrappedListener = (_event: Electron.IpcRendererEvent, url: unknown) => { |
| 329 | if (typeof url === "string") listener(url); |
| 330 | }; |
| 331 | ipcRenderer.on(IpcChannels.WEB_LINK_OPEN_CHANNEL, wrappedListener); |
| 332 | return () => { |
| 333 | ipcRenderer.removeListener(IpcChannels.WEB_LINK_OPEN_CHANNEL, wrappedListener); |
| 334 | }; |
| 335 | }, |
| 336 | }, |
| 337 | appActivation: { |
| 338 | setReady: (ready) => |
| 339 | ipcRenderer.invoke(IpcChannels.DESKTOP_APP_ACTIVATION_READY_CHANNEL, ready), |
| 340 | complete: (response) => |
| 341 | ipcRenderer.invoke(IpcChannels.DESKTOP_APP_ACTIVATION_COMPLETE_CHANNEL, response), |
| 342 | onRequest: (listener) => { |
| 343 | const wrappedListener = (_event: Electron.IpcRendererEvent, request: unknown) => { |
| 344 | if (typeof request !== "object" || request === null) return; |
| 345 | listener(request as Parameters<typeof listener>[0]); |
| 346 | }; |
| 347 | ipcRenderer.on(IpcChannels.DESKTOP_APP_ACTIVATION_REQUEST_CHANNEL, wrappedListener); |
| 348 | return () => { |
| 349 | ipcRenderer.removeListener( |
| 350 | IpcChannels.DESKTOP_APP_ACTIVATION_REQUEST_CHANNEL, |
| 351 | wrappedListener, |
| 352 | ); |
| 353 | }; |
| 354 | }, |
| 355 | }, |
| 356 | preview: { |
| 357 | setForwardedShortcuts: (shortcuts) => |
| 358 | ipcRenderer.invoke(IpcChannels.PREVIEW_SET_FORWARDED_SHORTCUTS_CHANNEL, shortcuts), |
| 359 | createTab: (tabId, defaults) => |
| 360 | ipcRenderer.invoke(IpcChannels.PREVIEW_CREATE_TAB_CHANNEL, { |
| 361 | tabId, |
| 362 | zoomFactor: defaults?.zoomFactor, |
| 363 | serverTab: defaults?.serverTab, |
| 364 | colorScheme: defaults?.colorScheme, |
| 365 | }), |
| 366 | closeTab: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_CLOSE_TAB_CHANNEL, { tabId }), |
| 367 | registerWebview: (tabId, webContentsId) => |
| 368 | ipcRenderer.invoke(IpcChannels.PREVIEW_REGISTER_WEBVIEW_CHANNEL, { tabId, webContentsId }), |
| 369 | navigate: (tabId, url) => |
| 370 | ipcRenderer.invoke(IpcChannels.PREVIEW_NAVIGATE_CHANNEL, { tabId, url }), |
| 371 | goBack: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_GO_BACK_CHANNEL, { tabId }), |
| 372 | goForward: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_GO_FORWARD_CHANNEL, { tabId }), |
| 373 | refresh: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_REFRESH_CHANNEL, { tabId }), |
| 374 | zoomIn: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_ZOOM_IN_CHANNEL, { tabId }), |
| 375 | zoomOut: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_ZOOM_OUT_CHANNEL, { tabId }), |
| 376 | resetZoom: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_RESET_ZOOM_CHANNEL, { tabId }), |
| 377 | setZoomFactor: (tabId, zoomFactor) => |
| 378 | ipcRenderer.invoke(IpcChannels.PREVIEW_SET_ZOOM_FACTOR_CHANNEL, { tabId, zoomFactor }), |
| 379 | hardReload: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_HARD_RELOAD_CHANNEL, { tabId }), |
| 380 | setColorScheme: (tabId, colorScheme) => |
| 381 | ipcRenderer.invoke(IpcChannels.PREVIEW_SET_COLOR_SCHEME_CHANNEL, { tabId, colorScheme }), |
| 382 | setAudioMuted: (tabId, audioMuted) => |
| 383 | ipcRenderer.invoke(IpcChannels.PREVIEW_SET_AUDIO_MUTED_CHANNEL, { tabId, audioMuted }), |
| 384 | openDevTools: (tabId) => |
| 385 | ipcRenderer.invoke(IpcChannels.PREVIEW_OPEN_DEVTOOLS_CHANNEL, { tabId }), |
| 386 | listBrowserImportSources: () => ipcRenderer.invoke(IpcChannels.PREVIEW_IMPORT_SOURCES_CHANNEL), |
| 387 | importBrowserCookies: (input) => |
| 388 | ipcRenderer.invoke(IpcChannels.PREVIEW_IMPORT_COOKIES_CHANNEL, input), |
| 389 | clearCookies: (environmentId, profileId) => |
| 390 | ipcRenderer.invoke(IpcChannels.PREVIEW_CLEAR_COOKIES_CHANNEL, { environmentId, profileId }), |
| 391 | clearCache: (environmentId, profileId) => |
| 392 | ipcRenderer.invoke(IpcChannels.PREVIEW_CLEAR_CACHE_CHANNEL, { environmentId, profileId }), |
| 393 | getPreviewConfig: (environmentId, profileId) => |
| 394 | ipcRenderer.invoke(IpcChannels.PREVIEW_GET_CONFIG_CHANNEL, { environmentId, profileId }), |
| 395 | setAnnotationTheme: (theme) => |
| 396 | ipcRenderer.invoke(IpcChannels.PREVIEW_SET_ANNOTATION_THEME_CHANNEL, { theme }), |
| 397 | setAnnotationSendEnabled: (tabId, enabled) => |
| 398 | ipcRenderer.invoke(IpcChannels.PREVIEW_SET_ANNOTATION_SEND_ENABLED_CHANNEL, { |
| 399 | tabId, |
| 400 | enabled, |
| 401 | }), |
| 402 | pickElement: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, { tabId }), |
| 403 | cancelPickElement: (tabId) => |
| 404 | ipcRenderer.invoke(IpcChannels.PREVIEW_CANCEL_PICK_ELEMENT_CHANNEL, { tabId }), |
| 405 | captureScreenshot: (tabId) => |
| 406 | ipcRenderer.invoke(IpcChannels.PREVIEW_CAPTURE_SCREENSHOT_CHANNEL, { tabId }), |
| 407 | revealArtifact: (path) => |
| 408 | ipcRenderer.invoke(IpcChannels.PREVIEW_REVEAL_ARTIFACT_CHANNEL, { path }), |
| 409 | copyArtifactToClipboard: (path) => |
| 410 | ipcRenderer.invoke(IpcChannels.PREVIEW_COPY_ARTIFACT_CHANNEL, { path }), |
| 411 | pictureInPicture: { |
| 412 | open: (tabId) => |
| 413 | ipcRenderer.invoke(IpcChannels.PREVIEW_PICTURE_IN_PICTURE_OPEN_CHANNEL, { tabId }), |
| 414 | close: (tabId) => |
| 415 | ipcRenderer.invoke(IpcChannels.PREVIEW_PICTURE_IN_PICTURE_CLOSE_CHANNEL, { tabId }), |
| 416 | }, |
| 417 | recording: { |
| 418 | onInput: (listener) => { |
| 419 | const wrappedListener = (_event: Electron.IpcRendererEvent, event: unknown) => { |
| 420 | if (typeof event !== "object" || event === null) return; |
| 421 | listener(event as DesktopPreviewRecordingInputEvent); |
| 422 | }; |
| 423 | ipcRenderer.on(IpcChannels.PREVIEW_RECORDING_INPUT_CHANNEL, wrappedListener); |
| 424 | return () => |
| 425 | ipcRenderer.removeListener(IpcChannels.PREVIEW_RECORDING_INPUT_CHANNEL, wrappedListener); |
| 426 | }, |
| 427 | startScreencast: (tabId) => |
| 428 | ipcRenderer.invoke(IpcChannels.PREVIEW_RECORDING_START_CHANNEL, { tabId }), |
| 429 | stopScreencast: (tabId) => |
| 430 | ipcRenderer.invoke(IpcChannels.PREVIEW_RECORDING_STOP_CHANNEL, { tabId }), |
| 431 | save: (tabId, mimeType, data) => |
| 432 | ipcRenderer.invoke(IpcChannels.PREVIEW_RECORDING_SAVE_CHANNEL, { |
| 433 | tabId, |
| 434 | mimeType, |
| 435 | data, |
| 436 | }), |
| 437 | onFrame: (listener) => { |
| 438 | const wrappedListener = (_event: Electron.IpcRendererEvent, frame: unknown) => { |
| 439 | if (typeof frame !== "object" || frame === null) return; |
| 440 | listener(frame as DesktopPreviewRecordingFrame); |
| 441 | }; |
| 442 | ipcRenderer.on(IpcChannels.PREVIEW_RECORDING_FRAME_CHANNEL, wrappedListener); |
| 443 | return () => |
| 444 | ipcRenderer.removeListener(IpcChannels.PREVIEW_RECORDING_FRAME_CHANNEL, wrappedListener); |
| 445 | }, |
| 446 | }, |
| 447 | onStateChange: (listener) => { |
| 448 | const wrappedListener = ( |
| 449 | _event: Electron.IpcRendererEvent, |
| 450 | tabId: unknown, |
| 451 | state: unknown, |
| 452 | ) => { |
| 453 | if (typeof tabId !== "string" || typeof state !== "object" || state === null) return; |
| 454 | listener(tabId, state as DesktopPreviewTabState); |
| 455 | }; |
| 456 | ipcRenderer.on(IpcChannels.PREVIEW_STATE_CHANGE_CHANNEL, wrappedListener); |
| 457 | return () => |
| 458 | ipcRenderer.removeListener(IpcChannels.PREVIEW_STATE_CHANGE_CHANNEL, wrappedListener); |
| 459 | }, |
| 460 | onPointerEvent: (listener) => { |
| 461 | const wrappedListener = (_event: Electron.IpcRendererEvent, pointerEvent: unknown) => { |
| 462 | if (typeof pointerEvent !== "object" || pointerEvent === null) return; |
| 463 | listener(pointerEvent as DesktopPreviewPointerEvent); |
| 464 | }; |
| 465 | ipcRenderer.on(IpcChannels.PREVIEW_POINTER_EVENT_CHANNEL, wrappedListener); |
| 466 | return () => |
| 467 | ipcRenderer.removeListener(IpcChannels.PREVIEW_POINTER_EVENT_CHANNEL, wrappedListener); |
| 468 | }, |
| 469 | onOpenLink: (listener) => { |
| 470 | const wrappedListener = (_event: Electron.IpcRendererEvent, linkEvent: unknown) => { |
| 471 | if (typeof linkEvent !== "object" || linkEvent === null) return; |
| 472 | listener(linkEvent as DesktopPreviewOpenLinkEvent); |
| 473 | }; |
| 474 | ipcRenderer.on(IpcChannels.PREVIEW_OPEN_LINK_CHANNEL, wrappedListener); |
| 475 | return () => |
| 476 | ipcRenderer.removeListener(IpcChannels.PREVIEW_OPEN_LINK_CHANNEL, wrappedListener); |
| 477 | }, |
| 478 | }, |
| 479 | } satisfies DesktopBridge); |