diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f42a92d..1f85736 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,18 +20,15 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 - + - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '22' - + - name: Install dependencies run: npm ci - - - name: Install app dependencies - run: npx electron-builder install-app-deps - + - name: Build Windows app run: npm run build:win env: @@ -234,8 +231,10 @@ jobs: - name: Install dependencies run: npm ci - - name: Install app dependencies - run: npx electron-builder install-app-deps + # bsdtar (from libarchive-tools) is required by fpm to build pacman + # packages. AppImage and deb don't need it; ubuntu-latest doesn't ship it. + - name: Install pacman build dependencies + run: sudo apt-get update && sudo apt-get install -y libarchive-tools - name: Build Linux app run: npm run build:linux @@ -250,4 +249,5 @@ jobs: release/**/*.AppImage release/**/*.zsync release/**/*.deb + release/**/*.pacman retention-days: 30 diff --git a/electron-builder.json5 b/electron-builder.json5 index ca053ef..ad6cd18 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -3,6 +3,11 @@ "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", "appId": "com.siddharthvaddem.openscreen", "asar": true, + // .node binaries can't be dlopen'd from inside an asar — must live unpacked. + "asarUnpack": [ + "node_modules/uiohook-napi/**/*", + "**/*.node" + ], "productName": "Openscreen", "npmRebuild": true, "buildDependenciesFromSource": true, @@ -52,7 +57,9 @@ }, "linux": { "target": [ - "AppImage" + "AppImage", + "deb", + "pacman" ], "icon": "icons/icons/png", "artifactName": "${productName}-Linux-${version}.${ext}", diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 5e26c44..2d11802 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -37,6 +37,11 @@ interface Window { status: string; error?: string; }>; + requestAccessibilityAccess: () => Promise<{ + success: boolean; + granted: boolean; + error?: string; + }>; assetBaseUrl: string; storeRecordedVideo: ( videoData: ArrayBuffer, @@ -68,6 +73,7 @@ interface Window { getCursorTelemetry: (videoPath?: string) => Promise<{ success: boolean; samples: CursorTelemetryPoint[]; + clicks: number[]; message?: string; error?: string; }>; diff --git a/electron/i18n.ts b/electron/i18n.ts index 4222741..7856357 100644 --- a/electron/i18n.ts +++ b/electron/i18n.ts @@ -1,6 +1,8 @@ // Lightweight i18n for the Electron main process. // Imports the same JSON translation files used by the renderer. +import commonAr from "../src/i18n/locales/ar/common.json"; +import dialogsAr from "../src/i18n/locales/ar/dialogs.json"; import commonEn from "../src/i18n/locales/en/common.json"; import dialogsEn from "../src/i18n/locales/en/dialogs.json"; import commonEs from "../src/i18n/locales/es/common.json"; @@ -18,7 +20,7 @@ import dialogsZh from "../src/i18n/locales/zh-CN/dialogs.json"; import commonZhTw from "../src/i18n/locales/zh-TW/common.json"; import dialogsZhTw from "../src/i18n/locales/zh-TW/dialogs.json"; -type Locale = "en" | "zh-CN" | "zh-TW" | "es" | "fr" | "ja-JP" | "ko-KR" | "tr"; +type Locale = "en" | "zh-CN" | "zh-TW" | "es" | "fr" | "ja-JP" | "ko-KR" | "tr" | "ar"; type Namespace = "common" | "dialogs"; type MessageMap = Record; @@ -31,6 +33,7 @@ const messages: Record> = { "ja-JP": { common: commonJa, dialogs: dialogsJa }, "ko-KR": { common: commonKo, dialogs: dialogsKo }, tr: { common: commonTr, dialogs: dialogsTr }, + ar: { common: commonAr, dialogs: dialogsAr }, }; let currentLocale: Locale = "en"; @@ -44,7 +47,8 @@ export function setMainLocale(locale: string) { locale === "fr" || locale === "ja-JP" || locale === "ko-KR" || - locale === "tr" + locale === "tr" || + locale === "ar" ) { currentLocale = locale; } diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index cb8d217..4e2b3aa 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -1,6 +1,10 @@ import fs from "node:fs/promises"; +import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; + +const nodeRequire = createRequire(import.meta.url); + import { app, BrowserWindow, @@ -56,6 +60,21 @@ function isPathAllowed(filePath: string): boolean { return getAllowedReadDirs().some((dir) => isPathWithinDir(resolved, dir)); } +/** + * Helper function to build dialog options with a parent window only when it's valid. + * This prevents passing stale or destroyed BrowserWindow references to dialog calls. + */ +function buildDialogOptions( + baseOptions: T, + parentWindow: BrowserWindow | null, +): T & { parent?: BrowserWindow } { + const mainWindow = parentWindow; + if (mainWindow && !mainWindow.isDestroyed()) { + return { ...baseOptions, parent: mainWindow }; + } + return baseOptions; +} + function hasAllowedImportVideoExtension(filePath: string): boolean { return ALLOWED_IMPORT_VIDEO_EXTENSIONS.has(path.extname(filePath).toLowerCase()); } @@ -280,19 +299,24 @@ async function storeRecordedSessionFiles(payload: StoreRecordedSessionInput) { const telemetryPath = `${screenVideoPath}.cursor.json`; const pendingBatch = cursorTelemetryBuffer.takeNextBatch(); - if (pendingBatch && pendingBatch.samples.length > 0) { + const pendingClicks = takeCursorClickTimestamps(); + if ((pendingBatch && pendingBatch.samples.length > 0) || pendingClicks.length > 0) { try { await fs.writeFile( telemetryPath, JSON.stringify( - { version: CURSOR_TELEMETRY_VERSION, samples: pendingBatch.samples }, + { + version: CURSOR_TELEMETRY_VERSION, + samples: pendingBatch?.samples ?? [], + clicks: pendingClicks, + }, null, 2, ), "utf-8", ); } catch (err) { - cursorTelemetryBuffer.prependBatch(pendingBatch); + if (pendingBatch) cursorTelemetryBuffer.prependBatch(pendingBatch); throw err; } } @@ -321,15 +345,114 @@ const cursorTelemetryBuffer = createCursorTelemetryBuffer({ maxActiveSamples: MAX_CURSOR_SAMPLES, }); +// Mouse click timestamps (macOS only — uiohook-napi behind Accessibility). +const MAX_CURSOR_CLICKS = 60 * 60 * 60; // ~1 click/sec for an hour +let cursorClickTimestampsMs: number[] = []; +let uioHookInstance: { + start: () => void; + stop: () => void; + on: (...a: unknown[]) => void; + off?: (...a: unknown[]) => void; + removeListener?: (...a: unknown[]) => void; +} | null = null; +let uioHookMouseDownHandler: ((event: { time?: number }) => void) | null = null; +let uioHookFailureLogged = false; + function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)); } +function loadUioHookForClicks(): typeof uioHookInstance { + try { + // Dynamic require + try/catch so a broken native binary doesn't crash startup. + const mod = nodeRequire("uiohook-napi"); + const candidate = mod.uIOhook ?? mod.default?.uIOhook ?? mod.uiohook ?? mod.default; + if (candidate && typeof candidate.start === "function" && typeof candidate.on === "function") { + return candidate; + } + return null; + } catch (error) { + if (!uioHookFailureLogged) { + uioHookFailureLogged = true; + console.warn("[clickCapture] uiohook-napi unavailable:", error); + } + return null; + } +} + +function startClickCapture() { + if (process.platform !== "darwin") return; + if (uioHookInstance) return; + + // Passive check — the prompt fires from the renderer when the user toggles + // "Only on clicks" so it doesn't stack with the screen-recording prompt. + try { + if (!systemPreferences.isTrustedAccessibilityClient(false)) { + if (!uioHookFailureLogged) { + uioHookFailureLogged = true; + console.warn( + "[clickCapture] Accessibility permission not granted — click capture disabled.", + ); + } + return; + } + } catch { + // fall through; uiohook will fail defensively below + } + + const hook = loadUioHookForClicks(); + if (!hook) return; + + uioHookMouseDownHandler = (event) => { + const elapsed = Math.max(0, Date.now() - cursorCaptureStartTimeMs); + void event; + if (cursorClickTimestampsMs.length >= MAX_CURSOR_CLICKS) return; + cursorClickTimestampsMs.push(elapsed); + }; + + try { + hook.on("mousedown", uioHookMouseDownHandler); + hook.start(); + uioHookInstance = hook; + } catch (error) { + if (!uioHookFailureLogged) { + uioHookFailureLogged = true; + console.warn("[clickCapture] failed to start uiohook:", error); + } + uioHookMouseDownHandler = null; + } +} + +function stopClickCapture() { + if (!uioHookInstance) return; + try { + if (uioHookMouseDownHandler) { + if (typeof uioHookInstance.off === "function") { + uioHookInstance.off("mousedown", uioHookMouseDownHandler); + } else if (typeof uioHookInstance.removeListener === "function") { + uioHookInstance.removeListener("mousedown", uioHookMouseDownHandler); + } + } + uioHookInstance.stop(); + } catch (error) { + console.warn("[clickCapture] failed to stop uiohook:", error); + } + uioHookInstance = null; + uioHookMouseDownHandler = null; +} + +function takeCursorClickTimestamps(): number[] { + const out = cursorClickTimestampsMs; + cursorClickTimestampsMs = []; + return out; +} + function stopCursorCapture() { if (cursorCaptureInterval) { clearInterval(cursorCaptureInterval); cursorCaptureInterval = null; } + stopClickCapture(); } function sampleCursorPoint() { @@ -526,14 +649,27 @@ export function registerIpcHandlers( }); ipcMain.handle("get-sources", async (_, opts) => { + const ownWindowSourceIds = new Set( + BrowserWindow.getAllWindows() + .map((win) => { + try { + return win.getMediaSourceId(); + } catch { + return null; + } + }) + .filter((id): id is string => Boolean(id)), + ); const sources = await desktopCapturer.getSources(opts); - return sources.map((source) => ({ - id: source.id, - name: source.name, - display_id: source.display_id, - thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null, - appIcon: source.appIcon ? source.appIcon.toDataURL() : null, - })); + return sources + .filter((source) => !ownWindowSourceIds.has(source.id)) + .map((source) => ({ + id: source.id, + name: source.name, + display_id: source.display_id, + thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null, + appIcon: source.appIcon ? source.appIcon.toDataURL() : null, + })); }); ipcMain.handle("select-source", (_, source: SelectedSource) => { @@ -581,6 +717,22 @@ export function registerIpcHandlers( } }); + // macOS Accessibility prompt for global click capture. First call shows the + // system dialog; the user has to toggle the app in System Settings (no + // programmatic grant exists for Accessibility). + ipcMain.handle("request-accessibility-access", () => { + if (process.platform !== "darwin") { + return { success: true, granted: true }; + } + try { + const granted = systemPreferences.isTrustedAccessibilityClient(true); + return { success: true, granted }; + } catch (error) { + console.error("Failed to request accessibility access:", error); + return { success: false, granted: false, error: String(error) }; + } + }); + ipcMain.handle("open-source-selector", () => { const sourceSelectorWin = getSourceSelectorWindow(); if (sourceSelectorWin) { @@ -710,6 +862,8 @@ export function registerIpcHandlers( const id = typeof recordingId === "number" ? recordingId : Date.now(); cursorTelemetryBuffer.startSession(id); cursorCaptureStartTimeMs = Date.now(); + cursorClickTimestampsMs = []; + startClickCapture(); sampleCursorPoint(); cursorCaptureInterval = setInterval(sampleCursorPoint, CURSOR_SAMPLE_INTERVAL_MS); } else { @@ -774,11 +928,19 @@ export function registerIpcHandlers( }) .sort((a: CursorTelemetryPoint, b: CursorTelemetryPoint) => a.timeMs - b.timeMs); - return { success: true, samples }; + const rawClicks = Array.isArray(parsed?.clicks) ? parsed.clicks : []; + const clicks: number[] = rawClicks + .map((value: unknown) => + typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : null, + ) + .filter((v: number | null): v is number => v !== null) + .sort((a: number, b: number) => a - b); + + return { success: true, samples, clicks }; } catch (error) { const nodeError = error as NodeJS.ErrnoException; if (nodeError.code === "ENOENT") { - return { success: true, samples: [] }; + return { success: true, samples: [], clicks: [] }; } console.error("Failed to load cursor telemetry:", error); return { @@ -786,6 +948,7 @@ export function registerIpcHandlers( message: "Failed to load cursor telemetry", error: String(error), samples: [], + clicks: [], }; } }); @@ -852,15 +1015,18 @@ export function registerIpcHandlers( ); } } - - const result = await dialog.showSaveDialog({ - title: isGif - ? mainT("dialogs", "fileDialogs.saveGif") - : mainT("dialogs", "fileDialogs.saveVideo"), - defaultPath: path.join(defaultDir, fileName), - filters, - properties: ["createDirectory", "showOverwriteConfirmation"], - }); + const dialogOptions = buildDialogOptions( + { + title: isGif + ? mainT("dialogs", "fileDialogs.saveGif") + : mainT("dialogs", "fileDialogs.saveVideo"), + defaultPath: path.join(defaultDir, fileName), + filters, + properties: ["createDirectory", "showOverwriteConfirmation"], + }, + getMainWindow(), + ); + const result = await dialog.showSaveDialog(dialogOptions); if (result.canceled || !result.filePath) { return { @@ -896,18 +1062,22 @@ export function registerIpcHandlers( ); ipcMain.handle("open-video-file-picker", async () => { try { - const result = await dialog.showOpenDialog({ - title: mainT("dialogs", "fileDialogs.selectVideo"), - defaultPath: RECORDINGS_DIR, - filters: [ - { - name: mainT("dialogs", "fileDialogs.videoFiles"), - extensions: ["webm", "mp4", "mov", "avi", "mkv"], - }, - { name: mainT("dialogs", "fileDialogs.allFiles"), extensions: ["*"] }, - ], - properties: ["openFile"], - }); + const dialogOptions = buildDialogOptions( + { + title: mainT("dialogs", "fileDialogs.selectVideo"), + defaultPath: RECORDINGS_DIR, + filters: [ + { + name: mainT("dialogs", "fileDialogs.videoFiles"), + extensions: ["webm", "mp4", "mov", "avi", "mkv"], + }, + { name: mainT("dialogs", "fileDialogs.allFiles"), extensions: ["*"] }, + ], + properties: ["openFile"], + }, + getMainWindow(), + ); + const result = await dialog.showOpenDialog(dialogOptions); if (result.canceled || result.filePaths.length === 0) { return { success: false, canceled: true }; @@ -986,18 +1156,22 @@ export function registerIpcHandlers( ? safeName : `${safeName}.${PROJECT_FILE_EXTENSION}`; - const result = await dialog.showSaveDialog({ - title: mainT("dialogs", "fileDialogs.saveProject"), - defaultPath: path.join(RECORDINGS_DIR, defaultName), - filters: [ - { - name: mainT("dialogs", "fileDialogs.openscreenProject"), - extensions: [PROJECT_FILE_EXTENSION], - }, - { name: "JSON", extensions: ["json"] }, - ], - properties: ["createDirectory", "showOverwriteConfirmation"], - }); + const dialogOptions = buildDialogOptions( + { + title: mainT("dialogs", "fileDialogs.saveProject"), + defaultPath: path.join(RECORDINGS_DIR, defaultName), + filters: [ + { + name: mainT("dialogs", "fileDialogs.openscreenProject"), + extensions: [PROJECT_FILE_EXTENSION], + }, + { name: "JSON", extensions: ["json"] }, + ], + properties: ["createDirectory", "showOverwriteConfirmation"], + }, + getMainWindow(), + ); + const result = await dialog.showSaveDialog(dialogOptions); if (result.canceled || !result.filePath) { return { @@ -1028,19 +1202,23 @@ export function registerIpcHandlers( ipcMain.handle("load-project-file", async () => { try { - const result = await dialog.showOpenDialog({ - title: mainT("dialogs", "fileDialogs.openProject"), - defaultPath: RECORDINGS_DIR, - filters: [ - { - name: mainT("dialogs", "fileDialogs.openscreenProject"), - extensions: [PROJECT_FILE_EXTENSION], - }, - { name: "JSON", extensions: ["json"] }, - { name: mainT("dialogs", "fileDialogs.allFiles"), extensions: ["*"] }, - ], - properties: ["openFile"], - }); + const dialogOptions = buildDialogOptions( + { + title: mainT("dialogs", "fileDialogs.openProject"), + defaultPath: RECORDINGS_DIR, + filters: [ + { + name: mainT("dialogs", "fileDialogs.openscreenProject"), + extensions: [PROJECT_FILE_EXTENSION], + }, + { name: "JSON", extensions: ["json"] }, + { name: mainT("dialogs", "fileDialogs.allFiles"), extensions: ["*"] }, + ], + properties: ["openFile"], + }, + getMainWindow(), + ); + const result = await dialog.showOpenDialog(dialogOptions); if (result.canceled || result.filePaths.length === 0) { return { success: false, canceled: true, message: "Open project canceled" }; diff --git a/electron/main.ts b/electron/main.ts index ad0a33f..c2bee86 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -30,6 +30,18 @@ if (process.platform === "darwin") { app.commandLine.appendSwitch("disable-features", "MacCatapLoopbackAudioForScreenShare"); } +// Enable Wayland support for proper screen capture and window management +// on Wayland compositors (Hyprland, GNOME, KDE, etc.) +if (process.platform === "linux") { + const isWayland = + process.env.XDG_SESSION_TYPE === "wayland" || process.env.WAYLAND_DISPLAY !== undefined; + if (isWayland) { + app.commandLine.appendSwitch("ozone-platform", "wayland"); + // Enable WebRTCPipeWireCapturer for screen capture on Wayland + app.commandLine.appendSwitch("enable-features", "WaylandWindowDrag,WebRTCPipeWireCapturer"); + } +} + export const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); async function ensureRecordingsDir() { @@ -124,15 +136,30 @@ function setupApplicationMenu() { template.push({ label: app.name, submenu: [ - { role: "about" }, + { + role: "about", + label: mainT("common", "actions.about") || "About OpenScreen", + }, { type: "separator" }, - { role: "services" }, + { + role: "services", + label: mainT("common", "actions.services") || "Services", + }, { type: "separator" }, - { role: "hide" }, - { role: "hideOthers" }, - { role: "unhide" }, + { + role: "hide", + label: mainT("common", "actions.hide") || "Hide OpenScreen", + }, + { + role: "hideOthers", + label: mainT("common", "actions.hideOthers") || "Hide Others", + }, + { + role: "unhide", + label: mainT("common", "actions.unhide") || "Show All", + }, { type: "separator" }, - { role: "quit" }, + { role: "quit", label: mainT("common", "actions.quit") || "Quit" }, ], }); } @@ -156,40 +183,89 @@ function setupApplicationMenu() { accelerator: "CmdOrCtrl+Shift+S", click: () => sendEditorMenuAction("menu-save-project-as"), }, - ...(isMac ? [] : [{ type: "separator" as const }, { role: "quit" as const }]), + ...(isMac + ? [] + : [ + { type: "separator" as const }, + { + role: "quit" as const, + label: mainT("common", "actions.quit") || "Quit", + }, + ]), ], }, { label: mainT("common", "actions.edit") || "Edit", submenu: [ - { role: "undo" }, - { role: "redo" }, + { role: "undo", label: mainT("common", "actions.undo") || "Undo" }, + { role: "redo", label: mainT("common", "actions.redo") || "Redo" }, { type: "separator" }, - { role: "cut" }, - { role: "copy" }, - { role: "paste" }, - { role: "selectAll" }, + { role: "cut", label: mainT("common", "actions.cut") || "Cut" }, + { role: "copy", label: mainT("common", "actions.copy") || "Copy" }, + { role: "paste", label: mainT("common", "actions.paste") || "Paste" }, + { + role: "selectAll", + label: mainT("common", "actions.selectAll") || "Select All", + }, ], }, { label: mainT("common", "actions.view") || "View", submenu: [ - { role: "reload" }, - { role: "forceReload" }, - { role: "toggleDevTools" }, + { + role: "reload", + label: mainT("common", "actions.reload") || "Reload", + }, + { + role: "forceReload", + label: mainT("common", "actions.forceReload") || "Force Reload", + }, + { + role: "toggleDevTools", + label: mainT("common", "actions.toggleDevTools") || "Toggle Developer Tools", + }, { type: "separator" }, - { role: "resetZoom" }, - { role: "zoomIn" }, - { role: "zoomOut" }, + { + role: "resetZoom", + label: mainT("common", "actions.actualSize") || "Actual Size", + }, + { + role: "zoomIn", + label: mainT("common", "actions.zoomIn") || "Zoom In", + }, + { + role: "zoomOut", + label: mainT("common", "actions.zoomOut") || "Zoom Out", + }, { type: "separator" }, - { role: "togglefullscreen" }, + { + role: "togglefullscreen", + label: mainT("common", "actions.toggleFullScreen") || "Toggle Full Screen", + }, ], }, { label: mainT("common", "actions.window") || "Window", submenu: isMac - ? [{ role: "minimize" }, { role: "zoom" }, { type: "separator" }, { role: "front" }] - : [{ role: "minimize" }, { role: "close" }], + ? [ + { + role: "minimize", + label: mainT("common", "actions.minimize") || "Minimize", + }, + { role: "zoom" }, + { type: "separator" }, + { role: "front" }, + ] + : [ + { + role: "minimize", + label: mainT("common", "actions.minimize") || "Minimize", + }, + { + role: "close", + label: mainT("common", "actions.close") || "Close", + }, + ], }, ); @@ -220,7 +296,11 @@ function getTrayIcon(filename: string, size: number) { function updateTrayMenu(recording: boolean = false) { if (!tray) return; const trayIcon = recording ? recordingTrayIcon : defaultTrayIcon; - const trayToolTip = recording ? `Recording: ${selectedSourceName}` : "OpenScreen"; + const trayToolTip = recording + ? mainT("common", "actions.recordingStatus", { + source: selectedSourceName, + }) || `Recording: ${selectedSourceName}` + : "OpenScreen"; const menuTemplate = recording ? [ { @@ -340,10 +420,11 @@ function createCountdownOverlayWindowWrapper() { return countdownOverlayWindow; } -// On macOS, applications and their menu bar stay active until the user quits -// explicitly with Cmd + Q. +// Closing every window quits the app entirely (tray icon goes too). +// The in-app "Return to Recorder" button covers the editor → HUD round-trip, +// so closing the last window is an explicit "I'm done" signal. app.on("window-all-closed", () => { - // Keep app running (macOS behavior) + app.quit(); }); app.on("activate", () => { @@ -365,6 +446,13 @@ app.on("activate", () => { // Register all IPC handlers when app is ready app.whenReady().then(async () => { + // Force the app into "regular" activation policy so the Dock icon appears. + // The HUD overlay (transparent + frameless + skipTaskbar) is the first + // window we open, and AppKit otherwise classifies us as an accessory app. + if (process.platform === "darwin") { + app.dock?.show(); + } + // Allow microphone/media permission checks session.defaultSession.setPermissionCheckHandler((_webContents, permission) => { const allowed = ["media", "audioCapture", "microphone", "videoCapture", "camera"]; diff --git a/electron/preload.ts b/electron/preload.ts index ec221b0..0d77ef2 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -40,6 +40,9 @@ contextBridge.exposeInMainWorld("electronAPI", { requestCameraAccess: () => { return ipcRenderer.invoke("request-camera-access"); }, + requestAccessibilityAccess: () => { + return ipcRenderer.invoke("request-accessibility-access"); + }, storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => { return ipcRenderer.invoke("store-recorded-video", videoData, fileName); diff --git a/nix/package.nix b/nix/package.nix index 13a8658..95018b2 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -11,7 +11,7 @@ buildNpmPackage { nodejs = nodejs_22; pname = "openscreen"; - version = "1.3.0"; + version = "1.4.0"; src = let diff --git a/package-lock.json b/package-lock.json index 4f854bc..e823ad1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@types/gif.js": "^0.2.5", "@uiw/color-convert": "^2.10.1", "@uiw/react-color-block": "^2.10.1", + "@uiw/react-color-colorful": "^2.9.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dnd-timeline": "^2.4.0", @@ -46,11 +47,13 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7", + "uiohook-napi": "^1.5.5", "uuid": "^13.0.0", "web-demuxer": "^4.0.0" }, "devDependencies": { "@biomejs/biome": "^2.4.12", + "@electron/rebuild": "^4.0.4", "@playwright/test": "^1.59.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", @@ -1077,25 +1080,18 @@ } }, "node_modules/@electron/rebuild": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", - "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", + "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", "dev": true, "license": "MIT", "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", - "detect-libc": "^2.0.1", - "got": "^11.7.0", - "graceful-fs": "^4.2.11", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", - "node-gyp": "^11.2.0", - "ora": "^5.1.0", - "read-binary-file-arch": "^1.0.6", - "semver": "^7.3.5", - "tar": "^7.5.6", - "yargs": "^17.0.1" + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" @@ -1104,19 +1100,6 @@ "node": ">=22.12.0" } }, - "node_modules/@electron/rebuild/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@electron/universal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", @@ -1790,80 +1773,6 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -2047,56 +1956,6 @@ "node": ">= 8" } }, - "node_modules/@npmcli/agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@npmcli/fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@pixi/color": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.4.3.tgz", @@ -2223,17 +2082,6 @@ "url": "^0.11.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@playwright/test": { "version": "1.59.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", @@ -4073,6 +3921,36 @@ "@babel/runtime": ">=7.19.0" } }, + "node_modules/@uiw/react-color-alpha": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@uiw/react-color-alpha/-/react-color-alpha-2.9.6.tgz", + "integrity": "sha512-DNzEVHZ0Izp4NAwzKqTcl4rLdPjSFjyZCP6Q2vKJEglugZ/bdPsmZaos9IYOrgnd1kPDmTSKZ/p8nI7vBIATGw==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.9.6", + "@uiw/react-drag-event-interactive": "2.9.6" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-alpha/node_modules/@uiw/color-convert": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.9.6.tgz", + "integrity": "sha512-w8TpU3MRcquurQJxWR1daKcRygu/a0hLP/VGsLMA3ebb41sAZGxMQLHtS+zC/e3ciFNB7BbPrSPlzOcz6w6cRg==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0" + } + }, "node_modules/@uiw/react-color-block": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/@uiw/react-color-block/-/react-color-block-2.10.1.tgz", @@ -4092,6 +3970,38 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@uiw/react-color-colorful": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@uiw/react-color-colorful/-/react-color-colorful-2.9.6.tgz", + "integrity": "sha512-h74zo+ve9Rpv7xwb1dRfoa23yN39b6eYScDIm7V2d5FzkXN6hR7jnnJ7ZUD9Joz/rdaCz1eFQD9ig+wp8+wSnQ==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.9.6", + "@uiw/react-color-alpha": "2.9.6", + "@uiw/react-color-hue": "2.9.6", + "@uiw/react-color-saturation": "2.9.6" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-colorful/node_modules/@uiw/color-convert": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.9.6.tgz", + "integrity": "sha512-w8TpU3MRcquurQJxWR1daKcRygu/a0hLP/VGsLMA3ebb41sAZGxMQLHtS+zC/e3ciFNB7BbPrSPlzOcz6w6cRg==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0" + } + }, "node_modules/@uiw/react-color-editable-input": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input/-/react-color-editable-input-2.10.1.tgz", @@ -4106,6 +4016,66 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@uiw/react-color-hue": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@uiw/react-color-hue/-/react-color-hue-2.9.6.tgz", + "integrity": "sha512-B99dW2/AHMD3py83BrXl94bhXeGCZR1FMpU/FNbIIbUrV9QTiIXDs2/SB/tMD9ltcSP59RD5Sc5m2vCb/8anjw==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.9.6", + "@uiw/react-color-alpha": "2.9.6" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-hue/node_modules/@uiw/color-convert": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.9.6.tgz", + "integrity": "sha512-w8TpU3MRcquurQJxWR1daKcRygu/a0hLP/VGsLMA3ebb41sAZGxMQLHtS+zC/e3ciFNB7BbPrSPlzOcz6w6cRg==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0" + } + }, + "node_modules/@uiw/react-color-saturation": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@uiw/react-color-saturation/-/react-color-saturation-2.9.6.tgz", + "integrity": "sha512-R1tiKbTG2WiJXerkmuaKnBFfzgyZUn08q9OjQSvNH1f3ov2/YeUVlOwQY9MbQE7ytZv+9x+1h0Lpk4QG7AdulQ==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.9.6", + "@uiw/react-drag-event-interactive": "2.9.6" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-saturation/node_modules/@uiw/color-convert": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.9.6.tgz", + "integrity": "sha512-w8TpU3MRcquurQJxWR1daKcRygu/a0hLP/VGsLMA3ebb41sAZGxMQLHtS+zC/e3ciFNB7BbPrSPlzOcz6w6cRg==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0" + } + }, "node_modules/@uiw/react-color-swatch": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/@uiw/react-color-swatch/-/react-color-swatch-2.10.1.tgz", @@ -4123,6 +4093,20 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@uiw/react-drag-event-interactive": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@uiw/react-drag-event-interactive/-/react-drag-event-interactive-2.9.6.tgz", + "integrity": "sha512-jXzt3Xis/BIYap2Hj2++gB3aEUD0mZoVNGfckurrwjAwxasxNiwkmTGxV5er3due0ZgaVKdOAfTRoYKlgZukSg==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", @@ -4327,13 +4311,13 @@ "license": "MIT" }, "node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/acorn": { @@ -4836,18 +4820,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -4936,6 +4908,7 @@ } ], "license": "MIT", + "optional": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -5035,92 +5008,6 @@ "node": ">= 10.0.0" } }, - "node_modules/cacache": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/cacache/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -5334,19 +5221,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-truncate": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", @@ -5411,16 +5285,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -5691,19 +5555,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -5772,16 +5623,6 @@ "node": ">=6" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -6001,13 +5842,6 @@ "license": "ISC", "peer": true }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -6259,17 +6093,6 @@ "dev": true, "license": "MIT" }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -6681,36 +6504,6 @@ "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", "license": "MIT" }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -6784,19 +6577,6 @@ "node": ">=6 <7 || >=8" } }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7329,17 +7109,8 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause" - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } + "license": "BSD-3-Clause", + "optional": true }, "node_modules/indent-string": { "version": "4.0.0", @@ -7370,16 +7141,6 @@ "dev": true, "license": "ISC" }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -7438,16 +7199,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -7464,19 +7215,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isbinaryfile": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", @@ -7506,22 +7244,6 @@ "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==", "license": "MIT" }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jake": { "version": "10.9.4", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", @@ -7865,23 +7587,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -8010,29 +7715,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/make-fetch-happen": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -8150,16 +7832,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -8229,136 +7901,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-fetch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", - "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/minizlib": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", @@ -8482,16 +8024,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/node-abi": { "version": "4.28.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", @@ -8550,28 +8082,49 @@ } }, "node_modules/node-gyp": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", - "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", + "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^7.4.3", + "tar": "^7.5.4", "tinyglobby": "^0.2.12", - "which": "^5.0.0" + "undici": "^6.25.0", + "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" } }, "node_modules/node-gyp/node_modules/semver": { @@ -8587,6 +8140,32 @@ "node": ">=10" } }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/node-releases": { "version": "2.0.37", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", @@ -8595,19 +8174,19 @@ "license": "MIT" }, "node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "dev": true, "license": "ISC", "dependencies": { - "abbrev": "^3.0.0" + "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/normalize-path": { @@ -8711,86 +8290,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -8817,26 +8316,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/parse-svg-path": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", @@ -8882,30 +8361,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -9301,13 +8756,13 @@ } }, "node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/progress": { @@ -9648,21 +9103,6 @@ "pify": "^2.3.0" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -9945,27 +9385,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -10226,41 +9645,12 @@ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -10309,19 +9699,6 @@ "license": "BSD-3-Clause", "optional": true }, - "node_modules/ssri": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -10346,16 +9723,6 @@ "dev": true, "license": "MIT" }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -10381,35 +9748,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -10439,20 +9777,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi/node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -10968,6 +10292,19 @@ "node": ">=14.17" } }, + "node_modules/uiohook-napi": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/uiohook-napi/-/uiohook-napi-1.5.5.tgz", + "integrity": "sha512-oSlTdnECw2GBfsJPTbBQBeE4v/EXP0EZmX6BJq5nzH/JgFaBE8JpFwEA/kLhiEP7HxQw28FViWiYgdIZzWuuJQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 16" + } + }, "node_modules/undici": { "version": "7.25.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", @@ -10985,32 +10322,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unique-filename": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/unique-slug": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -11383,16 +10694,6 @@ "node": ">=18" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, "node_modules/web-demuxer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/web-demuxer/-/web-demuxer-4.0.0.tgz", @@ -11488,38 +10789,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", diff --git a/package.json b/package.json index dbd5862..2ccb0b3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openscreen", "private": true, - "version": "1.3.0", + "version": "1.4.0", "type": "module", "packageManager": "npm@10.9.4", "engines": { @@ -21,15 +21,17 @@ "i18n:check": "node scripts/i18n-check.mjs", "preview": "vite preview", "build:mac": "tsc && vite build && electron-builder --mac", - "build:win": "tsc && vite build && electron-builder --win", - "build:linux": "tsc && vite build && electron-builder --linux AppImage deb", + "build:win": "tsc && vite build && electron-builder --win --config.npmRebuild=false", + "build:linux": "tsc && vite build && electron-builder --linux AppImage deb pacman --config.npmRebuild=false", "test": "vitest --run", "test:watch": "vitest", "build-vite": "tsc && vite build", "test:browser": "vitest --config vitest.browser.config.ts --run", "test:browser:install": "playwright install --with-deps chromium-headless-shell", "test:e2e": "playwright test", - "prepare": "husky" + "prepare": "husky", + "rebuild:native": "node ./scripts/rebuild-native.mjs", + "postinstall": "npm run rebuild:native" }, "dependencies": { "@fix-webm-duration/fix": "^1.0.1", @@ -49,6 +51,7 @@ "@types/gif.js": "^0.2.5", "@uiw/color-convert": "^2.10.1", "@uiw/react-color-block": "^2.10.1", + "@uiw/react-color-colorful": "^2.9.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dnd-timeline": "^2.4.0", @@ -70,11 +73,13 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7", + "uiohook-napi": "^1.5.5", "uuid": "^13.0.0", "web-demuxer": "^4.0.0" }, "devDependencies": { "@biomejs/biome": "^2.4.12", + "@electron/rebuild": "^4.0.4", "@playwright/test": "^1.59.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", diff --git a/scripts/rebuild-native.mjs b/scripts/rebuild-native.mjs new file mode 100644 index 0000000..e028602 --- /dev/null +++ b/scripts/rebuild-native.mjs @@ -0,0 +1,21 @@ +import { spawnSync } from "node:child_process"; +import process from "node:process"; + +// uiohook-napi click capture is macOS-only at runtime (gated in +// electron/ipc/handlers.ts). Skip the rebuild on other platforms so CI runners +// without X11 dev headers don't fail npm install. The library's prebuilt +// .node binaries are still bundled and loadable; we just don't need a fresh +// build against Electron's ABI on platforms where we don't load it. +if (process.platform !== "darwin") { + console.log( + `[rebuild:native] Skipping uiohook-napi rebuild on ${process.platform} (macOS-only).`, + ); + process.exit(0); +} + +const result = spawnSync( + process.execPath, + ["./node_modules/@electron/rebuild/lib/cli.js", "--force", "--only", "uiohook-napi"], + { stdio: "inherit" }, +); +process.exit(result.status ?? 0); diff --git a/src/App.tsx b/src/App.tsx index 4045b5d..f5fa7d6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,6 +25,20 @@ export default function App() { document.documentElement.style.background = "transparent"; document.getElementById("root")?.style.setProperty("background", "transparent"); } + + // HUD is a fixed-size BrowserWindow; pin the document shell and hide overflow + // so the renderer can't introduce scrollbars (see issue #305). + if (type === "hud-overlay") { + document.documentElement.style.height = "100%"; + document.documentElement.style.overflow = "hidden"; + document.body.style.height = "100%"; + document.body.style.margin = "0"; + document.body.style.overflow = "hidden"; + const root = document.getElementById("root"); + root?.style.setProperty("height", "100%"); + root?.style.setProperty("min-height", "0"); + root?.style.setProperty("overflow", "hidden"); + } }, [windowType]); useEffect(() => { diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 9b7d809..bffbd9c 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -314,7 +314,13 @@ export function LaunchWindow() { }; return ( -
+ // Root fills the HUD window only. Avoid w-screen/h-screen (100vw/100vh): + // 100vw can exceed the inner layout width when scrollbars affect the + // viewport (notably on Windows), causing a horizontal scrollbar once the + // recording toolbar widened (issue #305). +
{systemLocaleSuggestion && (
void; +}; + +type ColorPickerProps = + | (BaseProps & { + clearBackgroundOption?: false; + translations: Record<"colorWheel" | "colorPalette", string>; + }) + | (BaseProps & { + clearBackgroundOption: true; + translations: Record<"colorWheel" | "colorPalette" | "clearBackground", string>; + }); + +export default function ColorPicker(props: ColorPickerProps) { + const { selectedColor, colorPalette, translations, onUpdateColor } = props; + const [colorMode, setColorMode] = useState<"wheel" | "palette">("wheel"); + const [hexInput, setHexInput] = useState(selectedColor); + const [transparentColorHSVA, setTransparentColorHSVA] = useState({ + h: 0, + s: 0, + v: 0, + a: 0, + }); + + useEffect(() => { + setHexInput(selectedColor); + }, [selectedColor]); + + const getTextColor = (color: string) => { + if (color === "transparent") return "#ffffff"; + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + const luminance = 0.299 * r + 0.587 * g + 0.114 * b; + if (luminance > 186) return "#000000"; + return "#ffffff"; + }; + + // Normalize the hex input. + // Adds a # at the beginning of the input if it's not there. + const normalizeHexDraft = (raw: string) => { + const trimmed = raw.trim(); + if (trimmed === "") return ""; + if (/^[0-9A-Fa-f]/.test(trimmed[0])) return `#${trimmed}`; + return trimmed; + }; + + const handleColorInputChange = (e: React.ChangeEvent) => { + const normalized = normalizeHexDraft(e.target.value); + setHexInput(normalized); + // Check if the normalized hex is a valid hex color. + // It should follow the format #RRGGBB or #RGB. + const isValidHexColor = + /^#[0-9A-Fa-f]{3}$/.test(normalized) || /^#[0-9A-Fa-f]{6}$/.test(normalized); + if (isValidHexColor) { + onUpdateColor(normalized); + } + }; + + const toTransparent = (color: string) => { + if (color === "transparent") return; + const hsva = hexToHsva(color); + hsva.a = 0; + return hsva; + }; + return ( +
+
+ + +
+ {colorMode === "wheel" && ( + <> +
+ {selectedColor} +
+ { + onUpdateColor(color.hex); + }} + style={{ + borderRadius: "8px", + }} + disableAlpha={true} + /> + + + )} + {colorMode === "palette" && ( + { + onUpdateColor(color.hex); + }} + style={{ + width: "100%", + borderRadius: "8px", + }} + /> + )} + {props.clearBackgroundOption === true && ( + + )} +
+ ); +} diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index 4c26c88..3f8064e 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -31,6 +31,7 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { useScopedT } from "@/contexts/I18nContext"; import { type CustomFont, getCustomFonts } from "@/lib/customFonts"; import { cn } from "@/lib/utils"; +import ColorPicker from "../ui/color-picker"; import { AddCustomFontDialog } from "./AddCustomFontDialog"; import { getArrowComponent } from "./ArrowSvgs"; import { @@ -75,7 +76,6 @@ export function AnnotationSettingsPanel({ const t = useScopedT("settings"); const fileInputRef = useRef(null); const [customFonts, setCustomFonts] = useState([]); - const fontStyleLabels: Record = { classic: t("fontStyles.classic"), editor: t("fontStyles.editor"), @@ -388,15 +388,19 @@ export function AnnotationSettingsPanel({ - - { - onStyleChange({ color: color.hex }); + + { + onStyleChange({ color: color }); }} /> @@ -427,31 +431,23 @@ export function AnnotationSettingsPanel({ - - { - onStyleChange({ backgroundColor: color.hex }); + + { + onStyleChange({ backgroundColor: color }); }} /> -
diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index f21f018..76ff762 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -1,6 +1,6 @@ -import Block from "@uiw/react-color-block"; import { Bug, + ChevronDown, Crop, Download, Film, @@ -23,6 +23,7 @@ import { AccordionTrigger, } from "@/components/ui/accordion"; import { Button } from "@/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Select, SelectContent, @@ -41,6 +42,7 @@ import { cn } from "@/lib/utils"; import { resolveImageWallpaperUrl, WALLPAPER_PATHS } from "@/lib/wallpaper"; import { type AspectRatio, isPortraitAspectRatio } from "@/utils/aspectRatioUtils"; import { getTestId } from "@/utils/getTestId"; +import ColorPicker from "../ui/color-picker"; import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel"; import { BlurSettingsPanel } from "./BlurSettingsPanel"; import { CropControl } from "./CropControl"; @@ -52,13 +54,19 @@ import type { CropRegion, FigureData, PlaybackSpeed, + Rotation3DPreset, WebcamLayoutPreset, WebcamMaskShape, WebcamSizePreset, ZoomDepth, ZoomFocusMode, } from "./types"; -import { DEFAULT_WEBCAM_SIZE_PRESET, MAX_PLAYBACK_SPEED, SPEED_OPTIONS } from "./types"; +import { + DEFAULT_WEBCAM_SIZE_PRESET, + MAX_PLAYBACK_SPEED, + ROTATION_3D_PRESET_ORDER, + SPEED_OPTIONS, +} from "./types"; function CustomSpeedInput({ value, @@ -151,6 +159,12 @@ const GRADIENTS = [ ]; interface SettingsPanelProps { + cursorHighlight?: import("./videoPlayback/cursorHighlight").CursorHighlightConfig; + onCursorHighlightChange?: ( + next: import("./videoPlayback/cursorHighlight").CursorHighlightConfig, + ) => void; + // macOS only — gates the "Only on clicks" toggle (needs uiohook). + cursorHighlightSupportsClicks?: boolean; selected: string; onWallpaperChange: (path: string) => void; selectedZoomDepth?: ZoomDepth | null; @@ -160,6 +174,8 @@ interface SettingsPanelProps { hasCursorTelemetry?: boolean; selectedZoomId?: string | null; onZoomDelete?: (id: string) => void; + selectedZoomRotationPreset?: Rotation3DPreset | null; + onZoomRotationPresetChange?: (preset: Rotation3DPreset | null) => void; selectedTrimId?: string | null; onTrimDelete?: (id: string) => void; shadowIntensity?: number; @@ -238,6 +254,9 @@ const ZOOM_DEPTH_OPTIONS: Array<{ depth: ZoomDepth; label: string }> = [ ]; export function SettingsPanel({ + cursorHighlight, + onCursorHighlightChange, + cursorHighlightSupportsClicks = false, selected, onWallpaperChange, selectedZoomDepth, @@ -247,6 +266,8 @@ export function SettingsPanel({ hasCursorTelemetry = false, selectedZoomId, onZoomDelete, + selectedZoomRotationPreset, + onZoomRotationPresetChange, selectedTrimId, onTrimDelete, shadowIntensity = 0, @@ -636,6 +657,36 @@ export function SettingsPanel({ )}
)} + {zoomEnabled && ( +
+ + {t("zoom.threeD.title")} + +
+ {ROTATION_3D_PRESET_ORDER.map((preset) => { + const isActive = selectedZoomRotationPreset === preset; + return ( + + ); + })} +
+
+ )} + {zoomEnabled && (
+ {cursorHighlight && onCursorHighlightChange && ( +
+
+
+ {t("effects.cursorHighlight.title")} +
+ +
+
+ {(["dot", "ring"] as const).map((style) => ( + + ))} +
+
+
+
+ {t("effects.cursorHighlight.size")} +
+ + {cursorHighlight.sizePx}px + +
+ + onCursorHighlightChange({ + ...cursorHighlight, + sizePx: values[0], + }) + } + min={10} + max={36} + step={1} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" + /> +
+ {cursorHighlightSupportsClicks && ( +
+
+ {t("effects.cursorHighlight.onlyOnClicks")} +
+ +
+ )} +
+
+ {t("effects.cursorHighlight.color")} +
+ + + + + + + onCursorHighlightChange({ + ...cursorHighlight, + color, + }) + } + /> + + +
+
+
+
+ {t("effects.cursorHighlight.offsetX")} +
+ + {(cursorHighlight.offsetXNorm * 100).toFixed(1)}% + +
+ + onCursorHighlightChange({ + ...cursorHighlight, + offsetXNorm: values[0], + }) + } + min={-0.25} + max={0.25} + step={0.005} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" + /> +
+
+
+
+ {t("effects.cursorHighlight.offsetY")} +
+ + {(cursorHighlight.offsetYNorm * 100).toFixed(1)}% + +
+ + onCursorHighlightChange({ + ...cursorHighlight, + offsetYNorm: values[0], + }) + } + min={-0.25} + max={0.25} + step={0.005} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" + /> +
+
+ )} + - - {hasConflict && conflict?.conflictWith.type === "configurable" && ( -
- - ⚠{" "} - {t("alreadyUsedBy", { action: t(`actions.${conflict.conflictWith.action}`) })} - -
- - -
+
+
+

+ {t("configurable")} +

+ {SHORTCUT_ACTIONS.map((action) => { + const isCapturing = captureFor === action; + const hasConflict = conflict?.forAction === action; + return ( +
+
+ {t(`actions.${action}`)} +
- )} + {hasConflict && conflict?.conflictWith.type === "configurable" && ( +
+ + ⚠{" "} + {t("alreadyUsedBy", { + action: t(`actions.${conflict.conflictWith.action}`), + })} + +
+ + +
+
+ )} +
+ ); + })} +
+ +
+

+ {t("fixed")} +

+ {FIXED_SHORTCUTS.map(({ i18nKey, label, display }) => ( +
+ + {t(`fixedActions.${i18nKey}`, { defaultValue: label })} + + + {display} +
- ); - })} + ))} +
+ +

{t("helpText")}

-
-

- {t("fixed")} -

- {FIXED_SHORTCUTS.map(({ i18nKey, label, display }) => ( -
- - {t(`fixedActions.${i18nKey}`, { defaultValue: label })} - - - {display} - -
- ))} -
- -

{t("helpText")}

- - +
@@ -1972,6 +2014,9 @@ export default function VideoEditor() { {/* Right section: settings panel */}
pushState({ cursorHighlight: next })} + cursorHighlightSupportsClicks={isMac} selected={wallpaper} onWallpaperChange={(w) => pushState({ wallpaper: w })} selectedZoomDepth={ @@ -1987,6 +2032,12 @@ export default function VideoEditor() { hasCursorTelemetry={cursorTelemetry.length > 0} selectedZoomId={selectedZoomId} onZoomDelete={handleZoomDelete} + selectedZoomRotationPreset={ + selectedZoomId + ? (zoomRegions.find((z) => z.id === selectedZoomId)?.rotationPreset ?? null) + : null + } + onZoomRotationPresetChange={handleZoomRotationPresetChange} selectedTrimId={selectedTrimId} onTrimDelete={handleTrimDelete} shadowIntensity={shadowIntensity} diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 35e0077..ee52bd9 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -36,6 +36,11 @@ import { AnnotationOverlay } from "./AnnotationOverlay"; import { type AnnotationRegion, type BlurData, + computeRotation3DContainScale, + DEFAULT_ROTATION_3D, + isRotation3DIdentity, + lerpRotation3D, + rotation3DPerspective, type SpeedRegion, type TrimRegion, ZOOM_DEPTH_SCALES, @@ -51,7 +56,17 @@ import { ZOOM_SCALE_DEADZONE, ZOOM_TRANSLATION_DEADZONE_PX, } from "./videoPlayback/constants"; -import { adaptiveSmoothFactor, smoothCursorFocus } from "./videoPlayback/cursorFollowUtils"; +import { + adaptiveSmoothFactor, + interpolateCursorAt, + smoothCursorFocus, +} from "./videoPlayback/cursorFollowUtils"; +import { + type CursorHighlightConfig, + clickEmphasisAlpha, + DEFAULT_CURSOR_HIGHLIGHT, + drawCursorHighlightGraphics, +} from "./videoPlayback/cursorHighlight"; import { clampFocusToStage as clampFocusToStageUtil } from "./videoPlayback/focusUtils"; import { layoutVideoContent as layoutVideoContentUtil } from "./videoPlayback/layoutUtils"; import { clamp01 } from "./videoPlayback/mathUtils"; @@ -110,6 +125,8 @@ interface VideoPlaybackProps { onBlurDataChange?: (id: string, blurData: BlurData) => void; onBlurDataCommit?: () => void; cursorTelemetry?: import("./types").CursorTelemetryPoint[]; + cursorHighlight?: CursorHighlightConfig; + cursorClickTimestamps?: number[]; } export interface VideoPlaybackRef { @@ -168,6 +185,8 @@ const VideoPlayback = forwardRef( onBlurDataChange, onBlurDataCommit, cursorTelemetry = [], + cursorHighlight = DEFAULT_CURSOR_HIGHLIGHT, + cursorClickTimestamps = [], }, ref, ) => { @@ -186,11 +205,16 @@ const VideoPlayback = forwardRef( const overlayRef = useRef(null); const focusIndicatorRef = useRef(null); + const composite3DRef = useRef(null); + const outerWrapperRef = useRef(null); const [webcamLayout, setWebcamLayout] = useState(null); const [webcamDimensions, setWebcamDimensions] = useState(null); const currentTimeRef = useRef(0); const zoomRegionsRef = useRef([]); const cursorTelemetryRef = useRef([]); + const cursorHighlightRef = useRef(DEFAULT_CURSOR_HIGHLIGHT); + const cursorClickTimestampsRef = useRef([]); + const cursorHighlightGraphicsRef = useRef(null); const selectedZoomIdRef = useRef(null); const animationStateRef = useRef({ scale: 1, @@ -215,6 +239,9 @@ const VideoPlayback = forwardRef( const maskGraphicsRef = useRef(null); const isPlayingRef = useRef(isPlaying); const isSeekingRef = useRef(false); + const isScrubbingRef = useRef(false); + const scrubEndTimerRef = useRef(null); + const [isScrubbing, setIsScrubbing] = useState(false); const allowPlaybackRef = useRef(false); const lockedVideoDimensionsRef = useRef<{ width: number; @@ -515,6 +542,17 @@ const VideoPlayback = forwardRef( cursorTelemetryRef.current = cursorTelemetry; }, [cursorTelemetry]); + useEffect(() => { + cursorHighlightRef.current = cursorHighlight; + if (cursorHighlightGraphicsRef.current) { + drawCursorHighlightGraphics(cursorHighlightGraphicsRef.current, cursorHighlight); + } + }, [cursorHighlight]); + + useEffect(() => { + cursorClickTimestampsRef.current = cursorClickTimestamps; + }, [cursorClickTimestamps]); + useEffect(() => { selectedZoomIdRef.current = selectedZoomId; }, [selectedZoomId]); @@ -583,6 +621,24 @@ const VideoPlayback = forwardRef( }; }, [pixiReady, videoReady, layoutVideoContent]); + // Drop the PIXI canvas resolution to 1.0 while scrubbing (the user is + // navigating, not previewing) and restore native DPR on play/idle so the + // preview stays faithful. Mutating renderer.resolution per-frame would + // thrash texture uploads; we only do it on scrub-state transitions. + useEffect(() => { + if (!pixiReady) return; + const app = appRef.current; + const container = containerRef.current; + if (!app || !container) return; + + const targetResolution = isScrubbing ? 1 : window.devicePixelRatio || 1; + if (app.renderer.resolution === targetResolution) return; + + app.renderer.resolution = targetResolution; + app.renderer.resize(container.clientWidth, container.clientHeight); + layoutVideoContentRef.current?.(); + }, [isScrubbing, pixiReady]); + useEffect(() => { if (!pixiReady || !videoReady) return; updateOverlayForRegion(selectedZoom); @@ -738,6 +794,12 @@ const VideoPlayback = forwardRef( videoContainer.mask = maskGraphics; maskGraphicsRef.current = maskGraphics; + const cursorHighlightGraphics = new Graphics(); + cursorHighlightGraphics.visible = false; + videoContainer.addChild(cursorHighlightGraphics); + cursorHighlightGraphicsRef.current = cursorHighlightGraphics; + drawCursorHighlightGraphics(cursorHighlightGraphics, cursorHighlightRef.current); + animationStateRef.current = { scale: 1, focusX: DEFAULT_FOCUS.cx, @@ -770,6 +832,9 @@ const VideoPlayback = forwardRef( onTimeUpdate: (time) => onTimeUpdateRef.current(time), trimRegionsRef, speedRegionsRef, + isScrubbingRef, + scrubEndTimerRef, + onScrubChange: (scrubbing) => setIsScrubbing(scrubbing), }); video.addEventListener("play", handlePlay); @@ -797,6 +862,11 @@ const VideoPlayback = forwardRef( videoContainer.removeChild(maskGraphics); maskGraphics.destroy(); } + if (cursorHighlightGraphicsRef.current) { + videoContainer.removeChild(cursorHighlightGraphicsRef.current); + cursorHighlightGraphicsRef.current.destroy(); + cursorHighlightGraphicsRef.current = null; + } videoContainer.mask = null; maskGraphicsRef.current = null; if (blurFilterRef.current) { @@ -858,8 +928,10 @@ const VideoPlayback = forwardRef( }; let lastMotionBlurActive: boolean | null = null; + let lastTransformIsIdentity = true; + let lastPerspectiveValue = 0; const ticker = () => { - const { region, strength, blendedScale, transition } = findDominantRegion( + const { region, strength, blendedScale, rotation3D, transition } = findDominantRegion( zoomRegionsRef.current, currentTimeRef.current, { @@ -1016,7 +1088,41 @@ const VideoPlayback = forwardRef( motionVector, ); - const isMotionBlurActive = (motionBlurAmountRef.current || 0) > 0 && isPlayingRef.current; + const cursorGraphics = cursorHighlightGraphicsRef.current; + const cursorConfig = cursorHighlightRef.current; + const lockedDims = lockedVideoDimensionsRef.current; + if (cursorGraphics) { + if (cursorConfig.enabled && lockedDims && cursorTelemetryRef.current.length > 0) { + const emphasisAlpha = clickEmphasisAlpha( + currentTimeRef.current, + cursorClickTimestampsRef.current, + cursorConfig, + ); + const cursorPoint = + emphasisAlpha > 0 + ? interpolateCursorAt(cursorTelemetryRef.current, currentTimeRef.current) + : null; + if (cursorPoint) { + const baseScale = baseScaleRef.current; + const baseOffset = baseOffsetRef.current; + const cx = cursorPoint.cx + cursorConfig.offsetXNorm; + const cy = cursorPoint.cy + cursorConfig.offsetYNorm; + cursorGraphics.position.set( + baseOffset.x + cx * lockedDims.width * baseScale, + baseOffset.y + cy * lockedDims.height * baseScale, + ); + cursorGraphics.alpha = emphasisAlpha; + cursorGraphics.visible = true; + } else { + cursorGraphics.visible = false; + } + } else { + cursorGraphics.visible = false; + } + } + + const isMotionBlurActive = + (motionBlurAmountRef.current || 0) > 0 && isPlayingRef.current && !isScrubbingRef.current; if (isMotionBlurActive !== lastMotionBlurActive && videoContainerRef.current) { if (isMotionBlurActive) { @@ -1032,6 +1138,44 @@ const VideoPlayback = forwardRef( lastMotionBlurActive = false; } } + + const composite3D = composite3DRef.current; + const outerWrapper = outerWrapperRef.current; + if (composite3D && outerWrapper) { + const effectiveRotation = + region && targetProgress > 0 && !shouldShowUnzoomedView + ? lerpRotation3D(DEFAULT_ROTATION_3D, rotation3D, targetProgress) + : DEFAULT_ROTATION_3D; + const isIdentity = isRotation3DIdentity(effectiveRotation); + if (isIdentity) { + if (!lastTransformIsIdentity) { + composite3D.style.transform = ""; + composite3D.style.willChange = "auto"; + lastTransformIsIdentity = true; + } + if (lastPerspectiveValue !== 0) { + outerWrapper.style.perspective = ""; + lastPerspectiveValue = 0; + } + } else { + const wrapperW = outerWrapper.clientWidth || 1; + const wrapperH = outerWrapper.clientHeight || 1; + const persp = rotation3DPerspective(wrapperW, wrapperH); + const containScale = computeRotation3DContainScale( + effectiveRotation, + wrapperW, + wrapperH, + persp, + ); + composite3D.style.transform = `scale(${containScale}) rotateX(${effectiveRotation.rotationX}deg) rotateY(${effectiveRotation.rotationY}deg) rotateZ(${effectiveRotation.rotationZ}deg)`; + composite3D.style.willChange = "transform"; + lastTransformIsIdentity = false; + if (persp !== lastPerspectiveValue) { + outerWrapper.style.perspective = `${persp}px`; + lastPerspectiveValue = persp; + } + } + } }; app.ticker.add(ticker); @@ -1153,6 +1297,10 @@ const VideoPlayback = forwardRef( cancelAnimationFrame(videoReadyRafRef.current); videoReadyRafRef.current = null; } + if (scrubEndTimerRef.current !== null) { + window.clearTimeout(scrubEndTimerRef.current); + scrubEndTimerRef.current = null; + } }; }, []); @@ -1169,6 +1317,7 @@ const VideoPlayback = forwardRef( return (
( }} />
0 - ? `drop-shadow(0 ${shadowIntensity * 12}px ${shadowIntensity * 48}px rgba(0,0,0,${shadowIntensity * 0.7})) drop-shadow(0 ${shadowIntensity * 4}px ${shadowIntensity * 16}px rgba(0,0,0,${shadowIntensity * 0.5})) drop-shadow(0 ${shadowIntensity * 2}px ${shadowIntensity * 8}px rgba(0,0,0,${shadowIntensity * 0.3}))` - : "none", + transformStyle: "preserve-3d", + transformOrigin: "center center", }} - /> - {webcamVideoPath && - (() => { - const clipPath = getCssClipPath(webcamLayout?.maskShape ?? "rectangle"); - const useClipPath = !!clipPath; - return ( -
-
- ); - })()} - {/* Only render overlay after PIXI and video are fully initialized */} - {pixiReady && videoReady && ( + >
-
- {(() => { - const filteredAnnotations = (annotationRegions || []).filter((annotation) => { - if (typeof annotation.startMs !== "number" || typeof annotation.endMs !== "number") - return false; - - if (annotation.id === selectedAnnotationId) return true; - - const timeMs = Math.round(currentTime * 1000); - return timeMs >= annotation.startMs && timeMs < annotation.endMs; - }); - - const filteredBlurRegions = (blurRegions || []).filter((blurRegion) => { - if (typeof blurRegion.startMs !== "number" || typeof blurRegion.endMs !== "number") - return false; - - if (blurRegion.id === selectedBlurId) return true; - - const timeMs = Math.round(currentTime * 1000); - return timeMs >= blurRegion.startMs && timeMs < blurRegion.endMs; - }); - - const sorted = [ - ...filteredAnnotations.map((annotation) => ({ - kind: "annotation" as const, - region: annotation, - })), - ...filteredBlurRegions.map((blurRegion) => ({ - kind: "blur" as const, - region: blurRegion, - })), - ].sort((a, b) => a.region.zIndex - b.region.zIndex); - const previewSnapshotCanvas = - filteredBlurRegions.length > 0 - ? (() => { - const app = appRef.current; - if (!app?.renderer?.extract) return null; - try { - return app.renderer.extract.canvas(app.stage); - } catch { - return null; - } - })() - : null; - - // Handle click-through cycling: when clicking same annotation, cycle to next - const handleAnnotationClick = (clickedId: string) => { - if (!onSelectAnnotation) return; - - // If clicking on already selected annotation and there are multiple overlapping - if (clickedId === selectedAnnotationId && filteredAnnotations.length > 1) { - // Find current index and cycle to next - const currentIndex = filteredAnnotations.findIndex((a) => a.id === clickedId); - const nextIndex = (currentIndex + 1) % filteredAnnotations.length; - onSelectAnnotation(filteredAnnotations[nextIndex].id); - } else { - // First click or clicking different annotation - onSelectAnnotation(clickedId); - } - }; - - const handleBlurClick = (clickedId: string) => { - if (!onSelectBlur) return; - - if (clickedId === selectedBlurId && filteredBlurRegions.length > 1) { - const currentIndex = filteredBlurRegions.findIndex((a) => a.id === clickedId); - const nextIndex = (currentIndex + 1) % filteredBlurRegions.length; - onSelectBlur(filteredBlurRegions[nextIndex].id); - } else { - onSelectBlur(clickedId); - } - }; - - return sorted.map((item) => ( - `${Math.round(p.x)}_${Math.round(p.y)}`).join("-")}` - : `${item.region.id}-${overlaySize.width}-${overlaySize.height}` - } - annotation={item.region} - isSelected={ - item.kind === "blur" - ? item.region.id === selectedBlurId - : item.region.id === selectedAnnotationId - } - containerWidth={overlaySize.width} - containerHeight={overlaySize.height} - onPositionChange={(id, position) => - item.kind === "blur" - ? onBlurPositionChange?.(id, position) - : onAnnotationPositionChange?.(id, position) - } - onSizeChange={(id, size) => - item.kind === "blur" - ? onBlurSizeChange?.(id, size) - : onAnnotationSizeChange?.(id, size) - } - onBlurDataChange={ - item.kind === "blur" - ? (id, blurData) => onBlurDataChange?.(id, blurData) - : undefined - } - onBlurDataCommit={item.kind === "blur" ? onBlurDataCommit : undefined} - onClick={item.kind === "blur" ? handleBlurClick : handleAnnotationClick} - zIndex={item.region.zIndex} - isSelectedBoost={ - item.kind === "blur" - ? item.region.id === selectedBlurId - : item.region.id === selectedAnnotationId - } - previewSourceCanvas={previewSnapshotCanvas} - previewFrameVersion={Math.round(currentTime * 1000)} - /> - )); + ref={containerRef} + className="absolute inset-0" + style={{ + filter: + showShadow && shadowIntensity > 0 + ? `drop-shadow(0 ${shadowIntensity * 12}px ${shadowIntensity * 48}px rgba(0,0,0,${shadowIntensity * 0.7})) drop-shadow(0 ${shadowIntensity * 4}px ${shadowIntensity * 16}px rgba(0,0,0,${shadowIntensity * 0.5})) drop-shadow(0 ${shadowIntensity * 2}px ${shadowIntensity * 8}px rgba(0,0,0,${shadowIntensity * 0.3}))` + : "none", + }} + /> + {webcamVideoPath && + (() => { + const clipPath = getCssClipPath(webcamLayout?.maskShape ?? "rectangle"); + const useClipPath = !!clipPath; + return ( +
+
+ ); })()} -
- )} + {/* Only render overlay after PIXI and video are fully initialized */} + {pixiReady && videoReady && ( +
+
+ {(() => { + const filteredAnnotations = (annotationRegions || []).filter((annotation) => { + if ( + typeof annotation.startMs !== "number" || + typeof annotation.endMs !== "number" + ) + return false; + + if (annotation.id === selectedAnnotationId) return true; + + const timeMs = Math.round(currentTime * 1000); + return timeMs >= annotation.startMs && timeMs < annotation.endMs; + }); + + const filteredBlurRegions = (blurRegions || []).filter((blurRegion) => { + if ( + typeof blurRegion.startMs !== "number" || + typeof blurRegion.endMs !== "number" + ) + return false; + + if (blurRegion.id === selectedBlurId) return true; + + const timeMs = Math.round(currentTime * 1000); + return timeMs >= blurRegion.startMs && timeMs < blurRegion.endMs; + }); + + const sorted = [ + ...filteredAnnotations.map((annotation) => ({ + kind: "annotation" as const, + region: annotation, + })), + ...filteredBlurRegions.map((blurRegion) => ({ + kind: "blur" as const, + region: blurRegion, + })), + ].sort((a, b) => a.region.zIndex - b.region.zIndex); + const previewSnapshotCanvas = + filteredBlurRegions.length > 0 + ? (() => { + const app = appRef.current; + if (!app?.renderer?.extract) return null; + try { + return app.renderer.extract.canvas(app.stage); + } catch { + return null; + } + })() + : null; + + // Handle click-through cycling: when clicking same annotation, cycle to next + const handleAnnotationClick = (clickedId: string) => { + if (!onSelectAnnotation) return; + + // If clicking on already selected annotation and there are multiple overlapping + if (clickedId === selectedAnnotationId && filteredAnnotations.length > 1) { + // Find current index and cycle to next + const currentIndex = filteredAnnotations.findIndex((a) => a.id === clickedId); + const nextIndex = (currentIndex + 1) % filteredAnnotations.length; + onSelectAnnotation(filteredAnnotations[nextIndex].id); + } else { + // First click or clicking different annotation + onSelectAnnotation(clickedId); + } + }; + + const handleBlurClick = (clickedId: string) => { + if (!onSelectBlur) return; + + if (clickedId === selectedBlurId && filteredBlurRegions.length > 1) { + const currentIndex = filteredBlurRegions.findIndex((a) => a.id === clickedId); + const nextIndex = (currentIndex + 1) % filteredBlurRegions.length; + onSelectBlur(filteredBlurRegions[nextIndex].id); + } else { + onSelectBlur(clickedId); + } + }; + + return sorted.map((item) => ( + `${Math.round(p.x)}_${Math.round(p.y)}`).join("-")}` + : `${item.region.id}-${overlaySize.width}-${overlaySize.height}` + } + annotation={item.region} + isSelected={ + item.kind === "blur" + ? item.region.id === selectedBlurId + : item.region.id === selectedAnnotationId + } + containerWidth={overlaySize.width} + containerHeight={overlaySize.height} + onPositionChange={(id, position) => + item.kind === "blur" + ? onBlurPositionChange?.(id, position) + : onAnnotationPositionChange?.(id, position) + } + onSizeChange={(id, size) => + item.kind === "blur" + ? onBlurSizeChange?.(id, size) + : onAnnotationSizeChange?.(id, size) + } + onBlurDataChange={ + item.kind === "blur" + ? (id, blurData) => onBlurDataChange?.(id, blurData) + : undefined + } + onBlurDataCommit={item.kind === "blur" ? onBlurDataCommit : undefined} + onClick={item.kind === "blur" ? handleBlurClick : handleAnnotationClick} + zIndex={item.region.zIndex} + isSelectedBoost={ + item.kind === "blur" + ? item.region.id === selectedBlurId + : item.region.id === selectedAnnotationId + } + previewSourceCanvas={previewSnapshotCanvas} + previewFrameVersion={Math.round(currentTime * 1000)} + /> + )); + })()} +
+ )} +