Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 152 additions & 9 deletions dist-electron/main.js

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions dist-electron/preload.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,29 @@ electron.contextBridge.exposeInMainWorld("electronAPI", {
},
getPlatform: () => {
return electron.ipcRenderer.invoke("get-platform");
},
openAreaSelector: () => {
return electron.ipcRenderer.invoke("open-area-selector");
},
selectAreaRegion: (region) => {
return electron.ipcRenderer.invoke("select-area-region", region);
},
getSelectedAreaRegion: () => {
return electron.ipcRenderer.invoke("get-selected-area-region");
},
openCameraBubble: (deviceId, displayId) => {
return electron.ipcRenderer.invoke("open-camera-bubble", deviceId, displayId);
},
closeCameraBubble: () => {
return electron.ipcRenderer.invoke("close-camera-bubble");
},
moveCameraBubble: (x, y) => {
return electron.ipcRenderer.invoke("move-camera-bubble", x, y);
},
resizeCameraBubble: (size) => {
return electron.ipcRenderer.invoke("resize-camera-bubble", size);
},
openRecordingsFolder: () => {
return electron.ipcRenderer.invoke("open-recordings-folder");
}
});
25 changes: 24 additions & 1 deletion electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,21 @@ import fs from 'node:fs/promises'
import path from 'node:path'
import { RECORDINGS_DIR } from '../main'

let selectedSource: any = null
let selectedSource: {
id: string;
name: string;
thumbnail: string | null;
display_id: string;
appIcon: string | null;
microphoneId: string | null;
} | null = null

let selectedAreaRegion: {
x: number;
y: number;
width: number;
height: number;
} | null = null

