diff --git a/dist-electron/main.js b/dist-electron/main.js index 47c4487..93b8a14 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -1,4 +1,4 @@ -import { app, BrowserWindow, ipcMain, desktopCapturer } from "electron"; +import { app, BrowserWindow, ipcMain, desktopCapturer, screen } from "electron"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import path from "node:path"; @@ -10,6 +10,8 @@ const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron"); const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist"); process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, "public") : RENDERER_DIST; let win; +let sourceSelectorWindow = null; +let selectedSource = null; function createHudOverlayWindow() { win = new BrowserWindow({ width: 250, @@ -71,6 +73,37 @@ function createEditorWindow() { }); } } +function createSourceSelectorWindow() { + const { width, height } = screen.getPrimaryDisplay().workAreaSize; + sourceSelectorWindow = new BrowserWindow({ + width: 620, + height: 420, + minHeight: 350, + maxHeight: 500, + x: Math.round((width - 620) / 2), + y: Math.round((height - 420) / 2), + frame: false, + resizable: false, + alwaysOnTop: true, + backgroundColor: "#ffffff", + webPreferences: { + preload: path.join(__dirname, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true + } + }); + sourceSelectorWindow.on("closed", () => { + sourceSelectorWindow = null; + }); + if (VITE_DEV_SERVER_URL) { + sourceSelectorWindow.loadURL(VITE_DEV_SERVER_URL + "?windowType=source-selector"); + } else { + sourceSelectorWindow.loadFile(path.join(RENDERER_DIST, "index.html"), { + query: { windowType: "source-selector" } + }); + } + return sourceSelectorWindow; +} function createWindow() { createHudOverlayWindow(); } @@ -86,7 +119,33 @@ app.on("activate", () => { } }); ipcMain.handle("get-sources", async (_, opts) => { - return await desktopCapturer.getSources(opts); + const sources = await desktopCapturer.getSources(opts); + const processedSources = sources.map((source) => ({ + id: source.id, + name: source.name, + display_id: source.display_id, + thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null, + appIcon: source.appIcon ? source.appIcon.toDataURL() : null + })); + return processedSources; +}); +ipcMain.handle("select-source", (_, source) => { + selectedSource = source; + if (sourceSelectorWindow) { + sourceSelectorWindow.close(); + sourceSelectorWindow = null; + } + return selectedSource; +}); +ipcMain.handle("get-selected-source", () => { + return selectedSource; +}); +ipcMain.handle("open-source-selector", () => { + if (sourceSelectorWindow) { + sourceSelectorWindow.focus(); + return; + } + createSourceSelectorWindow(); }); ipcMain.handle("switch-to-editor", () => { if (win) { diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs index c80af2b..d80554d 100644 --- a/dist-electron/preload.mjs +++ b/dist-electron/preload.mjs @@ -6,5 +6,14 @@ electron.contextBridge.exposeInMainWorld("electronAPI", { }, switchToEditor: () => { return electron.ipcRenderer.invoke("switch-to-editor"); + }, + openSourceSelector: () => { + return electron.ipcRenderer.invoke("open-source-selector"); + }, + selectSource: (source) => { + return electron.ipcRenderer.invoke("select-source", source); + }, + getSelectedSource: () => { + return electron.ipcRenderer.invoke("get-selected-source"); } }); diff --git a/electron/main.ts b/electron/main.ts index aa4d73b..7364ab4 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, ipcMain, desktopCapturer } from 'electron' +import { app, BrowserWindow, ipcMain, desktopCapturer, screen } from 'electron' import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' import path from 'node:path' @@ -25,6 +25,8 @@ export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST let win: BrowserWindow | null +let sourceSelectorWindow: BrowserWindow | null = null +let selectedSource: any = null function createHudOverlayWindow() { win = new BrowserWindow({ @@ -95,6 +97,42 @@ function createEditorWindow() { } } +function createSourceSelectorWindow() { + const { width, height } = screen.getPrimaryDisplay().workAreaSize; + + sourceSelectorWindow = new BrowserWindow({ + width: 620, + height: 420, + minHeight: 350, + maxHeight: 500, + x: Math.round((width - 620) / 2), + y: Math.round((height - 420) / 2), + frame: false, + resizable: false, + alwaysOnTop: true, + backgroundColor: '#ffffff', + webPreferences: { + preload: path.join(__dirname, 'preload.mjs'), + nodeIntegration: false, + contextIsolation: true, + }, + }); + + sourceSelectorWindow.on('closed', () => { + sourceSelectorWindow = null; + }); + + if (VITE_DEV_SERVER_URL) { + sourceSelectorWindow.loadURL(VITE_DEV_SERVER_URL + '?windowType=source-selector'); + } else { + sourceSelectorWindow.loadFile(path.join(RENDERER_DIST, 'index.html'), { + query: { windowType: 'source-selector' } + }); + } + + return sourceSelectorWindow; +} + function createWindow() { createHudOverlayWindow() } @@ -118,7 +156,37 @@ app.on('activate', () => { }) ipcMain.handle('get-sources', async (_, opts) => { - return await desktopCapturer.getSources(opts) + const sources = await desktopCapturer.getSources(opts) + const processedSources = sources.map(source => ({ + id: source.id, + name: source.name, + display_id: source.display_id, + thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null, + appIcon: source.appIcon ? source.appIcon.toDataURL() : null + })) + + return processedSources +}) + +ipcMain.handle('select-source', (_, source) => { + selectedSource = source + if (sourceSelectorWindow) { + sourceSelectorWindow.close(); + sourceSelectorWindow = null; + } + return selectedSource +}) + +ipcMain.handle('get-selected-source', () => { + return selectedSource +}) + +ipcMain.handle('open-source-selector', () => { + if (sourceSelectorWindow) { + sourceSelectorWindow.focus(); + return; + } + createSourceSelectorWindow(); }) ipcMain.handle('switch-to-editor', () => { diff --git a/electron/preload.ts b/electron/preload.ts index 0d394ea..b18bf76 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -6,5 +6,14 @@ contextBridge.exposeInMainWorld('electronAPI', { }, switchToEditor: () => { return ipcRenderer.invoke('switch-to-editor') + }, + openSourceSelector: () => { + return ipcRenderer.invoke('open-source-selector') + }, + selectSource: (source: any) => { + return ipcRenderer.invoke('select-source', source) + }, + getSelectedSource: () => { + return ipcRenderer.invoke('get-selected-source') } }) \ No newline at end of file diff --git a/index.html b/index.html index 1136dde..b397070 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + Pangolin
diff --git a/package-lock.json b/package-lock.json index 5d35f72..d48b62b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,13 @@ "version": "0.0.0", "dependencies": { "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.545.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^5.5.0", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7" }, @@ -1457,6 +1459,38 @@ "node": ">=14" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1472,6 +1506,132 @@ } } }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1490,6 +1650,103 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1994,7 +2251,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -6599,6 +6856,15 @@ "react": "^18.3.1" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", diff --git a/package.json b/package.json index 3e82a85..3320fd7 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,13 @@ }, "dependencies": { "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.545.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^5.5.0", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7" }, diff --git a/src/App.tsx b/src/App.tsx index b0f87f5..217006a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { LaunchWindow } from "./components/LaunchWindow"; +import { SourceSelector } from "./components/SourceSelector"; import { useEffect, useState } from "react"; export default function App() { @@ -22,6 +23,10 @@ export default function App() { return ; } + if (windowType === 'source-selector') { + return ; + } + if (windowType === 'editor') { return (
@@ -33,7 +38,7 @@ export default function App() { return (
-

Default Window

+

Pangolin

); } diff --git a/src/components/LaunchWindow.tsx b/src/components/LaunchWindow.tsx index 5d9533a..d608630 100644 --- a/src/components/LaunchWindow.tsx +++ b/src/components/LaunchWindow.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from "react"; import { useScreenRecorder } from "../hooks/useScreenRecorder"; import { Button } from "@/components/ui/button"; import { BsRecordCircle } from "react-icons/bs"; @@ -6,43 +7,71 @@ import { MdMonitor } from "react-icons/md"; export function LaunchWindow() { const { recording, toggleRecording } = useScreenRecorder(); + const [selectedSource, setSelectedSource] = useState("Screen"); + const [hasSelectedSource, setHasSelectedSource] = useState(false); + + useEffect(() => { + const checkSelectedSource = async () => { + if (window.electronAPI) { + const source = await window.electronAPI.getSelectedSource(); + if (source) { + setSelectedSource(source.name); + setHasSelectedSource(true); + } else { + setSelectedSource("Screen"); + setHasSelectedSource(false); + } + } + }; + + checkSelectedSource(); + + const interval = setInterval(checkSelectedSource, 500); + return () => clearInterval(interval); + }, []); + + const truncateText = (text: string, maxLength: number = 6) => { + if (text.length <= maxLength) return text; + return text.substring(0, maxLength) + "..."; + }; + + const openSourceSelector = () => { + if (window.electronAPI) { + window.electronAPI.openSourceSelector(); + } + }; return (
-
+
-
+
diff --git a/src/components/SourceSelector.tsx b/src/components/SourceSelector.tsx new file mode 100644 index 0000000..999d2db --- /dev/null +++ b/src/components/SourceSelector.tsx @@ -0,0 +1,202 @@ +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Card } from "@/components/ui/card"; + +interface DesktopSource { + id: string; + name: string; + thumbnail: string | null; + display_id: string; + appIcon: string | null; +} + +export function SourceSelector() { + const [sources, setSources] = useState([]); + const [selectedSource, setSelectedSource] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadSources(); + }, []); + + const loadSources = async () => { + try { + setLoading(true); + const rawSources = await window.electronAPI.getSources({ + types: ['screen', 'window'], + thumbnailSize: { width: 320, height: 180 }, + fetchWindowIcons: true + }); + + const formattedSources = rawSources.map(source => { + let displayName = source.name; + + if (source.id.startsWith('window:') && source.name.includes(' — ')) { + displayName = source.name.split(' — ')[1] || source.name; + } + + return { + id: source.id, + name: displayName, + thumbnail: source.thumbnail, + display_id: source.display_id, + appIcon: source.appIcon + }; + }); + + setSources(formattedSources); + } catch (error) { + console.error('Error loading sources:', error); + } finally { + setLoading(false); + } + }; + + const screenSources = sources.filter(source => source.id.startsWith('screen:')); + const windowSources = sources.filter(source => source.id.startsWith('window:')); + + const handleSourceSelect = (source: DesktopSource) => { + setSelectedSource(source); + }; + + const handleShare = async () => { + if (selectedSource) { + await window.electronAPI.selectSource(selectedSource); + } + }; + + if (loading) { + return ( +
+
+
+

Loading sources...

+
+
+ ); + } + + return ( +
+
+ + + + Screens + + + Windows + + + +
+ +
+ {screenSources.map((source) => ( + handleSourceSelect(source)} + > +
+
+ {source.name} + {selectedSource?.id === source.id && ( +
+
+ + + +
+
+ )} +
+
+ {source.name} +
+
+
+ ))} +
+
+ + +
+ {windowSources.map((source) => ( + handleSourceSelect(source)} + > +
+
+ {source.name} + {selectedSource?.id === source.id && ( +
+
+ + + +
+
+ )} +
+
+ {source.appIcon && ( + App icon + )} +
+ {source.name} +
+
+
+
+ ))} +
+
+
+
+
+ +
+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..cabfbfc --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..85d83be --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 5e59fab..e9ae062 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -28,26 +28,33 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const startRecording = async () => { try { - const sources = await window.electronAPI.getSources({ - types: ["screen", "window"], - thumbnailSize: { width: 1920, height: 1080 }, - }); - // change this later to a picker screen - const source = sources[1]; - console.log(sources) + // Get the selected source from the main process + const selectedSource = await window.electronAPI.getSelectedSource(); + + if (!selectedSource) { + alert("Please select a source to record"); + return; + } + + // Use the selected source const stream = await (navigator.mediaDevices as any).getUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: "desktop", - chromeMediaSourceId: source.id, + chromeMediaSourceId: selectedSource.id, }, }, }); streamRef.current = stream; + + if (!streamRef.current) { + throw new Error("Failed to get media stream"); + } + chunksRef.current = []; let mimeType = "video/webm;codecs=vp9"; - const recorder = new MediaRecorder(stream, { + const recorder = new MediaRecorder(streamRef.current, { mimeType, videoBitsPerSecond: 8000000, }); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index b8f0d54..03da974 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,7 +1,19 @@ /// + +interface ProcessedDesktopSource { + id: string; + name: string; + display_id: string; + thumbnail: string | null; + appIcon: string | null; +} + interface Window { electronAPI: { - getSources: (opts: Electron.SourcesOptions) => Promise + getSources: (opts: Electron.SourcesOptions) => Promise switchToEditor: () => Promise + openSourceSelector: () => Promise + selectSource: (source: any) => Promise + getSelectedSource: () => Promise } } \ No newline at end of file