const sessions = new Map(); const tabToSession = new Map(); const windowToSession = new Map(); const previousWindowStates = new Map(); function makeSessionId() { return `${Date.now()}-${Math.random().toString(36).slice(2)}`; } async function getUsableDisplays() { const displays = await chrome.system.display.getInfo(); const unique = []; const seen = new Set(); for (const display of displays) { // Mirrored displays should behave like a single display. if (display.mirroringSourceId) continue; const bounds = display.bounds || {}; const key = `${bounds.left},${bounds.top},${bounds.width},${bounds.height}`; if (seen.has(key)) continue; seen.add(key); unique.push(display); } return unique; } function chooseSecondaryDisplay(displays) { const primary = displays.find((display) => display.isPrimary) || displays[0]; const secondary = displays.find((display) => display.id !== primary?.id); return { primary, secondary }; } async function waitForTabComplete(tabId, timeoutMs = 20000) { const existing = await chrome.tabs.get(tabId).catch(() => null); if (existing?.status === "complete") return; await new Promise((resolve, reject) => { const timeout = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); reject(new Error("Timed out waiting for the presentation page to load.")); }, timeoutMs); function listener(updatedTabId, changeInfo) { if (updatedTabId === tabId && changeInfo.status === "complete") { clearTimeout(timeout); chrome.tabs.onUpdated.removeListener(listener); resolve(); } } chrome.tabs.onUpdated.addListener(listener); }); } async function injectPresenter(tabId, config) { await chrome.scripting.executeScript({ target: { tabId }, files: ["presenter.js"] }); await chrome.tabs.sendMessage(tabId, { type: "QLEARN_PRESENTER_START", config }); } async function startSingleScreen(tab) { if (!tab.id || tab.windowId == null) return; const sessionId = makeSessionId(); const browserWindow = await chrome.windows.get(tab.windowId); previousWindowStates.set(tab.windowId, browserWindow.state || "maximized"); const session = { id: sessionId, mode: "single", sourceTabId: tab.id, audienceTabId: tab.id, audienceWindowId: tab.windowId, controlWindowId: null, pageTitle: "Hudra", current: 0, count: 0, audienceHidden: false, audienceZoom: 100, audienceScrollY: 0, currentPreview: "", nextPreview: "" }; sessions.set(sessionId, session); tabToSession.set(tab.id, sessionId); windowToSession.set(tab.windowId, sessionId); await injectPresenter(tab.id, { mode: "single", sessionId }); await chrome.windows.update(tab.windowId, { state: "fullscreen" }); } async function startDualScreen(tab, primary, secondary) { if (!tab.id || !tab.url) return; const sessionId = makeSessionId(); const sb = secondary.bounds; const pb = primary.bounds; const audienceWindow = await chrome.windows.create({ url: tab.url, type: "popup", focused: true, left: sb.left, top: sb.top, width: Math.max(500, sb.width), height: Math.max(400, sb.height) }); const audienceTab = audienceWindow.tabs?.[0]; if (!audienceWindow.id || !audienceTab?.id) { throw new Error("Could not create the audience presentation window."); } const controlUrl = chrome.runtime.getURL(`controls.html?session=${encodeURIComponent(sessionId)}`); const controlWindow = await chrome.windows.create({ url: controlUrl, type: "popup", focused: false, left: pb.left, top: pb.top, width: Math.max(700, pb.width), height: Math.max(500, pb.height) }); const session = { id: sessionId, mode: "dual", sourceTabId: tab.id, audienceTabId: audienceTab.id, audienceWindowId: audienceWindow.id, controlWindowId: controlWindow.id || null, pageTitle: "Loading lesson…", current: 0, count: 0, audienceHidden: false, audienceZoom: 100, audienceScrollY: 0, currentPreview: "", nextPreview: "" }; sessions.set(sessionId, session); tabToSession.set(audienceTab.id, sessionId); windowToSession.set(audienceWindow.id, sessionId); if (controlWindow.id != null) windowToSession.set(controlWindow.id, sessionId); await waitForTabComplete(audienceTab.id); await injectPresenter(audienceTab.id, { mode: "audience", sessionId }); // Position first, then fullscreen so the chosen monitor owns fullscreen. await chrome.windows.update(audienceWindow.id, { left: sb.left, top: sb.top, width: sb.width, height: sb.height, focused: true }); await chrome.windows.update(audienceWindow.id, { state: "fullscreen" }); if (controlWindow.id != null) { await chrome.windows.update(controlWindow.id, { left: pb.left, top: pb.top, width: pb.width, height: pb.height, focused: true }).catch(() => {}); await chrome.windows.update(controlWindow.id, { state: "fullscreen", focused: true }).catch(() => {}); } } async function endSession(sessionId, reason = "ended") { const session = sessions.get(sessionId); if (!session) return; sessions.delete(sessionId); if (session.audienceTabId != null) tabToSession.delete(session.audienceTabId); if (session.audienceWindowId != null) windowToSession.delete(session.audienceWindowId); if (session.controlWindowId != null) windowToSession.delete(session.controlWindowId); if (session.mode === "single") { await chrome.tabs.sendMessage(session.audienceTabId, { type: "QLEARN_PRESENTER_COMMAND", command: "exit", reason }).catch(() => {}); const previousState = previousWindowStates.get(session.audienceWindowId) || "maximized"; previousWindowStates.delete(session.audienceWindowId); await chrome.windows.update(session.audienceWindowId, { state: previousState }).catch(async () => { await chrome.windows.update(session.audienceWindowId, { state: "maximized" }).catch(() => {}); }); } else { if (session.audienceWindowId != null) { await chrome.windows.remove(session.audienceWindowId).catch(() => {}); } if (session.controlWindowId != null) { await chrome.windows.remove(session.controlWindowId).catch(() => {}); } } } async function sendCommand(sessionId, command, value) { const session = sessions.get(sessionId); if (!session?.audienceTabId) return; if (command === "exit") { await endSession(sessionId, "user"); return; } await chrome.tabs.sendMessage(session.audienceTabId, { type: "QLEARN_PRESENTER_COMMAND", command, value }).catch(() => {}); } async function toggleHudraForTab(tab) { if (!tab?.id) return; const windowSessionId = tab.windowId != null ? windowToSession.get(tab.windowId) : null; if (windowSessionId) { await endSession(windowSessionId, "toggle"); return; } const existingId = tabToSession.get(tab.id); if (existingId) { await endSession(existingId, "toggle"); return; } try { const displays = await getUsableDisplays(); const { primary, secondary } = chooseSecondaryDisplay(displays); if (primary && secondary) { await startDualScreen(tab, primary, secondary); } else { await startSingleScreen(tab); } } catch (error) { console.error("Hudra failed to start:", error); try { await startSingleScreen(tab); } catch (fallbackError) { console.error("Hudra fallback also failed:", fallbackError); } } } chrome.action.onClicked.addListener(toggleHudraForTab); chrome.commands.onCommand.addListener(async (command) => { if (command !== "toggle-hudra") return; const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (tab) await toggleHudraForTab(tab); }); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message?.type === "QLEARN_PRESENTER_STATE") { const session = sessions.get(message.sessionId); if (!session) return; session.current = Number(message.current) || 0; session.count = Number(message.count) || 0; session.pageTitle = message.pageTitle || session.pageTitle; session.audienceHidden = Boolean(message.audienceHidden); session.audienceZoom = Number(message.audienceZoom) || 100; session.audienceScrollY = Number(message.audienceScrollY) || 0; session.currentPreview = message.currentPreview || ""; session.nextPreview = message.nextPreview || ""; chrome.runtime.sendMessage({ type: "QLEARN_PRESENTER_STATE_UPDATE", sessionId: session.id, current: session.current, count: session.count, pageTitle: session.pageTitle, mode: session.mode, audienceHidden: session.audienceHidden, audienceZoom: session.audienceZoom, audienceScrollY: session.audienceScrollY, currentPreview: session.currentPreview, nextPreview: session.nextPreview }).catch(() => {}); return; } if (message?.type === "QLEARN_PRESENTER_ENDED") { endSession(message.sessionId, "audience").catch(() => {}); return; } if (message?.type === "QLEARN_PRESENTER_CONTROL_COMMAND") { sendCommand(message.sessionId, message.command, message.value).catch(() => {}); return; } if (message?.type === "QLEARN_PRESENTER_GET_SESSION") { const session = sessions.get(message.sessionId); sendResponse(session ? { sessionId: session.id, mode: session.mode, current: session.current, count: session.count, pageTitle: session.pageTitle, audienceHidden: session.audienceHidden, audienceZoom: session.audienceZoom, audienceScrollY: session.audienceScrollY, currentPreview: session.currentPreview, nextPreview: session.nextPreview } : null); return true; } }); chrome.windows.onRemoved.addListener((windowId) => { const sessionId = windowToSession.get(windowId); if (!sessionId) return; const session = sessions.get(sessionId); if (!session) return; // Closing either dual-screen window ends the whole presentation. if (session.mode === "dual") { endSession(sessionId, "window-closed").catch(() => {}); } });