import {APP_VERSION,DEFAULTS,settingsFrom,validShortcut,calculate,relayTarget,htmlGame,accentInk,viewItems} from "./hub-model.js"; const $=id=>document.getElementById(id); const paths={ home:'', grid:'', star:'', apps:'', game:'', tool:'', settings:'', plus:'',search:'',arrow:'', close:'', eye:'', "eye-off":'', maximize:'', minimize:'',chat:'', play:'',music:'', code:'',globe:'', note:'', clock:'', calculator:'', snake:'', memory:'', palette:'', link:'', drive:'' }; const ICON_SHEETS={"1":"/assets/icons-1.png","2":"/assets/icons-2.png"}; const spriteCells={"1":{grid:[185,76,39,32],apps:[185,76,39,32],star:[418,166,46,45],game:[123,216,45,44],settings:[271,174,44,44],tool:[271,174,44,44],chat:[72,121,50,44],play:[15,230,43,53],note:[135,77,36,31],clock:[219,258,43,43],calculator:[382,8,37,34]},"2":{grid:[145,298,73,73],apps:[145,298,73,73],game:[364,0,73,73],settings:[73,946,73,73],tool:[73,946,73,73],chat:[292,0,73,73],play:[292,0,73,73],music:[510,0,73,73],code:[730,0,73,73],globe:[0,0,73,73],note:[146,73,73,73],clock:[438,0,73,73],calculator:[73,946,73,73],memory:[145,298,73,73],palette:[0,584,73,73],drive:[145,298,73,73]}}; const BRAND_ICONS={discord:"/assets/brand-discord.svg",youtube:"/assets/brand-youtube.svg",spotify:"/assets/brand-spotify.svg",github:"/assets/brand-github.svg",photopea:"/assets/brand-photopea.svg",wikipedia:"/assets/brand-wikipedia.svg",drive:"/assets/brand-googledrive.svg"}; function icon(name,control=false){const set=control?"outline":document.documentElement.dataset.iconSet||"outline",cell=spriteCells[set]?.[name];if(cell)return '';return '';} document.querySelectorAll("[data-icon]").forEach(node=>node.innerHTML=icon(node.dataset.icon,true)); function read(key,fallback){try{const value=JSON.parse(localStorage.getItem("zs."+key));return value??fallback;}catch{return fallback;}} let toastTimer; function toast(message){$("toast").textContent=message;$("toast").classList.add("show");clearTimeout(toastTimer);toastTimer=setTimeout(()=>$("toast").classList.remove("show"),3000);} function save(key,value){try{localStorage.setItem("zs."+key,JSON.stringify(value));return true;}catch{$("save-status").textContent="Browser storage is unavailable. Changes last for this visit.";toast("Storage is full or unavailable. Export a backup to keep your data.");return false;}} let settings=settingsFrom(read("settings",{})); if(new URLSearchParams(location.search).get("resetUi")==="1"){ for(const key of ["backgroundHex","panelHex","textHex","borderHex","scrollbarHex","radius","textScale","density","scrollbarSize","customCss"])settings[key]=DEFAULTS[key]; save("settings",settings); } const storedCustom=read("shortcuts",[]),storedFavorites=read("favorites",[]); let custom=[];for(const entry of (Array.isArray(storedCustom)?storedCustom:[]).slice(0,100)){try{custom.push(validShortcut(entry));}catch{}} let favorites=new Set((Array.isArray(storedFavorites)?storedFavorites:[]).filter(value=>typeof value==="string")); let games=[];for(const entry of (Array.isArray(read("games",[]))?read("games",[]):[]).slice(0,10)){try{games.push(htmlGame(entry));}catch{}} let relayModes=read("relayModes",{});if(!relayModes||typeof relayModes!=="object"||Array.isArray(relayModes))relayModes={}; let recent=read("recent",{});if(!recent||typeof recent!=="object")recent={}; let selectedId=null,editingId=null; let section=settings.remember?read("section","all"):"all",filter="all"; if(!["all","favorites","apps","games","utilities"].includes(section))section="all"; const BUILTINS=[ {id:"discord",name:"Discord",category:"apps",description:"Open Discord with the relay.",kind:"proxy",icon:"chat",color:"#a8acff",bg:"#30345c"}, {id:"youtube",name:"YouTube",category:"apps",description:"Videos.",kind:"external",url:"https://www.youtube.com/",icon:"play",color:"#ff9695",bg:"#452828"}, {id:"spotify",name:"Spotify",category:"apps",description:"Music.",kind:"external",url:"https://open.spotify.com/",icon:"music",color:"#8ee9b1",bg:"#213d2d"}, {id:"notes",name:"Quick Notes",category:"utilities",description:"Write and save notes.",kind:"builtin",icon:"note",color:"#f0d28b",bg:"#3c3522"}, {id:"focus",name:"Focus Timer",category:"utilities",description:"Focus and break timer.",kind:"builtin",icon:"clock",color:"#9fd9e9",bg:"#253b40"}, {id:"calculator",name:"Calculator",category:"utilities",description:"Basic calculator.",kind:"builtin",icon:"calculator",color:"#b6bfef",bg:"#2c3248"}, {id:"github",name:"GitHub",category:"apps",description:"Code and repositories.",kind:"external",url:"https://github.com/",icon:"code",color:"#d9dce4",bg:"#34373e"}, {id:"photopea",name:"Photopea",category:"utilities",description:"Image editor.",kind:"external",url:"https://www.photopea.com/",icon:"palette",color:"#a1e4da",bg:"#203e38"}, {id:"wikipedia",name:"Wikipedia",category:"apps",description:"Search Wikipedia.",kind:"external",url:"https://www.wikipedia.org/",icon:"globe",color:"#e3e0cc",bg:"#38382f"}, {id:"drive",name:"Google Drive",category:"apps",description:"Files and storage.",kind:"external",url:"https://drive.google.com/",icon:"drive",color:"#f1cf91",bg:"#3d3426"} ]; const all=()=>[...BUILTINS,...custom,...games]; const media=matchMedia("(prefers-color-scheme: light)"); function applySettings(){ const root=document.documentElement; root.dataset.theme=settings.theme==="system"?(media.matches?"light":"dark"):settings.theme; for(const key of ["accent","motion","compact","grid"])root.dataset[key]=String(settings[key]); root.dataset.iconSet=settings.iconSet; root.dataset.density=settings.density; for(const [key,variable] of Object.entries({backgroundHex:"--hub-bg",panelHex:"--hub-panel",textHex:"--text",borderHex:"--line",scrollbarHex:"--scroll-thumb"})){ if(settings[key])root.style.setProperty(variable,settings[key]);else root.style.removeProperty(variable); const input=$("design-"+key);if(input)input.value=settings[key]||getComputedStyle(root).getPropertyValue(variable).trim()||"#747980"; } if(settings.borderHex)root.style.setProperty("--hub-line",settings.borderHex);else root.style.removeProperty("--hub-line"); if(settings.panelHex||settings.backgroundHex)root.style.setProperty("--surface2","color-mix(in srgb,var(--hub-panel) 60%,var(--hub-bg))");else root.style.removeProperty("--surface2"); root.style.setProperty("--ui-radius",settings.radius+"px");root.style.setProperty("--ui-scale",settings.textScale/100);root.style.setProperty("--scroll-size",settings.scrollbarSize+"px"); for(const key of ["radius","textScale","scrollbarSize"])$("design-"+key).value=settings[key]; $("design-density").value=settings.density; let sheet=$("user-design-css");if(!sheet){sheet=document.createElement("style");sheet.id="user-design-css";document.head.append(sheet);}sheet.textContent=settings.customCss; $("custom-css").value=settings.customCss; if(settings.accentHex){root.style.setProperty("--accent",settings.accentHex);root.style.setProperty("--on-accent",accentInk(settings.accentHex));root.style.setProperty("--soft",settings.accentHex+"18");}else for(const key of ["--accent","--on-accent","--soft"])root.style.removeProperty(key); $("accent-custom").value=settings.accentHex||getComputedStyle(root).getPropertyValue("--accent").trim();$("accent-hex").value=$("accent-custom").value.toUpperCase();$("icon-set").value=settings.iconSet; document.querySelectorAll("[data-icon]").forEach(node=>node.innerHTML=icon(node.dataset.icon,true)); $("live-clock").hidden=!settings.clock; $("setting-theme").value=settings.theme;$("setting-accent").value=settings.accent; document.querySelectorAll("[data-setting]").forEach(input=>input.checked=settings[input.dataset.setting]); } media.addEventListener("change",()=>{if(settings.theme==="system")applySettings();}); applySettings(); function tickClock(){$("live-clock").textContent=new Date().toLocaleTimeString([], {hour:"numeric",minute:"2-digit"});} tickClock();setInterval(tickClock,30_000); document.querySelectorAll("[data-version]").forEach(node=>node.textContent=APP_VERSION); const titles={all:"All stuff",favorites:"Favorites",apps:"Apps",games:"Games",utilities:"Utilities"}; function render(){ const query=$("search").value.trim(); const items=viewItems(all(),{section,filter,query,sort:$("sort").value,favorites:[...favorites],recent}); if(!items.some(app=>app.id===selectedId))selectedId=(items.find(app=>app.kind==="builtin")||items[0])?.id||null; $("page-title").textContent=section==="all"?"Pick something to open":section==="utilities"?"Tools":titles[section]; $("page-count").textContent=String(items.length); document.querySelector(".filters").hidden=section!=="all"; $("total-count").textContent=String(all().length);$("favorite-count").textContent=String(all().filter(app=>favorites.has(app.id)).length); document.querySelectorAll("[data-section]").forEach(button=>{button.classList.toggle("active",button.dataset.section===section);button.setAttribute("aria-current",button.dataset.section===section?"page":"false");}); document.querySelectorAll("[data-filter]").forEach(button=>{const active=button.dataset.filter===filter;button.classList.toggle("active",active);button.setAttribute("aria-pressed",String(active));}); $("card-grid").replaceChildren(); for(const app of items){ const card=document.createElement("article");card.className="launch-card";card.dataset.selected=String(app.id===selectedId); const launch=document.createElement("button");launch.className="card-launch";launch.dataset.launch=app.id;launch.setAttribute("aria-label","Launch "+app.name); const tile=document.createElement("span");tile.className="app-icon";tile.style.setProperty("--icon-bg",app.bg);tile.style.setProperty("--icon-color",app.color);if(BRAND_ICONS[app.id]){const image=document.createElement("img");image.src=BRAND_ICONS[app.id];image.dataset.brand=app.id;image.alt="";tile.append(image);}else tile.innerHTML=icon(app.icon); const title=document.createElement("h3");title.textContent=app.name; const description=document.createElement("p");description.textContent=app.description||"Custom shortcut."; const bottom=document.createElement("span");bottom.className="card-bottom"; const type=document.createElement("span");type.textContent=app.kind==="builtin"?"BUILT IN":app.kind==="html"?"HTML GAME":(relayModes[app.id]??app.kind==="proxy")?"RELAY":"WEBSITE"; bottom.append(type);const art=document.createElement("span");art.className="card-art";art.style.setProperty("--card-tint",app.bg||"#343434");art.append(tile);const select=document.createElement("button");select.className="card-select";select.textContent="OPEN";select.dataset.launch=app.id;select.setAttribute("aria-label","Open "+app.name);launch.append(title,art,description,bottom); const star=document.createElement("button");star.className="card-favorite";star.dataset.favorite=app.id;star.setAttribute("aria-label",(favorites.has(app.id)?"Unfavorite ":"Favorite ")+app.name);star.setAttribute("aria-pressed",String(favorites.has(app.id)));star.innerHTML=icon("star",true);const details=document.createElement("button");details.className="card-details";details.textContent="Details";details.dataset.select=app.id;details.setAttribute("aria-label","Details for "+app.name);card.append(launch,star,select,details); if(app.kind==="external"||app.kind==="proxy"){const relay=document.createElement("button");relay.className="card-relay";relay.dataset.relay=app.id;relay.setAttribute("aria-label","Use relay for "+app.name);relay.setAttribute("aria-pressed",String(relayModes[app.id]??app.kind==="proxy"));relay.textContent=(relayModes[app.id]??app.kind==="proxy")?"Relay: on":"Relay: off";card.append(relay);} $("card-grid").append(card); } $("empty-state").hidden=items.length!==0;$("shown-count").textContent=items.length+" items"; $("empty-description").textContent=query?"No results for “"+query+"”.":section==="favorites"?"Use the star on a card to save it here.":"Add a shortcut or import a game."; $("clear-search").hidden=!query;updatePreview(); } $("close-preview").addEventListener("click",()=>{document.body.dataset.preview="false";}); $("card-grid").addEventListener("click",event=>{if(event.target.closest("[data-select]"))document.body.dataset.preview="true";}); function switchSection(value){document.body.dataset.preview="false";section=value;filter="all";$("search").value="";if(settings.remember)save("section",section);render();} $("navigation").addEventListener("click",event=>{const button=event.target.closest("[data-section]");if(button)switchSection(button.dataset.section);}); document.querySelector(".filters").addEventListener("click",event=>{const button=event.target.closest("[data-filter]");if(button){filter=button.dataset.filter;render();}}); $("clear-search").addEventListener("click",()=>{$("search").value="";render();$("search").focus();}); $("search").addEventListener("input",render);$("sort").addEventListener("change",render); $("card-grid").addEventListener("click",event=>{ const selected=event.target.closest("[data-select]");if(selected){selectApp(selected.dataset.select);return;} const relay=event.target.closest("[data-relay]");if(relay){const app=all().find(a=>a.id===relay.dataset.relay);relayModes[app.id]=!(relayModes[app.id]??app.kind==="proxy");save("relayModes",relayModes);render();return;} const star=event.target.closest("[data-favorite]");if(star){const id=star.dataset.favorite;favorites.has(id)?favorites.delete(id):favorites.add(id);save("favorites",[...favorites]);render();return;} const button=event.target.closest("[data-launch]");if(button)launch(all().find(app=>app.id===button.dataset.launch)); }); function removeApp(id){ const nextCustom=custom.filter(app=>app.id!==id),nextGames=games.filter(app=>app.id!==id); if(nextCustom.length!==custom.length&&!save("shortcuts",nextCustom))return; if(nextGames.length!==games.length&&!save("games",nextGames))return; custom=nextCustom;games=nextGames;favorites.delete(id);save("favorites",[...favorites]);render();toast("Removed."); } function appType(app){return app.kind==="html"?"HTML GAME":app.kind==="builtin"?"BUILT IN":(relayModes[app.id]??app.kind==="proxy")?"RELAY":"WEBSITE";} function selectApp(id){selectedId=id;document.querySelectorAll("[data-select]").forEach(button=>{const selected=button.dataset.select===id;button.setAttribute("aria-pressed",String(selected));button.closest(".launch-card").dataset.selected=String(selected);});updatePreview();if(matchMedia("(max-width: 900px)").matches)$("item-preview").scrollIntoView({block:"nearest",behavior:settings.motion?"smooth":"auto"});} function updatePreview(){ const app=all().find(item=>item.id===selectedId);$("item-preview").hidden=!app;if(!app)return; $("preview-open").setAttribute("aria-label","Open selected "+app.name);$("preview-title").textContent=app.name;$("preview-description").textContent=app.description||"Custom shortcut.";$("preview-type").textContent=appType(app);$("preview-category").textContent={apps:"Apps",games:"Games",utilities:"Utilities"}[app.category]; const website=app.kind==="external"||app.kind==="proxy",relay=relayModes[app.id]??app.kind==="proxy"; $("preview-destination").textContent=website?new URL(app.url||"https://discord.com/login").hostname:"In this hub"; $("preview-relay-row").hidden=!website;$("preview-relay").checked=Boolean(relay);$("preview-relay").setAttribute("aria-label","Use relay for selected item"); $("preview-speed-row").hidden=!website; $("preview-speed").textContent=app.id==="discord"?"Slow — allow extra time to load through the relay.":"Unknown — not measured yet."; $("preview-help").textContent=website?(relay?"Uses the private relay. Some sites may not work.":settings.newTab?"Opens in a new tab.":"Opens in this tab."):app.kind==="html"?"Imported file. Runs in a sandbox.":"Built in. No relay needed."; $("preview-edit").hidden=!(app.custom&&app.kind==="external");$("preview-remove").hidden=!app.custom; const art=$("preview-art");art.replaceChildren();art.style.setProperty("--card-tint",app.bg||"#343434");const tile=document.createElement("span");tile.className="app-icon";tile.style.setProperty("--icon-color",app.color);if(BRAND_ICONS[app.id]){const image=document.createElement("img");image.src=BRAND_ICONS[app.id];image.dataset.brand=app.id;image.alt="";tile.append(image);}else tile.innerHTML=icon(app.icon);art.append(tile); $("preview-favorite").setAttribute("aria-pressed",String(favorites.has(app.id)));$("preview-favorite").setAttribute("aria-label",(favorites.has(app.id)?"Unfavorite ":"Favorite ")+app.name);$("preview-favorite").innerHTML=icon("star"); } $("preview-open").addEventListener("click",()=>launch(all().find(app=>app.id===selectedId))); $("preview-favorite").addEventListener("click",()=>{favorites.has(selectedId)?favorites.delete(selectedId):favorites.add(selectedId);save("favorites",[...favorites]);render();}); $("preview-relay").addEventListener("change",event=>{relayModes[selectedId]=event.target.checked;save("relayModes",relayModes);render();}); $("preview-remove").addEventListener("click",()=>removeApp(selectedId)); $("preview-edit").addEventListener("click",()=>addShortcut(all().find(app=>app.id===selectedId))); function launch(app){ if(!app)return;recent[app.id]=Date.now();save("recent",recent); if(app.kind==="html"){openHtmlGame(app);return;} if(app.kind==="external"||app.kind==="proxy"){ const url=app.url||"https://discord.com/login"; if(relayModes[app.id]??app.kind==="proxy"){openRelay(url);return;} if(settings.newTab)window.open(url,"_blank","noopener,noreferrer");else location.assign(url);return; } openApp(app); } // Top bar visibility is saved locally; floating controls keep the UI recoverable. function updateDisplayControls(){ const hidden=read("topbarHidden",false)===true; document.documentElement.dataset.topbarHidden=String(hidden); $("floating-display-controls").hidden=!hidden; const fullscreen=Boolean(document.fullscreenElement); for(const id of ["toggle-fullscreen","toggle-fullscreen-floating"]){ const button=$(id);button.setAttribute("aria-pressed",String(fullscreen)); button.setAttribute("aria-label",fullscreen?"Exit fullscreen":"Enter fullscreen"); button.title=fullscreen?"Exit fullscreen (F11 for browser fullscreen)":"Enter fullscreen"; button.innerHTML=icon(fullscreen?"minimize":"maximize",true)+(id==="toggle-fullscreen"?'Screen':''); } } $("hide-topbar").addEventListener("click",()=>{save("topbarHidden",true);updateDisplayControls();}); $("show-topbar").addEventListener("click",()=>{save("topbarHidden",false);updateDisplayControls();}); async function toggleFullscreen(){ try { if(document.fullscreenElement)await document.exitFullscreen(); else if(document.documentElement.requestFullscreen)await document.documentElement.requestFullscreen(); else throw new Error("Fullscreen API unavailable"); }catch{toast("Fullscreen is unavailable in this browser. Try the browser fullscreen shortcut.");} updateDisplayControls(); } for(const id of ["toggle-fullscreen","toggle-fullscreen-floating"])$(id).addEventListener("click",toggleFullscreen); document.addEventListener("fullscreenchange",updateDisplayControls); updateDisplayControls(); document.querySelectorAll(".settings-open").forEach(button=>button.addEventListener("click",()=>$("settings-dialog").showModal())); document.querySelectorAll("[data-close]").forEach(button=>button.addEventListener("click",()=>$(button.dataset.close).close())); function changeSettings(){settings=settingsFrom(settings);save("settings",settings);applySettings();updateDisplayControls();if(selectedId)updatePreview();} $("setting-theme").addEventListener("change",event=>{settings.theme=event.target.value;changeSettings();}); $("setting-accent").addEventListener("change",event=>{settings.accent=event.target.value;settings.accentHex="";changeSettings();render();}); document.querySelectorAll("[data-setting]").forEach(input=>input.addEventListener("change",()=>{settings[input.dataset.setting]=input.checked;changeSettings();})); $("reset-settings").addEventListener("click",()=>{settings={...DEFAULTS};changeSettings();render();toast("Preferences reset. Your notes and shortcuts are still here.");}); $("accent-custom").addEventListener("input",event=>{settings.accentHex=event.target.value;changeSettings();}); $("accent-hex").addEventListener("change",event=>{if(!/^#[0-9a-f]{6}$/i.test(event.target.value)){event.target.setCustomValidity("Use a color like #76FA74.");event.target.reportValidity();return;}event.target.setCustomValidity("");settings.accentHex=event.target.value;changeSettings();}); $("accent-hex").addEventListener("input",event=>{event.target.setCustomValidity("");if(/^#[0-9a-f]{6}$/i.test(event.target.value)){settings.accentHex=event.target.value;changeSettings();}}); $("accent-reset").addEventListener("click",()=>{settings.accentHex="";changeSettings();}); document.querySelectorAll("[data-design-color]").forEach(input=>input.addEventListener("input",()=>{settings[input.dataset.designColor]=input.value;changeSettings();})); document.querySelectorAll("[data-design-number]").forEach(input=>input.addEventListener("change",()=>{settings[input.dataset.designNumber]=Number(input.value);changeSettings();})); $("design-density").addEventListener("change",event=>{settings.density=event.target.value;changeSettings();}); $("reset-design").addEventListener("click",()=>{for(const key of ["backgroundHex","panelHex","textHex","borderHex","scrollbarHex","radius","textScale","density","scrollbarSize","customCss"])settings[key]=DEFAULTS[key];changeSettings();toast("Design reset. Your stuff is still here.");}); $("apply-css").addEventListener("click",()=>{const css=$("custom-css").value;const parsed=settingsFrom({...settings,customCss:css});if(css&&parsed.customCss!==css){$("css-status").textContent="Remove remote URLs or imports before applying.";return;}settings.customCss=css;changeSettings();$("css-status").textContent="CSS applied and saved.";}); $("clear-css").addEventListener("click",()=>{settings.customCss="";changeSettings();$("css-status").textContent="Custom CSS cleared.";}); $("icon-set").addEventListener("change",event=>{settings.iconSet=event.target.value;changeSettings();render();}); $("changelog-open").addEventListener("click",()=>$("changelog-dialog").showModal()); let introTimer,finishTimer,loadGeneration=0,introReturnFocus=null; function finishIntro(){ loadGeneration++; clearTimeout(introTimer);clearTimeout(finishTimer);$("intro").classList.add("intro-leaving"); finishTimer=setTimeout(()=>{$("intro").hidden=true;document.querySelector(".topbar").inert=false;$("launcher").inert=false;$("floating-display-controls").inert=false;$("workspace").inert=false;if($("intro").contains(document.activeElement))introReturnFocus?.focus({preventScroll:true});},settings.motion?350:0); } async function playIntro(force=false){ const reduced=matchMedia("(prefers-reduced-motion: reduce)").matches; if((!force&&!settings.intro)||!settings.motion||reduced){$("intro").hidden=true;return;} introReturnFocus=document.activeElement?.closest("#launcher")?document.activeElement:$("search"); const generation=++loadGeneration;clearTimeout(introTimer);clearTimeout(finishTimer);document.documentElement.dataset.intro="on"; $("intro").hidden=false;$("intro").classList.remove("intro-leaving");document.querySelector(".topbar").inert=true;$("launcher").inert=true;$("floating-display-controls").inert=true;$("workspace").inert=true; const label=$("loading-status"),progress=$("loading-progress"),skip=$("skip-intro");skip.disabled=false;skip.focus({preventScroll:true});progress.value=1;label.textContent="UI ready. Loading logo…"; introTimer=setTimeout(()=>{if(generation===loadGeneration)label.textContent="UI is ready. Optional assets are taking a while. You can skip.";},6000); const wait=(promise)=>Promise.race([promise,new Promise(resolve=>setTimeout(resolve,8000))]); await wait(Promise.allSettled([...document.querySelectorAll(".corner-logo img,.loading-mark img")].map(img=>img.decode()))); if(generation!==loadGeneration||$("intro").hidden)return;progress.value=2;label.textContent="Loading font and icons…"; await wait(Promise.allSettled([document.fonts.load("16px Ultraproxi"),...(spriteCells[settings.iconSet]?[settings.iconSet]:[]).map(set=>new Promise(resolve=>{const img=new Image();img.onload=img.onerror=resolve;img.src=ICON_SHEETS[set];}))])); if(generation!==loadGeneration||$("intro").hidden)return;progress.value=3;label.textContent="Ready.";clearTimeout(introTimer);introTimer=setTimeout(finishIntro,500); } $("skip-intro").addEventListener("click",()=>{if(!$("skip-intro").disabled)finishIntro();}); $("replay-intro").addEventListener("click",()=>{$("settings-dialog").close();if(!settings.motion)toast("Enable animations to replay the intro.");else playIntro(true);}); document.addEventListener("keydown",event=>{ if(event.key==="Escape"&&!$("intro").hidden){finishIntro();return;} if(event.key==="/"&&!document.querySelector("dialog[open]")&&$("workspace").hidden&&!["INPUT","TEXTAREA"].includes(document.activeElement.tagName)){event.preventDefault();$("search").focus();} }); function addShortcut(app){editingId=app?.custom&&app.kind==="external"?app.id:null;$("shortcut-form").reset();$("shortcut-error").textContent="";$("shortcut-title").textContent=editingId?"Edit shortcut":"Add shortcut";$("shortcut-submit-label").textContent=editingId?"Save changes":"Add shortcut";if(editingId)for(const key of ["name","url","category","description"])$("shortcut-form").elements[key].value=app[key]||"";$("shortcut-dialog").showModal();} $("add-shortcut").addEventListener("click",addShortcut);$("add-shortcut-inline").addEventListener("click",addShortcut); $("shortcut-form").addEventListener("submit",event=>{ event.preventDefault();try{if(!editingId&&custom.length>=100)throw new Error("You can save up to 100 shortcuts.");const app=validShortcut({...Object.fromEntries(new FormData(event.target)),...(editingId?{id:editingId}:{})});const next=editingId?custom.map(item=>item.id===editingId?app:item):[...custom,app];if(!save("shortcuts",next))return;custom=next;selectedId=app.id;$("shortcut-dialog").close();switchSection("all");toast(editingId?"Shortcut saved.":"Shortcut added.");}catch(error){$("shortcut-error").textContent=error.message;} }); function download(filename,data){const url=URL.createObjectURL(new Blob([data],{type:"application/json"}));const link=document.createElement("a");link.href=url;link.download=filename;link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);} $("export-data").addEventListener("click",()=>download("ZeyadsStuffV2-backup.json",JSON.stringify({version:1,settings,favorites:[...favorites],shortcuts:custom,notes:read("notes",""),snakeBest:read("snakeBest",0),games,relayModes},null,2))); $("import-data").addEventListener("click",()=>$("import-file").click()); $("import-file").addEventListener("change",async event=>{ const file=event.target.files[0];if(!file)return; try{ if(file.size>6*1024*1024)throw new Error("Choose a backup smaller than 6 MB."); const data=JSON.parse(await file.text());if(!data||data.version!==1||!Array.isArray(data.shortcuts)||data.shortcuts.length>100)throw new Error("That is not a compatible backup."); const imported=data.shortcuts.map(validShortcut);const importedGames=(Array.isArray(data.games)?data.games:[]).slice(0,10).map(htmlGame);if(new Set(imported.map(a=>a.id)).size!==imported.length)throw new Error("The backup has duplicate shortcut IDs."); settings=settingsFrom(data.settings);custom=imported;games=importedGames;save("games",games);relayModes=data.relayModes&&typeof data.relayModes==="object"&&!Array.isArray(data.relayModes)?Object.fromEntries(Object.entries(data.relayModes).filter(([k,v])=>typeof v==="boolean")):{};save("relayModes",relayModes);favorites=new Set(Array.isArray(data.favorites)?data.favorites.filter(x=>typeof x==="string"):[]); save("settings",settings);save("shortcuts",custom);save("favorites",[...favorites]); if(typeof data.notes==="string")save("notes",data.notes.slice(0,100000)); if(Number.isInteger(data.snakeBest)&&data.snakeBest>=0&&data.snakeBest<=400)save("snakeBest",data.snakeBest); applySettings();switchSection("all");toast("Your backup is restored."); }catch(error){toast(error.message);}finally{event.target.value="";} }); let cleanupApp=()=>{}; $("app-dialog").addEventListener("close",()=>{cleanupApp();cleanupApp=()=>{};}); function openApp(app){ cleanupApp();cleanupApp=()=>{};$("app-title").textContent=app.name;$("app-category").textContent=app.category==="games"?"GAME":"UTILITY"; $("app-body").replaceChildren(); ({notes:notesApp,focus:focusApp,calculator:calculatorApp,snake:snakeApp,memory:memoryApp}[app.id])(); $("app-dialog").showModal(); } function notesApp(){ const area=document.createElement("textarea");area.className="notes-area";area.setAttribute("aria-label","Your quick notes");area.placeholder="Type here…";area.maxLength=100000;area.value=read("notes",""); const hint=document.createElement("p");hint.className="muted";hint.textContent="Saved automatically on this device. "+area.value.length.toLocaleString()+" characters."; area.addEventListener("input",()=>{const success=save("notes",area.value);hint.textContent=(success?"Saved on this device. ":"Not saved — export a backup. ")+area.value.length.toLocaleString()+" characters.";}); $("app-body").append(area,hint); } function calculatorApp(){ const input=document.createElement("input");input.className="calc-display";input.setAttribute("aria-label","Expression");input.placeholder="0";input.maxLength=300;input.inputMode="decimal"; const hint=document.createElement("p");hint.className="muted";hint.setAttribute("role","status");hint.textContent="Parentheses and normal operation order supported. % is remainder."; const keypad=document.createElement("div");keypad.className="calc-grid"; function evaluate(){try{input.value=calculate(input.value);hint.textContent="Calculated."; }catch(error){hint.textContent=error.message;}} for(const key of ["C","(",")","÷","7","8","9","×","4","5","6","−","1","2","3","+","0",".","⌫","="]){ const button=document.createElement("button");button.textContent=key;button.addEventListener("click",()=>{if(key==="C")input.value="";else if(key==="⌫")input.value=input.value.slice(0,-1);else if(key==="=")evaluate();else input.value+=key==="−"?"-":key;});keypad.append(button); } input.addEventListener("keydown",event=>{if(event.key==="Enter"){event.preventDefault();evaluate();}}); $("app-body").append(input,keypad,hint); } function focusApp(){ $("app-body").innerHTML='

