Skip to content

✨ feat(linux): background export with system notification#337

Open
dongvv-2538 wants to merge 3 commits intosiddharthvaddem:mainfrom
dongvv-2538:feature/background-export-notification-linux
Open

✨ feat(linux): background export with system notification#337
dongvv-2538 wants to merge 3 commits intosiddharthvaddem:mainfrom
dongvv-2538:feature/background-export-notification-linux

Conversation

@dongvv-2538
Copy link
Copy Markdown

@dongvv-2538 dongvv-2538 commented Apr 5, 2026

Summary

When the user closes the editor window while an MP4 or GIF export is running, a native dialog now intercepts the close and offers three choices:

  • Continue in Background — hides the editor window and lets the export finish silently
  • Cancel Export & Close — cancels the running export and closes the window
  • Cancel — keeps the window open (no change)

When a background export completes, the file is auto-saved to the Downloads folder and a system notification fires showing the filename. Clicking the notification reveals the file in the file manager.

Platform: Linux (initial implementation). The tray icon keeps the app alive so the user can reopen the editor at any time.


Changes

Electron (main process)

  • electron/main.ts: added editorIsExporting flag; IPC listener for set-is-exporting; editor window close handler now shows the export-in-progress dialog and sends background-export-ready / cancel-export-and-close IPC events accordingly
  • electron/ipc/handlers.ts: two new handlers — save-exported-video-to-path (silent file write, no dialog) and send-export-notification (Electron Notification with click-to-reveal); added Notification to electron imports

Preload bridge

  • electron/preload.ts: exposed six new electronAPI methods: setIsExporting, saveExportedVideoToPath, sendExportNotification, onBackgroundExportReady, onCancelExportAndClose, exportCancelledDone
  • electron/electron-env.d.ts: TypeScript declarations for all six new methods

Renderer

  • src/components/video-editor/VideoEditor.tsx:
    • Added backgroundExportDirRef ref to hold the Downloads path when running headlessly
    • Calls window.electronAPI.setIsExporting(true/false) around every export to keep main process in sync
    • Registers onBackgroundExportReady listener (stores the downloads dir) and onCancelExportAndClose listener (cancels the exporter and ACKs)
    • Both the GIF and MP4 save paths check backgroundExportDirRef.current — if set, they auto-save via saveExportedVideoToPath + sendExportNotification instead of showing the save dialog
    • handleCancelExport also notifies the main process immediately

i18n

  • en/dialogs.json, es/dialogs.json, zh-CN/dialogs.json: new exportInBackground translation namespace

Testing

  1. Start an MP4 or GIF export (Settings panel → Export button)
  2. While the progress bar is running, click the window close button
  3. Verify the "Export in Progress" dialog appears with all three options
  4. Choose "Continue in Background" → window closes, export keeps running
  5. Wait for completion → system notification appears with filename
  6. Click the notification → file manager opens at the Downloads folder
  7. Repeat with "Cancel Export & Close" → export stops, window closes cleanly

Summary by CodeRabbit

  • New Features

    • Continue video exports in background when closing the app; option to continue or cancel-and-close.
    • Exports can auto-save to Downloads and trigger clickable completion notifications with format/filename.
    • Multilingual dialog text added (en/es/zh-CN).
  • Changes

    • GIF exports: new smaller frame-rate options (5/10 FPS), default lowered to 10 FPS, new "Small (480p)" size, optimized GIF output for smaller files, and adjusted encoder quality/dithering.

When the user closes the editor window while an MP4 or GIF export is
running, a native dialog offers three choices:
- Continue in Background: hides the window and lets the export finish
- Cancel Export & Close: cancels the job and closes
- Cancel: keeps the window open

On completion the file is auto-saved to the Downloads folder and a
system notification fires with the filename. Clicking the notification
reveals the file in the file manager.

Changes:
- electron/main.ts: track editorIsExporting; intercept close to show the
  export-in-progress dialog; send background-export-ready and
  cancel-export-and-close IPC events
