From 0315155e4f33f04b74597f0065107393f06cfd4d Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Sat, 5 Sep 2026 16:15:37 +1000
Subject: [PATCH 1/9] perf: optimize application startup
---
electron-builder.json5 | 10 +-
electron/main.ts | 46 ++++------
electron/rendererServer.ts | 11 ++-
electron/updater.ts | 11 ++-
electron/windows.ts | 3 +
src/App.tsx | 92 +++++++++----------
src/components/launch/HudWindow.tsx | 11 +++
src/components/launch/LaunchWindow.tsx | 2 +-
src/components/launch/MarqueeText.tsx | 37 ++++++++
src/components/launch/SourceSelector.tsx | 39 +-------
.../launch/popovers/ProjectPopover.tsx | 33 ++++---
src/components/video-editor/EditorWindow.tsx | 26 ++++++
src/components/video-editor/SettingsPanel.tsx | 8 +-
.../video-editor/export/useExportRunner.ts | 20 ++--
.../project/useProjectLibraryController.ts | 2 +-
.../video-editor/projectPersistence.ts | 2 +-
vite.config.ts | 1 -
17 files changed, 205 insertions(+), 149 deletions(-)
create mode 100644 src/components/launch/HudWindow.tsx
create mode 100644 src/components/launch/MarqueeText.tsx
create mode 100644 src/components/video-editor/EditorWindow.tsx
diff --git a/electron-builder.json5 b/electron-builder.json5
index a333a8723..197dcc179 100644
--- a/electron-builder.json5
+++ b/electron-builder.json5
@@ -8,7 +8,7 @@
"node_modules/ffmpeg-static/**",
"node_modules/ffprobe-static/**",
"node_modules/uiohook-napi/**",
- "electron/native/**"
+ "electron/native/bin/**"
],
"productName": "Recordly",
"npmRebuild": true,
@@ -19,8 +19,13 @@
},
"files": [
"dist",
+ "!dist/wallpapers/**",
"dist-electron",
- "electron/native",
+ "electron/native/bin",
+ "!electron/native/bin/**/whisper-bench*",
+ "!electron/native/bin/**/whisper-quantize*",
+ "!electron/native/bin/**/whisper-server*",
+ "!electron/native/bin/**/whisper-vad-speech-segments*",
"!node_modules/ffprobe-static/bin/darwin/**",
"!node_modules/ffprobe-static/bin/linux/**",
"!node_modules/ffprobe-static/bin/win32/ia32/**",
@@ -111,4 +116,3 @@
"artifactName": "${productName}-windows-${arch}.${ext}"
}
}
-
diff --git a/electron/main.ts b/electron/main.ts
index 1ea7dce62..890726670 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -935,17 +935,11 @@ app.whenReady().then(async () => {
// Recordly does not use WebHID, Web Serial, or WebUSB. Do not grant devices by default.
session.defaultSession.setDevicePermissionHandler(() => false);
- if (process.platform === "darwin") {
- const cameraStatus = systemPreferences.getMediaAccessStatus("camera");
- if (cameraStatus !== "granted") {
- await systemPreferences.askForMediaAccess("camera");
- }
-
- const micStatus = systemPreferences.getMediaAccessStatus("microphone");
- if (micStatus !== "granted") {
- await systemPreferences.askForMediaAccess("microphone");
- }
- } else if (process.platform === "win32") {
+ // macOS prompts for camera and microphone access at the point of use. Asking
+ // here blocks the first window behind two modal OS permission flows and makes
+ // a fresh install look hung. Windows has no equivalent request API, so retain
+ // its diagnostic warnings.
+ if (process.platform === "win32") {
const cameraStatus = systemPreferences.getMediaAccessStatus("camera");
const micStatus = systemPreferences.getMediaAccessStatus("microphone");
if (cameraStatus !== "granted") {
@@ -986,22 +980,20 @@ app.whenReady().then(async () => {
updateTrayMenu();
}
setupApplicationMenu();
- // Ensure recordings directory exists
- await ensureRecordingsDir();
-
- if (!VITE_DEV_SERVER_URL) {
- try {
- await ensurePackagedRendererServer(RENDERER_DIST);
- } catch (error) {
- console.warn("[renderer-server] Failed to start packaged renderer server:", error);
- }
- }
-
- try {
- await ensureMediaServer();
- } catch (error) {
- console.warn("[media-server] Failed to start media server:", error);
- }
+ await Promise.all([
+ ensureRecordingsDir(),
+ !VITE_DEV_SERVER_URL
+ ? ensurePackagedRendererServer(RENDERER_DIST).catch((error) => {
+ console.warn(
+ "[renderer-server] Failed to start packaged renderer server:",
+ error,
+ );
+ })
+ : Promise.resolve(),
+ ensureMediaServer().catch((error) => {
+ console.warn("[media-server] Failed to start media server:", error);
+ }),
+ ]);
registerIpcHandlers(
createEditorWindowWrapper,
diff --git a/electron/rendererServer.ts b/electron/rendererServer.ts
index 8f0d56627..f3453a06f 100644
--- a/electron/rendererServer.ts
+++ b/electron/rendererServer.ts
@@ -28,6 +28,15 @@ function getContentType(filePath: string): string {
return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
}
+function getCacheControl(filePath: string): string {
+ // Vite fingerprints production assets, so they can be reused across the HUD,
+ // picker, toast, and editor windows without revalidation. Keep index.html
+ // uncached because it points at the current fingerprinted files.
+ return /-[A-Za-z0-9_-]{8,}\.[^.]+$/.test(path.basename(filePath))
+ ? "public, max-age=31536000, immutable"
+ : "no-cache";
+}
+
function resolveRequestedFilePath(rootDir: string, requestPathname: string): string | null {
const trimmedPathname = requestPathname === "/" ? "/index.html" : requestPathname;
@@ -72,7 +81,7 @@ async function servePackagedRendererRequest(
const fileContents = await fs.readFile(filePath);
response.writeHead(200, {
- "Cache-Control": "no-cache",
+ "Cache-Control": getCacheControl(filePath),
"Content-Type": getContentType(filePath),
});
diff --git a/electron/updater.ts b/electron/updater.ts
index 86c2d76d2..b9c4e5dca 100644
--- a/electron/updater.ts
+++ b/electron/updater.ts
@@ -8,6 +8,7 @@ import { readAppSetting, writeAppSetting } from "./appSettingsStore";
import { EXPERIMENTAL_UPDATE_DESCRIPTION, getUpdateChannelConfiguration } from "./updateChannel";
const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
+const INITIAL_UPDATE_CHECK_DELAY_MS = 15 * 1000;
export const UPDATE_REMINDER_DELAY_MS = 3 * 60 * 60 * 1000;
const DISMISSED_READY_REMINDER_DELAY_MS = 5 * 60 * 1000;
const AUTO_UPDATES_DISABLED = process.env.RECORDLY_DISABLE_AUTO_UPDATES === "1";
@@ -71,6 +72,7 @@ let updaterInitialized = false;
let updateCheckInProgress = false;
let manualCheckRequested = false;
let periodicCheckTimer: NodeJS.Timeout | null = null;
+let initialCheckTimer: NodeJS.Timeout | null = null;
let deferredReminderTimer: NodeJS.Timeout | null = null;
let devPreviewProgressTimer: NodeJS.Timeout | null = null;
let currentToastPayload: UpdateToastPayload | null = null;
@@ -905,7 +907,10 @@ export function setupAutoUpdates(
void showDownloadedUpdateDialog(getMainWindow, info.version);
});
- void checkForAppUpdates(getMainWindow);
+ initialCheckTimer = setTimeout(() => {
+ initialCheckTimer = null;
+ void checkForAppUpdates(getMainWindow);
+ }, INITIAL_UPDATE_CHECK_DELAY_MS);
periodicCheckTimer = setInterval(() => {
void checkForAppUpdates(getMainWindow);
}, UPDATE_CHECK_INTERVAL_MS);
@@ -913,6 +918,10 @@ export function setupAutoUpdates(
app.on("before-quit", () => {
clearDeferredReminderTimer();
clearDevPreviewProgressTimer();
+ if (initialCheckTimer) {
+ clearTimeout(initialCheckTimer);
+ initialCheckTimer = null;
+ }
if (periodicCheckTimer) {
clearInterval(periodicCheckTimer);
periodicCheckTimer = null;
diff --git a/electron/windows.ts b/electron/windows.ts
index eb17b2e29..23e874fa1 100644
--- a/electron/windows.ts
+++ b/electron/windows.ts
@@ -447,6 +447,7 @@ ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean)
});
export function createHudOverlayWindow(): BrowserWindow {
+ const perfStart = Date.now();
loadHudOverlayCaptureProtectionSetting();
hudOverlayFallbackExpanded = false;
hudOverlayWebcamPreviewVisible = false;
@@ -557,6 +558,7 @@ export function createHudOverlayWindow(): BrowserWindow {
}
win.webContents.on("did-finish-load", () => {
+ console.log(`[PERF:MAIN] HUD Window: did-finish-load in ${Date.now() - perfStart}ms`);
win?.webContents.send("main-process-message", new Date().toLocaleString());
// Safety fallback if renderer-ready signal never arrives.
setTimeout(() => {
@@ -577,6 +579,7 @@ export function createHudOverlayWindow(): BrowserWindow {
const handleHudRendererReady = () => {
if (!win.isDestroyed()) {
+ console.log(`[PERF:MAIN] HUD Window: renderer-ready in ${Date.now() - perfStart}ms`);
showHudWindow();
}
};
diff --git a/src/App.tsx b/src/App.tsx
index 6970efb16..25401793d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,54 +1,56 @@
-import { useEffect, useState } from "react";
-import { AnnouncementDialog } from "./components/announcements/AnnouncementDialog";
-import { LiveAnnouncementNotifications } from "./components/announcements/LiveAnnouncementNotifications";
-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 { lazy, Suspense, useEffect, useState } from "react";
import { useI18n } from "./contexts/I18nContext";
-import { ShortcutsProvider } from "./contexts/ShortcutsContext";
-import { loadAllCustomFonts } from "./lib/customFonts";
+
+const HudWindow = lazy(() => import("./components/launch/HudWindow"));
+const SourceSelector = lazy(() =>
+ import("./components/launch/SourceSelector").then((module) => ({
+ default: module.SourceSelector,
+ })),
+);
+const CountdownOverlay = lazy(() =>
+ import("./components/countdown/CountdownOverlay").then((module) => ({
+ default: module.CountdownOverlay,
+ })),
+);
+const UpdateToastWindow = lazy(() =>
+ import("./components/launch/UpdateToastWindow").then((module) => ({
+ default: module.UpdateToastWindow,
+ })),
+);
+const EditorWindow = lazy(() => import("./components/video-editor/EditorWindow"));
export default function App() {
- const [windowType, setWindowType] = useState("");
+ const [windowType] = useState(
+ () => new URLSearchParams(window.location.search).get("windowType") || "",
+ );
const { t } = useI18n();
const appIconSrc = "/app-icons/recordly-128.png";
useEffect(() => {
- const params = new URLSearchParams(window.location.search);
- const type = params.get("windowType") || "";
- setWindowType(type);
- document.documentElement.dataset.windowType = type;
+ document.documentElement.dataset.windowType = windowType;
if (
- type === "hud-overlay" ||
- type === "source-selector" ||
- type === "countdown" ||
- type === "update-toast"
+ windowType === "hud-overlay" ||
+ windowType === "source-selector" ||
+ windowType === "countdown" ||
+ windowType === "update-toast"
) {
document.body.style.background = "transparent";
document.documentElement.style.background = "transparent";
document.getElementById("root")?.style.setProperty("background", "transparent");
}
- if (type === "hud-overlay") {
+ if (windowType === "hud-overlay") {
document.documentElement.classList.add("hud-overlay-window");
document.body.classList.add("hud-overlay-window");
document.getElementById("root")?.classList.add("hud-overlay-window");
window.electronAPI?.hudOverlaySetIgnoreMouse?.(true);
- } else if (type === "update-toast") {
+ } else if (windowType === "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);
- });
- }, []);
+ }, [windowType]);
useEffect(() => {
document.title =
@@ -57,33 +59,25 @@ export default function App() {
: t("app.name", "Recordly");
}, [windowType, t]);
+ let content;
switch (windowType) {
case "hud-overlay":
- return (
- <>
-
-
- >
- );
+ content = ;
+ break;
case "source-selector":
- return ;
+ content = ;
+ break;
case "countdown":
- return ;
+ content = ;
+ break;
case "update-toast":
- return ;
+ content = ;
+ break;
case "editor":
- return (
- <>
-
-
-
-
-
-
- >
- );
+ content = ;
+ break;
default:
- return (
+ content = (
![]()
);
}
+
+ return
{content};
}
diff --git a/src/components/launch/HudWindow.tsx b/src/components/launch/HudWindow.tsx
new file mode 100644
index 000000000..a62dc3ade
--- /dev/null
+++ b/src/components/launch/HudWindow.tsx
@@ -0,0 +1,11 @@
+import { Toaster } from "../ui/sonner";
+import { LaunchWindow } from "./LaunchWindow";
+
+export default function HudWindow() {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx
index f7583b28e..66cbe608b 100644
--- a/src/components/launch/LaunchWindow.tsx
+++ b/src/components/launch/LaunchWindow.tsx
@@ -30,6 +30,7 @@ import { useLaunchWindowSystemState } from "./hooks/useLaunchWindowSystemState";
import { useRecordingTimer } from "./hooks/useRecordingTimer";
import { useWebcamPreviewOverlay } from "./hooks/useWebcamPreviewOverlay";
import styles from "./LaunchWindow.module.css";
+import { MarqueeText } from "./MarqueeText";
import { CountdownPopover } from "./popovers/CountdownPopover";
import {
LaunchPopoverCoordinatorProvider,
@@ -41,7 +42,6 @@ import { ProjectPopover } from "./popovers/ProjectPopover";
import { SourcePopover } from "./popovers/SourcePopover";
import { WebcamPopover } from "./popovers/WebcamPopover";
import { RecordingControls } from "./RecordingControls";
-import { MarqueeText } from "./SourceSelector";
const SHOW_DEV_UPDATE_PREVIEW = import.meta.env.DEV;
diff --git a/src/components/launch/MarqueeText.tsx b/src/components/launch/MarqueeText.tsx
new file mode 100644
index 000000000..8788e7c1e
--- /dev/null
+++ b/src/components/launch/MarqueeText.tsx
@@ -0,0 +1,37 @@
+import { useLayoutEffect, useRef, useState } from "react";
+
+export function MarqueeText({ text }: { text: string }) {
+ const staticRef = useRef
(null);
+ const [overflowing, setOverflowing] = useState(false);
+
+ useLayoutEffect(() => {
+ const node = staticRef.current;
+ if (!node || node.textContent !== text) return;
+ const checkOverflow = () => {
+ setOverflowing(node.scrollWidth > node.clientWidth + 1);
+ };
+ checkOverflow();
+ const observer = new ResizeObserver(checkOverflow);
+ observer.observe(node);
+ return () => observer.disconnect();
+ }, [text]);
+
+ return (
+
+
+ {text}
+
+
+
+ {text}
+
+ {text}
+
+
+
+
+ );
+}
diff --git a/src/components/launch/SourceSelector.tsx b/src/components/launch/SourceSelector.tsx
index 8ce540006..d05f855e7 100644
--- a/src/components/launch/SourceSelector.tsx
+++ b/src/components/launch/SourceSelector.tsx
@@ -1,6 +1,6 @@
import { AppWindowIcon, CaretUpIcon, MonitorIcon } from "@phosphor-icons/react";
import * as React from "react";
-import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { useScopedT } from "@/contexts/I18nContext";
@@ -14,6 +14,7 @@ import {
import "./launchTheme.css";
import "./SourceSelector.css";
import { useHudInteraction } from "./contexts/HudInteractionContext";
+import { MarqueeText } from "./MarqueeText";
interface SourceSelectorProps {
/** List of available screen sources */
@@ -36,42 +37,6 @@ interface SourceSelectorProps {
children?: React.ReactNode;
}
-export function MarqueeText({ text }: { text: string }) {
- const staticRef = useRef(null);
- const [overflowing, setOverflowing] = useState(false);
-
- useLayoutEffect(() => {
- const node = staticRef.current;
- if (!node || node.textContent !== text) return;
- const checkOverflow = () => {
- setOverflowing(node.scrollWidth > node.clientWidth + 1);
- };
- checkOverflow();
- const observer = new ResizeObserver(checkOverflow);
- observer.observe(node);
- return () => observer.disconnect();
- }, [text]);
-
- return (
-
-
- {text}
-
-
-
- {text}
-
- {text}
-
-
-
-
- );
-}
-
/**
* SourceSelectorContent - The actual list of sources
*/
diff --git a/src/components/launch/popovers/ProjectPopover.tsx b/src/components/launch/popovers/ProjectPopover.tsx
index 22df11d05..4219fdfa7 100644
--- a/src/components/launch/popovers/ProjectPopover.tsx
+++ b/src/components/launch/popovers/ProjectPopover.tsx
@@ -1,8 +1,9 @@
-import type { ReactElement } from "react";
+import { lazy, type ReactElement, Suspense } from "react";
+import type { ProjectLibraryEntry } from "../../video-editor/ProjectBrowserDialog";
import { useLaunchPopoverCoordinator } from "./LaunchPopoverCoordinator";
import { HudPopover } from "./PopoverScaffold";
-import ProjectBrowserDialog from "../../video-editor/ProjectBrowserDialog";
-import type { ProjectLibraryEntry } from "../../video-editor/ProjectBrowserDialog";
+
+const ProjectBrowserDialog = lazy(() => import("../../video-editor/ProjectBrowserDialog"));
const POPOVER_ID = "projects";
@@ -31,18 +32,20 @@ export function ProjectPopover({
trigger={trigger}
align="center"
>
- {
- if (!nextOpen) requestClose(POPOVER_ID);
- }}
- entries={entries}
- renderMode="inline"
- onOpenProject={(path) => {
- onOpenProject(path);
- requestClose(POPOVER_ID);
- }}
- />
+
+ {
+ if (!nextOpen) requestClose(POPOVER_ID);
+ }}
+ entries={entries}
+ renderMode="inline"
+ onOpenProject={(path) => {
+ onOpenProject(path);
+ requestClose(POPOVER_ID);
+ }}
+ />
+
);
}
diff --git a/src/components/video-editor/EditorWindow.tsx b/src/components/video-editor/EditorWindow.tsx
new file mode 100644
index 000000000..f45d9cb6e
--- /dev/null
+++ b/src/components/video-editor/EditorWindow.tsx
@@ -0,0 +1,26 @@
+import { useEffect } from "react";
+import { ShortcutsProvider } from "../../contexts/ShortcutsContext";
+import { loadAllCustomFonts } from "../../lib/customFonts";
+import { AnnouncementDialog } from "../announcements/AnnouncementDialog";
+import { LiveAnnouncementNotifications } from "../announcements/LiveAnnouncementNotifications";
+import { ShortcutsConfigDialog } from "./ShortcutsConfigDialog";
+import VideoEditor from "./VideoEditor";
+
+export default function EditorWindow() {
+ useEffect(() => {
+ loadAllCustomFonts().catch((error) => {
+ console.error("Failed to load custom fonts:", error);
+ });
+ }, []);
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx
index 8be410188..7cb58576e 100644
--- a/src/components/video-editor/SettingsPanel.tsx
+++ b/src/components/video-editor/SettingsPanel.tsx
@@ -1110,6 +1110,8 @@ export function SettingsPanel({
);
const [experimentalUpdatesEnabled, setExperimentalUpdatesEnabled] = useState(false);
const [savingExperimentalUpdates, setSavingExperimentalUpdates] = useState(false);
+ const [internalActiveEffectSection] = useState("scene");
+ const activeEffectSection = activeEffectSectionProp ?? internalActiveEffectSection;
const removeBackgroundStateRef = useRef<{
aspectRatio: AspectRatio;
padding: Padding;
@@ -1166,6 +1168,8 @@ export function SettingsPanel({
};
useEffect(() => {
+ if (!isBackgroundPanel && activeEffectSection !== "scene") return;
+
let mounted = true;
(async () => {
try {
@@ -1196,7 +1200,7 @@ export function SettingsPanel({
return () => {
mounted = false;
};
- }, []);
+ }, [activeEffectSection, isBackgroundPanel]);
const colorPalette = [
"#FF0000",
@@ -1231,8 +1235,6 @@ export function SettingsPanel({
const customColorInputRef = useRef(null);
const cursorClickEffectColorInputRef = useRef(null);
const defaultWebcam = initialEditorPreferences.webcam;
- const [internalActiveEffectSection] = useState("scene");
- const activeEffectSection = activeEffectSectionProp ?? internalActiveEffectSection;
const [builtInCursorPreviewUrls, setBuiltInCursorPreviewUrls] = useState<
Partial>
>({});
diff --git a/src/components/video-editor/export/useExportRunner.ts b/src/components/video-editor/export/useExportRunner.ts
index 95aef628f..0472f9215 100644
--- a/src/components/video-editor/export/useExportRunner.ts
+++ b/src/components/video-editor/export/useExportRunner.ts
@@ -1,13 +1,8 @@
import { useCallback, useRef } from "react";
import { toast } from "sonner";
-import {
- DEFAULT_MP4_CODEC,
- type ExportSettings,
- GifExporter,
- ModernVideoExporter,
- VideoExporter,
-} from "@/lib/exporter";
import { getMp4ExportBitrate } from "@/lib/exporter/exportBitrate";
+import { DEFAULT_MP4_CODEC } from "@/lib/exporter/mp4Support";
+import type { ExportSettings } from "@/lib/exporter/types";
import { calculateMp4ExportDimensions } from "../exportDimensions";
import { resolveMp4ExportRouting } from "../mp4ExportRouting";
import { resolveMp4ExportSettings } from "../mp4ExportSettings";
@@ -116,6 +111,7 @@ export function useExportRunner(input: ExportRunnerInput) {
if (settings.format === "gif" && settings.gifConfig) {
// GIF Export
+ const { GifExporter } = await import("@/lib/exporter/gifExporter");
const gifExporter = new GifExporter({
videoUrl: videoPath,
width: settings.gifConfig.width,
@@ -143,7 +139,7 @@ export function useExportRunner(input: ExportRunnerInput) {
maxPendingFrames: smokeExportConfig.maxPendingFrames,
});
- exporterRef.current = gifExporter as unknown as VideoExporter;
+ exporterRef.current = gifExporter;
const result = await gifExporter.export();
if (result.success && result.blob) {
@@ -287,11 +283,15 @@ export function useExportRunner(input: ExportRunnerInput) {
const exporter =
pipelineModel === "modern"
- ? new ModernVideoExporter({
+ ? new (
+ await import("@/lib/exporter/modernVideoExporter")
+ ).ModernVideoExporter({
...exporterConfig,
backendPreference,
})
- : new VideoExporter(exporterConfig);
+ : new (await import("@/lib/exporter/videoExporter")).VideoExporter(
+ exporterConfig,
+ );
exporterRef.current = exporter;
const result = await exporter.export();
diff --git a/src/components/video-editor/project/useProjectLibraryController.ts b/src/components/video-editor/project/useProjectLibraryController.ts
index a2cdf5a53..3ecb33e4f 100644
--- a/src/components/video-editor/project/useProjectLibraryController.ts
+++ b/src/components/video-editor/project/useProjectLibraryController.ts
@@ -1,6 +1,6 @@
/* biome-ignore-all lint/correctness/useExhaustiveDependencies: grouped editor domain objects contain the thumbnail renderer dependencies. */
import { type RefObject, useCallback, useEffect, useRef } from "react";
-import { FrameRenderer } from "@/lib/exporter";
+import { FrameRenderer } from "@/lib/exporter/frameRenderer";
import { toFileUrl } from "../projectPersistence";
import type { useAppearanceState } from "../state/useAppearanceState";
import type { useProjectState } from "../state/useProjectState";
diff --git a/src/components/video-editor/projectPersistence.ts b/src/components/video-editor/projectPersistence.ts
index 563199c05..c50546d3e 100644
--- a/src/components/video-editor/projectPersistence.ts
+++ b/src/components/video-editor/projectPersistence.ts
@@ -9,7 +9,7 @@ import type {
GifFrameRate,
GifSizePreset,
} from "@/lib/exporter";
-import { isValidMp4FrameRate } from "@/lib/exporter";
+import { isValidMp4FrameRate } from "@/lib/exporter/types";
import {
TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT,
TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION,
diff --git a/vite.config.ts b/vite.config.ts
index 3dd36633d..b9b676043 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -126,7 +126,6 @@ export default defineConfig({
rollupOptions: {
output: {
manualChunks: {
- pixi: ["pixi.js"],
"react-vendor": ["react", "react-dom"],
"video-processing": ["mediabunny", "mp4box", "@fix-webm-duration/fix"],
},
From 158b141c4f2e874a194d0a3a7746996b06ca608b Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Sat, 5 Sep 2026 16:28:36 +1000
Subject: [PATCH 2/9] fix: stop looping startup permission prompts
---
src/components/launch/LaunchWindow.tsx | 3 +--
.../launch/hooks/useLaunchWindowSystemState.ts | 8 +-------
src/hooks/useScreenRecorder.ts | 12 ++++--------
src/i18n/locales/en/launch.json | 8 ++++----
4 files changed, 10 insertions(+), 21 deletions(-)
diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx
index 66cbe608b..7321b2b2f 100644
--- a/src/components/launch/LaunchWindow.tsx
+++ b/src/components/launch/LaunchWindow.tsx
@@ -78,7 +78,6 @@ function LaunchWindowContent() {
setWebcamDeviceId,
countdownDelay,
setCountdownDelay,
- preparePermissions,
} = useScreenRecorder();
const { elapsed, formatTime } = useRecordingTimer(recording, paused);
@@ -114,7 +113,7 @@ function LaunchWindowContent() {
hideHudFromCapture,
chooseRecordingsDirectory,
toggleHudCaptureProtection,
- } = useLaunchWindowSystemState(preparePermissions);
+ } = useLaunchWindowSystemState();
const hudCaptureProtectionSupported = supportsHudCaptureProtection(platform ?? "");
diff --git a/src/components/launch/hooks/useLaunchWindowSystemState.ts b/src/components/launch/hooks/useLaunchWindowSystemState.ts
index 56793fa36..58dc42dd7 100644
--- a/src/components/launch/hooks/useLaunchWindowSystemState.ts
+++ b/src/components/launch/hooks/useLaunchWindowSystemState.ts
@@ -1,8 +1,6 @@
import { useCallback, useEffect, useState } from "react";
-export function useLaunchWindowSystemState(
- preparePermissions: (args: { startup?: boolean }) => Promise,
-) {
+export function useLaunchWindowSystemState() {
const [recordingsDirectory, setRecordingsDirectory] = useState(null);
const [hudOverlayMousePassthroughSupported, setHudOverlayMousePassthroughSupported] = useState<
boolean | null
@@ -65,10 +63,6 @@ export function useLaunchWindowSystemState(
};
}, []);
- useEffect(() => {
- void preparePermissions({ startup: true });
- }, [preparePermissions]);
-
useEffect(() => {
let cancelled = false;
const loadVersion = async () => {
diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts
index 459652415..829845396 100644
--- a/src/hooks/useScreenRecorder.ts
+++ b/src/hooks/useScreenRecorder.ts
@@ -135,7 +135,7 @@ type UseScreenRecorderReturn = {
pauseRecording: () => void;
resumeRecording: () => void;
cancelRecording: () => void;
- preparePermissions: (options?: { startup?: boolean }) => Promise;
+ preparePermissions: () => Promise;
isMacOS: boolean;
microphoneEnabled: boolean;
setMicrophoneEnabled: (enabled: boolean) => void;
@@ -546,7 +546,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
micFallbackPauseIntervals.current = [];
}, []);
- const preparePermissions = useCallback(async (options: { startup?: boolean } = {}) => {
+ const preparePermissions = useCallback(async () => {
const platform = await window.electronAPI.getPlatform();
if (platform !== "darwin") {
return true;
@@ -556,9 +556,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
if (!screenPermission.success || screenPermission.status !== "granted") {
await window.electronAPI.openScreenRecordingPreferences();
alert(
- options.startup
- ? "Recordly needs Screen Recording permission before you start. System Settings has been opened. After enabling it, quit and reopen Recordly."
- : "Screen Recording permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.",
+ "Recordly needs Screen Recording permission before you can record. Enable it in System Settings, then quit and reopen Recordly.",
);
return false;
}
@@ -579,9 +577,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
await window.electronAPI.openAccessibilityPreferences();
alert(
- options.startup
- ? "Recordly also needs Accessibility permission for cursor tracking. System Settings has been opened. After enabling it, quit and reopen Recordly."
- : "Accessibility permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.",
+ "Recordly also needs Accessibility permission for cursor tracking. Enable it in System Settings, then quit and reopen Recordly.",
);
return false;
diff --git a/src/i18n/locales/en/launch.json b/src/i18n/locales/en/launch.json
index d4f7aba94..eee1c3f07 100644
--- a/src/i18n/locales/en/launch.json
+++ b/src/i18n/locales/en/launch.json
@@ -67,10 +67,10 @@
"share": "Share"
},
"permissions": {
- "screenRecordingNeeded": "Recordly needs Screen Recording permission before you start. System Settings has been opened. After enabling it, quit and reopen Recordly.",
- "screenRecordingMissing": "Screen Recording permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.",
- "accessibilityNeeded": "Recordly also needs Accessibility permission for cursor tracking. System Settings has been opened. After enabling it, quit and reopen Recordly.",
- "accessibilityMissing": "Accessibility permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.",
+ "screenRecordingNeeded": "Recordly needs Screen Recording permission before you can record. Enable it in System Settings, then quit and reopen Recordly.",
+ "screenRecordingMissing": "Recordly still needs Screen Recording permission. Enable it in System Settings, then quit and reopen Recordly.",
+ "accessibilityNeeded": "Recordly needs Accessibility permission for cursor tracking. Enable it in System Settings, then quit and reopen Recordly.",
+ "accessibilityMissing": "Recordly still needs Accessibility permission for cursor tracking. Enable it in System Settings, then quit and reopen Recordly.",
"selectSource": "Please select a source to record",
"systemAudioUnavailable": "System audio is not available for this source. Recording will continue without system audio.",
"microphoneDenied": "Microphone access was denied. Recording will continue without microphone audio.",
From 490eb546f19cee442e56d2a7586f7eb2a0871920 Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Sat, 5 Sep 2026 16:31:26 +1000
Subject: [PATCH 3/9] Revert "fix: stop looping startup permission prompts"
This reverts commit 158b141c4f2e874a194d0a3a7746996b06ca608b.
---
src/components/launch/LaunchWindow.tsx | 3 ++-
.../launch/hooks/useLaunchWindowSystemState.ts | 8 +++++++-
src/hooks/useScreenRecorder.ts | 12 ++++++++----
src/i18n/locales/en/launch.json | 8 ++++----
4 files changed, 21 insertions(+), 10 deletions(-)
diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx
index 7321b2b2f..66cbe608b 100644
--- a/src/components/launch/LaunchWindow.tsx
+++ b/src/components/launch/LaunchWindow.tsx
@@ -78,6 +78,7 @@ function LaunchWindowContent() {
setWebcamDeviceId,
countdownDelay,
setCountdownDelay,
+ preparePermissions,
} = useScreenRecorder();
const { elapsed, formatTime } = useRecordingTimer(recording, paused);
@@ -113,7 +114,7 @@ function LaunchWindowContent() {
hideHudFromCapture,
chooseRecordingsDirectory,
toggleHudCaptureProtection,
- } = useLaunchWindowSystemState();
+ } = useLaunchWindowSystemState(preparePermissions);
const hudCaptureProtectionSupported = supportsHudCaptureProtection(platform ?? "");
diff --git a/src/components/launch/hooks/useLaunchWindowSystemState.ts b/src/components/launch/hooks/useLaunchWindowSystemState.ts
index 58dc42dd7..56793fa36 100644
--- a/src/components/launch/hooks/useLaunchWindowSystemState.ts
+++ b/src/components/launch/hooks/useLaunchWindowSystemState.ts
@@ -1,6 +1,8 @@
import { useCallback, useEffect, useState } from "react";
-export function useLaunchWindowSystemState() {
+export function useLaunchWindowSystemState(
+ preparePermissions: (args: { startup?: boolean }) => Promise,
+) {
const [recordingsDirectory, setRecordingsDirectory] = useState(null);
const [hudOverlayMousePassthroughSupported, setHudOverlayMousePassthroughSupported] = useState<
boolean | null
@@ -63,6 +65,10 @@ export function useLaunchWindowSystemState() {
};
}, []);
+ useEffect(() => {
+ void preparePermissions({ startup: true });
+ }, [preparePermissions]);
+
useEffect(() => {
let cancelled = false;
const loadVersion = async () => {
diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts
index 829845396..459652415 100644
--- a/src/hooks/useScreenRecorder.ts
+++ b/src/hooks/useScreenRecorder.ts
@@ -135,7 +135,7 @@ type UseScreenRecorderReturn = {
pauseRecording: () => void;
resumeRecording: () => void;
cancelRecording: () => void;
- preparePermissions: () => Promise;
+ preparePermissions: (options?: { startup?: boolean }) => Promise;
isMacOS: boolean;
microphoneEnabled: boolean;
setMicrophoneEnabled: (enabled: boolean) => void;
@@ -546,7 +546,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
micFallbackPauseIntervals.current = [];
}, []);
- const preparePermissions = useCallback(async () => {
+ const preparePermissions = useCallback(async (options: { startup?: boolean } = {}) => {
const platform = await window.electronAPI.getPlatform();
if (platform !== "darwin") {
return true;
@@ -556,7 +556,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
if (!screenPermission.success || screenPermission.status !== "granted") {
await window.electronAPI.openScreenRecordingPreferences();
alert(
- "Recordly needs Screen Recording permission before you can record. Enable it in System Settings, then quit and reopen Recordly.",
+ options.startup
+ ? "Recordly needs Screen Recording permission before you start. System Settings has been opened. After enabling it, quit and reopen Recordly."
+ : "Screen Recording permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.",
);
return false;
}
@@ -577,7 +579,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
await window.electronAPI.openAccessibilityPreferences();
alert(
- "Recordly also needs Accessibility permission for cursor tracking. Enable it in System Settings, then quit and reopen Recordly.",
+ options.startup
+ ? "Recordly also needs Accessibility permission for cursor tracking. System Settings has been opened. After enabling it, quit and reopen Recordly."
+ : "Accessibility permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.",
);
return false;
diff --git a/src/i18n/locales/en/launch.json b/src/i18n/locales/en/launch.json
index eee1c3f07..d4f7aba94 100644
--- a/src/i18n/locales/en/launch.json
+++ b/src/i18n/locales/en/launch.json
@@ -67,10 +67,10 @@
"share": "Share"
},
"permissions": {
- "screenRecordingNeeded": "Recordly needs Screen Recording permission before you can record. Enable it in System Settings, then quit and reopen Recordly.",
- "screenRecordingMissing": "Recordly still needs Screen Recording permission. Enable it in System Settings, then quit and reopen Recordly.",
- "accessibilityNeeded": "Recordly needs Accessibility permission for cursor tracking. Enable it in System Settings, then quit and reopen Recordly.",
- "accessibilityMissing": "Recordly still needs Accessibility permission for cursor tracking. Enable it in System Settings, then quit and reopen Recordly.",
+ "screenRecordingNeeded": "Recordly needs Screen Recording permission before you start. System Settings has been opened. After enabling it, quit and reopen Recordly.",
+ "screenRecordingMissing": "Screen Recording permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.",
+ "accessibilityNeeded": "Recordly also needs Accessibility permission for cursor tracking. System Settings has been opened. After enabling it, quit and reopen Recordly.",
+ "accessibilityMissing": "Accessibility permission is still missing. System Settings has been opened again. Enable it, then quit and reopen Recordly before recording.",
"selectSource": "Please select a source to record",
"systemAudioUnavailable": "System audio is not available for this source. Recording will continue without system audio.",
"microphoneDenied": "Microphone access was denied. Recording will continue without microphone audio.",
From 6217ea4098e127da8f6487257b5d530f75a847e4 Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Sat, 5 Sep 2026 16:34:40 +1000
Subject: [PATCH 4/9] fix: resolve packaged wallpaper assets
---
electron/ipc/register/assets.ts | 8 +++++--
src/lib/assetPath.test.ts | 41 ++++++++++++++++++++++++++++++++-
src/lib/assetPath.ts | 19 ++++++++-------
3 files changed, 55 insertions(+), 13 deletions(-)
diff --git a/electron/ipc/register/assets.ts b/electron/ipc/register/assets.ts
index fae8d708d..a9e972788 100644
--- a/electron/ipc/register/assets.ts
+++ b/electron/ipc/register/assets.ts
@@ -2,10 +2,10 @@ import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
-import { ipcMain } from "electron";
+import { app, ipcMain } from "electron";
import { USER_DATA_PATH } from "../../appPaths";
-import { normalizePath } from "../utils";
import { getAssetRootPath } from "../project/manager";
+import { normalizePath } from "../utils";
export function registerAssetHandlers() {
async function resolveReadableLocalFilePath(filePath: string) {
@@ -73,6 +73,10 @@ export function registerAssetHandlers() {
// Return base path for assets so renderer can resolve file:// paths in production
ipcMain.handle("get-asset-base-path", () => {
+ if (!app.isPackaged) {
+ return null;
+ }
+
try {
const assetPath = getAssetRootPath();
return pathToFileURL(`${assetPath}${path.sep}`).toString();
diff --git a/src/lib/assetPath.test.ts b/src/lib/assetPath.test.ts
index 0cf6cd065..8956f2919 100644
--- a/src/lib/assetPath.test.ts
+++ b/src/lib/assetPath.test.ts
@@ -1,5 +1,44 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
-import { getExportableVideoUrl, getRenderableAssetUrl, getRenderableVideoUrl } from "./assetPath";
+import {
+ getAssetPath,
+ getExportableVideoUrl,
+ getRenderableAssetUrl,
+ getRenderableVideoUrl,
+} from "./assetPath";
+
+describe("getAssetPath", () => {
+ beforeEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ it("uses the packaged asset directory from an HTTP renderer", async () => {
+ vi.stubGlobal("window", {
+ location: { protocol: "http:" },
+ electronAPI: {
+ getAssetBasePath: vi.fn(
+ async () => "file:///Applications/Recordly.app/Contents/Resources/assets/",
+ ),
+ },
+ });
+
+ await expect(getAssetPath("wallpapers/tahoe-light.jpg")).resolves.toBe(
+ "file:///Applications/Recordly.app/Contents/Resources/assets/wallpapers/tahoe-light.jpg",
+ );
+ });
+
+ it("uses root-relative assets when the dev server has no packaged asset base", async () => {
+ vi.stubGlobal("window", {
+ location: { protocol: "http:" },
+ electronAPI: {
+ getAssetBasePath: vi.fn(async () => null),
+ },
+ });
+
+ await expect(getAssetPath("wallpapers/tahoe-light.jpg")).resolves.toBe(
+ "/wallpapers/tahoe-light.jpg",
+ );
+ });
+});
describe("getRenderableAssetUrl", () => {
beforeEach(() => {
diff --git a/src/lib/assetPath.ts b/src/lib/assetPath.ts
index d6acbfbc6..f810c1537 100644
--- a/src/lib/assetPath.ts
+++ b/src/lib/assetPath.ts
@@ -59,19 +59,13 @@ export async function getAssetPath(relativePath: string): Promise {
const isWebContext =
typeof window !== "undefined" && Boolean(window.location?.protocol?.startsWith("http"));
- if (isWebContext) {
- return `/${encodedRelativePath}`;
- }
-
try {
if (typeof window !== "undefined") {
if (typeof window.electronAPI?.getAssetBasePath === "function") {
const base = await window.electronAPI.getAssetBasePath();
- if (!base) {
- throw new Error(`Failed to resolve asset base path for ${relativePath}`);
+ if (base) {
+ return new URL(encodedRelativePath, ensureTrailingSlash(base)).toString();
}
-
- return new URL(encodedRelativePath, ensureTrailingSlash(base)).toString();
}
}
} catch (error) {
@@ -80,8 +74,13 @@ export async function getAssetPath(relativePath: string): Promise {
}
}
- // Fallback for web/dev server: public/wallpapers are served at '/wallpapers/...'
- return `/${encodedRelativePath}`;
+ if (isWebContext) {
+ // Dev and browser contexts serve public assets from the site root. Packaged
+ // Electron windows resolve above to the single extraResources asset copy.
+ return `/${encodedRelativePath}`;
+ }
+
+ throw new Error(`Failed to resolve asset base path for ${relativePath}`);
}
const BASE64_CHUNK_SIZE = 0x8000;
From 26008edd51b8b22595f891b88c64228f38cd46b9 Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Sat, 5 Sep 2026 16:47:12 +1000
Subject: [PATCH 5/9] fix: address startup review regressions
---
src/components/ui/sonner.tsx | 5 ++-
src/components/video-editor/SettingsPanel.tsx | 9 ++++-
src/components/video-editor/VideoEditor.tsx | 3 +-
.../export/useExportDialogActions.ts | 6 ++-
.../video-editor/export/useExportRunner.ts | 37 ++++++++++++-------
.../video-editor/export/useExportSession.ts | 2 +
6 files changed, 43 insertions(+), 19 deletions(-)
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
index fe1e39054..42ee8ac4d 100644
--- a/src/components/ui/sonner.tsx
+++ b/src/components/ui/sonner.tsx
@@ -1,11 +1,12 @@
import { Toaster as Sonner } from "sonner";
+import { cn } from "@/lib/utils";
type ToasterProps = React.ComponentProps;
-const Toaster = ({ ...props }: ToasterProps) => {
+const Toaster = ({ className, ...props }: ToasterProps) => {
return (
{
- if (!isBackgroundPanel && activeEffectSection !== "scene") return;
+ if (
+ !isBackgroundPanel &&
+ activeEffectSection !== "scene" &&
+ activeEffectSection !== "frame" &&
+ activeEffectSection !== "crop"
+ ) {
+ return;
+ }
let mounted = true;
(async () => {
diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx
index 84c973b4c..883e8fc9b 100644
--- a/src/components/video-editor/VideoEditor.tsx
+++ b/src/components/video-editor/VideoEditor.tsx
@@ -103,7 +103,7 @@ export default function VideoEditor() {
setExportPipelineModel,
} = exportSettings;
const exportSession = useExportSession();
- const { exporterRef, pendingExportSaveRef } = exportSession;
+ const { exporterRef, exportRunIdRef, pendingExportSaveRef } = exportSession;
const enableModernExportPipeline = useCallback(() => {
setExportPipelineModel("modern");
}, []);
@@ -165,6 +165,7 @@ export default function VideoEditor() {
useEffect(() => {
return () => {
+ exportRunIdRef.current += 1;
exporterRef.current?.cancel();
exporterRef.current = null;
const pending = pendingExportSaveRef.current;
diff --git a/src/components/video-editor/export/useExportDialogActions.ts b/src/components/video-editor/export/useExportDialogActions.ts
index 87de829a3..02cc8c25d 100644
--- a/src/components/video-editor/export/useExportDialogActions.ts
+++ b/src/components/video-editor/export/useExportDialogActions.ts
@@ -83,8 +83,10 @@ export function useExportDialogActions({
}, [videoPath, videoPlaybackRef, hasCaptionsForSidecar, settings, session, handleExport]);
const handleCancelExport = useCallback(() => {
- if (!session.exporterRef.current) return;
- session.exporterRef.current.cancel();
+ if (!session.isExporting) return;
+ session.exportRunIdRef.current += 1;
+ session.exporterRef.current?.cancel();
+ session.exporterRef.current = null;
toast.info("Export canceled");
session.clearPendingExportSave();
session.setShowExportDropdown(false);
diff --git a/src/components/video-editor/export/useExportRunner.ts b/src/components/video-editor/export/useExportRunner.ts
index 0472f9215..f36e70b5a 100644
--- a/src/components/video-editor/export/useExportRunner.ts
+++ b/src/components/video-editor/export/useExportRunner.ts
@@ -66,6 +66,7 @@ export function useExportRunner(input: ExportRunnerInput) {
pendingExportSaveRef,
clearPendingExportSave,
markExportAsSaving,
+ exportRunIdRef,
} = exportSession;
if (!videoPath) {
toast.error("No video loaded");
@@ -78,6 +79,10 @@ export function useExportRunner(input: ExportRunnerInput) {
return;
}
+ const exportRunId = exportRunIdRef.current + 1;
+ exportRunIdRef.current = exportRunId;
+ const exportWasCancelled = () => exportRunIdRef.current !== exportRunId;
+
setIsExporting(true);
setExportProgress(null);
setExportError(null);
@@ -112,6 +117,7 @@ export function useExportRunner(input: ExportRunnerInput) {
if (settings.format === "gif" && settings.gifConfig) {
// GIF Export
const { GifExporter } = await import("@/lib/exporter/gifExporter");
+ if (exportWasCancelled()) return;
const gifExporter = new GifExporter({
videoUrl: videoPath,
width: settings.gifConfig.width,
@@ -141,6 +147,7 @@ export function useExportRunner(input: ExportRunnerInput) {
exporterRef.current = gifExporter;
const result = await gifExporter.export();
+ if (exportWasCancelled()) return;
if (result.success && result.blob) {
const timestamp = Date.now();
@@ -224,6 +231,7 @@ export function useExportRunner(input: ExportRunnerInput) {
});
const supportedSourceDimensions =
await ensureSupportedMp4SourceDimensions(selectedMp4FrameRate);
+ if (exportWasCancelled()) return;
const { width: exportWidth, height: exportHeight } =
calculateMp4ExportDimensions(
supportedSourceDimensions.width,
@@ -281,20 +289,20 @@ export function useExportRunner(input: ExportRunnerInput) {
sourceAudioTrackSettings: sourceAudioTrackSettingsForExport,
};
+ const Exporter =
+ pipelineModel === "modern"
+ ? (await import("@/lib/exporter/modernVideoExporter"))
+ .ModernVideoExporter
+ : (await import("@/lib/exporter/videoExporter")).VideoExporter;
+ if (exportWasCancelled()) return;
const exporter =
pipelineModel === "modern"
- ? new (
- await import("@/lib/exporter/modernVideoExporter")
- ).ModernVideoExporter({
- ...exporterConfig,
- backendPreference,
- })
- : new (await import("@/lib/exporter/videoExporter")).VideoExporter(
- exporterConfig,
- );
+ ? new Exporter({ ...exporterConfig, backendPreference })
+ : new Exporter(exporterConfig);
exporterRef.current = exporter;
const result = await exporter.export();
+ if (exportWasCancelled()) return;
const smokeExportElapsedMs =
smokeExportStartedAt !== null
? Math.round(performance.now() - smokeExportStartedAt)
@@ -466,6 +474,7 @@ export function useExportRunner(input: ExportRunnerInput) {
video.currentTime = restoreTime;
}
} catch (error) {
+ if (exportWasCancelled()) return;
console.error("Export error:", error);
const errorMessage = error instanceof Error ? error.message : "Unknown error";
if (smokeExportConfig.enabled) {
@@ -487,10 +496,12 @@ export function useExportRunner(input: ExportRunnerInput) {
window.close();
}
} finally {
- setIsExporting(false);
- exporterRef.current = null;
- setShowExportDropdown(keepExportDialogOpen);
- remountPreview();
+ if (!exportWasCancelled()) {
+ setIsExporting(false);
+ exporterRef.current = null;
+ setShowExportDropdown(keepExportDialogOpen);
+ remountPreview();
+ }
}
},
[showExportSuccessToast],
diff --git a/src/components/video-editor/export/useExportSession.ts b/src/components/video-editor/export/useExportSession.ts
index b9c92d4e8..a922c0490 100644
--- a/src/components/video-editor/export/useExportSession.ts
+++ b/src/components/video-editor/export/useExportSession.ts
@@ -13,6 +13,7 @@ export function useExportSession() {
const [exportedFilePath, setExportedFilePath] = useState();
const [hasPendingExportSave, setHasPendingExportSave] = useState(false);
const exporterRef = useRef(null);
+ const exportRunIdRef = useRef(0);
const pendingExportSaveRef = useRef(null);
const clearPendingExportSave = useCallback(() => {
@@ -42,6 +43,7 @@ export function useExportSession() {
hasPendingExportSave,
setHasPendingExportSave,
exporterRef,
+ exportRunIdRef,
pendingExportSaveRef,
clearPendingExportSave,
markExportAsSaving,
From 1e496608e9a94f69682a68c698456bfc3ace6446 Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Sat, 5 Sep 2026 16:47:46 +1000
Subject: [PATCH 6/9] chore: prepare 1.3.5 beta 3 test release
---
package-lock.json | 4 ++--
package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 40b49f261..dfbc3d2da 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "recordly",
- "version": "1.3.5-beta.2",
+ "version": "1.3.5-beta.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "recordly",
- "version": "1.3.5-beta.2",
+ "version": "1.3.5-beta.3",
"hasInstallScript": true,
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
diff --git a/package.json b/package.json
index f1ef508f5..5ba90fae2 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
"url": "https://github.com/webadderallorg/Recordly/issues"
},
"private": true,
- "version": "1.3.5-beta.2",
+ "version": "1.3.5-beta.3",
"type": "module",
"scripts": {
"dev": "vite --config vite.config.ts",
From adc7df9f0b79412b76500b4fbdefa8b4ed206119 Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Sat, 5 Sep 2026 16:58:55 +1000
Subject: [PATCH 7/9] fix: harden export cancellation cleanup
---
src/components/video-editor/SettingsPanel.tsx | 4 +-
.../export/useExportDialogActions.ts | 1 +
.../video-editor/export/useExportRunner.ts | 39 +++++++++++++++++--
.../video-editor/export/useExportSession.ts | 2 +
4 files changed, 42 insertions(+), 4 deletions(-)
diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx
index 514ef0dc9..586e09d2a 100644
--- a/src/components/video-editor/SettingsPanel.tsx
+++ b/src/components/video-editor/SettingsPanel.tsx
@@ -1172,7 +1172,9 @@ export function SettingsPanel({
!isBackgroundPanel &&
activeEffectSection !== "scene" &&
activeEffectSection !== "frame" &&
- activeEffectSection !== "crop"
+ activeEffectSection !== "crop" &&
+ activeEffectSection !== "extensions" &&
+ !activeEffectSection.startsWith("ext:")
) {
return;
}
diff --git a/src/components/video-editor/export/useExportDialogActions.ts b/src/components/video-editor/export/useExportDialogActions.ts
index 02cc8c25d..0c7bc87f4 100644
--- a/src/components/video-editor/export/useExportDialogActions.ts
+++ b/src/components/video-editor/export/useExportDialogActions.ts
@@ -84,6 +84,7 @@ export function useExportDialogActions({
const handleCancelExport = useCallback(() => {
if (!session.isExporting) return;
+ session.cancelledExportRunIdRef.current = session.exportRunIdRef.current;
session.exportRunIdRef.current += 1;
session.exporterRef.current?.cancel();
session.exporterRef.current = null;
diff --git a/src/components/video-editor/export/useExportRunner.ts b/src/components/video-editor/export/useExportRunner.ts
index f36e70b5a..37edbdba4 100644
--- a/src/components/video-editor/export/useExportRunner.ts
+++ b/src/components/video-editor/export/useExportRunner.ts
@@ -67,6 +67,7 @@ export function useExportRunner(input: ExportRunnerInput) {
clearPendingExportSave,
markExportAsSaving,
exportRunIdRef,
+ cancelledExportRunIdRef,
} = exportSession;
if (!videoPath) {
toast.error("No video loaded");
@@ -81,7 +82,16 @@ export function useExportRunner(input: ExportRunnerInput) {
const exportRunId = exportRunIdRef.current + 1;
exportRunIdRef.current = exportRunId;
+ cancelledExportRunIdRef.current = null;
const exportWasCancelled = () => exportRunIdRef.current !== exportRunId;
+ const exportWasExplicitlyCancelled = () =>
+ cancelledExportRunIdRef.current === exportRunId;
+ const discardCancelledTemp = async (pending: PendingExportSave) => {
+ if (!pending.tempFilePath) return;
+ await window.electronAPI
+ .discardExportedTemp?.(pending.tempFilePath)
+ .catch(() => undefined);
+ };
setIsExporting(true);
setExportProgress(null);
@@ -90,10 +100,10 @@ export function useExportRunner(input: ExportRunnerInput) {
const smokeExportStartedAt = smokeExportConfig.enabled ? performance.now() : null;
let keepExportDialogOpen = false;
+ const wasPlaying = isPlaying;
+ const restoreTime = video.currentTime;
try {
- const wasPlaying = isPlaying;
- const restoreTime = video.currentTime;
if (wasPlaying) {
videoPlaybackRef.current?.pause();
}
@@ -136,6 +146,7 @@ export function useExportRunner(input: ExportRunnerInput) {
previewHeight,
shadowIntensity: effectiveShadowIntensity,
onProgress: (progress) => {
+ if (exportWasCancelled()) return;
recordSmokeProgress(progress);
setExportProgress(progress);
},
@@ -159,6 +170,10 @@ export function useExportRunner(input: ExportRunnerInput) {
fileName,
smokeExportConfig.enabled ? smokeExportConfig.outputPath : null,
);
+ if (exportWasCancelled()) {
+ await discardCancelledTemp(pendingSave);
+ return;
+ }
if (saveResult.canceled) {
pendingExportSaveRef.current = pendingSave;
@@ -277,6 +292,7 @@ export function useExportRunner(input: ExportRunnerInput) {
previewHeight,
shadowIntensity: effectiveShadowIntensity,
onProgress: (progress) => {
+ if (exportWasCancelled()) return;
recordSmokeProgress(progress);
setExportProgress(progress);
},
@@ -338,6 +354,14 @@ export function useExportRunner(input: ExportRunnerInput) {
: null,
captionSidecar: sidecarForThisExport,
});
+ if (exportWasCancelled()) {
+ await discardCancelledTemp({
+ fileName,
+ tempFilePath: result.tempFilePath,
+ captionSidecar: sidecarForThisExport,
+ });
+ return;
+ }
pendingOnCancel = {
fileName,
tempFilePath: result.tempFilePath,
@@ -353,6 +377,10 @@ export function useExportRunner(input: ExportRunnerInput) {
smokeExportConfig.enabled ? smokeExportConfig.outputPath : null,
sidecarForThisExport,
);
+ if (exportWasCancelled()) {
+ await discardCancelledTemp(blobSave.pendingSave);
+ return;
+ }
saveResult = blobSave.saveResult;
pendingOnCancel = blobSave.pendingSave;
} else {
@@ -496,7 +524,12 @@ export function useExportRunner(input: ExportRunnerInput) {
window.close();
}
} finally {
- if (!exportWasCancelled()) {
+ if (exportWasExplicitlyCancelled() && exportRunIdRef.current === exportRunId + 1) {
+ video.currentTime = restoreTime;
+ if (wasPlaying) {
+ await videoPlaybackRef.current?.play().catch(() => undefined);
+ }
+ } else if (!exportWasCancelled()) {
setIsExporting(false);
exporterRef.current = null;
setShowExportDropdown(keepExportDialogOpen);
diff --git a/src/components/video-editor/export/useExportSession.ts b/src/components/video-editor/export/useExportSession.ts
index a922c0490..a36ac6291 100644
--- a/src/components/video-editor/export/useExportSession.ts
+++ b/src/components/video-editor/export/useExportSession.ts
@@ -14,6 +14,7 @@ export function useExportSession() {
const [hasPendingExportSave, setHasPendingExportSave] = useState(false);
const exporterRef = useRef(null);
const exportRunIdRef = useRef(0);
+ const cancelledExportRunIdRef = useRef(null);
const pendingExportSaveRef = useRef(null);
const clearPendingExportSave = useCallback(() => {
@@ -44,6 +45,7 @@ export function useExportSession() {
setHasPendingExportSave,
exporterRef,
exportRunIdRef,
+ cancelledExportRunIdRef,
pendingExportSaveRef,
clearPendingExportSave,
markExportAsSaving,
From a35aa2326263033cc78b5ee94e8144d3c35b7dad Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Sat, 5 Sep 2026 17:30:26 +1000
Subject: [PATCH 8/9] ci: support public beta feed without a release
---
.github/workflows/release.yml | 89 ++++++++++++++++++++++++++++++++---
1 file changed, 83 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index dea29ea6d..8e37edddc 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -6,9 +6,21 @@ on:
workflow_dispatch:
inputs:
tag_name:
- description: Existing release tag to rebuild and publish
+ description: Version tag name (must match package.json)
required: true
type: string
+ source_sha:
+ description: Exact source commit (required for a Pages beta feed)
+ required: false
+ type: string
+ publish_target:
+ description: Where to publish update assets
+ required: true
+ default: release
+ type: choice
+ options:
+ - release
+ - pages
release_scope:
description: Platforms to publish
required: true
@@ -24,6 +36,7 @@ permissions:
id-token: write
attestations: write
artifact-metadata: write
+ pages: write
concurrency:
group: release-${{ github.event.release.tag_name || github.event.inputs.tag_name || github.run_id }}
@@ -39,11 +52,12 @@ jobs:
source_sha: ${{ steps.prepare.outputs.source_sha }}
prerelease: ${{ steps.prepare.outputs.prerelease }}
release_scope: ${{ steps.prepare.outputs.release_scope }}
+ publish_target: ${{ steps.prepare.outputs.publish_target }}
steps:
- name: Checkout release source
uses: actions/checkout@v4
with:
- ref: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.tag_name }}
+ ref: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.publish_target == 'pages' && github.event.inputs.source_sha || github.event.inputs.tag_name }}
fetch-depth: 0
- name: Validate release tag
@@ -55,19 +69,28 @@ jobs:
REQUESTED_SCOPE: ${{ github.event.inputs.release_scope }}
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
DISPATCH_TAG_NAME: ${{ github.event.inputs.tag_name }}
+ REQUESTED_SOURCE_SHA: ${{ github.event.inputs.source_sha }}
+ REQUESTED_PUBLISH_TARGET: ${{ github.event.inputs.publish_target }}
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "release" ]; then
TAG_NAME="$RELEASE_TAG_NAME"
IS_PRERELEASE="${{ github.event.release.prerelease }}"
RELEASE_SCOPE="all"
+ PUBLISH_TARGET="release"
if [ "$IS_PRERELEASE" = "true" ] && grep -Fq '' <<< "$RELEASE_BODY"; then
RELEASE_SCOPE="windows-linux"
fi
else
TAG_NAME="$DISPATCH_TAG_NAME"
- IS_PRERELEASE="$(gh release view "$TAG_NAME" --repo "${{ github.repository }}" --json isPrerelease --jq '.isPrerelease' 2>/dev/null || echo false)"
- RELEASE_SCOPE="${REQUESTED_SCOPE:-all}"
+ PUBLISH_TARGET="${REQUESTED_PUBLISH_TARGET:-release}"
+ if [ "$PUBLISH_TARGET" = "pages" ]; then
+ IS_PRERELEASE="true"
+ RELEASE_SCOPE="all"
+ else
+ IS_PRERELEASE="$(gh release view "$TAG_NAME" --repo "${{ github.repository }}" --json isPrerelease --jq '.isPrerelease' 2>/dev/null || echo false)"
+ RELEASE_SCOPE="${REQUESTED_SCOPE:-all}"
+ fi
fi
if [ -z "$TAG_NAME" ]; then
@@ -91,12 +114,32 @@ jobs:
;;
esac
+ case "$PUBLISH_TARGET" in
+ release|pages) ;;
+ *)
+ echo "Unsupported publish target: $PUBLISH_TARGET"
+ exit 1
+ ;;
+ esac
+
+ if [ "$PUBLISH_TARGET" = "pages" ]; then
+ requested_source_sha="${REQUESTED_SOURCE_SHA,,}"
+ if [[ ! "$requested_source_sha" =~ ^[0-9a-f]{40}$ ]]; then
+ echo "A Pages beta feed requires an exact 40-character source_sha."
+ exit 1
+ fi
+ if [ "$(git rev-parse HEAD)" != "$requested_source_sha" ]; then
+ echo "Checked-out source does not match source_sha."
+ exit 1
+ fi
+ fi
+
if [ "$IS_PRERELEASE" != "true" ] && [ "$RELEASE_SCOPE" != "all" ]; then
echo "Partial platform scope is allowed only for prereleases."
exit 1
fi
- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "$PUBLISH_TARGET" = "release" ]; then
gh release view "$TAG_NAME" --repo "${{ github.repository }}" >/dev/null 2>&1 || \
gh release create "$TAG_NAME" --repo "${{ github.repository }}" --verify-tag --draft --title "$TAG_NAME"
fi
@@ -106,6 +149,7 @@ jobs:
echo "source_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT"
echo "release_scope=$RELEASE_SCOPE" >> "$GITHUB_OUTPUT"
+ echo "publish_target=$PUBLISH_TARGET" >> "$GITHUB_OUTPUT"
build-macos-x64:
name: Build macOS x64
needs: prepare-release
@@ -707,6 +751,7 @@ jobs:
subject-checksums: release-assets/upload/SHA256SUMS.txt
- name: Upload release assets to GitHub
+ if: needs.prepare-release.outputs.publish_target == 'release'
shell: bash
run: |
set -euo pipefail
@@ -728,13 +773,45 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Add beta feed landing page
+ if: needs.prepare-release.outputs.publish_target == 'pages'
+ shell: bash
+ run: |
+ touch release-assets/upload/.nojekyll
+ printf '%s\n' \
+ '' \
+ 'Recordly Experimental Updates' \
+ 'Recordly Experimental Updates
Public updater assets for opt-in experimental builds. These builds may be unstable.
' \
+ > release-assets/upload/index.html
+
+ - name: Upload public beta feed artifact
+ if: needs.prepare-release.outputs.publish_target == 'pages'
+ uses: actions/upload-pages-artifact@v4
+ with:
+ path: release-assets/upload
+
+ deploy-experimental-update-feed:
+ name: Deploy experimental update feed
+ needs:
+ - prepare-release
+ - publish-release-assets
+ if: needs.prepare-release.outputs.publish_target == 'pages'
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Deploy public beta feed without a GitHub Release
+ id: deployment
+ uses: actions/deploy-pages@v4
+
trigger-homebrew-update:
name: Trigger Homebrew tap update
needs:
- prepare-release
- publish-release-assets
runs-on: ubuntu-latest
- if: needs.prepare-release.outputs.prerelease != 'true' && needs.prepare-release.outputs.release_scope == 'all'
+ if: needs.prepare-release.outputs.publish_target == 'release' && needs.prepare-release.outputs.prerelease != 'true' && needs.prepare-release.outputs.release_scope == 'all'
steps:
- name: Dispatch Homebrew tap workflow
shell: bash
From f945ca50fe7ce34b73e7e823c6829f04466452a9 Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Sat, 5 Sep 2026 17:37:41 +1000
Subject: [PATCH 9/9] Revert "ci: support public beta feed without a release"
This reverts commit a35aa2326263033cc78b5ee94e8144d3c35b7dad.
---
.github/workflows/release.yml | 89 +++--------------------------------
1 file changed, 6 insertions(+), 83 deletions(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8e37edddc..dea29ea6d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -6,21 +6,9 @@ on:
workflow_dispatch:
inputs:
tag_name:
- description: Version tag name (must match package.json)
+ description: Existing release tag to rebuild and publish
required: true
type: string
- source_sha:
- description: Exact source commit (required for a Pages beta feed)
- required: false
- type: string
- publish_target:
- description: Where to publish update assets
- required: true
- default: release
- type: choice
- options:
- - release
- - pages
release_scope:
description: Platforms to publish
required: true
@@ -36,7 +24,6 @@ permissions:
id-token: write
attestations: write
artifact-metadata: write
- pages: write
concurrency:
group: release-${{ github.event.release.tag_name || github.event.inputs.tag_name || github.run_id }}
@@ -52,12 +39,11 @@ jobs:
source_sha: ${{ steps.prepare.outputs.source_sha }}
prerelease: ${{ steps.prepare.outputs.prerelease }}
release_scope: ${{ steps.prepare.outputs.release_scope }}
- publish_target: ${{ steps.prepare.outputs.publish_target }}
steps:
- name: Checkout release source
uses: actions/checkout@v4
with:
- ref: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.publish_target == 'pages' && github.event.inputs.source_sha || github.event.inputs.tag_name }}
+ ref: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.tag_name }}
fetch-depth: 0
- name: Validate release tag
@@ -69,28 +55,19 @@ jobs:
REQUESTED_SCOPE: ${{ github.event.inputs.release_scope }}
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
DISPATCH_TAG_NAME: ${{ github.event.inputs.tag_name }}
- REQUESTED_SOURCE_SHA: ${{ github.event.inputs.source_sha }}
- REQUESTED_PUBLISH_TARGET: ${{ github.event.inputs.publish_target }}
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "release" ]; then
TAG_NAME="$RELEASE_TAG_NAME"
IS_PRERELEASE="${{ github.event.release.prerelease }}"
RELEASE_SCOPE="all"
- PUBLISH_TARGET="release"
if [ "$IS_PRERELEASE" = "true" ] && grep -Fq '' <<< "$RELEASE_BODY"; then
RELEASE_SCOPE="windows-linux"
fi
else
TAG_NAME="$DISPATCH_TAG_NAME"
- PUBLISH_TARGET="${REQUESTED_PUBLISH_TARGET:-release}"
- if [ "$PUBLISH_TARGET" = "pages" ]; then
- IS_PRERELEASE="true"
- RELEASE_SCOPE="all"
- else
- IS_PRERELEASE="$(gh release view "$TAG_NAME" --repo "${{ github.repository }}" --json isPrerelease --jq '.isPrerelease' 2>/dev/null || echo false)"
- RELEASE_SCOPE="${REQUESTED_SCOPE:-all}"
- fi
+ IS_PRERELEASE="$(gh release view "$TAG_NAME" --repo "${{ github.repository }}" --json isPrerelease --jq '.isPrerelease' 2>/dev/null || echo false)"
+ RELEASE_SCOPE="${REQUESTED_SCOPE:-all}"
fi
if [ -z "$TAG_NAME" ]; then
@@ -114,32 +91,12 @@ jobs:
;;
esac
- case "$PUBLISH_TARGET" in
- release|pages) ;;
- *)
- echo "Unsupported publish target: $PUBLISH_TARGET"
- exit 1
- ;;
- esac
-
- if [ "$PUBLISH_TARGET" = "pages" ]; then
- requested_source_sha="${REQUESTED_SOURCE_SHA,,}"
- if [[ ! "$requested_source_sha" =~ ^[0-9a-f]{40}$ ]]; then
- echo "A Pages beta feed requires an exact 40-character source_sha."
- exit 1
- fi
- if [ "$(git rev-parse HEAD)" != "$requested_source_sha" ]; then
- echo "Checked-out source does not match source_sha."
- exit 1
- fi
- fi
-
if [ "$IS_PRERELEASE" != "true" ] && [ "$RELEASE_SCOPE" != "all" ]; then
echo "Partial platform scope is allowed only for prereleases."
exit 1
fi
- if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "$PUBLISH_TARGET" = "release" ]; then
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
gh release view "$TAG_NAME" --repo "${{ github.repository }}" >/dev/null 2>&1 || \
gh release create "$TAG_NAME" --repo "${{ github.repository }}" --verify-tag --draft --title "$TAG_NAME"
fi
@@ -149,7 +106,6 @@ jobs:
echo "source_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT"
echo "release_scope=$RELEASE_SCOPE" >> "$GITHUB_OUTPUT"
- echo "publish_target=$PUBLISH_TARGET" >> "$GITHUB_OUTPUT"
build-macos-x64:
name: Build macOS x64
needs: prepare-release
@@ -751,7 +707,6 @@ jobs:
subject-checksums: release-assets/upload/SHA256SUMS.txt
- name: Upload release assets to GitHub
- if: needs.prepare-release.outputs.publish_target == 'release'
shell: bash
run: |
set -euo pipefail
@@ -773,45 +728,13 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Add beta feed landing page
- if: needs.prepare-release.outputs.publish_target == 'pages'
- shell: bash
- run: |
- touch release-assets/upload/.nojekyll
- printf '%s\n' \
- '' \
- 'Recordly Experimental Updates' \
- 'Recordly Experimental Updates
Public updater assets for opt-in experimental builds. These builds may be unstable.
' \
- > release-assets/upload/index.html
-
- - name: Upload public beta feed artifact
- if: needs.prepare-release.outputs.publish_target == 'pages'
- uses: actions/upload-pages-artifact@v4
- with:
- path: release-assets/upload
-
- deploy-experimental-update-feed:
- name: Deploy experimental update feed
- needs:
- - prepare-release
- - publish-release-assets
- if: needs.prepare-release.outputs.publish_target == 'pages'
- runs-on: ubuntu-latest
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- steps:
- - name: Deploy public beta feed without a GitHub Release
- id: deployment
- uses: actions/deploy-pages@v4
-
trigger-homebrew-update:
name: Trigger Homebrew tap update
needs:
- prepare-release
- publish-release-assets
runs-on: ubuntu-latest
- if: needs.prepare-release.outputs.publish_target == 'release' && needs.prepare-release.outputs.prerelease != 'true' && needs.prepare-release.outputs.release_scope == 'all'
+ if: needs.prepare-release.outputs.prerelease != 'true' && needs.prepare-release.outputs.release_scope == 'all'
steps:
- name: Dispatch Homebrew tap workflow
shell: bash