diff --git a/electron/main.ts b/electron/main.ts index 11d545fd..57e489f3 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -92,6 +92,10 @@ const defaultTrayIcon = getTrayIcon("openscreen.png", trayIconSize); const recordingTrayIcon = getTrayIcon("rec-button.png", trayIconSize); function createWindow() { + if (mainWindow && !mainWindow.isDestroyed()) { + return; + } + mainWindow = createHudOverlayWindow(); } @@ -108,6 +112,16 @@ function showMainWindow() { createWindow(); } +const hasSingleInstanceLock = app.requestSingleInstanceLock(); + +if (hasSingleInstanceLock) { + app.on("second-instance", () => { + showMainWindow(); + }); +} else { + app.quit(); +} + function isEditorWindow(window: BrowserWindow) { return window.webContents.getURL().includes("windowType=editor"); } @@ -456,7 +470,9 @@ app.on("will-quit", () => { unregisterAllGlobalShortcuts(); }); -app.whenReady().then(async () => { +const appReady = hasSingleInstanceLock ? app.whenReady() : null; + +appReady?.then(async () => { // Force "regular" activation policy so the Dock icon appears. The HUD overlay // (transparent, frameless, skipTaskbar) is the first window, and AppKit would // otherwise classify us as an accessory app. diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 373aeac5..6dde4543 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -522,6 +522,45 @@ export function LaunchWindow() { } }; const dragLastPositionRef = useRef<{ x: number; y: number } | null>(null); + const dragAnimationFrameRef = useRef(null); + const pendingDragDeltaRef = useRef({ x: 0, y: 0 }); + const flushHudDragMove = useCallback(() => { + dragAnimationFrameRef.current = null; + const { x, y } = pendingDragDeltaRef.current; + pendingDragDeltaRef.current = { x: 0, y: 0 }; + if (x === 0 && y === 0) return; + window.electronAPI?.moveHudOverlayBy?.(x, y); + }, []); + const scheduleHudDragMove = useCallback( + (deltaX: number, deltaY: number) => { + pendingDragDeltaRef.current = { + x: pendingDragDeltaRef.current.x + deltaX, + y: pendingDragDeltaRef.current.y + deltaY, + }; + + if (dragAnimationFrameRef.current === null) { + dragAnimationFrameRef.current = window.requestAnimationFrame(flushHudDragMove); + } + }, + [flushHudDragMove], + ); + const flushPendingHudDragMove = useCallback(() => { + if (dragAnimationFrameRef.current !== null) { + window.cancelAnimationFrame(dragAnimationFrameRef.current); + dragAnimationFrameRef.current = null; + } + const { x, y } = pendingDragDeltaRef.current; + pendingDragDeltaRef.current = { x: 0, y: 0 }; + if (x === 0 && y === 0) return; + window.electronAPI?.moveHudOverlayBy?.(x, y); + }, []); + useEffect(() => { + return () => { + if (dragAnimationFrameRef.current !== null) { + window.cancelAnimationFrame(dragAnimationFrameRef.current); + } + }; + }, []); const handleHudDragPointerDown = (event: React.PointerEvent) => { event.preventDefault(); event.stopPropagation(); @@ -535,10 +574,11 @@ export function LaunchWindow() { const deltaX = event.screenX - lastPosition.x; const deltaY = event.screenY - lastPosition.y; dragLastPositionRef.current = { x: event.screenX, y: event.screenY }; - window.electronAPI?.moveHudOverlayBy?.(deltaX, deltaY); + scheduleHudDragMove(deltaX, deltaY); }; const handleHudDragPointerEnd = (event: React.PointerEvent) => { dragLastPositionRef.current = null; + flushPendingHudDragMove(); if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index a2c5d401..5e0cc806 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -1705,22 +1705,22 @@ export function SettingsPanel({ - + {t("background.image")} {t("background.color")} {t("background.gradient")}