Files
Space-Theme/web/desktop/public/assets/ui.js
2026-02-03 09:18:15 +01:00

501 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function(){
const host = document.getElementById("toastHost");
function iconFor(type){
if(type === "success") return "✅";
if(type === "error") return "⛔";
if(type === "info") return "🛰️";
return "🔔";
}
window.toast = function(type="info", title="Notice", message="", ttl=3200){
if(!host) return;
const el = document.createElement("div");
el.className = `toast toast-${type}`;
el.innerHTML = `
<div class="toast-icon">${iconFor(type)}</div>
<div class="toast-body">
<div class="toast-title">${escapeHtml(title)}</div>
<div class="toast-msg">${escapeHtml(message)}</div>
</div>
<button class="toast-x" aria-label="Close">✕</button>
`;
const remove = () => {
el.classList.add("out");
el.addEventListener("animationend", () => el.remove(), { once:true });
};
el.querySelector(".toast-x").addEventListener("click", remove);
host.appendChild(el);
if (ttl > 0) setTimeout(remove, ttl);
};
function escapeHtml(s){
return String(s ?? "")
.replaceAll("&","&amp;")
.replaceAll("<","&lt;")
.replaceAll(">","&gt;")
.replaceAll('"',"&quot;")
.replaceAll("'","&#039;");
}
// Notifications dropdown (tiny JS)
const notif = document.getElementById("notifPanel");
window.toggleNotif = function(){
if(!notif) return;
const isHidden = notif.hasAttribute("hidden");
if(isHidden) notif.removeAttribute("hidden");
else notif.setAttribute("hidden","");
};
document.addEventListener("click", (e)=>{
if(!notif) return;
const btn = e.target.closest(".iconbtn");
const inside = e.target.closest("#notifPanel");
if(inside || btn) return;
notif.setAttribute("hidden","");
});
// Settings helpers: perf + alertpulse (no cookies)
function setStorage(key, val, remember){
try{
if (remember) {
localStorage.setItem(key, val);
sessionStorage.removeItem(key);
} else {
sessionStorage.setItem(key, val);
localStorage.removeItem(key);
}
}catch(e){}
}
function applyDataset(key, val){
document.documentElement.dataset[key] = val;
if (key === "perf") {
const label = document.getElementById("perfLabel");
if(label) label.textContent = val.toUpperCase();
}
}
window.applyPerfFromUI = function(){
const chosen = document.querySelector(".seg-btn.is-selected[data-perf]")?.dataset.perf;
const remember = document.getElementById("rememberPerf")?.checked;
const val = chosen || "auto";
setStorage("perf", val, remember);
applyDataset("perf", val);
toast("success","Performance", `Profil: ${val.toUpperCase()}`, 2200);
if (val === "low") toast("info","Hint","Low deaktiviert Starfield", 1800);
};
window.applyPulseFromUI = function(){
const chosen = document.querySelector(".seg-btn.is-selected[data-alertpulse]")?.dataset.alertpulse;
const remember = document.getElementById("rememberPulse")?.checked;
const val = chosen || "burst";
setStorage("alertpulse", val, remember);
applyDataset("alertpulse", val);
toast("success","Alerts", `Pulse: ${val.toUpperCase()}`, 2200);
};
// Segment button selection behavior
document.addEventListener("click", (e)=>{
const b = e.target.closest(".seg-btn");
if(!b) return;
const parent = b.closest(".seg");
if(parent){
parent.querySelectorAll(".seg-btn").forEach(x=>x.classList.remove("is-selected"));
b.classList.add("is-selected");
}
});
// Pre-select in settings pages
document.addEventListener("DOMContentLoaded", ()=>{
const perf = document.documentElement.dataset.perf || "auto";
document.querySelectorAll('.seg-btn[data-perf]').forEach(b=>{
if(b.dataset.perf === perf) b.classList.add("is-selected");
});
const pulse = document.documentElement.dataset.alertpulse || "burst";
document.querySelectorAll('.seg-btn[data-alertpulse]').forEach(b=>{
if(b.dataset.alertpulse === pulse) b.classList.add("is-selected");
});
});
const authView = document.getElementById("authView");
const gameView = document.getElementById("gameView");
let stateTimer = null;
const authPanels = {
login: document.getElementById("authLogin"),
"register-step1": document.getElementById("authRegisterStep1"),
"register-step2": document.getElementById("authRegisterStep2"),
"register-step3": document.getElementById("authRegisterStep3")
};
let racesCache = null;
let avatarsCache = null;
const regDraft = { race_key: null, avatar_key: null };
function formatNumber(val){
const num = Number.isFinite(val) ? val : 0;
return Math.round(num).toLocaleString("de-DE");
}
function updateResourceBar(state){
const values = state?.resources || {};
document.querySelectorAll("[data-resource-value]").forEach((valueEl)=>{
const key = valueEl.dataset.resourceValue;
if(!key) return;
const value = values[key];
if(typeof value !== "number") return;
const dot = valueEl.querySelector(".dot");
const display = key === "energy" ? Math.round(value) : Math.floor(value);
const prefix = key === "energy" && display >= 0 ? "+" : "";
valueEl.textContent = "";
if(dot) valueEl.appendChild(dot);
valueEl.appendChild(document.createTextNode(` ${prefix}${formatNumber(display)}`));
});
}
function updateQueuePanel(state){
const slotsEl = document.getElementById("queueSlots");
if(slotsEl){
const slots = Number.isFinite(state?.queue_slots) ? state.queue_slots : null;
slotsEl.textContent = slots === null ? "" : String(slots);
}
const list = document.getElementById("queueList");
if(!list) return;
list.textContent = "";
const jobs = Array.isArray(state?.active_build_jobs) ? state.active_build_jobs : [];
if(jobs.length === 0){
const li = document.createElement("li");
li.className = "muted";
li.textContent = "Keine aktiven Baujobs.";
list.appendChild(li);
return;
}
jobs.forEach((job)=>{
const li = document.createElement("li");
const slotIndex = Number.isFinite(Number(job?.slot_index)) ? Number(job.slot_index) + 1 : null;
const slotLabel = slotIndex ? `Slot ${slotIndex}: ` : "";
const key = job?.building_key ? String(job.building_key) : "Unbekannt";
const finish = job?.finish_at ? ` · fertig ${job.finish_at}` : "";
li.textContent = `${slotLabel}${key}${finish}`;
list.appendChild(li);
});
}
function showAuthView(){
if(authView) authView.removeAttribute("hidden");
if(gameView) gameView.setAttribute("hidden", "");
}
function showGameView(){
if(authView) authView.setAttribute("hidden", "");
if(gameView) gameView.removeAttribute("hidden");
}
function showAuthPanel(key){
Object.values(authPanels).forEach((panel)=>{
if(panel) panel.setAttribute("hidden", "");
});
const panel = authPanels[key];
if(panel) panel.removeAttribute("hidden");
}
function setMessage(el, message){
if(!el) return;
el.textContent = message || "";
}
async function fetchJson(url, options){
try{
const res = await fetch(url, options);
const data = await res.json().catch(()=> ({}));
return { ok: res.ok, status: res.status, data };
}catch(e){
return { ok: false, status: 0, data: {} };
}
}
async function loadRaces(){
if(racesCache) return racesCache;
const result = await fetchJson("/api/meta/races");
if(result.ok){
racesCache = result.data.races || [];
}else{
racesCache = [];
}
return racesCache;
}
async function loadAvatars(){
if(avatarsCache) return avatarsCache;
const result = await fetchJson("/api/meta/avatars");
if(result.ok){
avatarsCache = result.data.avatars || [];
}else{
avatarsCache = [];
}
return avatarsCache;
}
function renderRaces(races){
const list = document.getElementById("raceList");
if(!list) return;
list.textContent = "";
if(!Array.isArray(races) || races.length === 0){
const empty = document.createElement("div");
empty.className = "muted";
empty.textContent = "Keine Rassen verfügbar.";
list.appendChild(empty);
return;
}
races.forEach((race)=>{
const btn = document.createElement("button");
btn.type = "button";
btn.className = "auth-choice";
btn.dataset.raceKey = race.key;
if(regDraft.race_key === race.key) btn.classList.add("is-selected");
const summary = Array.isArray(race.modifier_summary) ? race.modifier_summary : [];
btn.innerHTML = `
<div class="auth-choice-title">${escapeHtml(race.name || race.key)}</div>
<div class="auth-choice-meta">${escapeHtml(race.description || "")}</div>
${summary.length ? `<div class="auth-choice-meta">${summary.map(s=>escapeHtml(s)).join("<br>")}</div>` : ""}
`;
btn.addEventListener("click", ()=>{
regDraft.race_key = race.key;
list.querySelectorAll(".auth-choice").forEach(el=>el.classList.remove("is-selected"));
btn.classList.add("is-selected");
});
list.appendChild(btn);
});
}
function renderAvatars(avatars){
const list = document.getElementById("avatarList");
if(!list) return;
list.textContent = "";
if(!Array.isArray(avatars) || avatars.length === 0){
const empty = document.createElement("div");
empty.className = "muted";
empty.textContent = "Keine Avatare verfügbar.";
list.appendChild(empty);
return;
}
avatars.forEach((avatar)=>{
const btn = document.createElement("button");
btn.type = "button";
btn.className = "auth-choice";
btn.dataset.avatarKey = avatar.key;
if(regDraft.avatar_key === avatar.key) btn.classList.add("is-selected");
btn.innerHTML = `
<img class="auth-avatar" src="${escapeHtml(avatar.image || "")}" alt="${escapeHtml(avatar.label || avatar.key)}">
<div class="auth-choice-title">${escapeHtml(avatar.label || avatar.key)}</div>
`;
btn.addEventListener("click", ()=>{
regDraft.avatar_key = avatar.key;
list.querySelectorAll(".auth-choice").forEach(el=>el.classList.remove("is-selected"));
btn.classList.add("is-selected");
});
list.appendChild(btn);
});
}
function startPolling(){
if(stateTimer) return;
stateTimer = setInterval(refreshState, 15000);
}
function stopPolling(){
if(!stateTimer) return;
clearInterval(stateTimer);
stateTimer = null;
}
async function fetchState(){
try{
const res = await fetch("/api/state", { credentials: "same-origin" });
if(res.status === 401) return { status: 401 };
if(!res.ok) return { status: res.status };
const data = await res.json();
return { status: 200, data };
}catch(e){
return { status: 0 };
}
}
async function refreshState(){
const result = await fetchState();
if(result.status === 200){
showGameView();
updateResourceBar(result.data);
updateQueuePanel(result.data);
ensureBuildButton();
startPolling();
return;
}
if(result.status === 401){
showAuthView();
showAuthPanel("login");
stopPolling();
}
}
function ensureBuildButton(){
const content = document.getElementById("content");
if(!content || document.getElementById("buildOreMine")) return;
const wrap = document.createElement("div");
wrap.className = "actions";
const btn = document.createElement("button");
btn.className = "btn btn-primary";
btn.id = "buildOreMine";
btn.type = "button";
btn.textContent = "Erzmine bauen";
btn.addEventListener("click", async ()=>{
try{
const res = await fetch("/api/build/start", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({building_key:"ore_mine", amount:1})
});
const data = await res.json();
if(res.ok){
toast("success","Bau gestartet","Erzmine in Queue gelegt");
updateResourceBar({resources: data.resources});
}else{
toast("error","Bau fehlgeschlagen", data.message || "Aktion nicht möglich");
}
}catch(e){
toast("error","Netzwerk","API nicht erreichbar");
}
});
wrap.appendChild(btn);
content.appendChild(wrap);
}
document.addEventListener("DOMContentLoaded", ()=>{
document.querySelectorAll("[data-auth-switch]").forEach((btn)=>{
btn.addEventListener("click", async ()=>{
const target = btn.dataset.authSwitch;
if(!target) return;
showAuthView();
showAuthPanel(target);
if(target === "register-step1"){
const races = await loadRaces();
renderRaces(races);
}
if(target === "register-step2"){
const avatars = await loadAvatars();
renderAvatars(avatars);
}
});
});
const loginForm = document.getElementById("loginForm");
if(loginForm){
loginForm.addEventListener("submit", async (e)=>{
e.preventDefault();
const identifier = document.getElementById("loginIdentifier")?.value?.trim() || "";
const password = document.getElementById("loginPassword")?.value || "";
const message = document.getElementById("loginMessage");
setMessage(message, "");
const result = await fetchJson("/api/auth/login", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ username_or_email: identifier, password })
});
if(result.ok){
showGameView();
refreshState();
}else{
setMessage(message, result.data?.message || "Login fehlgeschlagen.");
}
});
}
const raceNext = document.getElementById("raceNext");
if(raceNext){
raceNext.addEventListener("click", async ()=>{
const message = document.getElementById("raceMessage");
setMessage(message, "");
if(!regDraft.race_key){
setMessage(message, "Bitte eine Rasse wählen.");
return;
}
const result = await fetchJson("/api/auth/register/step1", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ race_key: regDraft.race_key })
});
if(result.ok){
showAuthPanel("register-step2");
const avatars = await loadAvatars();
renderAvatars(avatars);
}else{
setMessage(message, result.data?.message || "Schritt 1 fehlgeschlagen.");
}
});
}
const avatarNext = document.getElementById("avatarNext");
if(avatarNext){
avatarNext.addEventListener("click", async ()=>{
const title = document.getElementById("regTitle")?.value?.trim() || "";
const message = document.getElementById("avatarMessage");
setMessage(message, "");
if(!regDraft.avatar_key){
setMessage(message, "Bitte einen Avatar wählen.");
return;
}
if(title.length < 2){
setMessage(message, "Titel ist zu kurz.");
return;
}
const result = await fetchJson("/api/auth/register/step2", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ avatar_key: regDraft.avatar_key, title })
});
if(result.ok){
showAuthPanel("register-step3");
}else{
setMessage(message, result.data?.message || "Schritt 2 fehlgeschlagen.");
}
});
}
const registerForm = document.getElementById("registerForm");
if(registerForm){
registerForm.addEventListener("submit", async (e)=>{
e.preventDefault();
const username = document.getElementById("regUsername")?.value?.trim() || "";
const email = document.getElementById("regEmail")?.value?.trim() || "";
const password = document.getElementById("regPassword")?.value || "";
const message = document.getElementById("registerMessage");
setMessage(message, "");
const result = await fetchJson("/api/auth/register/step3", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ username, email, password })
});
if(result.ok){
showGameView();
refreshState();
}else{
setMessage(message, result.data?.message || "Registrierung fehlgeschlagen.");
}
});
}
const logoutBtn = document.getElementById("logoutBtn");
if(logoutBtn){
logoutBtn.addEventListener("click", async ()=>{
await fetchJson("/api/auth/logout", { method: "POST" });
stopPolling();
showAuthView();
showAuthPanel("login");
});
}
showAuthPanel("login");
refreshState();
});
})();