export function registerIpcHandlers(
createEditorWindow: () => void,
Expand Down Expand Up @@ -219,4 +233,13 @@ export function registerIpcHandlers(
ipcMain.handle('get-platform', () => {
return process.platform;
});

ipcMain.handle('select-area-region', (_, region) => {
selectedAreaRegion = region;
return { success: true };
});

ipcMain.handle('get-selected-area-region', () => {
return selectedAreaRegion;
});
}
72 changes: 52 additions & 20 deletions electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { app, BrowserWindow, Tray, Menu, nativeImage } from 'electron'
import { app, BrowserWindow, Tray, Menu, nativeImage, shell } from 'electron'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import fs from 'node:fs/promises'
import { createHudOverlayWindow, createEditorWindow, createSourceSelectorWindow } from './windows'
import { createHudOverlayWindow, createEditorWindow, createSourceSelectorWindow, createAreaSelectorWindow, createCameraBubbleWindow, closeCameraBubbleWindow, moveCameraBubbleWindow, resizeCameraBubbleWindow } from './windows'
import { registerIpcHandlers } from './ipc/handlers'


Expand Down Expand Up @@ -42,6 +42,7 @@ process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT,
// Window references
let mainWindow: BrowserWindow | null = null
let sourceSelectorWindow: BrowserWindow | null = null
let areaSelectorWindow: BrowserWindow | null = null
let tray: Tray | null = null
let selectedSourceName = ''

Expand Down Expand Up @@ -138,29 +139,60 @@ app.on('activate', () => {

// Register all IPC handlers when app is ready
app.whenReady().then(async () => {
// Listen for HUD overlay quit event (macOS only)
const { ipcMain } = await import('electron');

ipcMain.on('hud-overlay-close', () => {
app.quit();
});

ipcMain.handle('open-area-selector', () => {
if (areaSelectorWindow && !areaSelectorWindow.isDestroyed()) {
areaSelectorWindow.focus();
return;
}
areaSelectorWindow = createAreaSelectorWindow();
areaSelectorWindow.on('closed', () => {
areaSelectorWindow = null;
});
});

ipcMain.handle('open-camera-bubble', (_, deviceId: string, displayId?: string) => {
createCameraBubbleWindow(deviceId, displayId);
});

ipcMain.handle('close-camera-bubble', () => {
closeCameraBubbleWindow();
});

ipcMain.handle('move-camera-bubble', (_, x: number, y: number) => {
moveCameraBubbleWindow(x, y);
});

ipcMain.handle('resize-camera-bubble', (_, size: number) => {
resizeCameraBubbleWindow(size);
});

ipcMain.handle('open-recordings-folder', async () => {
await shell.openPath(RECORDINGS_DIR);
});

createTray()
updateTrayMenu()
// Ensure recordings directory exists
await ensureRecordingsDir()

registerIpcHandlers(
createEditorWindowWrapper,
createSourceSelectorWindowWrapper,
() => mainWindow,
() => sourceSelectorWindow,
(recording: boolean, sourceName: string) => {
selectedSourceName = sourceName
if (!tray) createTray();
updateTrayMenu(recording);
if (!recording) {
if (mainWindow) mainWindow.restore();
await ensureRecordingsDir()

registerIpcHandlers(
createEditorWindowWrapper,
createSourceSelectorWindowWrapper,
() => mainWindow,
() => sourceSelectorWindow,
(recording: boolean, sourceName: string) => {
selectedSourceName = sourceName
if (!tray) createTray();
updateTrayMenu(recording);
if (!recording) {
if (mainWindow) mainWindow.restore();
}
}
}
)
createWindow()
)
createWindow()
})
24 changes: 24 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,28 @@ contextBridge.exposeInMainWorld('electronAPI', {
getPlatform: () => {
return ipcRenderer.invoke('get-platform')
},
openAreaSelector: () => {
return ipcRenderer.invoke('open-area-selector')
},
selectAreaRegion: (region: { x: number; y: number; width: number; height: number }) => {
return ipcRenderer.invoke('select-area-region', region)
},
getSelectedAreaRegion: () => {
return ipcRenderer.invoke('get-selected-area-region')
},
openCameraBubble: (deviceId: string, displayId?: string) => {
return ipcRenderer.invoke('open-camera-bubble', deviceId, displayId)
},
closeCameraBubble: () => {
return ipcRenderer.invoke('close-camera-bubble')
},
moveCameraBubble: (x: number, y: number) => {
return ipcRenderer.invoke('move-camera-bubble', x, y)
},
resizeCameraBubble: (size: number) => {
return ipcRenderer.invoke('resize-camera-bubble', size)
},
openRecordingsFolder: () => {
return ipcRenderer.invoke('open-recordings-folder')
},
})
134 changes: 126 additions & 8 deletions electron/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,19 @@ export function createHudOverlayWindow(): BrowserWindow {
const primaryDisplay = screen.getPrimaryDisplay();
const { workArea } = primaryDisplay;


const windowWidth = 500;
const windowHeight = 100;
const windowWidth = 740;
const windowHeight = 260;

const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2);
const y = Math.floor(workArea.y + workArea.height - windowHeight - 5);
const y = Math.floor(workArea.y + workArea.height - windowHeight - 20);

const win = new BrowserWindow({
width: windowWidth,
height: windowHeight,
minWidth: 500,
maxWidth: 500,
minHeight: 100,
maxHeight: 100,
minWidth: 740,
maxWidth: 740,
minHeight: 200,
maxHeight: 300,
x: x,
y: y,
frame: false,
Expand Down Expand Up @@ -153,3 +152,122 @@ export function createSourceSelectorWindow(): BrowserWindow {

return win
}

export function createAreaSelectorWindow(): BrowserWindow {
const primaryDisplay = screen.getPrimaryDisplay();
const { bounds } = primaryDisplay;

const win = new BrowserWindow({
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
frame: false,
transparent: true,
alwaysOnTop: true,
skipTaskbar: true,
resizable: false,
movable: false,
fullscreen: true,
webPreferences: {
preload: path.join(__dirname, 'preload.mjs'),
nodeIntegration: false,
contextIsolation: true,
},
})

if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL + '?windowType=area-selector')
} else {
win.loadFile(path.join(RENDERER_DIST, 'index.html'), {
query: { windowType: 'area-selector' }
})
}

return win
}

let cameraBubbleWindow: BrowserWindow | null = null;

export function createCameraBubbleWindow(deviceId: string, displayId?: string): BrowserWindow {
if (cameraBubbleWindow && !cameraBubbleWindow.isDestroyed()) {
cameraBubbleWindow.close();
}

const displays = screen.getAllDisplays();
let targetDisplay = displays[0];

if (displayId) {
const found = displays.find(d => String(d.id) === displayId);
if (found) targetDisplay = found;
}

const size = 180;
const padding = 30;
const x = targetDisplay.bounds.x + padding;
const y = targetDisplay.bounds.y + targetDisplay.bounds.height - size - padding;

const win = new BrowserWindow({
width: size,
height: size,
x,
y,
frame: false,
transparent: true,
alwaysOnTop: true,
skipTaskbar: true,
resizable: false,
hasShadow: false,
focusable: false,
webPreferences: {
preload: path.join(__dirname, 'preload.mjs'),
nodeIntegration: false,
contextIsolation: true,
backgroundThrottling: false,
},
});

win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
win.setAlwaysOnTop(true, 'screen-saver');
win.setIgnoreMouseEvents(false);

if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL + `?windowType=camera-bubble&deviceId=${encodeURIComponent(deviceId)}&size=${size}`);
} else {
win.loadFile(path.join(RENDERER_DIST, 'index.html'), {
query: { windowType: 'camera-bubble', deviceId, size: String(size) }
});
}

cameraBubbleWindow = win;

win.on('closed', () => {
cameraBubbleWindow = null;
});

return win;
}

export function closeCameraBubbleWindow() {
if (cameraBubbleWindow && !cameraBubbleWindow.isDestroyed()) {
cameraBubbleWindow.close();
cameraBubbleWindow = null;
}
}

export function moveCameraBubbleWindow(x: number, y: number) {
if (cameraBubbleWindow && !cameraBubbleWindow.isDestroyed()) {
cameraBubbleWindow.setPosition(Math.round(x), Math.round(y));
}
}

export function resizeCameraBubbleWindow(size: number) {
if (cameraBubbleWindow && !cameraBubbleWindow.isDestroyed()) {
cameraBubbleWindow.setSize(size, size);
}
}

export function getCameraBubbleWindow(): BrowserWindow | null {
return cameraBubbleWindow;
}

10 changes: 8 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useEffect, useState } from "react";
import { LaunchWindow } from "./components/launch/LaunchWindow";
import { SourceSelector } from "./components/launch/SourceSelector";
import { AreaSelector } from "./components/launch/AreaSelector";
import { CameraBubble } from "./components/launch/CameraBubble";
import VideoEditor from "./components/video-editor/VideoEditor";

export default function App() {
Expand All @@ -10,7 +12,7 @@ export default function App() {
const params = new URLSearchParams(window.location.search);
const type = params.get('windowType') || '';
setWindowType(type);
if (type === 'hud-overlay' || type === 'source-selector') {
if (type === 'hud-overlay' || type === 'source-selector' || type === 'area-selector' || type === 'camera-bubble') {
document.body.style.background = 'transparent';
document.documentElement.style.background = 'transparent';
document.getElementById('root')?.style.setProperty('background', 'transparent');
Expand All @@ -22,9 +24,13 @@ export default function App() {
return <LaunchWindow />;
case 'source-selector':
return <SourceSelector />;
case 'area-selector':
return <AreaSelector />;
case 'camera-bubble':
return <CameraBubble />;
case 'editor':
return <VideoEditor />;
default:
default:
return (
<div className="w-full h-full bg-background text-foreground">
<h1>Openscreen</h1>
Expand Down
Loading