From 9a5ff5c0a21c57c2c2c9b224b3925587d7c4dd3c Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:50:09 +1000 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20ui=20overhaul=20=E2=80=94=20Phospho?= =?UTF-8?q?r=20icons,=20redesigned=20launcher,=20editor=20layout,=20and=20?= =?UTF-8?q?component=20refresh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace all lucide-react imports with @phosphor-icons/react across every component (LaunchWindow, AnnotationSettingsPanel, ExtensionManager, ExtensionIcon, VideoPlayback, AnnotationOverlay, CropControl, TutorialHelp, PlaybackControls, all shadcn ui/ primitives, etc.) - ExtensionIcon: new Phosphor-based resolver with Lucide name alias map for backwards-compatible extension manifests - LaunchWindow / SourceSelector: full redesign — new icon rail layout, updated backgrounds, improved source selection UX, audio/video controls, countdown overlay - VideoPlayback: overhaul playback controls and preview panel layout - Timeline items: border-radius 16px→8px, cyan→blue unification, pill height 85% for breathing room, resize handle vertical centering - shadcn ui/: refresh accordion, dialog, dropdown, select, slider, switch, tabs, toggle, popover, button, card, input, label, sonner, content-clamp, audio-level-meter - App.tsx / index.css / App.css / tailwind.config: global style updates - types.ts, captionLayout.ts, captionStyle.ts, webcamOverlay.ts: data model updates to support new timeline and export features - package.json: add @phosphor-icons/react, bump dependencies - scripts/, .eslintrc, biome, tsconfig, vite.config: tooling alignment - extension-sources: update built-in extension bundles --- .eslintrc.cjs | 34 +- README.md | 20 +- README.zh-CN.md | 14 +- biome.json | 30 +- package-lock.json | 19 +- package.json | 1 + postcss.config.cjs | 10 +- scripts/benchmark-export-queues.mjs | 94 +- scripts/build-native-helpers.mjs | 12 +- scripts/build-whisper-runtime.mjs | 11 +- scripts/build-windows-capture.mjs | 218 +- scripts/i18n-check.mjs | 134 +- scripts/native-helper-manifest.mjs | 21 +- scripts/postinstall.mjs | 8 +- src/App.css | 83 +- src/App.tsx | 184 +- src/components/launch/LaunchWindow.tsx | 521 +- .../launch/SourceSelector.module.css | 73 +- src/components/launch/SourceSelector.tsx | 483 +- src/components/launch/UpdateToastWindow.tsx | 72 +- src/components/ui/accordion.tsx | 87 +- src/components/ui/audio-level-meter.tsx | 56 +- src/components/ui/button.tsx | 95 +- src/components/ui/card.tsx | 132 +- src/components/ui/content-clamp.tsx | 166 +- src/components/ui/dialog.tsx | 184 +- src/components/ui/dropdown-menu.tsx | 320 +- src/components/ui/input.tsx | 46 +- src/components/ui/label.tsx | 42 +- src/components/ui/popover.tsx | 91 +- src/components/ui/select.tsx | 256 +- src/components/ui/slider.tsx | 40 +- src/components/ui/sonner.tsx | 37 +- src/components/ui/switch.tsx | 51 +- src/components/ui/tabs.tsx | 83 +- src/components/ui/toggle-group.tsx | 104 +- src/components/ui/toggle.tsx | 73 +- .../video-editor/AddCustomFontDialog.tsx | 363 +- .../video-editor/AnnotationOverlay.tsx | 436 +- .../video-editor/AnnotationSettingsPanel.tsx | 1388 ++--- src/components/video-editor/ArrowSvgs.tsx | 325 +- src/components/video-editor/CropControl.tsx | 465 +- .../video-editor/ExportSettingsMenu.tsx | 221 +- src/components/video-editor/ExtensionIcon.tsx | 199 +- .../video-editor/ExtensionManager.tsx | 1943 +++---- .../video-editor/FormatSelector.tsx | 6 +- .../video-editor/KeyboardShortcutsHelp.tsx | 14 +- .../video-editor/PlaybackControls.tsx | 16 +- .../video-editor/ProjectBrowserDialog.tsx | 26 +- .../video-editor/ShortcutsConfigDialog.tsx | 473 +- src/components/video-editor/SliderControl.tsx | 100 +- src/components/video-editor/TutorialHelp.tsx | 126 +- src/components/video-editor/VideoPlayback.tsx | 4772 +++++++++-------- src/components/video-editor/audio.test.ts | 77 +- src/components/video-editor/captionLayout.ts | 820 +-- src/components/video-editor/captionStyle.ts | 20 +- src/components/video-editor/index.ts | 11 +- src/components/video-editor/types.ts | 28 +- src/components/video-editor/webcamOverlay.ts | 13 +- src/index.css | 424 +- src/lib/assetPath.ts | 279 +- src/lib/customFonts.ts | 365 +- src/lib/geometry/squircle.ts | 51 +- src/lib/mediaTiming.test.ts | 194 +- src/lib/mediaTiming.ts | 163 +- src/lib/shortcuts.ts | 181 +- src/lib/utils.ts | 7 +- src/lib/wallpapers.ts | 295 +- src/main.tsx | 31 +- src/utils/aspectRatioUtils.ts | 130 +- src/utils/platformUtils.ts | 56 +- tailwind.config.cjs | 142 +- tsconfig.json | 1 + tsconfig.node.json | 23 +- vite.config.ts | 145 +- 75 files changed, 9478 insertions(+), 8756 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index dc6ce092b..f63fe7dcb 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,19 +1,15 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', - ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], - rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, -} - +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", + ], + ignorePatterns: ["dist", ".eslintrc.cjs"], + parser: "@typescript-eslint/parser", + plugins: ["react-refresh"], + rules: { + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + }, +}; diff --git a/README.md b/README.md index 1c4ec2048..f9ae83d9f 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Recordly is a desktop app for recording and editing screen captures with motion- Recordly runs on: -- **macOS** 13.0+ +- **macOS** 14.0+ - **Windows** 10 Build 19041+ - **Linux** on modern distros @@ -61,6 +61,12 @@ Use drag-and-drop timeline tools for zooms, trims, speed regions, annotations, e Recordly timeline editor screenshot

+## Extensions & Marketplace + +Recordly has a community-driven extension system. Anyone can build and publish extensions that add new capabilities to Recordly — cursor click sounds, device frames, browser mockups, wallpapers, render hooks, settings panels, and more. + +Browse and install community extensions from the [Recordly Marketplace](https://marketplace.recordly.dev/extensions). + --- ## All Features @@ -233,7 +239,7 @@ xattr -rd com.apple.quarantine /Applications/Recordly.app | Platform | Minimum version | Notes | |---|---|---| -| **macOS** | macOS 13.0 (Ventura) | Required for ScreenCaptureKit audio capture. | +| **macOS** | macOS 14.0 (Sonoma) | Required for ScreenCaptureKit audio and microphone capture. | | **Windows** | Windows 10 20H1 (Build 19041, May 2020) | Required for the native Windows Graphics Capture (WGC) helper and best cursor-hiding behavior. | | **Linux** | Any modern distro | Recording works through Electron capture. System audio generally requires PipeWire. | @@ -304,7 +310,7 @@ System audio support varies by platform. - Usually requires PipeWire **macOS** -- Requires macOS 13.0+ and the ScreenCaptureKit-based workflow +- Requires macOS 14.0+ and the ScreenCaptureKit-based workflow --- @@ -382,14 +388,6 @@ Recordly is licensed under the **AGPL 3.0**. --- -## Extensions - -Recordly has an extension system for adding device frames, click effects, render hooks, sounds, and settings panels. Built-in extensions ship under `public/builtin-extensions/`. - -See [EXTENSIONS.md](./EXTENSIONS.md) for the full API reference and examples. - ---- - # Credits ## Acknowledgements diff --git a/README.zh-CN.md b/README.zh-CN.md index b3e04f24d..28b3f59b6 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -26,7 +26,7 @@ Recordly 是一款桌面应用,用于录制并编辑屏幕内容,内置面 Recordly 运行于: -- **macOS** 13.0+ +- **macOS** 14.0+ - **Windows** 10 Build 19041+ - **Linux** 现代发行版 @@ -61,6 +61,12 @@ Recordly 可以根据操作自动强调重点区域,平滑光标运动,添 Recordly timeline editor screenshot

