diff --git a/js/forge.config.ts b/js/forge.config.ts
index a8b063c..e8dcd90 100644
--- a/js/forge.config.ts
+++ b/js/forge.config.ts
@@ -68,6 +68,14 @@ const config: ForgeConfig = {
name: 'main_window',
config: 'vite.renderer.config.mts',
},
+ {
+ name: 'autoplay_window',
+ config: 'vite.autoplay.config.mts',
+ },
+ {
+ name: 'huddle_window',
+ config: 'vite.huddle.config.mts',
+ },
],
}),
// Fuses are used to enable/disable various Electron functionality
diff --git a/js/src/autoplay/AutoplayApp.tsx b/js/src/autoplay/AutoplayApp.tsx
new file mode 100644
index 0000000..2f1b651
--- /dev/null
+++ b/js/src/autoplay/AutoplayApp.tsx
@@ -0,0 +1,7 @@
+export function AutoplayApp() {
+ return (
+
+ );
+}
diff --git a/js/src/autoplay_window/index.html b/js/src/autoplay_window/index.html
new file mode 100644
index 0000000..2dcab0c
--- /dev/null
+++ b/js/src/autoplay_window/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ llink - Autoplay
+
+
+
+
+
+
diff --git a/js/src/autoplay_window/renderer.tsx b/js/src/autoplay_window/renderer.tsx
new file mode 100644
index 0000000..ba5267b
--- /dev/null
+++ b/js/src/autoplay_window/renderer.tsx
@@ -0,0 +1,6 @@
+import { createRoot } from 'react-dom/client';
+import { AutoplayApp } from '@/autoplay/AutoplayApp';
+import '@/styles/globals.css';
+
+const root = createRoot(document.getElementById('root')!);
+root.render();
diff --git a/js/src/electron.d.ts b/js/src/electron.d.ts
index 758d8b0..3eb329a 100644
--- a/js/src/electron.d.ts
+++ b/js/src/electron.d.ts
@@ -7,6 +7,10 @@ declare global {
maximize: () => void;
fullscreen: () => void;
close: () => void;
+ openAutoplay: () => void;
+ closeAutoplay: () => void;
+ openHuddle: () => void;
+ closeHuddle: () => void;
};
electronLink: {
fetchMetadata: (url: string) => Promise;
diff --git a/js/src/features/network-root.tsx b/js/src/features/network-root.tsx
index 1d4e16b..68c1a18 100644
--- a/js/src/features/network-root.tsx
+++ b/js/src/features/network-root.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { List, LayoutGrid } from "lucide-react";
import { particlePath } from "@/lib/particle-path";
@@ -25,6 +25,11 @@ export default function NetworkRoot() {
const viewMode = useViewModeStore((s) => s.viewMode);
const setViewMode = useViewModeStore((s) => s.setViewMode);
+ useEffect(() => {
+ window.electronWindow.openAutoplay();
+ window.electronWindow.openHuddle();
+ }, []);
+
const { streams } = useStreamParticles(path);
const userId = useAuthStore((s) => s.user?.id);
useDockBadge(streams, userId);
diff --git a/js/src/huddle/HuddleApp.tsx b/js/src/huddle/HuddleApp.tsx
new file mode 100644
index 0000000..414206a
--- /dev/null
+++ b/js/src/huddle/HuddleApp.tsx
@@ -0,0 +1,47 @@
+import { Mic, MicOff, Video, VideoOff, PhoneOff } from 'lucide-react';
+import { useState } from 'react';
+
+export function HuddleApp() {
+ const [micOn, setMicOn] = useState(true);
+ const [videoOn, setVideoOn] = useState(true);
+
+ const handleLeave = () => {
+ window.electronWindow.closeHuddle();
+ };
+
+ return (
+
+ {/* Video grid */}
+
+
+ You
+
+
+ Participant
+
+
+
+ {/* Controls bar */}
+
+
+
+
+
+
+ );
+}
diff --git a/js/src/huddle_window/index.html b/js/src/huddle_window/index.html
new file mode 100644
index 0000000..edc0e47
--- /dev/null
+++ b/js/src/huddle_window/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ llink - Huddle
+
+
+
+
+
+
diff --git a/js/src/huddle_window/renderer.tsx b/js/src/huddle_window/renderer.tsx
new file mode 100644
index 0000000..49ee87c
--- /dev/null
+++ b/js/src/huddle_window/renderer.tsx
@@ -0,0 +1,6 @@
+import { createRoot } from 'react-dom/client';
+import { HuddleApp } from '@/huddle/HuddleApp';
+import '@/styles/globals.css';
+
+const root = createRoot(document.getElementById('root')!);
+root.render();
diff --git a/js/src/main.ts b/js/src/main.ts
index f8e5209..16d69a4 100644
--- a/js/src/main.ts
+++ b/js/src/main.ts
@@ -22,6 +22,9 @@ if (process.platform === 'darwin' && !app.isPackaged) {
app.dock?.setIcon(path.join(__dirname, '../../assets/icon.png'));
}
+let autoplayWindow: BrowserWindow | null = null;
+let huddleWindow: BrowserWindow | null = null;
+
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
@@ -46,6 +49,68 @@ const createWindow = () => {
}
};
+const createAutoplayWindow = () => {
+ if (autoplayWindow) {
+ autoplayWindow.focus();
+ return;
+ }
+
+ autoplayWindow = new BrowserWindow({
+ width: 300,
+ height: 100,
+ resizable: false,
+ frame: false,
+ alwaysOnTop: true,
+ skipTaskbar: true,
+ webPreferences: {
+ preload: path.join(__dirname, 'preload.js'),
+ },
+ });
+
+ if (AUTOPLAY_WINDOW_VITE_DEV_SERVER_URL) {
+ autoplayWindow.loadURL(AUTOPLAY_WINDOW_VITE_DEV_SERVER_URL);
+ } else {
+ autoplayWindow.loadFile(
+ path.join(__dirname, `../renderer/${AUTOPLAY_WINDOW_VITE_NAME}/index.html`),
+ );
+ }
+
+ autoplayWindow.on('closed', () => {
+ autoplayWindow = null;
+ });
+};
+
+const createHuddleWindow = () => {
+ if (huddleWindow) {
+ huddleWindow.focus();
+ return;
+ }
+
+ huddleWindow = new BrowserWindow({
+ width: 1024,
+ height: 768,
+ resizable: true,
+ fullscreenable: true,
+ frame: true,
+ title: 'Huddle',
+ webPreferences: {
+ preload: path.join(__dirname, 'preload.js'),
+ },
+ });
+
+ if (HUDDLE_WINDOW_VITE_DEV_SERVER_URL) {
+ huddleWindow.loadURL(HUDDLE_WINDOW_VITE_DEV_SERVER_URL);
+ } else {
+ huddleWindow.loadFile(
+ path.join(__dirname, `../renderer/${HUDDLE_WINDOW_VITE_NAME}/index.html`),
+ );
+ }
+
+ huddleWindow.on('closed', () => {
+ huddleWindow = null;
+ });
+};
+
// Window control IPC handlers
ipcMain.on('window:minimize', (event) => {
BrowserWindow.fromWebContents(event.sender)?.minimize();
@@ -66,6 +131,20 @@ ipcMain.on('window:fullscreen', (event) => {
if (win) win.setFullScreen(!win.isFullScreen());
});
+// Secondary window IPC handlers
+ipcMain.on('window:open-autoplay', () => {
+ createAutoplayWindow();
+});
+ipcMain.on('window:close-autoplay', () => {
+ autoplayWindow?.close();
+});
+ipcMain.on('window:open-huddle', () => {
+ createHuddleWindow();
+});
+ipcMain.on('window:close-huddle', () => {
+ huddleWindow?.close();
+});
+
// --- Link metadata ---
const metadataCache = new Map();
diff --git a/js/src/preload.ts b/js/src/preload.ts
index 449bbba..00acb92 100644
--- a/js/src/preload.ts
+++ b/js/src/preload.ts
@@ -7,6 +7,10 @@ contextBridge.exposeInMainWorld('electronWindow', {
maximize: () => ipcRenderer.send('window:maximize'),
fullscreen: () => ipcRenderer.send('window:fullscreen'),
close: () => ipcRenderer.send('window:close'),
+ openAutoplay: () => ipcRenderer.send('window:open-autoplay'),
+ closeAutoplay: () => ipcRenderer.send('window:close-autoplay'),
+ openHuddle: () => ipcRenderer.send('window:open-huddle'),
+ closeHuddle: () => ipcRenderer.send('window:close-huddle'),
});
contextBridge.exposeInMainWorld('electronLink', {
diff --git a/js/src/vite-env.d.ts b/js/src/vite-env.d.ts
new file mode 100644
index 0000000..6c35629
--- /dev/null
+++ b/js/src/vite-env.d.ts
@@ -0,0 +1,8 @@
+// Electron Forge VitePlugin injects these globals at dev/build time.
+// Each renderer entry in forge.config.ts gets a pair of constants.
+declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined;
+declare const MAIN_WINDOW_VITE_NAME: string;
+declare const AUTOPLAY_WINDOW_VITE_DEV_SERVER_URL: string | undefined;
+declare const AUTOPLAY_WINDOW_VITE_NAME: string;
+declare const HUDDLE_WINDOW_VITE_DEV_SERVER_URL: string | undefined;
+declare const HUDDLE_WINDOW_VITE_NAME: string;
diff --git a/js/vite.autoplay.config.mts b/js/vite.autoplay.config.mts
new file mode 100644
index 0000000..14650e2
--- /dev/null
+++ b/js/vite.autoplay.config.mts
@@ -0,0 +1,20 @@
+import path from "path"
+import tailwindcss from "@tailwindcss/vite"
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+// https://vitejs.dev/config
+export default defineConfig({
+ root: path.resolve(__dirname, './src/autoplay_window'),
+ plugins: [react(), tailwindcss()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+ server: {
+ fs: {
+ allow: [path.resolve(__dirname)],
+ },
+ },
+});
diff --git a/js/vite.huddle.config.mts b/js/vite.huddle.config.mts
new file mode 100644
index 0000000..b86d70c
--- /dev/null
+++ b/js/vite.huddle.config.mts
@@ -0,0 +1,20 @@
+import path from "path"
+import tailwindcss from "@tailwindcss/vite"
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+// https://vitejs.dev/config
+export default defineConfig({
+ root: path.resolve(__dirname, './src/huddle_window'),
+ plugins: [react(), tailwindcss()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+ server: {
+ fs: {
+ allow: [path.resolve(__dirname)],
+ },
+ },
+});