var THEME_KEY = "vyla-theme"; var ICONS = { system: '', light: '', dark: '', midnight: '', nord: '', dracula: '', forest: '', sunset: '', cyberpunk: '', halloween: '', christmas: '', autumn: '', winter: '', auto: '', sliders: '', star: '', search: '', menu: '', doc: '', hash: '', copy: '', check: '', chevron: '', chevronLeft: '', chevronRight: '' }; 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: "Manga", file: "/sdk/vyla-sdk/manga.md", path: "/sdk/vyla-sdk/manga" }, { 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 '' + ICONS[name] + ''; } function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """ }[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 = '
General
'; 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("" + copy.favicon + ""); } 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 '
' + esc(PROJECT_LABELS[entry.project] || entry.project) + "" + esc(r.tag_name || "") + "" + esc(date) + "
" + (r.name && r.name !== r.tag_name ? '
' + esc(r.name) + "
" : "") + (notes ? '

' + esc(notes) + "

" : "") + (url ? 'View on GitLab' : "") + "
"; }).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 = '

Loading contributors...

'; 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 = '

Contributors will show up here.

'; return; } box.innerHTML = people.map(function (p) { var avatar; if (p.name.toLowerCase() === "alwaysmesmerizingyou") { avatar = '' + esc(p.name) + ''; } else if (p.name.toLowerCase() === "zstrike") { avatar = '' + esc(p.name) + ''; } else { avatar = '' + esc(p.name.charAt(0).toUpperCase()) + ""; } return '' + avatar + esc(p.name) + " " + p.commits + ""; }).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 '"; }).join(""); } function switchRow(id, title, hint) { return ''; } 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 ''; }).join(""); var sizes = TEXT_STEPS.map(function (s) { return '"; }).join(""); modal.innerHTML = '
' + '"; 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(""); inList = false; } } function flushTable() { if (inTable) { var html = '
'; if (tableRows.length > 0) { html += ""; var ths = tableRows[0]; for (var i = 0; i < ths.length; i++) html += ""; html += ""; } if (tableRows.length > 1) { html += ""; for (var r = 1; r < tableRows.length; r++) { html += ""; var tds = tableRows[r]; for (var c = 0; c < tds.length; c++) html += ""; html += ""; } html += ""; } html += "
" + parseInline(ths[i]) + "
" + parseInline(tds[c]) + "
"; 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 '' + esc(alt) + '"; }) .replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (_, label, href) { return '' + label + ""; }) .replace(/`([^`]+)`/g, function (_, code) { return "" + esc(code) + ""; }) .replace(/\*\*([^*]+)\*\*/g, "$1") .replace(/\*([^*]+)\*/g, "$1"); } 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 = '
'; codeGroupTabs.forEach(function (tab, idx) { var activeClass = idx === 0 ? " active" : ""; cgHtml += ''; }); cgHtml += '
'; codeGroupPanels.forEach(function (panel, idx) { var activeClass = idx === 0 ? " active" : ""; var langClass = panel.lang ? ' class="language-' + panel.lang + '"' : ' class="language-none"'; cgHtml += '
' + esc(panel.code) + '
'; }); cgHtml += '
'; 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('
" + esc(codeContent) + "
"); } } continue; } if (inPre) { preCode.push(line); continue; } if (line.startsWith(":::callout-")) { flushList(); flushTable(); var type = line.slice(11).trim(); out.push('
'); continue; } if (line === ":::") { flushList(); flushTable(); out.push("
"); continue; } if (line.startsWith(" 0 && cells[0].match(/^:?-+:?$/)) { continue; } inTable = true; tableRows.push(cells); continue; } else { flushTable(); } if (line.match(/^---+$/)) { flushList(); out.push("
"); 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("' + titleText + ""); continue; } var olMatch = line.match(/^\d+\.\s+(.*)$/); if (olMatch) { if (!inList || listType !== "ol") { flushList(); inList = true; listType = "ol"; out.push("
    "); } out.push("
  1. " + parseInline(olMatch[1]) + "
  2. "); continue; } var ulMatch = line.match(/^[-*]\s+(.*)$/); if (ulMatch) { if (!inList || listType !== "ul") { flushList(); inList = true; listType = "ul"; out.push("
      "); } out.push("
    • " + parseInline(ulMatch[1]) + "
    • "); continue; } flushList(); if (line.trim().length === 0) { continue; } if (line.startsWith("> ")) { out.push("

      " + parseInline(line.slice(2)) + "

      "); continue; } out.push("

      " + parseInline(line) + "

      "); } 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 '' + esc(tab.title) + ""; }).join(""); } function renderSidebar(currentTab, currentItem) { var sidebar = document.getElementById("sidebar"); if (!sidebar) return; var html = ""; currentTab.groups.forEach(function (group) { html += '
      '; html += '

      ' + esc(group.title) + "

      "; html += "
        "; group.items.forEach(function (item) { var activeClass = item.path === currentItem.path ? ' class="active"' : ""; html += '
      • ' + esc(item.title) + "
      • "; }); html += "
      "; }); 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 = '' + '
      ' + '' + esc(route.group.title) + '' + 'โ€บ' + '' + esc(currentItem.title) + '' + '' + '
      '; 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 = '" + '
      ' + SITE_TREE.map(function (t) { var onClass = t.id === currentTab.id ? " on" : ""; return '"; }).join("") + "
      "; 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") + "" + esc(prev.title) + ""; prevBtn.classList.remove("hidden"); } else { prevBtn.classList.add("hidden"); } } if (nextBtn) { if (next) { nextBtn.href = next.path; nextBtn.innerHTML = "" + esc(next.title) + "" + 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 += '
    • ' + esc(h.textContent) + "
    • "; }); 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 = "

      Home

      Create /home.txt to customize this page.

      "; }); } 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 = '
      ' + '
      404
      ' + '
      PAGE NOT FOUND
      ' + '

      ' + esc(notFoundCopy()) + '

      ' + ' Take me home' + '
      '; 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 = ''; } 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 = "

      Page not found

      The requested page could not be located.

      "; } }); 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 = '
      Indexing pages...
      '; 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 = '
      ' + (tokens.length ? "No results found" : "Nothing saved or recent yet") + "
      "; return; } box.innerHTML = searchResults.map(function (it, i) { var label = ""; if (!tokens.length && i === 0 && savedCount) label = '
      Saved
      '; if (!tokens.length && i === savedCount) label = '
      Recent searches
      '; var crumbs = it.crumbs.join('โ€บ'); return label + '' + icon(i < savedCount && !tokens.length ? "star" : "doc") + '
      ' + esc(it.title) + "
      " + '
      ' + crumbs + "
      "; }).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.

      ' + '
      ' + 'View Guide' + '' + '
      ' + '
      '; 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"); } }); });