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 ( +
+

Autoplay Window

+
+ ); +} 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)], + }, + }, +});