diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f42a92d..35177bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -250,4 +250,5 @@ jobs: release/**/*.AppImage release/**/*.zsync release/**/*.deb + release/**/*.pacman retention-days: 30 diff --git a/electron-builder.json5 b/electron-builder.json5 index 1770238..ad6cd18 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -57,7 +57,9 @@ }, "linux": { "target": [ - "AppImage" + "AppImage", + "deb", + "pacman" ], "icon": "icons/icons/png", "artifactName": "${productName}-Linux-${version}.${ext}", diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 30fbf23..7361b26 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -60,6 +60,21 @@ function isPathAllowed(filePath: string): boolean { return getAllowedReadDirs().some((dir) => isPathWithinDir(resolved, dir)); } +/** + * Helper function to build dialog options with a parent window only when it's valid. + * This prevents passing stale or destroyed BrowserWindow references to dialog calls. + */ +function buildDialogOptions( + baseOptions: T, + parentWindow: BrowserWindow | null, +): T & { parent?: BrowserWindow } { + const mainWindow = parentWindow; + if (mainWindow && !mainWindow.isDestroyed()) { + return { ...baseOptions, parent: mainWindow }; + } + return baseOptions; +} + function hasAllowedImportVideoExtension(filePath: string): boolean { return ALLOWED_IMPORT_VIDEO_EXTENSIONS.has(path.extname(filePath).toLowerCase()); } @@ -978,14 +993,18 @@ export function registerIpcHandlers( ? [{ name: mainT("dialogs", "fileDialogs.gifImage"), extensions: ["gif"] }] : [{ name: mainT("dialogs", "fileDialogs.mp4Video"), extensions: ["mp4"] }]; - const result = await dialog.showSaveDialog({ - title: isGif - ? mainT("dialogs", "fileDialogs.saveGif") - : mainT("dialogs", "fileDialogs.saveVideo"), - defaultPath: path.join(app.getPath("downloads"), fileName), - filters, - properties: ["createDirectory", "showOverwriteConfirmation"], - }); + const dialogOptions = buildDialogOptions( + { + title: isGif + ? mainT("dialogs", "fileDialogs.saveGif") + : mainT("dialogs", "fileDialogs.saveVideo"), + defaultPath: path.join(app.getPath("downloads"), fileName), + filters, + properties: ["createDirectory", "showOverwriteConfirmation"], + }, + getMainWindow(), + ); + const result = await dialog.showSaveDialog(dialogOptions); if (result.canceled || !result.filePath) { return { @@ -1020,18 +1039,22 @@ export function registerIpcHandlers( }); ipcMain.handle("open-video-file-picker", async () => { try { - const result = await dialog.showOpenDialog({ - title: mainT("dialogs", "fileDialogs.selectVideo"), - defaultPath: RECORDINGS_DIR, - filters: [ - { - name: mainT("dialogs", "fileDialogs.videoFiles"), - extensions: ["webm", "mp4", "mov", "avi", "mkv"], - }, - { name: mainT("dialogs", "fileDialogs.allFiles"), extensions: ["*"] }, - ], - properties: ["openFile"], - }); + const dialogOptions = buildDialogOptions( + { + title: mainT("dialogs", "fileDialogs.selectVideo"), + defaultPath: RECORDINGS_DIR, + filters: [ + { + name: mainT("dialogs", "fileDialogs.videoFiles"), + extensions: ["webm", "mp4", "mov", "avi", "mkv"], + }, + { name: mainT("dialogs", "fileDialogs.allFiles"), extensions: ["*"] }, + ], + properties: ["openFile"], + }, + getMainWindow(), + ); + const result = await dialog.showOpenDialog(dialogOptions); if (result.canceled || result.filePaths.length === 0) { return { success: false, canceled: true }; @@ -1110,18 +1133,22 @@ export function registerIpcHandlers( ? safeName : `${safeName}.${PROJECT_FILE_EXTENSION}`; - const result = await dialog.showSaveDialog({ - title: mainT("dialogs", "fileDialogs.saveProject"), - defaultPath: path.join(RECORDINGS_DIR, defaultName), - filters: [ - { - name: mainT("dialogs", "fileDialogs.openscreenProject"), - extensions: [PROJECT_FILE_EXTENSION], - }, - { name: "JSON", extensions: ["json"] }, - ], - properties: ["createDirectory", "showOverwriteConfirmation"], - }); + const dialogOptions = buildDialogOptions( + { + title: mainT("dialogs", "fileDialogs.saveProject"), + defaultPath: path.join(RECORDINGS_DIR, defaultName), + filters: [ + { + name: mainT("dialogs", "fileDialogs.openscreenProject"), + extensions: [PROJECT_FILE_EXTENSION], + }, + { name: "JSON", extensions: ["json"] }, + ], + properties: ["createDirectory", "showOverwriteConfirmation"], + }, + getMainWindow(), + ); + const result = await dialog.showSaveDialog(dialogOptions); if (result.canceled || !result.filePath) { return { @@ -1152,19 +1179,23 @@ export function registerIpcHandlers( ipcMain.handle("load-project-file", async () => { try { - const result = await dialog.showOpenDialog({ - title: mainT("dialogs", "fileDialogs.openProject"), - defaultPath: RECORDINGS_DIR, - filters: [ - { - name: mainT("dialogs", "fileDialogs.openscreenProject"), - extensions: [PROJECT_FILE_EXTENSION], - }, - { name: "JSON", extensions: ["json"] }, - { name: mainT("dialogs", "fileDialogs.allFiles"), extensions: ["*"] }, - ], - properties: ["openFile"], - }); + const dialogOptions = buildDialogOptions( + { + title: mainT("dialogs", "fileDialogs.openProject"), + defaultPath: RECORDINGS_DIR, + filters: [ + { + name: mainT("dialogs", "fileDialogs.openscreenProject"), + extensions: [PROJECT_FILE_EXTENSION], + }, + { name: "JSON", extensions: ["json"] }, + { name: mainT("dialogs", "fileDialogs.allFiles"), extensions: ["*"] }, + ], + properties: ["openFile"], + }, + getMainWindow(), + ); + const result = await dialog.showOpenDialog(dialogOptions); if (result.canceled || result.filePaths.length === 0) { return { success: false, canceled: true, message: "Open project canceled" }; diff --git a/electron/main.ts b/electron/main.ts index ad0a33f..1da3603 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -30,6 +30,18 @@ if (process.platform === "darwin") { app.commandLine.appendSwitch("disable-features", "MacCatapLoopbackAudioForScreenShare"); } +// Enable Wayland support for proper screen capture and window management +// on Wayland compositors (Hyprland, GNOME, KDE, etc.) +if (process.platform === "linux") { + const isWayland = + process.env.XDG_SESSION_TYPE === "wayland" || process.env.WAYLAND_DISPLAY !== undefined; + if (isWayland) { + app.commandLine.appendSwitch("ozone-platform", "wayland"); + // Enable WebRTCPipeWireCapturer for screen capture on Wayland + app.commandLine.appendSwitch("enable-features", "WaylandWindowDrag,WebRTCPipeWireCapturer"); + } +} + export const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); async function ensureRecordingsDir() { diff --git a/package.json b/package.json index 855160f..2e8421e 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "preview": "vite preview", "build:mac": "tsc && vite build && electron-builder --mac", "build:win": "tsc && vite build && electron-builder --win", - "build:linux": "tsc && vite build && electron-builder --linux AppImage deb", + "build:linux": "tsc && vite build && electron-builder --linux AppImage deb pacman", "test": "vitest --run", "test:watch": "vitest", "build-vite": "tsc && vite build",