From fc9e0e6ecfe5d58b840c5e10534fef9c8c666c5f Mon Sep 17 00:00:00 2001 From: root Date: Sun, 11 Oct 2026 17:54:59 +0000 Subject: [PATCH] LabLinks Chrome-Erweiterung im Retro-Look Co-Authored-By: Claude Sonnet 5.5 --- icons/128.png | Bin 0 -> 387 bytes icons/16.png | Bin 0 -> 110 bytes icons/48.png | Bin 0 -> 150 bytes manifest.json | 13 +++++ popup.css | 107 +++++++++++++++++++++++++++++++++++++ popup.html | 44 +++++++++++++++ popup.js | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 309 insertions(+) create mode 100644 icons/128.png create mode 100644 icons/16.png create mode 100644 icons/48.png create mode 100644 manifest.json create mode 100644 popup.css create mode 100644 popup.html create mode 100644 popup.js diff --git a/icons/128.png b/icons/128.png new file mode 100644 index 0000000000000000000000000000000000000000..66f8d886e87b2bebd459775eab8cbac553333b28 GIT binary patch literal 387 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^VD$2IaSW-L^Y)Tqms6mKi(@gn zTA}f~sLU#Lqiz@!xm "; } +.ico { font-size: 18px; width: 22px; text-align: center; flex: none; } +.txt { min-width: 0; display: flex; flex-direction: column; } +.name { font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.url { font-size: 11px; color: var(--fg-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +.edit-btns { display: none; flex-direction: column; } +body.editing .edit-btns { display: flex; } +.edit-btns button { padding: 0 4px; font-size: 11px; } +.edit-btns .del { color: #ff4b3e; } + +#empty { color: var(--fg-dim); margin: 6px 0 10px; } + +#form { border-top: 2px dashed var(--line); padding-top: 10px; margin-top: 4px; } +.row { display: flex; gap: 6px; } +.row input:first-child:not(:only-child) { width: 52px; text-align: center; flex: none; } +.row button { margin-bottom: 6px; } + +footer { display: flex; gap: 4px; align-items: center; margin-top: 4px; min-height: 22px; } +#count { margin-left: auto; font-size: 11px; color: var(--fg-dim); } +[hidden] { display: none !important; } diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..cdb4a86 --- /dev/null +++ b/popup.html @@ -0,0 +1,44 @@ + + + + + LabLinks + + + +
+
+

LABLINKS

+
+ +
+
+ + + +
    + + + + +
    + + + + + +
    +
    + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..c80baf1 --- /dev/null +++ b/popup.js @@ -0,0 +1,145 @@ +const $ = (id) => document.getElementById(id); +const store = chrome.storage.sync; + +let links = []; +let editing = false; +let editId = null; + +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 data = await store.get("links"); + links = data.links || []; + render(); +} +const save = () => store.set({ links }); + +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]"; +} + +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); + 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() }; + 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(); +}); + +// 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()); + links = 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 || "" })); + await save(); + render(); + } catch { alert("Ungültige JSON-Datei."); } + e.target.value = ""; +}); + +load(); +$("search").focus();