const ICON_APPS = { mycomputer: { title: "My Computer", type: "explorer", startPath: "/", }, notepad: { title: "Notepad", type: "notepad", }, paint: { title: "Paint", type: "paint", }, ie: { title: "Internet Explorer", type: "ie", }, recycle: { title: "Recycle Bin", type: "explorer", startPath: "/Recycle Bin", }, }; const RETRO_WEB_HOME = 'home://portal'; let retroWebIndexPromise = null; let retroWebRoutes = new Map(); async function loadRetroWebIndex() { if (retroWebIndexPromise) return retroWebIndexPromise; retroWebIndexPromise = fetch('/fakeweb/index.json', { cache: 'no-store' }) .then((res) => (res.ok ? res.json() : { pages: [] })) .catch(() => ({ pages: [] })) .then((data) => { const pages = Array.isArray(data.pages) ? data.pages : []; retroWebRoutes = new Map( pages .filter((p) => p && p.route && p.file) .map((p) => [String(p.route).toLowerCase(), p]), ); return pages; }); return retroWebIndexPromise; } function retroRouteKey(url) { return String(url || '').trim().toLowerCase(); } function normalizeRetroUrl(raw) { if (!raw) return RETRO_WEB_HOME; const text = String(raw).trim(); if (!text) return RETRO_WEB_HOME; if (text.startsWith('home://') || text.startsWith('site://') || text.startsWith('search://')) { return text; } if (/^https?:\/\//i.test(text)) { return `search://${encodeURIComponent(text)}`; } if (text.includes(' ')) { return `search://${encodeURIComponent(text)}`; } if (text.includes('.')) { return `site://${text.toLowerCase()}`; } return `search://${encodeURIComponent(text)}`; } function buildRetroSearchPage(query, pages) { const q = decodeURIComponent(query || '').trim() || 'empty search'; const needle = q.toLowerCase(); const matches = pages .filter((p) => { const hay = `${p.route || ''} ${p.title || ''} ${(p.tags || []).join(' ')}`.toLowerCase(); return hay.includes(needle); }) .slice(0, 12) .map((p) => `
  • ${p.title || p.route}
    ${p.route}
  • `) .join(''); return { title: `Search results for "${q}"`, html: `

    RetroSearch 2000

    Results for: ${q}

      ${matches || '
    1. No direct matches. Try home://portal
    2. '}
    `, }; } async function getRetroPage(url) { const pages = await loadRetroWebIndex(); if (url.startsWith('search://')) { return buildRetroSearchPage(url.slice('search://'.length), pages); } const entry = retroWebRoutes.get(retroRouteKey(url)); if (!entry) return null; try { const res = await fetch(entry.file, { cache: 'no-store' }); if (!res.ok) return null; const html = await res.text(); return { title: entry.title || entry.route, html, route: entry.route, }; } catch { return null; } } function htmlEscape(text) { return String(text || '').replace(/[&<>"']/g, (m) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[m])); } const FILE_SYSTEM = { type: 'folder', name: 'My Computer', children: [ { type: 'folder', name: 'C:', children: [ { type: 'folder', name: 'Program Files', children: [ { type: 'file', name: 'readme.txt', fileType: 'txt', content: 'Welcome to veryextraOS 1.0\n\nThis is a mock file system.' }, ]}, { type: 'folder', name: 'WINDOWS', children: [ { type: 'file', name: 'win.ini', fileType: 'txt', content: '[fonts]\n[extensions]\n[desktop]' }, ]}, { type: 'folder', name: 'Documents and Settings', children: [ { type: 'folder', name: 'Dion', children: [ { type: 'folder', name: 'My Documents', children: [ { type: 'file', name: 'shopping-list.txt', fileType: 'txt', content: 'milk\ncoffee\nstickers' }, { type: 'file', name: 'desktop-ideas.txt', fileType: 'txt', content: 'Make everything cuter\nAdd sounds\nAdd themes' }, ]}, { type: 'folder', name: 'My Pictures', children: [ { type: 'file', name: 'wallpaper-preview.jpg', fileType: 'image', content: 'https://wallpaperaccess.com/full/4810999.jpg' }, { type: 'file', name: 'pink-theme.png', fileType: 'image', content: 'https://picsum.photos/seed/pinktheme/720/450' }, ]}, ]}, ]}, ], }, { type: 'folder', name: 'D:', children: [ { type: 'folder', name: 'Music', children: [ { type: 'file', name: 'favorites.txt', fileType: 'txt', content: 'Track 01\nTrack 02\nTrack 03' }, { type: 'file', name: 'violin.mp3', fileType: 'audio', content: '/assets/audio/violin.mp3' }, ]}, ], }, { type: 'folder', name: 'Recycle Bin', children: [ { type: 'file', name: 'old-theme.zip', fileType: 'txt', content: 'Archive is empty.' }, { type: 'file', name: 'notes (1).txt', fileType: 'txt', content: 'Draft notes moved to recycle bin.' }, ], }, ], }; const statusEl = document.getElementById('status'); const clockEl = document.getElementById('clock'); const iconsEl = document.getElementById('icons'); const iconButtons = Array.from(document.querySelectorAll('.desktop-icon')); const resetLayoutBtn = document.getElementById('reset-layout-btn'); const startBtn = document.getElementById('start-btn'); const taskbarWindowsEl = document.getElementById('taskbar-windows'); const showDesktopBtn = document.getElementById('show-desktop-btn'); const windowTemplate = document.getElementById('window-template'); const ICON_LAYOUT_KEY = 'veryextraOS.iconLayout.v1'; const selectedIcons = new Set(); const windowsById = new Map(); const startMenuEl = document.getElementById('start-menu'); let selectionBoxEl = null; let topWindowZ = 1000; let windowSeq = 0; function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function setStatus(text) { if (statusEl) statusEl.textContent = text; } let contextMenuEl = null; function ensureContextMenu() { if (contextMenuEl) return contextMenuEl; contextMenuEl = document.createElement('div'); contextMenuEl.id = 'context-menu'; contextMenuEl.className = 'hidden'; contextMenuEl.setAttribute('role', 'menu'); contextMenuEl.addEventListener('click', (e) => e.stopPropagation()); document.body.appendChild(contextMenuEl); return contextMenuEl; } function hideContextMenu() { if (!contextMenuEl) return; contextMenuEl.classList.add('hidden'); contextMenuEl.innerHTML = ''; } function showContextMenu(x, y, items = []) { if (!items.length) { hideContextMenu(); return; } const menu = ensureContextMenu(); menu.innerHTML = ''; items.forEach((item) => { if (item === 'separator') { const sep = document.createElement('div'); sep.className = 'context-menu-sep'; menu.appendChild(sep); return; } const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'context-menu-item'; btn.textContent = item.label || 'Action'; btn.disabled = Boolean(item.disabled); btn.setAttribute('role', 'menuitem'); btn.addEventListener('click', () => { hideContextMenu(); item.action?.(); }); menu.appendChild(btn); }); menu.classList.remove('hidden'); menu.style.left = '0px'; menu.style.top = '0px'; const pad = 6; const menuRect = menu.getBoundingClientRect(); const maxLeft = Math.max(pad, window.innerWidth - menuRect.width - pad); const maxTop = Math.max(pad, window.innerHeight - menuRect.height - pad); menu.style.left = `${clamp(x, pad, maxLeft)}px`; menu.style.top = `${clamp(y, pad, maxTop)}px`; } function openPropertiesWindow(title, rows = []) { const htmlRows = rows .map((row) => `
    ${htmlEscape(row.label)}: ${htmlEscape(row.value)}
    `) .join(''); openAppFromDescriptor({ title: `${title} Properties`, width: 360, height: 240, html: `
    ${htmlEscape(title)}
    ${htmlRows || '
    No details available.
    '}
    `, }); } function parsePosition(btn) { return { x: parseInt(btn.style.left || '0', 10) || 0, y: parseInt(btn.style.top || '0', 10) || 0, }; } function setIconPosition(btn, x, y) { const maxX = Math.max(0, iconsEl.clientWidth - btn.offsetWidth - 6); const maxY = Math.max(0, iconsEl.clientHeight - btn.offsetHeight - 6); btn.style.left = `${clamp(x, 0, maxX)}px`; btn.style.top = `${clamp(y, 0, maxY)}px`; } function saveIconLayout() { const payload = iconButtons.map((btn) => ({ app: btn.dataset.app, ...parsePosition(btn), })); localStorage.setItem(ICON_LAYOUT_KEY, JSON.stringify(payload)); } function loadIconLayout() { try { return JSON.parse(localStorage.getItem(ICON_LAYOUT_KEY) || 'null'); } catch { return null; } } function resetIconLayout() { iconButtons.forEach((btn, i) => { setIconPosition(btn, 8, 8 + i * 106); }); saveIconLayout(); setStatus('Icons reset to vertical stack'); } function applyInitialLayout() { const saved = loadIconLayout(); if (Array.isArray(saved) && saved.length) { const savedMap = new Map(saved.map((item) => [item.app, item])); iconButtons.forEach((btn, i) => { const s = savedMap.get(btn.dataset.app); if (s) { setIconPosition(btn, Number(s.x) || 0, Number(s.y) || 0); } else { setIconPosition(btn, 8, 8 + i * 106); } }); return; } resetIconLayout(); } function clearSelection() { selectedIcons.forEach((btn) => btn.classList.remove('selected')); selectedIcons.clear(); } function setSelection(buttons) { clearSelection(); buttons.forEach((btn) => { selectedIcons.add(btn); btn.classList.add('selected'); }); setStatus(`${selectedIcons.size} item(s) selected`); } function toggleSelection(btn) { if (selectedIcons.has(btn)) { selectedIcons.delete(btn); btn.classList.remove('selected'); } else { selectedIcons.add(btn); btn.classList.add('selected'); } if (selectedIcons.size) { setStatus(`${selectedIcons.size} item(s) selected`); } else { setStatus(windowsById.size ? `${windowsById.size} window(s) open` : 'No windows open'); } } function toIconRect(btn) { return { left: btn.offsetLeft, top: btn.offsetTop, right: btn.offsetLeft + btn.offsetWidth, bottom: btn.offsetTop + btn.offsetHeight, }; } function intersects(a, b) { return !(a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom); } function ensureSelectionBox() { if (!selectionBoxEl) { selectionBoxEl = document.createElement('div'); selectionBoxEl.id = 'selection-box'; selectionBoxEl.hidden = true; iconsEl.appendChild(selectionBoxEl); } return selectionBoxEl; } function getWindowRect(winEl) { return winEl.getBoundingClientRect(); } function splitPath(path) { return path.split('/').filter(Boolean); } function joinPath(parts) { return `/${parts.join('/')}` || '/'; } function getNodeByPath(path) { const parts = splitPath(path); let node = FILE_SYSTEM; for (const part of parts) { if (!node.children) return null; const next = node.children.find((child) => child.name === part); if (!next) return null; node = next; } return node; } function updateFileByPath(path, updater) { const node = getNodeByPath(path); if (!node || node.type !== 'file') return false; updater(node); return true; } function listFolder(path) { const node = getNodeByPath(path); if (!node || node.type !== 'folder') return []; const rank = (item) => { if (item.type === 'folder') return 0; if (item.fileType === 'audio') return 1; if (item.fileType === 'image') return 2; if (item.fileType === 'txt') return 3; return 4; }; return [...(node.children || [])].sort((a, b) => { const ra = rank(a); const rb = rank(b); if (ra !== rb) return ra - rb; return a.name.localeCompare(b.name); }); } function fileIconFor(item) { if (item.type === 'folder') return '๐Ÿ“'; if (item.fileType === 'image') return '๐Ÿ–ผ๏ธ'; if (item.fileType === 'audio') return '๐ŸŽต'; if (item.fileType === 'txt') return '๐Ÿ“„'; return '๐Ÿ“ฆ'; } function itemTypeLabel(item) { if (item.type === 'folder') return 'File Folder'; if (item.fileType === 'image') return 'Image File'; if (item.fileType === 'audio') return 'Audio File'; if (item.fileType === 'txt') return 'Text Document'; return 'File'; } function itemSizeLabel(item) { if (item.type === 'folder') return `${(item.children || []).length} item(s)`; const raw = String(item.content || '').length; const kb = Math.max(1, Math.round(raw / 40)); return `${kb} KB`; } function itemModifiedLabel(item) { const seed = item.name.length * 37; const month = ((seed % 12) + 1).toString().padStart(2, '0'); const day = ((seed % 27) + 1).toString().padStart(2, '0'); return `${month}/${day}/2006 3:${(seed % 60).toString().padStart(2, '0')} PM`; } function renderExplorerDetails(path, selectedItem) { return selectedItem ? `
    ${selectedItem.name}
    Type: ${itemTypeLabel(selectedItem)}
    Size: ${itemSizeLabel(selectedItem)}
    Modified: ${itemModifiedLabel(selectedItem)}
    Location: ${path === '/' ? 'My Computer' : path}
    ` : `
    No item selected
    Select a file or folder to view details.
    `; } function renderExplorer(path, selectedItem, nav, selectedAll = false) { const items = listFolder(path); const breadcrumb = path === '/' ? 'My Computer' : `My Computer > ${splitPath(path).join(' > ')}`; const list = items.map((item) => { const encodedName = encodeURIComponent(item.name); const selected = selectedAll || (selectedItem && selectedItem.name === item.name && selectedItem.type === item.type); return ` `; }).join(''); const detailsHtml = selectedAll ? `
    ${items.length} item(s) selected
    Press Enter to open the first selected item.
    ` : renderExplorerDetails(path, selectedItem); return `
    ${breadcrumb}
    ${list || '
    This folder is empty.
    '}
    `; } function renderNotepadApp(filePath, fileName, textValue = '') { const escapedName = String(fileName || 'Untitled').replace(/"/g, '"'); const escapedPath = String(filePath || '').replace(/"/g, '"'); const escapedText = String(textValue).replace(/[&<>]/g, (m) => ({ '&': '&', '<': '<', '>': '>' }[m])); return `
    ${escapedName}
    `; } function wireNotepadInteractions(winEl, options = {}) { const contentEl = winEl.querySelector('.window-content'); if (!contentEl) return; const editorEl = contentEl.querySelector('.xp-note-editor'); const filePath = options.filePath || null; const fileName = options.fileName || 'Untitled.txt'; if (!editorEl) return; function updateDirtyTitle(isDirty) { const titleEl = winEl.querySelector('.window-title'); if (!titleEl) return; titleEl.textContent = `${isDirty ? '* ' : ''}${fileName}`; const entry = windowsById.get(winEl.dataset.windowId || ''); if (entry) { entry.title = titleEl.textContent; updateTaskbarButtons(); } } let dirty = false; editorEl.addEventListener('input', () => { dirty = true; updateDirtyTitle(true); }); contentEl.querySelectorAll('.xp-note-btn').forEach((btn) => { btn.addEventListener('click', () => { const action = btn.getAttribute('data-note-action'); if (action === 'new') { editorEl.value = ''; dirty = true; updateDirtyTitle(true); return; } if (action === 'save') { if (filePath) { updateFileByPath(filePath, (node) => { node.content = editorEl.value; }); dirty = false; updateDirtyTitle(false); setStatus(`${fileName} saved`); } } }); }); editorEl.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) { e.preventDefault(); contentEl.querySelector('[data-note-action="save"]')?.click(); } }); } function openNotepadFile(filePath, fileName, textValue = '') { const winEl = openAppFromDescriptor({ title: fileName || 'Notepad', html: renderNotepadApp(filePath, fileName, textValue), }); if (winEl) wireNotepadInteractions(winEl, { filePath, fileName }); } function renderPaintApp() { return `
    `; } function wirePaintInteractions(winEl) { const contentEl = winEl.querySelector('.window-content'); if (!contentEl) return; const canvas = contentEl.querySelector('.xp-paint-canvas'); const ctx = canvas?.getContext('2d'); const toolEl = contentEl.querySelector('.xp-paint-tool'); const colorEl = contentEl.querySelector('.xp-paint-color'); const sizeEl = contentEl.querySelector('.xp-paint-size'); if (!canvas || !ctx || !toolEl || !colorEl || !sizeEl) return; const undoStack = []; const redoStack = []; function snapshot() { try { return canvas.toDataURL('image/png'); } catch { return null; } } function pushUndoState() { const shot = snapshot(); if (!shot) return; undoStack.push(shot); if (undoStack.length > 30) undoStack.shift(); redoStack.length = 0; } function restoreSnapshot(dataUrl) { if (!dataUrl) return; const img = new Image(); img.onload = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); }; img.src = dataUrl; } ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); let drawing = false; let lastX = 0; let lastY = 0; function applyStrokeStyle() { const tool = toolEl.value; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.lineWidth = Number(sizeEl.value || 4); if (tool === 'eraser') { ctx.strokeStyle = '#ffffff'; } else { ctx.strokeStyle = colorEl.value || '#d13f8c'; } } function pointFromEvent(e) { const rect = canvas.getBoundingClientRect(); return { x: (e.clientX - rect.left) * (canvas.width / rect.width), y: (e.clientY - rect.top) * (canvas.height / rect.height), }; } canvas.addEventListener('pointerdown', (e) => { e.preventDefault(); pushUndoState(); drawing = true; const p = pointFromEvent(e); lastX = p.x; lastY = p.y; applyStrokeStyle(); canvas.setPointerCapture(e.pointerId); }); canvas.addEventListener('pointermove', (e) => { if (!drawing) return; const p = pointFromEvent(e); applyStrokeStyle(); ctx.beginPath(); ctx.moveTo(lastX, lastY); ctx.lineTo(p.x, p.y); ctx.stroke(); lastX = p.x; lastY = p.y; }); function stopDrawing() { if (!drawing) return; drawing = false; } canvas.addEventListener('pointerup', stopDrawing); canvas.addEventListener('pointercancel', stopDrawing); canvas.addEventListener('pointerleave', stopDrawing); contentEl.querySelectorAll('.xp-paint-btn').forEach((btn) => { btn.addEventListener('click', () => { const action = btn.getAttribute('data-paint-action'); if (action === 'undo') { if (!undoStack.length) return; const current = snapshot(); if (current) redoStack.push(current); const prev = undoStack.pop(); restoreSnapshot(prev); return; } if (action === 'redo') { if (!redoStack.length) return; const current = snapshot(); if (current) undoStack.push(current); const next = redoStack.pop(); restoreSnapshot(next); return; } if (action === 'clear') { pushUndoState(); ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); return; } if (action === 'save') { const a = document.createElement('a'); a.href = canvas.toDataURL('image/png'); a.download = `paint-${Date.now()}.png`; a.click(); } }); }); } function openPaintApp() { const winEl = openAppFromDescriptor({ title: 'Paint', html: renderPaintApp(), }); if (winEl) wirePaintInteractions(winEl); } async function renderIeApp(startUrl = RETRO_WEB_HOME) { await loadRetroWebIndex(); return `
    Address
    Loading...
    `; } function wrapRetroShell(page, url) { const stamp = Math.floor(Math.abs(url.split('').reduce((a, c) => a + c.charCodeAt(0), 0)) % 9000) + 1000; return `
    โ˜… Welcome to ${htmlEscape(page.title || url)} โ˜…
    Visitors: ${stamp} ยท Best viewed at 800x600 ยท Netscape/IE compatible
    ${page.html}
    `; } function wireIeInteractions(winEl, startUrl = RETRO_WEB_HOME) { const contentEl = winEl.querySelector('.window-content'); if (!contentEl) return; const root = contentEl.querySelector('.xp-ie'); const addr = contentEl.querySelector('.xp-ie-address'); const body = contentEl.querySelector('.xp-ie-content'); const form = contentEl.querySelector('.xp-ie-address-row'); if (!root || !addr || !body || !form) return; const backStack = []; const forwardStack = []; let currentUrl = normalizeRetroUrl(startUrl); function setTitle(url, page) { const titleEl = winEl.querySelector('.window-title'); if (titleEl) { titleEl.textContent = `${page?.title || 'Internet Explorer'} - Internet Explorer`; } const entry = windowsById.get(winEl.dataset.windowId || ''); if (entry) { entry.title = `IE: ${page?.title || url}`; updateTaskbarButtons(); } } function updateNavButtons() { contentEl.querySelectorAll('.xp-ie-btn').forEach((btn) => { const action = btn.getAttribute('data-ie-action'); if (action === 'back') btn.disabled = backStack.length === 0; if (action === 'forward') btn.disabled = forwardStack.length === 0; }); } async function renderPage(url, pushHistory = false) { const normalized = normalizeRetroUrl(url); if (pushHistory && normalized !== currentUrl) { backStack.push(currentUrl); forwardStack.length = 0; } currentUrl = normalized; root.dataset.url = currentUrl; addr.value = currentUrl; body.innerHTML = '
    Dialing up... Loading retro page...
    '; const page = await getRetroPage(currentUrl); if (!page) { body.innerHTML = `

    The page cannot be displayed

    URL: ${htmlEscape(currentUrl)}

    Try home://portal or run a search.

    `; setTitle(currentUrl, { title: 'Cannot display page' }); } else { body.innerHTML = wrapRetroShell(page, currentUrl); setTitle(currentUrl, page); } updateNavButtons(); body.querySelectorAll('a[href]').forEach((a) => { a.addEventListener('click', (e) => { e.preventDefault(); const href = a.getAttribute('href'); if (href) renderPage(href, true); }); }); } contentEl.querySelectorAll('.xp-ie-btn').forEach((btn) => { btn.addEventListener('click', async () => { const action = btn.getAttribute('data-ie-action'); if (action === 'home') { renderPage(RETRO_WEB_HOME, true); return; } if (action === 'refresh') { renderPage(currentUrl, false); return; } if (action === 'back' && backStack.length) { forwardStack.push(currentUrl); const prev = backStack.pop(); if (prev) renderPage(prev, false); return; } if (action === 'forward' && forwardStack.length) { backStack.push(currentUrl); const next = forwardStack.pop(); if (next) renderPage(next, false); } }); }); form.addEventListener('submit', (e) => { e.preventDefault(); renderPage(addr.value, true); }); renderPage(currentUrl, false); } async function openIeApp() { const html = await renderIeApp(RETRO_WEB_HOME); const winEl = openAppFromDescriptor({ title: 'Internet Explorer', html, }); if (winEl) wireIeInteractions(winEl, RETRO_WEB_HOME); } function renderImageViewer(fileName, imageUrl) { const safeName = htmlEscape(fileName || 'Image'); const safeUrl = htmlEscape(imageUrl || ''); return `
    ${safeName}
    ${safeName}
    `; } function wireImageViewerInteractions(winEl) { const contentEl = winEl.querySelector('.window-content'); if (!contentEl) return; const img = contentEl.querySelector('.xp-image-content'); const stage = contentEl.querySelector('.xp-image-stage'); if (!img || !stage) return; let scale = 1; let fitMode = true; function applyScale() { if (fitMode) { img.style.maxWidth = '100%'; img.style.maxHeight = '100%'; img.style.width = 'auto'; img.style.height = 'auto'; img.style.transform = 'none'; return; } img.style.maxWidth = 'none'; img.style.maxHeight = 'none'; img.style.width = 'auto'; img.style.height = 'auto'; img.style.transformOrigin = 'center center'; img.style.transform = `scale(${scale})`; } contentEl.querySelectorAll('.xp-image-btn').forEach((btn) => { btn.addEventListener('click', () => { const action = btn.getAttribute('data-img-action'); if (action === 'fit') { fitMode = true; applyScale(); return; } if (action === 'actual') { fitMode = false; scale = 1; applyScale(); return; } if (action === 'zoom-in') { fitMode = false; scale = Math.min(6, scale + 0.2); applyScale(); return; } if (action === 'zoom-out') { fitMode = false; scale = Math.max(0.2, scale - 0.2); applyScale(); } }); }); img.addEventListener('dblclick', () => { fitMode = !fitMode; if (!fitMode) scale = 1; applyScale(); }); applyScale(); } function openImageViewer(fileName, imageUrl) { const winEl = openAppFromDescriptor({ title: fileName || 'Image Viewer', html: renderImageViewer(fileName, imageUrl), }); if (winEl) wireImageViewerInteractions(winEl); } function renderAudioPlayer(fileName, audioUrl) { const safeName = htmlEscape(fileName || 'Track'); const safeUrl = htmlEscape(audioUrl || ''); return `
    ${safeName} โ€ข ${safeName} โ€ข ${safeName}
    00:00 / 00:00
    `; } function wireAudioPlayerInteractions(winEl) { const contentEl = winEl.querySelector('.window-content'); if (!contentEl) return; const audio = contentEl.querySelector('.xp-audio-element'); const seek = contentEl.querySelector('.xp-audio-seek'); const timeEl = contentEl.querySelector('.xp-audio-time'); if (!audio || !seek || !timeEl) return; function fmt(sec) { if (!Number.isFinite(sec) || sec < 0) return '00:00'; const m = Math.floor(sec / 60).toString().padStart(2, '0'); const s = Math.floor(sec % 60).toString().padStart(2, '0'); return `${m}:${s}`; } function updateTime() { const dur = Number.isFinite(audio.duration) ? audio.duration : 0; const cur = Number.isFinite(audio.currentTime) ? audio.currentTime : 0; const pct = dur > 0 ? Math.round((cur / dur) * 1000) : 0; seek.value = String(pct); timeEl.textContent = `${fmt(cur)} / ${fmt(dur)}`; } let seeking = false; seek.addEventListener('input', () => { seeking = true; const dur = Number.isFinite(audio.duration) ? audio.duration : 0; const pct = Number(seek.value) / 1000; if (dur > 0) audio.currentTime = pct * dur; updateTime(); }); seek.addEventListener('change', () => { seeking = false; }); contentEl.querySelectorAll('.xp-audio-btn').forEach((btn) => { btn.addEventListener('click', async () => { const action = btn.getAttribute('data-audio-action'); if (action === 'play') { try { await audio.play(); } catch {} return; } if (action === 'pause') { audio.pause(); return; } if (action === 'stop') { audio.pause(); audio.currentTime = 0; updateTime(); } }); }); audio.addEventListener('timeupdate', () => { if (!seeking) updateTime(); }); audio.addEventListener('loadedmetadata', updateTime); audio.addEventListener('ended', () => { audio.currentTime = 0; updateTime(); }); updateTime(); } function openAudioPlayer(fileName, audioUrl) { const winEl = openAppFromDescriptor({ title: `Winamp - ${fileName || 'Track'}`, html: renderAudioPlayer(fileName, audioUrl), width: 420, height: 180, }); if (winEl) wireAudioPlayerInteractions(winEl); } function openVirtualFile(item, fullPath) { if (item.fileType === 'txt') { openNotepadFile(fullPath || null, item.name, String(item.content || '')); return; } if (item.fileType === 'image') { openImageViewer(item.name, item.content); return; } if (item.fileType === 'audio') { openAudioPlayer(item.name, item.content); return; } openAppFromDescriptor({ title: item.name, html: `

    Cannot open this file type.

    `, }); } function wireExplorerInteractions(winEl, startPath) { const initialPath = startPath; let currentPath = startPath; let selectedKey = null; let selectedAll = false; const backStack = []; const forwardStack = []; const contentEl = winEl.querySelector('.window-content'); if (!contentEl) return; function folderItems() { return listFolder(currentPath); } function selectedItemFromKey() { if (!selectedKey) return null; const [type, encoded] = selectedKey.split('|'); const name = decodeURIComponent(encoded || ''); const folder = getNodeByPath(currentPath); if (!folder || !folder.children) return null; return folder.children.find((child) => child.type === type && child.name === name) || null; } function firstSelectableKey() { const items = folderItems(); if (!items.length) return null; return `${items[0].type}|${encodeURIComponent(items[0].name)}`; } function openItemFromKey(key) { if (!key) return; const [type, encoded] = key.split('|'); const name = decodeURIComponent(encoded || ''); const folder = getNodeByPath(currentPath); if (!folder || !folder.children) return; const item = folder.children.find((child) => child.type === type && child.name === name); if (!item) return; const fullPath = joinPath([...splitPath(currentPath), item.name]); if (item.type === 'folder') { navigateTo(fullPath); return; } openVirtualFile(item, fullPath); } function selectByIndex(index) { const items = folderItems(); if (!items.length) return; const i = clamp(index, 0, items.length - 1); selectedAll = false; selectedKey = `${items[i].type}|${encodeURIComponent(items[i].name)}`; rerender(); } function moveSelection(delta) { const items = folderItems(); if (!items.length) return; if (!selectedKey || selectedAll) { selectByIndex(delta > 0 ? 0 : items.length - 1); return; } const currentIndex = items.findIndex((it) => `${it.type}|${encodeURIComponent(it.name)}` === selectedKey); if (currentIndex === -1) { selectByIndex(0); return; } selectByIndex(currentIndex + delta); } function navigateTo(path, pushHistory = true) { const node = getNodeByPath(path); if (!node || node.type !== 'folder') return; if (path === currentPath) return; if (pushHistory) { backStack.push(currentPath); forwardStack.length = 0; } currentPath = path; selectedKey = null; selectedAll = false; rerender(); } function rerender() { const selectedItem = selectedAll ? null : selectedItemFromKey(); contentEl.innerHTML = renderExplorer(currentPath, selectedItem, { canBack: backStack.length > 0, canForward: forwardStack.length > 0, }, selectedAll); const titleEl = winEl.querySelector('.window-title'); if (titleEl) { if (initialPath === '/Recycle Bin') { titleEl.textContent = 'Recycle Bin'; } else { titleEl.textContent = currentPath === '/' ? 'My Computer' : splitPath(currentPath).slice(-1)[0] || 'Explorer'; } } contentEl.querySelectorAll('.xp-nav-btn').forEach((btn) => { btn.addEventListener('click', () => { const action = btn.getAttribute('data-action'); if (action === 'back' && backStack.length) { forwardStack.push(currentPath); currentPath = backStack.pop() || '/'; selectedKey = null; selectedAll = false; rerender(); return; } if (action === 'forward' && forwardStack.length) { backStack.push(currentPath); currentPath = forwardStack.pop() || '/'; selectedKey = null; selectedAll = false; rerender(); return; } if (action === 'root') { navigateTo('/'); return; } if (action === 'up') { const parts = splitPath(currentPath); if (!parts.length) return; parts.pop(); navigateTo(joinPath(parts)); } }); }); contentEl.querySelectorAll('.xp-side-link').forEach((btn) => { btn.addEventListener('click', () => { const jump = btn.getAttribute('data-jump') || '/'; navigateTo(jump); }); }); contentEl.querySelectorAll('.xp-file-item').forEach((btn) => { btn.addEventListener('click', () => { const kind = btn.getAttribute('data-kind'); const encoded = btn.getAttribute('data-name'); if (!kind || !encoded) return; selectedAll = false; selectedKey = `${kind}|${encoded}`; contentEl.querySelectorAll('.xp-file-item.selected').forEach((el) => el.classList.remove('selected')); btn.classList.add('selected'); const folder = getNodeByPath(currentPath); const name = decodeURIComponent(encoded); const item = folder?.children?.find((child) => child.type === kind && child.name === name) || null; const detailsEl = contentEl.querySelector('.xp-details'); if (detailsEl) detailsEl.innerHTML = renderExplorerDetails(currentPath, item); }); btn.addEventListener('dblclick', () => { const encoded = btn.getAttribute('data-name'); const kind = btn.getAttribute('data-kind'); const name = decodeURIComponent(encoded || ''); if (!name || !kind) return; const folder = getNodeByPath(currentPath); if (!folder || !folder.children) return; const item = folder.children.find((child) => child.name === name && child.type === kind); if (!item) return; const fullPath = joinPath([...splitPath(currentPath), item.name]); if (item.type === 'folder') { navigateTo(fullPath); return; } openVirtualFile(item, fullPath); }); btn.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); const encoded = btn.getAttribute('data-name'); const kind = btn.getAttribute('data-kind'); const name = decodeURIComponent(encoded || ''); if (!name || !kind) return; const folder = getNodeByPath(currentPath); if (!folder || !folder.children) return; const item = folder.children.find((child) => child.name === name && child.type === kind); if (!item) return; selectedAll = false; selectedKey = `${kind}|${encodeURIComponent(name)}`; contentEl.querySelectorAll('.xp-file-item.selected').forEach((el) => el.classList.remove('selected')); btn.classList.add('selected'); const detailsEl = contentEl.querySelector('.xp-details'); if (detailsEl) detailsEl.innerHTML = renderExplorerDetails(currentPath, item); const fullPath = joinPath([...splitPath(currentPath), item.name]); const menuItems = [ { label: 'Open', action: () => { if (item.type === 'folder') navigateTo(fullPath); else openVirtualFile(item, fullPath); }, }, item.type === 'folder' ? { label: 'Open in New Window', action: () => openExplorerAtPath(fullPath, item.name), } : { label: 'Open Parent Folder', action: () => navigateTo(currentPath), }, 'separator', { label: 'Properties', action: () => { openPropertiesWindow(item.name, [ { label: 'Type', value: itemTypeLabel(item) }, { label: 'Path', value: fullPath }, { label: 'Size', value: itemSizeLabel(item) }, { label: 'Modified', value: itemModifiedLabel(item) }, ]); }, }, ]; showContextMenu(e.clientX, e.clientY, menuItems); }); }); } if (!getNodeByPath(currentPath)) currentPath = '/'; rerender(); winEl.addEventListener('keydown', (e) => { const activeEl = document.activeElement; const isTyping = activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA'); if (isTyping) return; if (e.ctrlKey && (e.key === 'a' || e.key === 'A')) { e.preventDefault(); const items = folderItems(); if (!items.length) return; selectedAll = true; selectedKey = firstSelectableKey(); rerender(); return; } if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { e.preventDefault(); moveSelection(1); return; } if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') { e.preventDefault(); moveSelection(-1); return; } if (e.key === 'Enter') { e.preventDefault(); openItemFromKey(selectedKey || firstSelectableKey()); return; } if (e.key === 'Backspace') { e.preventDefault(); const parts = splitPath(currentPath); if (!parts.length) return; parts.pop(); navigateTo(joinPath(parts)); } }); } function setWindowPosition(winEl, left, top) { const maxLeft = Math.max(0, window.innerWidth - winEl.offsetWidth - 8); const maxTop = Math.max(36, window.innerHeight - winEl.offsetHeight - 40); winEl.style.left = `${clamp(left, 0, maxLeft)}px`; winEl.style.top = `${clamp(top, 32, maxTop)}px`; winEl.style.transform = 'none'; } function updateTaskbarButtons() { windowsById.forEach((entry) => { const { taskBtn, winEl, title } = entry; taskBtn.textContent = title; taskBtn.classList.toggle('active', !winEl.classList.contains('hidden') && entry.active); }); } function activateWindow(id) { const target = windowsById.get(id); if (!target) return; windowsById.forEach((entry) => { entry.active = false; entry.winEl.classList.remove('active'); }); target.active = true; target.winEl.classList.add('active'); showDesktopBtn?.classList.remove('active'); topWindowZ += 1; target.winEl.style.zIndex = String(topWindowZ); updateTaskbarButtons(); } function bringToFront(winEl) { const id = winEl.dataset.windowId; if (!id) return; activateWindow(id); winEl.focus(); } function minimizeWindow(id) { const entry = windowsById.get(id); if (!entry) return; entry.winEl.classList.add('hidden'); entry.active = false; const nextVisible = Array.from(windowsById.values()).find((w) => !w.winEl.classList.contains('hidden')); if (nextVisible) { activateWindow(nextVisible.id); } updateTaskbarButtons(); setStatus(`${entry.title} minimized`); } function restoreWindow(id) { const entry = windowsById.get(id); if (!entry) return; entry.winEl.classList.remove('hidden'); activateWindow(id); setStatus(`${entry.title} restored`); } function toggleShowDesktop() { const entries = Array.from(windowsById.values()); if (!entries.length) return; const hasVisible = entries.some((entry) => !entry.winEl.classList.contains('hidden')); if (hasVisible) { entries.forEach((entry) => { entry.winEl.classList.add('hidden'); entry.active = false; }); showDesktopBtn?.classList.add('active'); updateTaskbarButtons(); setStatus('Showing desktop'); return; } const lastFocused = entries.sort((a, b) => { const za = parseInt(a.winEl.style.zIndex || '0', 10); const zb = parseInt(b.winEl.style.zIndex || '0', 10); return zb - za; })[0]; entries.forEach((entry) => entry.winEl.classList.remove('hidden')); showDesktopBtn?.classList.remove('active'); if (lastFocused) activateWindow(lastFocused.id); else updateTaskbarButtons(); setStatus('Windows restored'); } function closeWindow(id) { const entry = windowsById.get(id); if (!entry) return; entry.winEl.remove(); entry.taskBtn.remove(); windowsById.delete(id); if (windowsById.size) { const newest = Array.from(windowsById.values()).sort((a, b) => { const za = parseInt(a.winEl.style.zIndex || '0', 10); const zb = parseInt(b.winEl.style.zIndex || '0', 10); return zb - za; })[0]; if (newest) activateWindow(newest.id); } updateTaskbarButtons(); setStatus(windowsById.size ? `${windowsById.size} window(s) open` : 'No windows open'); } function bindWindowDrag(winEl) { const titlebar = winEl.querySelector('.window-titlebar'); const closeBtn = winEl.querySelector('.window-close'); const minimizeBtn = winEl.querySelector('.window-minimize'); const resizeHandle = winEl.querySelector('.window-resize-handle'); const id = winEl.dataset.windowId; if (!titlebar || !closeBtn || !minimizeBtn || !id) return; winEl.addEventListener('pointerdown', () => bringToFront(winEl)); titlebar.addEventListener('pointerdown', (e) => { if (e.button !== 0) return; if (e.target === closeBtn || e.target === minimizeBtn) return; bringToFront(winEl); const rect = getWindowRect(winEl); setWindowPosition(winEl, rect.left, rect.top); const startLeft = rect.left; const startTop = rect.top; const startX = e.clientX; const startY = e.clientY; let moved = false; winEl.classList.add('dragging'); titlebar.setPointerCapture(e.pointerId); const onMove = (ev) => { const dx = ev.clientX - startX; const dy = ev.clientY - startY; if (!moved && Math.hypot(dx, dy) > 2) moved = true; if (!moved) return; setWindowPosition(winEl, startLeft + dx, startTop + dy); }; const onUp = () => { winEl.classList.remove('dragging'); titlebar.removeEventListener('pointermove', onMove); titlebar.removeEventListener('pointerup', onUp); titlebar.removeEventListener('pointercancel', onUp); }; titlebar.addEventListener('pointermove', onMove); titlebar.addEventListener('pointerup', onUp); titlebar.addEventListener('pointercancel', onUp); }); if (resizeHandle) { resizeHandle.addEventListener('pointerdown', (e) => { if (e.button !== 0) return; e.stopPropagation(); bringToFront(winEl); const rect = getWindowRect(winEl); const startX = e.clientX; const startY = e.clientY; const startW = rect.width; const startH = rect.height; winEl.classList.add('resizing'); resizeHandle.setPointerCapture(e.pointerId); const onMove = (ev) => { const dx = ev.clientX - startX; const dy = ev.clientY - startY; const minW = 360; const minH = 240; const maxW = Math.max(minW, window.innerWidth - rect.left - 8); const maxH = Math.max(minH, window.innerHeight - rect.top - 40); const nextW = clamp(startW + dx, minW, maxW); const nextH = clamp(startH + dy, minH, maxH); winEl.style.width = `${nextW}px`; winEl.style.height = `${nextH}px`; }; const onUp = () => { winEl.classList.remove('resizing'); resizeHandle.removeEventListener('pointermove', onMove); resizeHandle.removeEventListener('pointerup', onUp); resizeHandle.removeEventListener('pointercancel', onUp); }; resizeHandle.addEventListener('pointermove', onMove); resizeHandle.addEventListener('pointerup', onUp); resizeHandle.addEventListener('pointercancel', onUp); }); } minimizeBtn.addEventListener('click', () => minimizeWindow(id)); closeBtn.addEventListener('click', () => closeWindow(id)); } function createTaskbarButton(id, title) { const btn = document.createElement('button'); btn.className = 'taskbar-btn'; btn.type = 'button'; btn.textContent = title; btn.title = title; btn.addEventListener('click', () => { const entry = windowsById.get(id); if (!entry) return; if (entry.winEl.classList.contains('hidden')) { restoreWindow(id); return; } if (entry.active) { minimizeWindow(id); } else { activateWindow(id); setStatus(`${entry.title} focused`); } }); taskbarWindowsEl.appendChild(btn); return btn; } function addWindowControls(winEl) { const titlebar = winEl.querySelector('.window-titlebar'); const closeBtn = winEl.querySelector('.window-close'); if (!titlebar || !closeBtn) return; const controls = document.createElement('div'); controls.className = 'window-controls'; const minimizeBtn = document.createElement('button'); minimizeBtn.className = 'window-minimize'; minimizeBtn.type = 'button'; minimizeBtn.textContent = 'โ€”'; minimizeBtn.setAttribute('aria-label', 'minimize'); closeBtn.classList.add('window-close'); controls.appendChild(minimizeBtn); controls.appendChild(closeBtn); titlebar.appendChild(controls); const resizeHandle = document.createElement('div'); resizeHandle.className = 'window-resize-handle'; resizeHandle.setAttribute('aria-hidden', 'true'); winEl.appendChild(resizeHandle); } function openAppFromDescriptor(descriptor) { if (!descriptor || !windowTemplate) return null; hideStartMenu(); windowSeq += 1; const id = `w${windowSeq}`; const winEl = windowTemplate.cloneNode(true); winEl.id = ''; winEl.dataset.windowId = id; winEl.classList.remove('hidden'); const titleEl = winEl.querySelector('.window-title'); const contentEl = winEl.querySelector('.window-content'); if (titleEl) titleEl.textContent = descriptor.title || 'Window'; if (contentEl) contentEl.innerHTML = descriptor.html || '

    Empty window

    '; addWindowControls(winEl); document.body.appendChild(winEl); const offset = ((windowSeq - 1) % 8) * 22; const left = (window.innerWidth - winEl.offsetWidth) / 2 + offset; const top = 68 + offset; if (descriptor.width) { winEl.style.width = typeof descriptor.width === 'number' ? `${descriptor.width}px` : String(descriptor.width); } if (descriptor.height) { winEl.style.height = typeof descriptor.height === 'number' ? `${descriptor.height}px` : String(descriptor.height); } setWindowPosition(winEl, left, top); const taskBtn = createTaskbarButton(id, descriptor.title || 'Window'); windowsById.set(id, { id, title: descriptor.title || 'Window', winEl, taskBtn, active: false, }); bindWindowDrag(winEl); activateWindow(id); updateTaskbarButtons(); winEl.setAttribute('tabindex', '0'); winEl.focus(); setStatus(`${descriptor.title || 'Window'} opened`); return winEl; } function openExplorerAtPath(path = '/', title = 'Explorer') { const winEl = openAppFromDescriptor({ title, html: '
    Loading...
    ', }); if (winEl) wireExplorerInteractions(winEl, path); } function openSystemPanel(kind) { const panels = { 'control-panel': { title: 'Control Panel', html: '
    โš™๏ธ Control Panel is under construction.
    Try My Documents, Music, Paint, and Notepad from Start.
    ', }, help: { title: 'Help and Support', html: '
    โ“ Need help? Double-click folders/files in Explorer, drag desktop icons, and use the tray reset/show-desktop buttons.
    ', }, run: { title: 'Run', html: '
    ๐Ÿƒ Run shortcut: open Internet, Notepad, Paint, or My Computer from Start.
    ', }, }; const panel = panels[kind]; if (!panel) return; openAppFromDescriptor(panel); } function performSystemAction(action) { if (action === 'logoff') { hideStartMenu(); clearSelection(); setStatus('Logged off (mock)'); return; } if (action === 'shutdown') { hideStartMenu(); windowsById.forEach((entry) => { entry.winEl.classList.add('hidden'); entry.active = false; }); showDesktopBtn?.classList.add('active'); updateTaskbarButtons(); setStatus('It is now safe to turn off your imagination โœจ'); return; } openSystemPanel(action); } function openApp(key) { const app = ICON_APPS[key]; if (!app) return; if (app.type === 'explorer') { openExplorerAtPath(app.startPath || '/', app.title || 'Explorer'); return; } if (app.type === 'notepad') { openNotepadFile(null, 'Untitled.txt', ''); return; } if (app.type === 'paint') { openPaintApp(); return; } if (app.type === 'ie') { openIeApp(); return; } openAppFromDescriptor(app); } function bindIconDrag(btn) { btn.addEventListener('pointerdown', (e) => { if (e.button !== 0) return; e.stopPropagation(); if (e.shiftKey) { toggleSelection(btn); } else if (!selectedIcons.has(btn)) { setSelection([btn]); } const dragSet = selectedIcons.size ? Array.from(selectedIcons) : [btn]; const starts = dragSet.map((icon) => ({ icon, ...parsePosition(icon) })); const startX = e.clientX; const startY = e.clientY; let moved = false; dragSet.forEach((icon) => icon.classList.add('dragging')); btn.setPointerCapture(e.pointerId); const onMove = (ev) => { const dx = ev.clientX - startX; const dy = ev.clientY - startY; if (!moved && Math.hypot(dx, dy) > 3) moved = true; if (!moved) return; starts.forEach(({ icon, x, y }) => setIconPosition(icon, x + dx, y + dy)); setStatus(`Moving ${dragSet.length} item(s)`); }; const onUp = () => { dragSet.forEach((icon) => icon.classList.remove('dragging')); btn.removeEventListener('pointermove', onMove); btn.removeEventListener('pointerup', onUp); btn.removeEventListener('pointercancel', onUp); if (moved) { btn.dataset.wasDragged = '1'; saveIconLayout(); setStatus('Desktop layout saved'); } }; btn.addEventListener('pointermove', onMove); btn.addEventListener('pointerup', onUp); btn.addEventListener('pointercancel', onUp); }); btn.addEventListener('click', (e) => { if (btn.dataset.wasDragged === '1') { btn.dataset.wasDragged = '0'; return; } if (e.shiftKey) { toggleSelection(btn); return; } if (!selectedIcons.has(btn) || selectedIcons.size !== 1) { setSelection([btn]); return; } }); btn.addEventListener('dblclick', (e) => { e.stopPropagation(); setSelection([btn]); openApp(btn.dataset.app); }); btn.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); if (!selectedIcons.has(btn)) { setSelection([btn]); } showContextMenu(e.clientX, e.clientY, [ { label: 'Open', action: () => openApp(btn.dataset.app), }, { label: 'Open All Selected', disabled: selectedIcons.size <= 1, action: () => { Array.from(selectedIcons).forEach((icon) => openApp(icon.dataset.app)); }, }, 'separator', { label: 'Reset Icon Layout', action: () => { clearSelection(); localStorage.removeItem(ICON_LAYOUT_KEY); resetIconLayout(); }, }, { label: 'Properties', action: () => { const appKey = btn.dataset.app || ''; const appTitle = ICON_APPS[appKey]?.title || btn.innerText || 'Desktop Item'; const pos = parsePosition(btn); openPropertiesWindow(appTitle, [ { label: 'Type', value: 'Desktop Shortcut' }, { label: 'App Key', value: appKey || 'unknown' }, { label: 'Position', value: `${pos.x}, ${pos.y}` }, ]); }, }, ]); }); } function showStartMenu() { if (!startMenuEl) return; startMenuEl.classList.remove('hidden'); startBtn?.classList.add('active'); } function hideStartMenu() { if (!startMenuEl) return; startMenuEl.classList.add('hidden'); startBtn?.classList.remove('active'); } function toggleStartMenu() { if (!startMenuEl) return; if (startMenuEl.classList.contains('hidden')) { showStartMenu(); } else { hideStartMenu(); } } function bindDesktopSelection() { const selectionBox = ensureSelectionBox(); iconsEl.addEventListener('pointerdown', (e) => { if (e.button !== 0) return; if (e.target.closest('.desktop-icon')) return; const rect = iconsEl.getBoundingClientRect(); const startX = clamp(e.clientX - rect.left, 0, iconsEl.clientWidth); const startY = clamp(e.clientY - rect.top, 0, iconsEl.clientHeight); let moved = false; if (!e.shiftKey) clearSelection(); selectionBox.hidden = false; selectionBox.style.left = `${startX}px`; selectionBox.style.top = `${startY}px`; selectionBox.style.width = '0px'; selectionBox.style.height = '0px'; iconsEl.setPointerCapture(e.pointerId); const onMove = (ev) => { const currentX = clamp(ev.clientX - rect.left, 0, iconsEl.clientWidth); const currentY = clamp(ev.clientY - rect.top, 0, iconsEl.clientHeight); const left = Math.min(startX, currentX); const top = Math.min(startY, currentY); const width = Math.abs(currentX - startX); const height = Math.abs(currentY - startY); moved = moved || width > 2 || height > 2; selectionBox.style.left = `${left}px`; selectionBox.style.top = `${top}px`; selectionBox.style.width = `${width}px`; selectionBox.style.height = `${height}px`; if (!moved) return; const boxRect = { left, top, right: left + width, bottom: top + height }; iconButtons.forEach((btn) => { const hit = intersects(boxRect, toIconRect(btn)); if (hit) { selectedIcons.add(btn); btn.classList.add('selected'); } else if (!e.shiftKey) { selectedIcons.delete(btn); btn.classList.remove('selected'); } }); if (selectedIcons.size) { setStatus(`${selectedIcons.size} item(s) selected`); } }; const onUp = () => { selectionBox.hidden = true; iconsEl.removeEventListener('pointermove', onMove); iconsEl.removeEventListener('pointerup', onUp); iconsEl.removeEventListener('pointercancel', onUp); if (!moved && !e.shiftKey) { clearSelection(); setStatus(windowsById.size ? `${windowsById.size} window(s) open` : 'No windows open'); } }; iconsEl.addEventListener('pointermove', onMove); iconsEl.addEventListener('pointerup', onUp); iconsEl.addEventListener('pointercancel', onUp); }); } iconButtons.forEach(bindIconDrag); bindDesktopSelection(); window.addEventListener('resize', () => { hideContextMenu(); iconButtons.forEach((btn) => { const { x, y } = parsePosition(btn); setIconPosition(btn, x, y); }); windowsById.forEach(({ winEl }) => { if (winEl.classList.contains('hidden')) return; const rect = getWindowRect(winEl); setWindowPosition(winEl, rect.left, rect.top); }); saveIconLayout(); }); resetLayoutBtn?.addEventListener('click', () => { clearSelection(); localStorage.removeItem(ICON_LAYOUT_KEY); resetIconLayout(); }); startBtn?.addEventListener('click', (e) => { e.stopPropagation(); hideContextMenu(); toggleStartMenu(); }); showDesktopBtn?.addEventListener('click', (e) => { e.stopPropagation(); hideContextMenu(); toggleShowDesktop(); }); startMenuEl?.querySelectorAll('[data-open-app]').forEach((btn) => { btn.addEventListener('click', () => { const appKey = btn.getAttribute('data-open-app'); if (appKey) openApp(appKey); }); }); startMenuEl?.querySelectorAll('[data-open-path]').forEach((btn) => { btn.addEventListener('click', () => { const path = btn.getAttribute('data-open-path') || '/'; const label = btn.textContent?.replace(/^[^\w]+\s*/, '').trim() || 'Explorer'; openExplorerAtPath(path, label); }); }); startMenuEl?.querySelectorAll('[data-system-action]').forEach((btn) => { btn.addEventListener('click', () => { const action = btn.getAttribute('data-system-action'); if (action) performSystemAction(action); }); }); document.addEventListener('click', (e) => { hideContextMenu(); if (!startMenuEl || startMenuEl.classList.contains('hidden')) return; if (startMenuEl.contains(e.target)) return; if (startBtn && startBtn.contains(e.target)) return; hideStartMenu(); }); document.addEventListener('contextmenu', () => { hideContextMenu(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { hideContextMenu(); hideStartMenu(); } }); function tickClock() { const d = new Date(); const timeText = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); const dateText = d.toLocaleDateString('en-US', { month: 'numeric', day: 'numeric', year: 'numeric' }); clockEl.innerHTML = `${timeText}${dateText}`; } applyInitialLayout(); setInterval(tickClock, 1000); tickClock();