';
THEMES_GENERAL.forEach(function (t) {
html += '';
});
html += '';
html += '
Seasonal
';
THEMES_SEASONAL.forEach(function (t) {
html += '';
});
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("");
} 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 =
'' +
'
' +
'
' +
'
Examples on this page will use your URL. It stays in this browser.
';
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 = '
Loading releases...
';
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 = '
No releases found yet.
';
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 '
";
}).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") + "Search...Ctrl K";
var modal = document.createElement("div");
modal.id = "search-modal";
modal.innerHTML =
'
' + icon("search") +
'
' +
'
';
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 =
'
' +
'' +
'' +
'' +
'
' +
'';
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 =
'' +
'
' +
'
' +
'
Help Add & Fix SDK Sources
' +
'
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.