This repository has been archived on 2026-10-28. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
karldns.de/karldns/web/scripts.py
T

244 lines
9.0 KiB
Python
Raw Normal View History

2026-05-20 23:55:27 +02:00
from __future__ import annotations
2026-05-21 10:05:40 +02:00
THEME_JS = """
2026-05-20 23:55:27 +02:00
(() => {
const media = window.matchMedia("(prefers-color-scheme: dark)");
const storageKey = "karldns-theme";
const systemTheme = () => (media.matches ? "dark" : "light");
const storedTheme = () => {
try {
return window.localStorage.getItem(storageKey);
} catch (_) {
return null;
}
};
const setStoredTheme = (theme) => {
try {
window.localStorage.setItem(storageKey, theme);
} catch (_) {}
};
const syncToggle = (theme) => {
document.querySelectorAll("[data-theme-toggle]").forEach((button) => {
2026-05-21 10:05:40 +02:00
const nextTheme = theme === "dark" ? "light" : "dark";
button.dataset.themeNext = nextTheme;
button.setAttribute("aria-label", `Switch to ${nextTheme.toLowerCase()} theme`);
2026-05-20 23:55:27 +02:00
});
};
const applyTheme = (requestedTheme = storedTheme() || systemTheme()) => {
const theme = requestedTheme === "dark" ? "dark" : "light";
document.documentElement.setAttribute("data-bs-theme", theme);
syncToggle(theme);
return theme;
};
applyTheme();
media.addEventListener("change", () => {
if (!storedTheme()) applyTheme();
});
document.addEventListener("DOMContentLoaded", () => syncToggle(document.documentElement.getAttribute("data-bs-theme") || "light"));
document.addEventListener("click", (event) => {
const toggle = event.target.closest("[data-theme-toggle]");
if (!toggle) return;
const currentTheme = document.documentElement.getAttribute("data-bs-theme") || systemTheme();
const nextTheme = currentTheme === "dark" ? "light" : "dark";
setStoredTheme(nextTheme);
applyTheme(nextTheme);
});
})();
2026-05-21 10:05:40 +02:00
""".strip()
2026-05-20 23:55:27 +02:00
2026-05-21 10:05:40 +02:00
CLIPBOARD_JS = """
2026-05-20 23:55:27 +02:00
document.addEventListener("click", async (event) => {
2026-05-21 14:18:39 +02:00
document.querySelectorAll("details.copy-menu[open]").forEach((menu) => {
if (!menu.contains(event.target)) menu.removeAttribute("open");
});
2026-05-20 23:55:27 +02:00
const toggle = event.target.closest("[data-secret-toggle]");
if (toggle) {
const group = toggle.closest(".copy-field");
const field = group ? group.querySelector("[data-secret-input], [data-secret-value]") : null;
if (!field) return;
if (field.matches("[data-secret-input]")) {
const masked = field.type === "password";
field.type = masked ? "text" : "password";
toggle.textContent = masked ? "Hide" : "View";
toggle.setAttribute("aria-label", masked ? "Hide secret value" : "Show secret value");
} else {
const masked = field.dataset.secretMasked !== "false";
field.dataset.secretMasked = masked ? "false" : "true";
field.textContent = masked ? (field.dataset.secretValue || "") : "********";
toggle.textContent = masked ? "Hide" : "View";
toggle.setAttribute("aria-label", masked ? "Hide secret value" : "Show secret value");
}
return;
}
const button = event.target.closest("[data-copy]");
if (!button) return;
try {
await navigator.clipboard.writeText(button.dataset.copy || "");
2026-05-21 14:18:39 +02:00
const label = button.querySelector("[data-copy-label]");
const oldLabel = label ? label.textContent : "";
button.classList.add("is-copied");
if (label) label.textContent = "Copied";
const menu = button.closest("details.copy-menu");
if (menu) {
setTimeout(() => {
menu.removeAttribute("open");
}, 250);
}
2026-05-20 23:55:27 +02:00
setTimeout(() => {
2026-05-21 14:18:39 +02:00
button.classList.remove("is-copied");
if (label) label.textContent = oldLabel;
2026-05-20 23:55:27 +02:00
}, 1200);
} catch (_) {}
});
2026-05-21 14:18:39 +02:00
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return;
document.querySelectorAll("details.copy-menu[open]").forEach((menu) => {
menu.removeAttribute("open");
});
});
2026-05-21 10:05:40 +02:00
""".strip()
2026-05-20 23:55:27 +02:00
2026-05-21 10:05:40 +02:00
ADMIN_TABLES_JS = """
2026-05-20 23:55:27 +02:00
document.querySelectorAll("[data-admin-table]").forEach((tableShell) => {
const table = tableShell.querySelector("table");
const tbody = tableShell.querySelector("tbody");
const search = tableShell.querySelector("[data-table-search]");
const pageSize = tableShell.querySelector("[data-table-page-size]");
const count = tableShell.querySelector("[data-table-count]");
const pagination = tableShell.querySelector("[data-table-pagination]");
if (!table || !tbody || !search || !pageSize || !count || !pagination) return;
const headerCount = table.querySelectorAll("thead th").length || 1;
const originalRows = Array.from(tbody.querySelectorAll("tr"));
const dataRows = originalRows.filter((row) => !row.classList.contains("empty-row"));
let currentPage = 1;
const emptyRow = (message) => {
const row = document.createElement("tr");
row.className = "empty-row";
const cell = document.createElement("td");
cell.className = "empty";
cell.colSpan = headerCount;
cell.textContent = message;
row.append(cell);
return row;
};
const visibleRows = () => {
const query = search.value.trim().toLowerCase();
if (!query) return dataRows;
return dataRows.filter((row) => row.textContent.toLowerCase().includes(query));
};
const renderButton = (label, page, disabled = false, active = false) => {
const item = document.createElement("li");
item.className = `page-item${disabled ? " disabled" : ""}${active ? " active" : ""}`;
const button = document.createElement("button");
button.type = "button";
button.className = "page-link";
button.textContent = label;
button.disabled = disabled;
button.addEventListener("click", () => {
currentPage = page;
render();
});
item.append(button);
return item;
};
const renderPagination = (totalPages) => {
pagination.replaceChildren();
if (totalPages <= 1) return;
pagination.append(renderButton("Previous", Math.max(1, currentPage - 1), currentPage === 1));
for (let page = 1; page <= totalPages; page += 1) {
if (totalPages > 7 && page !== 1 && page !== totalPages && Math.abs(page - currentPage) > 1) {
if (page === 2 || page === totalPages - 1) pagination.append(renderButton("...", currentPage, true));
continue;
}
pagination.append(renderButton(String(page), page, false, page === currentPage));
}
pagination.append(renderButton("Next", Math.min(totalPages, currentPage + 1), currentPage === totalPages));
};
const render = () => {
const rows = visibleRows();
const size = Number.parseInt(pageSize.value, 10) || 25;
const totalPages = Math.max(1, Math.ceil(rows.length / size));
currentPage = Math.min(currentPage, totalPages);
const start = (currentPage - 1) * size;
const pageRows = rows.slice(start, start + size);
tbody.replaceChildren(...(pageRows.length ? pageRows : [emptyRow(dataRows.length ? "No matching rows." : "No rows yet.")]));
const shownStart = rows.length ? start + 1 : 0;
const shownEnd = rows.length ? Math.min(start + size, rows.length) : 0;
count.textContent = `${shownStart}-${shownEnd} of ${rows.length}`;
renderPagination(totalPages);
};
search.addEventListener("input", () => {
currentPage = 1;
render();
});
pageSize.addEventListener("change", () => {
currentPage = 1;
render();
});
render();
});
""".strip()
2026-05-21 18:51:18 +02:00
VISITOR_IP_JS = """
(() => {
const ipv4Placeholder = "203.0.113.10";
const ipv6Placeholder = "2001:db8::10";
const needsIpv4 = () => document.body.textContent.includes(ipv4Placeholder) || document.querySelector(`[data-copy*="${ipv4Placeholder}"]`);
const needsIpv6 = () => document.body.textContent.includes(ipv6Placeholder) || document.querySelector(`[data-copy*="${ipv6Placeholder}"]`);
const fetchIp = async (url) => {
try {
const response = await fetch(url, { cache: "no-store", mode: "cors" });
if (!response.ok) return "";
const payload = await response.json();
return typeof payload.ip === "string" ? payload.ip : "";
} catch (_) {
return "";
}
};
const replaceInTextNodes = (search, replacement) => {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
const nodes = [];
while (walker.nextNode()) {
if (walker.currentNode.nodeValue.includes(search)) nodes.push(walker.currentNode);
}
nodes.forEach((node) => {
node.nodeValue = node.nodeValue.split(search).join(replacement);
});
};
const replaceInCopyValues = (search, replacement) => {
document.querySelectorAll("[data-copy]").forEach((element) => {
const value = element.dataset.copy || "";
if (value.includes(search)) element.dataset.copy = value.split(search).join(replacement);
});
};
const replaceEverywhere = (search, replacement) => {
if (!replacement || replacement === search) return;
replaceInTextNodes(search, replacement);
replaceInCopyValues(search, replacement);
};
document.addEventListener("DOMContentLoaded", async () => {
const tasks = [];
if (needsIpv4()) tasks.push(fetchIp("https://api4.ipify.org?format=json").then((ip) => replaceEverywhere(ipv4Placeholder, ip)));
if (needsIpv6()) tasks.push(fetchIp("https://api6.ipify.org?format=json").then((ip) => replaceEverywhere(ipv6Placeholder, ip)));
await Promise.allSettled(tasks);
});
})();
""".strip()