Time to focus.

The timer stops when you close this tool.

'; let remaining=settings.focusMinutes*60,running=false,phase="focus",deadline=0; const display=document.querySelector(".timer-display"),label=document.querySelector(".timer-phase"); $("focus-minutes").value=settings.focusMinutes;$("break-minutes").value=settings.breakMinutes; function paint(){display.textContent=String(Math.floor(remaining/60)).padStart(2,"0")+":"+String(remaining%60).padStart(2,"0");$("timer-start").textContent=running?"Pause":phase==="focus"?"Start focus":"Start break";} function pause(){running=false;remaining=Math.max(0,Math.ceil((deadline-Date.now())/1000));paint();} const interval=setInterval(()=>{if(!running)return;remaining=Math.max(0,Math.ceil((deadline-Date.now())/1000));if(!remaining){running=false;phase=phase==="focus"?"break":"focus";remaining=settings[phase==="focus"?"focusMinutes":"breakMinutes"]*60;label.textContent=phase==="focus"?"Focus time.":"Break time.";toast(phase==="break"?"Focus session complete. Time for a break.":"Break complete.");}paint();},250); $("timer-start").addEventListener("click",()=>{if(running)pause();else{running=true;deadline=Date.now()+remaining*1000;paint();}}); $("timer-reset").addEventListener("click",()=>{running=false;phase="focus";remaining=settings.focusMinutes*60;label.textContent="Time to focus.";paint();}); for(const [id,key] of [["focus-minutes","focusMinutes"],["break-minutes","breakMinutes"]])$(id).addEventListener("change",event=>{const value=Number(event.target.value),max=key==="focusMinutes"?180:60;if(!Number.isInteger(value)||value<1||value>max){event.target.value=settings[key];return;}settings[key]=value;save("settings",settings);if(!running){remaining=settings[phase==="focus"?"focusMinutes":"breakMinutes"]*60;paint();}}); cleanupApp=()=>clearInterval(interval);paint(); } function snakeApp(){ $("app-body").innerHTML='
Score 0Best