- electron/ipc/handlers.ts: add save-exported-video-to-path (silent save)
  and send-export-notification (Electron Notification) handlers
- electron/preload.ts: expose setIsExporting, saveExportedVideoToPath,
  sendExportNotification, onBackgroundExportReady, onCancelExportAndClose,
  exportCancelledDone
- electron/electron-env.d.ts: TypeScript declarations for new API methods
- src/components/video-editor/VideoEditor.tsx: add backgroundExportDirRef;
  notify main process of exporting state; branch save paths for background
  mode; register onBackgroundExportReady / onCancelExportAndClose listeners
- i18n (en/es/zh-CN): add exportInBackground translation keys
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dcaf4a33-6120-473e-a94d-5e0b3eb61962

📥 Commits

Reviewing files that changed from the base of the PR and between 09f6175 and 3c0a2b2.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • electron-builder.json5
  • electron/ipc/handlers.ts
  • package.json
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/projectPersistence.ts
  • src/lib/exporter/gifExporter.ts
  • src/lib/exporter/types.ts

📝 Walkthrough

Walkthrough

Adds background-export support across layers: new renderer↔preload APIs, IPC handlers to save files and send notifications (with GIF optimization), main-process close handling for active exports, renderer changes to auto-save to Downloads and handle cancellation, i18n strings, and gifsicle packaging.

Changes

Cohort / File(s) Summary
Type definitions
electron/electron-env.d.ts
Declared new window.electronAPI members: setIsExporting(), saveExportedVideoToPath(), sendExportNotification(), onBackgroundExportReady(), onCancelExportAndClose(), and exportCancelledDone().
IPC handlers & gif tooling
electron/ipc/handlers.ts, package.json
Added save-exported-video-to-path and send-export-notification handlers; introduced gifsicle invocation (getGifsiclePath, optimizeGifBuffer) and safe-join Downloads path guard. Added gifsicle dependency.
Main process
electron/main.ts
Added editorIsExporting state and revised window close flow: show background-export dialog, support "continue in background" (hide + emit background-export-ready) and "cancel and close" (emit cancel-export-and-close, await export-cancelled-done with 5s fallback).
Preload / API exposure
electron/preload.ts
Exposed new APIs on window.electronAPI: setIsExporting (send), saveExportedVideoToPath & sendExportNotification (invoke), onBackgroundExportReady & onCancelExportAndClose (subscribe/unsubscribe helpers), and exportCancelledDone (send).
Renderer — Video editor
src/components/video-editor/VideoEditor.tsx, src/components/video-editor/SettingsPanel.tsx, src/components/video-editor/projectPersistence.ts
Default GIF FPS changed to 10; added backgroundExportDirRef, registered IPC listeners, set/cleared exporting state via window.electronAPI.setIsExporting, branch export flow to auto-save via saveExportedVideoToPath + sendExportNotification when Downloads dir provided, and ensure cleanup on finish/cancel. Normalization updated to accept 5/10 FPS and small preset.
Exporter internals
src/lib/exporter/gifExporter.ts, src/lib/exporter/types.ts
GIF encoder params adjusted (quality lowered, dithering disabled); types/constants extended to include 5 and 10 FPS and small size preset (480p).
i18n
src/i18n/locales/en/dialogs.json, src/i18n/locales/es/dialogs.json, src/i18n/locales/zh-CN/dialogs.json
Added exportInBackground keys: dialog title/message/detail, actions (continueInBackground, cancelAndClose), and completion notification strings with {{format}}/{{filename}} placeholders.
Build config
electron-builder.json5
Added asarUnpack: ["node_modules/gifsicle/**"] and reformatted; ensures gifsicle binary is unpacked from ASAR.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • siddharthvaddem

Poem

🐰✨ hopped in at 2am, tweaked the flow so exports roam,
files land in Downloads while the main window goes home.
gifs get a trim, notifications sing,
cancel handshakes tidy, promises cling —
lowkey magic: background saves now safely roam.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately captures the main feature (background export with notification) but specifies only Linux when the implementation appears cross-platform compatible.
Description check ✅ Passed Description comprehensively covers all major changes across electron/preload/renderer/i18n with clear testing steps and well-structured sections matching the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c2686c023

