Files
documentation/assets/app.js
2026-10-11 11:06:58 -07:00

2362 lines
87 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
var THEME_KEY = "vyla-theme";
var ICONS = {
system: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>',
light: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M6.3 17.7l-1.4 1.4M19.1 4.9l-1.4 1.4"/>',
dark: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
midnight: '<path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 1 12 2Z"/>',
nord: '<path d="m2 12 5-5 5 5 5-5 5 5"/><path d="M12 7v14"/>',
dracula: '<path d="M7 3v7l5 3 5-3V3"/><path d="M7 14l5 7 5-7"/>',
forest: '<path d="m12 2 7 12h-4l3 6H6l3-6H5z"/>',
sunset: '<path d="M17 18a5 5 0 0 0-10 0"/><path d="M12 2v4M4.22 10.22l2.83 2.83M1 18h4M19 18h4M16.95 13.05l2.83-2.83M12 13a5 5 0 0 0-5 5h10a5 5 0 0 0-5-5z"/>',
cyberpunk: '<path d="M4 4h16v16H4z"/><path d="M4 9h16M9 4v16"/>',
halloween: '<circle cx="12" cy="12" r="9"/><path d="M9 10h.01M15 10h.01M8 15s1.5 2 4 2 4-2 4-2"/><path d="M12 3v3"/>',
christmas: '<path d="m12 3-5 7h3l-4 6h12l-4-6h3z"/><path d="M12 16v5"/>',
autumn: '<path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/>',
winter: '<path d="M12 2v20M4.93 7l14.14 10M4.93 17 19.07 7"/><path d="m9 4 3 2 3-2M9 20l3-2 3 2"/>',
auto: '<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M16 3v4M8 3v4M3 11h18"/>',
sliders: '<path d="M4 6h10M18 6h2M4 12h4M12 12h8M4 18h12M20 18h0"/><circle cx="16" cy="6" r="2"/><circle cx="10" cy="12" r="2"/><circle cx="18" cy="18" r="2"/>',
star: '<path d="m12 3 2.7 5.6 6.1.9-4.4 4.3 1 6.1L12 17l-5.4 2.9 1-6.1L3.2 9.5l6.1-.9z"/>',
search: '<circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>',
menu: '<path d="M4 6h16M4 12h10M4 18h16"/>',
doc: '<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5M9 13h6M9 17h6"/>',
hash: '<path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18"/>',
copy: '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/>',
check: '<path d="m5 12 5 5 9-10"/>',
chevron: '<path d="m6 9 6 6 6-6"/>',
chevronLeft: '<path d="m15 18-6-6 6-6"/>',
chevronRight: '<path d="m9 18 6-6-6-6"/>'
};
var THEMES_GENERAL = [
{ id: "auto", label: "Auto (seasonal)" },
{ id: "dark", label: "Dark" },
{ id: "light", label: "Light" },
{ id: "midnight", label: "Midnight" },
{ id: "nord", label: "Nord" },
{ id: "dracula", label: "Dracula" },
{ id: "forest", label: "Forest" },
{ id: "sunset", label: "Sunset" },
{ id: "cyberpunk", label: "Cyberpunk" }
];
var THEMES_SEASONAL = [
{ id: "halloween", label: "Halloween" },
{ id: "autumn", label: "Autumn" },
{ id: "winter", label: "Winter" },
{ id: "christmas", label: "Christmas" }
];
var THEME_VIDEOS = {
halloween: "https://www.pexels.com/download/video/5652678/",
autumn: "https://www.pexels.com/download/video/34463521/",
winter: "https://www.pexels.com/download/video/30181650/",
christmas: "https://www.pexels.com/download/video/35282426/",
dark: "https://www.pexels.com/download/video/31270356/",
light: "https://www.pexels.com/download/video/13956986/",
midnight: "https://www.pexels.com/download/video/15860507/",
nord: "https://www.pexels.com/download/video/31468607/",
dracula: "https://www.pexels.com/download/video/8116002/",
forest: "https://www.pexels.com/download/video/11265968/",
sunset: "https://www.pexels.com/download/video/855437/",
cyberpunk: "https://www.pexels.com/download/video/32336464/"
};
var SITE_TREE = [
{
id: "vyla",
title: "Vyla",
path: "/start-here/introduction",
groups: [
{
title: "Start here",
items: [
{ title: "Introduction", file: "/start-here/introduction.md", path: "/start-here/introduction" },
{ title: "Architecture", file: "/start-here/architecture.md", path: "/start-here/architecture" }
]
},
{
title: "Products",
items: [
{ title: "Home", file: "/products/home.md", path: "/products/home" },
{ title: "Vyla Go", file: "/products/go.md", path: "/products/go" },
{ title: "TV", file: "/products/tv.md", path: "/products/tv" },
{ title: "Player", file: "/products/player.md", path: "/products/player" }
]
},
{
title: "Concepts",
items: [
{ title: "Providers", file: "/concepts/providers.md", path: "/concepts/providers" }
]
}
]
},
{
id: "sdk",
title: "SDK",
path: "/sdk",
groups: [
{
title: "Vyla SDK",
items: [
{ title: "SDK", file: "/sdk/vyla-sdk/index.md", path: "/sdk" },
{ title: "SDK Quickstart", file: "/sdk/vyla-sdk/quickstart.md", path: "/sdk/vyla-sdk/quickstart" },
{ title: "Contributing", file: "/sdk/vyla-sdk/contributing.md", path: "/sdk/vyla-sdk/contributing" }
]
}
]
},
{
id: "stream-api",
title: "Stream API",
path: "/stream-api/overview",
groups: [
{
title: "Overview",
items: [
{ title: "Overview", file: "/stream-api/overview/index.md", path: "/stream-api/overview" }
]
},
{
title: "Reference",
items: [
{ title: "Paths", file: "/stream-api/reference/paths.md", path: "/stream-api/reference/paths" },
{ title: "Media Data", file: "/stream-api/reference/media-data.md", path: "/stream-api/reference/media-data" },
{ title: "Operations", file: "/stream-api/reference/operations.md", path: "/stream-api/reference/operations" }
]
},
{
title: "Deployment",
items: [
{ title: "Self Hosting", file: "/stream-api/deployment/self-hosting.md", path: "/stream-api/deployment/self-hosting" }
]
}
]
},
{
id: "misc",
title: "Misc",
path: "/misc/project/credit",
groups: [
{
title: "Project",
items: [
{ title: "Story", file: "/misc/project/story.md", path: "/misc/project/story" },
{ title: "Changelog", file: "/misc/project/changelog.md", path: "/misc/project/changelog" },
{ title: "Credit", file: "/misc/project/credit.md", path: "/misc/project/credit" },
{ title: "DMCA", file: "/misc/project/DMCA.md", path: "/misc/project/DMCA" }
]
}
]
}
];
var searchData = null;
var searchResults = [];
var searchActive = 0;
function icon(name) {
return '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + ICONS[name] + '</svg>';
}
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c];
});
}
function getThemeMode() {
return localStorage.getItem(THEME_KEY) || "auto";
}
function applyTheme() {
var mode = getThemeMode();
var theme = resolveTheme(mode);
document.documentElement.setAttribute("data-theme", theme);
var logo = document.getElementById("brand-logo");
if (logo) logo.src = theme === "light" ? "/assets/images/dark_logo.png" : "/assets/images/logo.png";
applyPreferences();
var targetSrc = THEME_VIDEOS[theme] || THEME_VIDEOS.halloween;
var videosOff = getPref("video", "on") === "off";
document.querySelectorAll(".body-blurred-video, .header-video").forEach(function (v) {
if (videosOff) {
v.pause();
if (v.getAttribute("src")) {
v.removeAttribute("src");
v.load();
}
return;
}
if (v.getAttribute("src") !== targetSrc) {
v.src = targetSrc;
v.load();
v.play().catch(function () { });
}
});
applyThemeCopy(theme);
}
function setTheme(mode) {
localStorage.setItem(THEME_KEY, mode);
applyTheme();
refreshThemeUI();
var menu = document.getElementById("theme-menu");
if (menu) menu.classList.remove("open");
var segMenu = document.getElementById("theme-seg-menu");
if (segMenu) segMenu.classList.remove("open");
}
function toggleThemeMenu(e) {
e.stopPropagation();
var menu = document.getElementById("theme-menu");
if (menu) menu.classList.toggle("open");
}
function refreshThemeUI() {
var mode = getThemeMode();
var btn = document.getElementById("theme-toggle");
if (btn) btn.innerHTML = icon(ICONS[mode] ? mode : "dark");
document.querySelectorAll("#theme-menu button, .st-theme").forEach(function (b) {
b.classList.toggle("on", b.getAttribute("data-mode") === mode);
});
}
function buildThemeControls() {
var btn = document.getElementById("theme-toggle");
var links = document.querySelector("#navbar .links");
if (!btn || !links) return;
var wrap = document.createElement("div");
wrap.className = "theme-wrap";
btn.parentNode.insertBefore(wrap, btn);
wrap.appendChild(btn);
var menu = document.createElement("div");
menu.id = "theme-menu";
var html = '<div class="theme-menu-label">General</div>';
THEMES_GENERAL.forEach(function (t) {
html += '<button type="button" data-mode="' + t.id + '">' + icon(t.id) + '<span>' + esc(t.label) + '</span><i class="check">✓</i></button>';
});
html += '<div class="theme-menu-divider"></div>';
html += '<div class="theme-menu-label">Seasonal</div>';
THEMES_SEASONAL.forEach(function (t) {
html += '<button type="button" data-mode="' + t.id + '">' + icon(t.id) + '<span>' + esc(t.label) + '</span><i class="check">✓</i></button>';
});
menu.innerHTML = html;
wrap.appendChild(menu);
menu.addEventListener("click", function (e) {
var b = e.target.closest("button");
if (b) setTheme(b.getAttribute("data-mode"));
});
refreshThemeUI();
}
function dismissBanner() {
var banner = document.getElementById("banner");
if (banner) banner.setAttribute("data-dismissed", "true");
document.body.classList.add("banner-dismissed");
localStorage.setItem("vyla-banner-dismissed", "true");
var header = document.getElementById("header-container");
if (header) document.documentElement.style.setProperty("--header-height", header.offsetHeight + "px");
}
function toggleSidebar() {
var sidebar = document.getElementById("sidebar");
var nav = document.getElementById("navbar");
var top = Math.max(0, Math.round(nav.getBoundingClientRect().bottom));
sidebar.style.setProperty("--sidebar-top", top + "px");
var open = sidebar.classList.toggle("open");
document.body.classList.toggle("nav-open", open);
}
function cgSwitch(btn) {
var group = btn.closest(".code-group");
var index = btn.getAttribute("data-tab-index");
group.querySelectorAll(".code-group-tab").forEach(function (t) {
t.classList.toggle("active", t === btn);
});
group.querySelectorAll(".code-group-panel").forEach(function (p, i) {
p.classList.toggle("active", String(i) === index);
});
window.dispatchEvent(new Event("resize"));
}
var savedCount = 0;
var PREF_KEYS = {
video: "vyla-video",
motion: "vyla-motion",
deco: "vyla-deco",
text: "vyla-text",
accent: "vyla-accent",
name: "vyla-name",
api: "vyla-api-url"
};
var THEME_COPY = {
base: {
banner: "",
footer: "Built by the community, for the community.",
notFound: "But if you don't change your direction, and if you keep looking, you may end up where you are heading.",
greeting: "",
favicon: "",
confetti: null
},
halloween: {
banner: "🎃 Happy Halloween!",
footer: "Stay spooky, keep streaming.",
notFound: "Something is lurking where this page should be. Let's get you back before the lights go out.",
greeting: "Spooky season is here.",
favicon: "🎃",
confetti: ["🎃", "🦇", "👻", "🕷️"]
},
autumn: {
banner: "🍂 Happy fall!",
footer: "Grab something warm and press play.",
notFound: "This page blew away with the leaves. Let's find you a better path.",
greeting: "Cozy season is here.",
favicon: "🍂",
confetti: ["🍂", "🍁", "🎃"]
},
winter: {
banner: "❄️ Stay warm!",
footer: "Cold outside, perfect for a movie night.",
notFound: "This page got buried in the snow. Let's dig you back out.",
greeting: "Perfect weather for staying in.",
favicon: "❄️",
confetti: ["❄️", "⛄", "✨"]
},
christmas: {
banner: "🎄 Happy holidays!",
footer: "Wishing you a quiet night and a great watchlist.",
notFound: "This page isn't on the nice list or the naughty list. Let's head home.",
greeting: "Happy holidays.",
favicon: "🎄",
confetti: ["🎄", "⭐", "🎁", "❄️"]
}
};
var KONAMI = ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"];
var PROJECTS = ["home", "go", "TV", "vyla-sdk", "stream-api"];
var PROJECT_LABELS = { home: "Vyla Home", go: "Vyla Go", TV: "Vyla TV", "vyla-sdk": "Vyla SDK", "stream-api": "Stream API" };
function seasonalTheme() {
var d = new Date();
var m = d.getMonth();
if (m === 9) return "halloween";
if (m === 10) return "autumn";
if (m === 11) return "christmas";
if (m === 0 || m === 1) return "winter";
if (m === 8 && d.getDate() >= 22) return "autumn";
return "dark";
}
function resolveTheme(mode) {
return mode === "auto" ? seasonalTheme() : mode;
}
function getPref(key, fallback) {
var v = null;
try { v = localStorage.getItem(PREF_KEYS[key]); } catch (e) { }
return v === null ? fallback : v;
}
function setPref(key, value) {
try {
if (value === null || value === "") localStorage.removeItem(PREF_KEYS[key]);
else localStorage.setItem(PREF_KEYS[key], value);
} catch (e) { }
}
function applyAccent(hex) {
var root = document.documentElement;
var props = ["--text", "--btn-primary-bg", "--btn-home-bg", "--code-tab-active", "--btn-primary-text", "--btn-home-text"];
if (!/^#[0-9a-f]{6}$/i.test(hex || "")) {
props.forEach(function (p) { root.style.removeProperty(p); });
return;
}
var r = parseInt(hex.slice(1, 3), 16);
var g = parseInt(hex.slice(3, 5), 16);
var b = parseInt(hex.slice(5, 7), 16);
var on = (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.6 ? "#000000" : "#ffffff";
root.style.setProperty("--text", hex);
root.style.setProperty("--btn-primary-bg", hex);
root.style.setProperty("--btn-home-bg", hex);
root.style.setProperty("--code-tab-active", hex);
root.style.setProperty("--btn-primary-text", on);
root.style.setProperty("--btn-home-text", on);
}
function applyPreferences() {
document.body.classList.toggle("no-video", getPref("video", "on") === "off");
document.body.classList.toggle("no-motion", getPref("motion", "on") === "off");
document.body.classList.toggle("no-deco", getPref("deco", "on") === "off");
var scale = parseInt(getPref("text", "100"), 10);
if (!(scale >= 85 && scale <= 130)) scale = 100;
document.documentElement.style.setProperty("--text-scale", String(scale / 100));
applyAccent(getPref("accent", ""));
}
function applyThemeCopy(theme) {
var copy = THEME_COPY[theme] || THEME_COPY.base;
var fav = document.querySelector('link[rel="icon"]');
if (fav) {
if (copy.favicon) {
fav.type = "image/svg+xml";
fav.href = "data:image/svg+xml," + encodeURIComponent("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>" + copy.favicon + "</text></svg>");
} else {
fav.removeAttribute("type");
fav.href = "/favicon.ico";
}
}
var note = document.getElementById("footer-note");
if (note) note.textContent = copy.footer;
var span = document.querySelector("#banner span");
if (span) {
var old = document.getElementById("banner-greet");
if (old) old.remove();
if (copy.banner) {
var g = document.createElement("strong");
g.id = "banner-greet";
g.textContent = copy.banner + " ";
span.insertBefore(g, span.firstChild);
}
}
}
function notFoundCopy() {
var copy = THEME_COPY[resolveTheme(getThemeMode())] || THEME_COPY.base;
return copy.notFound;
}
function cleanApiUrl(v) {
v = String(v || "").trim().replace(/\/+$/, "");
return /^https?:\/\/[^\s<>"'`]+$/i.test(v) ? v : "";
}
var API_DEFAULT = "http://localhost:7860";
function personalize(md) {
var url = cleanApiUrl(getPref("api", "")) || API_DEFAULT;
return md.split("YOUR_API_URL").join(url);
}
function insertApiCard() {
var main = document.getElementById("main-content");
var h1 = main && main.querySelector("h1");
if (!h1) return;
var card = document.createElement("div");
card.className = "personal-card";
card.innerHTML =
'<label for="api-card-input">Using your own server?</label>' +
'<div class="pc-row"><input id="api-card-input" type="url" placeholder="https://your-server.example.com" autocomplete="off" spellcheck="false">' +
'<button type="button" class="pc-btn">Apply</button></div>' +
'<p class="pc-hint">Examples on this page will use your URL. It stays in this browser.</p>';
h1.parentNode.insertBefore(card, h1.nextSibling);
var input = card.querySelector("input");
input.value = cleanApiUrl(getPref("api", ""));
function apply() {
var raw = input.value.trim();
var clean = cleanApiUrl(raw);
if (raw && !clean) {
input.classList.add("bad");
return;
}
input.classList.remove("bad");
setPref("api", clean);
loadPage();
}
card.querySelector(".pc-btn").addEventListener("click", apply);
input.addEventListener("keydown", function (e) {
if (e.key === "Enter") apply();
});
}
function rememberPage(route) {
if (!route || route.is404) return;
try {
localStorage.setItem("vyla-last", JSON.stringify({ title: route.item.title, path: route.item.path }));
} catch (e) { }
}
function timeGreeting() {
var h = new Date().getHours();
if (h < 5) return "Up late";
if (h < 12) return "Good morning";
if (h < 18) return "Good afternoon";
return "Good evening";
}
function getBookmarks() {
try { return JSON.parse(localStorage.getItem("vyla-bookmarks")) || []; } catch (e) { return []; }
}
function isBookmarked(href) {
return getBookmarks().some(function (b) { return b.href === href; });
}
function buildBookmarkButton(route) {
var b = document.createElement("button");
b.type = "button";
b.className = "bm-btn";
var href = route.item.path;
function paint() {
var on = isBookmarked(href);
b.classList.toggle("on", on);
b.setAttribute("aria-pressed", on ? "true" : "false");
b.setAttribute("aria-label", on ? "Remove bookmark" : "Save this page");
b.title = on ? "Remove bookmark" : "Save this page";
b.innerHTML = icon("star");
}
b.addEventListener("click", function (e) {
e.stopPropagation();
var list = getBookmarks();
var idx = list.findIndex(function (x) { return x.href === href; });
if (idx !== -1) list.splice(idx, 1);
else list.unshift({ title: route.item.title, crumbs: [route.tab.title, route.group.title, route.item.title], href: href, type: "page" });
try { localStorage.setItem("vyla-bookmarks", JSON.stringify(list.slice(0, 20))); } catch (err) { }
paint();
});
paint();
return b;
}
function detectPlatform() {
var ua = navigator.userAgent || "";
if (/Android.*(TV|AFT|BRAVIA|SHIELD)|AndroidTV|GoogleTV|SmartTV/i.test(ua)) return "tv";
if (/Android/i.test(ua)) return "android";
if (/Windows/i.test(ua)) return "windows";
if (/Mac OS X|Macintosh/i.test(ua)) return "mac";
if (/Linux|X11/i.test(ua)) return "linux";
return "other";
}
function markRecommended(project) {
var platform = detectPlatform();
var wanted = null;
if (project === "home" && platform === "windows") wanted = [".exe"];
else if (project === "home" && platform === "mac") wanted = [".dmg"];
else if (project === "home" && platform === "linux") wanted = ["build from source"];
else if (project === "go" && platform === "android") wanted = [".apk"];
else if (project === "TV" && platform === "tv") wanted = [".apk"];
if (!wanted) return;
var done = false;
document.querySelectorAll("#main-content table a").forEach(function (a) {
if (done || a.getAttribute("data-rec")) return;
var text = a.textContent.toLowerCase();
var hit = wanted.some(function (w) { return text.indexOf(w) !== -1; });
if (!hit) return;
done = true;
a.setAttribute("data-rec", "1");
var badge = document.createElement("span");
badge.className = "rec-badge";
badge.textContent = "Recommended for your device";
a.parentNode.insertBefore(badge, a.nextSibling);
var row = a.closest("tr");
if (row) {
row.classList.add("rec-row");
var body = row.parentNode;
if (body && body.firstElementChild !== row) body.insertBefore(row, body.firstElementChild);
}
});
}
function renderChangelog() {
var box = document.getElementById("changelog");
if (!box) return;
box.innerHTML = '<p class="muted-line">Loading releases...</p>';
Promise.all(PROJECTS.map(function (p) {
return fetchProjectReleases(p).then(function (list) {
return list.map(function (r) { return { project: p, rel: r }; });
});
})).then(function (groups) {
var all = [].concat.apply([], groups).sort(function (a, b) {
return new Date(b.rel.released_at || b.rel.created_at) - new Date(a.rel.released_at || a.rel.created_at);
}).slice(0, 15);
if (!all.length) {
box.innerHTML = '<p class="muted-line">No releases found yet.</p>';
return;
}
box.innerHTML = all.map(function (entry) {
var r = entry.rel;
var d = new Date(r.released_at || r.created_at);
var date = isNaN(d) ? "" : d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
var notes = String(r.description || "").replace(/[#*`>_\[\]]/g, "").replace(/\s+/g, " ").trim().slice(0, 280);
var url = r._links && typeof r._links.self === "string" && /^https?:\/\//.test(r._links.self) ? r._links.self : "";
return '<article class="cl-item"><div class="cl-meta"><span class="cl-tag">' + esc(PROJECT_LABELS[entry.project] || entry.project) + "</span><span>" + esc(r.tag_name || "") + "</span><span>" + esc(date) + "</span></div>" +
(r.name && r.name !== r.tag_name ? '<div class="cl-title">' + esc(r.name) + "</div>" : "") +
(notes ? '<p class="cl-notes">' + esc(notes) + "</p>" : "") +
(url ? '<a class="cl-link" href="' + esc(url) + '" target="_blank" rel="noopener">View on GitLab</a>' : "") +
"</article>";
}).join("");
});
}
function fetchContributors(project) {
var url = "https://gitlab.com/api/v4/projects/vyla-entertainment%2F" + encodeURIComponent(project) + "/repository/contributors?order_by=commits&sort=desc&per_page=100";
return fetch(url)
.then(function (res) {
if (!res.ok) throw new Error();
return res.json();
})
.then(function (data) { return Array.isArray(data) ? data : []; })
.catch(function () { return []; });
}
function renderContributors() {
var box = document.getElementById("contributors");
if (!box) return;
box.innerHTML = '<p class="muted-line">Loading contributors...</p>';
Promise.all(PROJECTS.map(fetchContributors)).then(function (groups) {
var totals = {};
[].concat.apply([], groups).forEach(function (c) {
var name = String(c.name || "").trim();
if (!name) return;
var key = name.toLowerCase();
if (!totals[key]) totals[key] = { name: name, commits: 0 };
totals[key].commits += Number(c.commits) || 0;
});
var people = Object.keys(totals).map(function (k) { return totals[k]; }).sort(function (a, b) { return b.commits - a.commits; });
if (!people.length) {
box.innerHTML = '<p class="muted-line">Contributors will show up here.</p>';
return;
}
box.innerHTML = people.map(function (p) {
var avatar;
if (p.name.toLowerCase() === "alwaysmesmerizingyou") {
avatar = '<img class="ct-avatar-img" src="https://secure.gravatar.com/avatar/f7a5dfd997d0aa0e81331fd1804d962099056d0b01d60d686e02deb3f8d9751c?s=80&d=identicon" alt="' + esc(p.name) + '" />';
} else if (p.name.toLowerCase() === "zstrike") {
avatar = '<img class="ct-avatar-img" src="https://secure.gravatar.com/avatar/50dccff77795ed47ea2afe0d67823fbde6e0a6c39d7b6cc3af41a08bd159611d?s=1600&d=identicon" alt="' + esc(p.name) + '" />';
} else {
avatar = '<span class="ct-avatar">' + esc(p.name.charAt(0).toUpperCase()) + "</span>";
}
return '<span class="ct-chip">' + avatar + esc(p.name) + " <em>" + p.commits + "</em></span>";
}).join("");
});
}
function burstConfetti() {
if (document.body.classList.contains("no-motion")) return;
var copy = THEME_COPY[resolveTheme(getThemeMode())] || THEME_COPY.base;
var canvas = document.createElement("canvas");
canvas.className = "confetti-canvas";
var dpr = window.devicePixelRatio || 1;
var w = window.innerWidth;
var h = window.innerHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
document.body.appendChild(canvas);
var ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
var accent = getComputedStyle(document.documentElement).getPropertyValue("--text").trim() || "#ffffff";
var colors = [accent, "#ffffff", "#ffd24d", "#4da6ff", "#ff5c8a", "#5fd68f"];
var glyphs = copy.confetti;
var parts = [];
for (var i = 0; i < 140; i++) {
parts.push({
x: w / 2 + (Math.random() - 0.5) * 120,
y: h * 0.85,
vx: (Math.random() - 0.5) * 16,
vy: -(8 + Math.random() * 12),
size: 8 + Math.random() * 8,
rot: Math.random() * 6.28,
vr: (Math.random() - 0.5) * 0.4,
color: colors[i % colors.length],
glyph: glyphs && i % 3 === 0 ? glyphs[i % glyphs.length] : null
});
}
var start = performance.now();
function frame(now) {
var t = now - start;
ctx.clearRect(0, 0, w, h);
parts.forEach(function (p) {
p.vy += 0.35;
p.vx *= 0.99;
p.x += p.vx;
p.y += p.vy;
p.rot += p.vr;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.globalAlpha = Math.max(0, 1 - t / 4200);
if (p.glyph) {
ctx.font = p.size * 2 + "px serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(p.glyph, 0, 0);
} else {
ctx.fillStyle = p.color;
ctx.fillRect(-p.size / 2, -p.size / 4, p.size, p.size / 2);
}
ctx.restore();
});
if (t < 4200) requestAnimationFrame(frame);
else canvas.remove();
}
requestAnimationFrame(frame);
}
function initEasterEggs() {
var progress = 0;
document.addEventListener("keydown", function (e) {
var t = e.target;
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA")) return;
var key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
if (key === KONAMI[progress]) {
progress++;
if (progress === KONAMI.length) {
progress = 0;
burstConfetti();
}
} else {
progress = key === KONAMI[0] ? 1 : 0;
}
});
try {
console.log("%cVyla", "font:700 22px Inter,sans-serif;color:#ff7518", "\nYou found the console. Vyla is built by its community. Come help: https://gitlab.com/vyla-entertainment/");
} catch (e) { }
}
var ACCENT_PRESETS = ["#ff7518", "#f4b942", "#ff5c5c", "#f472b6", "#c084fc", "#8ad1ff", "#5fd68f", "#ffffff"];
var TEXT_STEPS = [
{ v: "90", l: "Small" },
{ v: "100", l: "Default" },
{ v: "115", l: "Large" },
{ v: "130", l: "XL" }
];
function settingsEl(id) {
return document.getElementById(id);
}
function setApiHint(msg, isErr) {
var h = settingsEl("set-api-hint");
if (!h) return;
h.textContent = msg || "Used in code examples across the Stream API docs.";
h.classList.toggle("err", !!isErr);
}
function refreshPersonalPages() {
if (document.querySelector(".personal-card")) loadPage();
}
function themeChips(list) {
return list.map(function (t) {
return '<button type="button" class="st-theme" data-mode="' + t.id + '">' + icon(t.id) + "<span>" + esc(t.label) + "</span></button>";
}).join("");
}
function switchRow(id, title, hint) {
return '<label class="st-row"><span class="st-text"><span class="st-label">' + title + '</span><span class="st-hint">' + hint + '</span></span>' +
'<span class="st-switch"><input id="' + id + '" type="checkbox"><span class="st-track"></span></span></label>';
}
function syncSettingsUI() {
var m = settingsEl("settings-modal");
if (!m) return;
settingsEl("set-name").value = getPref("name", "");
settingsEl("set-api").value = getPref("api", "");
settingsEl("set-api").classList.remove("bad");
setApiHint("");
var accent = String(getPref("accent", "") || "").toLowerCase();
var validAccent = /^#[0-9a-f]{6}$/.test(accent);
m.querySelectorAll(".st-swatch[data-color]").forEach(function (b) {
b.classList.toggle("on", validAccent && b.getAttribute("data-color") === accent);
});
m.querySelector(".st-swatch.reset").classList.toggle("on", !validAccent);
m.querySelector(".st-swatch.custom").classList.toggle("on", validAccent && ACCENT_PRESETS.indexOf(accent) === -1);
settingsEl("set-accent").value = validAccent ? accent : "#ffffff";
var size = String(parseInt(getPref("text", "100"), 10) || 100);
m.querySelectorAll(".st-seg button").forEach(function (b) {
b.classList.toggle("on", b.getAttribute("data-v") === size);
});
settingsEl("set-video").checked = getPref("video", "on") !== "off";
settingsEl("set-motion").checked = getPref("motion", "on") !== "off";
settingsEl("set-deco").checked = getPref("deco", "on") !== "off";
}
var saveTimer = null;
function measureSettings() {
var m = settingsEl("settings-modal");
if (!m) return;
var panel = m.querySelector(".sheet-panel");
var top = m.querySelector(".st-top");
var bottom = m.querySelector(".st-bottom");
if (top) panel.style.setProperty("--st-top", Math.max(0, top.offsetHeight - 10) + "px");
if (bottom) panel.style.setProperty("--st-bottom", Math.max(0, bottom.offsetHeight - 10) + "px");
}
function openSettings() {
var m = settingsEl("settings-modal");
if (!m) return;
syncSettingsUI();
m.querySelector(".sheet-panel").style.transform = "";
m.classList.add("active");
measureSettings();
document.body.style.overflow = "hidden";
}
function closeSettings() {
var m = settingsEl("settings-modal");
if (!m || !m.classList.contains("active")) return;
m.querySelector(".sheet-panel").style.transform = "";
m.classList.remove("active");
document.body.style.overflow = "";
}
function buildSettings() {
var trigger = settingsEl("settings-trigger");
if (trigger) trigger.innerHTML = icon("sliders");
var modal = document.createElement("div");
modal.id = "settings-modal";
modal.className = "sheet-overlay settings-overlay";
var swatches = ACCENT_PRESETS.map(function (c) {
return '<button type="button" class="st-swatch" data-color="' + c + '" style="--sw:' + c + '" aria-label="Accent color ' + c + '"></button>';
}).join("");
var sizes = TEXT_STEPS.map(function (s) {
return '<button type="button" data-v="' + s.v + '">' + s.l + "</button>";
}).join("");
modal.innerHTML =
'<div class="sheet-backdrop"></div>' +
'<div class="sheet-panel settings-panel" role="dialog" aria-modal="true" aria-label="Personalize">' +
'<div class="st-top"><div class="sheet-handle-bar"><div class="sheet-handle"></div></div>' +
'<h3 class="sheet-title">Make it yours</h3>' +
'<p class="sheet-desc">Saved only in this browser. Nothing leaves your device.</p></div>' +
'<div class="st-scroll">' +
'<section class="st-group"><h4 class="st-heading">Theme</h4><div class="st-card st-themes">' +
'<div class="st-sub">General</div><div class="st-theme-grid">' + themeChips(THEMES_GENERAL) + "</div>" +
'<div class="st-sub">Seasonal</div><div class="st-theme-grid">' + themeChips(THEMES_SEASONAL) + "</div>" +
"</div></section>" +
'<section class="st-group"><h4 class="st-heading">You</h4><div class="st-card">' +
'<label class="st-field"><span class="st-label">Your name</span><input id="set-name" class="st-input" type="text" maxlength="24" placeholder="Shown in the home greeting" autocomplete="off"></label>' +
'<label class="st-field"><span class="st-label">Stream API URL</span><input id="set-api" class="st-input" type="url" placeholder="https://your-server.example.com" autocomplete="off" spellcheck="false"><span id="set-api-hint" class="st-hint"></span></label>' +
"</div></section>" +
'<section class="st-group"><h4 class="st-heading">Appearance</h4><div class="st-card">' +
'<div class="st-row stack"><span class="st-text"><span class="st-label">Accent color</span><span class="st-hint">Headings, buttons and highlights.</span></span>' +
'<span class="st-swatches"><button type="button" class="st-swatch reset" aria-label="Use theme color" title="Theme default"></button>' + swatches +
'<label class="st-swatch custom" title="Custom color"><input id="set-accent" type="color" value="#ffffff" aria-label="Custom accent color"></label></span></div>' +
'<div class="st-row stack"><span class="st-text"><span class="st-label">Text size</span><span class="st-hint">Applies to documentation pages.</span></span>' +
'<div class="st-seg">' + sizes + "</div></div>" +
"</div></section>" +
'<section class="st-group"><h4 class="st-heading">Experience</h4><div class="st-card">' +
switchRow("set-video", "Background video", "Looping video behind each theme.") +
switchRow("set-motion", "Animations", "Transitions, falling decorations and effects.") +
switchRow("set-deco", "Decorations", "Spiders, snow, leaves and lights.") +
"</div></section>" +
"</div>" +
'<div class="st-bottom"><button type="button" class="sheet-btn-never st-reset">Reset</button><button type="button" class="sheet-btn-primary st-save">Save preferences</button></div>' +
"</div>";
document.body.appendChild(modal);
refreshThemeUI();
measureSettings();
if (window.ResizeObserver) {
var settingsRO = new ResizeObserver(measureSettings);
settingsRO.observe(modal.querySelector(".st-top"));
settingsRO.observe(modal.querySelector(".st-bottom"));
}
var panel = modal.querySelector(".sheet-panel");
var handle = modal.querySelector(".st-top");
modal.addEventListener("click", function (e) {
if (e.target.closest(".sheet-backdrop")) {
closeSettings();
return;
}
var saveBtn = e.target.closest(".st-save");
if (saveBtn) {
var apiBox = settingsEl("set-api");
var apiRaw = apiBox.value.trim();
commitApi();
if (apiRaw && !cleanApiUrl(apiRaw)) {
apiBox.focus();
return;
}
saveBtn.textContent = "Saved";
saveBtn.classList.add("done");
clearTimeout(saveTimer);
saveTimer = setTimeout(function () {
closeSettings();
saveBtn.textContent = "Save preferences";
saveBtn.classList.remove("done");
}, 450);
return;
}
var tb = e.target.closest(".st-theme");
if (tb) {
setTheme(tb.getAttribute("data-mode"));
return;
}
var sw = e.target.closest(".st-swatch[data-color]");
if (sw) {
var color = sw.getAttribute("data-color");
setPref("accent", color);
applyAccent(color);
syncSettingsUI();
return;
}
if (e.target.closest(".st-swatch.reset")) {
setPref("accent", null);
applyAccent("");
syncSettingsUI();
return;
}
var seg = e.target.closest(".st-seg button");
if (seg) {
var v = seg.getAttribute("data-v");
setPref("text", v === "100" ? null : v);
applyPreferences();
syncSettingsUI();
return;
}
if (e.target.closest(".st-reset")) {
Object.keys(PREF_KEYS).forEach(function (k) { setPref(k, null); });
applyTheme();
syncSettingsUI();
refreshPersonalPages();
}
});
settingsEl("set-name").addEventListener("input", function () {
setPref("name", this.value.trim());
});
function commitApi() {
var input = settingsEl("set-api");
var raw = input.value.trim();
var clean = cleanApiUrl(raw);
if (raw && !clean) {
input.classList.add("bad");
setApiHint("Enter a full URL starting with http:// or https://", true);
return;
}
input.classList.remove("bad");
setPref("api", clean);
input.value = clean;
setApiHint(clean ? "Saved. Docs examples now use your server." : "");
refreshPersonalPages();
}
settingsEl("set-api").addEventListener("change", commitApi);
settingsEl("set-api").addEventListener("keydown", function (e) {
if (e.key === "Enter") commitApi();
});
settingsEl("set-accent").addEventListener("input", function () {
var color = this.value.toLowerCase();
setPref("accent", color);
applyAccent(color);
syncSettingsUI();
});
[["set-video", "video"], ["set-motion", "motion"], ["set-deco", "deco"]].forEach(function (pair) {
settingsEl(pair[0]).addEventListener("change", function () {
setPref(pair[1], this.checked ? null : "off");
applyTheme();
});
});
var drag = null;
handle.addEventListener("touchstart", function (e) {
if (window.innerWidth > 900) return;
drag = { y: e.touches[0].clientY, d: 0 };
panel.classList.add("dragging");
}, { passive: true });
window.addEventListener("touchmove", function (e) {
if (!drag) return;
drag.d = Math.max(0, e.touches[0].clientY - drag.y);
panel.style.transform = "translateY(" + drag.d + "px)";
}, { passive: true });
window.addEventListener("touchend", function () {
if (!drag) return;
var d = drag.d;
drag = null;
panel.classList.remove("dragging");
if (d > 110) closeSettings();
else panel.style.transform = "";
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && modal.classList.contains("active")) closeSettings();
});
}
function parseMarkdown(md) {
var lines = md.replace(/\r\n/g, "\n").split("\n");
var out = [];
var inPre = false;
var preLang = "";
var preCode = [];
var inTable = false;
var tableRows = [];
var inList = false;
var listType = "ul";
var inCodeGroup = false;
var codeGroupTabs = [];
var codeGroupPanels = [];
function flushList() {
if (inList) {
out.push("</" + listType + ">");
inList = false;
}
}
function flushTable() {
if (inTable) {
var html = '<div class="table-wrap"><table>';
if (tableRows.length > 0) {
html += "<thead><tr>";
var ths = tableRows[0];
for (var i = 0; i < ths.length; i++) html += "<th>" + parseInline(ths[i]) + "</th>";
html += "</tr></thead>";
}
if (tableRows.length > 1) {
html += "<tbody>";
for (var r = 1; r < tableRows.length; r++) {
html += "<tr>";
var tds = tableRows[r];
for (var c = 0; c < tds.length; c++) html += "<td>" + parseInline(tds[c]) + "</td>";
html += "</tr>";
}
html += "</tbody>";
}
html += "</table></div>";
out.push(html);
inTable = false;
tableRows = [];
}
}
function slugify(text) {
return text.toLowerCase().replace(/<[^>]+>/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}
function parseInline(text) {
return text
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function (_, alt, src) {
var cls = src.indexOf("devices.png") !== -1 || src.indexOf("site_preview.png") !== -1 || src.indexOf("site-preview.png") !== -1 ? ' class="vyla-banner"' : "";
return '<img src="' + src + '" alt="' + esc(alt) + '" loading="lazy" decoding="async" fetchpriority="low"' + cls + " />";
})
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (_, label, href) {
return '<a href="' + href + '">' + label + "</a>";
})
.replace(/`([^`]+)`/g, function (_, code) {
return "<code>" + esc(code) + "</code>";
})
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
.replace(/\*([^*]+)\*/g, "<em>$1</em>");
}
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line.startsWith(":::code-group")) {
flushList();
flushTable();
inCodeGroup = true;
codeGroupTabs = [];
codeGroupPanels = [];
continue;
}
if (inCodeGroup && line === ":::") {
flushList();
flushTable();
var cgHtml = '<div class="code-group"><div class="code-group-tabs">';
codeGroupTabs.forEach(function (tab, idx) {
var activeClass = idx === 0 ? " active" : "";
cgHtml += '<button class="code-group-tab' + activeClass + '" data-tab-index="' + idx + '" type="button" onclick="cgSwitch(this)">' + esc(tab) + '</button>';
});
cgHtml += '</div><div class="code-group-panels">';
codeGroupPanels.forEach(function (panel, idx) {
var activeClass = idx === 0 ? " active" : "";
var langClass = panel.lang ? ' class="language-' + panel.lang + '"' : ' class="language-none"';
cgHtml += '<div class="code-group-panel' + activeClass + '"><pre><code' + langClass + '>' + esc(panel.code) + '</code></pre></div>';
});
cgHtml += '</div></div>';
out.push(cgHtml);
inCodeGroup = false;
codeGroupTabs = [];
codeGroupPanels = [];
continue;
}
if (line.startsWith("```")) {
flushList();
flushTable();
if (!inPre) {
inPre = true;
var info = line.slice(3).trim();
var tabMatch = info.match(/\[([^\]]+)\]/);
var tabTitle = tabMatch ? tabMatch[1] : "";
preLang = info.replace(/\[([^\]]+)\]/, "").trim();
if (inCodeGroup && tabTitle) {
codeGroupTabs.push(tabTitle);
}
preCode = [];
} else {
inPre = false;
var codeContent = preCode.join("\n");
var resolvedLang = preLang;
if (!resolvedLang) {
if (codeContent.trim().startsWith("{") || codeContent.trim().startsWith("[")) resolvedLang = "json";
else if (codeContent.indexOf("=") !== -1 && !codeContent.includes("function") && !codeContent.includes(";")) resolvedLang = "ini";
else if (codeContent.startsWith("http://") || codeContent.startsWith("https://")) resolvedLang = "http";
else resolvedLang = "bash";
}
if (inCodeGroup) {
codeGroupPanels.push({ lang: resolvedLang, code: codeContent });
} else {
var langClass = resolvedLang ? ' class="language-' + resolvedLang + '"' : ' class="language-none"';
out.push('<div class="code-wrap"><pre><code' + langClass + ">" + esc(codeContent) + "</code></pre></div>");
}
}
continue;
}
if (inPre) {
preCode.push(line);
continue;
}
if (line.startsWith(":::callout-")) {
flushList();
flushTable();
var type = line.slice(11).trim();
out.push('<div class="callout callout-' + type + '"><div class="callout-content">');
continue;
}
if (line === ":::") {
flushList();
flushTable();
out.push("</div></div>");
continue;
}
if (line.startsWith("<div") || line.startsWith("</div") || line.startsWith("<span") || line.startsWith("</span") || line.startsWith("<img") || line.startsWith("<ul") || line.startsWith("</ul") || line.startsWith("<li") || line.startsWith("</li") || line.startsWith("<p class=")) {
flushList();
flushTable();
out.push(line);
continue;
}
if (line.indexOf("|") !== -1 && line.trim().startsWith("|") && line.trim().endsWith("|")) {
flushList();
var cells = line.split("|").slice(1, -1).map(function (c) { return c.trim(); });
if (cells.length > 0 && cells[0].match(/^:?-+:?$/)) {
continue;
}
inTable = true;
tableRows.push(cells);
continue;
} else {
flushTable();
}
if (line.match(/^---+$/)) {
flushList();
out.push("<hr />");
continue;
}
var hMatch = line.match(/^(#{1,6})\s+(.*)$/);
if (hMatch) {
flushList();
var level = hMatch[1].length;
var titleText = parseInline(hMatch[2]);
var rawTitle = hMatch[2];
var id = slugify(rawTitle);
out.push("<h" + level + ' id="' + id + '">' + titleText + "</h" + level + ">");
continue;
}
var olMatch = line.match(/^\d+\.\s+(.*)$/);
if (olMatch) {
if (!inList || listType !== "ol") {
flushList();
inList = true;
listType = "ol";
out.push("<ol>");
}
out.push("<li>" + parseInline(olMatch[1]) + "</li>");
continue;
}
var ulMatch = line.match(/^[-*]\s+(.*)$/);
if (ulMatch) {
if (!inList || listType !== "ul") {
flushList();
inList = true;
listType = "ul";
out.push("<ul>");
}
out.push("<li>" + parseInline(ulMatch[1]) + "</li>");
continue;
}
flushList();
if (line.trim().length === 0) {
continue;
}
if (line.startsWith("> ")) {
out.push("<blockquote><p>" + parseInline(line.slice(2)) + "</p></blockquote>");
continue;
}
out.push("<p>" + parseInline(line) + "</p>");
}
flushList();
flushTable();
return out.join("\n");
}
function resolveRoute(pathname) {
var path = pathname || window.location.pathname;
if (!path.startsWith("/")) path = "/" + path;
path = path.replace(/\/+$/, "") || "/";
if (path === "/") {
return { tab: SITE_TREE[0], group: { title: "Vyla" }, item: { title: "Home", path: "/" } };
}
for (var t = 0; t < SITE_TREE.length; t++) {
var tab = SITE_TREE[t];
for (var g = 0; g < tab.groups.length; g++) {
var grp = tab.groups[g];
for (var i = 0; i < grp.items.length; i++) {
var item = grp.items[i];
var itemPath = item.path.replace(/\/+$/, "") || "/";
if (itemPath.toLowerCase() === path.toLowerCase()) {
return { tab: tab, group: grp, item: item };
}
}
}
}
return {
is404: true,
tab: SITE_TREE[0],
group: { title: "Error" },
item: { title: "404 Not Found", path: path }
};
}
function renderNavTabs(currentTab) {
var nav = document.getElementById("nav-tabs");
if (!nav) return;
nav.innerHTML = SITE_TREE.map(function (tab) {
var activeClass = tab.id === currentTab.id ? " active" : "";
return '<a class="tab-link' + activeClass + '" href="' + tab.path + '">' + esc(tab.title) + "</a>";
}).join("");
}
function renderSidebar(currentTab, currentItem) {
var sidebar = document.getElementById("sidebar");
if (!sidebar) return;
var html = "";
currentTab.groups.forEach(function (group) {
html += '<div class="group">';
html += '<p class="group-title">' + esc(group.title) + "</p>";
html += "<ul>";
group.items.forEach(function (item) {
var activeClass = item.path === currentItem.path ? ' class="active"' : "";
html += '<li><a' + activeClass + ' href="' + item.path + '">' + esc(item.title) + "</a></li>";
});
html += "</ul></div>";
});
sidebar.innerHTML = html;
buildMobileBar(currentTab, currentItem);
}
function buildMobileBar(currentTab, currentItem) {
var oldBar = document.getElementById("mobile-bar");
if (oldBar) oldBar.remove();
var headerContainer = document.getElementById("header-container") || document.getElementById("navbar");
if (!headerContainer) return;
var route = resolveRoute(window.location.pathname);
var bar = document.createElement("div");
bar.id = "mobile-bar";
bar.innerHTML =
'<button id="mobile-toggle" type="button" aria-label="Toggle navigation" onclick="toggleSidebar()">' + icon("menu") + '</button>' +
'<div class="mobile-crumbs">' +
'<span class="crumb-group">' + esc(route.group.title) + '</span>' +
'<span class="crumb-sep">›</span>' +
'<span class="crumb-page" id="mobile-crumb-page">' + esc(currentItem.title) + '</span>' +
'<span class="mobile-crumb-sub-btn" id="mobile-crumb-sub" style="display:none;">' +
'<span class="crumb-sep">›</span>' +
'<span class="crumb-heading" id="mobile-crumb-target"></span>' +
'</span>' +
'</div>';
if (!document.body.classList.contains("is-home")) headerContainer.appendChild(bar);
var pageBtn = bar.querySelector("#mobile-crumb-page");
if (pageBtn) {
pageBtn.addEventListener("click", function () {
window.scrollTo({ top: 0, behavior: "smooth" });
});
}
var subBtn = bar.querySelector("#mobile-crumb-sub");
if (subBtn) {
subBtn.addEventListener("click", function () {
var targetId = subBtn.getAttribute("data-target-id");
var targetEl = targetId ? document.getElementById(targetId) : null;
if (targetEl) {
var header = document.getElementById("header-container") || document.getElementById("navbar");
var offset = header ? header.offsetHeight + 16 : 80;
var topPos = targetEl.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({ top: Math.max(0, topPos), behavior: "smooth" });
}
});
}
var sidebar = document.getElementById("sidebar");
if (sidebar) {
var existingTabs = sidebar.querySelector(".mobile-tabs");
if (existingTabs) existingTabs.remove();
var g = document.createElement("div");
g.className = "mobile-tabs";
g.innerHTML =
'<button type="button" class="mt-btn" aria-expanded="false"><span>' + esc(currentTab.title) + "</span>" + icon("chevron") + "</button>" +
'<div class="mt-menu">' + SITE_TREE.map(function (t) {
var onClass = t.id === currentTab.id ? " on" : "";
return '<button type="button" class="mt-item' + onClass + '" data-tab-id="' + t.id + '"><span>' + esc(t.title) + "</span>" + icon("check") + "</button>";
}).join("") + "</div>";
sidebar.insertBefore(g, sidebar.firstChild);
g.querySelector(".mt-btn").addEventListener("click", function () {
g.classList.toggle("open");
});
g.querySelectorAll(".mt-item").forEach(function (btn) {
btn.addEventListener("click", function (e) {
e.stopPropagation();
var targetId = btn.getAttribute("data-tab-id");
var targetTab = SITE_TREE.find(function (t) { return t.id === targetId; });
if (targetTab) {
g.querySelectorAll(".mt-item").forEach(function (el) { el.classList.remove("on"); });
btn.classList.add("on");
g.classList.remove("open");
renderSidebar(targetTab, currentItem);
sidebar.scrollTop = 0;
}
});
});
document.addEventListener("click", function (e) {
if (!g.contains(e.target)) g.classList.remove("open");
});
}
var existingHead = sidebar.querySelector(".side-head");
if (existingHead) existingHead.remove();
var head = document.createElement("div");
head.className = "side-head";
var brand = document.querySelector("#navbar .brand");
if (brand && sidebar) {
var b = brand.cloneNode(true);
b.querySelector("img").removeAttribute("id");
var sideLinks = document.createElement("div");
sideLinks.className = "side-links";
var gear = document.createElement("button");
gear.type = "button";
gear.className = "side-settings";
gear.setAttribute("aria-label", "Personalize");
gear.innerHTML = icon("sliders");
gear.addEventListener("click", function () {
sidebar.classList.remove("open");
document.body.classList.remove("nav-open");
openSettings();
});
sideLinks.appendChild(gear);
head.appendChild(b);
head.appendChild(sideLinks);
sidebar.insertBefore(head, sidebar.firstChild);
}
}
function updateFooterNav(currentItem) {
var allItems = [];
SITE_TREE.forEach(function (tab) {
tab.groups.forEach(function (grp) {
grp.items.forEach(function (itm) {
allItems.push(itm);
});
});
});
var idx = allItems.findIndex(function (it) {
return it.path.replace(/\/+$/, "") === currentItem.path.replace(/\/+$/, "");
});
var prev = idx > 0 ? allItems[idx - 1] : null;
var next = idx >= 0 && idx < allItems.length - 1 ? allItems[idx + 1] : null;
var prevBtn = document.getElementById("footer-prev");
var nextBtn = document.getElementById("footer-next");
if (prevBtn) {
if (prev) {
prevBtn.href = prev.path;
prevBtn.innerHTML = icon("chevronLeft") + "<span>" + esc(prev.title) + "</span>";
prevBtn.classList.remove("hidden");
} else {
prevBtn.classList.add("hidden");
}
}
if (nextBtn) {
if (next) {
nextBtn.href = next.path;
nextBtn.innerHTML = "<span>" + esc(next.title) + "</span>" + icon("chevronRight");
nextBtn.classList.remove("hidden");
} else {
nextBtn.classList.add("hidden");
}
}
}
function updateToc() {
var main = document.getElementById("main-content");
var tocList = document.getElementById("toc-list");
if (!main || !tocList) return;
var headings = main.querySelectorAll("h2");
var html = "";
headings.forEach(function (h) {
var id = h.id || h.textContent.toLowerCase().replace(/[^a-z0-9]+/g, "-");
h.id = id;
html += '<li><a href="#' + id + '">' + esc(h.textContent) + "</a></li>";
});
tocList.innerHTML = html;
initTocSpy();
}
function initTocSpy() {
var main = document.getElementById("main-content");
var links = Array.prototype.slice.call(document.querySelectorAll("aside#toc a"));
var headings = main ? Array.prototype.slice.call(main.querySelectorAll("h2, h3")) : [];
function update() {
var activeHeading = null;
headings.forEach(function (h) {
if (h.getBoundingClientRect().top <= 160) {
activeHeading = h;
}
});
if (links.length) {
var activeId = activeHeading ? activeHeading.id : "";
links.forEach(function (a) {
var href = a.getAttribute("href").replace(/^#/, "");
a.classList.toggle("active", href === activeId);
});
}
var crumbPage = document.getElementById("mobile-crumb-page");
var crumbSub = document.getElementById("mobile-crumb-sub");
var crumbTarget = document.getElementById("mobile-crumb-target");
if (crumbPage && crumbSub && crumbTarget) {
if (activeHeading && window.scrollY > 80) {
crumbPage.classList.add("muted");
crumbSub.style.display = "inline-flex";
crumbSub.setAttribute("data-target-id", activeHeading.id);
crumbTarget.textContent = activeHeading.textContent;
} else {
crumbPage.classList.remove("muted");
crumbSub.style.display = "none";
crumbSub.removeAttribute("data-target-id");
crumbTarget.textContent = "";
}
}
}
window.addEventListener("scroll", update, { passive: true });
update();
}
function addCopyButtons() {
document.querySelectorAll(".code-group").forEach(function (cg) {
if (cg.querySelector(".cg-copy-btn")) return;
var b = document.createElement("button");
b.type = "button";
b.className = "cg-copy-btn";
b.setAttribute("aria-label", "Copy");
b.innerHTML = icon("copy");
b.onclick = function () {
var activePre = cg.querySelector(".code-group-panel.active pre");
if (!activePre) return;
navigator.clipboard.writeText(activePre.innerText).then(function () {
b.innerHTML = icon("check");
setTimeout(function () { b.innerHTML = icon("copy"); }, 1500);
});
};
cg.appendChild(b);
});
document.querySelectorAll("main.prose pre").forEach(function (pre) {
if (pre.closest(".code-group")) return;
if (pre.parentNode.classList.contains("code-wrap") && pre.parentNode.querySelector(".copy-btn")) return;
var wrap = pre.parentNode.classList.contains("code-wrap") ? pre.parentNode : document.createElement("div");
if (!pre.parentNode.classList.contains("code-wrap")) {
wrap.className = "code-wrap";
pre.parentNode.insertBefore(wrap, pre);
wrap.appendChild(pre);
}
var b = document.createElement("button");
b.type = "button";
b.className = "copy-btn";
b.setAttribute("aria-label", "Copy");
b.innerHTML = icon("copy");
b.onclick = function () {
navigator.clipboard.writeText(pre.innerText).then(function () {
b.innerHTML = icon("check");
setTimeout(function () { b.innerHTML = icon("copy"); }, 1500);
});
};
wrap.appendChild(b);
});
}
function bindScrollFade(box, getScroller) {
function update() {
var el = getScroller();
var max = el.scrollWidth - el.clientWidth;
var left = 0;
var right = 0;
if (max > 1) {
left = Math.min(el.scrollLeft, 56);
right = Math.min(Math.max(Math.round(max - el.scrollLeft), 0), 56);
}
box.style.setProperty("--fade-l", left + "px");
box.style.setProperty("--fade-r", right + "px");
}
box.addEventListener("scroll", update, true);
window.addEventListener("resize", update);
update();
}
function initCodeFades() {
document.querySelectorAll("main.prose pre").forEach(function (pre) {
var code = pre.querySelector("code");
bindScrollFade(pre, function () {
return code && code.scrollWidth > code.clientWidth + 1 ? code : pre;
});
});
}
function wrapTables() {
document.querySelectorAll("main.prose table").forEach(function (table) {
if (table.parentNode.classList.contains("table-wrap")) {
bindScrollFade(table.parentNode, function () { return table.parentNode; });
}
});
}
function navigateTo(path) {
var cleanPath = path.startsWith("/") ? path : "/" + path;
if (window.location.pathname !== cleanPath) {
window.history.pushState(null, "", cleanPath);
}
loadPage();
}
function mountGreeting(root) {
var content = root.querySelector(".header-content");
var title = root.querySelector(".header-title");
if (!content || !title) return;
var name = getPref("name", "");
var copy = THEME_COPY[resolveTheme(getThemeMode())] || THEME_COPY.base;
var pill = document.createElement("div");
pill.className = "hero-greeting";
pill.textContent = timeGreeting() + (name ? ", " + name : "") + (copy.greeting ? " · " + copy.greeting : "");
content.insertBefore(pill, title);
var last = null;
try { last = JSON.parse(localStorage.getItem("vyla-last")); } catch (e) { }
var actions = root.querySelector(".header-actions");
if (actions && last && typeof last.path === "string" && last.path.charAt(0) === "/" && last.path.indexOf("//") !== 0) {
var resume = document.createElement("a");
resume.className = "btn-secondary hero-resume";
resume.href = last.path;
resume.textContent = "Continue: " + (last.title || last.path);
actions.appendChild(resume);
}
}
function loadPage() {
var path = window.location.pathname.replace(/\/+$/, "") || "/";
var isHome = path === "/";
document.body.classList.toggle("is-home", isHome);
var contentEl = document.getElementById("main-content");
if (isHome) {
document.title = "Vyla Entertainment - Vyla";
renderNavTabs(SITE_TREE[0]);
renderSidebar(SITE_TREE[0], { title: "Home", path: "/" });
if (contentEl) {
contentEl.classList.remove("content-animate", "caught-up");
fetch("/home.txt")
.then(function (res) {
if (!res.ok) throw new Error();
return res.text();
})
.then(function (html) {
contentEl.innerHTML = html;
mountGreeting(contentEl);
applyTheme();
window.scrollTo(0, 0);
})
.catch(function () {
contentEl.innerHTML = "<h1>Home</h1><p>Create /home.txt to customize this page.</p>";
});
}
var tocList = document.getElementById("toc-list");
if (tocList) tocList.innerHTML = "";
var prevBtn = document.getElementById("footer-prev");
var nextBtn = document.getElementById("footer-next");
if (prevBtn) prevBtn.classList.add("hidden");
if (nextBtn) nextBtn.classList.add("hidden");
var sidebar = document.getElementById("sidebar");
if (sidebar && sidebar.classList.contains("open")) {
sidebar.classList.remove("open");
document.body.classList.remove("nav-open");
}
return;
}
var route = resolveRoute(window.location.pathname);
renderNavTabs(route.tab);
renderSidebar(route.tab, route.item);
document.title = route.item.title + " - Vyla";
var contentEl = document.getElementById("main-content");
document.body.classList.remove("is-404");
var route = resolveRoute(window.location.pathname);
renderNavTabs(route.tab);
renderSidebar(route.tab, route.item);
document.title = route.item.title + " - Vyla";
var contentEl = document.getElementById("main-content");
if (route.is404) {
document.body.classList.add("is-404");
document.title = "404 - Page Not Found - Vyla";
if (contentEl) {
contentEl.classList.remove("content-animate");
contentEl.innerHTML =
'<div class="error-container">' +
' <div class="error-code">404</div>' +
' <div class="error-title">PAGE NOT FOUND</div>' +
' <p class="error-desc">' + esc(notFoundCopy()) + '</p>' +
' <a href="/" class="btn-home">Take me home</a>' +
'</div>';
var tocList = document.getElementById("toc-list");
if (tocList) tocList.innerHTML = "";
var prevBtn = document.getElementById("footer-prev");
var nextBtn = document.getElementById("footer-next");
if (prevBtn) prevBtn.classList.add("hidden");
if (nextBtn) nextBtn.classList.add("hidden");
window.scrollTo(0, 0);
}
var sidebar = document.getElementById("sidebar");
if (sidebar && sidebar.classList.contains("open")) {
sidebar.classList.remove("open");
document.body.classList.remove("nav-open");
}
return;
}
if (contentEl) {
contentEl.classList.remove("content-animate");
contentEl.innerHTML =
'<div class="skeleton-wrap" aria-hidden="true">' +
'<div class="skeleton-line skeleton-eyebrow"></div>' +
'<div class="skeleton-line skeleton-title"></div>' +
'<div class="skeleton-line skeleton-desc"></div>' +
'<div class="skeleton-media"></div>' +
'<div class="skeleton-line skeleton-text w-90"></div>' +
'<div class="skeleton-line skeleton-text w-100"></div>' +
'<div class="skeleton-line skeleton-text w-75"></div>' +
'<div class="skeleton-line skeleton-text w-85"></div>' +
'</div>';
}
fetch(route.item.file)
.then(function (res) {
var ctype = res.headers.get("content-type") || "";
if (!res.ok || ctype.indexOf("text/html") !== -1) throw new Error("Not found");
return res.text();
})
.then(function (md) {
if (contentEl) {
var hasApiToken = md.indexOf("YOUR_API_URL") !== -1;
contentEl.innerHTML = parseMarkdown(personalize(md));
if (window.Prism) {
window.Prism.highlightAllUnder(contentEl);
}
var eyebrow = document.createElement("div");
eyebrow.className = "eyebrow";
var eyebrowText = document.createElement("span");
eyebrowText.textContent = route.group.title;
eyebrow.appendChild(eyebrowText);
eyebrow.appendChild(buildBookmarkButton(route));
var h1 = contentEl.querySelector("h1");
if (h1) h1.parentNode.insertBefore(eyebrow, h1);
contentEl.classList.remove("caught-up");
contentEl.classList.add("content-animate");
var catchUpOnScroll = function () {
if (contentEl.classList.contains("content-animate")) {
contentEl.classList.add("caught-up");
}
window.removeEventListener("scroll", catchUpOnScroll, { passive: true });
window.removeEventListener("wheel", catchUpOnScroll, { passive: true });
window.removeEventListener("touchmove", catchUpOnScroll, { passive: true });
};
window.addEventListener("scroll", catchUpOnScroll, { passive: true });
window.addEventListener("wheel", catchUpOnScroll, { passive: true });
window.addEventListener("touchmove", catchUpOnScroll, { passive: true });
setTimeout(function () {
contentEl.classList.add("caught-up");
window.removeEventListener("scroll", catchUpOnScroll);
window.removeEventListener("wheel", catchUpOnScroll);
window.removeEventListener("touchmove", catchUpOnScroll);
}, 700);
updateFooterNav(route.item);
updateToc();
addCopyButtons();
initCodeFades();
wrapTables();
resolveLatestDownloads();
if (hasApiToken) insertApiCard();
renderChangelog();
renderContributors();
rememberPage(route);
if (window.location.hash) {
var hashId = window.location.hash.slice(1);
var hashEl = document.getElementById(hashId);
if (hashEl) {
var header = document.getElementById("header-container") || document.getElementById("navbar");
var offset = header ? header.offsetHeight + 16 : 80;
var topPos = hashEl.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({ top: Math.max(0, topPos), behavior: "smooth" });
} else {
window.scrollTo(0, 0);
}
} else {
window.scrollTo(0, 0);
}
}
})
.catch(function () {
if (contentEl) {
contentEl.innerHTML = "<h1>Page not found</h1><p>The requested page could not be located.</p>";
}
});
var sidebar = document.getElementById("sidebar");
if (sidebar && sidebar.classList.contains("open")) {
sidebar.classList.remove("open");
document.body.classList.remove("nav-open");
}
}
var RELEASE_CACHE = {};
function fetchProjectReleases(project) {
if (RELEASE_CACHE[project]) return Promise.resolve(RELEASE_CACHE[project]);
var url = "https://gitlab.com/api/v4/projects/vyla-entertainment%2F" + encodeURIComponent(project) + "/releases";
return fetch(url)
.then(function (res) {
if (!res.ok) throw new Error();
return res.json();
})
.then(function (data) {
RELEASE_CACHE[project] = Array.isArray(data) ? data : [];
return RELEASE_CACHE[project];
})
.catch(function () {
return [];
});
}
function findAssetInReleases(releases, ext) {
for (var r = 0; r < releases.length; r++) {
var rel = releases[r];
var tag = rel.tag_name || "";
var links = (rel.assets && rel.assets.links) || [];
for (var i = 0; i < links.length; i++) {
var l = links[i];
var name = (l.name || "").toLowerCase();
var url = (l.url || "").toLowerCase();
if (name.indexOf(ext) !== -1 || url.indexOf(ext) !== -1) {
return { tag: tag, url: l.direct_asset_url || l.url };
}
}
if (rel.description) {
var reg = new RegExp("\\[([^\\]]+)\\]\\((https?:[^)]+\\." + ext + "[^)]*)\\)", "i");
var match = rel.description.match(reg);
if (match) return { tag: tag, url: match[2] };
var rawMatch = rel.description.match(new RegExp("(https?:\\S+\\." + ext + ")", "i"));
if (rawMatch) return { tag: tag, url: rawMatch[1] };
}
}
return null;
}
function resolveLatestDownloads() {
var path = window.location.pathname;
var project = null;
if (path.indexOf("/products/home") !== -1) project = "home";
else if (path.indexOf("/products/go") !== -1) project = "go";
else if (path.indexOf("/products/tv") !== -1) project = "TV";
if (!project) return;
fetchProjectReleases(project).then(function (releases) {
markRecommended(project);
if (!releases.length) return;
var links = document.querySelectorAll("#main-content a");
links.forEach(function (a) {
var text = a.textContent.toLowerCase();
var href = a.getAttribute("href") || "";
if (project === "home") {
if (text.indexOf(".exe") !== -1 || text.indexOf("windows") !== -1) {
var matchExe = findAssetInReleases(releases, "exe");
if (matchExe) {
a.href = matchExe.url;
a.textContent = "Download: " + matchExe.tag + " (.exe)";
a.setAttribute("target", "_blank");
a.setAttribute("rel", "noopener");
}
} else if (text.indexOf(".dmg") !== -1 || text.indexOf("macos") !== -1) {
var matchDmg = findAssetInReleases(releases, "dmg");
if (matchDmg) {
a.href = matchDmg.url;
a.textContent = "Download: " + matchDmg.tag + " (.dmg)";
a.setAttribute("target", "_blank");
a.setAttribute("rel", "noopener");
}
}
} else if (project === "go" || project === "TV") {
if (text.indexOf(".apk") !== -1 || href.indexOf("releases") !== -1) {
var matchApk = findAssetInReleases(releases, "apk");
if (matchApk) {
a.href = matchApk.url;
a.textContent = "Download: " + matchApk.tag + " (.apk)";
a.setAttribute("target", "_blank");
a.setAttribute("rel", "noopener");
}
}
}
});
});
}
function getRecent() {
try { return JSON.parse(localStorage.getItem("vyla-recent")) || []; } catch (e) { return []; }
}
function saveRecent(item) {
if (!item) return;
var slim = { title: item.title, crumbs: item.crumbs, href: item.href, type: item.type };
var list = getRecent().filter(function (r) { return r.href !== slim.href; });
list.unshift(slim);
localStorage.setItem("vyla-recent", JSON.stringify(list.slice(0, 5)));
}
function loadSearchData() {
if (searchData) return Promise.resolve(searchData);
var promises = [];
SITE_TREE.forEach(function (tab) {
tab.groups.forEach(function (group) {
group.items.forEach(function (item) {
promises.push(
fetch(item.file)
.then(function (r) { return r.text(); })
.then(function (text) {
return {
title: item.title,
crumbs: [tab.title, group.title, item.title],
href: item.path,
type: "page",
text: text.replace(/[#*`[\]()]/g, " ").slice(0, 3000)
};
})
.catch(function () { return null; })
);
});
});
});
return Promise.all(promises).then(function (results) {
searchData = results.filter(Boolean);
return searchData;
});
}
function renderSearch(keepActive) {
var q = document.getElementById("search-input").value.trim().toLowerCase();
var tokens = q.split(/\s+/).filter(Boolean);
var box = document.getElementById("search-results");
if (!keepActive) searchActive = 0;
if (tokens.length) {
if (!searchData) {
searchResults = [];
box.innerHTML = '<div class="search-label">Indexing pages...</div>';
return;
}
searchResults = searchData
.map(function (it) {
var score = 0;
var t = it.title.toLowerCase();
var txt = it.text.toLowerCase();
tokens.forEach(function (tok) {
if (t.indexOf(tok) !== -1) score += 20;
if (txt.indexOf(tok) !== -1) score += 2;
});
return { it: it, s: score };
})
.filter(function (r) { return r.s > 0; })
.sort(function (a, b) { return b.s - a.s; })
.slice(0, 10)
.map(function (r) { return r.it; });
} else {
var saved = getBookmarks();
var recent = getRecent().filter(function (r) {
return !saved.some(function (s) { return s.href === r.href; });
});
savedCount = saved.length;
searchResults = saved.concat(recent);
}
if (!searchResults.length) {
box.innerHTML = '<div class="search-label">' + (tokens.length ? "No results found" : "Nothing saved or recent yet") + "</div>";
return;
}
box.innerHTML = searchResults.map(function (it, i) {
var label = "";
if (!tokens.length && i === 0 && savedCount) label = '<div class="search-label">Saved</div>';
if (!tokens.length && i === savedCount) label = '<div class="search-label">Recent searches</div>';
var crumbs = it.crumbs.join('<span class="sep">›</span>');
return label + '<a class="sr' + (i === searchActive ? " on" : "") + '" href="' + it.href + '" data-i="' + i + '">' +
icon(i < savedCount && !tokens.length ? "star" : "doc") +
'<div class="sr-body"><div class="t">' + esc(it.title) + "</div>" +
'<div class="p">' + crumbs + "</div></div></a>";
}).join("");
}
function openSearch() {
var input = document.getElementById("search-input");
document.getElementById("search-modal").classList.add("open");
document.body.style.overflow = "hidden";
input.value = "";
renderSearch(false);
loadSearchData().then(function () { renderSearch(false); });
input.focus();
}
function closeSearch() {
document.getElementById("search-modal").classList.remove("open");
document.body.style.overflow = "";
}
function onSearchKey(e) {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
openSearch();
return;
}
var modal = document.getElementById("search-modal");
if (!modal || !modal.classList.contains("open")) return;
if (e.key === "Escape") {
closeSearch();
} else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
var n = searchResults.length || 1;
searchActive = (searchActive + (e.key === "ArrowDown" ? 1 : -1) + n) % n;
renderSearch(true);
} else if (e.key === "Enter" && searchResults[searchActive]) {
var item = searchResults[searchActive];
saveRecent(item);
closeSearch();
navigateTo(item.href);
}
}
function buildSearch() {
var trigger = document.getElementById("search-trigger");
if (trigger) trigger.innerHTML = icon("search") + "<span>Search...</span><kbd>Ctrl K</kbd>";
var modal = document.createElement("div");
modal.id = "search-modal";
modal.innerHTML =
'<div class="search-backdrop"></div><div class="search-panel"><div class="search-input-row">' + icon("search") +
'<input id="search-input" type="text" placeholder="Search..." autocomplete="off" spellcheck="false"></div>' +
'<div id="search-results"></div><div class="search-footer"><span><kbd>↑</kbd><kbd>↓</kbd>Select</span><span><kbd>↵</kbd>Open</span><span class="close-hint"><kbd>esc</kbd>Close</span></div></div>';
document.body.appendChild(modal);
modal.querySelector(".search-backdrop").addEventListener("click", closeSearch);
document.getElementById("search-input").addEventListener("input", function () { renderSearch(false); });
document.getElementById("search-results").addEventListener("click", function (e) {
var a = e.target.closest(".sr");
if (a) {
e.preventDefault();
saveRecent(searchResults[Number(a.getAttribute("data-i"))]);
closeSearch();
navigateTo(a.getAttribute("href"));
}
});
document.addEventListener("keydown", onSearchKey);
}
var lightboxScale = 1;
var activeOriginImg = null;
function initLightbox() {
if (document.getElementById("image-lightbox")) return;
var lb = document.createElement("div");
lb.id = "image-lightbox";
lb.className = "image-lightbox";
lb.innerHTML =
'<div class="lightbox-controls">' +
'<button type="button" class="lightbox-btn lb-zoom-out" aria-label="Zoom out">' +
'<svg viewBox="0 -0.5 21 21" width="16" height="16" fill="currentColor"><g transform="translate(-3, -320)"><path d="M8.37269901,326.967331 L14.8199378,326.967331 C15.4130838,326.967331 15.8944776,327.416448 15.8944776,327.969825 C15.8944776,328.523201 15.4130838,328.972319 14.8199378,328.972319 L8.37269901,328.972319 C7.77955304,328.972319 7.29815921,328.523201 7.29815921,327.969825 C7.29815921,327.416448 7.77955304,326.967331 8.37269901,326.967331 Z M23.6848912,338.288493 C24.1050363,338.679465 24.1050363,339.315046 23.6848912,339.706019 L23.6848912,339.706019 C23.2647461,340.097994 22.5845624,340.097994 22.1654919,339.706019 L17.7888913,335.746169 L19.3082906,334.328642 L23.6848912,338.288493 Z M11.5963184,334.034912 C8.04174075,334.034912 5.14907961,331.337201 5.14907961,328.01995 C5.14907961,324.7037 8.04174075,322.004987 11.5963184,322.004987 C15.1508961,322.004987 18.0435572,324.7037 18.0435572,328.01995 C18.0435572,331.337201 15.1508961,334.034912 11.5963184,334.034912 Z M11.5963184,320 C6.84900157,320 3,323.590932 3,328.01995 C3,332.449969 6.84900157,336.039899 11.5963184,336.039899 C16.3436353,336.039899 20.1926368,332.449969 20.1926368,328.01995 C20.1926368,323.590932 16.3436353,320 11.5963184,320 Z"></path></g></svg>' +
'</button>' +
'<button type="button" class="lightbox-btn lb-zoom-in" aria-label="Zoom in">' +
'<svg viewBox="0 -0.5 21 21" width="16" height="16" fill="currentColor"><g transform="translate(-3, -320)"><path d="M8.37269901,326.967331 L10.5963184,326.967331 L10.5963184,324.743711 C10.5963184,324.190335 11.0777123,323.741217 11.6708583,323.741217 C12.2640042,323.741217 12.7453981,324.190335 12.7453981,324.743711 L12.7453981,326.967331 L14.9690175,326.967331 C15.5621635,326.967331 16.0435572,327.416448 16.0435572,327.969825 C16.0435572,328.523201 15.5621635,328.972319 14.9690175,328.972319 L12.7453981,328.972319 L12.7453981,331.195939 C12.7453981,331.749316 12.2640042,332.198433 11.6708583,332.198433 C11.0777123,332.198433 10.5963184,331.749316 10.5963184,331.195939 L10.5963184,328.972319 L8.37269901,328.972319 C7.77955304,328.972319 7.29815921,328.523201 7.29815921,327.969825 C7.29815921,327.416448 7.77955304,326.967331 8.37269901,326.967331 Z M23.6848912,338.288493 C24.1050363,338.679465 24.1050363,339.315046 23.6848912,339.706019 L23.6848912,339.706019 C23.2647461,340.097994 22.5845624,340.097994 22.1654919,339.706019 L17.7888913,335.746169 L19.3082906,334.328642 L23.6848912,338.288493 Z M11.5963184,334.034912 C8.04174075,334.034912 5.14907961,331.337201 5.14907961,328.01995 C5.14907961,324.7037 8.04174075,322.004987 11.5963184,322.004987 C15.1508961,322.004987 18.0435572,324.7037 18.0435572,328.01995 C18.0435572,331.337201 15.1508961,334.034912 11.5963184,334.034912 Z M11.5963184,320 C6.84900157,320 3,323.590932 3,328.01995 C3,332.449969 6.84900157,336.039899 11.5963184,336.039899 C16.3436353,336.039899 20.1926368,332.449969 20.1926368,328.01995 C20.1926368,323.590932 16.3436353,320 11.5963184,320 Z"></path></g></svg>' +
'</button>' +
'<button type="button" class="lightbox-btn lb-close" aria-label="Close">' +
'<svg viewBox="0 0 1024 1024" width="13" height="13" fill="currentColor"><path d="M195.2 195.2a64 64 0 0 1 90.496 0L512 421.504 738.304 195.2a64 64 0 0 1 90.496 90.496L602.496 512 828.8 738.304a64 64 0 0 1-90.496 90.496L512 602.496 285.696 828.8a64 64 0 0 1-90.496-90.496L421.504 512 195.2 285.696a64 64 0 0 1 0-90.496z"></path></svg>' +
'</button>' +
'</div>' +
'<img class="lightbox-img" src="" alt="" />';
document.body.appendChild(lb);
var img = lb.querySelector(".lightbox-img");
var btnClose = lb.querySelector(".lb-close");
var btnIn = lb.querySelector(".lb-zoom-in");
var btnOut = lb.querySelector(".lb-zoom-out");
function closeLb() {
if (!lb.classList.contains("active")) return;
lb.classList.remove("active");
lightboxScale = 1;
img.style.transform = "none";
if (activeOriginImg) {
var rect = activeOriginImg.getBoundingClientRect();
img.style.top = rect.top + "px";
img.style.left = rect.left + "px";
img.style.width = rect.width + "px";
img.style.height = rect.height + "px";
img.style.borderRadius = "16px";
}
document.body.style.overflow = "";
setTimeout(function () {
if (!lb.classList.contains("active")) {
img.src = "";
activeOriginImg = null;
}
}, 450);
}
function setScale(s) {
lightboxScale = Math.min(Math.max(0.5, s), 3);
img.style.transform = "scale(" + lightboxScale + ")";
}
btnClose.addEventListener("click", closeLb);
lb.addEventListener("click", function (e) {
if (e.target === lb) closeLb();
});
btnIn.addEventListener("click", function (e) {
e.stopPropagation();
setScale(lightboxScale + 0.25);
});
btnOut.addEventListener("click", function (e) {
e.stopPropagation();
setScale(lightboxScale - 0.25);
});
window.addEventListener("keydown", function (e) {
if (lb.classList.contains("active")) {
if (e.key === "Escape") closeLb();
else if (e.key === "+" || e.key === "=") setScale(lightboxScale + 0.25);
else if (e.key === "-") setScale(lightboxScale - 0.25);
}
});
}
function openLightbox(originImg) {
initLightbox();
var lb = document.getElementById("image-lightbox");
var img = lb.querySelector(".lightbox-img");
activeOriginImg = originImg;
var rect = originImg.getBoundingClientRect();
img.style.transition = "none";
img.style.top = rect.top + "px";
img.style.left = rect.left + "px";
img.style.width = rect.width + "px";
img.style.height = rect.height + "px";
img.style.borderRadius = "16px";
img.style.transform = "none";
img.src = originImg.src;
img.alt = originImg.alt || "";
lightboxScale = 1;
lb.classList.add("active");
document.body.style.overflow = "hidden";
void img.offsetWidth;
var naturalW = originImg.naturalWidth || rect.width;
var naturalH = originImg.naturalHeight || rect.height;
var maxW = window.innerWidth * 0.92;
var maxH = window.innerHeight * 0.88;
var ratio = Math.min(maxW / naturalW, maxH / naturalH, 1);
if (ratio <= 0) ratio = 1;
var targetW = naturalW * ratio;
var targetH = naturalH * ratio;
var targetTop = (window.innerHeight - targetH) / 2;
var targetLeft = (window.innerWidth - targetW) / 2;
img.style.transition = "";
img.style.top = targetTop + "px";
img.style.left = targetLeft + "px";
img.style.width = targetW + "px";
img.style.height = targetH + "px";
img.style.borderRadius = "14px";
}
function initContribSheet() {
if (localStorage.getItem("vyla-contrib-dismissed") === "true") return;
var overlay = document.createElement("div");
overlay.className = "sheet-overlay";
overlay.innerHTML =
'<div class="sheet-backdrop"></div>' +
'<div class="sheet-panel">' +
'<div class="sheet-handle-bar"><div class="sheet-handle"></div></div>' +
'<h3 class="sheet-title">Help Add & Fix SDK Sources</h3>' +
'<p class="sheet-desc">Streaming endpoints and scrapers constantly break as CDNs evolve. We are actively looking for contributors to help repair broken providers, add new video sources, and implement direct download scrapers.</p>' +
'<div class="sheet-actions">' +
'<a href="/sdk/vyla-sdk/contributing" class="sheet-btn-primary">View Guide</a>' +
'<button type="button" class="sheet-btn-never">Don\'t show again</button>' +
'</div>' +
'</div>';
document.body.appendChild(overlay);
var panel = overlay.querySelector(".sheet-panel");
var backdrop = overlay.querySelector(".sheet-backdrop");
var neverBtn = overlay.querySelector(".sheet-btn-never");
var guideLink = overlay.querySelector(".sheet-btn-primary");
var handleBar = overlay.querySelector(".sheet-handle-bar");
function openSheet() {
overlay.classList.add("active");
document.body.style.overflow = "hidden";
}
function closeSheet() {
overlay.classList.remove("active");
document.body.style.overflow = "";
setTimeout(function () {
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
}, 400);
}
function dismissForever() {
localStorage.setItem("vyla-contrib-dismissed", "true");
closeSheet();
}
backdrop.addEventListener("click", closeSheet);
neverBtn.addEventListener("click", dismissForever);
guideLink.addEventListener("click", closeSheet);
var isDragging = false;
var startY = 0;
var currentY = 0;
var currentTranslate = 0;
function getSnapOffsets() {
var panelH = panel.offsetHeight;
return [0, panelH * 0.35, panelH * 0.65];
}
handleBar.addEventListener("touchstart", function (e) {
if (window.innerWidth > 900) return;
isDragging = true;
startY = e.touches[0].clientY;
currentY = startY;
panel.classList.add("dragging");
}, { passive: true });
window.addEventListener("touchmove", function (e) {
if (!isDragging || window.innerWidth > 900) return;
currentY = e.touches[0].clientY;
var delta = currentY - startY;
var val = Math.max(0, currentTranslate + delta);
panel.style.transform = "translateY(" + val + "px)";
}, { passive: true });
window.addEventListener("touchend", function () {
if (!isDragging || window.innerWidth > 900) return;
isDragging = false;
panel.classList.remove("dragging");
var delta = currentY - startY;
var finalOffset = currentTranslate + delta;
if (delta > 110 || finalOffset > panel.offsetHeight * 0.7) {
panel.style.transform = "translateY(100%)";
closeSheet();
return;
}
var snaps = getSnapOffsets();
var closest = snaps.reduce(function (prev, curr) {
return Math.abs(curr - finalOffset) < Math.abs(prev - finalOffset) ? curr : prev;
}, 0);
currentTranslate = closest;
panel.style.transform = "translateY(" + closest + "px)";
});
setTimeout(openSheet, 400);
}
document.addEventListener("DOMContentLoaded", function () {
if (localStorage.getItem("vyla-banner-dismissed") === "true") {
var banner = document.getElementById("banner");
if (banner) banner.setAttribute("data-dismissed", "true");
document.body.classList.add("banner-dismissed");
}
var header = document.getElementById("header-container");
if (header) {
requestAnimationFrame(function () {
document.documentElement.style.setProperty("--header-height", header.offsetHeight + "px");
});
window.addEventListener("resize", function () {
requestAnimationFrame(function () {
document.documentElement.style.setProperty("--header-height", header.offsetHeight + "px");
});
});
}
applyTheme();
buildThemeControls();
buildSearch();
buildSettings();
initEasterEggs();
initContribSheet();
loadPage();
window.addEventListener("popstate", loadPage);
document.addEventListener("click", function (e) {
var img = e.target.closest("main.prose img");
if (img && !img.closest(".spider-body") && !img.closest(".cobweb")) {
e.preventDefault();
openLightbox(img);
return;
}
var heading = e.target.closest("main.prose h1, main.prose h2, main.prose h3, main.prose h4");
if (heading && heading.id) {
var header = document.getElementById("header-container") || document.getElementById("navbar");
var offset = header ? header.offsetHeight + 16 : 80;
var topPos = heading.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({ top: Math.max(0, topPos), behavior: "smooth" });
window.history.pushState(null, "", "#" + heading.id);
return;
}
var a = e.target.closest("a");
if (a) {
var href = a.getAttribute("href");
if (href && href.startsWith("#")) {
var targetId = href.slice(1);
var targetEl = document.getElementById(targetId);
if (!targetEl) {
var headings = document.querySelectorAll("h1, h2, h3, h4");
for (var h = 0; h < headings.length; h++) {
if (headings[h].textContent.trim().toLowerCase() === targetId.replace(/-/g, " ").toLowerCase()) {
targetEl = headings[h];
break;
}
}
}
if (targetEl) {
e.preventDefault();
var header = document.getElementById("header-container") || document.getElementById("navbar");
var offset = header ? header.offsetHeight + 16 : 80;
var topPos = targetEl.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({ top: Math.max(0, topPos), behavior: "smooth" });
window.history.pushState(null, "", "#" + (targetEl.id || targetId));
}
return;
}
if (href && href.startsWith("/") && !href.startsWith("//") && a.target !== "_blank") {
e.preventDefault();
navigateTo(href);
return;
}
}
var sidebar = document.getElementById("sidebar");
var menu = document.getElementById("theme-menu");
var segMenu = document.getElementById("theme-seg-menu");
if (menu && !e.target.closest(".theme-wrap")) menu.classList.remove("open");
if (segMenu && !e.target.closest("#theme-seg")) segMenu.classList.remove("open");
if (
sidebar &&
sidebar.classList.contains("open") &&
!sidebar.contains(e.target) &&
!e.target.closest("#mobile-toggle") &&
!e.target.closest(".theme-wrap") &&
!e.target.closest("#theme-seg")
) {
sidebar.classList.remove("open");
document.body.classList.remove("nav-open");
}
});
});