146 lines
4.4 KiB
JavaScript
146 lines
4.4 KiB
JavaScript
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 = '<span class="ico"></span><span class="txt"><span class="name"></span><span class="url"></span></span>';
|
||
|
|
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();
|