From ddf30ed60e6236a48eb6fa39620165763fd8f0ee Mon Sep 17 00:00:00 2001 From: Siddharth Date: Tue, 25 Nov 2025 21:18:57 -0700 Subject: [PATCH] record/ select your own video --- dist-electron/main.js | 68 ++++++++++++------- dist-electron/preload.mjs | 12 ++++ electron/electron-env.d.ts | 5 +- electron/ipc/handlers.ts | 55 +++++++++++++-- electron/main.ts | 28 +------- electron/preload.ts | 12 ++++ electron/windows.ts | 6 +- src/components/launch/LaunchWindow.module.css | 9 +++ src/components/launch/LaunchWindow.tsx | 36 +++++++++- src/components/video-editor/SettingsPanel.tsx | 13 ++-- src/components/video-editor/VideoEditor.tsx | 4 +- src/components/video-editor/VideoPlayback.tsx | 8 ++- src/vite-env.d.ts | 4 ++ 13 files changed, 186 insertions(+), 74 deletions(-) diff --git a/dist-electron/main.js b/dist-electron/main.js index 7343b29..4e8fbb5 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -8,10 +8,10 @@ const VITE_DEV_SERVER_URL$1 = process.env["VITE_DEV_SERVER_URL"]; const RENDERER_DIST$1 = path.join(APP_ROOT, "dist"); function createHudOverlayWindow() { const win = new BrowserWindow({ - width: 250, + width: 350, height: 80, - minWidth: 250, - maxWidth: 250, + minWidth: 350, + maxWidth: 350, minHeight: 80, maxHeight: 80, frame: false, @@ -145,6 +145,7 @@ function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, g try { const videoPath = path.join(RECORDINGS_DIR, fileName); await fs.writeFile(videoPath, Buffer.from(videoData)); + currentVideoPath = videoPath; return { success: true, path: videoPath, @@ -232,26 +233,48 @@ function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, g }; } }); + ipcMain.handle("open-video-file-picker", async () => { + try { + const result = await dialog.showOpenDialog({ + title: "Select Video File", + defaultPath: RECORDINGS_DIR, + filters: [ + { name: "Video Files", extensions: ["webm", "mp4", "mov", "avi", "mkv"] }, + { name: "All Files", extensions: ["*"] } + ], + properties: ["openFile"] + }); + if (result.canceled || result.filePaths.length === 0) { + return { success: false, cancelled: true }; + } + return { + success: true, + path: result.filePaths[0] + }; + } catch (error) { + console.error("Failed to open file picker:", error); + return { + success: false, + message: "Failed to open file picker", + error: String(error) + }; + } + }); + let currentVideoPath = null; + ipcMain.handle("set-current-video-path", (_, path2) => { + currentVideoPath = path2; + return { success: true }; + }); + ipcMain.handle("get-current-video-path", () => { + return currentVideoPath ? { success: true, path: currentVideoPath } : { success: false }; + }); + ipcMain.handle("clear-current-video-path", () => { + currentVideoPath = null; + return { success: true }; + }); } const __dirname = path.dirname(fileURLToPath(import.meta.url)); const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); -async function cleanupOldRecordings() { - try { - const files = await fs.readdir(RECORDINGS_DIR); - const now = Date.now(); - const maxAge = 1 * 24 * 60 * 60 * 1e3; - for (const file of files) { - const filePath = path.join(RECORDINGS_DIR, file); - const stats = await fs.stat(filePath); - if (now - stats.mtimeMs > maxAge) { - await fs.unlink(filePath); - console.log(`Deleted old recording: ${file}`); - } - } - } catch (error) { - console.error("Failed to cleanup old recordings:", error); - } -} async function ensureRecordingsDir() { try { await fs.mkdir(RECORDINGS_DIR, { recursive: true }); @@ -317,11 +340,6 @@ app.on("activate", () => { createWindow(); } }); -app.on("before-quit", async (event) => { - event.preventDefault(); - await cleanupOldRecordings(); - app.exit(0); -}); app.whenReady().then(async () => { await ensureRecordingsDir(); registerIpcHandlers( diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs index 7f7797b..7fe77fe 100644 --- a/dist-electron/preload.mjs +++ b/dist-electron/preload.mjs @@ -38,5 +38,17 @@ electron.contextBridge.exposeInMainWorld("electronAPI", { }, saveExportedVideo: (videoData, fileName) => { return electron.ipcRenderer.invoke("save-exported-video", videoData, fileName); + }, + openVideoFilePicker: () => { + return electron.ipcRenderer.invoke("open-video-file-picker"); + }, + setCurrentVideoPath: (path) => { + return electron.ipcRenderer.invoke("set-current-video-path", path); + }, + getCurrentVideoPath: () => { + return electron.ipcRenderer.invoke("get-current-video-path"); + }, + clearCurrentVideoPath: () => { + return electron.ipcRenderer.invoke("clear-current-video-path"); } }); diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 2b817e6..fed165e 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -30,12 +30,15 @@ interface Window { selectSource: (source: any) => Promise getSelectedSource: () => Promise storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string }> - getRecordedVideoPath: () => Promise<{ success: boolean; path?: string; message?: string }> setRecordingState: (recording: boolean) => Promise onStopRecordingFromTray: (callback: () => void) => () => void openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }> saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string; cancelled?: boolean }> + openVideoFilePicker: () => Promise<{ success: boolean; path?: string; cancelled?: boolean }> + setCurrentVideoPath: (path: string) => Promise<{ success: boolean }> + getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }> + clearCurrentVideoPath: () => Promise<{ success: boolean }> } } diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 2a4267c..0cf0b66 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -60,6 +60,7 @@ export function registerIpcHandlers( try { const videoPath = path.join(RECORDINGS_DIR, fileName) await fs.writeFile(videoPath, Buffer.from(videoData)) + currentVideoPath = videoPath; return { success: true, path: videoPath, @@ -128,7 +129,6 @@ export function registerIpcHandlers( ipcMain.handle('save-exported-video', async (_, videoData: ArrayBuffer, fileName: string) => { try { - // Show save dialog to let user choose location and filename const result = await dialog.showSaveDialog({ title: 'Save Exported Video', defaultPath: path.join(app.getPath('downloads'), fileName), @@ -138,7 +138,6 @@ export function registerIpcHandlers( properties: ['createDirectory', 'showOverwriteConfirmation'] }); - // User cancelled the dialog if (result.canceled || !result.filePath) { return { success: false, @@ -146,8 +145,6 @@ export function registerIpcHandlers( message: 'Export cancelled' }; } - - // Write the file to the chosen location await fs.writeFile(result.filePath, Buffer.from(videoData)); return { @@ -156,12 +153,58 @@ export function registerIpcHandlers( message: 'Video exported successfully' }; } catch (error) { - console.error('Failed to save exported video:', error); + console.error('Failed to save exported video:', error) return { success: false, message: 'Failed to save exported video', error: String(error) - }; + } } }) + + ipcMain.handle('open-video-file-picker', async () => { + try { + const result = await dialog.showOpenDialog({ + title: 'Select Video File', + defaultPath: RECORDINGS_DIR, + filters: [ + { name: 'Video Files', extensions: ['webm', 'mp4', 'mov', 'avi', 'mkv'] }, + { name: 'All Files', extensions: ['*'] } + ], + properties: ['openFile'] + }); + + if (result.canceled || result.filePaths.length === 0) { + return { success: false, cancelled: true }; + } + + return { + success: true, + path: result.filePaths[0] + }; + } catch (error) { + console.error('Failed to open file picker:', error); + return { + success: false, + message: 'Failed to open file picker', + error: String(error) + }; + } + }); + + let currentVideoPath: string | null = null; + + ipcMain.handle('set-current-video-path', (_, path: string) => { + currentVideoPath = path; + return { success: true }; + }); + + ipcMain.handle('get-current-video-path', () => { + return currentVideoPath ? { success: true, path: currentVideoPath } : { success: false }; + }); + + ipcMain.handle('clear-current-video-path', () => { + currentVideoPath = null; + return { success: true }; + }); } diff --git a/electron/main.ts b/electron/main.ts index 8a7fdb9..2722632 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -10,26 +10,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) export const RECORDINGS_DIR = path.join(app.getPath('userData'), 'recordings') -// Cleanup old recordings (older than 1 day) -async function cleanupOldRecordings() { - try { - const files = await fs.readdir(RECORDINGS_DIR) - const now = Date.now() - const maxAge = 1 * 24 * 60 * 60 * 1000 - - for (const file of files) { - const filePath = path.join(RECORDINGS_DIR, file) - const stats = await fs.stat(filePath) - - if (now - stats.mtimeMs > maxAge) { - await fs.unlink(filePath) - console.log(`Deleted old recording: ${file}`) - } - } - } catch (error) { - console.error('Failed to cleanup old recordings:', error) - } -} async function ensureRecordingsDir() { try { @@ -124,19 +104,13 @@ app.on('activate', () => { } }) -// Cleanup old recordings on quit (both macOS and other platforms) -app.on('before-quit', async (event) => { - event.preventDefault() - await cleanupOldRecordings() - app.exit(0) -}) // Register all IPC handlers when app is ready app.whenReady().then(async () => { // Ensure recordings directory exists await ensureRecordingsDir() - + registerIpcHandlers( createEditorWindowWrapper, createSourceSelectorWindowWrapper, diff --git a/electron/preload.ts b/electron/preload.ts index 192cc45..f853d10 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -42,4 +42,16 @@ contextBridge.exposeInMainWorld('electronAPI', { saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => { return ipcRenderer.invoke('save-exported-video', videoData, fileName) }, + openVideoFilePicker: () => { + return ipcRenderer.invoke('open-video-file-picker') + }, + setCurrentVideoPath: (path: string) => { + return ipcRenderer.invoke('set-current-video-path', path) + }, + getCurrentVideoPath: () => { + return ipcRenderer.invoke('get-current-video-path') + }, + clearCurrentVideoPath: () => { + return ipcRenderer.invoke('clear-current-video-path') + }, }) \ No newline at end of file diff --git a/electron/windows.ts b/electron/windows.ts index 754cdb2..045a5e1 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -10,10 +10,10 @@ const RENDERER_DIST = path.join(APP_ROOT, 'dist') export function createHudOverlayWindow(): BrowserWindow { const win = new BrowserWindow({ - width: 250, + width: 350, height: 80, - minWidth: 250, - maxWidth: 250, + minWidth: 350, + maxWidth: 350, minHeight: 80, maxHeight: 80, frame: false, diff --git a/src/components/launch/LaunchWindow.module.css b/src/components/launch/LaunchWindow.module.css index 0f1f63d..2eb5eb1 100644 --- a/src/components/launch/LaunchWindow.module.css +++ b/src/components/launch/LaunchWindow.module.css @@ -1,6 +1,15 @@ .electronDrag { -webkit-app-region: drag; } + .electronNoDrag { -webkit-app-region: no-drag; } + +.folderButton { + cursor: pointer; +} + +.folderButton:hover { + text-decoration: underline; +} \ No newline at end of file diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index ae576f3..e952c37 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -5,6 +5,8 @@ import { Button } from "../ui/button"; import { BsRecordCircle } from "react-icons/bs"; import { FaRegStopCircle } from "react-icons/fa"; import { MdMonitor } from "react-icons/md"; +import { RxDragHandleDots2 } from "react-icons/rx"; +import { FaFolderMinus } from "react-icons/fa6"; export function LaunchWindow() { const { recording, toggleRecording } = useScreenRecorder(); @@ -42,10 +44,23 @@ export function LaunchWindow() { } }; + const openVideoFile = async () => { + const result = await window.electronAPI.openVideoFilePicker(); + + if (result.cancelled) { + return; + } + + if (result.success && result.path) { + await window.electronAPI.setCurrentVideoPath(result.path); + await window.electronAPI.switchToEditor(); + } + }; + return (
+
+ +
+
@@ -73,7 +92,7 @@ export function LaunchWindow() { size="sm" onClick={hasSelectedSource ? toggleRecording : openSourceSelector} disabled={!hasSelectedSource && !recording} - className={`gap-1 bg-transparent hover:bg-transparent px-0 flex-1 text-right text-xs ${styles.electronNoDrag}`} + className={`gap-1 bg-transparent hover:bg-transparent px-0 flex-1 text-center text-xs ${styles.electronNoDrag}`} > {recording ? ( <> @@ -87,6 +106,17 @@ export function LaunchWindow() { )} + +
+ +
); diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index d5ecbb5..02759cd 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -8,7 +8,8 @@ import { Button } from "@/components/ui/button"; import { useState } from "react"; import Colorful from '@uiw/react-color-colorful'; import { hsvaToHex } from '@uiw/color-convert'; -import { Trash2, Download, Crop, X, Bug, Upload, Coffee } from "lucide-react"; +import { Trash2, Download, Crop, X, Bug, Upload } from "lucide-react"; +import { GiHearts } from "react-icons/gi"; import { toast } from "sonner"; import type { ZoomDepth, CropRegion } from "./types"; import { CropControl } from "./CropControl"; @@ -415,9 +416,9 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth, onClick={() => { window.electronAPI?.openExternalUrl('https://github.com/siddharthvaddem/openscreen/issues/new'); }} - className="flex-1 flex items-center justify-center gap-2 text-xs text-slate-500 hover:text-slate-300 transition-colors py-2 group" + className="flex-1 flex items-center justify-center gap-2 text-xs py-2" > - + Report a Bug
diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 22b3b77..460a524 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -66,13 +66,13 @@ export default function VideoEditor() { useEffect(() => { async function loadVideo() { try { - const result = await window.electronAPI.getRecordedVideoPath(); + const result = await window.electronAPI.getCurrentVideoPath(); if (result.success && result.path) { const videoUrl = toFileUrl(result.path); setVideoPath(videoUrl); } else { - setError(result.message || 'Failed to load video'); + setError('No video to load. Please record or select a video.'); } } catch (err) { setError('Error loading video: ' + String(err)); diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index f0e2546..74cec08 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -665,7 +665,13 @@ const VideoPlayback = forwardRef(({ video.pause(); allowPlaybackRef.current = false; currentTimeRef.current = 0; - setVideoReady(true); + + // hacky fix: To ensure video is fully ready for PixiJS + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setVideoReady(true); + }); + }); }; const [resolvedWallpaper, setResolvedWallpaper] = useState(null); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index ae0edea..0afbf58 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -38,5 +38,9 @@ interface Window { message?: string cancelled?: boolean }> + openVideoFilePicker: () => Promise<{ success: boolean; path?: string; cancelled?: boolean }> + setCurrentVideoPath: (path: string) => Promise<{ success: boolean }> + getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }> + clearCurrentVideoPath: () => Promise<{ success: boolean }> } } \ No newline at end of file