reworked
4
home.txt
@@ -271,6 +271,8 @@
|
||||
@media (max-width: 900px) {
|
||||
#header-container {
|
||||
border-radius: 100px;
|
||||
top: max(6px, env(safe-area-inset-top));
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,7 +333,7 @@
|
||||
|
||||
<div class="header-content">
|
||||
<div class="header-logo-badge">
|
||||
<img src="/assets/images/logo.png" alt="Vyla" />
|
||||
<img src="/src/assets/images/logo.png" alt="Vyla" />
|
||||
</div>
|
||||
<h1 class="header-title">Vyla Entertainment</h1>
|
||||
<p class="header-desc">A community-built media project focused on freedom, privacy, and giving people
|
||||
|
||||
39
index.html
@@ -23,7 +23,7 @@
|
||||
content="A community-built media ecosystem focused on freedom, privacy, and control. Featuring native Desktop, Android, and TV apps backed by modular developer tools." />
|
||||
<meta property="og:url" content="https://vyla.cc/" />
|
||||
<meta property="og:site_name" content="Vyla Entertainment" />
|
||||
<meta property="og:image" content="/assets/images/devices.png" />
|
||||
<meta property="og:image" content="/src/assets/images/devices.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content="Vyla Entertainment Ecosystem" />
|
||||
@@ -32,7 +32,7 @@
|
||||
<meta name="twitter:title" content="Vyla Entertainment - Free & Decentralized Media Platform" />
|
||||
<meta name="twitter:description"
|
||||
content="Explore Vyla Home, Go, TV, and the developer SDK. Built for privacy, performance, and complete streaming control." />
|
||||
<meta name="twitter:image" content="/assets/images/devices.png" />
|
||||
<meta name="twitter:image" content="/src/assets/images/devices.png" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
@@ -53,7 +53,7 @@
|
||||
"@id": "/#organization",
|
||||
"name": "Vyla Entertainment",
|
||||
"url": "/",
|
||||
"logo": "/assets/images/logo.png",
|
||||
"logo": "/src/assets/images/logo.png",
|
||||
"sameAs": [
|
||||
"https://gitlab.com/vyla-entertainment/",
|
||||
"https://discord.gg/5eqRhKeywG"
|
||||
@@ -75,16 +75,16 @@
|
||||
}
|
||||
</script>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="preload" href="/assets/styling/roots.css" as="style" />
|
||||
<link rel="stylesheet" href="/assets/styling/roots.css" />
|
||||
<link rel="stylesheet" href="/assets/styling/layout.css" />
|
||||
<link rel="stylesheet" href="/assets/styling/prose.css" />
|
||||
<link rel="stylesheet" href="/assets/styling/components.css" />
|
||||
<link rel="stylesheet" href="/assets/styling/themes.css" />
|
||||
<link rel="stylesheet" href="/assets/styling/overlay.css" />
|
||||
<link rel="stylesheet" href="/assets/styling/features.css" />
|
||||
<link rel="stylesheet" href="/assets/styling/highlight.css" />
|
||||
<link rel="stylesheet" href="/assets/styling/404.css" />
|
||||
<link rel="preload" href="/src/assets/css/roots.css" as="style" />
|
||||
<link rel="stylesheet" href="/src/assets/css/roots.css" />
|
||||
<link rel="stylesheet" href="/src/assets/css/layout.css" />
|
||||
<link rel="stylesheet" href="/src/assets/css/prose.css" />
|
||||
<link rel="stylesheet" href="/src/assets/css/components.css" />
|
||||
<link rel="stylesheet" href="/src/assets/css/themes.css" />
|
||||
<link rel="stylesheet" href="/src/assets/css/overlay.css" />
|
||||
<link rel="stylesheet" href="/src/assets/css/features.css" />
|
||||
<link rel="stylesheet" href="/src/assets/css/highlight.css" />
|
||||
<link rel="stylesheet" href="/src/assets/css/404.css" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="ard" href="/.well-known/ard.json" />
|
||||
@@ -106,10 +106,10 @@
|
||||
|
||||
<body>
|
||||
<div class="cobweb cobweb-top-left" aria-hidden="true">
|
||||
<img class="cobweb-media" src="/assets/images/themes/halloween/cobweb.png" alt="" />
|
||||
<img class="cobweb-media" src="/src/assets/images/themes/halloween/cobweb.png" alt="" />
|
||||
</div>
|
||||
<div class="cobweb cobweb-top-right" aria-hidden="true">
|
||||
<img class="cobweb-media" src="/assets/images/themes/halloween/cobweb.png" alt="" />
|
||||
<img class="cobweb-media" src="/src/assets/images/themes/halloween/cobweb.png" alt="" />
|
||||
</div>
|
||||
<div class="spider-container" aria-hidden="true">
|
||||
<div class="spider-thread"></div>
|
||||
@@ -130,7 +130,7 @@
|
||||
<div id="header-container">
|
||||
<header id="navbar">
|
||||
<div class="nav-top">
|
||||
<a class="brand" href="/"><img src="/assets/images/logo.png" alt="Vyla" id="brand-logo" width="28"
|
||||
<a class="brand" href="/"><img src="/src/assets/images/logo.png" alt="Vyla" id="brand-logo" width="28"
|
||||
height="28" decoding="async" /></a>
|
||||
<button id="search-trigger" type="button" onclick="openSearch()" aria-label="Search"></button>
|
||||
<div class="links">
|
||||
@@ -194,7 +194,12 @@
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/app.js"></script>
|
||||
<script src="/src/assets/js/main.js"></script>
|
||||
<script src="/src/assets/js/api/contribute.js"></script>
|
||||
<script src="/src/assets/js/api/download.js"></script>
|
||||
<script src="/src/assets/js/api/release.js"></script>
|
||||
<script src="/src/assets/js/main/settings.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -15,13 +15,13 @@
|
||||
],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/images/android-chrome-192x192.png",
|
||||
"src": "/src/assets/images/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/assets/images/android-chrome-512x512.png",
|
||||
"src": "/src/assets/images/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<p class="page-description">The Android control center for running the Vyla media stack locally.</p>
|
||||
|
||||
<img class="vyla-banner" src="/assets/images/android/site_preview.png" alt="A collage of films and shows available through a Vyla experience" loading="lazy" fetchpriority="low" />
|
||||
<img class="vyla-banner" src="/src/assets/images/android/site_preview.png" alt="A collage of films and shows available through a Vyla experience" loading="lazy" fetchpriority="low" />
|
||||
|
||||
**Vyla Go** is the Android application in the Vyla ecosystem. It prepares and runs the local services that power your media experience, then opens the interface in a secure embedded window. It is designed for people who want Vyla on their Android device without managing a separate server process or service lifecycle by hand.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<p class="page-description">The desktop control center for running the Vyla media stack locally.</p>
|
||||
|
||||
<img class="vyla-banner" src="/assets/images/desktop/site_preview.png" alt="Vyla Home desktop application preview" loading="lazy" fetchpriority="low" />
|
||||
<img class="vyla-banner" src="/src/assets/images/desktop/site_preview.png" alt="Vyla Home desktop application preview" loading="lazy" fetchpriority="low" />
|
||||
|
||||
## Vyla Home
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<p class="page-description">The Android TV application for running the Vyla media experience on your television.</p>
|
||||
|
||||
<img class="vyla-banner" src="/assets/images/android-tv/site-preview.png" alt="A collage of films and shows available through a Vyla experience" loading="lazy" fetchpriority="low" />
|
||||
<img class="vyla-banner" src="/src/assets/images/android-tv/site-preview.png" alt="A collage of films and shows available through a Vyla experience" loading="lazy" fetchpriority="low" />
|
||||
|
||||
## Vyla TV
|
||||
|
||||
|
||||
@@ -815,11 +815,9 @@ body.is-home #mobile-bar {
|
||||
}
|
||||
|
||||
#header-container {
|
||||
width: 90%;
|
||||
border-radius: 30px;
|
||||
backdrop-filter: blur(15px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(15px) saturate(1.2);
|
||||
top: max(6px, env(safe-area-inset-top));
|
||||
width: 100%;
|
||||
border-radius: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
#navbar .tabs,
|
||||
@@ -16,7 +16,7 @@
|
||||
--code-text: #abb2bf;
|
||||
--bg-menu: #000000;
|
||||
--bg-modal: rgba(10, 10, 10, 0.35);
|
||||
--hover-bg: rgba(255, 255, 255, 0.08);
|
||||
--hover-bg: rgba(120, 120, 120, 0.05);
|
||||
--search-bg: rgba(50, 50, 50, 0.1);
|
||||
--search-panel-bg: rgba(10, 10, 10, 0.6);
|
||||
--code-group-bg: #0d0d0d;
|
||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 1016 KiB After Width: | Height: | Size: 1016 KiB |
|
Before Width: | Height: | Size: 441 KiB After Width: | Height: | Size: 441 KiB |
|
Before Width: | Height: | Size: 360 B After Width: | Height: | Size: 360 B |
|
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 652 B |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 3.9 MiB After Width: | Height: | Size: 3.9 MiB |
|
Before Width: | Height: | Size: 698 KiB After Width: | Height: | Size: 698 KiB |
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 3.3 MiB |
84
src/assets/js/api/contribute.js
Normal file
@@ -0,0 +1,84 @@
|
||||
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 || name.toLowerCase() === "devin bot") 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("");
|
||||
});
|
||||
}
|
||||
50
src/assets/js/api/download.js
Normal file
@@ -0,0 +1,50 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
42
src/assets/js/api/release.js
Normal file
@@ -0,0 +1,42 @@
|
||||
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;
|
||||
}
|
||||
@@ -176,7 +176,7 @@ function applyTheme() {
|
||||
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";
|
||||
if (logo) logo.src = theme === "light" ? "/src/assets/images/dark_logo.png" : "/src/assets/images/logo.png";
|
||||
applyPreferences();
|
||||
var targetSrc = THEME_VIDEOS[theme] || THEME_VIDEOS.halloween;
|
||||
var videosOff = getPref("video", "on") === "off";
|
||||
@@ -561,80 +561,6 @@ function markRecommended(project) {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -713,7 +639,6 @@ function initEasterEggs() {
|
||||
}
|
||||
});
|
||||
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) { }
|
||||
}
|
||||
|
||||
@@ -751,237 +676,6 @@ function switchRow(id, title, hint) {
|
||||
'<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 = [];
|
||||
@@ -1790,100 +1484,6 @@ function loadPage() {
|
||||
}
|
||||
}
|
||||
|
||||
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 []; }
|
||||
}
|
||||
230
src/assets/js/main/settings.js
Normal file
@@ -0,0 +1,230 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<p class="page-description">A community-built media project focused on freedom, privacy, and giving people control over their own experience.</p>
|
||||
|
||||
<img class="vyla-banner" src="/assets/images/devices.png" alt="A collage of films and shows available through a Vyla experience" loading="lazy" fetchpriority="low" />
|
||||
<img class="vyla-banner" src="/src/assets/images/devices.png" alt="A collage of films and shows available through a Vyla experience" loading="lazy" fetchpriority="low" />
|
||||
|
||||
## About Vyla
|
||||
|
||||
|
||||