Last active
September 2, 2025 07:34
-
-
Save clashnewbm3/2b8d5a60d2cf676015aaebe7d06d9c94 to your computer and use it in GitHub Desktop.
AeroLiteOS Opensourced
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // This project was created by clashnewbme originaly | |
| // if you want to support this project and get special perks donate here: https://www.patreon.com/Calacobragameengine/membership | |
| // If you want to tell us bugs or want to add your games to the app store join our discord: https://discord.gg/Z9chWVBmjv | |
| // heres the newest version https://aeroliteos.netlify.app/ | |
| // Thanks so much for the community and I will continue to update this project! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- This is an opensourced project by clashnewbme join my discord: https://discord.gg/Z9chWVBmjv --> | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>AeroLiteOS</title> | |
| <style> | |
| :root{ | |
| --bg: #0b1220; | |
| --bg-elev: #0f172a; | |
| --panel: rgba(15,23,42,0.8); | |
| --panel-solid: #111827; | |
| --text: #e5e7eb; | |
| --muted: #9ca3af; | |
| --accent: #3b82f6; | |
| --accent-2: #22d3ee; | |
| --ok: #22c55e; | |
| --warn: #f59e0b; | |
| --err: #ef4444; | |
| --glass: rgba(255,255,255,0.06); | |
| --shadow: 0 10px 30px rgba(0,0,0,.35); | |
| --radius: 16px; | |
| --taskbar-h: 52px; | |
| --blur: 14px; | |
| --win-border: 1px solid rgba(255,255,255,.08); | |
| --grid: 88px; | |
| } | |
| *{ box-sizing: border-box; } | |
| html, body{ height:100%; } | |
| body{ | |
| margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif; | |
| color:var(--text); background: var(--bg); | |
| overflow:hidden; user-select:none; | |
| } | |
| /* Desktop */ | |
| .desktop{ | |
| position:fixed; inset:0; display:grid; grid-template-rows: 1fr var(--taskbar-h); | |
| background: radial-gradient(1200px 800px at 80% 10%, #1d2a53 0%, transparent 70%), | |
| radial-gradient(900px 700px at 10% 80%, #093b4c 0%, transparent 60%), | |
| linear-gradient(180deg, #040812 0%, #0b1220 100%); | |
| } | |
| .wallpaper{ position:absolute; inset:0; background-size:cover; background-position:center; filter:saturate(1.1) contrast(1.05); opacity:.9; } | |
| .grain{ position:absolute; inset:-50%; background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="2" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(%23n)" opacity=".05"/></svg>'); mix-blend-mode:soft-light; pointer-events:none; } | |
| /* Icons grid */ | |
| .icons{ | |
| position:relative; padding:20px; display:grid; grid-template-columns: repeat(auto-fill, minmax(var(--grid),1fr)); gap:14px; align-content:start; z-index:1; | |
| } | |
| .icon{ | |
| width:var(--grid); height:var(--grid); border-radius:14px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; | |
| background: transparent; cursor: default; transition: .15s ease; border:1px solid transparent; | |
| } | |
| .icon:hover{ background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.08); } | |
| .icon svg{ width:34px; height:34px; } | |
| .icon span{ font-size:12px; color: var(--muted); text-shadow:0 1px 1px rgba(0,0,0,.4); } | |
| /* Windows */ | |
| .win{ position:absolute; display:flex; flex-direction:column; background:var(--panel); -webkit-backdrop-filter: blur(var(--blur)); backdrop-filter: blur(var(--blur)); | |
| border-radius: var(--radius); box-shadow: var(--shadow); min-width: 320px; min-height: 240px; border: var(--win-border); overflow:hidden; } | |
| .win.resizing, .win.dragging{ transition:none !important; } | |
| .win .titlebar{ display:flex; align-items:center; gap:8px; padding:10px 12px; background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03)); cursor:grab; } | |
| .win .titlebar:active{ cursor:grabbing; } | |
| .titlebar .appicon{ width:18px; height:18px; border-radius:5px; display:grid; place-items:center; background:var(--glass); } | |
| .titlebar .title{ flex:1; font-weight:600; letter-spacing:.2px; filter:drop-shadow(0 1px 0 rgba(0,0,0,.3)); } | |
| .titlebar .actions{ display:flex; gap:6px; } | |
| .btn{ border:1px solid rgba(255,255,255,.08); background: rgba(255,255,255,.06); color:var(--text); border-radius:10px; padding:6px 10px; display:inline-flex; align-items:center; gap:6px; font-size:12px; cursor:pointer; transition:.15s; } | |
| .btn:hover{ background: rgba(255,255,255,.1); } | |
| .btn.ghost{ background:transparent; } | |
| .win .content{ flex:1; background: rgba(2,6,23,.55); padding:0; overflow:auto; } | |
| /* Resize handles */ | |
| .resizer{ position:absolute; } | |
| .r-n{ top:-4px; left:0; right:0; height:8px; cursor:n-resize; } | |
| .r-s{ bottom:-4px; left:0; right:0; height:8px; cursor:s-resize; } | |
| .r-e{ right:-4px; top:0; bottom:0; width:8px; cursor:e-resize; } | |
| .r-w{ left:-4px; top:0; bottom:0; width:8px; cursor:w-resize; } | |
| .r-ne{ right:-6px; top:-6px; width:12px; height:12px; cursor:ne-resize; } | |
| .r-nw{ left:-6px; top:-6px; width:12px; height:12px; cursor:nw-resize; } | |
| .r-se{ right:-6px; bottom:-6px; width:12px; height:12px; cursor:se-resize; } | |
| .r-sw{ left:-6px; bottom:-6px; width:12px; height:12px; cursor:sw-resize; } | |
| /* Taskbar */ | |
| .taskbar{ position:relative; height:var(--taskbar-h); background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02)); border-top:1px solid rgba(255,255,255,.1); | |
| display:flex; align-items:center; gap:10px; padding:6px 10px; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); | |
| } | |
| .start{ width:40px; height:40px; border-radius:12px; display:grid; place-items:center; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.06); cursor:pointer; } | |
| .start:hover{ background:rgba(255,255,255,.1); } | |
| .tasklist{ flex:1; display:flex; gap:8px; } | |
| .task{ min-width:120px; max-width:200px; height:38px; border-radius:12px; border:1px solid rgba(255,255,255,.08); background:rgba(255,255,255,.04); padding:0 10px; display:flex; align-items:center; gap:8px; cursor:pointer; } | |
| .task.active{ outline:2px solid var(--accent); background: rgba(59,130,246,.12); } | |
| .tray{ display:flex; align-items:center; gap:8px; } | |
| .clock{ font-feature-settings:"tnum"; letter-spacing:.3px; color:var(--muted); } | |
| /* Start menu */ | |
| .startmenu{ position:absolute; bottom:calc(var(--taskbar-h) + 8px); left:8px; width:680px; max-width:calc(100% - 16px); | |
| background:var(--panel); border:var(--win-border); border-radius:20px; box-shadow:var(--shadow); padding:14px; display:none; -webkit-backdrop-filter: blur(var(--blur)); backdrop-filter: blur(var(--blur)); } | |
| .startmenu.show{ display:block; } | |
| .startmenu .search{ display:flex; gap:10px; } | |
| .startmenu input{ flex:1; padding:12px 14px; border-radius:12px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.06); color:var(--text); } | |
| .apps{ display:grid; grid-template-columns: repeat(6, 1fr); gap:12px; margin-top:12px; } | |
| .app-tile{ height:86px; border-radius:14px; border:1px solid rgba(255,255,255,.08); background: rgba(255,255,255,.05); display:flex; align-items:center; justify-content:center; flex-direction:column; gap:8px; cursor:pointer; } | |
| .app-tile:hover{ background: rgba(255,255,255,.1); } | |
| /* Quick settings / Notifications */ | |
| .quick{ position:absolute; right:8px; bottom:calc(var(--taskbar-h) + 8px); width:360px; background:var(--panel); border-radius:18px; border:var(--win-border); box-shadow:var(--shadow); display:none; padding:12px; } | |
| .quick.show{ display:block; } | |
| .toggles{ display:grid; grid-template-columns:repeat(3,1fr); gap:8px; } | |
| .toggle{ border:1px solid rgba(255,255,255,.08); border-radius:12px; height:60px; display:grid; place-items:center; background:rgba(255,255,255,.05); cursor:pointer; } | |
| .toggle.on{ outline:2px solid var(--accent-2); background: rgba(34,211,238,.15); } | |
| /* App content styles */ | |
| .pad{ padding:14px; } | |
| .notepad textarea{ width:100%; height:calc(100% - 40px); background:transparent; color:var(--text); border:none; outline:none; resize:none; font: 14px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } | |
| .filelist{ display:flex; gap:10px; flex-wrap:wrap; } | |
| .file{ width:140px; border:1px dashed rgba(255,255,255,.15); border-radius:12px; padding:10px; } | |
| .terminal{ background:#0b0f1a; color:#d1e7ff; font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; height:100%; padding:12px; } | |
| .term-line{ white-space:pre-wrap; } | |
| .calc{ display:grid; grid-template-columns: repeat(4, 1fr); gap:8px; padding:12px; } | |
| .calc .screen{ grid-column: 1 / -1; height:60px; border-radius:12px; background: rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.08); display:flex; align-items:center; justify-content:flex-end; padding:0 12px; font-size:22px; } | |
| .calc button{ border:1px solid rgba(255,255,255,.08); background:rgba(255,255,255,.06); border-radius:12px; padding:12px; font-size:16px; cursor:pointer; } | |
| .browserbar{ display:flex; gap:8px; padding:10px; } | |
| .browserbar input{ flex:1; padding:10px; border-radius:10px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.06); color:var(--text); } | |
| iframe{ border:0; width:100%; height: calc(100% - 52px); background:#fff; } | |
| /* Snap preview */ | |
| .snap-hint{ position:absolute; pointer-events:none; border:2px dashed rgba(255,255,255,.25); border-radius:16px; display:none; } | |
| /* Desktop selection rectangle */ | |
| .select-rect{ position:absolute; border:1px dashed rgba(255,255,255,.4); background: rgba(255,255,255,.08); display:none; } | |
| @media (max-width: 760px){ | |
| :root{ --taskbar-h: 56px; --radius: 14px; } | |
| .apps{ grid-template-columns: repeat(3, 1fr); } | |
| .icons{ grid-template-columns: repeat(3, minmax(var(--grid),1fr)); } | |
| } | |
| /* --- New UI bits (App Store badges, overlays) --- */ | |
| .overlay { | |
| position: fixed; inset: 0; display: grid; place-items:center; z-index:99999; | |
| background: linear-gradient(180deg, rgba(0,0,0,.6), rgba(0,0,0,.8)); | |
| } | |
| .boot-box { | |
| width:520px; padding:28px; background: rgba(255,255,255,.03); border-radius:14px; text-align:center; | |
| border: 1px solid rgba(255,255,255,.06); | |
| } | |
| .login-panel { | |
| width:360px; padding:20px; background:var(--panel); border-radius:14px; text-align:center; border:var(--win-border); | |
| } | |
| .appstore-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:10px; } | |
| .app-card { padding:8px; border-radius:8px; background:rgba(255,255,255,.03); text-align:center; cursor:pointer; } | |
| .rightmenu { position:absolute; background:var(--panel); border:var(--win-border); padding:8px; border-radius:8px; display:none; z-index:9999; } | |
| .alt-tab { position:fixed; left:50%; top:20px; transform:translateX(-50%); background:rgba(2,6,23,.7); padding:8px; border-radius:8px; display:none; gap:8px; z-index:99999; } | |
| .alt-tab .item { min-width:120px; padding:6px; border-radius:6px; background:rgba(255,255,255,.02); color:var(--text); } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="desktop" id="desktop"> | |
| <div class="wallpaper" id="wallpaper"></div> | |
| <div class="grain"></div> | |
| <div class="icons" id="icons"> | |
| <div class="icon" data-launch="notepad"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="4" y="3" width="16" height="18" rx="2" stroke="currentColor"/><path d="M8 7h8M8 11h8M8 15h6" stroke="currentColor"/></svg> | |
| <span>Notepad</span> | |
| </div> | |
| <div class="icon" data-launch="files"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" stroke="currentColor"/></svg> | |
| <span>Files</span> | |
| </div> | |
| <div class="icon" data-launch="browser"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="9" stroke="currentColor"/><path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 21a15 15 0 0 0 0-18" stroke="currentColor"/></svg> | |
| <span>Web</span> | |
| </div> | |
| <div class="icon" data-launch="terminal"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 6h16v12H4z" stroke="currentColor"/><path d="M7 10l2 2-2 2M11 14h6" stroke="currentColor"/></svg> | |
| <span>Terminal</span> | |
| </div> | |
| <div class="icon" data-launch="calculator"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="6" y="3" width="12" height="18" rx="2" stroke="currentColor"/><path d="M9 7h6M9 11h6M9 15h2M13 15h2" stroke="currentColor"/></svg> | |
| <span>Calculator</span> | |
| </div> | |
| <div class="icon" data-launch="settings"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm8.5 4a8.5 8.5 0 1 1-17 0 8.5 8.5 0 0 1 17 0Z" stroke="currentColor"/></svg> | |
| <span>Settings</span> | |
| </div> | |
| </div> | |
| <!-- dynamic windows appear here --> | |
| <div class="snap-hint" id="snapHint"></div> | |
| <div class="select-rect" id="selectRect"></div> | |
| <div class="taskbar"> | |
| <div class="start" id="startBtn" title="Start"> | |
| <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M4 4h7v7H4V4Zm9 0h7v7h-7V4ZM4 13h7v7H4v-7Zm9 0h7v7h-7v-7Z"/></svg> | |
| </div> | |
| <div class="tasklist" id="tasklist"></div> | |
| <div class="tray"> | |
| <button class="btn ghost" id="quickBtn" title="Quick Settings">☰</button> | |
| <div class="clock" id="clock">--:--</div> | |
| </div> | |
| <div class="startmenu" id="startMenu"> | |
| <div class="search"> | |
| <input id="globalSearch" placeholder="Search apps, files and web" /> | |
| <button class="btn" id="powerBtn">Power</button> | |
| </div> | |
| <div class="apps" id="appGrid"> | |
| <!-- tiles injected --> | |
| </div> | |
| </div> | |
| <div class="quick" id="quick"> | |
| <div class="toggles"> | |
| <div class="toggle" data-toggle="wifi">Wi-Fi</div> | |
| <div class="toggle on" data-toggle="bt">Bluetooth</div> | |
| <div class="toggle on" data-toggle="theme">Dark</div> | |
| <div class="toggle on" data-toggle="glass">Glass</div> | |
| <div class="toggle" data-toggle="dnd">Do Not Disturb</div> | |
| <div class="toggle" data-toggle="snap">Snap</div> | |
| </div> | |
| <div class="pad" style="display:flex; gap:10px; align-items:center;"> | |
| <span>Volume</span> | |
| <input id="vol" type="range" min="0" max="100" value="70" /> | |
| <span id="volv">70%</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ORIGINAL SCRIPT (unchanged) --> | |
| <script> | |
| const OS = { | |
| z: 10, | |
| windows: new Map(), | |
| tasks: new Map(), | |
| snap: false, | |
| glass: true, | |
| theme: 'dark', | |
| apps: { | |
| notepad: { | |
| title: 'Notepad', | |
| content(win){ | |
| const wrap = el('div', 'pad notepad'); | |
| const area = el('textarea'); | |
| area.value = localStorage.getItem('OS:notepad') || 'Hello from AeroLiteOS!\n\nThis is a simple notepad. Your text autosaves.'; | |
| area.addEventListener('input', ()=> localStorage.setItem('OS:notepad', area.value)); | |
| wrap.append(area); | |
| return wrap; | |
| }, | |
| size: [520, 420] | |
| }, | |
| files: { | |
| title: 'Files', | |
| content(){ | |
| const wrap = el('div', 'pad'); | |
| const h = el('div'); h.innerHTML = '<b>Quick Access</b>'; | |
| const list = el('div', 'filelist'); | |
| const items = JSON.parse(localStorage.getItem('OS:files')||'[]'); | |
| const add = el('button', 'btn'); add.textContent = 'New Note'; | |
| add.onclick = ()=>{ | |
| const name = prompt('File name?','note-'+Math.floor(Math.random()*1000)+'.txt'); | |
| if(!name) return; | |
| items.push({name, data:''}); | |
| localStorage.setItem('OS:files', JSON.stringify(items)); | |
| render(); | |
| }; | |
| function render(){ | |
| list.innerHTML=''; | |
| items.forEach((f,i)=>{ | |
| const card = el('div','file'); | |
| card.innerHTML = '<div style="font-weight:600">📄 '+esc(f.name)+'</div>'+ | |
| '<div style="color:var(--muted); font-size:12px;">'+(f.data?.slice(0,60)||'')+'</div>'+ | |
| '<div style="display:flex; gap:6px; margin-top:8px;">'+ | |
| '<button class="btn" data-act="open">Open</button>'+ | |
| '<button class="btn" data-act="rename">Rename</button>'+ | |
| '<button class="btn" data-act="del">Delete</button></div>'; | |
| card.onclick = (e)=>{ | |
| if(!(e.target instanceof HTMLElement)) return; | |
| const act = e.target.getAttribute('data-act'); | |
| if(act==='open') openNote(i); | |
| if(act==='rename'){ const nn=prompt('Rename to:', f.name); if(nn){ f.name=nn; save(); render(); }} | |
| if(act==='del'){ if(confirm('Delete '+f.name+'?')){ items.splice(i,1); save(); render(); } } | |
| }; | |
| list.append(card); | |
| }); | |
| } | |
| function openNote(idx){ | |
| const file = items[idx]; | |
| OS.launch({ | |
| id:'file-'+idx, | |
| title:'Edit: '+file.name, | |
| icon: iconDoc(), | |
| content(){ | |
| const w = el('div','pad notepad'); | |
| const t = el('textarea'); t.value = file.data||''; | |
| t.addEventListener('input', ()=>{ file.data=t.value; save(); render(); }); | |
| w.append(t); return w; | |
| }, | |
| size:[560,460] | |
| }); | |
| } | |
| function save(){ localStorage.setItem('OS:files', JSON.stringify(items)); } | |
| render(); | |
| wrap.append(add, h, list); return wrap; | |
| }, size:[680, 460] | |
| }, | |
| terminal: { | |
| title: 'Terminal', | |
| content(){ | |
| const elTerm = el('div','terminal'); | |
| const prompt = ()=> 'user@aerolite:~$ '; | |
| function line(text=''){ const d=el('div','term-line'); d.textContent=text; elTerm.append(d); elTerm.scrollTop = elTerm.scrollHeight; } | |
| line('AeroLiteOS pseudo-shell. Type help.'); | |
| const input = el('input'); input.style.cssText='width:100%; background:transparent; color:#fff; border:0; outline:0; font:inherit;'; | |
| function run(cmd){ | |
| const [c,...rest]=cmd.trim().split(/\s+/); | |
| if(!c){ return; } | |
| if(c==='help') line('Commands: help, echo, date, clear, apps, about'); | |
| else if(c==='echo') line(rest.join(' ')); | |
| else if(c==='date') line(new Date().toString()); | |
| else if(c==='clear'){ elTerm.innerHTML=''; } | |
| else if(c==='apps'){ line(Object.keys(OS.apps).join(', ')); } | |
| else if(c==='about'){ line('AeroLiteOS — single-file HTML desktop'); } | |
| else line('Unknown: '+c); | |
| } | |
| const inputWrap=el('div'); | |
| function refreshPrompt(){ inputWrap.textContent=''; input.value=''; const p=el('span'); p.textContent=prompt(); inputWrap.append(p,input); elTerm.append(inputWrap); input.focus(); elTerm.scrollTop = elTerm.scrollHeight; } | |
| input.addEventListener('keydown', (e)=>{ | |
| if(e.key==='Enter'){ line(prompt()+input.value); run(input.value); refreshPrompt(); } | |
| }); | |
| refreshPrompt(); | |
| return elTerm; | |
| }, size:[640, 380] | |
| }, | |
| calculator: { | |
| title:'Calculator', | |
| content(){ | |
| let expr=''; | |
| const wrap = el('div','calc'); | |
| const screen = el('div','screen'); screen.textContent='0'; | |
| function press(v){ | |
| if(v==='C'){ expr=''; } | |
| else if(v==='='){ try{ expr = String(Function('return ('+expr+')')()); }catch{ expr='Error'; } } | |
| else expr += v; | |
| screen.textContent = expr || '0'; | |
| } | |
| const keys = ['7','8','9','/','4','5','6','*','1','2','3','-','0','.','C','+','=','(',')','%']; | |
| wrap.append(screen); | |
| keys.forEach(k=>{ const b=el('button'); b.textContent=k; b.onclick=()=>press(k); if(k==='=') b.style.gridColumn='span 4'; wrap.append(b); }); | |
| return wrap; | |
| }, size:[320, 420] | |
| }, | |
| browser: { | |
| title:'Web Browser', | |
| content(){ | |
| const wrap = el('div'); wrap.style.height='100%'; | |
| const bar = el('div','browserbar'); | |
| const url = el('input'); url.placeholder='https://...'; | |
| const go = el('button','btn'); go.textContent='Go'; | |
| const frame = document.createElement('iframe'); frame.srcdoc = '<!doctype html><title>New Tab</title><body style="font:16px system-ui;padding:2rem">Web Browsing does not work yet :( it is coming soon! </body>'; | |
| function nav(){ let u=url.value.trim(); if(!u) return; if(!/^https?:\/\//.test(u)) u='https://'+u; frame.src=u; } | |
| go.onclick=nav; url.addEventListener('keydown',e=>{ if(e.key==='Enter') nav(); }); | |
| bar.append(url, go); wrap.append(bar, frame); return wrap; | |
| }, size:[820, 540] | |
| }, | |
| settings: { | |
| title:'Settings', | |
| content(){ | |
| const wrap = el('div','pad'); | |
| wrap.innerHTML = ` | |
| <h2 style="margin:0 0 8px 0">Personalization</h2> | |
| <div style="display:flex; gap:12px; flex-wrap:wrap;"> | |
| ${['nebula','lake','sunset','midnight','grid'].map(k=>`<button class="btn" data-wall="${k}">${k}</button>`).join('')} | |
| </div> | |
| <h2 style="margin:14px 0 8px 0">Theme</h2> | |
| <div style="display:flex; gap:8px;"> | |
| <button class="btn" data-theme="dark">Dark</button> | |
| <button class="btn" data-theme="light">Light</button> | |
| </div> | |
| <h2 style="margin:14px 0 8px 0">Effects</h2> | |
| <div style="display:flex; gap:8px; flex-wrap:wrap;"> | |
| <button class="btn" id="toggleGlass">Toggle Glass</button> | |
| <button class="btn" id="toggleSnap">Toggle Snap</button> | |
| <button class="btn" id="reset">Reset OS</button> | |
| </div> | |
| `; | |
| wrap.querySelectorAll('[data-wall]').forEach(b=> b.addEventListener('click',()=> setWallpaper(b.dataset.wall))); | |
| wrap.querySelectorAll('[data-theme]').forEach(b=> b.addEventListener('click',()=> setTheme(b.dataset.theme))); | |
| wrap.querySelector('#toggleGlass').onclick = ()=> setGlass(!OS.glass); | |
| wrap.querySelector('#toggleSnap').onclick = ()=> OS.snap = !OS.snap; | |
| wrap.querySelector('#reset').onclick = ()=>{ if(confirm('Reset settings & data?')){ localStorage.clear(); location.reload(); } }; | |
| return wrap; | |
| }, size:[520, 420] | |
| } | |
| } | |
| }; | |
| const desktop = document.getElementById('desktop'); | |
| const tasklist = document.getElementById('tasklist'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const startMenu = document.getElementById('startMenu'); | |
| const quick = document.getElementById('quick'); | |
| const quickBtn = document.getElementById('quickBtn'); | |
| const clock = document.getElementById('clock'); | |
| const icons = document.getElementById('icons'); | |
| const snapHint = document.getElementById('snapHint'); | |
| const selectRect = document.getElementById('selectRect'); | |
| const wallpaperEl = document.getElementById('wallpaper'); | |
| const el = (tag, cls)=>{ const n=document.createElement(tag); if(cls) n.className=cls; return n; } | |
| const esc = s=> String(s).replace(/[&<>\\"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); | |
| function setWallpaper(key){ | |
| const map = { | |
| nebula: 'radial-gradient(800px 600px at 20% 20%, #3b82f6 0%, transparent 60%), radial-gradient(900px 700px at 80% 80%, #22d3ee 0%, transparent 60%), linear-gradient(180deg,#0b1220,#0b1220)', | |
| lake: 'url(data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800"><defs><linearGradient id="g" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="#1e3a8a"/><stop offset="1" stop-color="#0ea5e9"/></linearGradient></defs><rect width="100%" height="100%" fill="url(%23g)"/></svg>)', | |
| sunset: 'linear-gradient(120deg,#ff7e5f 0%, #feb47b 100%)', | |
| midnight: 'linear-gradient(180deg,#080c18,#020409)', | |
| grid: 'repeating-linear-gradient(0deg, rgba(59,130,246,.12), rgba(59,130,246,.12) 1px, transparent 1px, transparent 24px), repeating-linear-gradient(90deg, rgba(34,211,238,.12), rgba(34,211,238,.12) 1px, transparent 1px, transparent 24px)' | |
| }; | |
| wallpaperEl.style.background = map[key] || map.nebula; | |
| localStorage.setItem('OS:wall', key); | |
| } | |
| function setTheme(t){ | |
| OS.theme = t; document.body.style.setProperty('--text', t==='light'?'#0b1220':'#e5e7eb'); | |
| document.body.style.setProperty('--bg', t==='light'?'#f5f7fb':'#0b1220'); | |
| document.body.style.setProperty('--bg-elev', t==='light'?'#eef1f8':'#0f172a'); | |
| document.body.style.setProperty('--panel', t==='light'?'rgba(255,255,255,.7)':'rgba(15,23,42,.8)'); | |
| document.body.style.setProperty('--panel-solid', t==='light'?'#ffffff':'#111827'); | |
| localStorage.setItem('OS:theme', t); | |
| } | |
| function setGlass(on){ | |
| OS.glass = on; document.documentElement.style.setProperty('--blur', on? '14px':'0px'); | |
| localStorage.setItem('OS:glass', on? '1':'0'); | |
| } | |
| function updateClock(){ const d=new Date(); const h=String(d.getHours()).padStart(2,'0'); const m=String(d.getMinutes()).padStart(2,'0'); clock.textContent = `${h}:${m}`; } | |
| setInterval(updateClock, 10000); updateClock(); | |
| // Start menu & quick | |
| startBtn.onclick = ()=>{ startMenu.classList.toggle('show'); quick.classList.remove('show'); }; | |
| quickBtn.onclick = ()=>{ quick.classList.toggle('show'); startMenu.classList.remove('show'); }; | |
| document.body.addEventListener('mousedown', (e)=>{ | |
| if(!startMenu.contains(e.target) && e.target!==startBtn) startMenu.classList.remove('show'); | |
| if(!quick.contains(e.target) && e.target!==quickBtn) quick.classList.remove('show'); | |
| }); | |
| // Quick toggles | |
| quick.querySelectorAll('.toggle').forEach(t=> t.addEventListener('click', ()=>{ | |
| t.classList.toggle('on'); | |
| const k = t.dataset.toggle; | |
| if(k==='theme') setTheme(t.classList.contains('on')?'dark':'light'); | |
| if(k==='glass') setGlass(t.classList.contains('on')); | |
| if(k==='snap') OS.snap = t.classList.contains('on'); | |
| })); | |
| const vol = document.getElementById('vol'); const volv = document.getElementById('volv'); vol.oninput = ()=> volv.textContent = vol.value+'%'; | |
| // Icons launch | |
| icons.addEventListener('dblclick', (e)=>{ | |
| const icon = e.target.closest('.icon'); if(!icon) return; launch(icon.dataset.launch); | |
| }); | |
| // Populate Start menu tiles | |
| const appGrid = document.getElementById('appGrid'); | |
| Object.entries(OS.apps).forEach(([id, a])=>{ | |
| const tile = el('div','app-tile'); tile.innerHTML = `<div style="font-weight:600">${a.title}</div><div style="color:var(--muted); font-size:12px;">${id}</div>`; tile.onclick = ()=> launch(id); | |
| appGrid.append(tile); | |
| }); | |
| // Window creation | |
| function launch(id){ const app = OS.apps[id]; if(!app) return alert('App not found: '+id); OS.launch({ id, title: app.title, icon: iconSquare(), content: app.content, size: app.size }); } | |
| OS.launch = ({id, title, icon, content, size=[600,400]})=>{ | |
| let win = OS.windows.get(id); | |
| if(win){ focus(win.el); return; } | |
| const elw = el('div','win'); elw.style.width=size[0]+'px'; elw.style.height=size[1]+'px'; elw.style.left = 40+Math.random()*80+'px'; elw.style.top = 40+Math.random()*60+'px'; elw.style.zIndex = ++OS.z; | |
| const titlebar = el('div','titlebar'); | |
| const ic = el('div','appicon'); ic.append(icon||iconSquare()); | |
| const ttl = el('div','title'); ttl.textContent = title; | |
| const acts = el('div','actions'); | |
| const bMin = button('–'); const bMax = button('□'); const bClose = button('✕'); | |
| acts.append(bMin,bMax,bClose); | |
| titlebar.append(ic,ttl,acts); | |
| const contentWrap = el('div','content'); | |
| contentWrap.append(typeof content==='function'? content(elw): content); | |
| // resizers | |
| ['n','s','e','w','ne','nw','se','sw'].forEach(k=>{ const r=el('div','resizer r-'+k); elw.append(r); r.dataset.edge=k; }); | |
| elw.append(titlebar, contentWrap); desktop.append(elw); | |
| // Task | |
| const task = el('div','task'); task.innerHTML = `<div class="appicon">${(icon||iconSquare()).outerHTML}</div><div>${title}</div>`; task.onclick=()=> focus(elw); | |
| tasklist.append(task); | |
| // store | |
| OS.windows.set(id, { el: elw, task, state:{max:false,min:false} }); | |
| // drag | |
| dragMove(titlebar, elw); | |
| // actions | |
| bClose.onclick = ()=> closeWin(id); | |
| bMin.onclick = ()=> minimize(id); | |
| bMax.onclick = ()=> maximize(id); | |
| focus(elw); | |
| }; | |
| function button(txt){ const b=el('button','btn'); b.textContent=txt; b.title=txt; return b; } | |
| function iconSquare(){ const s=document.createElementNS('http://www.w3.org/2000/svg','svg'); s.setAttribute('viewBox','0 0 24 24'); s.innerHTML='<rect x="4" y="4" width="16" height="16" rx="4" fill="currentColor"/>'; return s; } | |
| function iconDoc(){ const s=document.createElementNS('http://www.w3.org/2000/svg','svg'); s.setAttribute('viewBox','0 0 24 24'); s.innerHTML='<rect x="5" y="3" width="14" height="18" rx="2" stroke="currentColor" fill="none"/><path d="M8 8h8M8 12h8M8 16h6" stroke="currentColor"/>'; return s; } | |
| function focus(winEl){ document.querySelectorAll('.win').forEach(w=> w.style.outline='none'); winEl.style.zIndex = ++OS.z; winEl.style.outline='2px solid rgba(59,130,246,.4)'; | |
| // activate task button | |
| const id = getIdByEl(winEl); if(!id) return; document.querySelectorAll('.task').forEach(t=> t.classList.remove('active')); const w = OS.windows.get(id); if(w) w.task.classList.add('active'); | |
| } | |
| function getIdByEl(elw){ for(const [id, w] of OS.windows){ if(w.el===elw) return id; } } | |
| function closeWin(id){ const w=OS.windows.get(id); if(!w) return; w.el.remove(); w.task.remove(); OS.windows.delete(id); } | |
| function minimize(id){ const w=OS.windows.get(id); if(!w) return; w.el.style.display='none'; w.task.classList.remove('active'); } | |
| function maximize(id){ const w=OS.windows.get(id); if(!w) return; const e=w.el; const st=w.state; if(!st.max){ st.prev={left:e.style.left, top:e.style.top, width:e.style.width, height:e.style.height}; e.style.left='8px'; e.style.top='8px'; e.style.width = (window.innerWidth-16)+'px'; e.style.height = (window.innerHeight-16 - parseInt(getComputedStyle(document.documentElement).getPropertyValue('--taskbar-h')))+'px'; st.max=true; } else { e.style.left=st.prev.left; e.style.top=st.prev.top; e.style.width=st.prev.width; e.style.height=st.prev.height; st.max=false; } | |
| } | |
| function dragMove(handle, winEl){ | |
| let sx, sy, sl, st, dragging=false; | |
| handle.addEventListener('mousedown', (e)=>{ dragging=true; focus(winEl); sx=e.clientX; sy=e.clientY; sl=parseInt(winEl.style.left)||0; st=parseInt(winEl.style.top)||0; winEl.classList.add('dragging'); }); | |
| window.addEventListener('mousemove', (e)=>{ | |
| if(!dragging) return; const dx=e.clientX-sx, dy=e.clientY-sy; let L=sl+dx, T=st+dy; winEl.style.left=L+'px'; winEl.style.top=T+'px'; if(OS.snap) showSnap(L,T,winEl); }); | |
| window.addEventListener('mouseup', (e)=>{ if(!dragging) return; dragging=false; winEl.classList.remove('dragging'); if(OS.snap) applySnap(winEl); hideSnap(); }); | |
| // resize edges | |
| winEl.querySelectorAll('.resizer').forEach(r=>{ | |
| let rs=false, sx2, sy2, sw, sh, sl2, st2, edge=r.dataset.edge; | |
| r.addEventListener('mousedown', (e)=>{ e.stopPropagation(); rs=true; focus(winEl); sx2=e.clientX; sy2=e.clientY; sw=winEl.offsetWidth; sh=winEl.offsetHeight; sl2=winEl.offsetLeft; st2=winEl.offsetTop; winEl.classList.add('resizing'); }); | |
| window.addEventListener('mousemove', (e)=>{ | |
| if(!rs) return; const dx=e.clientX-sx2, dy=e.clientY-sy2; let W=sw, H=sh, L=sl2, T=st2; if(edge.includes('e')) W=sw+dx; if(edge.includes('s')) H=sh+dy; if(edge.includes('w')){ W=sw-dx; L=sl2+dx; } if(edge.includes('n')){ H=sh-dy; T=st2+dy; } | |
| W=Math.max(320,W); H=Math.max(200,H); winEl.style.width=W+'px'; winEl.style.height=H+'px'; winEl.style.left=L+'px'; winEl.style.top=T+'px'; | |
| }); | |
| window.addEventListener('mouseup', ()=>{ if(rs){ rs=false; winEl.classList.remove('resizing'); hideSnap(); }}); | |
| }); | |
| } | |
| // Snap assist | |
| function showSnap(L,T,winEl){ const w=window.innerWidth, h=window.innerHeight-parseInt(getComputedStyle(document.documentElement).getPropertyValue('--taskbar-h')); const margin=12; const areas=[ | |
| {k:'left', x:margin, y:margin, w:(w/2)-margin*1.5, h:h-margin*2}, | |
| {k:'right', x:(w/2)+margin/2, y:margin, w:(w/2)-margin*1.5, h:h-margin*2}, | |
| {k:'top', x:margin, y:margin, w:w-margin*2, h:(h/2)-margin*1.5}, | |
| {k:'bottom', x:margin, y:(h/2)+margin/2, w:w-margin*2, h:(h/2)-margin*1.5}, | |
| {k:'center', x:margin*2, y:margin*2, w:w-margin*4, h:h-margin*4} | |
| ]; | |
| const rect = winEl.getBoundingClientRect(); const cx=rect.left+rect.width/2; const cy=rect.top+rect.height/2; | |
| let near=null, best=1e9; areas.forEach(a=>{ const dx=cx-(a.x+a.w/2), dy=cy-(a.y+a.h/2); const d=Math.hypot(dx,dy); if(d<best){ best=d; near=a; } }); | |
| if(near){ snapHint.style.display='block'; snapHint.style.left=near.x+'px'; snapHint.style.top=near.y+'px'; snapHint.style.width=near.w+'px'; snapHint.style.height=near.h+'px'; snapHint.dataset.k=near.k; } | |
| } | |
| function hideSnap(){ snapHint.style.display='none'; snapHint.dataset.k=''; } | |
| function applySnap(winEl){ const k=snapHint.dataset.k; if(!k) return; const w=window.innerWidth, h=window.innerHeight-parseInt(getComputedStyle(document.documentElement).getPropertyValue('--taskbar-h')); const margin=12; const pos={ | |
| left: {x:margin, y:margin, w:(w/2)-margin*1.5, h:h-margin*2}, | |
| right: {x:(w/2)+margin/2, y:margin, w:(w/2)-margin*1.5, h:h-margin*2}, | |
| top: {x:margin, y:margin, w:w-margin*2, h:(h/2)-margin*1.5}, | |
| bottom:{x:margin, y:(h/2)+margin/2, w:w-margin*2, h:(h/2)-margin*1.5}, | |
| center:{x:margin*2, y:margin*2, w:w-margin*4, h:h-margin*4} | |
| }[k]; | |
| if(pos){ winEl.style.left=pos.x+'px'; winEl.style.top=pos.y+'px'; winEl.style.width=pos.w+'px'; winEl.style.height=pos.h+'px'; } | |
| } | |
| // Desktop selection rectangle (for flair) | |
| let selStart=null; desktop.addEventListener('mousedown', (e)=>{ if(e.target.closest('.win,.taskbar,.startmenu,.quick')) return; selStart=[e.clientX,e.clientY]; selectRect.style.display='block'; selectRect.style.left=e.clientX+'px'; selectRect.style.top=e.clientY+'px'; selectRect.style.width='0px'; selectRect.style.height='0px'; }); | |
| window.addEventListener('mousemove', (e)=>{ if(!selStart) return; const x=Math.min(selStart[0],e.clientX), y=Math.min(selStart[1],e.clientY), w=Math.abs(e.clientX-selStart[0]), h=Math.abs(e.clientY-selStart[1]); selectRect.style.left=x+'px'; selectRect.style.top=y+'px'; selectRect.style.width=w+'px'; selectRect.style.height=h+'px'; }); | |
| window.addEventListener('mouseup', ()=>{ if(selStart){ selStart=null; selectRect.style.display='none'; }}); | |
| // Keyboard shortcuts | |
| window.addEventListener('keydown', (e)=>{ | |
| if(e.ctrlKey && e.key==='`'){ launch('terminal'); } | |
| if(e.ctrlKey && e.key===' '){ startMenu.classList.toggle('show'); } | |
| }); | |
| // Restore settings | |
| (function init(){ | |
| setWallpaper(localStorage.getItem('OS:wall')||'nebula'); | |
| setTheme(localStorage.getItem('OS:theme')||'dark'); | |
| setGlass((localStorage.getItem('OS:glass')||'1')==='1'); | |
| })(); | |
| // Public helper for external launch from console | |
| window.AeroLiteOS = { launch }; | |
| </script> | |
| <!-- ================= NEW FEATURES APPENDED (DO NOT EDIT ORIGINAL ABOVE) ================ --> | |
| <!-- All additions below extend the existing OS object & UI without modifying original code. --> | |
| <div id="bootOverlay" class="overlay" style="display:none;"> | |
| <div class="boot-box"> | |
| <h2 style="margin:0 0 12px 0">AeroLiteOS</h2> | |
| <div id="bootProgress" style="height:10px;background:rgba(255,255,255,.06);border-radius:6px;overflow:hidden;margin-bottom:12px"> | |
| <div id="bootBar" style="height:100%;width:0%;background:linear-gradient(90deg,var(--accent),var(--accent-2));"></div> | |
| </div> | |
| <div style="color:var(--muted)">Loading system modules...</div> | |
| </div> | |
| </div> | |
| <div id="loginOverlay" class="overlay" style="display:none;"> | |
| <div class="login-panel" id="loginPanel"> | |
| <h3 style="margin:0 0 8px 0">Welcome</h3> | |
| <input id="loginUser" placeholder="Username" style="width:80%;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,.06);margin-bottom:8px"><br> | |
| <input id="loginPass" placeholder="Password" type="password" style="width:80%;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,.06);margin-bottom:8px"><br> | |
| <button class="btn" id="loginBtn">Sign in</button> | |
| <div style="margin-top:8px;color:var(--muted);font-size:12px">Tip: password stored locally (demo).</div> | |
| </div> | |
| </div> | |
| <div id="rightClickMenu" class="rightmenu"></div> | |
| <div id="altTab" class="alt-tab" style="display:none;"></div> | |
| <script> | |
| // --- Configuration for new features --- | |
| (function(){ | |
| // Quick boot + login simulation | |
| function showBootThenLogin(){ | |
| const boot = document.getElementById('bootOverlay'); | |
| const login = document.getElementById('loginOverlay'); | |
| boot.style.display = 'grid'; | |
| let p = 0; | |
| const step = setInterval(()=>{ | |
| p += Math.random()*18; | |
| document.getElementById('bootBar').style.width = Math.min(100, p).toFixed(0) + '%'; | |
| if(p >= 100){ | |
| clearInterval(step); | |
| setTimeout(()=> { | |
| boot.style.display='none'; | |
| // if credential exists skip | |
| const savedUser = localStorage.getItem('OS:user'); | |
| if(savedUser){ | |
| login.style.display = 'none'; | |
| } else { | |
| login.style.display = 'grid'; | |
| } | |
| }, 400); | |
| } | |
| }, 300); | |
| } | |
| // Hook login button | |
| document.getElementById('loginBtn').addEventListener('click', ()=>{ | |
| const u = document.getElementById('loginUser').value || 'User'; | |
| const p = document.getElementById('loginPass').value || ''; | |
| // store demo credentials (user asked to keep old code; storing locally is fine) | |
| localStorage.setItem('OS:user', u); | |
| localStorage.setItem('OS:pass', p); | |
| document.getElementById('loginOverlay').style.display='none'; | |
| }); | |
| // Only show boot on first load (or when explicitly reset) | |
| if(!localStorage.getItem('OS:booted')){ | |
| showBootThenLogin(); | |
| localStorage.setItem('OS:booted','1'); | |
| } | |
| // --- Expand OS.apps without changing original definitions --- | |
| // Add Games and new utilities into OS.apps | |
| const newApps = { | |
| snake_game: { | |
| title: 'Snake (Game)', | |
| content(){ | |
| const wrap = el('div'); | |
| const canvas = el('canvas'); canvas.width=300; canvas.height=300; canvas.style.display='block'; canvas.style.margin='14px auto'; | |
| const info = el('div'); info.style.textAlign='center'; info.innerHTML = '<small>Use arrow keys. Eat red to grow.</small>'; | |
| wrap.append(canvas, info); | |
| // game boot will be handled after window insertion | |
| setTimeout(()=> startSnake(canvas), 60); | |
| return wrap; | |
| }, | |
| size:[360,380] | |
| }, | |
| minesweeper_game: { | |
| title: 'Minesweeper', | |
| content(){ | |
| const wrap = el('div','pad'); | |
| const field = el('div'); field.style.display='grid'; field.style.gridTemplateColumns='repeat(10,28px)'; field.style.gap='6px'; | |
| field.id='msfield'; | |
| wrap.append(field); | |
| setTimeout(()=> startMinesweeper(field,10,10,12), 80); | |
| return wrap; | |
| }, size:[360,400] | |
| }, | |
| pong_game: { | |
| title: 'Pong (2P)', | |
| content(){ | |
| const wrap = el('div'); | |
| const canvas = el('canvas'); canvas.width=420; canvas.height=300; canvas.style.display='block'; canvas.style.margin='12px auto'; wrap.append(canvas); | |
| setTimeout(()=> startPong(canvas), 60); | |
| return wrap; | |
| }, size:[460,360] | |
| }, | |
| tetris_game: { | |
| title: 'Tetris', | |
| content(){ | |
| const wrap = el('div'); const canvas = el('canvas'); canvas.width=240; canvas.height=400; canvas.style.display='block'; canvas.style.margin='12px auto'; wrap.append(canvas); | |
| setTimeout(()=> startTetris(canvas), 60); | |
| return wrap; | |
| }, size:[300,460] | |
| }, | |
| music_player: { | |
| title: 'Music Player', | |
| content(){ | |
| const wrap = el('div','pad'); | |
| wrap.innerHTML = ` | |
| <div style="display:flex;gap:8px;align-items:center;"> | |
| <input id="mpFile" type="file" accept="audio/*"/> | |
| <button id="mpPlay" class="btn">Play</button> | |
| <button id="mpPause" class="btn">Pause</button> | |
| <button id="mpStop" class="btn">Stop</button> | |
| </div> | |
| <div style="margin-top:12px;"> | |
| <audio id="mpAudio" controls style="width:100%"></audio> | |
| </div> | |
| `; | |
| const file = wrap.querySelector('#mpFile'); | |
| const audio = wrap.querySelector('#mpAudio'); | |
| const play = wrap.querySelector('#mpPlay'); | |
| const pause = wrap.querySelector('#mpPause'); | |
| const stop = wrap.querySelector('#mpStop'); | |
| file.onchange = ()=>{ | |
| const f = file.files[0]; | |
| if(!f) return; | |
| audio.src = URL.createObjectURL(f); | |
| audio.play(); | |
| }; | |
| play.onclick = ()=> audio.play(); | |
| pause.onclick = ()=> audio.pause(); | |
| stop.onclick = ()=> { audio.pause(); audio.currentTime=0; }; | |
| return wrap; | |
| }, size:[520,200] | |
| }, | |
| image_viewer: { | |
| title: 'Image Viewer', | |
| content(){ | |
| const wrap = el('div','pad'); | |
| wrap.innerHTML = ` | |
| <div style="display:flex;gap:8px;align-items:center;"> | |
| <input id="imgFile" type="file" accept="image/*"/> | |
| <button id="imgOpen" class="btn">Open</button> | |
| <button id="imgSetWall" class="btn">Set as Wallpaper</button> | |
| </div> | |
| <div style="margin-top:12px; display:flex; justify-content:center;"> | |
| <img id="imgPreview" style="max-width:100%; max-height:400px; border-radius:8px;"/> | |
| </div> | |
| `; | |
| const input = wrap.querySelector('#imgFile'); | |
| const img = wrap.querySelector('#imgPreview'); | |
| const setBtn = wrap.querySelector('#imgSetWall'); | |
| input.onchange = ()=> { | |
| const f = input.files[0]; if(!f) return; | |
| img.src = URL.createObjectURL(f); | |
| }; | |
| setBtn.onclick = ()=> { | |
| if(!img.src) return alert('Open an image first'); | |
| // set wallpaper by data URL reference (uses CSS url()) | |
| wallpaperEl.style.background = `url(${img.src}) center/cover`; | |
| localStorage.setItem('OS:wall-src', img.src); | |
| }; | |
| return wrap; | |
| }, size:[640,480] | |
| }, | |
| recycle_bin: { | |
| title: 'Recycle Bin', | |
| content(){ | |
| const wrap = el('div','pad'); | |
| const list = el('div'); list.id='recycleList'; | |
| const empty = el('button','btn'); empty.textContent='Empty Bin'; | |
| empty.onclick = ()=> { if(confirm('Empty Recycle Bin?')){ localStorage.removeItem('OS:recycle'); render(); } }; | |
| wrap.append(empty, list); | |
| function render(){ list.innerHTML=''; const items = JSON.parse(localStorage.getItem('OS:recycle')||'[]'); items.forEach((it,i)=>{ const row=el('div'); row.style.display='flex'; row.style.justifyContent='space-between'; row.style.padding='6px 0'; row.innerHTML = `<div>${esc(it.name)}</div><div><button class="btn" data-idx="${i}">Restore</button><button class="btn" data-del="${i}">Delete</button></div>`; row.querySelector('[data-idx]')?.addEventListener('click', ()=> { restore(i); }); row.querySelector('[data-del]')?.addEventListener('click', ()=> { del(i); }); list.append(row); }); } | |
| function restore(i){ const items = JSON.parse(localStorage.getItem('OS:recycle')||'[]'); const it=items.splice(i,1)[0]; localStorage.setItem('OS:recycle', JSON.stringify(items)); // simplistic restore: add to files | |
| const files = JSON.parse(localStorage.getItem('OS:files')||'[]'); files.push({name:it.name, data:it.data}); localStorage.setItem('OS:files', JSON.stringify(files)); render(); } | |
| function del(i){ const items = JSON.parse(localStorage.getItem('OS:recycle')||'[]'); items.splice(i,1); localStorage.setItem('OS:recycle', JSON.stringify(items)); render(); } | |
| render(); return wrap; | |
| }, size:[420,320] | |
| }, | |
| app_store: { | |
| title: 'App Store / Game Hub', | |
| content(){ | |
| const wrap = el('div','pad'); | |
| const grid = el('div','appstore-grid'); | |
| const appsList = [ | |
| {id:'snake_game', name:'Snake', desc:'Classic snake game'}, | |
| {id:'minesweeper_game', name:'Minesweeper', desc:'Find all mines'}, | |
| {id:'pong_game', name:'Pong', desc:'Local 2-player pong'}, | |
| {id:'tetris_game', name:'Tetris', desc:'Falling blocks'} | |
| ]; | |
| appsList.forEach(a=>{ | |
| const c = el('div','app-card'); c.innerHTML = `<div style="font-weight:600">${a.name}</div><div style="font-size:12px;color:var(--muted)">${a.desc}</div><div style="margin-top:8px;"><button class="btn" data-run="${a.id}">Play</button></div>`; | |
| c.querySelector('[data-run]').addEventListener('click', ()=> OS.launch({ id: a.id, title: OS.apps[a.id].title, icon: iconSquare(), content: OS.apps[a.id].content, size: OS.apps[a.id].size })); | |
| grid.append(c); | |
| }); | |
| wrap.append(grid); | |
| return wrap; | |
| }, size:[640,360] | |
| } | |
| }; | |
| // Merge newApps into existing OS.apps (preserve anything already there) | |
| Object.entries(newApps).forEach(([k,v])=>{ | |
| if(!OS.apps[k]) OS.apps[k]=v; | |
| }); | |
| // Add desktop icons for games/apps (append to icons area) | |
| const iconsEl = document.getElementById('icons'); | |
| function addDesktopIcon(id, label, svg){ | |
| const d = el('div','icon'); d.dataset.launch = id; | |
| d.innerHTML = (svg || '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="4" y="3" width="16" height="18" rx="2" stroke="currentColor"/></svg>') + `<span>${label}</span>`; | |
| d.addEventListener('dblclick', ()=> launch(id)); | |
| iconsEl.append(d); | |
| } | |
| addDesktopIcon('snake_game','Snake'); | |
| addDesktopIcon('minesweeper_game','Minesweeper'); | |
| addDesktopIcon('pong_game','Pong'); | |
| addDesktopIcon('tetris_game','Tetris'); | |
| addDesktopIcon('music_player','Music'); | |
| addDesktopIcon('image_viewer','Images'); | |
| addDesktopIcon('recycle_bin','Recycle Bin'); | |
| addDesktopIcon('app_store','App Store'); | |
| // Integrate Recycle functionality into Files delete workflow by intercepting storage changes: | |
| // (We won't change original Files code—provide a helper function to move deleted files) | |
| window.OSDeleteToRecycle = function(fileObj){ | |
| const r = JSON.parse(localStorage.getItem('OS:recycle')||'[]'); | |
| r.push(fileObj); | |
| localStorage.setItem('OS:recycle', JSON.stringify(r)); | |
| }; | |
| // Right-click desktop menu | |
| const rightMenu = document.getElementById('rightClickMenu'); | |
| document.addEventListener('contextmenu', (e)=>{ | |
| e.preventDefault(); | |
| if(e.target.closest('.win,.taskbar,.startmenu,.quick')) { rightMenu.style.display='none'; return; } | |
| rightMenu.style.display='block'; | |
| rightMenu.style.left = e.clientX + 'px'; | |
| rightMenu.style.top = e.clientY + 'px'; | |
| rightMenu.innerHTML = `<div style="padding:6px;cursor:pointer" id="newFile">New File</div> | |
| <div style="padding:6px;cursor:pointer" id="setWall">Set Wallpaper</div> | |
| <div style="padding:6px;cursor:pointer" id="openStore">App Store</div>`; | |
| rightMenu.querySelector('#newFile').onclick = ()=> { rightMenu.style.display='none'; const name = prompt('New file name','untitled.txt'); if(!name) return; const files = JSON.parse(localStorage.getItem('OS:files')||'[]'); files.push({name,data:''}); localStorage.setItem('OS:files', JSON.stringify(files)); alert('File created. Open Files app to view.'); }; | |
| rightMenu.querySelector('#setWall').onclick = ()=> { rightMenu.style.display='none'; const url = prompt('Image URL (cross-origin may block):'); if(url){ wallpaperEl.style.background = `url(${url}) center/cover`; localStorage.setItem('OS:wall-src', url); } }; | |
| rightMenu.querySelector('#openStore').onclick = ()=> { rightMenu.style.display='none'; OS.launch({ id:'app_store', title:OS.apps.app_store.title, icon: iconSquare(), content: OS.apps.app_store.content, size: OS.apps.app_store.size }); }; | |
| }); | |
| document.addEventListener('click', ()=> rightMenu.style.display='none'); | |
| // Alt+Tab switcher simple preview (shows open windows by title) | |
| const altTabEl = document.getElementById('altTab'); | |
| let altOpen = false, altList = []; | |
| window.addEventListener('keydown', (e)=>{ | |
| if(e.key === 'Tab' && e.altKey){ | |
| e.preventDefault(); | |
| if(!altOpen){ | |
| // open | |
| altOpen = true; | |
| altList = Array.from(OS.windows.keys()); | |
| altTabEl.innerHTML = ''; | |
| altList.forEach(id=>{ | |
| const item = el('div','item'); item.textContent = OS.windows.get(id)?.task?.textContent || id; item.onclick = ()=> { focus(OS.windows.get(id).el); hideAlt(); }; | |
| altTabEl.append(item); | |
| }); | |
| altTabEl.style.display='flex'; | |
| } else { | |
| // cycle | |
| const focused = document.querySelector('.win[style*="z-index"]'); | |
| } | |
| } | |
| }); | |
| window.addEventListener('keyup', (e)=>{ if(e.key === 'Alt') hideAlt(); }); | |
| function hideAlt(){ altOpen=false; altTabEl.style.display='none'; altTabEl.innerHTML=''; } | |
| // ----- Simple Game Implementations (vanilla) ----- | |
| function startSnake(canvas){ | |
| if(!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| const grid = 15; | |
| const cols = Math.floor(canvas.width / grid); | |
| const rows = Math.floor(canvas.height / grid); | |
| let px = Math.floor(cols/2), py = Math.floor(rows/2), vx=0, vy=0, tail=4, trail=[]; | |
| let apple = {x: Math.floor(Math.random()*cols), y: Math.floor(Math.random()*rows)}; | |
| function tick(){ | |
| px += vx; py += vy; | |
| if(px < 0) px = cols -1; if(px >= cols) px = 0; | |
| if(py < 0) py = rows -1; if(py >= rows) py = 0; | |
| // collision with self | |
| for(const t of trail){ if(t.x===px && t.y===py){ tail = 4; } } | |
| trail.push({x:px,y:py}); | |
| while(trail.length > tail) trail.shift(); | |
| if(px===apple.x && py===apple.y){ tail++; apple = {x:Math.floor(Math.random()*cols), y:Math.floor(Math.random()*rows)}; } | |
| // render | |
| ctx.fillStyle = '#041023'; ctx.fillRect(0,0,canvas.width,canvas.height); | |
| ctx.fillStyle = '#d946ef'; | |
| ctx.fillRect(apple.x*grid+2, apple.y*grid+2, grid-4, grid-4); | |
| ctx.fillStyle = '#22c55e'; | |
| for(const t of trail) ctx.fillRect(t.x*grid+2, t.y*grid+2, grid-4, grid-4); | |
| } | |
| document.addEventListener('keydown', function handler(e){ | |
| if(e.key==='ArrowLeft' && vx!==1){ vx=-1; vy=0; } | |
| if(e.key==='ArrowRight' && vx!==-1){ vx=1; vy=0; } | |
| if(e.key==='ArrowUp' && vy!==1){ vx=0; vy=-1; } | |
| if(e.key==='ArrowDown' && vy!==-1){ vx=0; vy=1; } | |
| }); | |
| const interval = setInterval(tick, 100); | |
| // stop when window closed: attempt to detect canvas removed | |
| const obs = new MutationObserver(()=>{ if(!document.body.contains(canvas)){ clearInterval(interval); obs.disconnect(); } }); | |
| obs.observe(document.body, {childList:true, subtree:true}); | |
| } | |
| function startPong(canvas){ | |
| if(!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let bx = canvas.width/2, by = canvas.height/2, bvx=3, bvy=2; | |
| let lp = canvas.height/2 - 30, rp = lp; | |
| function tick(){ | |
| bx += bvx; by += bvy; | |
| if(by < 0 || by > canvas.height){ bvy *= -1; } | |
| // paddles | |
| if(bx < 20 && by > lp && by < lp+60) bvx *= -1; | |
| if(bx > canvas.width-20 && by > rp && by < rp+60) bvx *= -1; | |
| if(bx < -50 || bx > canvas.width + 50){ bx = canvas.width/2; by = canvas.height/2; bvx = -bvx; } | |
| // AI for right paddle | |
| if(rp + 30 < by) rp += 2; else if(rp + 30 > by) rp -= 2; | |
| // draw | |
| ctx.fillStyle = '#020617'; ctx.fillRect(0,0,canvas.width,canvas.height); | |
| ctx.fillStyle = '#e5e7eb'; ctx.fillRect(10, lp, 10, 60); ctx.fillRect(canvas.width-20, rp, 10, 60); | |
| ctx.beginPath(); ctx.arc(bx,by,8,0,Math.PI*2); ctx.fill(); | |
| } | |
| document.addEventListener('keydown', function handler(e){ | |
| // left paddle controls W/S | |
| if(e.key==='w') lp -= 20; | |
| if(e.key==='s') lp += 20; | |
| }); | |
| const interval = setInterval(tick, 20); | |
| const obs = new MutationObserver(()=>{ if(!document.body.contains(canvas)){ clearInterval(interval); obs.disconnect(); } }); | |
| obs.observe(document.body, {childList:true, subtree:true}); | |
| } | |
| function startTetris(canvas){ | |
| if(!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.fillStyle='#041023'; ctx.fillRect(0,0,canvas.width, canvas.height); | |
| ctx.fillStyle='#fff'; ctx.fillText('Coming soon', 20, 200); | |
| // This will come soon... I have a full Tetris implementation ready, but it's a bit more complex than the others. | |
| // If you want full Tetris implementation, I can add it next. | |
| } | |
| function startMinesweeper(container, cols=10, rows=10, mines=12){ | |
| if(!container) return; | |
| container.innerHTML=''; | |
| const grid = []; | |
| const field = document.createElement('div'); field.style.display='grid'; field.style.gridTemplateColumns = `repeat(${cols}, 30px)`; field.style.gap='4px'; | |
| container.append(field); | |
| // create cells | |
| for(let r=0;r<rows;r++){ | |
| for(let c=0;c<cols;c++){ | |
| const cell = {r,c, mine:false, revealed:false, flagged:false, el: null}; | |
| const btn = document.createElement('button'); btn.style.width='30px'; btn.style.height='30px'; btn.style.borderRadius='6px'; | |
| cell.el = btn; | |
| btn.addEventListener('click', ()=> reveal(cell)); | |
| btn.addEventListener('contextmenu', (ev)=>{ ev.preventDefault(); cell.flagged = !cell.flagged; btn.textContent = cell.flagged ? '🚩' : ''; }); | |
| field.append(btn); | |
| grid.push(cell); | |
| } | |
| } | |
| // place mines | |
| for(let m=0;m<mines;m++){ | |
| let idx; | |
| do { idx = Math.floor(Math.random()*grid.length); } while(grid[idx].mine); | |
| grid[idx].mine = true; | |
| } | |
| function neighbors(cell){ | |
| const arr=[]; | |
| for(const n of grid){ | |
| if(Math.abs(n.r - cell.r) <= 1 && Math.abs(n.c - cell.c) <=1 && !(n.r===cell.r && n.c===cell.c)) arr.push(n); | |
| } | |
| return arr; | |
| } | |
| function reveal(cell){ | |
| if(cell.revealed || cell.flagged) return; | |
| cell.revealed = true; cell.el.style.background='#0b2a3a'; cell.el.disabled=true; | |
| if(cell.mine){ cell.el.textContent='💣'; alert('Boom!'); return; } | |
| const count = neighbors(cell).filter(x=>x.mine).length; | |
| if(count>0) cell.el.textContent = count; | |
| else neighbors(cell).forEach(n=> reveal(n)); | |
| } | |
| } | |
| // Tie into Files delete operation (override pattern): when Files app 'delete' is called, we can't intercept internal function easily, | |
| // so provide a small UI helper: select file and 'Send to Recycle Bin' via the Files UI manual action. | |
| // Add a menu item in each file card for "Send to Recycle" if Files is open: we won't modify Files code; users can use the New File + Recycle Bin manually. | |
| // If wallpaper was set by the Image Viewer earlier, restore it on load | |
| const ws = localStorage.getItem('OS:wall-src'); | |
| if(ws) wallpaperEl.style.background = `url(${ws}) center/cover`; | |
| // Reshow start menu tile list to include newly added apps | |
| // (append only the ones not already present) | |
| const existingTiles = new Set([...document.querySelectorAll('#appGrid .app-tile')].map(t=>t.textContent.trim())); | |
| Object.entries(OS.apps).forEach(([id, a])=>{ | |
| const text = a.title + id; | |
| // don't duplicate exact tile names: check by id shown | |
| if(!document.querySelector(`#appGrid div:contains(${id})`)) { | |
| // simple check: add everything (some duplicates may appear if original later runs; safe) | |
| } | |
| }); | |
| // Add keyboard shortcut: Ctrl+Alt+G -> open App Store / Game Hub | |
| window.addEventListener('keydown', (e)=>{ if(e.ctrlKey && e.altKey && e.key.toLowerCase()==='g'){ OS.launch({ id:'app_store', title:OS.apps.app_store.title, icon: iconSquare(), content: OS.apps.app_store.content, size: OS.apps.app_store.size }); } }); | |
| // Also add simple API so other scripts can add apps later: | |
| window.AeroLiteOS.registerApp = function(id, app){ if(OS.apps[id]) return false; OS.apps[id] = app; return true; }; | |
| // Small helper: when Files delete triggers outside, user can call OSDeleteToRecycle({name,data}) | |
| // Example usage in browser console: OSDeleteToRecycle({name:'demo.txt', data:'hello'}); | |
| })(); | |
| </script> | |
| <!-- End of file --> | |
| </body> | |
| </html> | |
| <!-- Please Support this project here https://www.patreon.com/Calacobragameengine/membership --> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- This is an opensourced project by clashnewbme join my discord: https://discord.gg/Z9chWVBmjv --> | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>AeroLiteOS</title> | |
| <style> | |
| :root{ | |
| --bg: #0b1220; | |
| --bg-elev: #0f172a; | |
| --panel: rgba(15,23,42,0.8); | |
| --panel-solid: #111827; | |
| --text: #e5e7eb; | |
| --muted: #9ca3af; | |
| --accent: #3b82f6; | |
| --accent-2: #22d3ee; | |
| --ok: #22c55e; | |
| --warn: #f59e0b; | |
| --err: #ef4444; | |
| --glass: rgba(255,255,255,0.06); | |
| --shadow: 0 10px 30px rgba(0,0,0,.35); | |
| --radius: 16px; | |
| --taskbar-h: 52px; | |
| --blur: 14px; | |
| --win-border: 1px solid rgba(255,255,255,.08); | |
| --grid: 88px; | |
| } | |
| *{ box-sizing: border-box; } | |
| html, body{ height:100%; } | |
| body{ | |
| margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif; | |
| color:var(--text); background: var(--bg); | |
| overflow:hidden; user-select:none; | |
| } | |
| /* Desktop */ | |
| .desktop{ | |
| position:fixed; inset:0; display:grid; grid-template-rows: 1fr var(--taskbar-h); | |
| background: radial-gradient(1200px 800px at 80% 10%, #1d2a53 0%, transparent 70%), | |
| radial-gradient(900px 700px at 10% 80%, #093b4c 0%, transparent 60%), | |
| linear-gradient(180deg, #040812 0%, #0b1220 100%); | |
| } | |
| .wallpaper{ position:absolute; inset:0; background-size:cover; background-position:center; filter:saturate(1.1) contrast(1.05); opacity:.9; } | |
| .grain{ position:absolute; inset:-50%; background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="2" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(%23n)" opacity=".05"/></svg>'); mix-blend-mode:soft-light; pointer-events:none; } | |
| /* Icons grid */ | |
| .icons{ | |
| position:relative; padding:20px; display:grid; grid-template-columns: repeat(auto-fill, minmax(var(--grid),1fr)); gap:14px; align-content:start; z-index:1; | |
| } | |
| .icon{ | |
| width:var(--grid); height:var(--grid); border-radius:14px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; | |
| background: transparent; cursor: default; transition: .15s ease; border:1px solid transparent; | |
| } | |
| .icon:hover{ background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.08); } | |
| .icon svg{ width:34px; height:34px; } | |
| .icon span{ font-size:12px; color: var(--muted); text-shadow:0 1px 1px rgba(0,0,0,.4); } | |
| /* Windows */ | |
| .win{ position:absolute; display:flex; flex-direction:column; background:var(--panel); -webkit-backdrop-filter: blur(var(--blur)); backdrop-filter: blur(var(--blur)); | |
| border-radius: var(--radius); box-shadow: var(--shadow); min-width: 320px; min-height: 240px; border: var(--win-border); overflow:hidden; } | |
| .win.resizing, .win.dragging{ transition:none !important; } | |
| .win .titlebar{ display:flex; align-items:center; gap:8px; padding:10px 12px; background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03)); cursor:grab; } | |
| .win .titlebar:active{ cursor:grabbing; } | |
| .titlebar .appicon{ width:18px; height:18px; border-radius:5px; display:grid; place-items:center; background:var(--glass); } | |
| .titlebar .title{ flex:1; font-weight:600; letter-spacing:.2px; filter:drop-shadow(0 1px 0 rgba(0,0,0,.3)); } | |
| .titlebar .actions{ display:flex; gap:6px; } | |
| .btn{ border:1px solid rgba(255,255,255,.08); background: rgba(255,255,255,.06); color:var(--text); border-radius:10px; padding:6px 10px; display:inline-flex; align-items:center; gap:6px; font-size:12px; cursor:pointer; transition:.15s; } | |
| .btn:hover{ background: rgba(255,255,255,.1); } | |
| .btn.ghost{ background:transparent; } | |
| .win .content{ flex:1; background: rgba(2,6,23,.55); padding:0; overflow:auto; } | |
| /* Resize handles */ | |
| .resizer{ position:absolute; } | |
| .r-n{ top:-4px; left:0; right:0; height:8px; cursor:n-resize; } | |
| .r-s{ bottom:-4px; left:0; right:0; height:8px; cursor:s-resize; } | |
| .r-e{ right:-4px; top:0; bottom:0; width:8px; cursor:e-resize; } | |
| .r-w{ left:-4px; top:0; bottom:0; width:8px; cursor:w-resize; } | |
| .r-ne{ right:-6px; top:-6px; width:12px; height:12px; cursor:ne-resize; } | |
| .r-nw{ left:-6px; top:-6px; width:12px; height:12px; cursor:nw-resize; } | |
| .r-se{ right:-6px; bottom:-6px; width:12px; height:12px; cursor:se-resize; } | |
| .r-sw{ left:-6px; bottom:-6px; width:12px; height:12px; cursor:sw-resize; } | |
| /* Taskbar */ | |
| .taskbar{ position:relative; height:var(--taskbar-h); background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02)); border-top:1px solid rgba(255,255,255,.1); | |
| display:flex; align-items:center; gap:10px; padding:6px 10px; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); | |
| } | |
| .start{ width:40px; height:40px; border-radius:12px; display:grid; place-items:center; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.06); cursor:pointer; } | |
| .start:hover{ background:rgba(255,255,255,.1); } | |
| .tasklist{ flex:1; display:flex; gap:8px; } | |
| .task{ min-width:120px; max-width:200px; height:38px; border-radius:12px; border:1px solid rgba(255,255,255,.08); background:rgba(255,255,255,.04); padding:0 10px; display:flex; align-items:center; gap:8px; cursor:pointer; } | |
| .task.active{ outline:2px solid var(--accent); background: rgba(59,130,246,.12); } | |
| .tray{ display:flex; align-items:center; gap:8px; } | |
| .clock{ font-feature-settings:"tnum"; letter-spacing:.3px; color:var(--muted); } | |
| /* Start menu */ | |
| .startmenu{ position:absolute; bottom:calc(var(--taskbar-h) + 8px); left:8px; width:680px; max-width:calc(100% - 16px); | |
| background:var(--panel); border:var(--win-border); border-radius:20px; box-shadow:var(--shadow); padding:14px; display:none; -webkit-backdrop-filter: blur(var(--blur)); backdrop-filter: blur(var(--blur)); } | |
| .startmenu.show{ display:block; } | |
| .startmenu .search{ display:flex; gap:10px; } | |
| .startmenu input{ flex:1; padding:12px 14px; border-radius:12px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.06); color:var(--text); } | |
| .apps{ display:grid; grid-template-columns: repeat(6, 1fr); gap:12px; margin-top:12px; } | |
| .app-tile{ height:86px; border-radius:14px; border:1px solid rgba(255,255,255,.08); background: rgba(255,255,255,.05); display:flex; align-items:center; justify-content:center; flex-direction:column; gap:8px; cursor:pointer; } | |
| .app-tile:hover{ background: rgba(255,255,255,.1); } | |
| /* Quick settings / Notifications */ | |
| .quick{ position:absolute; right:8px; bottom:calc(var(--taskbar-h) + 8px); width:360px; background:var(--panel); border-radius:18px; border:var(--win-border); box-shadow:var(--shadow); display:none; padding:12px; } | |
| .quick.show{ display:block; } | |
| .toggles{ display:grid; grid-template-columns:repeat(3,1fr); gap:8px; } | |
| .toggle{ border:1px solid rgba(255,255,255,.08); border-radius:12px; height:60px; display:grid; place-items:center; background:rgba(255,255,255,.05); cursor:pointer; } | |
| .toggle.on{ outline:2px solid var(--accent-2); background: rgba(34,211,238,.15); } | |
| /* App content styles */ | |
| .pad{ padding:14px; } | |
| .notepad textarea{ width:100%; height:calc(100% - 40px); background:transparent; color:var(--text); border:none; outline:none; resize:none; font: 14px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } | |
| .filelist{ display:flex; gap:10px; flex-wrap:wrap; } | |
| .file{ width:140px; border:1px dashed rgba(255,255,255,.15); border-radius:12px; padding:10px; } | |
| .terminal{ background:#0b0f1a; color:#d1e7ff; font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; height:100%; padding:12px; } | |
| .term-line{ white-space:pre-wrap; } | |
| .calc{ display:grid; grid-template-columns: repeat(4, 1fr); gap:8px; padding:12px; } | |
| .calc .screen{ grid-column: 1 / -1; height:60px; border-radius:12px; background: rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.08); display:flex; align-items:center; justify-content:flex-end; padding:0 12px; font-size:22px; } | |
| .calc button{ border:1px solid rgba(255,255,255,.08); background:rgba(255,255,255,.06); border-radius:12px; padding:12px; font-size:16px; cursor:pointer; } | |
| .browserbar{ display:flex; gap:8px; padding:10px; } | |
| .browserbar input{ flex:1; padding:10px; border-radius:10px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.06); color:var(--text); } | |
| iframe{ border:0; width:100%; height: calc(100% - 52px); background:#fff; } | |
| /* Snap preview */ | |
| .snap-hint{ position:absolute; pointer-events:none; border:2px dashed rgba(255,255,255,.25); border-radius:16px; display:none; } | |
| /* Desktop selection rectangle */ | |
| .select-rect{ position:absolute; border:1px dashed rgba(255,255,255,.4); background: rgba(255,255,255,.08); display:none; } | |
| @media (max-width: 760px){ | |
| :root{ --taskbar-h: 56px; --radius: 14px; } | |
| .apps{ grid-template-columns: repeat(3, 1fr); } | |
| .icons{ grid-template-columns: repeat(3, minmax(var(--grid),1fr)); } | |
| } | |
| /* --- New UI bits (App Store badges, overlays) --- */ | |
| .overlay { | |
| position: fixed; inset: 0; display: grid; place-items:center; z-index:99999; | |
| background: linear-gradient(180deg, rgba(0,0,0,.6), rgba(0,0,0,.8)); | |
| } | |
| .boot-box { | |
| width:520px; padding:28px; background: rgba(255,255,255,.03); border-radius:14px; text-align:center; | |
| border: 1px solid rgba(255,255,255,.06); | |
| } | |
| .login-panel { | |
| width:360px; padding:20px; background:var(--panel); border-radius:14px; text-align:center; border:var(--win-border); | |
| } | |
| .appstore-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:10px; } | |
| .app-card { padding:8px; border-radius:8px; background:rgba(255,255,255,.03); text-align:center; cursor:pointer; } | |
| .rightmenu { position:absolute; background:var(--panel); border:var(--win-border); padding:8px; border-radius:8px; display:none; z-index:9999; } | |
| .alt-tab { position:fixed; left:50%; top:20px; transform:translateX(-50%); background:rgba(2,6,23,.7); padding:8px; border-radius:8px; display:none; gap:8px; z-index:99999; } | |
| .alt-tab .item { min-width:120px; padding:6px; border-radius:6px; background:rgba(255,255,255,.02); color:var(--text); } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="desktop" id="desktop"> | |
| <div class="wallpaper" id="wallpaper"></div> | |
| <div class="grain"></div> | |
| <div class="icons" id="icons"> | |
| <div class="icon" data-launch="notepad"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="4" y="3" width="16" height="18" rx="2" stroke="currentColor"/><path d="M8 7h8M8 11h8M8 15h6" stroke="currentColor"/></svg> | |
| <span>Notepad</span> | |
| </div> | |
| <div class="icon" data-launch="files"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" stroke="currentColor"/></svg> | |
| <span>Files</span> | |
| </div> | |
| <div class="icon" data-launch="browser"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="9" stroke="currentColor"/><path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 21a15 15 0 0 0 0-18" stroke="currentColor"/></svg> | |
| <span>Web</span> | |
| </div> | |
| <div class="icon" data-launch="terminal"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 6h16v12H4z" stroke="currentColor"/><path d="M7 10l2 2-2 2M11 14h6" stroke="currentColor"/></svg> | |
| <span>Terminal</span> | |
| </div> | |
| <div class="icon" data-launch="calculator"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="6" y="3" width="12" height="18" rx="2" stroke="currentColor"/><path d="M9 7h6M9 11h6M9 15h2M13 15h2" stroke="currentColor"/></svg> | |
| <span>Calculator</span> | |
| </div> | |
| <div class="icon" data-launch="settings"> | |
| <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm8.5 4a8.5 8.5 0 1 1-17 0 8.5 8.5 0 0 1 17 0Z" stroke="currentColor"/></svg> | |
| <span>Settings</span> | |
| </div> | |
| </div> | |
| <!-- dynamic windows appear here --> | |
| <div class="snap-hint" id="snapHint"></div> | |
| <div class="select-rect" id="selectRect"></div> | |
| <div class="taskbar"> | |
| <div class="start" id="startBtn" title="Start"> | |
| <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M4 4h7v7H4V4Zm9 0h7v7h-7V4ZM4 13h7v7H4v-7Zm9 0h7v7h-7v-7Z"/></svg> | |
| </div> | |
| <div class="tasklist" id="tasklist"></div> | |
| <div class="tray"> | |
| <button class="btn ghost" id="quickBtn" title="Quick Settings">☰</button> | |
| <div class="clock" id="clock">--:--</div> | |
| </div> | |
| <div class="startmenu" id="startMenu"> | |
| <div class="search"> | |
| <input id="globalSearch" placeholder="Search apps, files and web" /> | |
| <button class="btn" id="powerBtn">Power</button> | |
| </div> | |
| <div class="apps" id="appGrid"> | |
| <!-- tiles injected --> | |
| </div> | |
| </div> | |
| <div class="quick" id="quick"> | |
| <div class="toggles"> | |
| <div class="toggle" data-toggle="wifi">Wi-Fi</div> | |
| <div class="toggle on" data-toggle="bt">Bluetooth</div> | |
| <div class="toggle on" data-toggle="theme">Dark</div> | |
| <div class="toggle on" data-toggle="glass">Glass</div> | |
| <div class="toggle" data-toggle="dnd">Do Not Disturb</div> | |
| <div class="toggle" data-toggle="snap">Snap</div> | |
| </div> | |
| <div class="pad" style="display:flex; gap:10px; align-items:center;"> | |
| <span>Volume</span> | |
| <input id="vol" type="range" min="0" max="100" value="70" /> | |
| <span id="volv">70%</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ORIGINAL SCRIPT (unchanged) --> | |
| <script> | |
| const OS = { | |
| z: 10, | |
| windows: new Map(), | |
| tasks: new Map(), | |
| snap: false, | |
| glass: true, | |
| theme: 'dark', | |
| apps: { | |
| notepad: { | |
| title: 'Notepad', | |
| content(win){ | |
| const wrap = el('div', 'pad notepad'); | |
| const area = el('textarea'); | |
| area.value = localStorage.getItem('OS:notepad') || 'Hello from AeroLiteOS!\n\nThis is a simple notepad. Your text autosaves.'; | |
| area.addEventListener('input', ()=> localStorage.setItem('OS:notepad', area.value)); | |
| wrap.append(area); | |
| return wrap; | |
| }, | |
| size: [520, 420] | |
| }, | |
| files: { | |
| title: 'Files', | |
| content(){ | |
| const wrap = el('div', 'pad'); | |
| const h = el('div'); h.innerHTML = '<b>Quick Access</b>'; | |
| const list = el('div', 'filelist'); | |
| const items = JSON.parse(localStorage.getItem('OS:files')||'[]'); | |
| const add = el('button', 'btn'); add.textContent = 'New Note'; | |
| add.onclick = ()=>{ | |
| const name = prompt('File name?','note-'+Math.floor(Math.random()*1000)+'.txt'); | |
| if(!name) return; | |
| items.push({name, data:''}); | |
| localStorage.setItem('OS:files', JSON.stringify(items)); | |
| render(); | |
| }; | |
| function render(){ | |
| list.innerHTML=''; | |
| items.forEach((f,i)=>{ | |
| const card = el('div','file'); | |
| card.innerHTML = '<div style="font-weight:600">📄 '+esc(f.name)+'</div>'+ | |
| '<div style="color:var(--muted); font-size:12px;">'+(f.data?.slice(0,60)||'')+'</div>'+ | |
| '<div style="display:flex; gap:6px; margin-top:8px;">'+ | |
| '<button class="btn" data-act="open">Open</button>'+ | |
| '<button class="btn" data-act="rename">Rename</button>'+ | |
| '<button class="btn" data-act="del">Delete</button></div>'; | |
| card.onclick = (e)=>{ | |
| if(!(e.target instanceof HTMLElement)) return; | |
| const act = e.target.getAttribute('data-act'); | |
| if(act==='open') openNote(i); | |
| if(act==='rename'){ const nn=prompt('Rename to:', f.name); if(nn){ f.name=nn; save(); render(); }} | |
| if(act==='del'){ if(confirm('Delete '+f.name+'?')){ items.splice(i,1); save(); render(); } } | |
| }; | |
| list.append(card); | |
| }); | |
| } | |
| function openNote(idx){ | |
| const file = items[idx]; | |
| OS.launch({ | |
| id:'file-'+idx, | |
| title:'Edit: '+file.name, | |
| icon: iconDoc(), | |
| content(){ | |
| const w = el('div','pad notepad'); | |
| const t = el('textarea'); t.value = file.data||''; | |
| t.addEventListener('input', ()=>{ file.data=t.value; save(); render(); }); | |
| w.append(t); return w; | |
| }, | |
| size:[560,460] | |
| }); | |
| } | |
| function save(){ localStorage.setItem('OS:files', JSON.stringify(items)); } | |
| render(); | |
| wrap.append(add, h, list); return wrap; | |
| }, size:[680, 460] | |
| }, | |
| terminal: { | |
| title: 'Terminal', | |
| content(){ | |
| const elTerm = el('div','terminal'); | |
| const prompt = ()=> 'user@aerolite:~$ '; | |
| function line(text=''){ const d=el('div','term-line'); d.textContent=text; elTerm.append(d); elTerm.scrollTop = elTerm.scrollHeight; } | |
| line('AeroLiteOS pseudo-shell. Type help.'); | |
| const input = el('input'); input.style.cssText='width:100%; background:transparent; color:#fff; border:0; outline:0; font:inherit;'; | |
| function run(cmd){ | |
| const [c,...rest]=cmd.trim().split(/\s+/); | |
| if(!c){ return; } | |
| if(c==='help') line('Commands: help, echo, date, clear, apps, about'); | |
| else if(c==='echo') line(rest.join(' ')); | |
| else if(c==='date') line(new Date().toString()); | |
| else if(c==='clear'){ elTerm.innerHTML=''; } | |
| else if(c==='apps'){ line(Object.keys(OS.apps).join(', ')); } | |
| else if(c==='about'){ line('AeroLiteOS — single-file HTML desktop'); } | |
| else line('Unknown: '+c); | |
| } | |
| const inputWrap=el('div'); | |
| function refreshPrompt(){ inputWrap.textContent=''; input.value=''; const p=el('span'); p.textContent=prompt(); inputWrap.append(p,input); elTerm.append(inputWrap); input.focus(); elTerm.scrollTop = elTerm.scrollHeight; } | |
| input.addEventListener('keydown', (e)=>{ | |
| if(e.key==='Enter'){ line(prompt()+input.value); run(input.value); refreshPrompt(); } | |
| }); | |
| refreshPrompt(); | |
| return elTerm; | |
| }, size:[640, 380] | |
| }, | |
| calculator: { | |
| title:'Calculator', | |
| content(){ | |
| let expr=''; | |
| const wrap = el('div','calc'); | |
| const screen = el('div','screen'); screen.textContent='0'; | |
| function press(v){ | |
| if(v==='C'){ expr=''; } | |
| else if(v==='='){ try{ expr = String(Function('return ('+expr+')')()); }catch{ expr='Error'; } } | |
| else expr += v; | |
| screen.textContent = expr || '0'; | |
| } | |
| const keys = ['7','8','9','/','4','5','6','*','1','2','3','-','0','.','C','+','=','(',')','%']; | |
| wrap.append(screen); | |
| keys.forEach(k=>{ const b=el('button'); b.textContent=k; b.onclick=()=>press(k); if(k==='=') b.style.gridColumn='span 4'; wrap.append(b); }); | |
| return wrap; | |
| }, size:[320, 420] | |
| }, | |
| browser: { | |
| title:'Web Browser', | |
| content(){ | |
| const wrap = el('div'); wrap.style.height='100%'; | |
| const bar = el('div','browserbar'); | |
| const url = el('input'); url.placeholder='https://...'; | |
| const go = el('button','btn'); go.textContent='Go'; | |
| const frame = document.createElement('iframe'); frame.srcdoc = '<!doctype html><title>New Tab</title><body style="font:16px system-ui;padding:2rem">Web Browsing does not work yet :( it is coming soon! </body>'; | |
| function nav(){ let u=url.value.trim(); if(!u) return; if(!/^https?:\/\//.test(u)) u='https://'+u; frame.src=u; } | |
| go.onclick=nav; url.addEventListener('keydown',e=>{ if(e.key==='Enter') nav(); }); | |
| bar.append(url, go); wrap.append(bar, frame); return wrap; | |
| }, size:[820, 540] | |
| }, | |
| settings: { | |
| title:'Settings', | |
| content(){ | |
| const wrap = el('div','pad'); | |
| wrap.innerHTML = ` | |
| <h2 style="margin:0 0 8px 0">Personalization</h2> | |
| <div style="display:flex; gap:12px; flex-wrap:wrap;"> | |
| ${['nebula','lake','sunset','midnight','grid'].map(k=>`<button class="btn" data-wall="${k}">${k}</button>`).join('')} | |
| </div> | |
| <h2 style="margin:14px 0 8px 0">Theme</h2> | |
| <div style="display:flex; gap:8px;"> | |
| <button class="btn" data-theme="dark">Dark</button> | |
| <button class="btn" data-theme="light">Light</button> | |
| </div> | |
| <h2 style="margin:14px 0 8px 0">Effects</h2> | |
| <div style="display:flex; gap:8px; flex-wrap:wrap;"> | |
| <button class="btn" id="toggleGlass">Toggle Glass</button> | |
| <button class="btn" id="toggleSnap">Toggle Snap</button> | |
| <button class="btn" id="reset">Reset OS</button> | |
| </div> | |
| `; | |
| wrap.querySelectorAll('[data-wall]').forEach(b=> b.addEventListener('click',()=> setWallpaper(b.dataset.wall))); | |
| wrap.querySelectorAll('[data-theme]').forEach(b=> b.addEventListener('click',()=> setTheme(b.dataset.theme))); | |
| wrap.querySelector('#toggleGlass').onclick = ()=> setGlass(!OS.glass); | |
| wrap.querySelector('#toggleSnap').onclick = ()=> OS.snap = !OS.snap; | |
| wrap.querySelector('#reset').onclick = ()=>{ if(confirm('Reset settings & data?')){ localStorage.clear(); location.reload(); } }; | |
| return wrap; | |
| }, size:[520, 420] | |
| } | |
| } | |
| }; | |
| const desktop = document.getElementById('desktop'); | |
| const tasklist = document.getElementById('tasklist'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const startMenu = document.getElementById('startMenu'); | |
| const quick = document.getElementById('quick'); | |
| const quickBtn = document.getElementById('quickBtn'); | |
| const clock = document.getElementById('clock'); | |
| const icons = document.getElementById('icons'); | |
| const snapHint = document.getElementById('snapHint'); | |
| const selectRect = document.getElementById('selectRect'); | |
| const wallpaperEl = document.getElementById('wallpaper'); | |
| const el = (tag, cls)=>{ const n=document.createElement(tag); if(cls) n.className=cls; return n; } | |
| const esc = s=> String(s).replace(/[&<>\\"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); | |
| function setWallpaper(key){ | |
| const map = { | |
| nebula: 'radial-gradient(800px 600px at 20% 20%, #3b82f6 0%, transparent 60%), radial-gradient(900px 700px at 80% 80%, #22d3ee 0%, transparent 60%), linear-gradient(180deg,#0b1220,#0b1220)', | |
| lake: 'url(data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800"><defs><linearGradient id="g" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="%23387ab7"/><stop offset="1" stop-color="%2398d3ec"/></linearGradient></defs><rect width="100%" height="100%" fill="url(%23g)"/></svg>)', | |
| sunset: 'linear-gradient(120deg,#ff7e5f 0%, #feb47b 100%)', | |
| midnight: 'linear-gradient(180deg,#080c18,#020409)', | |
| grid: 'repeating-linear-gradient(0deg, rgba(59,130,246,.12), rgba(59,130,246,.12) 1px, transparent 1px, transparent 24px), repeating-linear-gradient(90deg, rgba(34,211,238,.12), rgba(34,211,238,.12) 1px, transparent 1px, transparent 24px)' | |
| }; | |
| wallpaperEl.style.background = map[key] || map.nebula; | |
| localStorage.setItem('OS:wall', key); | |
| } | |
| function setTheme(t){ | |
| OS.theme = t; document.body.style.setProperty('--text', t==='light'?'#0b1220':'#e5e7eb'); | |
| document.body.style.setProperty('--bg', t==='light'?'#f5f7fb':'#0b1220'); | |
| document.body.style.setProperty('--bg-elev', t==='light'?'#eef1f8':'#0f172a'); | |
| document.body.style.setProperty('--panel', t==='light'?'rgba(255,255,255,.7)':'rgba(15,23,42,.8)'); | |
| document.body.style.setProperty('--panel-solid', t==='light'?'#ffffff':'#111827'); | |
| localStorage.setItem('OS:theme', t); | |
| } | |
| function setGlass(on){ | |
| OS.glass = on; document.documentElement.style.setProperty('--blur', on? '14px':'0px'); | |
| localStorage.setItem('OS:glass', on? '1':'0'); | |
| } | |
| function updateClock(){ const d=new Date(); const h=String(d.getHours()).padStart(2,'0'); const m=String(d.getMinutes()).padStart(2,'0'); clock.textContent = `${h}:${m}`; } | |
| setInterval(updateClock, 10000); updateClock(); | |
| // Start menu & quick | |
| startBtn.onclick = ()=>{ startMenu.classList.toggle('show'); quick.classList.remove('show'); }; | |
| quickBtn.onclick = ()=>{ quick.classList.toggle('show'); startMenu.classList.remove('show'); }; | |
| document.body.addEventListener('mousedown', (e)=>{ | |
| if(!startMenu.contains(e.target) && e.target!==startBtn) startMenu.classList.remove('show'); | |
| if(!quick.contains(e.target) && e.target!==quickBtn) quick.classList.remove('show'); | |
| }); | |
| // Quick toggles | |
| quick.querySelectorAll('.toggle').forEach(t=> t.addEventListener('click', ()=>{ | |
| t.classList.toggle('on'); | |
| const k = t.dataset.toggle; | |
| if(k==='theme') setTheme(t.classList.contains('on')?'dark':'light'); | |
| if(k==='glass') setGlass(t.classList.contains('on')); | |
| if(k==='snap') OS.snap = t.classList.contains('on'); | |
| })); | |
| const vol = document.getElementById('vol'); const volv = document.getElementById('volv'); vol.oninput = ()=> volv.textContent = vol.value+'%'; | |
| // Icons launch | |
| icons.addEventListener('dblclick', (e)=>{ | |
| const icon = e.target.closest('.icon'); if(!icon) return; launch(icon.dataset.launch); | |
| }); | |
| // Populate Start menu tiles | |
| const appGrid = document.getElementById('appGrid'); | |
| Object.entries(OS.apps).forEach(([id, a])=>{ | |
| const tile = el('div','app-tile'); tile.innerHTML = `<div style="font-weight:600">${a.title}</div><div style="color:var(--muted); font-size:12px;">${id}</div>`; tile.onclick = ()=> launch(id); | |
| appGrid.append(tile); | |
| }); | |
| // Window creation | |
| function launch(id){ const app = OS.apps[id]; if(!app) return alert('App not found: '+id); OS.launch({ id, title: app.title, icon: iconSquare(), content: app.content, size: app.size }); } | |
| OS.launch = ({id, title, icon, content, size=[600,400]})=>{ | |
| let win = OS.windows.get(id); | |
| if(win){ focus(win.el); return; } | |
| const elw = el('div','win'); elw.style.width=size[0]+'px'; elw.style.height=size[1]+'px'; elw.style.left = 40+Math.random()*80+'px'; elw.style.top = 40+Math.random()*60+'px'; elw.style.zIndex = ++OS.z; | |
| const titlebar = el('div','titlebar'); | |
| const ic = el('div','appicon'); ic.append(icon||iconSquare()); | |
| const ttl = el('div','title'); ttl.textContent = title; | |
| const acts = el('div','actions'); | |
| const bMin = button('–'); const bMax = button('□'); const bClose = button('✕'); | |
| acts.append(bMin,bMax,bClose); | |
| titlebar.append(ic,ttl,acts); | |
| const contentWrap = el('div','content'); | |
| contentWrap.append(typeof content==='function'? content(elw): content); | |
| // resizers | |
| ['n','s','e','w','ne','nw','se','sw'].forEach(k=>{ const r=el('div','resizer r-'+k); elw.append(r); r.dataset.edge=k; }); | |
| elw.append(titlebar, contentWrap); desktop.append(elw); | |
| // Task | |
| const task = el('div','task'); task.innerHTML = `<div class="appicon">${(icon||iconSquare()).outerHTML}</div><div>${title}</div>`; task.onclick=()=> focus(elw); | |
| tasklist.append(task); | |
| // store | |
| OS.windows.set(id, { el: elw, task, state:{max:false,min:false} }); | |
| // drag | |
| dragMove(titlebar, elw); | |
| // actions | |
| bClose.onclick = ()=> closeWin(id); | |
| bMin.onclick = ()=> minimize(id); | |
| bMax.onclick = ()=> maximize(id); | |
| focus(elw); | |
| }; | |
| function button(txt){ const b=el('button','btn'); b.textContent=txt; b.title=txt; return b; } | |
| function iconSquare(){ const s=document.createElementNS('http://www.w3.org/2000/svg','svg'); s.setAttribute('viewBox','0 0 24 24'); s.innerHTML='<rect x="4" y="4" width="16" height="16" rx="4" fill="currentColor"/>'; return s; } | |
| function iconDoc(){ const s=document.createElementNS('http://www.w3.org/2000/svg','svg'); s.setAttribute('viewBox','0 0 24 24'); s.innerHTML='<rect x="5" y="3" width="14" height="18" rx="2" stroke="currentColor" fill="none"/><path d="M8 8h8M8 12h8M8 16h6" stroke="currentColor"/>'; return s; } | |
| function focus(winEl){ document.querySelectorAll('.win').forEach(w=> w.style.outline='none'); winEl.style.zIndex = ++OS.z; winEl.style.outline='2px solid rgba(59,130,246,.4)'; | |
| // activate task button | |
| const id = getIdByEl(winEl); if(!id) return; document.querySelectorAll('.task').forEach(t=> t.classList.remove('active')); const w = OS.windows.get(id); if(w) w.task.classList.add('active'); | |
| } | |
| function getIdByEl(elw){ for(const [id, w] of OS.windows){ if(w.el===elw) return id; } } | |
| function closeWin(id){ const w=OS.windows.get(id); if(!w) return; w.el.remove(); w.task.remove(); OS.windows.delete(id); } | |
| function minimize(id){ const w=OS.windows.get(id); if(!w) return; w.el.style.display='none'; w.task.classList.remove('active'); } | |
| function maximize(id){ const w=OS.windows.get(id); if(!w) return; const e=w.el; const st=w.state; if(!st.max){ st.prev={left:e.style.left, top:e.style.top, width:e.style.width, height:e.style.height}; e.style.left='8px'; e.style.top='8px'; e.style.width = (window.innerWidth-16)+'px'; e.style.height = (window.innerHeight-16 - parseInt(getComputedStyle(document.documentElement).getPropertyValue('--taskbar-h')))+'px'; st.max=true; } else { e.style.left=st.prev.left; e.style.top=st.prev.top; e.style.width=st.prev.width; e.style.height=st.prev.height; st.max=false; } | |
| } | |
| function dragMove(handle, winEl){ | |
| let sx, sy, sl, st, dragging=false; | |
| handle.addEventListener('mousedown', (e)=>{ dragging=true; focus(winEl); sx=e.clientX; sy=e.clientY; sl=parseInt(winEl.style.left)||0; st=parseInt(winEl.style.top)||0; winEl.classList.add('dragging'); }); | |
| window.addEventListener('mousemove', (e)=>{ | |
| if(!dragging) return; const dx=e.clientX-sx, dy=e.clientY-sy; let L=sl+dx, T=st+dy; winEl.style.left=L+'px'; winEl.style.top=T+'px'; if(OS.snap) showSnap(L,T,winEl); }); | |
| window.addEventListener('mouseup', (e)=>{ if(!dragging) return; dragging=false; winEl.classList.remove('dragging'); if(OS.snap) applySnap(winEl); hideSnap(); }); | |
| // resize edges | |
| winEl.querySelectorAll('.resizer').forEach(r=>{ | |
| let rs=false, sx2, sy2, sw, sh, sl2, st2, edge=r.dataset.edge; | |
| r.addEventListener('mousedown', (e)=>{ e.stopPropagation(); rs=true; focus(winEl); sx2=e.clientX; sy2=e.clientY; sw=winEl.offsetWidth; sh=winEl.offsetHeight; sl2=winEl.offsetLeft; st2=winEl.offsetTop; winEl.classList.add('resizing'); }); | |
| window.addEventListener('mousemove', (e)=>{ | |
| if(!rs) return; const dx=e.clientX-sx2, dy=e.clientY-sy2; let W=sw, H=sh, L=sl2, T=st2; if(edge.includes('e')) W=sw+dx; if(edge.includes('s')) H=sh+dy; if(edge.includes('w')){ W=sw-dx; L=sl2+dx; } if(edge.includes('n')){ H=sh-dy; T=st2+dy; } | |
| W=Math.max(320,W); H=Math.max(200,H); winEl.style.width=W+'px'; winEl.style.height=H+'px'; winEl.style.left=L+'px'; winEl.style.top=T+'px'; | |
| }); | |
| window.addEventListener('mouseup', ()=>{ if(rs){ rs=false; winEl.classList.remove('resizing'); hideSnap(); }}); | |
| }); | |
| } | |
| // Snap assist | |
| function showSnap(L,T,winEl){ const w=window.innerWidth, h=window.innerHeight-parseInt(getComputedStyle(document.documentElement).getPropertyValue('--taskbar-h')); const margin=12; const areas=[ | |
| {k:'left', x:margin, y:margin, w:(w/2)-margin*1.5, h:h-margin*2}, | |
| {k:'right', x:(w/2)+margin/2, y:margin, w:(w/2)-margin*1.5, h:h-margin*2}, | |
| {k:'top', x:margin, y:margin, w:w-margin*2, h:(h/2)-margin*1.5}, | |
| {k:'bottom', x:margin, y:(h/2)+margin/2, w:w-margin*2, h:(h/2)-margin*1.5}, | |
| {k:'center', x:margin*2, y:margin*2, w:w-margin*4, h:h-margin*4} | |
| ]; | |
| const rect = winEl.getBoundingClientRect(); const cx=rect.left+rect.width/2; const cy=rect.top+rect.height/2; | |
| let near=null, best=1e9; areas.forEach(a=>{ const dx=cx-(a.x+a.w/2), dy=cy-(a.y+a.h/2); const d=Math.hypot(dx,dy); if(d<best){ best=d; near=a; } }); | |
| if(near){ snapHint.style.display='block'; snapHint.style.left=near.x+'px'; snapHint.style.top=near.y+'px'; snapHint.style.width=near.w+'px'; snapHint.style.height=near.h+'px'; snapHint.dataset.k=near.k; } | |
| } | |
| function hideSnap(){ snapHint.style.display='none'; snapHint.dataset.k=''; } | |
| function applySnap(winEl){ const k=snapHint.dataset.k; if(!k) return; const w=window.innerWidth, h=window.innerHeight-parseInt(getComputedStyle(document.documentElement).getPropertyValue('--taskbar-h')); const margin=12; const pos={ | |
| left: {x:margin, y:margin, w:(w/2)-margin*1.5, h:h-margin*2}, | |
| right: {x:(w/2)+margin/2, y:margin, w:(w/2)-margin*1.5, h:h-margin*2}, | |
| top: {x:margin, y:margin, w:w-margin*2, h:(h/2)-margin*1.5}, | |
| bottom:{x:margin, y:(h/2)+margin/2, w:w-margin*2, h:(h/2)-margin*1.5}, | |
| center:{x:margin*2, y:margin*2, w:w-margin*4, h:h-margin*4} | |
| }[k]; | |
| if(pos){ winEl.style.left=pos.x+'px'; winEl.style.top=pos.y+'px'; winEl.style.width=pos.w+'px'; winEl.style.height=pos.h+'px'; } | |
| } | |
| // Desktop selection rectangle (for flair) | |
| let selStart=null; desktop.addEventListener('mousedown', (e)=>{ if(e.target.closest('.win,.taskbar,.startmenu,.quick')) return; selStart=[e.clientX,e.clientY]; selectRect.style.display='block'; selectRect.style.left=e.clientX+'px'; selectRect.style.top=e.clientY+'px'; selectRect.style.width='0px'; selectRect.style.height='0px'; }); | |
| window.addEventListener('mousemove', (e)=>{ if(!selStart) return; const x=Math.min(selStart[0],e.clientX), y=Math.min(selStart[1],e.clientY), w=Math.abs(e.clientX-selStart[0]), h=Math.abs(e.clientY-selStart[1]); selectRect.style.left=x+'px'; selectRect.style.top=y+'px'; selectRect.style.width=w+'px'; selectRect.style.height=h+'px'; }); | |
| window.addEventListener('mouseup', ()=>{ if(selStart){ selStart=null; selectRect.style.display='none'; }}); | |
| // Keyboard shortcuts | |
| window.addEventListener('keydown', (e)=>{ | |
| if(e.ctrlKey && e.key==='`'){ launch('terminal'); } | |
| if(e.ctrlKey && e.key===' '){ startMenu.classList.toggle('show'); } | |
| }); | |
| // Restore settings | |
| (function init(){ | |
| setWallpaper(localStorage.getItem('OS:wall')||'nebula'); | |
| setTheme(localStorage.getItem('OS:theme')||'dark'); | |
| setGlass((localStorage.getItem('OS:glass')||'1')==='1'); | |
| })(); | |
| // Public helper for external launch from console | |
| window.AeroLiteOS = { launch }; | |
| </script> | |
| <!-- ================= More soon! ================ --> | |
| <!-- All additions below extend the existing OS features & games. --> | |
| <div id="bootOverlay" class="overlay" style="display:none;"> | |
| <div class="boot-box"> | |
| <h2 style="margin:0 0 12px 0">AeroLiteOS</h2> | |
| <div id="bootProgress" style="height:10px;background:rgba(255,255,255,.06);border-radius:6px;overflow:hidden;margin-bottom:12px"> | |
| <div id="bootBar" style="height:100%;width:0%;background:linear-gradient(90deg,var(--accent),var(--accent-2));"></div> | |
| </div> | |
| <div style="color:var(--muted)">Loading system modules...</div> | |
| </div> | |
| </div> | |
| <div id="loginOverlay" class="overlay" style="display:none;"> | |
| <div class="login-panel" id="loginPanel"> | |
| <h3 style="margin:0 0 8px 0">Welcome</h3> | |
| <input id="loginUser" placeholder="Username" style="width:80%;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,.06);margin-bottom:8px"><br> | |
| <input id="loginPass" placeholder="Password" type="password" style="width:80%;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,.06);margin-bottom:8px"><br> | |
| <button class="btn" id="loginBtn">Sign in</button> | |
| <div style="margin-top:8px;color:var(--muted);font-size:12px">Tip: password stored locally (demo).</div> | |
| </div> | |
| </div> | |
| <div id="rightClickMenu" class="rightmenu"></div> | |
| <div id="altTab" class="alt-tab" style="display:none;"></div> | |
| <script> | |
| // --- Configuration for new features --- | |
| (function(){ | |
| // Quick boot + login simulation | |
| function showBootThenLogin(){ | |
| const boot = document.getElementById('bootOverlay'); | |
| const login = document.getElementById('loginOverlay'); | |
| boot.style.display = 'grid'; | |
| let p = 0; | |
| const step = setInterval(()=>{ | |
| p += Math.random()*18; | |
| document.getElementById('bootBar').style.width = Math.min(100, p).toFixed(0) + '%'; | |
| if(p >= 100){ | |
| clearInterval(step); | |
| setTimeout(()=> { | |
| boot.style.display='none'; | |
| // if credential exists skip | |
| const savedUser = localStorage.getItem('OS:user'); | |
| if(savedUser){ | |
| login.style.display = 'none'; | |
| } else { | |
| login.style.display = 'grid'; | |
| } | |
| }, 400); | |
| } | |
| }, 300); | |
| } | |
| // Hook login button | |
| document.getElementById('loginBtn').addEventListener('click', ()=>{ | |
| const u = document.getElementById('loginUser').value || 'User'; | |
| const p = document.getElementById('loginPass').value || ''; | |
| // store demo credentials (user asked to keep old code; storing locally is fine) | |
| localStorage.setItem('OS:user', u); | |
| localStorage.setItem('OS:pass', p); | |
| document.getElementById('loginOverlay').style.display='none'; | |
| }); | |
| // Only show boot on first load (or when explicitly reset) | |
| if(!localStorage.getItem('OS:booted')){ | |
| showBootThenLogin(); | |
| localStorage.setItem('OS:booted','1'); | |
| } | |
| // --- Expand OS.apps without changing original definitions --- | |
| // Add Games and new utilities into OS.apps | |
| const newApps = { | |
| snake_game: { | |
| title: 'Snake (Game)', | |
| content(){ | |
| const wrap = el('div'); | |
| const canvas = el('canvas'); canvas.width=300; canvas.height=300; canvas.style.display='block'; canvas.style.margin='14px auto'; | |
| const info = el('div'); info.style.textAlign='center'; info.innerHTML = '<small>Use arrow keys. Eat red to grow.</small>'; | |
| wrap.append(canvas, info); | |
| // game boot will be handled after window insertion | |
| setTimeout(()=> startSnake(canvas), 60); | |
| return wrap; | |
| }, | |
| size:[360,380] | |
| }, | |
| minesweeper_game: { | |
| title: 'Minesweeper', | |
| content(){ | |
| const wrap = el('div','pad'); | |
| const field = el('div'); field.style.display='grid'; field.style.gridTemplateColumns='repeat(10,28px)'; field.style.gap='6px'; | |
| field.id='msfield'; | |
| wrap.append(field); | |
| setTimeout(()=> startMinesweeper(field,10,10,12), 80); | |
| return wrap; | |
| }, size:[360,400] | |
| }, | |
| pong_game: { | |
| title: 'Pong (2P)', | |
| content(){ | |
| const wrap = el('div'); | |
| const canvas = el('canvas'); canvas.width=420; canvas.height=300; canvas.style.display='block'; canvas.style.margin='12px auto'; wrap.append(canvas); | |
| setTimeout(()=> startPong(canvas), 60); | |
| return wrap; | |
| }, size:[460,360] | |
| }, | |
| tetris_game: { | |
| title: 'Tetris', | |
| content(){ | |
| const wrap = el('div'); const canvas = el('canvas'); canvas.width=240; canvas.height=400; canvas.style.display='block'; canvas.style.margin='12px auto'; wrap.append(canvas); | |
| setTimeout(()=> startTetris(canvas), 60); | |
| return wrap; | |
| }, size:[300,460] | |
| }, | |
| roblox_game: { | |
| title: 'Roblox', | |
| content(){ | |
| const wrap = el('div'); | |
| setTimeout(()=> startRoblox(wrap), 60); // builds Roblox UI inside the wrap | |
| return wrap; | |
| }, size:[660,400] | |
| }, | |
| music_player: { | |
| title: 'Music Player', | |
| content(){ | |
| const wrap = el('div','pad'); | |
| wrap.innerHTML = ` | |
| <div style="display:flex;gap:8px;align-items:center;"> | |
| <input id="mpFile" type="file" accept="audio/*"/> | |
| <button id="mpPlay" class="btn">Play</button> | |
| <button id="mpPause" class="btn">Pause</button> | |
| <button id="mpStop" class="btn">Stop</button> | |
| </div> | |
| <div style="margin-top:12px;"> | |
| <audio id="mpAudio" controls style="width:100%"></audio> | |
| </div> | |
| `; | |
| const file = wrap.querySelector('#mpFile'); | |
| const audio = wrap.querySelector('#mpAudio'); | |
| const play = wrap.querySelector('#mpPlay'); | |
| const pause = wrap.querySelector('#mpPause'); | |
| const stop = wrap.querySelector('#mpStop'); | |
| file.onchange = ()=>{ | |
| const f = file.files[0]; | |
| if(!f) return; | |
| audio.src = URL.createObjectURL(f); | |
| audio.play(); | |
| }; | |
| play.onclick = ()=> audio.play(); | |
| pause.onclick = ()=> audio.pause(); | |
| stop.onclick = ()=> { audio.pause(); audio.currentTime=0; }; | |
| return wrap; | |
| }, size:[520,200] | |
| }, | |
| image_viewer: { | |
| title: 'Image Viewer', | |
| content(){ | |
| const wrap = el('div','pad'); | |
| wrap.innerHTML = ` | |
| <div style="display:flex;gap:8px;align-items:center;"> | |
| <input id="imgFile" type="file" accept="image/*"/> | |
| <button id="imgOpen" class="btn">Open</button> | |
| <button id="imgSetWall" class="btn">Set as Wallpaper</button> | |
| </div> | |
| <div style="margin-top:12px; display:flex; justify-content:center;"> | |
| <img id="imgPreview" style="max-width:100%; max-height:400px; border-radius:8px;"/> | |
| </div> | |
| `; | |
| const input = wrap.querySelector('#imgFile'); | |
| const img = wrap.querySelector('#imgPreview'); | |
| const setBtn = wrap.querySelector('#imgSetWall'); | |
| input.onchange = ()=> { | |
| const f = input.files[0]; if(!f) return; | |
| img.src = URL.createObjectURL(f); | |
| }; | |
| setBtn.onclick = ()=> { | |
| if(!img.src) return alert('Open an image first'); | |
| // set wallpaper by data URL reference (uses CSS url()) | |
| wallpaperEl.style.background = `url(${img.src}) center/cover`; | |
| localStorage.setItem('OS:wall-src', img.src); | |
| }; | |
| return wrap; | |
| }, size:[640,480] | |
| }, | |
| recycle_bin: { | |
| title: 'Recycle Bin', | |
| content(){ | |
| const wrap = el('div','pad'); | |
| const list = el('div'); list.id='recycleList'; | |
| const empty = el('button','btn'); empty.textContent='Empty Bin'; | |
| empty.onclick = ()=> { if(confirm('Empty Recycle Bin?')){ localStorage.removeItem('OS:recycle'); render(); } }; | |
| wrap.append(empty, list); | |
| function render(){ list.innerHTML=''; const items = JSON.parse(localStorage.getItem('OS:recycle')||'[]'); items.forEach((it,i)=>{ const row=el('div'); row.style.display='flex'; row.style.justifyContent='space-between'; row.style.padding='6px 0'; row.innerHTML = `<div>${esc(it.name)}</div><div><button class="btn" data-idx="${i}">Restore</button><button class="btn" data-del="${i}">Delete</button></div>`; row.querySelector('[data-idx]')?.addEventListener('click', ()=> { restore(i); }); row.querySelector('[data-del]')?.addEventListener('click', ()=> { del(i); }); list.append(row); }); } | |
| function restore(i){ const items = JSON.parse(localStorage.getItem('OS:recycle')||'[]'); const it=items.splice(i,1)[0]; localStorage.setItem('OS:recycle', JSON.stringify(items)); // simplistic restore: add to files | |
| const files = JSON.parse(localStorage.getItem('OS:files')||'[]'); files.push({name:it.name, data:it.data}); localStorage.setItem('OS:files', JSON.stringify(files)); render(); } | |
| function del(i){ const items = JSON.parse(localStorage.getItem('OS:recycle')||'[]'); items.splice(i,1); localStorage.setItem('OS:recycle', JSON.stringify(items)); render(); } | |
| render(); return wrap; | |
| }, size:[420,320] | |
| }, | |
| app_store: { | |
| title: 'App Store', | |
| content(){ | |
| const wrap = el('div','pad'); | |
| const grid = el('div','appstore-grid'); | |
| const appsList = [ | |
| {id:'snake_game', name:'Snake', desc:'Classic snake game'}, | |
| {id:'minesweeper_game', name:'Minesweeper', desc:'Dont touch the mines'}, | |
| {id:'pong_game', name:'Pong', desc:'The classic atari game, PONG'}, | |
| {id:'tetris_game', name:'Tetris', desc:'The classic russian game!'}, | |
| {id:'roblox_game', name:'Roblox', desc:'An old simple remake of old roblox #freeschlep'} | |
| ]; | |
| appsList.forEach(a=>{ | |
| const c = el('div','app-card'); c.innerHTML = `<div style="font-weight:600">${a.name}</div><div style="font-size:12px;color:var(--muted)">${a.desc}</div><div style="margin-top:8px;"><button class="btn" data-run="${a.id}">Play</button></div>`; | |
| c.querySelector('[data-run]').addEventListener('click', ()=> OS.launch({ id: a.id, title: OS.apps[a.id].title, icon: iconSquare(), content: OS.apps[a.id].content, size: OS.apps[a.id].size })); | |
| grid.append(c); | |
| }); | |
| wrap.append(grid); | |
| return wrap; | |
| }, size:[640,360] | |
| } | |
| }; | |
| // Merge newApps into existing OS.apps (preserve anything already there) | |
| Object.entries(newApps).forEach(([k,v])=>{ | |
| if(!OS.apps[k]) OS.apps[k]=v; | |
| }); | |
| // Add desktop icons for games/apps (append to icons area) | |
| const iconsEl = document.getElementById('icons'); | |
| function addDesktopIcon(id, label, svg){ | |
| const d = el('div','icon'); d.dataset.launch = id; | |
| d.innerHTML = (svg || '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="4" y="3" width="16" height="18" rx="2" stroke="currentColor"/></svg>') + `<span>${label}</span>`; | |
| d.addEventListener('dblclick', ()=> launch(id)); | |
| iconsEl.append(d); | |
| } | |
| addDesktopIcon('snake_game','Snake'); | |
| addDesktopIcon('minesweeper_game','Minesweeper'); | |
| addDesktopIcon('pong_game','Pong'); | |
| addDesktopIcon('tetris_game','Tetris'); | |
| addDesktopIcon('music_player','Music'); | |
| addDesktopIcon('image_viewer','Images'); | |
| addDesktopIcon('recycle_bin','Recycle Bin'); | |
| addDesktopIcon('roblox_game','Roblox'); | |
| addDesktopIcon('app_store','App Store'); | |
| // Integrate Recycle functionality into Files delete workflow by intercepting storage changes: | |
| // (We won't change original Files code—provide a helper function to move deleted files) | |
| window.OSDeleteToRecycle = function(fileObj){ | |
| const r = JSON.parse(localStorage.getItem('OS:recycle')||'[]'); | |
| r.push(fileObj); | |
| localStorage.setItem('OS:recycle', JSON.stringify(r)); | |
| }; | |
| // Right-click desktop menu | |
| const rightMenu = document.getElementById('rightClickMenu'); | |
| document.addEventListener('contextmenu', (e)=>{ | |
| e.preventDefault(); | |
| if(e.target.closest('.win,.taskbar,.startmenu,.quick')) { rightMenu.style.display='none'; return; } | |
| rightMenu.style.display='block'; | |
| rightMenu.style.left = e.clientX + 'px'; | |
| rightMenu.style.top = e.clientY + 'px'; | |
| rightMenu.innerHTML = `<div style="padding:6px;cursor:pointer" id="newFile">New File</div> | |
| <div style="padding:6px;cursor:pointer" id="setWall">Set Wallpaper</div> | |
| <div style="padding:6px;cursor:pointer" id="openStore">App Store</div>`; | |
| rightMenu.querySelector('#newFile').onclick = ()=> { rightMenu.style.display='none'; const name = prompt('New file name','untitled.txt'); if(!name) return; const files = JSON.parse(localStorage.getItem('OS:files')||'[]'); files.push({name,data:''}); localStorage.setItem('OS:files', JSON.stringify(files)); alert('File created. Open Files app to view.'); }; | |
| rightMenu.querySelector('#setWall').onclick = ()=> { rightMenu.style.display='none'; const url = prompt('Image URL (cross-origin may block):'); if(url){ wallpaperEl.style.background = `url(${url}) center/cover`; localStorage.setItem('OS:wall-src', url); } }; | |
| rightMenu.querySelector('#openStore').onclick = ()=> { rightMenu.style.display='none'; OS.launch({ id:'app_store', title:OS.apps.app_store.title, icon: iconSquare(), content: OS.apps.app_store.content, size: OS.apps.app_store.size }); }; | |
| }); | |
| document.addEventListener('click', ()=> rightMenu.style.display='none'); | |
| // Alt+Tab switcher simple preview (shows open windows by title) | |
| const altTabEl = document.getElementById('altTab'); | |
| let altOpen = false, altList = []; | |
| window.addEventListener('keydown', (e)=>{ | |
| if(e.key === 'Tab' && e.altKey){ | |
| e.preventDefault(); | |
| if(!altOpen){ | |
| // open | |
| altOpen = true; | |
| altList = Array.from(OS.windows.keys()); | |
| altTabEl.innerHTML = ''; | |
| altList.forEach(id=>{ | |
| const item = el('div','item'); item.textContent = OS.windows.get(id)?.task?.textContent || id; item.onclick = ()=> { focus(OS.windows.get(id).el); hideAlt(); }; | |
| altTabEl.append(item); | |
| }); | |
| altTabEl.style.display='flex'; | |
| } else { | |
| // cycle | |
| const focused = document.querySelector('.win[style*="z-index"]'); | |
| } | |
| } | |
| }); | |
| window.addEventListener('keyup', (e)=>{ if(e.key === 'Alt') hideAlt(); }); | |
| function hideAlt(){ altOpen=false; altTabEl.style.display='none'; altTabEl.innerHTML=''; } | |
| // ----- Simple Game Implementations (vanilla) ----- | |
| function startSnake(canvas){ | |
| if(!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| const grid = 15; | |
| const cols = Math.floor(canvas.width / grid); | |
| const rows = Math.floor(canvas.height / grid); | |
| let px = Math.floor(cols/2), py = Math.floor(rows/2), vx=0, vy=0, tail=4, trail=[]; | |
| let apple = {x: Math.floor(Math.random()*cols), y: Math.floor(Math.random()*rows)}; | |
| function tick(){ | |
| px += vx; py += vy; | |
| if(px < 0) px = cols -1; if(px >= cols) px = 0; | |
| if(py < 0) py = rows -1; if(py >= rows) py = 0; | |
| // collision with self | |
| for(const t of trail){ if(t.x===px && t.y===py){ tail = 4; } } | |
| trail.push({x:px,y:py}); | |
| while(trail.length > tail) trail.shift(); | |
| if(px===apple.x && py===apple.y){ tail++; apple = {x:Math.floor(Math.random()*cols), y:Math.floor(Math.random()*rows)}; } | |
| // render | |
| ctx.fillStyle = '#041023'; ctx.fillRect(0,0,canvas.width,canvas.height); | |
| ctx.fillStyle = '#d946ef'; | |
| ctx.fillRect(apple.x*grid+2, apple.y*grid+2, grid-4, grid-4); | |
| ctx.fillStyle = '#22c55e'; | |
| for(const t of trail) ctx.fillRect(t.x*grid+2, t.y*grid+2, grid-4, grid-4); | |
| } | |
| document.addEventListener('keydown', function handler(e){ | |
| if(e.key==='ArrowLeft' && vx!==1){ vx=-1; vy=0; } | |
| if(e.key==='ArrowRight' && vx!==-1){ vx=1; vy=0; } | |
| if(e.key==='ArrowUp' && vy!==1){ vx=0; vy=-1; } | |
| if(e.key==='ArrowDown' && vy!==-1){ vx=0; vy=1; } | |
| }); | |
| const interval = setInterval(tick, 100); | |
| // stop when window closed: attempt to detect canvas removed | |
| const obs = new MutationObserver(()=>{ if(!document.body.contains(canvas)){ clearInterval(interval); obs.disconnect(); } }); | |
| obs.observe(document.body, {childList:true, subtree:true}); | |
| } | |
| function startPong(canvas){ | |
| if(!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let bx = canvas.width/2, by = canvas.height/2, bvx=3, bvy=2; | |
| let lp = canvas.height/2 - 30, rp = lp; | |
| function tick(){ | |
| bx += bvx; by += bvy; | |
| if(by < 0 || by > canvas.height){ bvy *= -1; } | |
| // paddles | |
| if(bx < 20 && by > lp && by < lp+60) bvx *= -1; | |
| if(bx > canvas.width-20 && by > rp && by < rp+60) bvx *= -1; | |
| if(bx < -50 || bx > canvas.width + 50){ bx = canvas.width/2; by = canvas.height/2; bvx = -bvx; } | |
| // AI for right paddle | |
| if(rp + 30 < by) rp += 2; else if(rp + 30 > by) rp -= 2; | |
| // draw | |
| ctx.fillStyle = '#020617'; ctx.fillRect(0,0,canvas.width,canvas.height); | |
| ctx.fillStyle = '#e5e7eb'; ctx.fillRect(10, lp, 10, 60); ctx.fillRect(canvas.width-20, rp, 10, 60); | |
| ctx.beginPath(); ctx.arc(bx,by,8,0,Math.PI*2); ctx.fill(); | |
| } | |
| document.addEventListener('keydown', function handler(e){ | |
| // left paddle controls W/S | |
| if(e.key==='w') lp -= 20; | |
| if(e.key==='s') lp += 20; | |
| }); | |
| const interval = setInterval(tick, 20); | |
| const obs = new MutationObserver(()=>{ if(!document.body.contains(canvas)){ clearInterval(interval); obs.disconnect(); } }); | |
| obs.observe(document.body, {childList:true, subtree:true}); | |
| } | |
| function startTetris(canvas){ | |
| if(!canvas) return; | |
| // --- TETRIS IMPLEMENTATION (keeps function signature exactly the same) --- | |
| const ctx = canvas.getContext('2d'); | |
| // Cleanup previous game on same canvas (if any) | |
| if (canvas._tetris && canvas._tetris.cleanup) canvas._tetris.cleanup(); | |
| // Board dimensions | |
| const COLS = 10; | |
| const ROWS = 20; | |
| // cell size fits canvas while preserving board aspect | |
| const cellSize = Math.floor(Math.min(canvas.width / COLS, canvas.height / ROWS)); | |
| const boardWidth = cellSize * COLS; | |
| const boardHeight = cellSize * ROWS; | |
| const offsetX = Math.floor((canvas.width - boardWidth) / 2); | |
| const offsetY = Math.floor((canvas.height - boardHeight) / 2); | |
| // Visual settings | |
| ctx.textBaseline = 'top'; | |
| ctx.font = `${Math.max(12, Math.floor(cellSize * 0.6))}px monospace`; | |
| // Tetromino definitions (rotation states) | |
| const TETROMINOS = { | |
| I: [ | |
| [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], | |
| [[0,0,1,0],[0,0,1,0],[0,0,1,0],[0,0,1,0]], | |
| ], | |
| J: [ | |
| [[1,0,0],[1,1,1],[0,0,0]], | |
| [[0,1,1],[0,1,0],[0,1,0]], | |
| [[0,0,0],[1,1,1],[0,0,1]], | |
| [[0,1,0],[0,1,0],[1,1,0]], | |
| ], | |
| L: [ | |
| [[0,0,1],[1,1,1],[0,0,0]], | |
| [[0,1,0],[0,1,0],[0,1,1]], | |
| [[0,0,0],[1,1,1],[1,0,0]], | |
| [[1,1,0],[0,1,0],[0,1,0]], | |
| ], | |
| O: [ | |
| [[1,1],[1,1]], | |
| ], | |
| S: [ | |
| [[0,1,1],[1,1,0],[0,0,0]], | |
| [[0,1,0],[0,1,1],[0,0,1]], | |
| ], | |
| T: [ | |
| [[0,1,0],[1,1,1],[0,0,0]], | |
| [[0,1,0],[0,1,1],[0,1,0]], | |
| [[0,0,0],[1,1,1],[0,1,0]], | |
| [[0,1,0],[1,1,0],[0,1,0]], | |
| ], | |
| Z: [ | |
| [[1,1,0],[0,1,1],[0,0,0]], | |
| [[0,0,1],[0,1,1],[0,1,0]], | |
| ], | |
| }; | |
| const COLORS = { | |
| I: '#4ecdc4', | |
| J: '#496af1', | |
| L: '#f39c12', | |
| O: '#f1c40f', | |
| S: '#2ecc71', | |
| T: '#9b59b6', | |
| Z: '#e74c3c', | |
| X: '#666' // filled/ghost fallback | |
| }; | |
| const PIECE_KEYS = Object.keys(TETROMINOS); | |
| // Game state | |
| let board = createMatrix(ROWS, COLS); | |
| let current = null; | |
| let next = null; | |
| let dropInterval = 800; // ms (will speed up) | |
| let dropAccumulator = 0; | |
| let lastTime = 0; | |
| let score = 0; | |
| let level = 0; | |
| let lines = 0; | |
| let paused = false; | |
| let gameOver = false; | |
| // Utility: create matrix rows x cols filled with 0 | |
| function createMatrix(r, c){ | |
| const m = new Array(r); | |
| for(let i=0;i<r;i++) m[i] = new Array(c).fill(0); | |
| return m; | |
| } | |
| // Spawn piece from next | |
| function spawnPiece(){ | |
| current = next || randomPiece(); | |
| current.row = 0; | |
| current.col = Math.floor((COLS - current.matrix[0].length) / 2); | |
| next = randomPiece(); | |
| if (collides(board, current.matrix, current.row, current.col)){ | |
| // immediate collision => game over | |
| gameOver = true; | |
| paused = false; | |
| } | |
| } | |
| function randomPiece(){ | |
| const k = PIECE_KEYS[Math.floor(Math.random()*PIECE_KEYS.length)]; | |
| const states = TETROMINOS[k]; | |
| // pick initial rotation index 0 | |
| return { | |
| type: k, | |
| matrix: states[0].map(row => row.slice()), | |
| rotIndex: 0, | |
| states: states | |
| }; | |
| } | |
| // Collision detection: returns true if matrix at (r,c) overlaps filled board or out of bounds | |
| function collides(board, matrix, row, col){ | |
| for(let r=0;r<matrix.length;r++){ | |
| for(let c=0;c<matrix[r].length;c++){ | |
| if (!matrix[r][c]) continue; | |
| const br = row + r; | |
| const bc = col + c; | |
| if (br < 0 || br >= ROWS || bc < 0 || bc >= COLS) return true; | |
| if (board[br][bc]) return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function mergeToBoard(board, matrix, row, col, type){ | |
| for(let r=0;r<matrix.length;r++){ | |
| for(let c=0;c<matrix[r].length;c++){ | |
| if (matrix[r][c]) board[row + r][col + c] = type; | |
| } | |
| } | |
| } | |
| function rotatePiece(dir=1){ | |
| if (!current) return; | |
| const states = current.states; | |
| const nextIndex = (current.rotIndex + dir + states.length) % states.length; | |
| const nextMatrix = states[nextIndex]; | |
| // Try to wall-kick horizontally (simple) | |
| const kicks = [0, -1, 1, -2, 2]; | |
| for (let k of kicks) { | |
| const newCol = current.col + k; | |
| if (!collides(board, nextMatrix, current.row, newCol)) { | |
| current.rotIndex = nextIndex; | |
| current.matrix = nextMatrix.map(r => r.slice()); | |
| current.col = newCol; | |
| return; | |
| } | |
| } | |
| } | |
| function hardDrop(){ | |
| if(!current) return; | |
| while(!collides(board, current.matrix, current.row+1, current.col)){ | |
| current.row++; | |
| } | |
| lockPiece(); | |
| } | |
| function lockPiece(){ | |
| mergeToBoard(board, current.matrix, current.row, current.col, current.type || current.type === 0 ? current.type : current.type = current.type || current.type); | |
| clearLines(); | |
| spawnPiece(); | |
| } | |
| function clearLines(){ | |
| let cleared = 0; | |
| for (let r = ROWS - 1; r >= 0; r--){ | |
| if (board[r].every(cell => cell)) { | |
| board.splice(r, 1); | |
| board.unshift(new Array(COLS).fill(0)); | |
| cleared++; | |
| r++; // recheck same row index after splice | |
| } | |
| } | |
| if (cleared > 0){ | |
| lines += cleared; | |
| // Scoring: classic-ish (single/double/triple/tetris) | |
| const scoreTable = [0, 40, 100, 300, 1200]; // multiplied by (level+1) | |
| score += (scoreTable[cleared] || 0) * (level + 1); | |
| // Level up every 10 lines | |
| const newLevel = Math.floor(lines / 10); | |
| if (newLevel > level) { | |
| level = newLevel; | |
| dropInterval = Math.max(100, 800 - level * 60); | |
| } | |
| } | |
| } | |
| // Move piece horizontally | |
| function move(offset){ | |
| if(!current) return; | |
| if (!collides(board, current.matrix, current.row, current.col + offset)) { | |
| current.col += offset; | |
| } | |
| } | |
| // Soft drop (one step) | |
| function softDrop(){ | |
| if(!current) return; | |
| if (!collides(board, current.matrix, current.row + 1, current.col)) { | |
| current.row++; | |
| score += 1; // small reward for soft drop | |
| } else { | |
| lockPiece(); | |
| } | |
| } | |
| // Draw helpers | |
| function drawCell(x, y, color, stroke=true){ | |
| const px = offsetX + x * cellSize; | |
| const py = offsetY + y * cellSize; | |
| ctx.fillStyle = color || '#777'; | |
| ctx.fillRect(px + 1, py + 1, cellSize - 2, cellSize - 2); | |
| if (stroke) { | |
| ctx.strokeStyle = 'rgba(0,0,0,0.25)'; | |
| ctx.lineWidth = 1; | |
| ctx.strokeRect(px + 0.5, py + 0.5, cellSize - 1, cellSize - 1); | |
| } | |
| } | |
| function clearCanvas(){ | |
| ctx.fillStyle = '#041023'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| } | |
| function drawBoard(){ | |
| // board background | |
| ctx.fillStyle = '#061222'; | |
| ctx.fillRect(offsetX, offsetY, boardWidth, boardHeight); | |
| // grid + cells | |
| for (let r=0;r<ROWS;r++){ | |
| for (let c=0;c<COLS;c++){ | |
| const cell = board[r][c]; | |
| if (cell) { | |
| const color = COLORS[cell] || COLORS.X; | |
| drawCell(c, r, color); | |
| } else { | |
| // draw faint grid cell background | |
| ctx.strokeStyle = 'rgba(255,255,255,0.03)'; | |
| ctx.strokeRect(offsetX + c*cellSize + 0.5, offsetY + r*cellSize + 0.5, cellSize - 1, cellSize - 1); | |
| } | |
| } | |
| } | |
| // draw current piece | |
| if (current){ | |
| for (let r=0;r<current.matrix.length;r++){ | |
| for (let c=0;c<current.matrix[r].length;c++){ | |
| if (current.matrix[r][c]){ | |
| const color = COLORS[current.type] || COLORS.X; | |
| drawCell(current.col + c, current.row + r, color); | |
| } | |
| } | |
| } | |
| } | |
| // draw ghost (landing position) | |
| if (current) { | |
| let ghostRow = current.row; | |
| while(!collides(board, current.matrix, ghostRow+1, current.col)) ghostRow++; | |
| ctx.globalAlpha = 0.25; | |
| for (let r=0;r<current.matrix.length;r++){ | |
| for (let c=0;c<current.matrix[r].length;c++){ | |
| if (current.matrix[r][c]) { | |
| drawCell(current.col + c, ghostRow + r, COLORS[current.type]); | |
| } | |
| } | |
| } | |
| ctx.globalAlpha = 1; | |
| } | |
| } | |
| function drawHUD(){ | |
| // Score/level/lines | |
| const hudX = offsetX + boardWidth + Math.max(10, Math.floor(cellSize*0.5)); | |
| const hudTop = offsetY; | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.font = `${Math.max(10, Math.floor(cellSize * 0.45))}px monospace`; | |
| ctx.fillText(`Score: ${score}`, hudX, hudTop); | |
| ctx.fillText(`Level: ${level}`, hudX, hudTop + 24); | |
| ctx.fillText(`Lines: ${lines}`, hudX, hudTop + 44); | |
| // Next piece box | |
| ctx.fillStyle = '#081827'; | |
| const boxX = hudX; | |
| const boxY = hudTop + 84; | |
| const boxW = cellSize * 6; | |
| const boxH = cellSize * 6; | |
| ctx.fillRect(boxX, boxY, boxW, boxH); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.06)'; | |
| ctx.strokeRect(boxX + 0.5, boxY + 0.5, boxW - 1, boxH - 1); | |
| ctx.fillStyle = '#fff'; | |
| ctx.fillText('Next', boxX, boxY - 18); | |
| if (next){ | |
| const nm = next.matrix; | |
| const padX = Math.floor((6 - nm[0].length)/2); | |
| const padY = Math.floor((6 - nm.length)/2); | |
| ctx.globalAlpha = 1; | |
| for (let r=0;r<nm.length;r++){ | |
| for (let c=0;c<nm[r].length;c++){ | |
| if (nm[r][c]){ | |
| const px = boxX + (padX + c) * cellSize; | |
| const py = boxY + (padY + r) * cellSize; | |
| ctx.fillStyle = COLORS[next.type] || COLORS.X; | |
| ctx.fillRect(px + 1, py + 1, cellSize - 2, cellSize - 2); | |
| ctx.strokeStyle = 'rgba(0,0,0,0.25)'; | |
| ctx.strokeRect(px + 0.5, py + 0.5, cellSize - 1, cellSize - 1); | |
| } | |
| } | |
| } | |
| } | |
| // bottom small instructions | |
| ctx.fillStyle = 'rgba(255,255,255,0.7)'; | |
| ctx.font = `${Math.max(9, Math.floor(cellSize * 0.35))}px monospace`; | |
| const infoY = offsetY + boardHeight + 6; | |
| ctx.fillText('← → : move ↑ / X : rotate Z : rotate ccw', offsetX, infoY); | |
| ctx.fillText('↓ : soft drop Space : hard drop P : pause', offsetX, infoY + 18); | |
| } | |
| function draw(){ | |
| clearCanvas(); | |
| drawBoard(); | |
| drawHUD(); | |
| if (paused) { | |
| ctx.fillStyle = 'rgba(0,0,0,0.5)'; | |
| ctx.fillRect(offsetX, offsetY, boardWidth, boardHeight); | |
| ctx.fillStyle = '#fff'; | |
| ctx.font = `${Math.max(18, Math.floor(cellSize * 0.8))}px monospace`; | |
| ctx.fillText('PAUSED', offsetX + boardWidth/2 - 40, offsetY + boardHeight/2 - 12); | |
| } | |
| if (gameOver){ | |
| ctx.fillStyle = 'rgba(0,0,0,0.6)'; | |
| ctx.fillRect(offsetX, offsetY, boardWidth, boardHeight); | |
| ctx.fillStyle = '#ffdddd'; | |
| ctx.font = `${Math.max(18, Math.floor(cellSize * 0.8))}px monospace`; | |
| ctx.fillText('GAME OVER', offsetX + boardWidth/2 - 64, offsetY + boardHeight/2 - 12); | |
| ctx.font = `${Math.max(12, Math.floor(cellSize * 0.45))}px monospace`; | |
| ctx.fillText('Press R to restart', offsetX + boardWidth/2 - 62, offsetY + boardHeight/2 + 18); | |
| } | |
| } | |
| // Game loop | |
| function update(time = 0){ | |
| const dt = time - lastTime; | |
| lastTime = time; | |
| if(!paused && !gameOver){ | |
| dropAccumulator += dt; | |
| if (dropAccumulator > dropInterval){ | |
| dropAccumulator = 0; | |
| if (!current) spawnPiece(); | |
| else { | |
| if (!collides(board, current.matrix, current.row + 1, current.col)) { | |
| current.row++; | |
| } else { | |
| lockPiece(); | |
| } | |
| } | |
| } | |
| } | |
| draw(); | |
| if (!canvas._tetris._stop) requestAnimationFrame(update); | |
| } | |
| // Input handling | |
| const keyState = {}; | |
| function onKeyDown(e){ | |
| if (e.repeat) return; | |
| const k = e.key; | |
| if (k === 'ArrowLeft') { move(-1); e.preventDefault(); } | |
| else if (k === 'ArrowRight') { move(1); e.preventDefault(); } | |
| else if (k === 'ArrowDown') { softDrop(); e.preventDefault(); } | |
| else if (k === ' ' || k === 'Spacebar') { hardDrop(); e.preventDefault(); } | |
| else if (k === 'x' || k === 'X' || k === 'ArrowUp') { rotatePiece(1); e.preventDefault(); } | |
| else if (k === 'z' || k === 'Z') { rotatePiece(-1); e.preventDefault(); } | |
| else if (k === 'p' || k === 'P') { paused = !paused; e.preventDefault(); } | |
| else if (k === 'r' || k === 'R') { | |
| if (gameOver) resetGame(); | |
| } | |
| } | |
| // Mouse / touch to pause on canvas double-click | |
| function onDoubleClick(){ paused = !paused; } | |
| // Restart | |
| function resetGame(){ | |
| board = createMatrix(ROWS, COLS); | |
| current = null; | |
| next = randomPiece(); | |
| score = 0; | |
| level = 0; | |
| lines = 0; | |
| dropInterval = 800; | |
| dropAccumulator = 0; | |
| lastTime = performance.now(); | |
| paused = false; | |
| gameOver = false; | |
| spawnPiece(); | |
| } | |
| // Expose cleanup so future calls can remove listeners | |
| function cleanup(){ | |
| window.removeEventListener('keydown', onKeyDown); | |
| canvas.removeEventListener('dblclick', onDoubleClick); | |
| canvas._tetris._stop = true; | |
| } | |
| // Save state on canvas to avoid multiple listeners on repeated calls | |
| canvas._tetris = canvas._tetris || {}; | |
| canvas._tetris.cleanup = cleanup; | |
| canvas._tetris._stop = false; | |
| // Attach listeners | |
| window.addEventListener('keydown', onKeyDown); | |
| canvas.addEventListener('dblclick', onDoubleClick); | |
| // Initialize | |
| next = randomPiece(); | |
| spawnPiece(); | |
| lastTime = performance.now(); | |
| requestAnimationFrame(update); | |
| } | |
| // Boblox 2004 Clone with playable games | |
| // Utility function to create elements | |
| function el(tag, cls) { const e=document.createElement(tag); if(cls)e.className=cls; return e; } | |
| // Storage for player data | |
| const BobloxData = { | |
| username: 'Player22', | |
| coins: 100, | |
| avatarColor: '#0f0', | |
| }; | |
| // --- Start Boblox --- | |
| function startRoblox(wrap){ | |
| if(!wrap) return; | |
| wrap.innerHTML=''; | |
| // Header | |
| const header = el('div'); | |
| header.style.background='#0055aa'; | |
| header.style.color='#fff'; | |
| header.style.padding='12px'; | |
| header.style.fontWeight='bold'; | |
| header.style.textAlign='center'; | |
| header.textContent='Boblox 2004'; | |
| wrap.append(header); | |
| // Sidebar (Avatar & coins) | |
| const sidebar = el('div'); | |
| sidebar.style.width='120px'; | |
| sidebar.style.float='left'; | |
| sidebar.style.padding='10px'; | |
| sidebar.style.borderRight='1px solid #aaa'; | |
| sidebar.style.height='360px'; | |
| // Avatar display | |
| const avatar = el('div'); | |
| avatar.style.width='60px'; | |
| avatar.style.height='60px'; | |
| avatar.style.background=BobloxData.avatarColor; | |
| avatar.style.border='2px solid #000'; | |
| avatar.style.marginBottom='10px'; | |
| sidebar.append(avatar); | |
| // Username | |
| const usernameDisplay = el('div'); | |
| usernameDisplay.textContent=BobloxData.username; | |
| usernameDisplay.style.marginBottom='10px'; | |
| sidebar.append(usernameDisplay); | |
| // Coins | |
| const coinsDisplay = el('div'); | |
| coinsDisplay.textContent='Coins: '+BobloxData.coins; | |
| coinsDisplay.style.marginBottom='10px'; | |
| sidebar.append(coinsDisplay); | |
| // Catalog / Avatar Editor | |
| const catalogBtn = el('button'); | |
| catalogBtn.textContent='Catalog'; | |
| catalogBtn.style.display='block'; | |
| catalogBtn.style.marginBottom='6px'; | |
| catalogBtn.addEventListener('click', ()=> startCatalog(wrap)); | |
| sidebar.append(catalogBtn); | |
| const avatarBtn = el('button'); | |
| avatarBtn.textContent='Avatar'; | |
| avatarBtn.style.display='block'; | |
| avatarBtn.addEventListener('click', ()=> startAvatarEditor(wrap, avatar)); | |
| sidebar.append(avatarBtn); | |
| wrap.append(sidebar); | |
| // Main content (Games grid) | |
| const main = el('div'); | |
| main.style.marginLeft='140px'; | |
| const games = [ | |
| {name:'Obby', play:startObby}, | |
| {name:'Coin Tycoon', play:startTycoon}, | |
| {name:'Jump Test', play:startBattle}, | |
| {name:'Content Trains Boblox', play:startRace} | |
| ]; | |
| const gamesGrid = el('div'); | |
| gamesGrid.style.display='grid'; | |
| gamesGrid.style.gridTemplateColumns='repeat(auto-fit, minmax(120px, 1fr))'; | |
| gamesGrid.style.gap='6px'; | |
| gamesGrid.style.marginTop='10px'; | |
| games.forEach(game=>{ | |
| const card = el('div'); | |
| card.style.border='1px solid #aaa'; | |
| card.style.padding='6px'; | |
| card.style.cursor='pointer'; | |
| card.style.background='#fff'; | |
| card.style.textAlign='center'; | |
| card.innerHTML=`<div style="height:60px;background:#ccc;margin-bottom:4px;">🎮</div><strong>${game.name}</strong>`; | |
| card.addEventListener('click', ()=>{ | |
| main.innerHTML=''; // clear games grid | |
| const back = el('button'); | |
| back.textContent='Back to Home'; | |
| back.addEventListener('click', ()=> startRoblox(wrap)); | |
| main.append(back); | |
| game.play(main); | |
| }); | |
| gamesGrid.append(card); | |
| }); | |
| main.append(gamesGrid); | |
| wrap.append(main); | |
| } | |
| // --- Catalog / Avatar Editor --- | |
| function startCatalog(wrap){ | |
| wrap.innerHTML=''; | |
| const back = el('button'); | |
| back.textContent='Back'; | |
| back.addEventListener('click', ()=> startRoblox(wrap)); | |
| wrap.append(back); | |
| const shopTitle = el('h3'); shopTitle.textContent='Catalog'; | |
| wrap.append(shopTitle); | |
| const items = [ | |
| {name:'Red Hat', cost:20, color:'#f00'}, | |
| {name:'Blue Shirt', cost:30, color:'#00f'}, | |
| {name:'Yellow Pants', cost:50, color:'#ff0'} | |
| ]; | |
| items.forEach(item=>{ | |
| const itemDiv = el('div'); | |
| itemDiv.style.border='1px solid #aaa'; | |
| itemDiv.style.padding='6px'; | |
| itemDiv.style.margin='6px'; | |
| itemDiv.style.cursor='pointer'; | |
| itemDiv.innerHTML=`<div style="width:40px;height:40px;background:${item.color};display:inline-block;margin-right:6px;"></div>${item.name} - ${item.cost} coins`; | |
| itemDiv.addEventListener('click', ()=>{ | |
| if(BobloxData.coins>=item.cost){ | |
| BobloxData.coins-=item.cost; | |
| alert(`You bought ${item.name}!`); | |
| } else alert('Not enough coins!'); | |
| }); | |
| wrap.append(itemDiv); | |
| }); | |
| } | |
| function startAvatarEditor(wrap, avatarEl){ | |
| wrap.innerHTML=''; | |
| const back = el('button'); | |
| back.textContent='Back'; | |
| back.addEventListener('click', ()=> startRoblox(wrap)); | |
| wrap.append(back); | |
| const colors = ['#0f0','#f00','#00f','#ff0','#f0f','#0ff']; | |
| colors.forEach(c=>{ | |
| const btn = el('button'); | |
| btn.style.background=c; | |
| btn.style.width='40px'; | |
| btn.style.height='40px'; | |
| btn.style.margin='4px'; | |
| btn.addEventListener('click', ()=>{ | |
| BobloxData.avatarColor=c; | |
| avatarEl.style.background=c; | |
| }); | |
| wrap.append(btn); | |
| }); | |
| } | |
| // --- NPC Helper --- | |
| function createNPC(name){ | |
| const npc=document.createElement('div'); | |
| npc.textContent=name[0]; | |
| npc.style.position='absolute'; | |
| npc.style.width='24px'; | |
| npc.style.height='24px'; | |
| npc.style.background='#ff0'; | |
| npc.style.borderRadius='50%'; | |
| npc.style.textAlign='center'; | |
| npc.style.lineHeight='24px'; | |
| return npc; | |
| } | |
| // --- games yk --- | |
| // 1. Obby | |
| function startObby(container){ | |
| // obby | |
| const info=document.createElement('div'); | |
| info.textContent='🏃 Obby! Arrow keys to move'; | |
| container.append(info); | |
| const gameArea=document.createElement('div'); | |
| gameArea.style.position='relative'; | |
| gameArea.style.height='200px'; | |
| gameArea.style.border='1px solid #000'; | |
| gameArea.style.background='#ccf'; | |
| container.append(gameArea); | |
| const player=document.createElement('div'); | |
| player.style.position='absolute'; | |
| player.style.left='20px'; | |
| player.style.bottom='0px'; | |
| player.style.width='24px'; | |
| player.style.height='24px'; | |
| player.style.background=BobloxData.avatarColor; | |
| gameArea.append(player); | |
| const platforms=[]; | |
| for(let i=0;i<6;i++){ | |
| const plat=document.createElement('div'); | |
| plat.style.position='absolute'; | |
| plat.style.width='60px'; | |
| plat.style.height='10px'; | |
| plat.style.background='#555'; | |
| plat.style.left=`${80*i}px`; | |
| plat.style.bottom=`${30*i}px`; | |
| gameArea.append(plat); | |
| platforms.push(plat); | |
| } | |
| const npcNames=['A','B','C']; | |
| const npcs = npcNames.map(name=>{ | |
| const npc=createNPC(name); | |
| npc.style.left='20px'; | |
| npc.style.bottom='0px'; | |
| gameArea.append(npc); | |
| return {el:npc,pathIndex:0,vy:0}; | |
| }); | |
| let playerLeft=20, playerBottom=0, vy=0, gravity=2; | |
| function checkCollision(objBottom,objLeft){ | |
| for(const plat of platforms){ | |
| const platLeft=parseInt(plat.style.left); | |
| const platBottom=parseInt(plat.style.bottom); | |
| const platWidth=parseInt(plat.style.width); | |
| const platHeight=parseInt(plat.style.height); | |
| if(objLeft+24>platLeft && objLeft<platLeft+platWidth && | |
| objBottom+24>=platBottom && objBottom<=platBottom+platHeight){ | |
| return platBottom+platHeight; | |
| } | |
| } | |
| return -1; | |
| } | |
| function npcFollowObby(npc){ | |
| const targetPlat=platforms[npc.pathIndex]; | |
| let npcLeft=parseInt(npc.el.style.left); | |
| let npcBottom=parseInt(npc.el.style.bottom); | |
| if(npcLeft<parseInt(targetPlat.style.left)) npcLeft+=2; | |
| if(npcLeft>parseInt(targetPlat.style.left)) npcLeft-=2; | |
| const coll=checkCollision(npcBottom,npcLeft); | |
| if(coll>=0) npcBottom=coll; | |
| else npc.vy-=gravity; | |
| npcBottom+=npc.vy; | |
| npc.el.style.left=npcLeft+'px'; | |
| npc.el.style.bottom=npcBottom+'px'; | |
| if(Math.abs(npcLeft-parseInt(targetPlat.style.left))<3 && npcBottom<=parseInt(targetPlat.style.bottom)){ | |
| npc.pathIndex=(npc.pathIndex+1)%platforms.length; | |
| npc.vy=10; | |
| } | |
| } | |
| document.addEventListener('keydown',e=>{ | |
| if(e.code==='ArrowLeft') playerLeft-=10; | |
| if(e.code==='ArrowRight') playerLeft+=10; | |
| if(e.code==='Space' && playerBottom===0) vy=20; | |
| }); | |
| setInterval(()=>{ | |
| vy-=gravity; | |
| playerBottom+=vy; | |
| const coll=checkCollision(playerBottom,playerLeft); | |
| if(coll>=0){ playerBottom=coll; vy=0; } | |
| if(playerBottom<0){ playerBottom=0; vy=0; } | |
| player.style.left=playerLeft+'px'; | |
| player.style.bottom=playerBottom+'px'; | |
| npcs.forEach(npcFollowObby); | |
| },40); | |
| } | |
| // 2. Tycoon | |
| function startTycoon(container){ | |
| const info=document.createElement('div'); | |
| info.textContent='💰 Coin Tycoon! Click buildings to collect coins.'; | |
| container.append(info); | |
| const area=document.createElement('div'); | |
| area.style.position='relative'; | |
| area.style.height='200px'; | |
| area.style.border='1px solid #000'; | |
| container.append(area); | |
| let coins=BobloxData.coins; | |
| const coinDisplay=document.createElement('div'); | |
| coinDisplay.textContent='Coins: '+coins; | |
| container.append(coinDisplay); | |
| const buildings=[]; | |
| for(let i=0;i<3;i++){ | |
| const b=document.createElement('div'); | |
| b.style.position='absolute'; | |
| b.style.width='40px'; | |
| b.style.height='40px'; | |
| b.style.background='#a52'; | |
| b.style.bottom='0px'; | |
| b.style.left=`${50+i*60}px`; | |
| b.style.cursor='pointer'; | |
| b.addEventListener('click', ()=> { coins+=5; coinDisplay.textContent='Coins: '+coins; BobloxData.coins=coins; }); | |
| area.append(b); | |
| buildings.push(b); | |
| } | |
| const npcs=['X','Y','Z'].map(createNPC); | |
| npcs.forEach((npc,i)=>{ | |
| npc.style.left=`${60+i*50}px`; | |
| npc.style.bottom='0px'; | |
| area.append(npc); | |
| setInterval(()=>{ | |
| coins+=1; | |
| coinDisplay.textContent='Coins: '+coins; | |
| BobloxData.coins=coins; | |
| },1000); | |
| }); | |
| } | |
| // 3. Jump Test | |
| function startBattle(container){ | |
| const info=document.createElement('div'); | |
| info.textContent='Jump test! Space to jump.'; | |
| container.append(info); | |
| const area=document.createElement('div'); | |
| area.style.position='relative'; | |
| area.style.height='200px'; | |
| area.style.border='1px solid #000'; | |
| container.append(area); | |
| const player=createNPC('P'); | |
| player.style.background=BobloxData.avatarColor; | |
| player.style.left='20px'; | |
| player.style.bottom='0px'; | |
| area.append(player); | |
| const npcs=['A','B','C'].map(createNPC); | |
| npcs.forEach((npc,i)=>{ | |
| npc.style.left=`${100+i*50}px`; | |
| npc.style.bottom='0px'; | |
| area.append(npc); | |
| setInterval(()=>{ | |
| npc.style.bottom=(Math.random()*100)+'px'; | |
| },700+Math.random()*500); | |
| }); | |
| let left=20, bottom=0; | |
| document.addEventListener('keydown',e=>{ | |
| if(e.code==='ArrowLeft') left-=10; | |
| if(e.code==='ArrowRight') left+=10; | |
| if(e.code==='ArrowUp') bottom+=20; | |
| player.style.left=left+'px'; | |
| player.style.bottom=bottom+'px'; | |
| setTimeout(()=> { bottom=0; player.style.bottom=bottom+'px'; },300); | |
| }); | |
| } | |
| // 4. Content Trains Boblox | |
| function startRace(container){ | |
| const info=document.createElement('div'); | |
| info.textContent='🚂 Content Trains Boblox! Press Arrows to move.'; | |
| container.append(info); | |
| const track=document.createElement('div'); | |
| track.style.position='relative'; | |
| track.style.height='150px'; | |
| track.style.border='1px solid #000'; | |
| container.append(track); | |
| const player=createNPC('P'); | |
| player.style.background=BobloxData.avatarColor; | |
| player.style.left='20px'; | |
| player.style.bottom='0px'; | |
| track.append(player); | |
| const npcs=['A','B','C'].map(createNPC); | |
| npcs.forEach((npc,i)=>{ | |
| npc.style.left=`${20+i*40}px`; | |
| npc.style.bottom='0px'; | |
| track.append(npc); | |
| setInterval(()=>{ | |
| const cur=parseInt(npc.style.left); | |
| npc.style.left=(cur+Math.random()*5)+'px'; | |
| },200); | |
| }); | |
| let left=20; | |
| document.addEventListener('keydown',e=>{ | |
| if(e.code==='ArrowRight'){ | |
| left+=10; | |
| player.style.left=left+'px'; | |
| } | |
| }); | |
| } | |
| function startMinesweeper(container, cols=10, rows=10, mines=12){ | |
| if(!container) return; | |
| container.innerHTML=''; | |
| const grid = []; | |
| const field = document.createElement('div'); field.style.display='grid'; field.style.gridTemplateColumns = `repeat(${cols}, 30px)`; field.style.gap='4px'; | |
| container.append(field); | |
| // create cells | |
| for(let r=0;r<rows;r++){ | |
| for(let c=0;c<cols;c++){ | |
| const cell = {r,c, mine:false, revealed:false, flagged:false, el: null}; | |
| const btn = document.createElement('button'); btn.style.width='30px'; btn.style.height='30px'; btn.style.borderRadius='6px'; | |
| cell.el = btn; | |
| btn.addEventListener('click', ()=> reveal(cell)); | |
| btn.addEventListener('contextmenu', (ev)=>{ ev.preventDefault(); cell.flagged = !cell.flagged; btn.textContent = cell.flagged ? '🚩' : ''; }); | |
| field.append(btn); | |
| grid.push(cell); | |
| } | |
| } | |
| // place mines | |
| for(let m=0;m<mines;m++){ | |
| let idx; | |
| do { idx = Math.floor(Math.random()*grid.length); } while(grid[idx].mine); | |
| grid[idx].mine = true; | |
| } | |
| function neighbors(cell){ | |
| const arr=[]; | |
| for(const n of grid){ | |
| if(Math.abs(n.r - cell.r) <= 1 && Math.abs(n.c - cell.c) <=1 && !(n.r===cell.r && n.c===cell.c)) arr.push(n); | |
| } | |
| return arr; | |
| } | |
| function reveal(cell){ | |
| if(cell.revealed || cell.flagged) return; | |
| cell.revealed = true; cell.el.style.background='#0b2a3a'; cell.el.disabled=true; | |
| if(cell.mine){ cell.el.textContent='💣'; alert('Boom!'); return; } | |
| const count = neighbors(cell).filter(x=>x.mine).length; | |
| if(count>0) cell.el.textContent = count; | |
| else neighbors(cell).forEach(n=> reveal(n)); | |
| } | |
| } | |
| // Tie into Files delete operation (override pattern): when Files app 'delete' is called, we can't intercept internal function easily, | |
| // so provide a small UI helper: select file and 'Send to Recycle Bin' via the Files UI manual action. | |
| // Add a menu item in each file card for "Send to Recycle" if Files is open: we won't modify Files code; users can use the New File + Recycle Bin manually. | |
| // If wallpaper was set by the Image Viewer earlier, restore it on load | |
| const ws = localStorage.getItem('OS:wall-src'); | |
| if(ws) wallpaperEl.style.background = `url(${ws}) center/cover`; | |
| // Reshow start menu tile list to include newly added apps | |
| // (append only the ones not already present) | |
| const existingTiles = new Set([...document.querySelectorAll('#appGrid .app-tile')].map(t=>t.textContent.trim())); | |
| Object.entries(OS.apps).forEach(([id, a])=>{ | |
| const text = a.title + id; | |
| // don't duplicate exact tile names: check by id shown | |
| if(!document.querySelector(`#appGrid div:contains(${id})`)) { | |
| // simple check: add everything (some duplicates may appear if original later runs; safe) | |
| } | |
| }); | |
| // Add keyboard shortcut: Ctrl+Alt+G -> open App Store / Game Hub | |
| window.addEventListener('keydown', (e)=>{ if(e.ctrlKey && e.altKey && e.key.toLowerCase()==='g'){ OS.launch({ id:'app_store', title:OS.apps.app_store.title, icon: iconSquare(), content: OS.apps.app_store.content, size: OS.apps.app_store.size }); } }); | |
| // Also add simple API so other scripts can add apps later: | |
| window.AeroLiteOS.registerApp = function(id, app){ if(OS.apps[id]) return false; OS.apps[id] = app; return true; }; | |
| // Small helper: when Files delete triggers outside, user can call OSDeleteToRecycle({name,data}) | |
| // Example usage in browser console: OSDeleteToRecycle({name:'demo.txt', data:'hello'}); | |
| })(); | |
| </script> | |
| <!-- End of file --> | |
| </body> | |
| </html> | |
| <!-- Please Support this project here https://www.patreon.com/Calacobragameengine/membership --> |
Author
Author
A new update is now here lemme update the code!
Author
0.7 is here! Help fund the project! https://www.patreon.com/Calacobragameengine/membership
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You can donate to this project here https://www.patreon.com/Calacobragameengine/membership