Studio Dasboard with Separated UI for future scalability and Fixes#577
Studio Dasboard with Separated UI for future scalability and Fixes#577makaradam wants to merge 43 commits into
Conversation
…mport - Add Clapperboard icon button to HUD toolbar that calls switchToEditor(), opening the editor without a recording (Open Studio) - Add EditorEmptyState component shown when no video is loaded, featuring: - Drag-and-drop zone for .openscreen project files - Import Video File button (MP4, MOV, WebM, MKV, AVI, M4V, WMV, FLV, TS) - Load Project button - Add File menu items: New Project (Ctrl+N) and Import Video File (Ctrl+I) - Add loadProjectFileFromPath to the full native-bridge chain so drag-dropped .openscreen files can be loaded without a file-picker dialog - Expand ALLOWED_IMPORT_VIDEO_EXTENSIONS and file picker filters for all common video formats from any screen recorder - Add i18n keys: tooltips.openStudio, unsavedChanges.newProject/importVideo
When switchToEditor() is called from the HUD (Open Studio), there is no current video/session/project. Previously this set an error state causing a white/broken screen. Now we simply leave videoPath as null, which lets the EditorEmptyState component render correctly.
Using the CSS 'hidden' class kept VideoPlayback mounted with an empty src, which fired an error event and triggered setError(). Switch to proper conditional rendering so VideoPlayback only mounts when a video is actually loaded.
… video
- Add missing ipcMain.handle('start-new-recording') that calls switchToHud,
fixing the broken Return to Recorder confirm button
- Skip the Return to Recorder confirmation dialog when no video is loaded
(Open Studio with nothing imported) — just switch back immediately
Replace light bg-background with dark #09090b on the loading and error screens so they match the editor theme. Add a green spinning SVG loader and muted text instead of the jarring white flash.
- Call clearCurrentVideoPath before startNewRecording so the session is properly dropped; next editor open starts fresh instead of reloading the previous video - Set body background to #09090b in index.html so the pre-React paint is dark, eliminating the white flash before the spinner appears
The handler was closing mainWindow (the HUD) directly, then calling createEditorWindowWrapper which closes it again. This double-close left a ghost transparent window on each open/close cycle, causing the HUD shadow to visually compound darker on every return-to-recorder. createEditorWindowWrapper already handles closing the current window cleanly, so the redundant close is removed.
The HUD pill uses box-shadow with a 60px blur radius. The previous 600x160 window was a tight fit, causing the shadow to be hard-clipped at the transparent window edge. Increase to 800x260 to give the shadow room — the pill's visual screen position is unchanged since it uses CSS 'fixed bottom-5' relative to the window bottom.
- Use show:false + ready-to-show event so window only appears once content is painted, eliminating the black rectangle flash - Increase window height to 320px and shift y down 55px so the pill (now bottom-20 = 80px from window bottom) has 80px of transparent space below it for the downward shadow to render unclipped
VideoPlayback's pointer drag handler was calling clampFocusToStage with region.depth, which ignored customScale entirely. When dragging the zoom indicator with a custom scale set, the drag was clamped to the preset depth boundaries instead of the actual scale — making it impossible to drag beyond those boundaries. Switch to clampFocusToScale(focus, getZoomScale(region)) so the drag respects customScale the same way the overlay indicator and export do.
The Open Studio button (Clapperboard) now handles opening the editor where users can import any video. The separate open-video-file icon and its handler are redundant and have been removed.
Open Studio already leads to the editor empty state which has a Load Project button. The separate HUD toolbar shortcut is redundant. Removed the button, its handler, and the now-unused folder icon import.
Use show:false + ready-to-show on the editor window so it stays hidden until the first paint. Also set backgroundColor to #09090b so even if the window becomes visible before React mounts, it shows the correct dark background rather than white.
…nsertCSS - Remove File > Import Video File (Ctrl+I) — importing lives in the editor empty state only, avoiding confusion with 'add on top of existing video' semantics - Inject 'background: #09090b' on dom-ready so html/body/#root are dark before React mounts, eliminating the white sub-titlebar flash even on a cold first Vite load
The flash was caused by two compounding issues: 1. The Tailwind/shadcn CSS uses `.dark` class to switch from light (--background: white) to dark variables. Since no code applied `.dark` to <html> before React mounted, every `bg-background` usage resolved to white. 2. The lazy-loaded VideoEditor Suspense fallback (`bg-background`) was the visible culprit — it rendered white for the ~50-100ms while VideoEditor was being imported. Fix: add `class="dark"` directly to the <html> element in index.html. The attribute is parsed before any CSS or JS runs, so dark CSS variables are in effect from frame 0 — no flash. HUD/source-selector windows are unaffected because they override `background: transparent` via inline styles (which beat stylesheet rules). Also harden the Suspense fallback to use an explicit `bg-[#09090b]` instead of `bg-background` as a belt-and-suspenders guard.
- Add `loadingEditor` translation key to all 11 locale editor.json files - Suspense fallback in App.tsx now shows "Loading editor..." since at that point we don't yet know whether there is a video to load - VideoEditor loading spinner starts as "Loading editor..." and flips to "Loading video..." only once loadInitialData discovers a recording session or video path to load — no video means it stays as "Loading editor..." throughout
…no project file hasProjectUnsavedChanges only fires when the current snapshot differs from the baseline — but for a fresh import or recording, the baseline IS the current state (nothing edited yet), so the app considered it "saved" even though no .openscreen file exists on disk. Extend the hasUnsavedChanges check: if a video is loaded but currentProjectPath is null (no file saved yet), treat it as unsaved regardless of the snapshot diff. After a successful Save, currentProjectPath is set and this clause becomes false, handing control back to the normal snapshot comparison.
When File > New Project is triggered with an unsaved video/project loaded,
show the UnsavedChangesDialog with a "newProject" variant instead of
immediately clearing state. The variant swaps the copy to:
- "Do you want to save your project before creating a new one?"
- "Save & New Project" / "Discard & New Project" / Cancel
Implementation:
- UnsavedChangesDialog accepts an optional variant prop ("close"|"newProject")
that switches detail text and button labels via translation keys
- showCloseConfirmDialog boolean replaced by confirmDialogVariant state
("close"|"newProject"|null) — one dialog handles both cases
- handleNewProject checks hasUnsavedChanges before clearing state;
new doNewProject helper resets videoPath, currentProjectPath and
lastSavedSnapshot cleanly
- Three new translation keys added to all 11 locale dialogs.json files:
detailNewProject, saveAndNewProject, discardAndNewProject
The countdown-overlay-show handler was calling showInactive() before waiting for the page to load — Chromium showed a black rectangle while still painting the first frame. Fix: wait for the ready-to-show event (fires after first paint) before calling showInactive(), so the window only becomes visible once its content is fully rendered. The redundant did-finish-load wait is replaced by ready-to-show which is the correct Electron signal for this purpose.
…sing When isNativeWindowsCaptureAvailable returns reason="missing-helper" (helper binary not installed — always the case in dev mode), the code was throwing instead of returning false, which killed the recording entirely instead of falling through to the standard web MediaRecorder path. Treat missing-helper the same as unsupported-os: silently return false and let the web recorder take over.
…dia on Windows The Windows code path used navigator.mediaDevices.getDisplayMedia() which requires setDisplayMediaRequestHandler to be registered in the main process. That handler was never implemented, causing a "Not supported" error and preventing recording from starting on Windows when falling back from the native helper. Replace the platform-branched capture logic with a single getUserMedia + chromeMediaSource: "desktop" path that works on both macOS and Windows. Cursor capture mode is already handled separately via setRecordingState.
Use a ref to keep the last non-null confirmDialogVariant stable while the dialog animates out. Previously, setting variant to null caused the fallback ??\"close\" to kick in mid-animation, briefly showing \"Save & Close\" content and firing a spurious sendCloseConfirmResponse(\"cancel\") IPC call.
The mic/webcam selectors were positioned at bottom-[68px] but the HUD bar sits at bottom-20 (80px), causing selectors to render behind the bar. Moved to bottom-[136px] (bar bottom 80px + bar height ~46px + 10px gap).
doNewProject() was not resetting webcamVideoPath/webcamVideoSourcePath, causing the webcam track from a previous recording to bleed into the next project. Also clear them in handleImportVideo since imported external videos never carry a webcam track.
The drop handler called onProjectLoaded() which was wired to handleLoadProject, causing it to re-open a file picker dialog after the file was already loaded. Added onProjectFileDropped callback so the loaded project data is passed directly to applyLoadedProject without a second dialog. Also improved the empty state UI: moved the drag-and-drop hint below the supported formats line with a small upload icon for visual clarity.
- Non-.openscreen files now show "Unsupported Format" dialog instead of silently doing nothing - Failed project loads (e.g. video path no longer accessible) show a "Could Not Open File" dialog with a helpful message - Both use the same design language as the rest of the app (dark dialog, app icon, close button)
- Unified EditorEmptyState to a single onProjectOpened(project, path) callback used by both Load Project button and drag-and-drop, eliminating the double file-picker that was opening (EditorEmptyState + VideoEditor each calling loadProjectFile independently) - Made loadProjectFileFromPath resilient to getApprovedProjectSession failures: path approval errors no longer block the project from loading, they just skip setting the recording session (video player handles the "file not found" case gracefully)
…Path via preload - Import webUtils from electron in preload and expose getPathForFile(file) via contextBridge — replaces the removed File.path property (Electron 32+) - Expose loadProjectFileFromPath(filePath) as a direct ipcRenderer.invoke call, bypassing the native-bridge routing layer for drag-drop use - Add matching TypeScript declarations to electron-env.d.ts so the renderer can call window.electronAPI.getPathForFile / window.electronAPI.loadProjectFileFromPath with full type safety Files changed: electron/preload.ts — webUtils import + two new contextBridge entries electron/electron-env.d.ts — type declarations for both new API methods
…w project clearCurrentVideoPath() previously only nulled currentVideoPath, leaving currentProjectPath set. On next editor open, loadCurrentProjectFile() would find a stale path and reload the discarded project. Fix by also clearing currentProjectPath and calling setCurrentRecordingSessionState(null) so the main-process state is fully reset when the user chooses New Project. Files changed: electron/ipc/handlers.ts — clearCurrentVideoPath clears all three state vars
…alog polish
- Replace File.path (removed in Electron 32+) with
window.electronAPI.getPathForFile() for drag-drop path resolution
- Use window.electronAPI.loadProjectFileFromPath() with try-catch for
robust error handling on dropped project files
- Wire all UI strings through useScopedT("editor") / useScopedT("common")
so the empty state is fully localised across 11 languages
- Add frozen-ref flash fix (lastDropErrorRef) so the drop-error dialog
doesn't snap to the wrong content during the Radix closing animation
- Polish drop-error dialog layout: icon wrapped in a ring container
(w-10 h-10 rounded-full bg-white/5 ring-1 ring-white/10) with a
smaller w-5 h-5 AlertCircle inside for correct visual balance
- Add drag-over overlay and drag-leave boundary guard
Files changed:
src/components/video-editor/EditorEmptyState.tsx — full rewrite
…ader button UnsavedChangesDialog: - Add "loadProject" as a third variant alongside "close" and "newProject" - Detail text, save label, and discard label all branch on the new variant using nested ternaries for zero-duplication logic VideoEditor: - Rename old handleLoadProject to doLoadProject (the raw IPC work) - New handleLoadProject wrapper: shows confirmDialogVariant="loadProject" when hasUnsavedChanges, otherwise calls doLoadProject directly - Add handleLoadProjectConfirmSave / handleLoadProjectConfirmDiscard callbacks wired to the UnsavedChangesDialog for the loadProject path - Add "New Project" button (FilePlus icon) to the editor header bar, positioned before the existing Load Project button - Expand confirmDialogVariant state to "close"|"newProject"|"loadProject"|null with a matching lastConfirmVariantRef to prevent flash on dialog close Files changed: src/components/video-editor/UnsavedChangesDialog.tsx — loadProject variant src/components/video-editor/VideoEditor.tsx — guard + header button
…uages Add three keys to every locale's dialogs.json: detailLoadProject — context sentence shown under the dialog title saveAndLoadProject — primary action button label discardAndLoadProject — secondary (destructive) action button label Locales updated: ar, en, es, fr, ja-JP, ko-KR, ru, tr, vi, zh-CN, zh-TW Files changed: src/i18n/locales/*/dialogs.json — 11 files, 3 new keys each
Add the full "emptyState" section to every locale's editor.json: title, description, importVideoButton, loadProjectButton, supportedFormats, dragDropHint, dropOverlay, and the nested dropErrors object (unsupportedFormatTitle/Message, couldNotOpenTitle/Message) Locales updated: ar, en, es, fr, ja-JP, ko-KR, ru, tr, vi, zh-CN, zh-TW Files changed: src/i18n/locales/*/editor.json — 11 files, full emptyState section each
Add project.new key to every locale's settings.json alongside the existing project.save and project.load keys. Used by the new New Project button in the editor header bar. Locales updated: ar, en, es, fr, ja-JP, ko-KR, ru, tr, vi, zh-CN, zh-TW Files changed: src/i18n/locales/*/settings.json — 11 files, 1 new key each
📝 WalkthroughWalkthroughAdds preload/native-bridge and IPC to load projects by filesystem path, a new Editor empty-state with drag/drop and menu flows, unsaved-change dialog variants for new/load/close, ready-to-show window polish, Windows cursor overlay and HUD cursor polling, editor-history reset, recorder unification, and broad i18n additions. ChangesStudio Dashboard & Empty State Workflow
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 370323c417
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
electron/main.ts (1)
114-127:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winLowkey risky: Line 117 can force-close an existing editor and nuke unsaved work.
The concern is valid. At Line 114, if the focused window isn't an editor,
createEditorWindowWrapper()gets called (Line 117). That function force-closesmainWindowby settingisForceClosing = true(Line 365), which bypasses the unsaved-changes save prompt in the close handler (Line 374). So if an editor is already open but unfocused, this drops all unsaved changes without warning.Better approach: first search
BrowserWindow.getAllWindows()for an existing unfocused editor usingisEditorWindow()and route the action there. Only callcreateEditorWindowWrapper()if no editor exists.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@electron/main.ts` around lines 114 - 127, The current logic calls createEditorWindowWrapper() (which force-closes mainWindow) whenever the focused window isn't an editor, risking loss of unsaved work; change it to first search BrowserWindow.getAllWindows() for an existing editor via isEditorWindow() and route the action to that unfocused editor (set targetWindow to that window and send the channel or wait for did-finish-load if necessary); only call createEditorWindowWrapper() to create a new editor and assign mainWindow if no existing editor window is found. Ensure you reference BrowserWindow.getFocusedWindow(), BrowserWindow.getAllWindows(), isEditorWindow(), createEditorWindowWrapper(), and mainWindow when making the change so you avoid invoking the force-close path when an editor already exists.
🧹 Nitpick comments (8)
src/i18n/locales/zh-CN/settings.json (1)
1-1: 💤 Low valuenit: utf-8 bom at the start
there's a byte order mark (BOM) before the opening brace. most modern parsers handle it fine, but json specs kinda discourage it. lowkey might wanna check if your editor is adding this automatically - could cause weird issues with stricter parsers down the line.
not blocking anything though, electron/node will parse this just fine.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/locales/zh-CN/settings.json` at line 1, The file settings.json contains a UTF-8 BOM (U+FEFF) before the opening brace; remove the BOM so the file starts with "{" to avoid issues with strict JSON parsers, then save the file as UTF-8 without BOM (adjust your editor/IDE or use "Save with encoding" to do this). Optionally add an .editorconfig or CI lint rule to enforce "utf-8" without BOM for JSON files and re-commit the cleaned settings.json.src/i18n/locales/ar/settings.json (1)
1-1: ⚡ Quick winRemove BOM from all locale JSON files for consistency
All 11
settings.jsonfiles insrc/i18n/locales/have a UTF-8 BOM (U+FEFF) at the start. While Node.js 22.22.1 handles this fine, Vite's bundler and the i18n loader work correctly regardless, it's cleaner to strip it. There's already precedent inscripts/test-windows-native-cursor.mjswhere BOM is explicitly stripped.Affected files (all locale variants)
src/i18n/locales/ar/settings.json src/i18n/locales/en/settings.json src/i18n/locales/es/settings.json src/i18n/locales/fr/settings.json src/i18n/locales/ja-JP/settings.json src/i18n/locales/ko-KR/settings.json src/i18n/locales/ru/settings.json src/i18n/locales/tr/settings.json src/i18n/locales/vi/settings.json src/i18n/locales/zh-CN/settings.json src/i18n/locales/zh-TW/settings.json🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/locales/ar/settings.json` at line 1, Remove the UTF-8 BOM (U+FEFF) at the start of each locale settings.json file listed (e.g., src/i18n/locales/ar/settings.json and the other 10 locale variants) so the files begin with the normal JSON '{' character; open each settings.json, delete the invisible BOM character at the very start, save the file as UTF-8 without BOM, and re-run build/tests to confirm the i18n loader and Vite bundler behave the same.src/hooks/useScreenRecorder.ts (1)
776-811: 💤 Low valuenit: the video-only
getUserMediacall is written twice.low priority, but the
audio: false, video: videoConstraintscall shows up in both the systemAudio-catch branch and the else branch. a tiny helper would dedupe and make the control flow easier to read:♻️ optional cleanup
+ const captureVideoOnly = () => + navigator.mediaDevices.getUserMedia({ + audio: false, + video: videoConstraints, + } as unknown as MediaStreamConstraints); + if (systemAudioEnabled) { try { screenMediaStream = await navigator.mediaDevices.getUserMedia({ audio: { mandatory: { chromeMediaSource: CHROME_MEDIA_SOURCE, chromeMediaSourceId: selectedSource.id, }, }, video: videoConstraints, } as unknown as MediaStreamConstraints); } catch (audioErr) { console.warn("System audio capture failed, falling back to video-only:", audioErr); toast.error(t("recording.systemAudioUnavailable")); - screenMediaStream = await navigator.mediaDevices.getUserMedia({ - audio: false, - video: videoConstraints, - } as unknown as MediaStreamConstraints); + screenMediaStream = await captureVideoOnly(); } } else { - screenMediaStream = await navigator.mediaDevices.getUserMedia({ - audio: false, - video: videoConstraints, - } as unknown as MediaStreamConstraints); + screenMediaStream = await captureVideoOnly(); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/hooks/useScreenRecorder.ts` around lines 776 - 811, Duplicate getUserMedia calls for video-only should be extracted into a small helper to reduce repetition: create a function (e.g., getVideoOnlyStream or fetchVideoOnlyStream) that returns navigator.mediaDevices.getUserMedia({ audio: false, video: videoConstraints } as MediaStreamConstraints) and replace both occurrences where screenMediaStream is assigned in the systemAudioEnabled catch block and the else branch; keep existing references to videoConstraints, screenMediaStream, systemAudioEnabled, audioErr, toast and t unchanged so behavior and error handling remain the same.src/i18n/locales/ko-KR/editor.json (1)
1-1: ⚡ Quick winBOM is present, but it's not actually a problem
Yeah, there's a UTF-8 BOM here (
), but honestly this is a non-issue. 32 other translation files across all locales have the same thing, and Vite's JSON loader handles it gracefully at build time. If it was breaking JSON parsing, you'd already be seeing errors in like, half your locales.If it bugs you, stripping BOM from all the translation files (
dialogs.json,editor.json,settings.jsonacross all locales) would be a nice cleanup—but that's optional polish, not a fix. Just keep it consistent with the rest of the codebase if you touch it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/locales/ko-KR/editor.json` at line 1, The file contains a UTF-8 BOM character at the start of src/i18n/locales/ko-KR/editor.json; remove the leading BOM (the invisible U+FEFF character) from this file and, if you touch other locale files, remove the BOM consistently in dialogs.json, editor.json, and settings.json across locales so all translation JSON files start with a plain '{' and remain consistent with the rest of the codebase and Vite's JSON loader.src/components/video-editor/EditorEmptyState.tsx (2)
110-111: Dialog onOpenChange could cause raceline 110:
onOpenChange={(open) => !open && setDropError(null)}works but kinda subtle. if the dialog is opened programmatically (not by user interaction), this callback fires immediately and could clear the error before the animation completes. thelastDropErrorRefpattern saves you here but it's a bit fragile.nit: could be clearer as
onOpenChange={(open) => { if (!open) setDropError(null); }}but current code works fine.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/video-editor/EditorEmptyState.tsx` around lines 110 - 111, The onOpenChange handler on the Dialog (onOpenChange={(open) => !open && setDropError(null)}) can clear dropError prematurely when the dialog is toggled programmatically; change the handler to explicitly check for a closing transition and only clear after the dialog has fully closed (e.g., use onOpenChange={(open) => { if (!open) setDropError(null); }} combined with waiting for the Dialog's onClose or animation end callback instead of immediately calling setDropError, and remove reliance on the fragile lastDropErrorRef pattern; target the Dialog component and the setDropError usage to implement this safer clear-on-close behavior.
64-75: ⚡ Quick winfile extension check needs case handling
line 64 checks
f.name.endsWith(".openscreen")which is case-sensitive. someone could dropproject.OpenScreenorPROJECT.OPENSCREENand it'd be rejected. probably rare but easy fix.case-insensitive check
- const projectFile = files.find((f) => f.name.endsWith(".openscreen")); + const projectFile = files.find((f) => f.name.toLowerCase().endsWith(".openscreen"));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/video-editor/EditorEmptyState.tsx` around lines 64 - 75, The file extension check in EditorEmptyState.tsx uses files.find((f) => f.name.endsWith(".openscreen")) which is case-sensitive and will reject variants like "project.OpenScreen"; change the predicate to perform a case-insensitive comparison (e.g., compare f.name.toLowerCase() with ".openscreen") and guard against missing/undefined names (use f?.name or String(f.name) before lowercasing); keep the rest of the flow (setting projectFile, setDropError, and calling window.electronAPI.getPathForFile) unchanged.electron/electron-env.d.ts (1)
169-169: ⚡ Quick wingetPathForFile return type might be wrong
the type says
(file: File) => stringbutwebUtils.getPathForFile()can potentially throw or return empty string on failure. might wanna make thisstring | nullorstring | undefinedfor safety, or at least document that it can throw.safer type
- getPathForFile: (file: File) => string; + getPathForFile: (file: File) => string | null;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@electron/electron-env.d.ts` at line 169, The declaration for getPathForFile is too narrow: change its return type from (file: File) => string to reflect failures (e.g., (file: File) => string | null or string | undefined) and add a brief JSDoc note that webUtils.getPathForFile may also throw; then update any callers (or ensure callers already check for null/undefined and catch exceptions) to handle the absence of a path or thrown errors. Reference symbols: getPathForFile and webUtils.getPathForFile.src/i18n/locales/en/editor.json (1)
1-1: ⚡ Quick winBOM character at start of file
there's a UTF-8 BOM () at the beginning of this file. this can cause parsing issues with some JSON parsers or tools. most modern parsers handle it but it's cleaner without it.
nit: remove the BOM if it wasn't intentional. most editors can strip it on save.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/locales/en/editor.json` at line 1, The file begins with a UTF-8 BOM character (U+FEFF) immediately before the opening '{' which can break some JSON parsers; remove that BOM so the file starts with '{', re-save it as UTF-8 without BOM, and run the JSON validator/linters to confirm parsing succeeds; also update your editor/save settings (or project pre-commit hook) to prevent writing BOMs in the future.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@electron/ipc/handlers.ts`:
- Around line 1780-1783: Change the case-sensitive endsWith check in the
drop/project validation to compare the file extension case-insensitively: use
path.extname(filePath).toLowerCase() and compare it to ("." +
PROJECT_FILE_EXTENSION).toLowerCase() (or lowercased PROJECT_FILE_EXTENSION)
instead of filePath.endsWith(`.${PROJECT_FILE_EXTENSION}`) in the validation
block inside electron/ipc/handlers.ts so names like "MyProject.OPENSCREEN" are
accepted; ensure path is imported if not already and keep the same early-return
shape ({ success: false, message: ... }) used around this check.
In `@electron/preload.ts`:
- Line 127: Wrap the call to webUtils.getPathForFile(file) inside getPathForFile
in the preload API with a try/catch so any thrown errors (e.g., inaccessible or
synthetic files) are caught and the function returns null instead of
propagating; update the exposed signature in electron-env.d.ts to return string
| null so callers can handle the nullable path. Ensure you reference
getPathForFile (preload) and webUtils.getPathForFile in the change and log or
silently swallow the caught error per project convention.
In `@src/components/video-editor/EditorEmptyState.tsx`:
- Around line 70-75: Wrap the call to
window.electronAPI.getPathForFile(projectFile) in a try/catch inside
EditorEmptyState (the drop handler) so exceptions from getPathForFile are
caught; on catch call setDropError("load-failed") (and optionally log the error)
and return, and keep the existing !filePath check after the try to handle falsy
returns. This ensures projectFile conversion won't throw and crash the handler.
In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 2179-2186: The quick-start click handler currently calls
window.electronAPI.startNewRecording() and ignores its promise, swallowing
failures; change the empty-state branch to reuse the existing
handleNewRecordingConfirm() flow (or call startNewRecording() and await its
result, then run the same success/error path as handleNewRecordingConfirm) so
errors and { success: false } outcomes are handled the same as when
setShowNewRecordingDialog(true) is used; update the onClick branch that checks
videoPath to invoke handleNewRecordingConfirm() (or await and check the returned
value) and propagate errors to the same UI/error handling logic.
- Around line 716-724: doNewProject only clears file/path state; add a full
per-project editor reset by extracting the media-path clears into a new
resetEditorState helper that also clears timeline/history, undo/redo stacks,
crop state, wallpaper, cursor/mode state, selection(s), and any project-scoped
transient UI flags, then call that helper from doNewProject (replace the
existing state clears with a call to resetEditorState) and reuse the same
resetEditorState from the import flow handler(s) (e.g. your
importVideo/handleImport functions) so importing into an empty project can't
carry over previous edits; reference the current symbols (doNewProject,
nativeBridgeClient.project.clearCurrentVideoPath, setVideoPath,
setVideoSourcePath, setWebcamVideoPath, setWebcamVideoSourcePath,
setCurrentProjectPath, setLastSavedSnapshot) when locating where to plug in the
new reset.
In `@src/hooks/useScreenRecorder.ts`:
- Around line 787-811: When getUserMedia with system audio constraints fails
inside the block that checks systemAudioEnabled, call the setter to disable the
system-audio toggle so UI state matches reality (similar to how the mic path
uses setMicrophoneEnabled(false)); specifically, in the catch handling audioErr
for the block that assigns screenMediaStream with
chromeMediaSource/chromeMediaSourceId, add setSystemAudioEnabled(false) before
showing the toast and falling back to the video-only getUserMedia so the
systemAudioEnabled flag is cleared and the toggle won’t remain on.
In `@src/i18n/locales/ja-JP/settings.json`:
- Line 1: The file begins with a UTF-8 BOM character before the opening brace
"{" which should be removed; open the locale file, delete the BOM at the very
start (so the first character is '{'), and re-save the file encoded as UTF-8
without BOM to ensure cleaner, portable JSON (no code changes required beyond
removing the BOM).
In `@src/i18n/locales/ru/settings.json`:
- Line 1: The JSON file ru/settings.json (and the other locale JSON files like
dialogs.json, editor.json, settings.json in each locale) currently contains a
UTF-8 BOM at the start; remove the leading BOM character from the beginning of
these files so the first byte is the opening brace '{' (perform a bulk cleanup
across all 33 files to strip BOMs), verify files are valid UTF-8 without BOM and
commit the cleaned files.
In `@src/i18n/locales/tr/settings.json`:
- Line 1: Remove the UTF-8 BOM (U+FEFF) present at the start of each locale
settings.json (e.g., settings.json in every locale folder) so the files begin
directly with '{'. Locate files that start with the invisible BOM character and
delete that first character (ensure the first byte is not 0xEF,0xBB,0xBF), then
save and run your JSON/lint checks to confirm parsing still succeeds. Keep all
file encoding as UTF-8 without BOM going forward.
In `@src/i18n/locales/vi/settings.json`:
- Line 1: The file src/i18n/locales/vi/settings.json contains a UTF-8 BOM at the
start which can break some JSON parsers; remove the BOM bytes so the file begins
directly with "{" (i.e., re-save the file without BOM/UTF-8 signature using your
editor or run a tool/command that strips BOM) and ensure the file is saved as
plain UTF-8 without BOM so json loaders consume it correctly.
---
Outside diff comments:
In `@electron/main.ts`:
- Around line 114-127: The current logic calls createEditorWindowWrapper()
(which force-closes mainWindow) whenever the focused window isn't an editor,
risking loss of unsaved work; change it to first search
BrowserWindow.getAllWindows() for an existing editor via isEditorWindow() and
route the action to that unfocused editor (set targetWindow to that window and
send the channel or wait for did-finish-load if necessary); only call
createEditorWindowWrapper() to create a new editor and assign mainWindow if no
existing editor window is found. Ensure you reference
BrowserWindow.getFocusedWindow(), BrowserWindow.getAllWindows(),
isEditorWindow(), createEditorWindowWrapper(), and mainWindow when making the
change so you avoid invoking the force-close path when an editor already exists.
---
Nitpick comments:
In `@electron/electron-env.d.ts`:
- Line 169: The declaration for getPathForFile is too narrow: change its return
type from (file: File) => string to reflect failures (e.g., (file: File) =>
string | null or string | undefined) and add a brief JSDoc note that
webUtils.getPathForFile may also throw; then update any callers (or ensure
callers already check for null/undefined and catch exceptions) to handle the
absence of a path or thrown errors. Reference symbols: getPathForFile and
webUtils.getPathForFile.
In `@src/components/video-editor/EditorEmptyState.tsx`:
- Around line 110-111: The onOpenChange handler on the Dialog
(onOpenChange={(open) => !open && setDropError(null)}) can clear dropError
prematurely when the dialog is toggled programmatically; change the handler to
explicitly check for a closing transition and only clear after the dialog has
fully closed (e.g., use onOpenChange={(open) => { if (!open) setDropError(null);
}} combined with waiting for the Dialog's onClose or animation end callback
instead of immediately calling setDropError, and remove reliance on the fragile
lastDropErrorRef pattern; target the Dialog component and the setDropError usage
to implement this safer clear-on-close behavior.
- Around line 64-75: The file extension check in EditorEmptyState.tsx uses
files.find((f) => f.name.endsWith(".openscreen")) which is case-sensitive and
will reject variants like "project.OpenScreen"; change the predicate to perform
a case-insensitive comparison (e.g., compare f.name.toLowerCase() with
".openscreen") and guard against missing/undefined names (use f?.name or
String(f.name) before lowercasing); keep the rest of the flow (setting
projectFile, setDropError, and calling window.electronAPI.getPathForFile)
unchanged.
In `@src/hooks/useScreenRecorder.ts`:
- Around line 776-811: Duplicate getUserMedia calls for video-only should be
extracted into a small helper to reduce repetition: create a function (e.g.,
getVideoOnlyStream or fetchVideoOnlyStream) that returns
navigator.mediaDevices.getUserMedia({ audio: false, video: videoConstraints } as
MediaStreamConstraints) and replace both occurrences where screenMediaStream is
assigned in the systemAudioEnabled catch block and the else branch; keep
existing references to videoConstraints, screenMediaStream, systemAudioEnabled,
audioErr, toast and t unchanged so behavior and error handling remain the same.
In `@src/i18n/locales/ar/settings.json`:
- Line 1: Remove the UTF-8 BOM (U+FEFF) at the start of each locale
settings.json file listed (e.g., src/i18n/locales/ar/settings.json and the other
10 locale variants) so the files begin with the normal JSON '{' character; open
each settings.json, delete the invisible BOM character at the very start, save
the file as UTF-8 without BOM, and re-run build/tests to confirm the i18n loader
and Vite bundler behave the same.
In `@src/i18n/locales/en/editor.json`:
- Line 1: The file begins with a UTF-8 BOM character (U+FEFF) immediately before
the opening '{' which can break some JSON parsers; remove that BOM so the file
starts with '{', re-save it as UTF-8 without BOM, and run the JSON
validator/linters to confirm parsing succeeds; also update your editor/save
settings (or project pre-commit hook) to prevent writing BOMs in the future.
In `@src/i18n/locales/ko-KR/editor.json`:
- Line 1: The file contains a UTF-8 BOM character at the start of
src/i18n/locales/ko-KR/editor.json; remove the leading BOM (the invisible U+FEFF
character) from this file and, if you touch other locale files, remove the BOM
consistently in dialogs.json, editor.json, and settings.json across locales so
all translation JSON files start with a plain '{' and remain consistent with the
rest of the codebase and Vite's JSON loader.
In `@src/i18n/locales/zh-CN/settings.json`:
- Line 1: The file settings.json contains a UTF-8 BOM (U+FEFF) before the
opening brace; remove the BOM so the file starts with "{" to avoid issues with
strict JSON parsers, then save the file as UTF-8 without BOM (adjust your
editor/IDE or use "Save with encoding" to do this). Optionally add an
.editorconfig or CI lint rule to enforce "utf-8" without BOM for JSON files and
re-commit the cleaned settings.json.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ad976fb7-5aa5-486b-848e-bbd49c9a6da7
📒 Files selected for processing (51)
electron/electron-env.d.tselectron/ipc/handlers.tselectron/ipc/nativeBridge.tselectron/main.tselectron/native-bridge/services/projectService.tselectron/preload.tselectron/windows.tsindex.htmlsrc/App.tsxsrc/components/launch/LaunchWindow.tsxsrc/components/video-editor/EditorEmptyState.tsxsrc/components/video-editor/UnsavedChangesDialog.tsxsrc/components/video-editor/VideoEditor.tsxsrc/components/video-editor/VideoPlayback.tsxsrc/hooks/useScreenRecorder.tssrc/i18n/locales/ar/dialogs.jsonsrc/i18n/locales/ar/editor.jsonsrc/i18n/locales/ar/settings.jsonsrc/i18n/locales/en/dialogs.jsonsrc/i18n/locales/en/editor.jsonsrc/i18n/locales/en/launch.jsonsrc/i18n/locales/en/settings.jsonsrc/i18n/locales/es/dialogs.jsonsrc/i18n/locales/es/editor.jsonsrc/i18n/locales/es/settings.jsonsrc/i18n/locales/fr/dialogs.jsonsrc/i18n/locales/fr/editor.jsonsrc/i18n/locales/fr/settings.jsonsrc/i18n/locales/ja-JP/dialogs.jsonsrc/i18n/locales/ja-JP/editor.jsonsrc/i18n/locales/ja-JP/settings.jsonsrc/i18n/locales/ko-KR/dialogs.jsonsrc/i18n/locales/ko-KR/editor.jsonsrc/i18n/locales/ko-KR/settings.jsonsrc/i18n/locales/ru/dialogs.jsonsrc/i18n/locales/ru/editor.jsonsrc/i18n/locales/ru/settings.jsonsrc/i18n/locales/tr/dialogs.jsonsrc/i18n/locales/tr/editor.jsonsrc/i18n/locales/tr/settings.jsonsrc/i18n/locales/vi/dialogs.jsonsrc/i18n/locales/vi/editor.jsonsrc/i18n/locales/vi/settings.jsonsrc/i18n/locales/zh-CN/dialogs.jsonsrc/i18n/locales/zh-CN/editor.jsonsrc/i18n/locales/zh-CN/settings.jsonsrc/i18n/locales/zh-TW/dialogs.jsonsrc/i18n/locales/zh-TW/editor.jsonsrc/i18n/locales/zh-TW/settings.jsonsrc/native/client.tssrc/native/contracts.ts
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
doNewProject() was only clearing media/project pointers (videoPath, webcamVideoPath, projectPath). All undoable editor state — zoom regions, trim regions, speed regions, annotation regions, crop, wallpaper, shadow, blur, aspect ratio, webcam layout, etc. — was left intact, so importing a new video after discarding a project would silently inherit all previous edits. Fix by: - Adding resetState() to useEditorHistory — resets present to INITIAL_EDITOR_STATE and clears the full undo/redo history stack - Calling resetState() in doNewProject() - Also resetting non-undoable selection state (selectedZoomId, etc.), playback state (currentTime, isPlaying), cursor preferences (showCursor, cursorSize, cursorSmoothing, cursorMotionBlur, cursorClickBounce), and region ID counters (nextZoomIdRef, etc.) Files changed: src/hooks/useEditorHistory.ts — add resetState() src/components/video-editor/VideoEditor.tsx — call resetState() + full reset in doNewProject
endsWith('.openscreen') silently rejected files with uppercase extensions
(e.g. MyProject.OPENSCREEN) — a real false negative on Windows/macOS where
the OS can preserve or change extension casing. Replaced with
path.extname(filePath).toLowerCase() for consistent behaviour.
Files changed:
electron/ipc/handlers.ts — loadProjectFileFromPath extension validation
PowerShell's default UTF-8-with-BOM encoding introduced ef bb bf at the start of every locale file when the files were batch-patched earlier. The upstream files were BOM-free. Stripped with sed across all 33 locale JSON files (dialogs.json, editor.json, settings.json × 11 locales). Files changed: src/i18n/locales/*/dialogs.json — BOM removed (11 files) src/i18n/locales/*/editor.json — BOM removed (11 files) src/i18n/locales/*/settings.json — BOM removed (11 files)
webUtils.getPathForFile() can throw if the file object is a synthetic blob or comes from a non-filesystem context. Without a guard the exception would surface as an unhandled rejection in the renderer. Wrap in try/catch and return an empty string on failure so callers can rely on the existing falsy check. Files changed: electron/preload.ts — getPathForFile wrapped in try/catch
The !filePath guard only catches a falsy return value — it won't catch an exception thrown by webUtils.getPathForFile() (e.g. synthetic blob, virtual file from certain drag sources). Wrap the call in try/catch so any thrown error is caught and shown as a "load-failed" drop error instead of crashing the drop handler. Files changed: src/components/video-editor/EditorEmptyState.tsx — try/catch around getPathForFile
… button The empty-state fast path (no video loaded) was calling startNewRecording() with void, silently dropping any failure result. Replace with handleNewRecordingConfirm() which checks result.success and calls setError() on failure — consistent with the dialog path. Files changed: src/components/video-editor/VideoEditor.tsx — fast-path uses handleNewRecordingConfirm
When getUserMedia with system audio constraints throws, the code fell back to video-only and showed a toast, but systemAudioEnabled stayed true. The toggle kept showing "on" despite audio not being captured, and every subsequent recording start would hit the same failed branch and re-show the same toast. Added setSystemAudioEnabled(false) in the catch block, consistent with how the mic path calls setMicrophoneEnabled(false) on failure. Files changed: src/hooks/useScreenRecorder.ts — setSystemAudioEnabled(false) on audio capture failure
…egression This fix has been reverted twice by parallel PRs touching VideoPlayback.tsx. The comment explains why getZoomScale(region) must be used instead of region.depth in the drag handler, and explicitly warns against switching it back. Files changed: src/components/video-editor/VideoPlayback.tsx — protective comment on clampFocusForRegion
|
Hey @siddharthvaddem, I've sent you an email about this Feature as this is kinda gigantic like it was a 5-6hr session, that explains some things plus there's an unlisted video there that explains. Also I've added now with the last commit in a6e91e3, for some reason after figuring out the edge case for the custom zoom, maybe a later merge had it removed or something, now it's more clearer (I'm explaining the bug in the video sent via email) Hope you like so far, I'll check the cursor later (in the current main windows cursor gets baked in either way when cursor button is toggled) I really hope that I tested the most of the edge cases (more than 20-30 QAs ran before shipping) |
52ddaf4 to
a6e91e3
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts`:
- Around line 227-246: The current emission block caches
lastEmittedCursorType/lastEmittedAssetId even when currentAssetId is present but
its bitmap/nativeAsset is not yet available, which suppresses a later real asset
emission; change the logic in the emission branch so you first resolve
nativeAsset = currentAssetId ? this.assets.get(currentAssetId) : null, then: if
currentAssetId && !nativeAsset, call
this.options.onCursorTypeChange(currentType, null) but do NOT update
this.lastEmittedCursorType or this.lastEmittedAssetId (so the real bitmap can
later trigger); otherwise (nativeAsset exists OR currentAssetId is null meaning
no custom asset) update lastEmittedCursorType/lastEmittedAssetId and emit the
payload including the nativeAsset properties when present; this uses
this.assets, this.options.onCursorTypeChange, this.lastEmittedCursorType, and
this.lastEmittedAssetId to locate the code to change.
In `@electron/windows.ts`:
- Around line 274-280: The overlay window is using
screen.getPrimaryDisplay().bounds so it only covers the primary monitor; replace
that with virtual-desktop bounds computed from screen.getAllDisplays() (or, if
you want the overlay on the display with the cursor, use
screen.getCursorScreenPoint() + screen.getDisplayNearestPoint()) and use that
computed bounds when constructing the BrowserWindow (update where bounds is read
and the BrowserWindow options in the win creation). Ensure you compute combined
x/y/width/height from displays' bounds (union) and then pass those values into
new BrowserWindow instead of screen.getPrimaryDisplay().bounds.
In `@src/components/launch/CursorOverlay.tsx`:
- Around line 243-246: The onCursorTypeChange handler currently only calls
setLiveAsset when asset is truthy, which leaves the previous bitmap set when the
main process sends null; update the handler registered via
window.electronAPI.onCursorTypeChange to always call setLiveAsset(asset) (or
setLiveAsset(asset ?? null)) regardless of truthiness and keep
setCursorType(type) as-is so the state clears correctly and the fallback SVG can
render; refer to the onCursorTypeChange registration, setCursorType, and
setLiveAsset symbols in CursorOverlay.tsx when making the change.
In `@src/components/video-editor/SettingsPanel.tsx`:
- Around line 1399-1419: The UI strings in SettingsPanel.tsx are hardcoded
(e.g., "Cursor Style", "Native OS", "Custom cursor", "Size") — update these to
use the existing i18n helper (t(...)) like the rest of the panel: replace
literal labels inside the Select/SelectTrigger/SelectItem blocks and other
occurrences around the cursor controls (including the range 1429-1490) with
t('key') calls, using the same translation keys/style used elsewhere in this
component; ensure cursorDisplayMode and onCursorDisplayModeChange behavior is
unchanged and only the displayed strings are wrapped with t(...) so translations
render correctly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2f2c8d81-b664-4712-92bf-a5600e33948d
📒 Files selected for processing (12)
electron/electron-env.d.tselectron/ipc/handlers.tselectron/native-bridge/cursor/recording/factory.tselectron/native-bridge/cursor/recording/windowsNativeRecordingSession.tselectron/native-bridge/cursor/recording/windowsNativeRecordingSession.types.tselectron/preload.tselectron/windows.tssrc/App.tsxsrc/components/launch/CursorOverlay.tsxsrc/components/video-editor/SettingsPanel.tsxsrc/components/video-editor/VideoEditor.tsxsrc/components/video-editor/VideoPlayback.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- electron/electron-env.d.ts
- src/App.tsx
- src/components/video-editor/VideoEditor.tsx
Pull Request Template
Description
Studio / Editor Dashboard (Empty State)
Closes #576
Studio / Editor Dashboard (Empty State)
TOOLBAR BEFORE
TOOLBAR AFTER
Removed Open Video and Load Project from the main recorder toolbar; replaced with a single Studio button that opens the editor — a clean entry point that can scale to tutorials, FAQ, changelogs, or sponsor spots later
Built the editor empty state dashboard with Import Video File… and Load Project… buttons, supported formats hint, and a drag & drop zone
Project Management Logic
currentProjectPathwas never nulled out, so switching to the recorder and back would reopen the discarded projectDesign Decision — No "Import Video" in the File Menu
The File menu intentionally does not include an Import Video option.
The editor currently operates on a single video / single timeline model.
Adding File → Import Video would have effectively triggered a "new project"
action under the hood — silently discarding the current session and loading
a new video — which would be deeply confusing for end users with no visual
indication that their work was replaced.
The deliberate choice is to keep video import on the empty state dashboard
only, where the context is unambiguous (there is nothing open to lose).
Once the editor evolves to support multi-clip timelines or video stitching,
a proper File → Import Video flow can be introduced with the right UX
scaffolding around it.
Drag & Drop
.openscreenproject files only.openscreenfile shows an "Unsupported Format" dialog explaining to use Import Video insteadFile.pathwas removed in Electron 32+ (this project runs Electron 41); replaced withwebUtils.getPathForFile()Editor Loading & White Flash Fix
index.htmlnow setsbackground: #09090bas an inline body style so the dark background is applied instantly before any JavaScript loads;VideoEditoris now lazy-loaded viaReact.lazy+Suspensewith a custom dark-background spinner as the fallback, so the code-split chunk download no longer shows a blank white screenBug Fixes
bottom-[68px]vs the actual80pxtoolbar height); corrected so they float above it properlyEditorEmptyStateandVideoEditorwere independently callingloadProjectFile(); fixed by unifying to a single callbackCustom Zoom Regression (Restored Fix)
The image represents that even if you wanted to use "Custom zoom" to put it to the corner, you couldn't because of an "invisible boundary". In the previous PR when "Custom-zoom" was created I already handled this edge case, but for some reason with a possible merge from an earlier version, the code might have been erased causing the bug again.
MOST functional bug fixes (based on recent main):
1103c93 — When isNativeWindowsCaptureAvailable returned reason="missing-helper", the code was throwing an error instead of gracefully falling back. This killed the recording entirely rather than continuing with the standard web MediaRecorder. Fixed by treating missing-helper the same as unsupported-os — silently return false and let the web recorder take over.
28819bd — Even after that fallback was unblocked, recording still failed on Windows because the fallback code path used navigator.mediaDevices.getDisplayMedia(), which requires a setDisplayMediaRequestHandler registered in the main process — that handler was never implemented. Fixed by replacing the platform-branched capture logic with a single getUserMedia + chromeMediaSource: "desktop" path that works on both macOS and Windows.
Localization
Motivation
Type of Change
Related Issue(s)
Closes #576
Screenshots / Video
Video (if applicable):
I'll send an unlisted YouTube Video for this, as this is quite of a big PR
Test Plan
Empty State & Navigation
Drag & Drop
.openscreenfile → project opensUnsaved Changes Guard
UI & Layout
Localization
Checklist
Edge Cases & Fixes
1103c93+28819bd— Recording fallback on WindowsNative Windows capture helper is absent in dev mode and optional in
production. The code was throwing instead of falling back, killing
recording entirely. Fixed by treating
missing-helperas gracefulfallback to web
getUserMedia + chromeMediaSource: "desktop"— workson both macOS and Windows without the binary.
4cf966a— Drag & drop broken on Electron 32+ (File.pathremoved)File.pathwas silently returningundefinedon Electron 41 — droppedfiles appeared to load but nothing happened. Replaced with
webUtils.getPathForFile()exposed viacontextBridgein preload.61de5ea— New Project didn't actually reset stateclearCurrentVideoPath()only nulledcurrentVideoPath.currentProjectPathstayed set in the main process, so switching to the recorder and reopening
the editor reloaded the discarded project. Fixed by also clearing
currentProjectPathandcurrentRecordingSession.c46aea5— Double file picker on Load ProjectBoth
EditorEmptyStateandVideoEditorwere independently callingloadProjectFile(), opening two file pickers back to back. Fixed byunifying to a single callback passed down as a prop.
4694ea4+2faa676— Dialog content flash during close animationRadix UI's closing animation sets the controlled value to
nullbeforethe exit animation completes. Both
UnsavedChangesDialog(New Project vsClose variants) and the drop error dialog (Unsupported Format vs Could Not
Open variants) were snapping to the wrong content mid-animation. Fixed with
a frozen ref that preserves the last non-null variant for the duration of
the animation.
801f1f6— Webcam state leak between projectsAfter discarding a project or importing a new video, the webcam preview
path from the previous session was still mounted. Fixed by explicitly
clearing webcam state on both New Project and video import.
7dd438e— Device selector panels hidden behind toolbarWebcam and microphone selector panels had
bottom-[68px]but the toolbaris actually
80pxtall, causing them to render behind it. Corrected tothe right offset.
7e4595f+7700c19— Imported/recorded video not treated as unsavedImporting a video or finishing a recording bypassed the unsaved changes
flag, so closing or starting a new project skipped the Save dialog
entirely. Fixed by marking the project dirty on both entry points.
96dc6d5— Custom zoom drag stuck near canvas edgesThe drag boundary clamping used a hardcoded scale instead of the custom
zoom scale, creating invisible walls near the canvas edges. The zoom region
couldn't be dragged to the intended position. Restored the correct
scale-aware boundary calculation.
edf5953+3c84f11+770f05f— White flash on editor openThree compounding causes: (1)
<body>had no background so the OS defaultwhite showed before React painted; (2)
VideoEditorwas eagerly bundled,so the JS parse delay showed a blank screen; (3) an
insertCSScall wasn'tapplied early enough. Fixed with an inline
background: #09090bon<body>in
index.htmland lazy-loadingVideoEditorviaReact.lazy + Suspensewith a dark spinner fallback.
Thank you for contributing!
Summary by CodeRabbit
New Features
Bug Fixes
Chores