Arrow keys or WASD. Touch controls work too.

'; const canvas=document.querySelector("canvas"),ctx=canvas.getContext("2d"),scope=new AbortController(); const theme=getComputedStyle(document.documentElement),accent=theme.getPropertyValue("--accent").trim(),bg=theme.getPropertyValue("--bg").trim(); let snake,dir,pending,food,timer,running=false,score=0,best=read("snakeBest",0); function foodAt(){const empty=[];for(let y=0;y<20;y++)for(let x=0;x<20;x++)if(!snake.some(p=>p.x===x&&p.y===y))empty.push({x,y});return empty[Math.floor(Math.random()*empty.length)];} function draw(){ctx.fillStyle=bg;ctx.fillRect(0,0,320,320);if(food){ctx.fillStyle="#efbf85";ctx.beginPath();ctx.arc(food.x*16+8,food.y*16+8,4,0,Math.PI*2);ctx.fill();}snake.forEach((p,i)=>{ctx.globalAlpha=i===0?1:.65;ctx.fillStyle=accent;ctx.fillRect(p.x*16+1,p.y*16+1,14,14);});ctx.globalAlpha=1;} function stop(message){running=false;clearInterval(timer);$("snake-start").textContent="Play again";$("snake-help").textContent=message;} function step(){dir=pending;const head={x:snake[0].x+dir.x,y:snake[0].y+dir.y},growing=head.x===food.x&&head.y===food.y;const body=growing?snake:snake.slice(0,-1);if(head.x<0||head.x>=20||head.y<0||head.y>=20||body.some(p=>p.x===head.x&&p.y===head.y)){stop("Game over.");return;}snake.unshift(head);if(growing){score++;if(score>best){best=score;save("snakeBest",best);}$("snake-score").textContent=score;$("snake-best").textContent=best;food=foodAt();if(!food){stop("Board complete.");draw();return;}}else snake.pop();draw();} function reset(){clearInterval(timer);snake=[{x:9,y:10},{x:8,y:10},{x:7,y:10}];dir={x:1,y:0};pending=dir;score=0;food=foodAt();$("snake-score").textContent="0";$("snake-best").textContent=best;draw();} const directions={up:{x:0,y:-1},down:{x:0,y:1},left:{x:-1,y:0},right:{x:1,y:0}}; function move(name){const next=directions[name];if(next&&(next.x!==-dir.x||next.y!==-dir.y))pending=next;} $("snake-start").addEventListener("click",()=>{reset();running=true;$("snake-start").textContent="Restart";$("snake-help").textContent="Arrow keys or WASD. Touch controls work too.";timer=setInterval(step,115);}); document.addEventListener("keydown",event=>{const name={ArrowUp:"up",w:"up",ArrowDown:"down",s:"down",ArrowLeft:"left",a:"left",ArrowRight:"right",d:"right"}[event.key];if(name){event.preventDefault();if(running)move(name);}},{signal:scope.signal}); document.querySelectorAll("[data-dir]").forEach(button=>button.addEventListener("click",()=>{if(running)move(button.dataset.dir);})); cleanupApp=()=>{clearInterval(timer);scope.abort();};reset(); } function memoryApp(){ $("app-body").innerHTML='
Moves 0Find all 8 pairs.
'; let deck=[],opened=[],matched=new Set(),moves=0,busy=false,flipTimer; const grid=document.querySelector(".memory-grid"),symbols=["✦","☾","✿","◆","☀","♣","♥","⚡"]; function draw(){grid.replaceChildren();deck.forEach((value,index)=>{const button=document.createElement("button"),revealed=opened.includes(index)||matched.has(index);button.className="memory-card"+(matched.has(index)?" matched":revealed?" flipped":"");button.textContent=revealed?symbols[value]:"·";button.setAttribute("aria-label",revealed?"Card "+(index+1)+": "+["star","moon","flower","diamond","sun","club","heart","lightning"][value]:"Reveal card "+(index+1));button.disabled=matched.has(index);button.addEventListener("click",()=>flip(index));grid.append(button);});} function flip(index){if(busy||matched.has(index)||opened.includes(index))return;opened.push(index);if(opened.length===2){moves++;$("memory-moves").textContent=moves;const[a,b]=opened;if(deck[a]===deck[b]){matched.add(a);matched.add(b);opened=[];if(matched.size===16)$("memory-state").textContent="All matched in "+moves+" moves!";}else{busy=true;flipTimer=setTimeout(()=>{opened=[];busy=false;draw();},800);}}draw();} function reset(){clearTimeout(flipTimer);deck=[0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7];for(let i=deck.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[deck[i],deck[j]]=[deck[j],deck[i]];}opened=[];matched.clear();busy=false;moves=0;$("memory-moves").textContent="0";$("memory-state").textContent="Find all 8 pairs.";draw();} $("memory-reset").addEventListener("click",reset);cleanupApp=()=>clearTimeout(flipTimer);reset(); } function openRelay(raw){ let url;try{url=relayTarget(raw);}catch(error){toast(error.message);return;} if(document.documentElement.dataset.singleFile==="true"){ const hosted=new URL("https://zeyadsstuffv2.zeyadmused.workers.dev/embed");hosted.searchParams.set("target",url); $("browser").src=hosted.href;$("launcher").hidden=true;$("workspace").hidden=false;$("home").hidden=false;$("relay-tab").href=hosted.href;$("relay-direct").href=url;$("relay-page-status").textContent="Hosted relay. If sign-in is blocked here, open in a tab.";return; } window.dispatchEvent(new CustomEvent("zs:relay",{detail:{url}})); } $("import-game").addEventListener("click",()=>$("game-file").click());$("import-game-inline").addEventListener("click",()=>$("game-file").click()); $("game-file").addEventListener("change",async event=>{const file=event.target.files[0];if(!file)return;try{if(file.size>1024*1024||games.length>=10)throw new Error("Use an HTML file under 1 MB. Up to 10 games.");const game=htmlGame({name:file.name.replace(/\.html?$/i,""),html:await file.text()});if(save("games",[...games,game])){games.push(game);switchSection("games");toast("Game added.");}}catch(error){toast(error.message);}finally{event.target.value="";}}); function openHtmlGame(app){cleanupApp();$("app-title").textContent=app.name;$("app-category").textContent="HTML GAME";const frame=document.createElement("iframe");frame.className="html-game";frame.title=app.name;frame.sandbox="allow-scripts allow-pointer-lock";frame.allow="fullscreen; gamepad";frame.srcdoc=app.html;$("app-body").replaceChildren(frame);cleanupApp=()=>frame.remove();$("app-dialog").showModal();} if(document.documentElement.dataset.singleFile==="true")$("relay-retry").addEventListener("click",()=>{const src=$("browser").getAttribute("src");if(src&&src!=="about:blank")$("browser").src=src;}); if(document.documentElement.dataset.singleFile==="true")$("home").addEventListener("click",()=>{$("browser").src="about:blank";$("workspace").hidden=true;$("launcher").hidden=false;$("home").hidden=true;}); render();playIntro();