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();