mirror of
https://github.com/siddharthvaddem/openscreen.git
synced 2026-09-21 05:43:27 +08:00
record/ select your own video
This commit is contained in:
+43
-25
@@ -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(
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+4
-1
@@ -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 }>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Vendored
+4
@@ -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 }>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user