diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index a4972be61..72e73d792 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -41,6 +41,10 @@ interface Window { error?: string; }; }>; + openNotes: () => Promise<{ + opened: boolean; + reason?: string; + }>; selectSource: (source: ProcessedDesktopSource) => Promise; getSelectedSource: () => Promise; onSelectedSourceChanged: (callback: (source: ProcessedDesktopSource) => void) => () => void; diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 85d154881..d8a1ee55a 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -1280,8 +1280,10 @@ export function registerIpcHandlers( createEditorWindow: () => void, createSourceSelectorWindow: () => BrowserWindow, createCountdownOverlayWindow: () => BrowserWindow, + createNotesWindowWrapper: () => BrowserWindow, getMainWindow: () => BrowserWindow | null, getSourceSelectorWindow: () => BrowserWindow | null, + getNotesWindow: () => BrowserWindow | null, getCountdownOverlayWindow?: () => BrowserWindow | null, onRecordingStateChange?: (recording: boolean, sourceName: string) => void, _switchToHud?: () => void, @@ -1479,6 +1481,17 @@ export function registerIpcHandlers( return { opened: true }; }); + ipcMain.handle("open-notes", async () => { + const notesSelectorWin = getNotesWindow(); + if (notesSelectorWin) { + notesSelectorWin.focus(); + return { opened: true }; + } + + createNotesWindowWrapper(); + return { opened: true }; + }); + ipcMain.handle("switch-to-editor", () => { // createEditorWindow already closes the current mainWindow (the HUD) before // opening the editor. Closing it here too double-closes, leaving ghost diff --git a/electron/main.ts b/electron/main.ts index a8e743fc9..fde69aa6f 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -25,6 +25,7 @@ import { createCountdownOverlayWindow, createEditorWindow, createHudOverlayWindow, + createNotesWindow, createSourceSelectorWindow, } from "./windows"; @@ -84,6 +85,7 @@ process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL let mainWindow: BrowserWindow | null = null; let sourceSelectorWindow: BrowserWindow | null = null; let countdownOverlayWindow: BrowserWindow | null = null; +let notesWindow: BrowserWindow | null = null; let tray: Tray | null = null; let selectedSourceName = ""; const isMac = process.platform === "darwin"; @@ -437,6 +439,19 @@ function createSourceSelectorWindowWrapper() { return sourceSelectorWindow; } +function createNotesWindowWrapper() { + { + notesWindow = createNotesWindow(); + notesWindow.on("closed", () => { + notesWindow = null; + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("notes-window-closed"); + } + }); + return notesWindow; + } +} + function createCountdownOverlayWindowWrapper() { if (countdownOverlayWindow && !countdownOverlayWindow.isDestroyed()) { return countdownOverlayWindow; @@ -575,8 +590,10 @@ appReady?.then(async () => { createEditorWindowWrapper, createSourceSelectorWindowWrapper, createCountdownOverlayWindowWrapper, + createNotesWindowWrapper, () => mainWindow, () => sourceSelectorWindow, + () => notesWindow, () => countdownOverlayWindow, (recording: boolean, sourceName: string) => { selectedSourceName = sourceName; diff --git a/electron/preload.ts b/electron/preload.ts index fefcac044..e0d246bda 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -47,6 +47,9 @@ contextBridge.exposeInMainWorld("electronAPI", { openSourceSelector: () => { return ipcRenderer.invoke("open-source-selector"); }, + openNotes: () => { + return ipcRenderer.invoke("open-notes"); + }, selectSource: (source: ProcessedDesktopSource) => { return ipcRenderer.invoke("select-source", source); }, diff --git a/electron/windows.ts b/electron/windows.ts index c3830157f..70bacbee2 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -320,3 +320,44 @@ export function createCountdownOverlayWindow(): BrowserWindow { return win; } + +// Frameless Notes Window for taking notes during a recording. +export function createNotesWindow(): BrowserWindow { + const win = new BrowserWindow({ + width: 400, + height: 540, + minWidth: 320, + minHeight: 400, + maxWidth: 640, + maxHeight: 720, + title: "OpenScreen - Notes", + backgroundColor: "#09090b", + resizable: true, + alwaysOnTop: true, + skipTaskbar: false, + show: false, + webPreferences: { + preload: path.join(__dirname, "preload.mjs"), + additionalArguments: [ASSET_BASE_URL_ARG], + nodeIntegration: false, + contextIsolation: true, + backgroundThrottling: false, + }, + }); + + win.setContentProtection(true); + win.once("ready-to-show", () => { + win.setContentProtection(true); + win.show(); + }); + + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL + "?showNotes=true"); + } else { + win.loadFile(path.join(RENDERER_DIST, "index.html"), { + query: { showNotes: "true" }, + }); + } + + return win; +} diff --git a/src/App.tsx b/src/App.tsx index 0c8875d04..8c766611e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { lazy, Suspense, useEffect, useState } from "react"; import { CountdownOverlay } from "./components/launch/CountdownOverlay.tsx"; import { LaunchWindow } from "./components/launch/LaunchWindow"; +import { NotesWindow } from "./components/launch/NotesWindow.tsx"; import { SourceSelector } from "./components/launch/SourceSelector"; import { Toaster } from "./components/ui/sonner"; import { TooltipProvider } from "./components/ui/tooltip"; @@ -19,6 +20,8 @@ export default function App() { const [windowType, setWindowType] = useState( () => new URLSearchParams(window.location.search).get("windowType") || "", ); + const showNotes = new URLSearchParams(window.location.search).get("showNotes") === "true"; + const tEditor = useScopedT("editor"); useEffect(() => { @@ -102,8 +105,10 @@ export default function App() { ); default: return ( -
-

Openscreen

+
+
+

Openscreen

+
); } @@ -111,6 +116,7 @@ export default function App() { return ( + {showNotes && } {content} diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 24d273b49..8f7ef5cd8 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -1,4 +1,12 @@ -import { Check, ChevronDown, Clapperboard, Columns3, Languages, Rows3 } from "lucide-react"; +import { + Check, + ChevronDown, + Clapperboard, + Columns3, + Languages, + NotepadText, + Rows3, +} from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { BsPauseCircle, BsPlayCircle, BsRecordCircle } from "react-icons/bs"; @@ -697,7 +705,10 @@ export function LaunchWindow() { onMouseLeave={() => setIsMicHovered(false)} onFocus={() => setIsMicFocused(true)} onBlur={() => setIsMicFocused(false)} - style={{ width: micExpanded ? "240px" : "140px", transition: "width 300ms ease" }} + style={{ + width: micExpanded ? "240px" : "140px", + transition: "width 300ms ease", + }} >
{!micExpanded && ( @@ -743,7 +754,10 @@ export function LaunchWindow() { onMouseLeave={() => setIsWebcamHovered(false)} onFocus={() => setIsWebcamFocused(true)} onBlur={() => setIsWebcamFocused(false)} - style={{ width: webcamExpanded ? "240px" : "140px", transition: "width 300ms ease" }} + style={{ + width: webcamExpanded ? "240px" : "140px", + transition: "width 300ms ease", + }} >
{!webcamExpanded && ( @@ -1031,6 +1045,17 @@ export function LaunchWindow() {
)} + + + + {!recording && (