DisplayMode.exe
@@ -220,20 +226,38 @@
+
+
diff --git a/static/css/layout.css b/static/css/layout.css
index 39743fa..824b1a0 100644
--- a/static/css/layout.css
+++ b/static/css/layout.css
@@ -499,15 +499,6 @@ body.is-dragging-window .ui-tool-title-bar {
width: min(27rem, 92vw);
}
-.terminal-modal-overlay {
- position: fixed;
- inset: 0;
- z-index: 72;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
.terminal-window {
width: min(46rem, 94vw);
}
diff --git a/static/js/room.js b/static/js/room.js
index 5ad23f0..2755243 100644
--- a/static/js/room.js
+++ b/static/js/room.js
@@ -16,15 +16,13 @@ const participantList = document.getElementById('participant-list');
const adminControls = document.getElementById('admin-controls');
const revealBtn = document.getElementById('reveal-btn');
const resetBtn = document.getElementById('reset-btn');
-const terminalBtn = document.getElementById('terminal-btn');
const shareLinkInput = document.getElementById('share-link');
const shareAdminToggle = document.getElementById('share-admin-toggle');
const votesCounter = document.getElementById('votes-counter');
const roomMessage = document.getElementById('room-message');
const changeNameBtn = document.getElementById('change-name-btn');
-const terminalModalOverlay = document.getElementById('terminal-modal-overlay');
-const terminalCloseBtn = document.getElementById('terminal-close-btn');
const terminalLogOutput = document.getElementById('terminal-log-output');
+const TERMINAL_WINDOW_ID = 'terminal-tool-window';
const joinPanel = document.getElementById('join-panel');
const joinForm = document.getElementById('join-form');
@@ -315,15 +313,6 @@ function renderTerminalLogs(logs) {
terminalLogOutput.scrollTop = terminalLogOutput.scrollHeight;
}
-function openTerminal() {
- terminalModalOverlay.classList.remove('hidden');
- renderTerminalLogs(latestAdminLogs);
-}
-
-function closeTerminal() {
- terminalModalOverlay.classList.add('hidden');
-}
-
function renderState(state) {
roomTitle.textContent = `${state.roomName} (${state.roomId})`;
revealModeLabel.textContent = `Reveal mode: ${state.revealMode}`;
@@ -341,16 +330,18 @@ function renderState(state) {
latestLinks = state.links || { participantLink: '', adminLink: '' };
updateShareLink();
+ if (typeof window.setUIWindowAccess === 'function') {
+ window.setUIWindowAccess({ admin: state.viewerIsAdmin });
+ }
+
if (state.viewerIsAdmin) {
adminControls.classList.remove('hidden');
- terminalBtn.classList.remove('hidden');
} else {
adminControls.classList.add('hidden');
- terminalBtn.classList.add('hidden');
- closeTerminal();
}
+
latestAdminLogs = Array.isArray(state.adminLogs) ? state.adminLogs : [];
- if (state.viewerIsAdmin && !terminalModalOverlay.classList.contains('hidden')) {
+ if (state.viewerIsAdmin && typeof window.isUIWindowOpen === 'function' && window.isUIWindowOpen(TERMINAL_WINDOW_ID)) {
renderTerminalLogs(latestAdminLogs);
}
@@ -466,21 +457,21 @@ async function changeName() {
revealBtn.addEventListener('click', () => adminAction('reveal'));
resetBtn.addEventListener('click', () => adminAction('reset'));
-terminalBtn.addEventListener('click', openTerminal);
-terminalCloseBtn.addEventListener('click', closeTerminal);
-terminalModalOverlay.addEventListener('click', (event) => {
- if (event.target === terminalModalOverlay) {
- closeTerminal();
- }
-});
shareAdminToggle.addEventListener('change', updateShareLink);
changeNameBtn.addEventListener('click', () => {
void changeName();
});
-window.addEventListener('keydown', (event) => {
- if (event.key === 'Escape') {
- closeTerminal();
+document.addEventListener('click', (event) => {
+ const openBtn = event.target.closest('[data-role="open-window"]');
+ if (!openBtn || openBtn.dataset.target !== TERMINAL_WINDOW_ID) {
+ return;
}
+
+ requestAnimationFrame(() => {
+ if (typeof window.isUIWindowOpen === 'function' && window.isUIWindowOpen(TERMINAL_WINDOW_ID)) {
+ renderTerminalLogs(latestAdminLogs);
+ }
+ });
});
joinForm.addEventListener('submit', async (event) => {
diff --git a/static/js/ui-controls.js b/static/js/ui-controls.js
index 58bc524..c1f2fdb 100644
--- a/static/js/ui-controls.js
+++ b/static/js/ui-controls.js
@@ -5,12 +5,11 @@
const DEFAULT_THEME = 'win98';
const MODE_ICON_LIGHT = '/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png';
const MODE_ICON_DARK = '/static/img/Windows Icons - PNG/desk.cpl_14_40-6.png';
- const DEFAULT_WINDOW_LAYOUTS = {
- 'theme-tool-window': { left: 16, top: 88, width: 390, height: 250 },
- 'mode-tool-window': { left: 424, top: 88, width: 340, height: 190 },
- };
+
let floatingWindowZ = 80;
let windowLayouts = {};
+ let windowDefs = [];
+ let accessState = { admin: false };
function isMobileViewport() {
return window.matchMedia('(max-width: 899px)').matches;
@@ -29,58 +28,37 @@
document.documentElement.removeAttribute('data-theme');
}
+ function getCurrentTheme() {
+ return document.documentElement.getAttribute('data-ui-theme') || DEFAULT_THEME;
+ }
+
function getCurrentMode() {
return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
}
- function isWindowOpen(id) {
- const win = document.getElementById(id);
- return Boolean(win && !win.classList.contains('hidden'));
- }
-
- function syncTaskButtons() {
- document.querySelectorAll('[data-role="open-window"]').forEach((button) => {
- const target = button.dataset.target;
- button.classList.toggle('is-active', isWindowOpen(target));
- });
- }
-
- function syncControls() {
- const theme = document.documentElement.getAttribute('data-ui-theme') || DEFAULT_THEME;
- const mode = getCurrentMode();
- const modeLabel = mode === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode';
- const modeIcon = mode === 'dark' ? MODE_ICON_DARK : MODE_ICON_LIGHT;
-
- document.querySelectorAll('[data-role="theme-option"]').forEach((button) => {
- const selected = button.dataset.theme === theme;
- button.classList.toggle('is-selected', selected);
- button.setAttribute('aria-pressed', selected ? 'true' : 'false');
- });
-
- document.querySelectorAll('[data-role="mode-toggle-label"]').forEach((el) => {
- el.textContent = modeLabel;
- });
-
- document.querySelectorAll('[data-role="mode-icon"]').forEach((el) => {
- el.src = modeIcon;
- });
-
- document.querySelectorAll('#mode-status-text').forEach((el) => {
- el.textContent = `Current mode: ${mode === 'dark' ? 'Dark' : 'Light'}`;
- });
-
- syncTaskButtons();
- }
-
- function bringWindowToFront(windowEl) {
- floatingWindowZ += 1;
- windowEl.style.zIndex = String(floatingWindowZ);
- }
-
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
+ function parseJSON(value, fallback) {
+ if (!value) {
+ return fallback;
+ }
+ try {
+ return JSON.parse(value);
+ } catch (_err) {
+ return fallback;
+ }
+ }
+
+ function hasWindowAccess(def) {
+ const rights = def.rights || 'all';
+ if (rights === 'admin') {
+ return Boolean(accessState.admin);
+ }
+ return true;
+ }
+
function loadWindowLayouts() {
try {
const raw = localStorage.getItem(WINDOW_LAYOUTS_KEY);
@@ -88,10 +66,7 @@
return {};
}
const parsed = JSON.parse(raw);
- if (!parsed || typeof parsed !== 'object') {
- return {};
- }
- return parsed;
+ return parsed && typeof parsed === 'object' ? parsed : {};
} catch (_err) {
return {};
}
@@ -130,6 +105,7 @@
const height = clamp(layout.height, minHeight, maxHeight);
const left = clamp(layout.left, margin, Math.max(margin, window.innerWidth - width - margin));
const top = clamp(layout.top, margin, Math.max(margin, window.innerHeight - height - margin));
+
return { left, top, width, height };
}
@@ -153,94 +129,217 @@
};
}
+ function bringWindowToFront(windowEl) {
+ floatingWindowZ += 1;
+ windowEl.style.zIndex = String(floatingWindowZ);
+ }
+
+ function getDefaultLayout(el, index) {
+ const step = 28 * index;
+ return normalizeLayout({
+ left: Number(el.dataset.windowDefaultLeft) || 16 + step,
+ top: Number(el.dataset.windowDefaultTop) || 88 + step,
+ width: Number(el.dataset.windowDefaultWidth) || 360,
+ height: Number(el.dataset.windowDefaultHeight) || 220,
+ });
+ }
+
function persistWindowLayout(windowEl) {
const id = windowEl.id;
- if (!id) {
+ if (!id || isMobileViewport()) {
return;
}
- const next = clampLayoutToViewport(readLayoutFromDOM(windowEl));
- windowLayouts[id] = next;
+ windowLayouts[id] = clampLayoutToViewport(readLayoutFromDOM(windowEl));
saveWindowLayouts();
}
- function ensureWindowLayout(windowEl) {
- const id = windowEl.id;
- if (!id) {
- return;
- }
+ function ensureWindowLayout(def) {
if (isMobileViewport()) {
- windowEl.style.right = 'auto';
- windowEl.style.bottom = 'auto';
- windowEl.style.transform = 'none';
+ def.el.style.right = 'auto';
+ def.el.style.bottom = 'auto';
+ def.el.style.transform = 'none';
return;
}
- const defaults = DEFAULT_WINDOW_LAYOUTS[id];
- const saved = windowLayouts[id];
- const normalized = normalizeLayout(saved, defaults);
+
+ const normalized = normalizeLayout(windowLayouts[def.id], def.defaultLayout);
const clamped = clampLayoutToViewport(normalized);
- applyLayout(windowEl, clamped);
- windowLayouts[id] = clamped;
+ applyLayout(def.el, clamped);
+ windowLayouts[def.id] = clamped;
}
- function openToolWindow(id) {
- const windowEl = document.getElementById(id);
- if (!windowEl) {
+ function isWindowOpen(id) {
+ const def = windowDefs.find((item) => item.id === id);
+ return Boolean(def && !def.el.classList.contains('hidden'));
+ }
+
+ function closeWindowById(id) {
+ const def = windowDefs.find((item) => item.id === id);
+ if (!def) {
return;
}
- ensureWindowLayout(windowEl);
- windowEl.classList.remove('hidden');
- bringWindowToFront(windowEl);
+ def.el.classList.add('hidden');
syncTaskButtons();
}
- function closeToolWindow(id) {
- const windowEl = document.getElementById(id);
- if (!windowEl) {
+ function openWindowById(id) {
+ const def = windowDefs.find((item) => item.id === id);
+ if (!def || !hasWindowAccess(def)) {
return;
}
- windowEl.classList.add('hidden');
+ ensureWindowLayout(def);
+ def.el.classList.remove('hidden');
+ bringWindowToFront(def.el);
syncTaskButtons();
}
+ function resolveWindowIcon(def, theme) {
+ return def.icons[theme] || def.icons.default || '/static/img/Windows Icons - PNG/main.cpl_14_109-1.png';
+ }
+
+ function renderTaskbarButtons() {
+ const theme = getCurrentTheme();
+ const visibleDefs = windowDefs
+ .filter(hasWindowAccess)
+ .sort((a, b) => a.order - b.order);
+
+ const html = visibleDefs.map((def) => {
+ const icon = resolveWindowIcon(def, theme);
+ const safeTitle = def.title;
+ return [
+ `
',
+ ].join('');
+ }).join('');
+
+ document.querySelectorAll('[data-role="taskbar-program-list"]').forEach((container) => {
+ container.innerHTML = html;
+ });
+
+ syncTaskButtons();
+ }
+
+ function syncTaskButtons() {
+ document.querySelectorAll('[data-role="open-window"]').forEach((button) => {
+ const target = button.dataset.target;
+ button.classList.toggle('is-active', isWindowOpen(target));
+ });
+ }
+
+ function syncControls() {
+ const theme = getCurrentTheme();
+ const mode = getCurrentMode();
+ const modeLabel = mode === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode';
+ const modeIcon = mode === 'dark' ? MODE_ICON_DARK : MODE_ICON_LIGHT;
+
+ document.querySelectorAll('[data-role="theme-option"]').forEach((button) => {
+ const selected = button.dataset.theme === theme;
+ button.classList.toggle('is-selected', selected);
+ button.setAttribute('aria-pressed', selected ? 'true' : 'false');
+ });
+
+ document.querySelectorAll('[data-role="mode-toggle-label"]').forEach((el) => {
+ el.textContent = modeLabel;
+ });
+
+ document.querySelectorAll('[data-role="mode-icon"]').forEach((el) => {
+ el.src = modeIcon;
+ });
+
+ document.querySelectorAll('#mode-status-text').forEach((el) => {
+ el.textContent = `Current mode: ${mode === 'dark' ? 'Dark' : 'Light'}`;
+ });
+
+ renderTaskbarButtons();
+ }
+
+ function collectWindowDefinitions() {
+ const windows = Array.from(document.querySelectorAll('[data-ui-window]'));
+ windowDefs = windows.map((el, index) => ({
+ id: el.id,
+ el,
+ title: el.dataset.windowTitle || el.id,
+ rights: el.dataset.windowRights || 'all',
+ order: Number(el.dataset.windowOrder) || (index + 1) * 10,
+ icons: parseJSON(el.dataset.windowIcons, {}),
+ defaultLayout: getDefaultLayout(el, index),
+ })).filter((def) => Boolean(def.id));
+ }
+
+ function setWindowAccess(nextAccess) {
+ accessState = {
+ ...accessState,
+ ...(nextAccess || {}),
+ };
+
+ windowDefs.forEach((def) => {
+ if (!hasWindowAccess(def)) {
+ def.el.classList.add('hidden');
+ }
+ });
+
+ syncControls();
+ }
+
+ function initWindowLayouts() {
+ windowLayouts = loadWindowLayouts();
+
+ windowDefs.forEach((def) => {
+ ensureWindowLayout(def);
+ });
+
+ window.addEventListener('resize', () => {
+ if (isMobileViewport()) {
+ return;
+ }
+
+ windowDefs.forEach((def) => {
+ const normalized = normalizeLayout(windowLayouts[def.id], def.defaultLayout);
+ const clamped = clampLayoutToViewport(normalized);
+ applyLayout(def.el, clamped);
+ windowLayouts[def.id] = clamped;
+ });
+
+ saveWindowLayouts();
+ });
+ }
+
function initDraggableWindows() {
let dragState = null;
- document.querySelectorAll('[data-role="drag-handle"]').forEach((handle) => {
- handle.addEventListener('pointerdown', (event) => {
- if (event.button !== 0) {
- return;
- }
- if (event.target.closest('button')) {
- return;
- }
+ document.addEventListener('pointerdown', (event) => {
+ const handle = event.target.closest('[data-role="drag-handle"]');
+ if (!handle || event.button !== 0 || event.target.closest('button')) {
+ return;
+ }
+ if (isMobileViewport()) {
+ return;
+ }
- const windowEl = handle.closest('.ui-tool-window');
- if (!windowEl || windowEl.classList.contains('hidden')) {
- return;
- }
- if (isMobileViewport()) {
- return;
- }
+ const windowEl = handle.closest('.ui-tool-window[data-ui-window]');
+ if (!windowEl || windowEl.classList.contains('hidden')) {
+ return;
+ }
- bringWindowToFront(windowEl);
- const rect = windowEl.getBoundingClientRect();
- windowEl.style.left = `${rect.left}px`;
- windowEl.style.top = `${rect.top}px`;
- windowEl.style.right = 'auto';
- windowEl.style.bottom = 'auto';
- windowEl.style.transform = 'none';
+ bringWindowToFront(windowEl);
+ const rect = windowEl.getBoundingClientRect();
+ windowEl.style.left = `${rect.left}px`;
+ windowEl.style.top = `${rect.top}px`;
+ windowEl.style.right = 'auto';
+ windowEl.style.bottom = 'auto';
+ windowEl.style.transform = 'none';
- dragState = {
- pointerId: event.pointerId,
- windowEl,
- offsetX: event.clientX - rect.left,
- offsetY: event.clientY - rect.top,
- };
+ dragState = {
+ pointerId: event.pointerId,
+ windowEl,
+ offsetX: event.clientX - rect.left,
+ offsetY: event.clientY - rect.top,
+ };
- document.body.classList.add('is-dragging-window');
- handle.setPointerCapture(event.pointerId);
- event.preventDefault();
- });
+ document.body.classList.add('is-dragging-window');
+ handle.setPointerCapture(event.pointerId);
+ event.preventDefault();
});
window.addEventListener('pointermove', (event) => {
@@ -279,59 +378,64 @@
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const windowEl = entry.target;
- if (windowEl.classList.contains('hidden')) {
- return;
- }
- if (isMobileViewport()) {
+ if (windowEl.classList.contains('hidden') || isMobileViewport()) {
return;
}
persistWindowLayout(windowEl);
});
});
- document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
- observer.observe(windowEl);
- });
+ windowDefs.forEach((def) => observer.observe(def.el));
}
- function initWindowLayouts() {
- windowLayouts = loadWindowLayouts();
- document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
- ensureWindowLayout(windowEl);
- });
-
- window.addEventListener('resize', () => {
- if (isMobileViewport()) {
- return;
- }
- document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
- const id = windowEl.id;
- if (!id) {
+ function initTaskbarAndWindowEvents() {
+ document.addEventListener('click', (event) => {
+ const openBtn = event.target.closest('[data-role="open-window"]');
+ if (openBtn) {
+ const target = openBtn.dataset.target;
+ if (!target) {
return;
}
- const normalized = normalizeLayout(windowLayouts[id], DEFAULT_WINDOW_LAYOUTS[id]);
- const clamped = clampLayoutToViewport(normalized);
- applyLayout(windowEl, clamped);
- windowLayouts[id] = clamped;
+ if (isWindowOpen(target)) {
+ closeWindowById(target);
+ } else {
+ openWindowById(target);
+ }
+ return;
+ }
+
+ const closeBtn = event.target.closest('[data-role="close-window"]');
+ if (!closeBtn) {
+ return;
+ }
+
+ const target = closeBtn.dataset.target || closeBtn.closest('.ui-tool-window')?.id;
+ if (target) {
+ closeWindowById(target);
+ }
+ });
+
+ windowDefs.forEach((def) => {
+ def.el.addEventListener('pointerdown', () => {
+ if (def.el.classList.contains('hidden')) {
+ return;
+ }
+ bringWindowToFront(def.el);
});
- saveWindowLayouts();
+ });
+
+ window.addEventListener('keydown', (event) => {
+ if (event.key !== 'Escape') {
+ return;
+ }
+ windowDefs.forEach((def) => {
+ def.el.classList.add('hidden');
+ });
+ syncTaskButtons();
});
}
- function initUIControls() {
- if (window.__uiControlsInitialized) {
- syncControls();
- return;
- }
- window.__uiControlsInitialized = true;
-
- const savedTheme = localStorage.getItem(THEME_KEY) || DEFAULT_THEME;
- const savedMode = localStorage.getItem(MODE_KEY) || 'light';
- applyTheme(savedTheme);
- applyMode(savedMode);
- initWindowLayouts();
- syncControls();
-
+ function initThemeAndModeHandlers() {
document.querySelectorAll('[data-role="theme-option"]').forEach((button) => {
button.addEventListener('click', () => {
const value = button.dataset.theme || DEFAULT_THEME;
@@ -349,58 +453,34 @@
syncControls();
});
});
+ }
- document.querySelectorAll('[data-role="open-window"]').forEach((button) => {
- button.addEventListener('click', () => {
- const target = button.dataset.target;
- if (!target) {
- return;
- }
- if (isWindowOpen(target)) {
- closeToolWindow(target);
- return;
- }
- openToolWindow(target);
- });
- });
+ function initUIControls() {
+ if (window.__uiControlsInitialized) {
+ syncControls();
+ return;
+ }
+ window.__uiControlsInitialized = true;
- document.querySelectorAll('[data-role="close-window"]').forEach((button) => {
- button.addEventListener('click', () => {
- const target = button.dataset.target;
- if (!target) {
- return;
- }
- closeToolWindow(target);
- });
- });
-
- document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
- windowEl.addEventListener('pointerdown', () => {
- if (windowEl.classList.contains('hidden')) {
- return;
- }
- bringWindowToFront(windowEl);
- });
- });
-
- window.addEventListener('keydown', (event) => {
- if (event.key !== 'Escape') {
- return;
- }
- document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
- if (windowEl.classList.contains('hidden')) {
- return;
- }
- windowEl.classList.add('hidden');
- });
- syncTaskButtons();
- });
+ const savedTheme = localStorage.getItem(THEME_KEY) || DEFAULT_THEME;
+ const savedMode = localStorage.getItem(MODE_KEY) || 'light';
+ applyTheme(savedTheme);
+ applyMode(savedMode);
+ collectWindowDefinitions();
+ initWindowLayouts();
+ initTaskbarAndWindowEvents();
+ initThemeAndModeHandlers();
initDraggableWindows();
initResizableWindows();
+ syncControls();
}
window.initUIControls = initUIControls;
+ window.isUIWindowOpen = isWindowOpen;
+ window.openUIWindow = openWindowById;
+ window.closeUIWindow = closeWindowById;
+ window.setUIWindowAccess = setWindowAccess;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initUIControls, { once: true });