ℹ️ 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".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c2686c023

ℹ️ 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".

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@electron/ipc/handlers.ts`:
- Around line 514-525: The IPC handler "save-exported-video-to-path" currently
writes arbitrary renderer-supplied paths; restrict it to the user's downloads
folder by resolving and validating the destination in main. In the handler
(ipcMain.handle "save-exported-video-to-path") derive the downloads root via
app.getPath("downloads"), extract/sanitize the incoming filename (e.g.,
path.basename and strip path separators), construct the destination with
path.join(downloadsRoot, safeFilename), then path.resolve and confirm the
resolved path begins with the downloadsRoot; if not, reject and return {
success: false, error: 'invalid path' }. Keep the file write to fs.writeFile
only after this validation and return the success/path result.

In `@electron/main.ts`:
- Around line 311-323: The "Cancel Export & Close" branch currently returns
early and force-closes the window, bypassing the existing unsaved-changes
safeguard; remove the early "return" so execution falls through into the
existing unsaved-changes flow and modify the
ipcMain.once("export-cancelled-done", ...) handler (and the fallback timeout) to
clear the fallback but not call forceCloseEditorWindow(win) directly—allow the
normal unsaved-changes logic to prompt/save/discard the document after the
renderer signals export cancellation (keep the fallback only to clear or
escalate if the renderer never responds).

In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 1149-1159: Ensure the background-save path is deterministic and
surface failures by checking and awaiting backgroundExportDirRef.current before
attempting background save in the VideoEditor export logic: if
backgroundExportDirRef.current is undefined, wait for or derive a deterministic
fallback directory (e.g., a known Downloads path) rather than falling through to
saveExportedVideo; wrap calls to saveExportedVideoToPath and
sendExportNotification in try/catch and on failure fall back to calling
saveExportedVideo (which shows the save dialog) and surface the error to the
user (e.g., via existing UI error handler), updating the branches around
backgroundExportDirRef.current, saveExportedVideoToPath, and
sendExportNotification (also apply same fixes to the similar block at the other
location).
- Around line 474-480: The exportCancelledDone() IPC call is emitted immediately
after calling exporterRef.current.cancel(), which can let main close the window
before handleExport() finishes; instead, change the cancel flow so
exportCancelledDone() is sent only after the exporter has fully unwound.
Specifically, update the useEffect cancel handler to await a
cancellation-completion signal from the exporter (e.g., make
exporterRef.current.cancel() return a Promise or expose a completion
event/promise like exporterRef.current.onCancelled / exporterRef.current.done)
and call window.electronAPI.exportCancelledDone() only after that promise/event
resolves; ensure this cooperates with handleExport()’s await exporter.export()
and its finally block so state cleanup runs before notifying main.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e331cd9-80e5-44ef-939f-1e4664e2ce3d

📥 Commits

Reviewing files that changed from the base of the PR and between 11788ad and 0c2686c.

📒 Files selected for processing (8)
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/main.ts
  • electron/preload.ts
  • src/components/video-editor/VideoEditor.tsx
  • src/i18n/locales/en/dialogs.json
  • src/i18n/locales/es/dialogs.json
  • src/i18n/locales/zh-CN/dialogs.json

- handlers.ts: validate save-exported-video-to-path destination is within
  the Downloads folder (path.resolve + startsWith check) to prevent path
  traversal; derive safe filename with path.basename; write to the resolved
  destination and return it in the success response
- main.ts: 'Cancel Export & Close' now calls win.close() after the renderer
  ACKs cancellation instead of force-closing, allowing the normal
  unsaved-changes guard to run; fallback timeout cleans up the listener
  instead of force-closing
- VideoEditor.tsx: wrap background save/notify (GIF and MP4) in try/catch;
  on failure clear backgroundExportDirRef and fall back to the normal save
  dialog so the user can still save their export
- VideoEditor.tsx: defer exportCancelledDone() until after cancel() has
  propagated (via a zero-timeout microtask flush) so the main process does
  not race ahead of handleExport's finally-block cleanup
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
electron/main.ts (1)

315-326: ⚠️ Potential issue | 🟠 Major

Keep the timeout path inside the normal close flow.

Line 325 still jumps straight to forceCloseEditorWindow(win). If the renderer never ACKs within 5 seconds, this bypasses the unsaved-changes guard at Lines 333-365 and can drop dirty project state without offering Save / Discard / Cancel. Re-enter the normal win.close() path here instead of force-closing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/main.ts` around lines 315 - 326, The fallback timeout currently
calls forceCloseEditorWindow(win) which bypasses the normal unsaved-changes
flow; instead, change the timeout handler to remove the "export-cancelled-done"
listener, clear the fallback timeout, and call win.close() so the normal
close/unsaved-changes handling (the logic tied to win.close() and the
unsaved-changes check) runs; update or remove any flags/listeners as needed so
this re-entrancy does not loop (use the existing onCancelled and
editor-exporting state rather than forceCloseEditorWindow).
src/components/video-editor/VideoEditor.tsx (2)

