Capture, decrypt, and restream toolkit with compiled-in app modules (RTE, TG4, BBC), on-device MITM proxy, streamd control plane, and www. BBC module.yaml is published (clear streams); other module values stay local.
192 lines
6.3 KiB
JavaScript
192 lines
6.3 KiB
JavaScript
(() => {
|
|
const OWNER = "nulldrm";
|
|
const REPO = "Null-DRM-Official";
|
|
const API = "/git-api/api/v1/repos/" + OWNER + "/" + REPO;
|
|
const FORGEJO = "https://git.nulldrm.com/" + OWNER + "/" + REPO;
|
|
const TG_URL = "https://t.me/+GZwQ7d_HD2w2ZTM8";
|
|
|
|
function setText(id, value) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.textContent = value;
|
|
}
|
|
|
|
function shortDate(iso) {
|
|
if (!iso) return "—";
|
|
const d = new Date(iso);
|
|
if (Number.isNaN(d.getTime())) return "—";
|
|
return d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return String(s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
async function loadRepo() {
|
|
if (!document.getElementById("git-stats")) return;
|
|
try {
|
|
const res = await fetch(API);
|
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
const data = await res.json();
|
|
setText("stat-stars", String(data.stars_count ?? data.stars ?? 0));
|
|
setText("stat-forks", String(data.forks_count ?? data.forks ?? 0));
|
|
setText("stat-issues", String(data.open_issues_count ?? data.open_issues ?? 0));
|
|
setText("stat-updated", shortDate(data.updated_at || data.pushed_at));
|
|
} catch {
|
|
["stat-stars", "stat-forks", "stat-issues", "stat-updated"].forEach((id) => setText(id, "—"));
|
|
}
|
|
}
|
|
|
|
async function loadReleases() {
|
|
const status = document.getElementById("releases-status");
|
|
const list = document.getElementById("releases-list");
|
|
if (!status || !list) return;
|
|
|
|
try {
|
|
const res = await fetch(API + "/releases?limit=5");
|
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
const releases = await res.json();
|
|
if (!Array.isArray(releases) || releases.length === 0) {
|
|
status.textContent = "No releases published yet — clone the repo or watch it on Forgejo.";
|
|
status.hidden = false;
|
|
list.hidden = true;
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = releases.map((r) => {
|
|
const title = escapeHtml(r.name || r.tag_name || "Release");
|
|
const tag = escapeHtml(r.tag_name || "");
|
|
const when = shortDate(r.published_at || r.created_at);
|
|
const url = escapeHtml(r.html_url || (FORGEJO + "/releases/tag/" + encodeURIComponent(r.tag_name || "")));
|
|
const body = (r.body || "").trim();
|
|
const preview = body
|
|
? '<p class="release-body">' + escapeHtml(body.slice(0, 180)) + (body.length > 180 ? "…" : "") + "</p>"
|
|
: "";
|
|
const assets = Array.isArray(r.assets) ? r.assets : [];
|
|
const assetLinks = assets.slice(0, 4).map((a) => {
|
|
const href = escapeHtml(a.browser_download_url || a.html_url || "#");
|
|
const name = escapeHtml(a.name || "asset");
|
|
return '<a href="' + href + '">' + name + "</a>";
|
|
}).join(" · ");
|
|
return (
|
|
"<li>" +
|
|
'<p class="release-title"><a href="' + url + '">' + title + "</a></p>" +
|
|
'<p class="release-meta">' + tag + (tag ? " · " : "") + when +
|
|
(assetLinks ? " · " + assetLinks : "") + "</p>" +
|
|
preview +
|
|
"</li>"
|
|
);
|
|
}).join("");
|
|
|
|
status.hidden = true;
|
|
list.hidden = false;
|
|
} catch {
|
|
status.innerHTML =
|
|
'Could not load releases. Open <a href="' + FORGEJO + '/releases" rel="noopener">Forgejo releases</a> directly.';
|
|
status.hidden = false;
|
|
list.hidden = true;
|
|
}
|
|
}
|
|
|
|
// Telegram invite: shown once per browser.
|
|
function initTelegramInvite() {
|
|
const KEY = "nulldrm_tg_seen";
|
|
const modal = document.getElementById("tg-modal");
|
|
if (!modal) return;
|
|
|
|
let seen = false;
|
|
try {
|
|
seen = localStorage.getItem(KEY) === "1";
|
|
} catch {
|
|
seen = true; // private mode: do not nag
|
|
}
|
|
|
|
const markSeen = () => {
|
|
try {
|
|
localStorage.setItem(KEY, "1");
|
|
} catch {
|
|
/* private mode */
|
|
}
|
|
};
|
|
|
|
// Belt and braces: the hidden attribute plus an inline display, so the
|
|
// overlay cannot survive a stylesheet that sets display on .tg-modal.
|
|
const close = () => {
|
|
markSeen();
|
|
modal.hidden = true;
|
|
modal.style.display = "none";
|
|
document.body.style.overflow = "";
|
|
};
|
|
|
|
const open = () => {
|
|
modal.hidden = false;
|
|
modal.style.display = "grid";
|
|
document.body.style.overflow = "hidden";
|
|
const join = document.getElementById("tg-join");
|
|
if (join) join.focus();
|
|
};
|
|
|
|
// Wire every dismiss control directly rather than relying on delegation.
|
|
modal.querySelectorAll("[data-tg-dismiss]").forEach((el) => {
|
|
el.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
close();
|
|
});
|
|
});
|
|
|
|
// Clicking anywhere outside the dialog also closes it.
|
|
modal.addEventListener("click", (e) => {
|
|
const t = e.target;
|
|
if (t instanceof Element && !t.closest(".tg-dialog")) close();
|
|
});
|
|
|
|
const join = document.getElementById("tg-join");
|
|
if (join) join.addEventListener("click", close);
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape" && !modal.hidden) close();
|
|
});
|
|
|
|
if (seen) {
|
|
modal.hidden = true;
|
|
modal.style.display = "none";
|
|
} else {
|
|
open();
|
|
}
|
|
}
|
|
|
|
// Highlight the section currently in view.
|
|
function initNavHighlight() {
|
|
const links = Array.from(document.querySelectorAll('.site-nav a[href^="#"]'));
|
|
if (!links.length || !("IntersectionObserver" in window)) return;
|
|
const byId = new Map(links.map((a) => [a.getAttribute("href").slice(1), a]));
|
|
const sections = Array.from(byId.keys())
|
|
.map((id) => document.getElementById(id))
|
|
.filter(Boolean);
|
|
if (!sections.length) return;
|
|
|
|
const obs = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
const link = byId.get(entry.target.id);
|
|
if (link) link.classList.toggle("active", entry.isIntersecting);
|
|
});
|
|
},
|
|
{ rootMargin: "-45% 0px -50% 0px" }
|
|
);
|
|
sections.forEach((s) => obs.observe(s));
|
|
}
|
|
|
|
loadRepo();
|
|
loadReleases();
|
|
initTelegramInvite();
|
|
initNavHighlight();
|
|
|
|
// Any Telegram link left without an href falls back to the canonical invite.
|
|
document.querySelectorAll('a[href^="https://t.me/"]').forEach((a) => {
|
|
if (!a.getAttribute("href")) a.setAttribute("href", TG_URL);
|
|
});
|
|
})();
|