+## 扩展与市场 + +Recordly 拥有一个社区驱动的扩展系统。任何人都可以构建和发布扩展来为 Recordly 添加新功能——光标点击音效、设备边框、浏览器模拟外壳、壁纸、渲染钩子、设置面板等等。 + +浏览并安装社区扩展:[Recordly 扩展市场](https://marketplace.recordly.dev/extensions)。 + --- ## 全部功能 @@ -234,8 +240,8 @@ xattr -rd com.apple.quarantine /Applications/Recordly.app | 平台 | 最低版本 | 说明 | |---|---|---| -| **macOS** | macOS 13.0 (Ventura) | 使用 ScreenCaptureKit 音频捕获所必需。 | -| **Windows** | Windows 10 20H1(Build 19041,2020 年 5 月) | 原生 Windows Graphics Capture(WGC)辅助程序以及最佳光标隐藏行为所必需。 | +| **macOS** | macOS 14.0 (Sonoma) | 使用 ScreenCaptureKit 捕获音频和麦克风所必需。 | +| **Windows** | Windows 10 20H1(Build 19041,2020 年 5 月) | 原生 Windows Graphics Capture(WGC)辅助程序及最佳光标隐藏行为所必需。 | | **Linux** | 任意现代发行版 | 通过 Electron 捕获录制。系统音频通常需要 PipeWire。 | > [!IMPORTANT] @@ -305,7 +311,7 @@ Recordly 会在录制画面上渲染一个经过美化的光标叠加层,但 - 通常需要 PipeWire **macOS** -- 需要 macOS 13.0+ 以及基于 ScreenCaptureKit 的工作流 +- 需要 macOS 14.0+ 和基于 ScreenCaptureKit 的工作流 --- diff --git a/biome.json b/biome.json index 98c88e9ac..798cd2720 100644 --- a/biome.json +++ b/biome.json @@ -44,7 +44,7 @@ "noUnsafeOptionalChaining": "error", "noUnusedLabels": "error", "noUnusedVariables": "error", - "useExhaustiveDependencies": "warn", + "useExhaustiveDependencies": "off", "useHookAtTopLevel": "error", "useIsNan": "error", "useValidForDirection": "error", @@ -55,7 +55,7 @@ "noNamespace": "error", "useArrayLiterals": "error", "useAsConstAssertion": "error", - "useComponentExportOnlyModules": "warn" + "useComponentExportOnlyModules": "off" }, "suspicious": { "noAssignInExpressions": "error", @@ -96,6 +96,7 @@ "includes": ["**", "**/dist", "**/.eslintrc.cjs", "**", "**/dist", "**/.eslintrc.cjs"] }, "javascript": { "formatter": { "quoteStyle": "double" } }, + "css": { "parser": { "tailwindDirectives": true } }, "overrides": [ { "includes": ["*.ts", "*.tsx", "*.mts", "*.cts"], @@ -156,6 +157,31 @@ } } } + }, + { + "includes": ["src/components/ui/**/*.tsx"], + "linter": { + "rules": { + "style": { + "useComponentExportOnlyModules": "off" + } + } + } + }, + { + "includes": ["**/*.test.ts", "**/*.test.tsx"], + "linter": { + "rules": { + "correctness": { + "noEmptyPattern": "off", + "noUnusedVariables": "off" + }, + "suspicious": { + "noEmptyBlockStatements": "off", + "noExplicitAny": "off" + } + } + } } ], "assist": { diff --git a/package-lock.json b/package-lock.json index 46d2bfe70..8367a4c0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.1.23", "hasInstallScript": true, "dependencies": { + "@phosphor-icons/react": "^2.1.10", "capturekit": "^1.0.13", "electron-updater": "^6.8.3", "ffmpeg-static": "^5.3.0", @@ -2843,6 +2844,19 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", + "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, "node_modules/@pixi/color": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.4.3.tgz", @@ -9046,7 +9060,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -9361,7 +9374,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -11227,7 +11239,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -11240,7 +11251,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -11948,7 +11958,6 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" diff --git a/package.json b/package.json index 68af12b76..d9e99cb95 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "test:watch": "vitest" }, "dependencies": { + "@phosphor-icons/react": "^2.1.10", "capturekit": "^1.0.13", "electron-updater": "^6.8.3", "ffmpeg-static": "^5.3.0", diff --git a/postcss.config.cjs b/postcss.config.cjs index 33ad091d2..e873f1a4f 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -1,6 +1,6 @@ module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/scripts/benchmark-export-queues.mjs b/scripts/benchmark-export-queues.mjs index db6902c0f..3028ab12d 100644 --- a/scripts/benchmark-export-queues.mjs +++ b/scripts/benchmark-export-queues.mjs @@ -17,8 +17,14 @@ const rendererEntry = path.join(repoRoot, "dist", "index.html"); const width = parseEvenInteger(process.env.RECORDLY_BENCH_EXPORT_WIDTH ?? "1280", "Width"); const height = parseEvenInteger(process.env.RECORDLY_BENCH_EXPORT_HEIGHT ?? "720", "Height"); const frameRate = parsePositiveInteger(process.env.RECORDLY_BENCH_EXPORT_FPS ?? "60", "Frame rate"); -const durationSeconds = parsePositiveInteger(process.env.RECORDLY_BENCH_EXPORT_DURATION ?? "15", "Duration"); -const timeoutMs = parsePositiveInteger(process.env.RECORDLY_BENCH_EXPORT_TIMEOUT_MS ?? "180000", "Timeout"); +const durationSeconds = parsePositiveInteger( + process.env.RECORDLY_BENCH_EXPORT_DURATION ?? "15", + "Duration", +); +const timeoutMs = parsePositiveInteger( + process.env.RECORDLY_BENCH_EXPORT_TIMEOUT_MS ?? "180000", + "Timeout", +); const runsPerVariant = parsePositiveInteger(process.env.RECORDLY_BENCH_EXPORT_RUNS ?? "2", "Runs"); const useNativeExport = process.env.RECORDLY_BENCH_EXPORT_USE_NATIVE === "1"; const useWebcamOverlay = process.env.RECORDLY_BENCH_EXPORT_ENABLE_WEBCAM === "1"; @@ -39,9 +45,7 @@ const webcamHeight = parseEvenInteger( const webcamShadowIntensity = parseExportShadowIntensity( process.env.RECORDLY_BENCH_EXPORT_WEBCAM_SHADOW ?? null, ); -const webcamSize = parseExportWebcamSize( - process.env.RECORDLY_BENCH_EXPORT_WEBCAM_SIZE ?? null, -); +const webcamSize = parseExportWebcamSize(process.env.RECORDLY_BENCH_EXPORT_WEBCAM_SIZE ?? null); const MODERN_BACKEND_SWEEP = ["auto", "webcodecs", "breeze"]; const exportPipeline = parseExportPipeline(process.env.RECORDLY_BENCH_EXPORT_PIPELINE ?? null); const exportBackend = parseExportBackend(process.env.RECORDLY_BENCH_EXPORT_BACKEND ?? null); @@ -106,9 +110,7 @@ function parseExportBackend(rawValue) { return rawValue; } - throw new Error( - "RECORDLY_BENCH_EXPORT_BACKEND must be 'auto', 'webcodecs', or 'breeze'", - ); + throw new Error("RECORDLY_BENCH_EXPORT_BACKEND must be 'auto', 'webcodecs', or 'breeze'"); } function parseExportBackendList(rawValue) { @@ -172,9 +174,7 @@ function parseExportEncodingMode(rawValue) { return rawValue; } - throw new Error( - "RECORDLY_BENCH_EXPORT_ENCODING_MODE must be 'fast', 'balanced', or 'quality'", - ); + throw new Error("RECORDLY_BENCH_EXPORT_ENCODING_MODE must be 'fast', 'balanced', or 'quality'"); } function parseExportShadowIntensity(rawValue) { @@ -209,7 +209,10 @@ function summarizeSmokeProgress(progressSamples) { } const extractingSamples = progressSamples.filter( - (sample) => sample?.phase === "extracting" && typeof sample?.currentFrame === "number" && sample.currentFrame > 1, + (sample) => + sample?.phase === "extracting" && + typeof sample?.currentFrame === "number" && + sample.currentFrame > 1, ); const fpsSource = extractingSamples.length > 0 ? extractingSamples : progressSamples; const renderFpsSamples = fpsSource @@ -245,7 +248,6 @@ async function ensureBuildArtifacts() { await fs.access(rendererEntry); } - async function createFixtureVideo( ffmpegPath, targetPath, @@ -256,16 +258,7 @@ async function createFixtureVideo( videoFilter = `testsrc2=size=${fixtureWidth}x${fixtureHeight}:rate=${frameRate}`, } = {}, ) { - const args = [ - "-y", - "-hide_banner", - "-loglevel", - "error", - "-f", - "lavfi", - "-i", - videoFilter, - ]; + const args = ["-y", "-hide_banner", "-loglevel", "error", "-f", "lavfi", "-i", videoFilter]; if (includeAudio) { args.push( @@ -421,9 +414,7 @@ function printTable(title, columns, rows) { console.log(divider); for (const row of formattedRows) { console.log( - `| ${row - .map((value, columnIndex) => value.padEnd(widths[columnIndex])) - .join(" | ")} |`, + `| ${row.map((value, columnIndex) => value.padEnd(widths[columnIndex])).join(" | ")} |`, ); } } @@ -471,7 +462,8 @@ function calculateDelta(referenceValue, nextValue) { return { deltaMs: nextValue - referenceValue, - deltaPercent: referenceValue > 0 ? ((nextValue - referenceValue) / referenceValue) * 100 : 0, + deltaPercent: + referenceValue > 0 ? ((nextValue - referenceValue) / referenceValue) * 100 : 0, }; } @@ -545,7 +537,10 @@ function printTimingSummaryTable(benchmarkResults) { { header: "Avg export", getValue: (row) => formatMs(row.averageSmokeElapsedMs) }, { header: "Min", getValue: (row) => formatMs(row.minElapsedMs) }, { header: "Max", getValue: (row) => formatMs(row.maxElapsedMs) }, - { header: "Avg output", getValue: (row) => formatSeconds(row.averageOutputDurationSeconds) }, + { + header: "Avg output", + getValue: (row) => formatSeconds(row.averageOutputDurationSeconds), + }, { header: "Avg size", getValue: (row) => formatMegabytes(row.averageSizeBytes) }, { header: "Webcam", getValue: (row) => formatBoolean(row.webcamEnabled) }, ], @@ -590,7 +585,9 @@ function printBackendDetailTable(benchmarkResults) { function buildDeltaTableRows(benchmarkResults) { return benchmarkResults .map((result) => { - const baseline = result.summaries.find((summary) => summary.variant.name === "baseline"); + const baseline = result.summaries.find( + (summary) => summary.variant.name === "baseline", + ); const tuned = result.summaries.find((summary) => summary.variant.name === "tuned"); if (!baseline || !tuned) { return null; @@ -623,9 +620,21 @@ function printDeltaTable(benchmarkResults) { [ { header: "Pipeline", getValue: (row) => row.pipeline }, { header: "Backend", getValue: (row) => row.backend }, - { header: "Avg delta", getValue: (row) => `${formatDeltaMs(row.averageDeltaMs)} (${formatPercent(row.averageDeltaPercent)})` }, - { header: "Median delta", getValue: (row) => `${formatDeltaMs(row.medianDeltaMs)} (${formatPercent(row.medianDeltaPercent)})` }, - { header: "Export delta", getValue: (row) => `${formatDeltaMs(row.exportDeltaMs)} (${formatPercent(row.exportDeltaPercent)})` }, + { + header: "Avg delta", + getValue: (row) => + `${formatDeltaMs(row.averageDeltaMs)} (${formatPercent(row.averageDeltaPercent)})`, + }, + { + header: "Median delta", + getValue: (row) => + `${formatDeltaMs(row.medianDeltaMs)} (${formatPercent(row.medianDeltaPercent)})`, + }, + { + header: "Export delta", + getValue: (row) => + `${formatDeltaMs(row.exportDeltaMs)} (${formatPercent(row.exportDeltaPercent)})`, + }, ], buildDeltaTableRows(benchmarkResults), ); @@ -659,9 +668,7 @@ async function runVariant( ...(exportShadowIntensity !== null ? { RECORDLY_SMOKE_EXPORT_SHADOW_INTENSITY: String(exportShadowIntensity) } : {}), - ...(webcamInputPath - ? { RECORDLY_SMOKE_EXPORT_WEBCAM_INPUT: webcamInputPath } - : {}), + ...(webcamInputPath ? { RECORDLY_SMOKE_EXPORT_WEBCAM_INPUT: webcamInputPath } : {}), ...(webcamShadowIntensity !== null ? { RECORDLY_SMOKE_EXPORT_WEBCAM_SHADOW: String(webcamShadowIntensity) } : {}), @@ -744,9 +751,7 @@ async function runVariant( outputDuration, webcamEnabled: !!webcamInputPath, smokeExportReport: smokeExportReport?.report ?? null, - smokeProgressSummary: summarizeSmokeProgress( - smokeExportReport?.report?.progressSamples, - ), + smokeProgressSummary: summarizeSmokeProgress(smokeExportReport?.report?.progressSamples), }; } @@ -877,7 +882,9 @@ async function main() { console.log(`[benchmark-export-queues] Generating fixture video: ${inputPath}`); await createFixtureVideo(ffmpegStatic, inputPath); if (webcamInputPath) { - console.log(`[benchmark-export-queues] Generating webcam fixture video: ${webcamInputPath}`); + console.log( + `[benchmark-export-queues] Generating webcam fixture video: ${webcamInputPath}`, + ); await createFixtureVideo(ffmpegStatic, webcamInputPath, { fixtureWidth: webcamWidth, fixtureHeight: webcamHeight, @@ -945,12 +952,11 @@ async function main() { const baseline = result.summaries[0]; const tuned = result.summaries[1]; const deltaMs = tuned.averageElapsedMs - baseline.averageElapsedMs; - const percent = baseline.averageElapsedMs > 0 ? (deltaMs / baseline.averageElapsedMs) * 100 : 0; + const percent = + baseline.averageElapsedMs > 0 ? (deltaMs / baseline.averageElapsedMs) * 100 : 0; const medianDeltaMs = tuned.medianElapsedMs - baseline.medianElapsedMs; const medianPercent = - baseline.medianElapsedMs > 0 - ? (medianDeltaMs / baseline.medianElapsedMs) * 100 - : 0; + baseline.medianElapsedMs > 0 ? (medianDeltaMs / baseline.medianElapsedMs) * 100 : 0; const backendLabel = result.request.backend ?? "default"; console.log( `[benchmark-export-queues] ${backendLabel} tuned vs baseline: ${deltaMs}ms (${percent.toFixed(1)}%)`, @@ -969,4 +975,4 @@ main().catch((error) => { `[benchmark-export-queues] ${error instanceof Error ? error.message : String(error)}`, ); process.exitCode = 1; -}); \ No newline at end of file +}); diff --git a/scripts/build-native-helpers.mjs b/scripts/build-native-helpers.mjs index 8cf67c4ad..778b19b87 100644 --- a/scripts/build-native-helpers.mjs +++ b/scripts/build-native-helpers.mjs @@ -14,11 +14,11 @@ function getTargetConfigs() { return [ { archTag: "darwin-arm64", - swiftTarget: "arm64-apple-macos13.0", + swiftTarget: "arm64-apple-macos14.0", }, { archTag: "darwin-x64", - swiftTarget: "x86_64-apple-macos13.0", + swiftTarget: "x86_64-apple-macos14.0", }, ]; } @@ -26,19 +26,19 @@ function getTargetConfigs() { const helpers = [ { source: "ScreenCaptureKitRecorder.swift", - output: "openscreen-screencapturekit-helper", + output: "recordly-screencapturekit-helper", }, { source: "ScreenCaptureKitWindowList.swift", - output: "openscreen-window-list", + output: "recordly-window-list", }, { source: "SystemCursorAssets.swift", - output: "openscreen-system-cursors", + output: "recordly-system-cursors", }, { source: "NativeCursorMonitor.swift", - output: "openscreen-native-cursor-monitor", + output: "recordly-native-cursor-monitor", }, ]; diff --git a/scripts/build-whisper-runtime.mjs b/scripts/build-whisper-runtime.mjs index 0bdf9dd84..13c944e65 100644 --- a/scripts/build-whisper-runtime.mjs +++ b/scripts/build-whisper-runtime.mjs @@ -101,7 +101,12 @@ function getTargetConfigs() { archTag, buildRoot: path.join(cacheRoot, `build-${archTag}`), outputDir: path.join(nativeRoot, "bin", archTag), - configureArgs: ["-G", "Visual Studio 17 2022", "-A", arch === "arm64" ? "ARM64" : "x64"], + configureArgs: [ + "-G", + "Visual Studio 17 2022", + "-A", + arch === "arm64" ? "ARM64" : "x64", + ], }, ]; } @@ -258,7 +263,9 @@ async function shouldSkipBuild(target) { const binaryName = target.platform === "win32" ? "whisper-cli.exe" : "whisper-cli"; const binaryPath = path.join(target.outputDir, binaryName); return ( - manifest.version === whisperVersion && manifest.arch === target.arch && existsSync(binaryPath) + manifest.version === whisperVersion && + manifest.arch === target.arch && + existsSync(binaryPath) ); } catch { return false; diff --git a/scripts/build-windows-capture.mjs b/scripts/build-windows-capture.mjs index 18f5ca36b..3ae981b28 100644 --- a/scripts/build-windows-capture.mjs +++ b/scripts/build-windows-capture.mjs @@ -1,11 +1,11 @@ import { execSync } from "node:child_process"; -import { copyFileSync, mkdirSync, existsSync, rmSync } from "node:fs"; +import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; import path from "node:path"; import { - formatNativeHelperManifestWarning, - updateNativeHelperManifest, - verifyNativeHelperManifest, + formatNativeHelperManifestWarning, + updateNativeHelperManifest, + verifyNativeHelperManifest, } from "./native-helper-manifest.mjs"; const projectRoot = process.cwd(); @@ -21,81 +21,83 @@ const bundledDir = path.join( const bundledExePath = path.join(bundledDir, "wgc-capture.exe"); const helperId = "wgc-capture"; -if (process.platform !== 'win32') { - console.log('[build-windows-capture] Skipping native Windows capture build: host platform is not Windows.'); - process.exit(0); +if (process.platform !== "win32") { + console.log( + "[build-windows-capture] Skipping native Windows capture build: host platform is not Windows.", + ); + process.exit(0); } -if (!existsSync(path.join(sourceDir, 'CMakeLists.txt'))) { - console.error('[build-windows-capture] CMakeLists.txt not found at', sourceDir); - process.exit(1); +if (!existsSync(path.join(sourceDir, "CMakeLists.txt"))) { + console.error("[build-windows-capture] CMakeLists.txt not found at", sourceDir); + process.exit(1); } function findCmake() { - // Check PATH first - try { - execSync('cmake --version', { stdio: 'pipe' }); - return 'cmake'; - } catch { - // not on PATH - } + // Check PATH first + try { + execSync("cmake --version", { stdio: "pipe" }); + return "cmake"; + } catch { + // not on PATH + } - const standaloneCmakePaths = [ - path.join('C:', 'Program Files', 'CMake', 'bin', 'cmake.exe'), - path.join('C:', 'Program Files (x86)', 'CMake', 'bin', 'cmake.exe'), - ]; - for (const cmakePath of standaloneCmakePaths) { - if (existsSync(cmakePath)) { - return `"${cmakePath}"`; - } - } + const standaloneCmakePaths = [ + path.join("C:", "Program Files", "CMake", "bin", "cmake.exe"), + path.join("C:", "Program Files (x86)", "CMake", "bin", "cmake.exe"), + ]; + for (const cmakePath of standaloneCmakePaths) { + if (existsSync(cmakePath)) { + return `"${cmakePath}"`; + } + } - // VS 2022 bundled CMake - const vsRoots = [ - path.join('C:', 'Program Files', 'Microsoft Visual Studio'), - path.join('C:', 'Program Files (x86)', 'Microsoft Visual Studio'), - ]; - const vsEditions = ['Community', 'Professional', 'Enterprise', 'BuildTools']; - const vsVersions = ['2022', '2019']; - for (const root of vsRoots) { - for (const version of vsVersions) { - for (const edition of vsEditions) { - const cmakePath = path.join( - root, - version, - edition, - 'Common7', - 'IDE', - 'CommonExtensions', - 'Microsoft', - 'CMake', - 'CMake', - 'bin', - 'cmake.exe' - ); - if (existsSync(cmakePath)) { - return `"${cmakePath}"`; - } - } - } - } + // VS 2022 bundled CMake + const vsRoots = [ + path.join("C:", "Program Files", "Microsoft Visual Studio"), + path.join("C:", "Program Files (x86)", "Microsoft Visual Studio"), + ]; + const vsEditions = ["Community", "Professional", "Enterprise", "BuildTools"]; + const vsVersions = ["2022", "2019"]; + for (const root of vsRoots) { + for (const version of vsVersions) { + for (const edition of vsEditions) { + const cmakePath = path.join( + root, + version, + edition, + "Common7", + "IDE", + "CommonExtensions", + "Microsoft", + "CMake", + "CMake", + "bin", + "cmake.exe", + ); + if (existsSync(cmakePath)) { + return `"${cmakePath}"`; + } + } + } + } - return null; + return null; } const cmake = findCmake(); if (!cmake) { if (existsSync(bundledExePath)) { - const verification = verifyNativeHelperManifest({ - projectRoot, - helperId, - sourceDir, - binaryPath: bundledExePath, - binaryName: "wgc-capture.exe", - }); - if (!verification.ok) { - console.warn(formatNativeHelperManifestWarning("build-windows-capture", verification)); - } + const verification = verifyNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "wgc-capture.exe", + }); + if (!verification.ok) { + console.warn(formatNativeHelperManifestWarning("build-windows-capture", verification)); + } console.log(`[build-windows-capture] Using bundled helper: ${bundledExePath}`); process.exit(0); } @@ -107,64 +109,64 @@ if (!cmake) { } mkdirSync(buildDir, { recursive: true }); -const cacheFile = path.join(buildDir, 'CMakeCache.txt'); -const cacheDir = path.join(buildDir, 'CMakeFiles'); +const cacheFile = path.join(buildDir, "CMakeCache.txt"); +const cacheDir = path.join(buildDir, "CMakeFiles"); function clearCmakeCache() { - rmSync(cacheFile, { force: true }); - rmSync(cacheDir, { recursive: true, force: true }); + rmSync(cacheFile, { force: true }); + rmSync(cacheDir, { recursive: true, force: true }); } -console.log('[build-windows-capture] Configuring CMake...'); +console.log("[build-windows-capture] Configuring CMake..."); try { - clearCmakeCache(); - execSync(`${cmake} .. -G "Visual Studio 17 2022" -A x64`, { - cwd: buildDir, - stdio: 'inherit', - timeout: 120000, - }); + clearCmakeCache(); + execSync(`${cmake} .. -G "Visual Studio 17 2022" -A x64`, { + cwd: buildDir, + stdio: "inherit", + timeout: 120000, + }); } catch { - console.log('[build-windows-capture] VS 2022 generator not found, trying VS 2019...'); - try { - clearCmakeCache(); - execSync(`${cmake} .. -G "Visual Studio 16 2019" -A x64`, { - cwd: buildDir, - stdio: 'inherit', - timeout: 120000, - }); - } catch (innerError) { - console.error('[build-windows-capture] CMake configure failed:', innerError.message); - process.exit(1); - } + console.log("[build-windows-capture] VS 2022 generator not found, trying VS 2019..."); + try { + clearCmakeCache(); + execSync(`${cmake} .. -G "Visual Studio 16 2019" -A x64`, { + cwd: buildDir, + stdio: "inherit", + timeout: 120000, + }); + } catch (innerError) { + console.error("[build-windows-capture] CMake configure failed:", innerError.message); + process.exit(1); + } } -console.log('[build-windows-capture] Building native Windows capture helper...'); +console.log("[build-windows-capture] Building native Windows capture helper..."); try { - execSync(`${cmake} --build . --config Release`, { - cwd: buildDir, - stdio: 'inherit', - timeout: 300000, - }); + execSync(`${cmake} --build . --config Release`, { + cwd: buildDir, + stdio: "inherit", + timeout: 300000, + }); } catch (error) { - console.error('[build-windows-capture] Build failed:', error.message); - process.exit(1); + console.error("[build-windows-capture] Build failed:", error.message); + process.exit(1); } -const exePath = path.join(buildDir, 'Release', 'wgc-capture.exe'); +const exePath = path.join(buildDir, "Release", "wgc-capture.exe"); if (existsSync(exePath)) { console.log(`[build-windows-capture] Built successfully: ${exePath}`); mkdirSync(bundledDir, { recursive: true }); copyFileSync(exePath, bundledExePath); console.log(`[build-windows-capture] Staged bundled helper: ${bundledExePath}`); - const manifestPath = updateNativeHelperManifest({ - projectRoot, - helperId, - sourceDir, - binaryPath: bundledExePath, - binaryName: "wgc-capture.exe", - }); - console.log(`[build-windows-capture] Updated helper manifest: ${manifestPath}`); + const manifestPath = updateNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "wgc-capture.exe", + }); + console.log(`[build-windows-capture] Updated helper manifest: ${manifestPath}`); } else { - console.error('[build-windows-capture] Expected exe not found at', exePath); - process.exit(1); + console.error("[build-windows-capture] Expected exe not found at", exePath); + process.exit(1); } diff --git a/scripts/i18n-check.mjs b/scripts/i18n-check.mjs index 19535bda9..cff56c298 100644 --- a/scripts/i18n-check.mjs +++ b/scripts/i18n-check.mjs @@ -1,86 +1,86 @@ -import fs from 'node:fs' -import path from 'node:path' +import fs from "node:fs"; +import path from "node:path"; -const root = process.cwd() -const localesDir = path.join(root, 'src', 'i18n', 'locales') +const root = process.cwd(); +const localesDir = path.join(root, "src", "i18n", "locales"); const locales = fs.readdirSync(localesDir).filter((entry) => { - const fullPath = path.join(localesDir, entry) - return fs.statSync(fullPath).isDirectory() -}) + const fullPath = path.join(localesDir, entry); + return fs.statSync(fullPath).isDirectory(); +}); -if (!locales.includes('en')) { - console.error('i18n-check: expected base locale directory "en"') - process.exit(1) +if (!locales.includes("en")) { + console.error('i18n-check: expected base locale directory "en"'); + process.exit(1); } function loadJson(filePath) { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) + return JSON.parse(fs.readFileSync(filePath, "utf8")); } -function collectKeyPaths(obj, prefix = '') { - if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { - return prefix ? [prefix] : [] - } - - const keys = Object.keys(obj) - if (keys.length === 0) { - return prefix ? [prefix] : [] - } - - const paths = [] - for (const key of keys) { - const nextPrefix = prefix ? `${prefix}.${key}` : key - const value = obj[key] - if (value && typeof value === 'object' && !Array.isArray(value)) { - paths.push(...collectKeyPaths(value, nextPrefix)) - } else { - paths.push(nextPrefix) - } - } - return paths +function collectKeyPaths(obj, prefix = "") { + if (!obj || typeof obj !== "object" || Array.isArray(obj)) { + return prefix ? [prefix] : []; + } + + const keys = Object.keys(obj); + if (keys.length === 0) { + return prefix ? [prefix] : []; + } + + const paths = []; + for (const key of keys) { + const nextPrefix = prefix ? `${prefix}.${key}` : key; + const value = obj[key]; + if (value && typeof value === "object" && !Array.isArray(value)) { + paths.push(...collectKeyPaths(value, nextPrefix)); + } else { + paths.push(nextPrefix); + } + } + return paths; } -const baseLocaleDir = path.join(localesDir, 'en') -const namespaceFiles = fs.readdirSync(baseLocaleDir).filter((file) => file.endsWith('.json')) +const baseLocaleDir = path.join(localesDir, "en"); +const namespaceFiles = fs.readdirSync(baseLocaleDir).filter((file) => file.endsWith(".json")); -let hasErrors = false +let hasErrors = false; for (const namespaceFile of namespaceFiles) { - const baseData = loadJson(path.join(baseLocaleDir, namespaceFile)) - const baseKeys = new Set(collectKeyPaths(baseData)) - - for (const locale of locales) { - if (locale === 'en') continue - - const localeFile = path.join(localesDir, locale, namespaceFile) - if (!fs.existsSync(localeFile)) { - console.error(`i18n-check: missing namespace file ${locale}/${namespaceFile}`) - hasErrors = true - continue - } - - const localeData = loadJson(localeFile) - const localeKeys = new Set(collectKeyPaths(localeData)) - - for (const key of baseKeys) { - if (!localeKeys.has(key)) { - console.error(`i18n-check: missing key ${locale}/${namespaceFile}:${key}`) - hasErrors = true - } - } - - for (const key of localeKeys) { - if (!baseKeys.has(key)) { - console.error(`i18n-check: extra key ${locale}/${namespaceFile}:${key}`) - hasErrors = true - } - } - } + const baseData = loadJson(path.join(baseLocaleDir, namespaceFile)); + const baseKeys = new Set(collectKeyPaths(baseData)); + + for (const locale of locales) { + if (locale === "en") continue; + + const localeFile = path.join(localesDir, locale, namespaceFile); + if (!fs.existsSync(localeFile)) { + console.error(`i18n-check: missing namespace file ${locale}/${namespaceFile}`); + hasErrors = true; + continue; + } + + const localeData = loadJson(localeFile); + const localeKeys = new Set(collectKeyPaths(localeData)); + + for (const key of baseKeys) { + if (!localeKeys.has(key)) { + console.error(`i18n-check: missing key ${locale}/${namespaceFile}:${key}`); + hasErrors = true; + } + } + + for (const key of localeKeys) { + if (!baseKeys.has(key)) { + console.error(`i18n-check: extra key ${locale}/${namespaceFile}:${key}`); + hasErrors = true; + } + } + } } if (hasErrors) { - process.exit(1) + process.exit(1); } -console.log('i18n-check: locale files are structurally consistent') \ No newline at end of file +console.log("i18n-check: locale files are structurally consistent"); diff --git a/scripts/native-helper-manifest.mjs b/scripts/native-helper-manifest.mjs index 5c13996e1..0cf96e0f3 100644 --- a/scripts/native-helper-manifest.mjs +++ b/scripts/native-helper-manifest.mjs @@ -1,12 +1,5 @@ import { createHash } from "node:crypto"; -import { - existsSync, - mkdirSync, - readFileSync, - readdirSync, - statSync, - writeFileSync, -} from "node:fs"; +import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import path from "node:path"; const MANIFEST_FILE_NAME = "helpers-manifest.json"; @@ -66,7 +59,11 @@ function hashFile(filePath) { return hashBuffer(readFileSync(filePath)); } -export function getNativeHelperManifestPath({ projectRoot, platform = process.platform, arch = process.arch }) { +export function getNativeHelperManifestPath({ + projectRoot, + platform = process.platform, + arch = process.arch, +}) { return path.join( projectRoot, "electron", @@ -161,7 +158,9 @@ export function verifyNativeHelperManifest({ const reasons = []; if (helperManifest.binaryName !== binaryName) { - reasons.push(`expected binary ${binaryName}, found ${helperManifest.binaryName ?? "unknown"}`); + reasons.push( + `expected binary ${binaryName}, found ${helperManifest.binaryName ?? "unknown"}`, + ); } const expectedBinaryHash = helperManifest.binarySha256; @@ -186,4 +185,4 @@ export function verifyNativeHelperManifest({ export function formatNativeHelperManifestWarning(helperLabel, verificationResult) { const reasonText = verificationResult.reasons.join(", "); return `[${helperLabel}] Bundled helper provenance check failed (${reasonText}). Rebuild the helper to refresh ${path.basename(verificationResult.manifestPath)}.`; -} \ No newline at end of file +} diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs index 3a35d3db5..b0715a831 100644 --- a/scripts/postinstall.mjs +++ b/scripts/postinstall.mjs @@ -23,9 +23,7 @@ function runScript(scriptName) { }); if (result.error) { - console.error( - `[postinstall] Failed to start "${scriptName}" (${result.error.message}).`, - ); + console.error(`[postinstall] Failed to start "${scriptName}" (${result.error.message}).`); return false; } @@ -35,9 +33,7 @@ function runScript(scriptName) { } if (result.status !== 0) { - console.error( - `[postinstall] "${scriptName}" exited with code ${result.status}.`, - ); + console.error(`[postinstall] "${scriptName}" exited with code ${result.status}.`); return false; } diff --git a/src/App.css b/src/App.css index d936c2c56..df674c0d8 100644 --- a/src/App.css +++ b/src/App.css @@ -1,43 +1,42 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/src/App.tsx b/src/App.tsx index 4622bddac..3ba05874c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,90 +1,94 @@ -import { useEffect, useState } from "react"; -import { CountdownOverlay } from "./components/countdown/CountdownOverlay"; -import { LaunchWindow } from "./components/launch/LaunchWindow"; -import { SourceSelector } from "./components/launch/SourceSelector"; -import { UpdateToastWindow } from "./components/launch/UpdateToastWindow"; -import { Toaster } from "./components/ui/sonner"; -import { ShortcutsConfigDialog } from "./components/video-editor/ShortcutsConfigDialog"; -import VideoEditor from "./components/video-editor/VideoEditor"; -import { useI18n } from "./contexts/I18nContext"; -import { ShortcutsProvider } from "./contexts/ShortcutsContext"; -import { loadAllCustomFonts } from "./lib/customFonts"; - -export default function App() { - const [windowType, setWindowType] = useState(""); - const { locale, t } = useI18n(); - - useEffect(() => { - const params = new URLSearchParams(window.location.search); - const type = params.get("windowType") || ""; - const isMacOS = /mac/i.test(navigator.platform); - setWindowType(type); - - if ( - type === "hud-overlay" || - type === "source-selector" || - type === "countdown" || - (type === "update-toast" && isMacOS) - ) { - document.body.style.background = "transparent"; - document.documentElement.style.background = "transparent"; - document.getElementById("root")?.style.setProperty("background", "transparent"); - } - - if (type === "hud-overlay" || type === "update-toast") { - document.documentElement.style.overflow = "visible"; - document.body.style.overflow = "visible"; - document.getElementById("root")?.style.setProperty("overflow", "visible"); - } - - loadAllCustomFonts().catch((error) => { - console.error("Failed to load custom fonts:", error); - }); - }, []); - - useEffect(() => { - document.title = - windowType === "editor" ? t("app.editorTitle", "Recordly Editor") : t("app.name", "Recordly"); - }, [windowType, locale, t]); - - switch (windowType) { - case "hud-overlay": - return ( - <> - - - - ); - case "source-selector": - return ; - case "countdown": - return ; - case "update-toast": - return ; - case "editor": - return ( - - - - - ); - default: - return ( -
-
- {t("app.name", -
-

{t("app.name", "Recordly")}

-

- {t("app.subtitle", "Screen recording and editing")} -

-
-
-
- ); - } -} +import { useEffect, useState } from "react"; +import { CountdownOverlay } from "./components/countdown/CountdownOverlay"; +import { LaunchWindow } from "./components/launch/LaunchWindow"; +import { SourceSelector } from "./components/launch/SourceSelector"; +import { UpdateToastWindow } from "./components/launch/UpdateToastWindow"; +import { Toaster } from "./components/ui/sonner"; +import { ShortcutsConfigDialog } from "./components/video-editor/ShortcutsConfigDialog"; +import VideoEditor from "./components/video-editor/VideoEditor"; +import { useI18n } from "./contexts/I18nContext"; +import { ShortcutsProvider } from "./contexts/ShortcutsContext"; +import { loadAllCustomFonts } from "./lib/customFonts"; + +export default function App() { + const [windowType, setWindowType] = useState(""); + const { t } = useI18n(); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const type = params.get("windowType") || ""; + const isMacOS = /mac/i.test(navigator.platform); + setWindowType(type); + + if ( + type === "hud-overlay" || + type === "source-selector" || + type === "countdown" || + (type === "update-toast" && isMacOS) + ) { + document.body.style.background = "transparent"; + document.documentElement.style.background = "transparent"; + document.getElementById("root")?.style.setProperty("background", "transparent"); + } + + if (type === "hud-overlay" || type === "update-toast") { + document.documentElement.style.overflow = "visible"; + document.body.style.overflow = "visible"; + document.getElementById("root")?.style.setProperty("overflow", "visible"); + } + + loadAllCustomFonts().catch((error) => { + console.error("Failed to load custom fonts:", error); + }); + }, []); + + useEffect(() => { + document.title = + windowType === "editor" + ? t("app.editorTitle", "Recordly Editor") + : t("app.name", "Recordly"); + }, [windowType, t]); + + switch (windowType) { + case "hud-overlay": + return ( + <> + + + + ); + case "source-selector": + return ; + case "countdown": + return ; + case "update-toast": + return ; + case "editor": + return ( + + + + + ); + default: + return ( +
+
+ {t("app.name", +
+

+ {t("app.name", "Recordly")} +

+

+ {t("app.subtitle", "Screen recording and editing")} +

+
+
+
+ ); + } +} diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 5e182e785..2b12ea299 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -1,29 +1,29 @@ import { AppWindow, - ArrowUpCircle, - ChevronUp, - CheckCircle2, + ArrowCircleUp as ArrowUpCircle, + ArrowClockwise as RefreshCw, + CaretUp as ChevronUp, + CheckCircle as CheckCircle2, + DotsThreeVertical as MoreVertical, Eye, - EyeOff, + EyeSlash as EyeOff, FolderOpen, - Languages, - Mic, - MicOff, + Microphone as Mic, + MicrophoneSlash as MicOff, Minus, Monitor, - MoreVertical, Pause, Play, - RefreshCw, - Square, + SpeakerHigh as Volume2, + SpeakerX as VolumeX, + Stop as Square, Timer, - Video, - VideoIcon, - VideoOff, - Volume2, - VolumeX, + Translate as Languages, + VideoCamera as Video, + VideoCamera as VideoIcon, + VideoCameraSlash as VideoOff, X, -} from "lucide-react"; +} from "@phosphor-icons/react"; import { AnimatePresence, motion } from "motion/react"; import type { ReactNode } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; @@ -143,9 +143,7 @@ function MicDeviceRow({ className={`${styles.ddItem} ${selected ? styles.ddItemSelected : ""}`} onClick={onSelect} > - - {selected ? : } - + {selected ? : } {device.label} @@ -189,21 +187,14 @@ export function LaunchWindow() { const [activeDropdown, setActiveDropdown] = useState< "none" | "sources" | "more" | "mic" | "countdown" | "webcam" >("none"); - const [projectLibraryEntries, setProjectLibraryEntries] = useState< - ProjectLibraryEntry[] - >([]); + const [projectLibraryEntries, setProjectLibraryEntries] = useState([]); const [projectBrowserOpen, setProjectBrowserOpen] = useState(false); const [sources, setSources] = useState([]); const [sourcesLoading, setSourcesLoading] = useState(false); const [hideHudFromCapture, setHideHudFromCapture] = useState(true); - const [showFloatingWebcamPreview, setShowFloatingWebcamPreview] = - useState(true); - const [webcamPreviewOffset, setWebcamPreviewOffset] = useState( - DEFAULT_WEBCAM_PREVIEW_OFFSET, - ); - const [recordingHudOffset, setRecordingHudOffset] = useState( - DEFAULT_RECORDING_HUD_OFFSET, - ); + const [showFloatingWebcamPreview, setShowFloatingWebcamPreview] = useState(true); + const [webcamPreviewOffset, setWebcamPreviewOffset] = useState(DEFAULT_WEBCAM_PREVIEW_OFFSET); + const [recordingHudOffset, setRecordingHudOffset] = useState(DEFAULT_RECORDING_HUD_OFFSET); const [platform, setPlatform] = useState(null); const [appVersion, setAppVersion] = useState(null); const [updateStatus, setUpdateStatus] = useState<{ @@ -230,9 +221,7 @@ export function LaunchWindow() { const moreButtonRef = useRef(null); const webcamPreviewRef = useRef(null); const recordingWebcamPreviewRef = useRef(null); - const recordingWebcamPreviewContainerRef = useRef( - null, - ); + const recordingWebcamPreviewContainerRef = useRef(null); const previewStreamRef = useRef(null); const webcamPreviewDragStartRef = useRef<{ pointerId: number; @@ -271,17 +260,13 @@ export function LaunchWindow() { const micDropdownOpen = activeDropdown === "mic"; const webcamDropdownOpen = activeDropdown === "webcam"; const showWebcamControls = webcamEnabled && !recording; - const showRecordingWebcamPreview = - webcamEnabled && showFloatingWebcamPreview; + const showRecordingWebcamPreview = webcamEnabled && showFloatingWebcamPreview; const shouldStreamWebcamPreview = - webcamEnabled && - (showFloatingWebcamPreview || - (showWebcamControls && webcamDropdownOpen)); - const { devices, selectedDeviceId, setSelectedDeviceId } = - useMicrophoneDevices( - microphoneEnabled || micDropdownOpen, - microphoneDeviceId, - ); + webcamEnabled && (showFloatingWebcamPreview || (showWebcamControls && webcamDropdownOpen)); + const { devices, selectedDeviceId, setSelectedDeviceId } = useMicrophoneDevices( + microphoneEnabled || micDropdownOpen, + microphoneDeviceId, + ); const { devices: videoDevices, selectedDeviceId: selectedVideoDeviceId, @@ -295,9 +280,7 @@ export function LaunchWindow() { return; } - setMicrophoneDeviceId( - selectedDeviceId === "default" ? undefined : selectedDeviceId, - ); + setMicrophoneDeviceId(selectedDeviceId === "default" ? undefined : selectedDeviceId); }, [selectedDeviceId, setMicrophoneDeviceId]); useEffect(() => { @@ -322,9 +305,7 @@ export function LaunchWindow() { } }, [showRecordingWebcamPreview]); - const handleWebcamPreviewPointerDown = ( - event: React.PointerEvent, - ) => { + const handleWebcamPreviewPointerDown = (event: React.PointerEvent) => { if (event.button !== 0) { return; } @@ -348,9 +329,7 @@ export function LaunchWindow() { event.currentTarget.setPointerCapture(event.pointerId); }; - const handleWebcamPreviewPointerMove = ( - event: React.PointerEvent, - ) => { + const handleWebcamPreviewPointerMove = (event: React.PointerEvent) => { const dragState = webcamPreviewDragStartRef.current; if (!dragState || dragState.pointerId !== event.pointerId) { return; @@ -359,10 +338,7 @@ export function LaunchWindow() { const deltaX = event.clientX - dragState.startX; const deltaY = event.clientY - dragState.startY; - if ( - !dragState.dragging && - Math.hypot(deltaX, deltaY) < WEBCAM_PREVIEW_DRAG_THRESHOLD - ) { + if (!dragState.dragging && Math.hypot(deltaX, deltaY) < WEBCAM_PREVIEW_DRAG_THRESHOLD) { return; } @@ -371,14 +347,8 @@ export function LaunchWindow() { isWebcamPreviewDraggingRef.current = true; } - const viewportWidth = Math.max( - window.innerWidth, - window.screen?.width ?? 0, - ); - const viewportHeight = Math.max( - window.innerHeight, - window.screen?.height ?? 0, - ); + const viewportWidth = Math.max(window.innerWidth, window.screen?.width ?? 0); + const viewportHeight = Math.max(window.innerHeight, window.screen?.height ?? 0); const unclampedLeft = dragState.initialLeft + deltaX; const unclampedTop = dragState.initialTop + deltaY; const clampedLeft = Math.min( @@ -396,9 +366,7 @@ export function LaunchWindow() { }); }; - const handleWebcamPreviewPointerUp = ( - event: React.PointerEvent, - ) => { + const handleWebcamPreviewPointerUp = (event: React.PointerEvent) => { const dragState = webcamPreviewDragStartRef.current; if (!dragState || dragState.pointerId !== event.pointerId) { return; @@ -415,9 +383,7 @@ export function LaunchWindow() { } }; - const handleHudBarPointerDown = ( - event: React.PointerEvent, - ) => { + const handleHudBarPointerDown = (event: React.PointerEvent) => { if (event.button !== 0) { return; } @@ -448,16 +414,10 @@ export function LaunchWindow() { pointerId: event.pointerId, mode: "overlay", }; - window.electronAPI?.hudOverlayDrag?.( - "start", - event.screenX, - event.screenY, - ); + window.electronAPI?.hudOverlayDrag?.("start", event.screenX, event.screenY); }; - const handleHudBarPointerMove = ( - event: React.PointerEvent, - ) => { + const handleHudBarPointerMove = (event: React.PointerEvent) => { const dragState = hudDragStartRef.current; if (!dragState || dragState.pointerId !== event.pointerId) { return; @@ -466,14 +426,8 @@ export function LaunchWindow() { if (dragState.mode === "webcam-preview") { const deltaX = event.clientX - dragState.startX; const deltaY = event.clientY - dragState.startY; - const viewportWidth = Math.max( - window.innerWidth, - window.screen?.width ?? 0, - ); - const viewportHeight = Math.max( - window.innerHeight, - window.screen?.height ?? 0, - ); + const viewportWidth = Math.max(window.innerWidth, window.screen?.width ?? 0); + const viewportHeight = Math.max(window.innerHeight, window.screen?.height ?? 0); const unclampedLeft = dragState.initialLeft + deltaX; const unclampedTop = dragState.initialTop + deltaY; const clampedLeft = Math.min( @@ -492,16 +446,10 @@ export function LaunchWindow() { return; } - window.electronAPI?.hudOverlayDrag?.( - "move", - event.screenX, - event.screenY, - ); + window.electronAPI?.hudOverlayDrag?.("move", event.screenX, event.screenY); }; - const handleHudBarPointerUp = ( - event: React.PointerEvent, - ) => { + const handleHudBarPointerUp = (event: React.PointerEvent) => { const dragState = hudDragStartRef.current; if (!dragState || dragState.pointerId !== event.pointerId) { return; @@ -522,27 +470,20 @@ export function LaunchWindow() { } }; - const attachPreviewStreamToNode = useCallback( - (videoElement: HTMLVideoElement | null) => { - const previewStream = previewStreamRef.current; - if ( - !videoElement || - !previewStream || - videoElement.srcObject === previewStream - ) { - return; - } + const attachPreviewStreamToNode = useCallback((videoElement: HTMLVideoElement | null) => { + const previewStream = previewStreamRef.current; + if (!videoElement || !previewStream || videoElement.srcObject === previewStream) { + return; + } - videoElement.srcObject = previewStream; - const playPromise = videoElement.play(); - if (playPromise) { - playPromise.catch(() => { - // Ignore autoplay interruptions while the preview element mounts. - }); - } - }, - [], - ); + videoElement.srcObject = previewStream; + const playPromise = videoElement.play(); + if (playPromise) { + playPromise.catch(() => { + // Ignore autoplay interruptions while the preview element mounts. + }); + } + }, []); const setWebcamPreviewNode = useCallback( (node: HTMLVideoElement | null) => { @@ -569,23 +510,21 @@ export function LaunchWindow() { } try { - const previewStream = await navigator.mediaDevices.getUserMedia( - { - video: webcamDeviceId - ? { - deviceId: { exact: webcamDeviceId }, - width: { ideal: 320 }, - height: { ideal: 320 }, - frameRate: { ideal: 24, max: 30 }, - } - : { - width: { ideal: 320 }, - height: { ideal: 320 }, - frameRate: { ideal: 24, max: 30 }, - }, - audio: false, - }, - ); + const previewStream = await navigator.mediaDevices.getUserMedia({ + video: webcamDeviceId + ? { + deviceId: { exact: webcamDeviceId }, + width: { ideal: 320 }, + height: { ideal: 320 }, + frameRate: { ideal: 24, max: 30 }, + } + : { + width: { ideal: 320 }, + height: { ideal: 320 }, + frameRate: { ideal: 24, max: 30 }, + }, + audio: false, + }); if (!mounted) { previewStream.getTracks().forEach((track) => track.stop()); @@ -638,12 +577,7 @@ export function LaunchWindow() { } timer = setInterval(() => { if (recordingStart) { - setElapsed( - Math.floor( - (Date.now() - recordingStart - pausedTotal) / - 1000, - ), - ); + setElapsed(Math.floor((Date.now() - recordingStart - pausedTotal) / 1000)); } }, 1000); } @@ -670,9 +604,7 @@ export function LaunchWindow() { useEffect(() => { let mounted = true; - const applySelectedSource = ( - source: { name?: string } | null | undefined, - ) => { + const applySelectedSource = (source: { name?: string } | null | undefined) => { if (!mounted) { return; } @@ -734,8 +666,7 @@ export function LaunchWindow() { const refreshUpdateStatus = async () => { try { - const summary = - await window.electronAPI.getUpdateStatusSummary(); + const summary = await window.electronAPI.getUpdateStatusSummary(); if (mounted) { setUpdateStatus(summary); } @@ -775,16 +706,12 @@ export function LaunchWindow() { let cancelled = false; const loadHudCaptureProtection = async () => { try { - const result = - await window.electronAPI.getHudOverlayCaptureProtection(); + const result = await window.electronAPI.getHudOverlayCaptureProtection(); if (!cancelled && result.success) { setHideHudFromCapture(result.enabled); } } catch (error) { - console.error( - "Failed to load HUD capture protection state:", - error, - ); + console.error("Failed to load HUD capture protection state:", error); } }; void loadHudCaptureProtection(); @@ -795,9 +722,7 @@ export function LaunchWindow() { useEffect(() => { const expanded = - activeDropdown !== "none" || - projectBrowserOpen || - showRecordingWebcamPreview; + activeDropdown !== "none" || projectBrowserOpen || showRecordingWebcamPreview; window.electronAPI.setHudOverlayExpanded(expanded); return () => { @@ -813,21 +738,10 @@ export function LaunchWindow() { } if (showRecordingWebcamPreview) { - const viewportWidth = Math.max( - window.innerWidth, - window.screen?.width ?? 0, - ); - const viewportHeight = Math.max( - window.innerHeight, - window.screen?.height ?? 0, - ); - window.electronAPI.setHudOverlayCompactWidth( - Math.ceil(viewportWidth), - ); - window.electronAPI.setHudOverlayMeasuredHeight( - Math.ceil(viewportHeight), - true, - ); + const viewportWidth = Math.max(window.innerWidth, window.screen?.width ?? 0); + const viewportHeight = Math.max(window.innerHeight, window.screen?.height ?? 0); + window.electronAPI.setHudOverlayCompactWidth(Math.ceil(viewportWidth)); + window.electronAPI.setHudOverlayMeasuredHeight(Math.ceil(viewportHeight), true); return; } @@ -839,14 +753,9 @@ export function LaunchWindow() { hudContentRect.width, hudContent.scrollWidth, ); - const standardHeight = Math.max( - hudContentRect.height, - hudContent.scrollHeight, - ); + const standardHeight = Math.max(hudContentRect.height, hudContent.scrollHeight); - window.electronAPI.setHudOverlayCompactWidth( - Math.ceil(standardWidth + 24), - ); + window.electronAPI.setHudOverlayCompactWidth(Math.ceil(standardWidth + 24)); window.electronAPI.setHudOverlayMeasuredHeight( Math.ceil(standardHeight + 24), activeDropdown !== "none" || projectBrowserOpen, @@ -893,10 +802,7 @@ export function LaunchWindow() { useEffect(() => { const handleClick = (e: MouseEvent) => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(e.target as Node) - ) { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setActiveDropdown("none"); setProjectBrowserOpen(false); } @@ -917,15 +823,13 @@ export function LaunchWindow() { setSources( rawSources.map((s) => { const isWindow = s.id.startsWith("window:"); - const type = - s.sourceType ?? (isWindow ? "window" : "screen"); + const type = s.sourceType ?? (isWindow ? "window" : "screen"); let displayName = s.name; let appName = s.appName; if (isWindow && !appName && s.name.includes(" — ")) { const parts = s.name.split(" — "); appName = parts[0]?.trim(); - displayName = - parts.slice(1).join(" — ").trim() || s.name; + displayName = parts.slice(1).join(" — ").trim() || s.name; } else if (isWindow && s.windowTitle) { displayName = s.windowTitle; } @@ -948,9 +852,7 @@ export function LaunchWindow() { } }, []); - const toggleDropdown = ( - which: "sources" | "more" | "mic" | "countdown" | "webcam", - ) => { + const toggleDropdown = (which: "sources" | "more" | "mic" | "countdown" | "webcam") => { setProjectBrowserOpen(false); setActiveDropdown(activeDropdown === which ? "none" : which); if (activeDropdown !== which && which === "sources") fetchSources(); @@ -963,9 +865,7 @@ export function LaunchWindow() { setActiveDropdown("none"); window.electronAPI.showSourceHighlight?.({ ...source, - name: source.appName - ? `${source.appName} — ${source.name}` - : source.name, + name: source.appName ? `${source.appName} — ${source.name}` : source.name, appName: source.appName, }); }; @@ -1004,8 +904,7 @@ export function LaunchWindow() { const openProjectFromLibrary = useCallback(async (projectPath: string) => { try { - const result = - await window.electronAPI.openProjectFileAtPath(projectPath); + const result = await window.electronAPI.openProjectFileAtPath(projectPath); if (result.canceled || !result.success) { return; } @@ -1033,10 +932,7 @@ export function LaunchWindow() { const nextValue = !hideHudFromCapture; setHideHudFromCapture(nextValue); try { - const result = - await window.electronAPI.setHudOverlayCaptureProtection( - nextValue, - ); + const result = await window.electronAPI.setHudOverlayCaptureProtection(nextValue); if (!result.success) { setHideHudFromCapture(!nextValue); return; @@ -1067,29 +963,18 @@ export function LaunchWindow() { const updateButtonTitle = (() => { switch (updateStatus.status) { case "up-to-date": - return t( - "recording.update.upToDateTitle", - "Recordly {{version}} is up to date.", - { - version: updateStatus.currentVersion, - }, - ); + return t("recording.update.upToDateTitle", "Recordly {{version}} is up to date.", { + version: updateStatus.currentVersion, + }); case "available": case "ready": return updateStatus.availableVersion - ? t( - "recording.update.availableTitle", - "Recordly {{version}} is available.", - { - version: updateStatus.availableVersion, - }, - ) + ? t("recording.update.availableTitle", "Recordly {{version}} is available.", { + version: updateStatus.availableVersion, + }) : t("recording.update.availableGenericTitle"); case "downloading": - return ( - updateStatus.detail ?? - t("recording.update.downloadingTitle") - ); + return updateStatus.detail ?? t("recording.update.downloadingTitle"); case "checking": return t("recording.update.checkingTitle"); case "error": @@ -1105,9 +990,7 @@ export function LaunchWindow() { return ; case "checking": case "downloading": - return ( - - ); + return ; default: return ; } @@ -1217,10 +1100,7 @@ export function LaunchWindow() { title={selectedSource} > - + {selectedSource} {webcamEnabled ?