475-485: ⚠️ Potential issue | 🟠 Major

Don't ACK cancellation before handleExport() has actually unwound.

Lines 482-484 send exportCancelledDone() after a timer tick, and Line 1483 clears setIsExporting(false) immediately after cancel(). electron/main.ts treats those signals as “safe to close”, but await exporter.export() can still be in flight and its finally may not have run yet. Please drive both signals from a real cancellation-complete promise/event, not a microtask delay.

Also applies to: 1480-1484

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/VideoEditor.tsx` around lines 475 - 485, The code
currently calls exporter.cancel() and then uses a timer tick to call
window.electronAPI.exportCancelledDone(), which can race with handleExport's
finally that clears state (setIsExporting(false)); instead, change the flow so
exportCancelledDone() is invoked only after a real "cancellation complete"
promise/event from the exporter or from handleExport. Concretely: update the
exporter API or handleExport to expose a cancellation-completion promise or
event (e.g., exporter.cancel() returns a Promise or
emit/exporter.on('cancelled') when fully unwound), then in the
onCancelExportAndClose handler await that cancellation-complete signal before
calling window.electronAPI.exportCancelledDone() and before clearing
isExporting; replace the setTimeout/microtask trick with awaiting that true
completion to avoid races between exporter.cancel(), handleExport, and
exportCancelledDone().

1157-1160: ⚠️ Potential issue | 🟠 Major

Wait for the Downloads path before deciding between background-save and dialog-save.

Lines 1157-1159 and 1324-1326 snapshot backgroundExportDirRef.current once, then Lines 1190 and 1357 immediately fall back to saveExportedVideo() if it's still null. If the export finishes just before onBackgroundExportReady() runs, this can open the normal save dialog while the editor is hidden. Gate this branch on a promise/latch for the ready IPC, or another deterministic Downloads path, instead of a best-effort ref read.

Also applies to: 1189-1190, 1324-1327, 1356-1357

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/VideoEditor.tsx` around lines 1157 - 1160, The
current logic reads backgroundExportDirRef.current once and immediately falls
back to saveExportedVideo() if null, which races with the IPC that supplies the
Downloads path; change this to await a deterministic readiness signal instead of
a best-effort ref read: introduce or reuse a promise/latch that
onBackgroundExportReady resolves (e.g., backgroundExportReadyPromise), then in
the branches that currently check backgroundExportDirRef.current (the blocks
that build fullPath and the fallbacks to saveExportedVideo()), await that
promise (with a short timeout if needed) and only treat a missing Downloads path
as final after the promise settles; update all places referencing
backgroundExportDirRef.current and the immediate calls to saveExportedVideo() to
first await the readiness signal so the dialog isn’t opened while the editor is
hidden.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 1161-1188: The try/catch currently couples the file write and
notification so a notification failure triggers the fallback save; change the
logic in the block using saveExportedVideoToPath / sendExportNotification so you
first call await window.electronAPI.saveExportedVideoToPath(arrayBuffer,
fullPath) and handle its failure by entering the fallback that calls
window.electronAPI.saveExportedVideo(...) (clearing
backgroundExportDirRef.current, setting setUnsavedExport, etc.); if the write
succeeds, then call window.electronAPI.sendExportNotification("GIF",
saveResult.path) inside its own try/catch and treat notification errors as
non-fatal (log them but do not fall back to manual save or change
setUnsavedExport/handleExportSaved), and apply the same refactor to the
analogous block with saveExportedVideoToPath/sendExportNotification at lines
~1328-1355 so only actual write failures trigger the manual save flow.

