Files
veryextra-arg-site/site/script.js
2026-03-13 01:00:30 -04:00

2157 lines
63 KiB
JavaScript

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) => `<li><a href="${p.route}">${p.title || p.route}</a><div>${p.route}</div></li>`)
.join('');
return {
title: `Search results for "${q}"`,
html: `
<h2>RetroSearch 2000</h2>
<p>Results for: <strong>${q}</strong></p>
<ol class="ie-results">${matches || '<li>No direct matches. Try <a href="home://portal">home://portal</a></li>'}</ol>
`,
};
}
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) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
}[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) => `<div><strong>${htmlEscape(row.label)}:</strong> ${htmlEscape(row.value)}</div>`)
.join('');
openAppFromDescriptor({
title: `${title} Properties`,
width: 360,
height: 240,
html: `
<div class="xp-props">
<div class="xp-props-title">${htmlEscape(title)}</div>
<div class="xp-props-body">${htmlRows || '<div>No details available.</div>'}</div>
</div>
`,
});
}
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 ? `
<div class="xp-details-title">${selectedItem.name}</div>
<div><strong>Type:</strong> ${itemTypeLabel(selectedItem)}</div>
<div><strong>Size:</strong> ${itemSizeLabel(selectedItem)}</div>
<div><strong>Modified:</strong> ${itemModifiedLabel(selectedItem)}</div>
<div><strong>Location:</strong> ${path === '/' ? 'My Computer' : path}</div>
` : `
<div class="xp-details-title">No item selected</div>
<div>Select a file or folder to view details.</div>
`;
}
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 `
<button class="xp-file-item ${selected ? 'selected' : ''}" data-kind="${item.type}" data-name="${encodedName}">
<span class="xp-file-icon">${fileIconFor(item)}</span>
<span class="xp-file-label">${item.name}</span>
</button>
`;
}).join('');
const detailsHtml = selectedAll
? `<div class="xp-details-title">${items.length} item(s) selected</div><div>Press Enter to open the first selected item.</div>`
: renderExplorerDetails(path, selectedItem);
return `
<div class="xp-explorer" data-path="${path}">
<div class="xp-toolbar">
<button class="xp-nav-btn xp-nav-icon" data-action="back" title="Back" aria-label="Back" ${nav?.canBack ? '' : 'disabled'}><span aria-hidden="true">◀</span></button>
<button class="xp-nav-btn xp-nav-icon" data-action="forward" title="Forward" aria-label="Forward" ${nav?.canForward ? '' : 'disabled'}><span aria-hidden="true">▶</span></button>
<button class="xp-nav-btn xp-nav-icon" data-action="up" title="Up" aria-label="Up"><span aria-hidden="true">⤴</span></button>
<button class="xp-nav-btn" data-action="root" title="My Computer" aria-label="My Computer">🖥️</button>
<div class="xp-breadcrumb">${breadcrumb}</div>
</div>
<div class="xp-explorer-body">
<aside class="xp-sidebar">
<div class="xp-side-title">Quick Access</div>
<button class="xp-side-link" data-jump="/">🖥️ My Computer</button>
<button class="xp-side-link" data-jump="/C:">💽 Local Disk (C:)</button>
<button class="xp-side-link" data-jump="/D:">💾 Data (D:)</button>
<button class="xp-side-link" data-jump="/C:/Documents and Settings/Dion/My Documents">📂 My Documents</button>
<button class="xp-side-link" data-jump="/C:/Documents and Settings/Dion/My Pictures">🖼️ My Pictures</button>
<button class="xp-side-link" data-jump="/D:/Music">🎵 Music</button>
<button class="xp-side-link" data-jump="/Recycle Bin">🗑️ Recycle Bin</button>
</aside>
<div class="xp-main">
<section class="xp-files">
${list || '<div class="xp-empty">This folder is empty.</div>'}
</section>
<aside class="xp-details">
${detailsHtml}
</aside>
</div>
</div>
</div>
`;
}
function renderNotepadApp(filePath, fileName, textValue = '') {
const escapedName = String(fileName || 'Untitled').replace(/"/g, '&quot;');
const escapedPath = String(filePath || '').replace(/"/g, '&quot;');
const escapedText = String(textValue).replace(/[&<>]/g, (m) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[m]));
return `
<div class="xp-notepad" data-file-path="${escapedPath}">
<div class="xp-notepad-toolbar">
<button class="xp-note-btn" data-note-action="new">New</button>
<button class="xp-note-btn" data-note-action="save">Save</button>
<span class="xp-note-file">${escapedName}</span>
</div>
<textarea class="xp-note-editor" spellcheck="false">${escapedText}</textarea>
</div>
`;
}
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 `
<div class="xp-paint">
<div class="xp-paint-toolbar">
<label>Tool
<select class="xp-paint-tool">
<option value="brush">Brush</option>
<option value="eraser">Eraser</option>
</select>
</label>
<label>Color <input class="xp-paint-color" type="color" value="#d13f8c" /></label>
<label>Size <input class="xp-paint-size" type="range" min="1" max="32" value="4" /></label>
<button class="xp-paint-btn" data-paint-action="undo">Undo</button>
<button class="xp-paint-btn" data-paint-action="redo">Redo</button>
<button class="xp-paint-btn" data-paint-action="clear">Clear</button>
<button class="xp-paint-btn" data-paint-action="save">Save PNG</button>
</div>
<div class="xp-paint-stage">
<canvas class="xp-paint-canvas" width="900" height="520"></canvas>
</div>
</div>
`;
}
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 `
<div class="xp-ie" data-url="${htmlEscape(startUrl)}">
<div class="xp-ie-toolbar">
<button class="xp-ie-btn" data-ie-action="back">Back</button>
<button class="xp-ie-btn" data-ie-action="forward">Forward</button>
<button class="xp-ie-btn" data-ie-action="refresh">Refresh</button>
<button class="xp-ie-btn" data-ie-action="home">Home</button>
</div>
<form class="xp-ie-address-row">
<span>Address</span>
<input class="xp-ie-address" type="text" value="${htmlEscape(startUrl)}" />
<button class="xp-ie-btn" type="submit">Go</button>
</form>
<div class="xp-ie-content">Loading...</div>
</div>
`;
}
function wrapRetroShell(page, url) {
const stamp = Math.floor(Math.abs(url.split('').reduce((a, c) => a + c.charCodeAt(0), 0)) % 9000) + 1000;
return `
<div class="ie-retro-shell">
<marquee behavior="alternate" scrollamount="4">★ Welcome to ${htmlEscape(page.title || url)} ★</marquee>
<div class="ie-retro-meta">Visitors: ${stamp} · Best viewed at 800x600 · Netscape/IE compatible</div>
<article class="ie-page">${page.html}</article>
<div class="ie-retro-footer">\u00A9 2001-2006 retro web collective · <a href="home://portal">return to portal</a></div>
</div>
`;
}
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 = '<div class="ie-loading">Dialing up... Loading retro page...</div>';
const page = await getRetroPage(currentUrl);
if (!page) {
body.innerHTML = `
<div class="ie-error">
<h3>The page cannot be displayed</h3>
<p>URL: ${htmlEscape(currentUrl)}</p>
<p>Try <a href="${RETRO_WEB_HOME}">home://portal</a> or run a search.</p>
</div>
`;
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 `
<div class="xp-image-viewer">
<div class="xp-image-toolbar">
<button class="xp-image-btn" data-img-action="zoom-out">-</button>
<button class="xp-image-btn" data-img-action="zoom-in">+</button>
<button class="xp-image-btn" data-img-action="actual">100%</button>
<button class="xp-image-btn" data-img-action="fit">Fit</button>
<span class="xp-image-title">${safeName}</span>
</div>
<div class="xp-image-stage">
<img class="xp-image-content" src="${safeUrl}" alt="${safeName}" />
</div>
</div>
`;
}
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 `
<div class="xp-audio-player">
<div class="xp-audio-top">
<div class="xp-audio-logo">WA</div>
<div class="xp-audio-track-wrap">
<div class="xp-audio-track-marquee">${safeName}${safeName}${safeName}</div>
</div>
</div>
<div class="xp-audio-controls">
<button class="xp-audio-btn" data-audio-action="play">▶</button>
<button class="xp-audio-btn" data-audio-action="pause">⏸</button>
<button class="xp-audio-btn" data-audio-action="stop">⏹</button>
<input class="xp-audio-seek" type="range" min="0" max="1000" value="0" step="1" />
</div>
<div class="xp-audio-time">00:00 / 00:00</div>
<audio class="xp-audio-element" preload="metadata" src="${safeUrl}"></audio>
</div>
`;
}
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: `<p>Cannot open this file type.</p>`,
});
}
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 || '<p>Empty window</p>';
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: '<div class="xp-loading">Loading...</div>',
});
if (winEl) wireExplorerInteractions(winEl, path);
}
function openSystemPanel(kind) {
const panels = {
'control-panel': {
title: 'Control Panel',
html: '<div class="xp-loading">⚙️ Control Panel is under construction.<br/>Try My Documents, Music, Paint, and Notepad from Start.</div>',
},
help: {
title: 'Help and Support',
html: '<div class="xp-loading">❓ Need help? Double-click folders/files in Explorer, drag desktop icons, and use the tray reset/show-desktop buttons.</div>',
},
run: {
title: 'Run',
html: '<div class="xp-loading">🏃 Run shortcut: open Internet, Notepad, Paint, or My Computer from Start.</div>',
},
};
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 = `<span class="clock-time">${timeText}</span><span class="clock-date">${dateText}</span>`;
}
applyInitialLayout();
setInterval(tickClock, 1000);
tickClock();