record/ select your own video

This commit is contained in:
Siddharth
2025-11-25 21:18:57 -07:00
parent 98d6acaa6a
commit ddf30ed60e
13 changed files with 186 additions and 74 deletions
+43 -25
View File
@@ -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"); const RENDERER_DIST$1 = path.join(APP_ROOT, "dist");
function createHudOverlayWindow() { function createHudOverlayWindow() {
const win = new BrowserWindow({ const win = new BrowserWindow({
width: 250, width: 350,
height: 80, height: 80,
minWidth: 250, minWidth: 350,
maxWidth: 250, maxWidth: 350,
minHeight: 80, minHeight: 80,
maxHeight: 80, maxHeight: 80,
frame: false, frame: false,
@@ -145,6 +145,7 @@ function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, g
try { try {
const videoPath = path.join(RECORDINGS_DIR, fileName); const videoPath = path.join(RECORDINGS_DIR, fileName);
await fs.writeFile(videoPath, Buffer.from(videoData)); await fs.writeFile(videoPath, Buffer.from(videoData));
currentVideoPath = videoPath;
return { return {
success: true, success: true,
path: videoPath, 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 __dirname = path.dirname(fileURLToPath(import.meta.url));
const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); 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() { async function ensureRecordingsDir() {
try { try {
await fs.mkdir(RECORDINGS_DIR, { recursive: true }); await fs.mkdir(RECORDINGS_DIR, { recursive: true });
@@ -317,11 +340,6 @@ app.on("activate", () => {
createWindow(); createWindow();
} }
}); });
app.on("before-quit", async (event) => {
event.preventDefault();
await cleanupOldRecordings();
app.exit(0);
});
app.whenReady().then(async () => { app.whenReady().then(async () => {
await ensureRecordingsDir(); await ensureRecordingsDir();
registerIpcHandlers( registerIpcHandlers(
+12
View File
@@ -38,5 +38,17 @@ electron.contextBridge.exposeInMainWorld("electronAPI", {
}, },
saveExportedVideo: (videoData, fileName) => { saveExportedVideo: (videoData, fileName) => {
return electron.ipcRenderer.invoke("save-exported-video", 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");
} }
}); });
+4 -1
View File
@@ -30,12 +30,15 @@ interface Window {
selectSource: (source: any) => Promise<any> selectSource: (source: any) => Promise<any>
getSelectedSource: () => Promise<any> getSelectedSource: () => Promise<any>
storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string }> storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string }>
getRecordedVideoPath: () => Promise<{ success: boolean; path?: string; message?: string }> getRecordedVideoPath: () => Promise<{ success: boolean; path?: string; message?: string }>
setRecordingState: (recording: boolean) => Promise<void> setRecordingState: (recording: boolean) => Promise<void>
onStopRecordingFromTray: (callback: () => void) => () => void onStopRecordingFromTray: (callback: () => void) => () => void
openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }> openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }>
saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string; cancelled?: boolean }> 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 }>
} }
} }
+49 -6
View File
@@ -60,6 +60,7 @@ export function registerIpcHandlers(
try { try {
const videoPath = path.join(RECORDINGS_DIR, fileName) const videoPath = path.join(RECORDINGS_DIR, fileName)
await fs.writeFile(videoPath, Buffer.from(videoData)) await fs.writeFile(videoPath, Buffer.from(videoData))
currentVideoPath = videoPath;
return { return {
success: true, success: true,
path: videoPath, path: videoPath,
@@ -128,7 +129,6 @@ export function registerIpcHandlers(
ipcMain.handle('save-exported-video', async (_, videoData: ArrayBuffer, fileName: string) => { ipcMain.handle('save-exported-video', async (_, videoData: ArrayBuffer, fileName: string) => {
try { try {
// Show save dialog to let user choose location and filename
const result = await dialog.showSaveDialog({ const result = await dialog.showSaveDialog({
title: 'Save Exported Video', title: 'Save Exported Video',
defaultPath: path.join(app.getPath('downloads'), fileName), defaultPath: path.join(app.getPath('downloads'), fileName),
@@ -138,7 +138,6 @@ export function registerIpcHandlers(
properties: ['createDirectory', 'showOverwriteConfirmation'] properties: ['createDirectory', 'showOverwriteConfirmation']
}); });
// User cancelled the dialog
if (result.canceled || !result.filePath) { if (result.canceled || !result.filePath) {
return { return {
success: false, success: false,
@@ -146,8 +145,6 @@ export function registerIpcHandlers(
message: 'Export cancelled' message: 'Export cancelled'
}; };
} }
// Write the file to the chosen location
await fs.writeFile(result.filePath, Buffer.from(videoData)); await fs.writeFile(result.filePath, Buffer.from(videoData));
return { return {
@@ -156,12 +153,58 @@ export function registerIpcHandlers(
message: 'Video exported successfully' message: 'Video exported successfully'
}; };
} catch (error) { } catch (error) {
console.error('Failed to save exported video:', error); console.error('Failed to save exported video:', error)
return { return {
success: false, success: false,
message: 'Failed to save exported video', message: 'Failed to save exported video',
error: String(error) 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 };
});
} }
+1 -27
View File
@@ -10,26 +10,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
export const RECORDINGS_DIR = path.join(app.getPath('userData'), 'recordings') 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() { async function ensureRecordingsDir() {
try { 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 // Register all IPC handlers when app is ready
app.whenReady().then(async () => { app.whenReady().then(async () => {
// Ensure recordings directory exists // Ensure recordings directory exists
await ensureRecordingsDir() await ensureRecordingsDir()
registerIpcHandlers( registerIpcHandlers(
createEditorWindowWrapper, createEditorWindowWrapper,
createSourceSelectorWindowWrapper, createSourceSelectorWindowWrapper,
+12
View File
@@ -42,4 +42,16 @@ contextBridge.exposeInMainWorld('electronAPI', {
saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => { saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => {
return ipcRenderer.invoke('save-exported-video', videoData, fileName) 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')
},
}) })
+3 -3
View File
@@ -10,10 +10,10 @@ const RENDERER_DIST = path.join(APP_ROOT, 'dist')
export function createHudOverlayWindow(): BrowserWindow { export function createHudOverlayWindow(): BrowserWindow {
const win = new BrowserWindow({ const win = new BrowserWindow({
width: 250, width: 350,
height: 80, height: 80,
minWidth: 250, minWidth: 350,
maxWidth: 250, maxWidth: 350,
minHeight: 80, minHeight: 80,
maxHeight: 80, maxHeight: 80,
frame: false, frame: false,
@@ -1,6 +1,15 @@
.electronDrag { .electronDrag {
-webkit-app-region: drag; -webkit-app-region: drag;
} }
.electronNoDrag { .electronNoDrag {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }
.folderButton {
cursor: pointer;
}
.folderButton:hover {
text-decoration: underline;
}
+33 -3
View File
@@ -5,6 +5,8 @@ import { Button } from "../ui/button";
import { BsRecordCircle } from "react-icons/bs"; import { BsRecordCircle } from "react-icons/bs";
import { FaRegStopCircle } from "react-icons/fa"; import { FaRegStopCircle } from "react-icons/fa";
import { MdMonitor } from "react-icons/md"; import { MdMonitor } from "react-icons/md";
import { RxDragHandleDots2 } from "react-icons/rx";
import { FaFolderMinus } from "react-icons/fa6";
export function LaunchWindow() { export function LaunchWindow() {
const { recording, toggleRecording } = useScreenRecorder(); 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 ( return (
<div className="w-full h-full flex items-center bg-transparent"> <div className="w-full h-full flex items-center bg-transparent">
<div <div
className={`w-full max-w-2xl mx-auto flex items-center justify-between px-3 py-1.5 ${styles.electronDrag}`} className={`w-full max-w-3xl mx-auto flex items-center justify-between px-3 py-1.5 ${styles.electronDrag}`}
style={{ style={{
borderRadius: 14, borderRadius: 14,
background: 'linear-gradient(135deg, rgba(30,30,40,0.85) 0%, rgba(20,20,30,0.75) 100%)', background: 'linear-gradient(135deg, rgba(30,30,40,0.85) 0%, rgba(20,20,30,0.75) 100%)',
@@ -56,6 +71,10 @@ export function LaunchWindow() {
minHeight: 36, minHeight: 36,
}} }}
> >
<div className={`flex items-center gap-1 ${styles.electronDrag}`}>
<RxDragHandleDots2 size={16} className="text-white/40" />
</div>
<Button <Button
variant="link" variant="link"
size="sm" size="sm"
@@ -63,7 +82,7 @@ export function LaunchWindow() {
onClick={openSourceSelector} onClick={openSourceSelector}
> >
<MdMonitor size={13} className="text-white" /> <MdMonitor size={13} className="text-white" />
{truncateText(selectedSource)} {truncateText(selectedSource, 6)}
</Button> </Button>
<div className="w-px h-5 bg-white/30" /> <div className="w-px h-5 bg-white/30" />
@@ -73,7 +92,7 @@ export function LaunchWindow() {
size="sm" size="sm"
onClick={hasSelectedSource ? toggleRecording : openSourceSelector} onClick={hasSelectedSource ? toggleRecording : openSourceSelector}
disabled={!hasSelectedSource && !recording} 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 ? ( {recording ? (
<> <>
@@ -87,6 +106,17 @@ export function LaunchWindow() {
</> </>
)} )}
</Button> </Button>
<div className="w-px h-5 bg-white/30" />
<Button
variant="link"
size="sm"
onClick={openVideoFile}
className={`gap-1 bg-transparent hover:bg-transparent px-0 flex-1 text-right text-xs ${styles.electronNoDrag} folderButton`}
>
<FaFolderMinus size={13} className="text-white" />
</Button>
</div> </div>
</div> </div>
); );
@@ -8,7 +8,8 @@ import { Button } from "@/components/ui/button";
import { useState } from "react"; import { useState } from "react";
import Colorful from '@uiw/react-color-colorful'; import Colorful from '@uiw/react-color-colorful';
import { hsvaToHex } from '@uiw/color-convert'; 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 { toast } from "sonner";
import type { ZoomDepth, CropRegion } from "./types"; import type { ZoomDepth, CropRegion } from "./types";
import { CropControl } from "./CropControl"; import { CropControl } from "./CropControl";
@@ -415,9 +416,9 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
onClick={() => { onClick={() => {
window.electronAPI?.openExternalUrl('https://github.com/siddharthvaddem/openscreen/issues/new'); 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"
> >
<Bug className="w-3 h-3 group-hover:text-[#34B27B] transition-colors" /> <Bug className="w-3 h-3 text-[#34B27B]" />
<span>Report a Bug</span> <span>Report a Bug</span>
</button> </button>
<button <button
@@ -425,10 +426,10 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
onClick={() => { onClick={() => {
window.electronAPI?.openExternalUrl('https://buymeacoffee.com/siddharthvaddem'); window.electronAPI?.openExternalUrl('https://buymeacoffee.com/siddharthvaddem');
}} }}
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"
> >
<Coffee className="w-3 h-3 group-hover:text-[#FFDD00] transition-colors" /> <GiHearts className="w-3 h-3 text-red-500" />
<span>Buy me a Coffee</span> <span>Support my work</span>
</button> </button>
</div> </div>
</div> </div>
+2 -2
View File
@@ -66,13 +66,13 @@ export default function VideoEditor() {
useEffect(() => { useEffect(() => {
async function loadVideo() { async function loadVideo() {
try { try {
const result = await window.electronAPI.getRecordedVideoPath(); const result = await window.electronAPI.getCurrentVideoPath();
if (result.success && result.path) { if (result.success && result.path) {
const videoUrl = toFileUrl(result.path); const videoUrl = toFileUrl(result.path);
setVideoPath(videoUrl); setVideoPath(videoUrl);
} else { } else {
setError(result.message || 'Failed to load video'); setError('No video to load. Please record or select a video.');
} }
} catch (err) { } catch (err) {
setError('Error loading video: ' + String(err)); setError('Error loading video: ' + String(err));
@@ -665,7 +665,13 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
video.pause(); video.pause();
allowPlaybackRef.current = false; allowPlaybackRef.current = false;
currentTimeRef.current = 0; currentTimeRef.current = 0;
setVideoReady(true);
// hacky fix: To ensure video is fully ready for PixiJS
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setVideoReady(true);
});
});
}; };
const [resolvedWallpaper, setResolvedWallpaper] = useState<string | null>(null); const [resolvedWallpaper, setResolvedWallpaper] = useState<string | null>(null);
+4
View File
@@ -38,5 +38,9 @@ interface Window {
message?: string message?: string
cancelled?: boolean 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 }>
} }
} }