Initial commit: Null DRM Official
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.
This commit is contained in:
commit
2fa8f2435f
121 changed files with 17802 additions and 0 deletions
192
www/script.js
Normal file
192
www/script.js
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
(() => {
|
||||
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);
|
||||
});
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue