520 lines
22 KiB
JavaScript
520 lines
22 KiB
JavaScript
// FolxPlay clone — vanilla JS SPA
|
|
// Each channel plays its JW Player promo (HLS from cdn.jwplayer.com).
|
|
// No tracking, no AdSense, no external API.
|
|
|
|
const JW_HLS = id => `https://cdn.jwplayer.com/manifests/${id}.m3u8`;
|
|
const JW_POSTER = id => `https://cdn.jwplayer.com/v2/media/${id}/poster.jpg?width=1280`;
|
|
|
|
const CHANNELS = [
|
|
{ key: 'one', name: 'One Music TV', color: '#a84d7e', logo: '/logos/one_mt_neg_b.png', alt: 'logo of One', jw: 'CBM2fSnV', live: 'https://oneapp.b-cdn.net/master.m3u8' },, bg: '/backgrounds/ch_one.jpg' }
|
|
{ key: 'zwei', name: 'Zwei Music TV', color: '#f36700', logo: '/logos/zwei_neg_b.png', alt: 'logo of Zwei', jw: 'LZVUn3Qy', live: 'https://zweiapp.b-cdn.net/master.m3u8' },, bg: '/backgrounds/ch_zwei.jpg' }
|
|
{ key: 'mt', name: 'Folx Music TV', color: '#C43651', logo: '/logos/mt_neg_b.png', alt: 'logo of Folx Music',jw: 'UYU6dx3Q', live: 'https://folxapp.b-cdn.net/master.m3u8' },, bg: '/backgrounds/ch_mt.jpg' }
|
|
{ key: 'adria', name: 'Adria Music TV', color: '#30a6d1', logo: '/logos/adria_neg_b.png', alt: 'logo of Adria', jw: 'lI4SC7Do', live: 'https://adriaapp.b-cdn.net/master.m3u8' },, bg: '/backgrounds/ch_adria.jpg' }
|
|
{ key: 'folx-slo', name: 'Folx Slovenija', color: '#a8b539', logo: '/logos/folx_slo_neg_b.png', alt: 'logo of Folx Slovenija', jw: 'aoCWwwSg', live: 'https://folxsloapp.b-cdn.net/master.m3u8' },, bg: '/backgrounds/ch_folx-slo.jpg' }
|
|
{ key: 'one-adria', name: 'One Adria Music TV', color: '#a84d7e', logo: '/logos/one_neg_b.png', alt: 'logo of One Adria', jw: 'IGOSgmvW', live: 'https://oneadriaapp.b-cdn.net/master.m3u8' },, bg: '/backgrounds/ch_one-adria.jpg' }
|
|
];
|
|
|
|
// Coloured dot per channel (colour taken from the channel's brand colour).
|
|
const DOT = (color) => `<span style="color:${color};">●</span>`;
|
|
|
|
// One flowing About paragraph, each station introduced with its brand-colour dot.
|
|
const ABOUT_INTRO = `<strong>FOLX PLAY</strong> brings together the channels of the <strong>FOLX NETWORK</strong> — a family of music television stations, each with its own sound and its own audience. ${DOT('#C43651')} <strong>FOLX MUSIC TELEVISION</strong> is dedicated to folk music, with more than 90% of its programming produced in-house by <strong>FOLX NETWORK</strong>. ${DOT('#a84d7e')} <strong>ONE MUSIC TELEVISION</strong> covers contemporary German-language and international pop and rock, enriched with beloved classics of the 80s, 90s and 00s, while ${DOT('#f36700')} <strong>ZWEI MUSIC TELEVISION</strong> mixes today's pop hits with familiar favourites and adds Latin American sounds with a touch of reggaeton for daily variety. ${DOT('#30a6d1')} <strong>ADRIA MUSIC TELEVISION</strong> speaks to everyone in Germany, Austria and Switzerland with Adriatic roots — from Slovenia, Croatia, Bosnia and Herzegovina, Serbia, Macedonia and Montenegro — bringing traditional Dalmatian music, Klapa singing and the biggest pop and rock hits of the region. ${DOT('#a8b539')} <strong>FOLX SLOVENIJA MUSIC TELEVISION</strong> carries European folk music with exclusive, largely in-house programming, and ${DOT('#a84d7e')} <strong>ONE ADRIA MUSIC TELEVISION</strong> rounds out the family with fresh new music from the Adriatic alongside international pop, rock and global hits of the 80s, 90s and 00s. Together, these stations make <strong>FOLX PLAY</strong> a single home for the full spectrum of the <strong>FOLX NETWORK</strong> sound.`;
|
|
|
|
const PLAY_SVG = `<svg aria-hidden="true" viewBox="0 0 448 512"><path fill="currentColor" d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z"/></svg>`;
|
|
|
|
// pick a random background on each load
|
|
const bgs = ['bg_01','bg_02','bg_03','bg_04','bg_05','bg_06','bg_07'];
|
|
const BG_URL = `/backgrounds/${bgs[Math.floor(Math.random() * bgs.length)]}.jpg`;
|
|
|
|
// ---------- helpers ----------
|
|
function el(html) {
|
|
const t = document.createElement('template');
|
|
t.innerHTML = html.trim();
|
|
return t.content.firstElementChild;
|
|
}
|
|
function escapeHtml(s) {
|
|
return String(s ?? '').replace(/[&<>"']/g, c => ({
|
|
'&':'&','<':'<','>':'>','"':'"',"'":'''
|
|
}[c]));
|
|
}
|
|
async function api() { return null; /* no backend */ }
|
|
|
|
// ---------- shell ----------
|
|
function renderShell() {
|
|
document.getElementById('root').innerHTML = `
|
|
<div class="mainBackground" style="background-image: url('${BG_URL}');">
|
|
<div class="nav">
|
|
<div class="nav__width">
|
|
<div class="logo">
|
|
<a href="/" data-link><img alt="Folx.network logo" src="/logos/folxnetwork.png"></a>
|
|
</div>
|
|
<div class="links">
|
|
<a href="/" data-link data-route="/">Home</a>
|
|
<a href="/folxnetwork" data-link data-route="/folxnetwork">About</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<main id="main"></main>
|
|
<footer class="siteFooter">© FOLX NETWORK · folxplay.tv</footer>
|
|
</div>
|
|
`;
|
|
// intercept <a data-link> for client-side navigation
|
|
document.body.addEventListener('click', (e) => {
|
|
const a = e.target.closest('a[data-link]');
|
|
if (!a) return;
|
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
|
|
e.preventDefault();
|
|
navigate(a.getAttribute('href'));
|
|
});
|
|
window.addEventListener('popstate', () => render(location.pathname));
|
|
}
|
|
|
|
function navigate(path) {
|
|
if (location.pathname !== path) history.pushState({}, '', path);
|
|
render(path);
|
|
}
|
|
|
|
// ---------- pages ----------
|
|
const pages = {
|
|
'/': renderHome,
|
|
'/artists': renderArtists,
|
|
'/folxnetwork': renderAbout,
|
|
'/policies': renderPolicies,
|
|
'/log': renderLog,
|
|
};
|
|
|
|
function render(path) {
|
|
const handler = pages[path] || renderNotFound;
|
|
// mark active link
|
|
document.querySelectorAll('.nav .links a').forEach(a => {
|
|
a.classList.toggle('active', a.getAttribute('data-route') === path);
|
|
});
|
|
handler();
|
|
}
|
|
|
|
// ===== HOME =====
|
|
let shakaInstance = null;
|
|
let currentChannel = null;
|
|
let playToken = 0; // invalidates async work when channel changes
|
|
let liveRetryTimer = null; // background timer that re-checks the live stream
|
|
let promoFallbackActive = false;
|
|
|
|
function renderHome() {
|
|
const main = document.getElementById('main');
|
|
main.innerHTML = `
|
|
<div class="pageHome">
|
|
<div class="split">
|
|
<div class="split__video">
|
|
<div id="player-area"></div>
|
|
<div id="currently-playing" class="currentlyPlaying" style="display:none;"></div>
|
|
</div>
|
|
<div class="split__channels">
|
|
<div class="channelList">
|
|
${CHANNELS.map(c => `
|
|
<a class="channelList__channel" href="#" data-channel="${c.key}">
|
|
<div class="channelList__channelIcon" style="--var-bgColor: ${c.color};">
|
|
${PLAY_SVG}
|
|
</div>
|
|
<div class="channelList__channelLogo">
|
|
<img src="${c.logo}" alt="${escapeHtml(c.alt)}">
|
|
</div>
|
|
</a>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
showSelectChannelBox();
|
|
|
|
// channel click handlers (sidebar)
|
|
main.querySelectorAll('[data-channel]').forEach(a => {
|
|
a.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const key = a.getAttribute('data-channel');
|
|
playChannel(key);
|
|
});
|
|
});
|
|
}
|
|
|
|
function showSelectChannelBox() {
|
|
document.getElementById('player-area').innerHTML = `
|
|
<div class="selectChannelBox">
|
|
<div>
|
|
<div class="lineHeader">FolxPlay Live TV</div>
|
|
<div class="lineSelectChannel">Select channel on right</div>
|
|
<div class="lineRandom" id="random-btn">Or ${PLAY_SVG} random channel</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.getElementById('random-btn')?.addEventListener('click', () => {
|
|
const ch = CHANNELS[Math.floor(Math.random() * CHANNELS.length)];
|
|
playChannel(ch.key);
|
|
});
|
|
}
|
|
|
|
function channelOverlayBg(color) {
|
|
// Exactly the channel colour (same value the sidebar button uses), flat fill.
|
|
return color;
|
|
}
|
|
|
|
function showVideoPlayer(ch) {
|
|
const color = ch ? ch.color : '#000';
|
|
const logo = ch ? ch.logo : '';
|
|
const bgImg = ch && ch.bg ? `background-image:url('${ch.bg}');background-size:cover;background-position:center;` : '';
|
|
document.getElementById('player-area').innerHTML = `
|
|
<div class="split__video_ratio" style="position:relative;">
|
|
<video id="video" controls autoplay loop playsinline></video>
|
|
<div id="video-loading" class="channelLoading${ch && ch.bg ? ' channelLoading--img' : ''}" style="--ch:${color};${bgImg}position:absolute;inset:0;border-radius:15px;z-index:2;pointer-events:none;"></div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Hide the channel-logo loading overlay once real playback starts.
|
|
// Instant hide (no opacity fade) so the player never shows through a
|
|
// half-transparent overlay mid-transition.
|
|
function hideLoadingOverlay() {
|
|
const el = document.getElementById('video-loading');
|
|
if (el) el.style.display = 'none';
|
|
}
|
|
// Show it again (e.g. when switching channel / falling back).
|
|
function showLoadingOverlay(ch) {
|
|
const el = document.getElementById('video-loading');
|
|
if (!el) return;
|
|
if (ch) {
|
|
el.style.setProperty('--ch', ch.color);
|
|
if (ch.bg) {
|
|
el.classList.add('channelLoading--img');
|
|
el.style.backgroundImage = `url('${ch.bg}')`;
|
|
el.style.backgroundSize = 'cover';
|
|
el.style.backgroundPosition = 'center';
|
|
} else {
|
|
el.classList.remove('channelLoading--img');
|
|
el.style.backgroundImage = '';
|
|
}
|
|
}
|
|
el.style.display = 'flex'; el.style.opacity = '1';
|
|
}
|
|
|
|
// Load an HLS url into the <video> element (Shaka where supported, native HLS
|
|
// on Safari). Returns the created Shaka player (or null for native).
|
|
async function loadHls(video, url, isLive) {
|
|
if (shakaInstance) {
|
|
try { await shakaInstance.destroy(); } catch (_) {}
|
|
shakaInstance = null;
|
|
}
|
|
if (window.shaka && shaka.Player.isBrowserSupported()) {
|
|
shakaInstance = new shaka.Player(video);
|
|
if (isLive) {
|
|
// These channels sit ~90-110s behind the live edge (deep HLS buffer).
|
|
// Give Shaka enough buffer headroom so playback begins instead of
|
|
// stalling on the spinner. Keep only fields valid in Shaka 4.7.
|
|
try {
|
|
shakaInstance.configure('streaming.bufferingGoal', 30);
|
|
shakaInstance.configure('streaming.rebufferingGoal', 4);
|
|
shakaInstance.configure('streaming.bufferBehind', 60);
|
|
shakaInstance.configure('streaming.lowLatencyMode', false);
|
|
} catch (e) { console.warn('shaka configure skipped:', e?.message); }
|
|
}
|
|
await shakaInstance.load(url);
|
|
return shakaInstance;
|
|
}
|
|
video.src = url;
|
|
return null;
|
|
}
|
|
|
|
function clearLiveRetry() {
|
|
if (liveRetryTimer) { clearInterval(liveRetryTimer); liveRetryTimer = null; }
|
|
}
|
|
|
|
function setNowPlaying(ch, mode) {
|
|
const now = document.getElementById('currently-playing');
|
|
if (!now) return;
|
|
now.style.display = '';
|
|
const label = mode === 'live' ? 'LIVE' : 'promo';
|
|
now.innerHTML = `Now playing: <b>${escapeHtml(ch.name)}</b> — ${label}`;
|
|
}
|
|
|
|
async function playChannel(key) {
|
|
const ch = CHANNELS.find(c => c.key === key);
|
|
if (!ch) return;
|
|
currentChannel = ch;
|
|
const token = ++playToken; // anything older than this is stale
|
|
clearLiveRetry();
|
|
destroyHls();
|
|
promoFallbackActive = false;
|
|
|
|
// mark active in sidebar
|
|
document.querySelectorAll('.channelList__channelIcon').forEach(el => {
|
|
el.classList.remove('channelList__channelIconActive');
|
|
});
|
|
const link = document.querySelector(`.channelList__channel[data-channel="${key}"] .channelList__channelIcon`);
|
|
if (link) link.classList.add('channelList__channelIconActive');
|
|
|
|
showVideoPlayer(ch);
|
|
await waitForShaka();
|
|
if (token !== playToken) return; // user switched channel meanwhile
|
|
|
|
// Hide the logo overlay as soon as real playback begins (any source).
|
|
const vid = document.getElementById('video');
|
|
if (vid) vid.addEventListener('playing', hideLoadingOverlay, { once: true });
|
|
|
|
if (ch.live) {
|
|
startLive(ch, token);
|
|
} else {
|
|
startPromo(ch, token); // promo-only channels (Folx Slo, One Adria)
|
|
}
|
|
}
|
|
|
|
// ---- LIVE: play the channel's live HLS via hls.js (handles the deep ~90s
|
|
// live edge these channels sit at, which Shaka stalls on). On any
|
|
// failure/stall, drop to the looping promo and retry live in background.
|
|
let hlsInstance = null;
|
|
function destroyHls() {
|
|
if (hlsInstance) { try { hlsInstance.destroy(); } catch (_) {} hlsInstance = null; }
|
|
}
|
|
|
|
async function startLive(ch, token) {
|
|
const video = document.getElementById('video');
|
|
if (!video || token !== playToken) return;
|
|
|
|
const onProblem = () => {
|
|
if (token !== playToken) return;
|
|
if (!promoFallbackActive) fallbackToPromo(ch, token);
|
|
};
|
|
|
|
// Watchdog: real playback must start within 15s, else fall to promo.
|
|
let started = false;
|
|
const watchdog = setTimeout(() => {
|
|
if (token === playToken && !started) onProblem();
|
|
}, 15000);
|
|
video.addEventListener('playing', () => {
|
|
started = true; clearTimeout(watchdog);
|
|
}, { once: true });
|
|
|
|
// tear down any previous players
|
|
if (shakaInstance) { try { await shakaInstance.destroy(); } catch (_) {} shakaInstance = null; }
|
|
destroyHls();
|
|
video.loop = true;
|
|
video.onerror = onProblem;
|
|
|
|
if (window.Hls && Hls.isSupported()) {
|
|
hlsInstance = new Hls({
|
|
liveSyncDuration: 30,
|
|
liveMaxLatencyDuration: 120,
|
|
maxBufferLength: 30,
|
|
manifestLoadingTimeOut: 20000,
|
|
fragLoadingTimeOut: 20000
|
|
});
|
|
hlsInstance.on(Hls.Events.ERROR, (e, d) => {
|
|
if (d && d.fatal) { console.warn('hls fatal', d.type, d.details); onProblem(); }
|
|
});
|
|
hlsInstance.loadSource(ch.live);
|
|
hlsInstance.attachMedia(video);
|
|
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
if (token === playToken) video.play().catch(() => {});
|
|
});
|
|
setNowPlaying(ch, 'live');
|
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
// Safari native HLS
|
|
video.src = ch.live;
|
|
video.play().catch(() => {});
|
|
setNowPlaying(ch, 'live');
|
|
} else {
|
|
clearTimeout(watchdog);
|
|
onProblem();
|
|
}
|
|
}
|
|
|
|
// ---- PROMO fallback (looping) + background live retry every 15s.
|
|
async function fallbackToPromo(ch, token) {
|
|
if (token !== playToken) return;
|
|
promoFallbackActive = true;
|
|
destroyHls();
|
|
const video = document.getElementById('video');
|
|
if (!video) return;
|
|
showLoadingOverlay(ch);
|
|
video.addEventListener('playing', hideLoadingOverlay, { once: true });
|
|
video.onerror = null; video.onstalled = null;
|
|
try {
|
|
await loadHls(video, JW_HLS(ch.jw), false);
|
|
if (token !== playToken) return;
|
|
video.loop = true; // promo loops until live returns
|
|
video.play().catch(() => {});
|
|
setNowPlaying(ch, 'promo');
|
|
} catch (_) {}
|
|
|
|
clearLiveRetry();
|
|
liveRetryTimer = setInterval(() => {
|
|
if (token !== playToken) { clearLiveRetry(); return; }
|
|
probeLive(ch.live).then(ok => {
|
|
if (ok && token === playToken) { // live verified playable, switch back
|
|
clearLiveRetry();
|
|
promoFallbackActive = false;
|
|
startLive(ch, token);
|
|
}
|
|
});
|
|
}, 15000);
|
|
}
|
|
|
|
// Probe whether the live stream actually plays (not just HTTP 200). Spins up a
|
|
// detached hls.js on a throwaway <video>, waits for a real timeupdate, then
|
|
// tears it down. Resolves true only if playback genuinely advanced.
|
|
function probeLive(url) {
|
|
return new Promise(resolve => {
|
|
if (!(window.Hls && Hls.isSupported())) { resolve(false); return; }
|
|
const v = document.createElement('video');
|
|
v.muted = true; v.playsInline = true;
|
|
const h = new Hls({ liveSyncDuration: 30, manifestLoadingTimeOut: 12000, fragLoadingTimeOut: 12000 });
|
|
let done = false;
|
|
const finish = (ok) => {
|
|
if (done) return; done = true;
|
|
try { h.destroy(); } catch (_) {}
|
|
v.remove();
|
|
resolve(ok);
|
|
};
|
|
h.on(Hls.Events.ERROR, (e, d) => { if (d && d.fatal) finish(false); });
|
|
v.addEventListener('timeupdate', () => { if (v.currentTime > 0.1) finish(true); });
|
|
h.loadSource(url);
|
|
h.attachMedia(v);
|
|
h.on(Hls.Events.MANIFEST_PARSED, () => v.play().catch(() => {}));
|
|
setTimeout(() => finish(false), 12000); // give it 12s to actually start
|
|
});
|
|
}
|
|
|
|
// ---- PROMO-only channels: loop the promo forever (no live source).
|
|
async function startPromo(ch, token) {
|
|
const video = document.getElementById('video');
|
|
if (!video || token !== playToken) return;
|
|
destroyHls();
|
|
try {
|
|
await loadHls(video, JW_HLS(ch.jw), false);
|
|
if (token !== playToken) return;
|
|
video.loop = true;
|
|
video.play().catch(() => {});
|
|
setNowPlaying(ch, 'promo');
|
|
} catch (e) {
|
|
console.warn('promo load failed:', e?.message);
|
|
}
|
|
}
|
|
|
|
function waitForShaka() {
|
|
return new Promise(resolve => {
|
|
if (window.shaka) return resolve();
|
|
const i = setInterval(() => {
|
|
if (window.shaka) { clearInterval(i); resolve(); }
|
|
}, 50);
|
|
setTimeout(() => { clearInterval(i); resolve(); }, 4000);
|
|
});
|
|
}
|
|
|
|
async function loadCurrentlyPlaying() { /* no-op (no backend) */ }
|
|
async function loadTop10() { /* no-op (no backend) */ }
|
|
|
|
// ===== ARTISTS =====
|
|
function renderArtists() {
|
|
const main = document.getElementById('main');
|
|
main.innerHTML = `
|
|
<div class="box">
|
|
<h1>Artists on FolxPlay</h1>
|
|
<p>Six channels of original folk, schlager and pop production from FOLX NETWORK. Pick a channel below to watch the promo.</p>
|
|
<div class="artistsList">
|
|
${CHANNELS.map(c => `
|
|
<a href="#" data-channel-link="${c.key}">
|
|
<strong style="color:${c.color};">●</strong> ${escapeHtml(c.name)}
|
|
</a>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
main.querySelectorAll('[data-channel-link]').forEach(a => {
|
|
a.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
navigate('/');
|
|
// wait one tick so home renders, then play
|
|
setTimeout(() => playChannel(a.getAttribute('data-channel-link')), 50);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ===== ABOUT (folxnetwork) =====
|
|
function renderAbout() {
|
|
const main = document.getElementById('main');
|
|
main.innerHTML = `
|
|
<div class="box">
|
|
<div>
|
|
<p class="aboutIntro">${ABOUT_INTRO}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ===== POLICIES =====
|
|
function renderPolicies() {
|
|
const main = document.getElementById('main');
|
|
main.innerHTML = `
|
|
<div class="box">
|
|
<h1>Terms of service</h1>
|
|
<p>These Terms of Service ("Account Terms") govern your use of the FolxPlay platform and services, and certain purchases and rentals made through the FolxPlay Channel Store, including videos, audio, graphics, photographs, text and product information ("Original Content") provided by publishers ("Publishers").</p>
|
|
|
|
<h3>Definitions</h3>
|
|
<p><strong>"Channel"</strong> constitutes the entirety of content published, marketed and made available to you by Publishers or on a Publisher's behalf.</p>
|
|
<p><strong>"Original Content"</strong> includes video, audio, graphics, photographs, text and product information.</p>
|
|
<p><strong>"Publisher Service"</strong> means a service Publishers make available through the FolxPlay platform.</p>
|
|
<p><strong>"Member"</strong> refers to anyone that uses the service, also referred to herein as "you".</p>
|
|
|
|
<h3>Changes to Terms of Service</h3>
|
|
<p>FolxPlay may amend the Terms of Service at any time by posting the amended Terms on the FolxPlay website or via the FolxPlay platform, whichever occurs first. By continuing to use the service after the amended Terms are posted, you agree to the amended Terms.</p>
|
|
|
|
<h3>Privacy Policy and Use of Data</h3>
|
|
<p>The FolxPlay Privacy Policy explains how the service collects, uses, transmits, and discloses information you provide.</p>
|
|
|
|
<h3>Fees and Charges</h3>
|
|
<p>FolxPlay does not take part in any commercial activity between Members and Publishers. Fees required to access content will be charged to the payment method on file (credit card or PayPal).</p>
|
|
|
|
<h3>Content Availability</h3>
|
|
<p>The content viewed through the FolxPlay service is solely for your personal and non-commercial enjoyment. Content is protected by copyright and other intellectual property laws and treaties.</p>
|
|
<p>You only have access to channels and content authorized for the country of your residence. Channels and content will vary by geographic location or country.</p>
|
|
|
|
<h3>Children and Age-restricted Content</h3>
|
|
<p>The service contains content which may not be appropriate for children. You are responsible for ensuring that any age-restricted content is not viewed by any person not meeting the applicable age limits without consent from a parent or guardian.</p>
|
|
<p>Age ratings consist of five categories: Kids (All), Older Kids (7+), Teens (13+), Young Adults (16+), Adults (18+).</p>
|
|
|
|
<h3>Service Updates</h3>
|
|
<p>FolxPlay reserves the right to update the service, including bug fixes and updates, at any time and without notice.</p>
|
|
|
|
<h3>Limitation of Liability</h3>
|
|
<p>The FolxPlay service is provided "as is" and "as available" with all faults and without warranty of any kind. To the maximum extent permissible by law, FolxPlay does not guarantee, represent, or warrant that the service will be uninterrupted, secure, virus-free or error-free.</p>
|
|
|
|
<h3>Contact Information</h3>
|
|
<p>If you wish to contact us, please send your correspondence to <a href="mailto:office@folx.tv">office@folx.tv</a>.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ===== PLAYLOG =====
|
|
function renderLog() {
|
|
const main = document.getElementById('main');
|
|
main.innerHTML = `
|
|
<div class="box">
|
|
<h1>Channel PlayLog</h1>
|
|
<p>The full per-channel play history is available on the production site at
|
|
<a href="https://folxplay.tv/log">folxplay.tv/log</a>.</p>
|
|
<p>This preview at <strong>folxplay.tv</strong> ships the look and feel of the site together with the channel promos served from JW Player Cloud.</p>
|
|
<hr>
|
|
<p><a href="/" data-link>← Back to Home</a></p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderNotFound() {
|
|
document.getElementById('main').innerHTML = `
|
|
<div class="box"><h1>Page not found</h1><p>That page does not exist.</p></div>
|
|
`;
|
|
}
|
|
|
|
// ---------- boot ----------
|
|
renderShell();
|
|
render(location.pathname);
|