prototype with two windows

This commit is contained in:
talksik
2026-03-31 13:35:53 -07:00
parent 7f490b4596
commit 1effd9bb43
14 changed files with 237 additions and 1 deletions
+8
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
export function AutoplayApp() {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', color: 'white', background: '#1a1a1a' }}>
<p>Autoplay Window</p>
</div>
);
}
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html class="dark">
<head>
<meta charset="UTF-8" />
<title>llink - Autoplay</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./renderer.tsx"></script>
</body>
</html>
+6
View File
@@ -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(<AutoplayApp />);
+4
View File
@@ -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<LinkMetadata | null>;
+6 -1
View File
@@ -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);
+47
View File
@@ -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 (
<div className="flex h-screen flex-col bg-background text-foreground">
{/* Video grid */}
<div className="flex flex-1 items-center justify-center gap-4 p-6">
<div className="bg-muted flex aspect-video w-full max-w-md items-center justify-center rounded-xl">
<span className="text-muted-foreground text-sm">You</span>
</div>
<div className="bg-muted flex aspect-video w-full max-w-md items-center justify-center rounded-xl">
<span className="text-muted-foreground text-sm">Participant</span>
</div>
</div>
{/* Controls bar */}
<div className="flex items-center justify-center gap-3 border-t px-6 py-4">
<button
className="rounded-full bg-muted p-3 hover:bg-muted/80 transition-colors"
onClick={() => setMicOn(!micOn)}
>
{micOn ? <Mic className="size-5" /> : <MicOff className="size-5 text-destructive" />}
</button>
<button
className="rounded-full bg-muted p-3 hover:bg-muted/80 transition-colors"
onClick={() => setVideoOn(!videoOn)}
>
{videoOn ? <Video className="size-5" /> : <VideoOff className="size-5 text-destructive" />}
</button>
<button
className="rounded-full bg-destructive p-3 text-destructive-foreground hover:bg-destructive/90 transition-colors"
onClick={handleLeave}
>
<PhoneOff className="size-5" />
</button>
</div>
</div>
);
}
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html class="dark">
<head>
<meta charset="UTF-8" />
<title>llink - Huddle</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./renderer.tsx"></script>
</body>
</html>
+6
View File
@@ -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(<HuddleApp />);
+79
View File
@@ -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<string, LinkMetadata>();
+4
View File
@@ -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', {
+8
View File
@@ -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;
+20
View File
@@ -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)],
},
},
});
+20
View File
@@ -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)],
},
},
});