const $ = (id) => document.getElementById(id); let links = []; let deleted = []; let editing = false; let editId = null; let syncOn = false; let syncState = ""; // "", "busy", "ok", "err" const uid = () => Math.random().toString(36).slice(2, 10); function normalizeUrl(raw) { raw = raw.trim(); if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) raw = "http://" + raw; try { return new URL(raw).href; } catch { return null; } } async function load() { const s = await LabSync.readLocal(); links = s.links; deleted = s.deleted; // Einträge aus Version 1.0 haben noch keinen Zeitstempel if (links.some((l) => !l.updated)) { links = links.map((l) => ({ ...l, updated: l.updated || Date.now() })); await save(false); } render(); syncOn = !!(await LabSync.getConfig())?.url; render(); if (syncOn) runSync(); } async function save(sync = true) { await LabSync.writeLocal({ links, deleted, modified: Date.now() }); if (sync && syncOn) runSync(); } async function runSync() { setSync("busy"); try { await LabSync.sync(); const s = await LabSync.readLocal(); links = s.links; deleted = s.deleted; setSync("ok"); } catch (e) { setSync("err", e.message); } render(); } function setSync(state, msg = "") { syncState = state; $("syncBtn").title = msg || "Mit WebDAV synchronisieren"; } function render() { const q = $("search").value.trim().toLowerCase(); const list = $("list"); list.textContent = ""; const shown = links.filter((l) => !q || (l.name + " " + l.url).toLowerCase().includes(q)); for (const l of shown) { const li = document.createElement("li"); const a = document.createElement("a"); a.href = l.url; a.target = "_blank"; a.rel = "noopener"; a.innerHTML = ''; a.querySelector(".ico").textContent = l.icon || "▣"; a.querySelector(".name").textContent = l.name; a.querySelector(".url").textContent = l.url.replace(/^https?:\/\//, "").replace(/\/$/, ""); if (editing) a.addEventListener("click", (e) => e.preventDefault()); li.append(a); const eb = document.createElement("div"); eb.className = "edit-btns"; eb.append( btn("[✎]", () => openForm(l)), btn("[↑]", () => move(l.id, -1)), btn("[↓]", () => move(l.id, 1)), Object.assign(btn("[✕]", () => remove(l.id)), { className: "del" }) ); li.append(eb); list.append(li); } $("empty").hidden = links.length > 0 || !!editId; $("count").textContent = links.length ? `${links.length} Einträge` : ""; document.body.classList.toggle("editing", editing); for (const id of ["add", "export", "import"]) $(id).hidden = !editing; $("editToggle").textContent = editing ? "[FERTIG]" : "[EDIT]"; $("syncBtn").hidden = !syncOn; $("syncBtn").className = syncState; } function btn(label, fn) { const b = document.createElement("button"); b.textContent = label; b.addEventListener("click", fn); return b; } async function move(id, dir) { const i = links.findIndex((l) => l.id === id); const j = i + dir; if (j < 0 || j >= links.length) return; [links[i], links[j]] = [links[j], links[i]]; await save(); render(); } async function remove(id) { links = links.filter((l) => l.id !== id); deleted = [...deleted.filter((t) => t.id !== id), { id, updated: Date.now() }]; await save(); render(); } function openForm(link) { editId = link ? link.id : "new"; $("fIcon").value = link?.icon || ""; $("fName").value = link?.name || ""; $("fUrl").value = link?.url || ""; $("form").hidden = false; $("fName").focus(); } function closeForm() { editId = null; $("form").hidden = true; render(); } $("form").addEventListener("submit", async (e) => { e.preventDefault(); const url = normalizeUrl($("fUrl").value); if (!url) { $("fUrl").focus(); return; } const entry = { name: $("fName").value.trim(), url, icon: $("fIcon").value.trim(), updated: Date.now() }; if (editId === "new") links.push({ id: uid(), ...entry }); else Object.assign(links.find((l) => l.id === editId), entry); await save(); closeForm(); }); $("cancel").addEventListener("click", closeForm); $("add").addEventListener("click", () => openForm()); $("search").addEventListener("input", render); $("editToggle").addEventListener("click", () => { editing = !editing; if (!editing) { editId = null; $("form").hidden = true; } render(); }); $("syncBtn").addEventListener("click", runSync); $("settings").addEventListener("click", () => chrome.runtime.openOptionsPage()); // Enter im Suchfeld öffnet den ersten Treffer $("search").addEventListener("keydown", (e) => { if (e.key === "Enter" && !editing) $("list").querySelector("a")?.click(); }); $("export").addEventListener("click", () => { const blob = new Blob([JSON.stringify(links, null, 2)], { type: "application/json" }); const a = Object.assign(document.createElement("a"), { href: URL.createObjectURL(blob), download: "lablinks.json" }); a.click(); URL.revokeObjectURL(a.href); }); $("import").addEventListener("click", () => $("file").click()); $("file").addEventListener("change", async (e) => { const file = e.target.files[0]; if (!file) return; try { const arr = JSON.parse(await file.text()); const now = Date.now(); const imported = arr .filter((x) => x && x.name && x.url) .map((x) => ({ id: uid(), name: String(x.name), url: normalizeUrl(String(x.url)) || String(x.url), icon: x.icon || "", updated: now })); deleted = [...deleted, ...links.map((l) => ({ id: l.id, updated: now }))]; links = imported; await save(); render(); } catch { alert("Ungültige JSON-Datei."); } e.target.value = ""; }); load(); $("search").focus();