---

Duplicate comments:
In `@electron/main.ts`:
- Around line 315-326: The fallback timeout currently calls
forceCloseEditorWindow(win) which bypasses the normal unsaved-changes flow;
instead, change the timeout handler to remove the "export-cancelled-done"
listener, clear the fallback timeout, and call win.close() so the normal
close/unsaved-changes handling (the logic tied to win.close() and the
unsaved-changes check) runs; update or remove any flags/listeners as needed so
this re-entrancy does not loop (use the existing onCancelled and
editor-exporting state rather than forceCloseEditorWindow).

In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 475-485: The code currently calls exporter.cancel() and then uses
a timer tick to call window.electronAPI.exportCancelledDone(), which can race
with handleExport's finally that clears state (setIsExporting(false)); instead,
change the flow so exportCancelledDone() is invoked only after a real
"cancellation complete" promise/event from the exporter or from handleExport.
Concretely: update the exporter API or handleExport to expose a
cancellation-completion promise or event (e.g., exporter.cancel() returns a
Promise or emit/exporter.on('cancelled') when fully unwound), then in the
onCancelExportAndClose handler await that cancellation-complete signal before
calling window.electronAPI.exportCancelledDone() and before clearing
isExporting; replace the setTimeout/microtask trick with awaiting that true
completion to avoid races between exporter.cancel(), handleExport, and
exportCancelledDone().
- Around line 1157-1160: The current logic reads backgroundExportDirRef.current
once and immediately falls back to saveExportedVideo() if null, which races with
the IPC that supplies the Downloads path; change this to await a deterministic
readiness signal instead of a best-effort ref read: introduce or reuse a
promise/latch that onBackgroundExportReady resolves (e.g.,
backgroundExportReadyPromise), then in the branches that currently check
backgroundExportDirRef.current (the blocks that build fullPath and the fallbacks
to saveExportedVideo()), await that promise (with a short timeout if needed) and
only treat a missing Downloads path as final after the promise settles; update
all places referencing backgroundExportDirRef.current and the immediate calls to
saveExportedVideo() to first await the readiness signal so the dialog isn’t
opened while the editor is hidden.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: a1a151d7-4a07-4d7c-bb76-a975d6a674de

📥 Commits

Reviewing files that changed from the base of the PR and between 0c2686c and 09f6175.

📒 Files selected for processing (3)
  • electron/ipc/handlers.ts
  • electron/main.ts
  • src/components/video-editor/VideoEditor.tsx
✅ Files skipped from review due to trivial changes (1)
  • electron/ipc/handlers.ts

- Install gifsicle npm package and unpack binary via asarUnpack
  so it is accessible outside the asar archive in packaged builds
- Add optimizeGifBuffer() in IPC handlers that runs gifsicle -O3
  --lossy=40 --colors 256 after gif.js encoding; integrated into
  both save-exported-video and save-exported-video-to-path handlers
- Resolve gifsicle binary path at runtime (app.getAppPath in dev,
  process.resourcesPath in packaged) to avoid import.meta.url crash
  when Vite bundles the main process
- Disable FloydSteinberg dithering in gif.js (dither noise destroys
  LZW compression) and set quality:1 for accurate palette sampling
- Add Small (480p) size preset and 5/10 FPS frame rate options to
  give users file-size-friendly export choices
- Change default GIF frame rate from 15 FPS to 10 FPS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant