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 (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Screens
+
+
+ Windows
+
+
+
+
+
+
+ {screenSources.map((source) => (
+
handleSourceSelect(source)}
+ >
+
+
+

+ {selectedSource?.id === source.id && (
+
+ )}
+
+
+ {source.name}
+
+
+
+ ))}
+
+
+
+
+
+ {windowSources.map((source) => (
+
handleSourceSelect(source)}
+ >
+
+
+

+ {selectedSource?.id === source.id && (
+
+ )}
+
+
+ {source.appIcon && (
+

+ )}
+
+ {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