Windows
This commit is contained in:
@@ -17,16 +17,7 @@
|
|||||||
<body data-page="room" data-room-id="{{ .RoomID }}" class="prejoin">
|
<body data-page="room" data-room-id="{{ .RoomID }}" class="prejoin">
|
||||||
<div class="mobile-control-strip">
|
<div class="mobile-control-strip">
|
||||||
<div class="taskbar-shell">
|
<div class="taskbar-shell">
|
||||||
<div class="taskbar-program-list">
|
<div class="taskbar-program-list" data-role="taskbar-program-list"></div>
|
||||||
<button class="taskbar-program-btn" type="button" data-role="open-window" data-target="theme-tool-window" aria-label="Open theme picker">
|
|
||||||
<img class="taskbar-icon" src="/static/img/Windows Icons - PNG/main.cpl_14_109-1.png" alt="">
|
|
||||||
<span>ThemePicker.exe</span>
|
|
||||||
</button>
|
|
||||||
<button class="taskbar-program-btn" type="button" data-role="open-window" data-target="mode-tool-window" aria-label="Open display mode settings">
|
|
||||||
<img class="taskbar-icon" data-role="mode-icon" src="/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png" alt="">
|
|
||||||
<span>DisplayMode.exe</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -124,7 +115,6 @@
|
|||||||
<div id="admin-controls" class="admin-controls hidden">
|
<div id="admin-controls" class="admin-controls hidden">
|
||||||
<button type="button" id="reveal-btn" class="btn">Reveal</button>
|
<button type="button" id="reveal-btn" class="btn">Reveal</button>
|
||||||
<button type="button" id="reset-btn" class="btn">Reset</button>
|
<button type="button" id="reset-btn" class="btn">Reset</button>
|
||||||
<button type="button" id="terminal-btn" class="btn">Terminal</button>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="room-message" class="status-line">Waiting for join...</p>
|
<p id="room-message" class="status-line">Waiting for join...</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,21 +155,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div id="terminal-modal-overlay" class="terminal-modal-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="terminal-title">
|
<section
|
||||||
<section class="window terminal-window">
|
id="theme-tool-window"
|
||||||
<div class="title-bar">
|
class="window ui-tool-window hidden"
|
||||||
<span id="terminal-title">RoomTerminal.exe</span>
|
role="dialog"
|
||||||
<div class="title-bar-controls">
|
aria-modal="false"
|
||||||
<button type="button" id="terminal-close-btn">×</button>
|
aria-labelledby="theme-tool-title"
|
||||||
</div>
|
data-ui-window
|
||||||
</div>
|
data-window-title="ThemePicker.exe"
|
||||||
<div class="window-content terminal-window-content">
|
data-window-rights="all"
|
||||||
<div id="terminal-log-output" class="terminal-log-output" aria-live="polite"></div>
|
data-window-order="10"
|
||||||
</div>
|
data-window-default-left="16"
|
||||||
</section>
|
data-window-default-top="88"
|
||||||
</div>
|
data-window-default-width="390"
|
||||||
|
data-window-default-height="250"
|
||||||
<section id="theme-tool-window" class="window ui-tool-window hidden" role="dialog" aria-modal="false" aria-labelledby="theme-tool-title">
|
data-window-icons='{"win98":"/static/img/Windows Icons - PNG/main.cpl_14_109-1.png","modern":"/static/img/Windows Icons - PNG/msconfig.exe_14_128-0.png","none":"/static/img/Windows Icons - PNG/taskmgr.exe_14_118-1.png","default":"/static/img/Windows Icons - PNG/main.cpl_14_109-1.png"}'
|
||||||
|
>
|
||||||
<div class="title-bar ui-tool-title-bar" data-role="drag-handle">
|
<div class="title-bar ui-tool-title-bar" data-role="drag-handle">
|
||||||
<span id="theme-tool-title">ThemePicker.exe</span>
|
<span id="theme-tool-title">ThemePicker.exe</span>
|
||||||
<div class="title-bar-controls">
|
<div class="title-bar-controls">
|
||||||
@@ -205,7 +196,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="mode-tool-window" class="window ui-tool-window hidden" role="dialog" aria-modal="false" aria-labelledby="mode-tool-title">
|
<section
|
||||||
|
id="mode-tool-window"
|
||||||
|
class="window ui-tool-window hidden"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="false"
|
||||||
|
aria-labelledby="mode-tool-title"
|
||||||
|
data-ui-window
|
||||||
|
data-window-title="DisplayMode.exe"
|
||||||
|
data-window-rights="all"
|
||||||
|
data-window-order="20"
|
||||||
|
data-window-default-left="424"
|
||||||
|
data-window-default-top="88"
|
||||||
|
data-window-default-width="340"
|
||||||
|
data-window-default-height="190"
|
||||||
|
data-window-icons='{"win98":"/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png","modern":"/static/img/Windows Icons - PNG/desk.cpl_14_100-0.png","none":"/static/img/Windows Icons - PNG/timedate.cpl_14_200-6.png","default":"/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png"}'
|
||||||
|
>
|
||||||
<div class="title-bar ui-tool-title-bar" data-role="drag-handle">
|
<div class="title-bar ui-tool-title-bar" data-role="drag-handle">
|
||||||
<span id="mode-tool-title">DisplayMode.exe</span>
|
<span id="mode-tool-title">DisplayMode.exe</span>
|
||||||
<div class="title-bar-controls">
|
<div class="title-bar-controls">
|
||||||
@@ -220,20 +226,38 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
id="terminal-tool-window"
|
||||||
|
class="window ui-tool-window terminal-window hidden"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="false"
|
||||||
|
aria-labelledby="terminal-title"
|
||||||
|
data-ui-window
|
||||||
|
data-window-title="RoomTerminal.exe"
|
||||||
|
data-window-rights="admin"
|
||||||
|
data-window-order="30"
|
||||||
|
data-window-default-left="780"
|
||||||
|
data-window-default-top="88"
|
||||||
|
data-window-default-width="500"
|
||||||
|
data-window-default-height="350"
|
||||||
|
data-window-icons='{"win98":"/static/img/Windows Icons - PNG/taskmgr.exe_14_107-1.png","modern":"/static/img/Windows Icons - PNG/taskmgr.exe_14_137.png","none":"/static/img/Windows Icons - PNG/taskmgr.exe_14_118-1.png","default":"/static/img/Windows Icons - PNG/taskmgr.exe_14_107-1.png"}'
|
||||||
|
>
|
||||||
|
<div class="title-bar ui-tool-title-bar" data-role="drag-handle">
|
||||||
|
<span id="terminal-title">RoomTerminal.exe</span>
|
||||||
|
<div class="title-bar-controls">
|
||||||
|
<button type="button" data-role="close-window" data-target="terminal-tool-window">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="window-content terminal-window-content">
|
||||||
|
<div id="terminal-log-output" class="terminal-log-output" aria-live="polite"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="taskbar desktop-taskbar" aria-label="Desktop taskbar">
|
<footer class="taskbar desktop-taskbar" aria-label="Desktop taskbar">
|
||||||
<div class="taskbar-shell">
|
<div class="taskbar-shell">
|
||||||
<div class="taskbar-program-list">
|
<div class="taskbar-program-list" data-role="taskbar-program-list"></div>
|
||||||
<button class="taskbar-program-btn" type="button" data-role="open-window" data-target="theme-tool-window" aria-label="Open theme picker">
|
|
||||||
<img class="taskbar-icon" src="/static/img/Windows Icons - PNG/main.cpl_14_109-1.png" alt="">
|
|
||||||
<span>ThemePicker.exe</span>
|
|
||||||
</button>
|
|
||||||
<button class="taskbar-program-btn" type="button" data-role="open-window" data-target="mode-tool-window" aria-label="Open display mode settings">
|
|
||||||
<img class="taskbar-icon" data-role="mode-icon" src="/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png" alt="">
|
|
||||||
<span>DisplayMode.exe</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -499,15 +499,6 @@ body.is-dragging-window .ui-tool-title-bar {
|
|||||||
width: min(27rem, 92vw);
|
width: min(27rem, 92vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 72;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-window {
|
.terminal-window {
|
||||||
width: min(46rem, 94vw);
|
width: min(46rem, 94vw);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,13 @@ const participantList = document.getElementById('participant-list');
|
|||||||
const adminControls = document.getElementById('admin-controls');
|
const adminControls = document.getElementById('admin-controls');
|
||||||
const revealBtn = document.getElementById('reveal-btn');
|
const revealBtn = document.getElementById('reveal-btn');
|
||||||
const resetBtn = document.getElementById('reset-btn');
|
const resetBtn = document.getElementById('reset-btn');
|
||||||
const terminalBtn = document.getElementById('terminal-btn');
|
|
||||||
const shareLinkInput = document.getElementById('share-link');
|
const shareLinkInput = document.getElementById('share-link');
|
||||||
const shareAdminToggle = document.getElementById('share-admin-toggle');
|
const shareAdminToggle = document.getElementById('share-admin-toggle');
|
||||||
const votesCounter = document.getElementById('votes-counter');
|
const votesCounter = document.getElementById('votes-counter');
|
||||||
const roomMessage = document.getElementById('room-message');
|
const roomMessage = document.getElementById('room-message');
|
||||||
const changeNameBtn = document.getElementById('change-name-btn');
|
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 terminalLogOutput = document.getElementById('terminal-log-output');
|
||||||
|
const TERMINAL_WINDOW_ID = 'terminal-tool-window';
|
||||||
|
|
||||||
const joinPanel = document.getElementById('join-panel');
|
const joinPanel = document.getElementById('join-panel');
|
||||||
const joinForm = document.getElementById('join-form');
|
const joinForm = document.getElementById('join-form');
|
||||||
@@ -315,15 +313,6 @@ function renderTerminalLogs(logs) {
|
|||||||
terminalLogOutput.scrollTop = terminalLogOutput.scrollHeight;
|
terminalLogOutput.scrollTop = terminalLogOutput.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTerminal() {
|
|
||||||
terminalModalOverlay.classList.remove('hidden');
|
|
||||||
renderTerminalLogs(latestAdminLogs);
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeTerminal() {
|
|
||||||
terminalModalOverlay.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderState(state) {
|
function renderState(state) {
|
||||||
roomTitle.textContent = `${state.roomName} (${state.roomId})`;
|
roomTitle.textContent = `${state.roomName} (${state.roomId})`;
|
||||||
revealModeLabel.textContent = `Reveal mode: ${state.revealMode}`;
|
revealModeLabel.textContent = `Reveal mode: ${state.revealMode}`;
|
||||||
@@ -341,16 +330,18 @@ function renderState(state) {
|
|||||||
latestLinks = state.links || { participantLink: '', adminLink: '' };
|
latestLinks = state.links || { participantLink: '', adminLink: '' };
|
||||||
updateShareLink();
|
updateShareLink();
|
||||||
|
|
||||||
|
if (typeof window.setUIWindowAccess === 'function') {
|
||||||
|
window.setUIWindowAccess({ admin: state.viewerIsAdmin });
|
||||||
|
}
|
||||||
|
|
||||||
if (state.viewerIsAdmin) {
|
if (state.viewerIsAdmin) {
|
||||||
adminControls.classList.remove('hidden');
|
adminControls.classList.remove('hidden');
|
||||||
terminalBtn.classList.remove('hidden');
|
|
||||||
} else {
|
} else {
|
||||||
adminControls.classList.add('hidden');
|
adminControls.classList.add('hidden');
|
||||||
terminalBtn.classList.add('hidden');
|
|
||||||
closeTerminal();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
latestAdminLogs = Array.isArray(state.adminLogs) ? state.adminLogs : [];
|
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);
|
renderTerminalLogs(latestAdminLogs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,21 +457,21 @@ async function changeName() {
|
|||||||
|
|
||||||
revealBtn.addEventListener('click', () => adminAction('reveal'));
|
revealBtn.addEventListener('click', () => adminAction('reveal'));
|
||||||
resetBtn.addEventListener('click', () => adminAction('reset'));
|
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);
|
shareAdminToggle.addEventListener('change', updateShareLink);
|
||||||
changeNameBtn.addEventListener('click', () => {
|
changeNameBtn.addEventListener('click', () => {
|
||||||
void changeName();
|
void changeName();
|
||||||
});
|
});
|
||||||
window.addEventListener('keydown', (event) => {
|
document.addEventListener('click', (event) => {
|
||||||
if (event.key === 'Escape') {
|
const openBtn = event.target.closest('[data-role="open-window"]');
|
||||||
closeTerminal();
|
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) => {
|
joinForm.addEventListener('submit', async (event) => {
|
||||||
|
|||||||
@@ -5,12 +5,11 @@
|
|||||||
const DEFAULT_THEME = 'win98';
|
const DEFAULT_THEME = 'win98';
|
||||||
const MODE_ICON_LIGHT = '/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png';
|
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 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 floatingWindowZ = 80;
|
||||||
let windowLayouts = {};
|
let windowLayouts = {};
|
||||||
|
let windowDefs = [];
|
||||||
|
let accessState = { admin: false };
|
||||||
|
|
||||||
function isMobileViewport() {
|
function isMobileViewport() {
|
||||||
return window.matchMedia('(max-width: 899px)').matches;
|
return window.matchMedia('(max-width: 899px)').matches;
|
||||||
@@ -29,58 +28,37 @@
|
|||||||
document.documentElement.removeAttribute('data-theme');
|
document.documentElement.removeAttribute('data-theme');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentTheme() {
|
||||||
|
return document.documentElement.getAttribute('data-ui-theme') || DEFAULT_THEME;
|
||||||
|
}
|
||||||
|
|
||||||
function getCurrentMode() {
|
function getCurrentMode() {
|
||||||
return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
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) {
|
function clamp(value, min, max) {
|
||||||
return Math.min(max, Math.max(min, value));
|
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() {
|
function loadWindowLayouts() {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(WINDOW_LAYOUTS_KEY);
|
const raw = localStorage.getItem(WINDOW_LAYOUTS_KEY);
|
||||||
@@ -88,10 +66,7 @@
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
if (!parsed || typeof parsed !== 'object') {
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -130,6 +105,7 @@
|
|||||||
const height = clamp(layout.height, minHeight, maxHeight);
|
const height = clamp(layout.height, minHeight, maxHeight);
|
||||||
const left = clamp(layout.left, margin, Math.max(margin, window.innerWidth - width - margin));
|
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));
|
const top = clamp(layout.top, margin, Math.max(margin, window.innerHeight - height - margin));
|
||||||
|
|
||||||
return { left, top, width, height };
|
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) {
|
function persistWindowLayout(windowEl) {
|
||||||
const id = windowEl.id;
|
const id = windowEl.id;
|
||||||
if (!id) {
|
if (!id || isMobileViewport()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const next = clampLayoutToViewport(readLayoutFromDOM(windowEl));
|
windowLayouts[id] = clampLayoutToViewport(readLayoutFromDOM(windowEl));
|
||||||
windowLayouts[id] = next;
|
|
||||||
saveWindowLayouts();
|
saveWindowLayouts();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureWindowLayout(windowEl) {
|
function ensureWindowLayout(def) {
|
||||||
const id = windowEl.id;
|
|
||||||
if (!id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isMobileViewport()) {
|
if (isMobileViewport()) {
|
||||||
windowEl.style.right = 'auto';
|
def.el.style.right = 'auto';
|
||||||
windowEl.style.bottom = 'auto';
|
def.el.style.bottom = 'auto';
|
||||||
windowEl.style.transform = 'none';
|
def.el.style.transform = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const defaults = DEFAULT_WINDOW_LAYOUTS[id];
|
|
||||||
const saved = windowLayouts[id];
|
const normalized = normalizeLayout(windowLayouts[def.id], def.defaultLayout);
|
||||||
const normalized = normalizeLayout(saved, defaults);
|
|
||||||
const clamped = clampLayoutToViewport(normalized);
|
const clamped = clampLayoutToViewport(normalized);
|
||||||
applyLayout(windowEl, clamped);
|
applyLayout(def.el, clamped);
|
||||||
windowLayouts[id] = clamped;
|
windowLayouts[def.id] = clamped;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openToolWindow(id) {
|
function isWindowOpen(id) {
|
||||||
const windowEl = document.getElementById(id);
|
const def = windowDefs.find((item) => item.id === id);
|
||||||
if (!windowEl) {
|
return Boolean(def && !def.el.classList.contains('hidden'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWindowById(id) {
|
||||||
|
const def = windowDefs.find((item) => item.id === id);
|
||||||
|
if (!def) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ensureWindowLayout(windowEl);
|
def.el.classList.add('hidden');
|
||||||
windowEl.classList.remove('hidden');
|
|
||||||
bringWindowToFront(windowEl);
|
|
||||||
syncTaskButtons();
|
syncTaskButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeToolWindow(id) {
|
function openWindowById(id) {
|
||||||
const windowEl = document.getElementById(id);
|
const def = windowDefs.find((item) => item.id === id);
|
||||||
if (!windowEl) {
|
if (!def || !hasWindowAccess(def)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
windowEl.classList.add('hidden');
|
ensureWindowLayout(def);
|
||||||
|
def.el.classList.remove('hidden');
|
||||||
|
bringWindowToFront(def.el);
|
||||||
syncTaskButtons();
|
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 [
|
||||||
|
`<button class="taskbar-program-btn" type="button" data-role="open-window" data-target="${def.id}" aria-label="Open ${safeTitle}">`,
|
||||||
|
`<img class="taskbar-icon" src="${icon}" alt="">`,
|
||||||
|
`<span>${safeTitle}</span>`,
|
||||||
|
'</button>',
|
||||||
|
].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() {
|
function initDraggableWindows() {
|
||||||
let dragState = null;
|
let dragState = null;
|
||||||
|
|
||||||
document.querySelectorAll('[data-role="drag-handle"]').forEach((handle) => {
|
document.addEventListener('pointerdown', (event) => {
|
||||||
handle.addEventListener('pointerdown', (event) => {
|
const handle = event.target.closest('[data-role="drag-handle"]');
|
||||||
if (event.button !== 0) {
|
if (!handle || event.button !== 0 || event.target.closest('button')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.target.closest('button')) {
|
if (isMobileViewport()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const windowEl = handle.closest('.ui-tool-window');
|
const windowEl = handle.closest('.ui-tool-window[data-ui-window]');
|
||||||
if (!windowEl || windowEl.classList.contains('hidden')) {
|
if (!windowEl || windowEl.classList.contains('hidden')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isMobileViewport()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bringWindowToFront(windowEl);
|
bringWindowToFront(windowEl);
|
||||||
const rect = windowEl.getBoundingClientRect();
|
const rect = windowEl.getBoundingClientRect();
|
||||||
windowEl.style.left = `${rect.left}px`;
|
windowEl.style.left = `${rect.left}px`;
|
||||||
windowEl.style.top = `${rect.top}px`;
|
windowEl.style.top = `${rect.top}px`;
|
||||||
windowEl.style.right = 'auto';
|
windowEl.style.right = 'auto';
|
||||||
windowEl.style.bottom = 'auto';
|
windowEl.style.bottom = 'auto';
|
||||||
windowEl.style.transform = 'none';
|
windowEl.style.transform = 'none';
|
||||||
|
|
||||||
dragState = {
|
dragState = {
|
||||||
pointerId: event.pointerId,
|
pointerId: event.pointerId,
|
||||||
windowEl,
|
windowEl,
|
||||||
offsetX: event.clientX - rect.left,
|
offsetX: event.clientX - rect.left,
|
||||||
offsetY: event.clientY - rect.top,
|
offsetY: event.clientY - rect.top,
|
||||||
};
|
};
|
||||||
|
|
||||||
document.body.classList.add('is-dragging-window');
|
document.body.classList.add('is-dragging-window');
|
||||||
handle.setPointerCapture(event.pointerId);
|
handle.setPointerCapture(event.pointerId);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('pointermove', (event) => {
|
window.addEventListener('pointermove', (event) => {
|
||||||
@@ -279,59 +378,64 @@
|
|||||||
const observer = new ResizeObserver((entries) => {
|
const observer = new ResizeObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
const windowEl = entry.target;
|
const windowEl = entry.target;
|
||||||
if (windowEl.classList.contains('hidden')) {
|
if (windowEl.classList.contains('hidden') || isMobileViewport()) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isMobileViewport()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
persistWindowLayout(windowEl);
|
persistWindowLayout(windowEl);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
|
windowDefs.forEach((def) => observer.observe(def.el));
|
||||||
observer.observe(windowEl);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initWindowLayouts() {
|
function initTaskbarAndWindowEvents() {
|
||||||
windowLayouts = loadWindowLayouts();
|
document.addEventListener('click', (event) => {
|
||||||
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
|
const openBtn = event.target.closest('[data-role="open-window"]');
|
||||||
ensureWindowLayout(windowEl);
|
if (openBtn) {
|
||||||
});
|
const target = openBtn.dataset.target;
|
||||||
|
if (!target) {
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
if (isMobileViewport()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
|
|
||||||
const id = windowEl.id;
|
|
||||||
if (!id) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const normalized = normalizeLayout(windowLayouts[id], DEFAULT_WINDOW_LAYOUTS[id]);
|
if (isWindowOpen(target)) {
|
||||||
const clamped = clampLayoutToViewport(normalized);
|
closeWindowById(target);
|
||||||
applyLayout(windowEl, clamped);
|
} else {
|
||||||
windowLayouts[id] = clamped;
|
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() {
|
function initThemeAndModeHandlers() {
|
||||||
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();
|
|
||||||
|
|
||||||
document.querySelectorAll('[data-role="theme-option"]').forEach((button) => {
|
document.querySelectorAll('[data-role="theme-option"]').forEach((button) => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
const value = button.dataset.theme || DEFAULT_THEME;
|
const value = button.dataset.theme || DEFAULT_THEME;
|
||||||
@@ -349,58 +453,34 @@
|
|||||||
syncControls();
|
syncControls();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll('[data-role="open-window"]').forEach((button) => {
|
function initUIControls() {
|
||||||
button.addEventListener('click', () => {
|
if (window.__uiControlsInitialized) {
|
||||||
const target = button.dataset.target;
|
syncControls();
|
||||||
if (!target) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
window.__uiControlsInitialized = true;
|
||||||
if (isWindowOpen(target)) {
|
|
||||||
closeToolWindow(target);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
openToolWindow(target);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('[data-role="close-window"]').forEach((button) => {
|
const savedTheme = localStorage.getItem(THEME_KEY) || DEFAULT_THEME;
|
||||||
button.addEventListener('click', () => {
|
const savedMode = localStorage.getItem(MODE_KEY) || 'light';
|
||||||
const target = button.dataset.target;
|
applyTheme(savedTheme);
|
||||||
if (!target) {
|
applyMode(savedMode);
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
collectWindowDefinitions();
|
||||||
|
initWindowLayouts();
|
||||||
|
initTaskbarAndWindowEvents();
|
||||||
|
initThemeAndModeHandlers();
|
||||||
initDraggableWindows();
|
initDraggableWindows();
|
||||||
initResizableWindows();
|
initResizableWindows();
|
||||||
|
syncControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.initUIControls = initUIControls;
|
window.initUIControls = initUIControls;
|
||||||
|
window.isUIWindowOpen = isWindowOpen;
|
||||||
|
window.openUIWindow = openWindowById;
|
||||||
|
window.closeUIWindow = closeWindowById;
|
||||||
|
window.setUIWindowAccess = setWindowAccess;
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initUIControls, { once: true });
|
document.addEventListener('DOMContentLoaded', initUIControls, { once: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user