folxplay-clone/public/app.js

354 lines
15 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' },
{ key: 'zwei', name: 'Zwei Music TV', color: '#f36700', logo: '/logos/zwei_neg_b.png', alt: 'logo of Zwei', jw: 'LZVUn3Qy' },
{ key: 'mt', name: 'Folx Music TV', color: '#dc1d1d', logo: '/logos/mt_neg_b.png', alt: 'logo of Folx Music',jw: 'UYU6dx3Q' },
{ key: 'adria', name: 'Adria Music TV', color: '#30a6d1', logo: '/logos/adria_neg_b.png', alt: 'logo of Adria', jw: 'lI4SC7Do' },
{ key: 'folx-slo', name: 'Folx Slovenija', color: '#a8b539', logo: '/logos/folx_slo_neg_b.png', alt: 'logo of Folx Slovenija', jw: 'aoCWwwSg' },
{ key: 'one-adria', name: 'One Adria Music TV', color: '#a84d7e', logo: '/logos/one_neg_b.png', alt: 'logo of One Adria', jw: 'IGOSgmvW' },
];
const ABOUT = [
{ title: 'FOLX MUSIC TELEVISION', text: 'the whole area of folk music is covered with exclusive content. Over 90% of the content is created by our own FOLX NETWORK production.' },
{ title: 'ONE MUSIC TELEVISION', text: 'covers the area of the new German-speaking and world-famous pop and rock sound production and is additionally enriched with old German and world-famous classics of the 80s, 90s and 00s.' },
{ title: 'ZWEI MUSIC TELEVISION', text: 'covers the area of current pop music as well as old, well-known hits. Daily playlists are enriched with the current pop hits, Latin American music with a touch of reggaeton provides daily variety.' },
{ title: 'ADRIA MUSIC TELEVISION', text: 'is aimed at residents of Germany, Austria and Switzerland with Slovenian, Croatian, Bosnian, Herzegovinian, Serbian, Macedonian or Montenegrin roots. The traditional Dalmatian music, Klape, the most famous pop and rock hits are able to inspire everyone from the Adriatic region.' },
{ title: 'FOLX SLOVENIJA MUSIC TELEVISION',text: 'covers the area of European folk music with exclusive content, 90% of the broadcast content was created or is created in our production FOLX NETWORK.' },
{ title: 'ONE ADRIA MUSIC TELEVISION', text: 'covers the area of new fresh music from the Adriatic (SLO, CRO, BIH, SRB, MKD, MG), world pop and rock production and adds world hits of the 80s, 90s and 00s.' },
];
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 => ({
'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
}[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="/artists" data-link data-route="/artists">Artists</a>
<a href="/folxnetwork" data-link data-route="/folxnetwork">About</a>
<a href="/policies" data-link data-route="/policies">Terms of service</a>
</div>
</div>
</div>
<main id="main"></main>
<footer class="siteFooter">© FOLX NETWORK · folxplay.biba.live</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;
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 showVideoPlayer(ch) {
const poster = ch ? ` poster="${JW_POSTER(ch.jw)}"` : '';
document.getElementById('player-area').innerHTML = `
<div class="split__video_ratio">
<video id="video" controls autoplay loop playsinline${poster}></video>
</div>
`;
}
async function playChannel(key) {
const ch = CHANNELS.find(c => c.key === key);
if (!ch) return;
currentChannel = ch;
// 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);
// Try Shaka first (HLS); fall back to native <video src=mp4-fallback>
await waitForShaka();
const video = document.getElementById('video');
const hlsUrl = JW_HLS(ch.jw);
try {
if (shakaInstance) {
try { await shakaInstance.destroy(); } catch (_) {}
shakaInstance = null;
}
if (window.shaka && shaka.Player.isBrowserSupported()) {
shakaInstance = new shaka.Player(video);
await shakaInstance.load(hlsUrl);
} else {
// Safari / native HLS
video.src = hlsUrl;
}
// When this promo ends, jump to the next channel (cycle through all 6).
// Use `once: true` so the handler fires only for this promo — when we
// start the next channel, a fresh `ended` listener is attached again.
video.loop = false;
video.addEventListener('ended', () => {
const i = CHANNELS.findIndex(c => c.key === currentChannel?.key);
const next = CHANNELS[(i + 1) % CHANNELS.length];
playChannel(next.key);
}, { once: true });
video.play().catch(() => { /* autoplay may be blocked, user can press play */ });
} catch (e) {
console.warn('HLS load failed, falling back to native src:', e?.message);
video.src = hlsUrl;
video.loop = false;
video.addEventListener('ended', () => {
const i = CHANNELS.findIndex(c => c.key === currentChannel?.key);
const next = CHANNELS[(i + 1) % CHANNELS.length];
playChannel(next.key);
}, { once: true });
video.play().catch(() => {});
}
// Show now playing
const now = document.getElementById('currently-playing');
if (now) {
now.style.display = '';
now.innerHTML = `Now playing: <b>${escapeHtml(ch.name)}</b> — promo`;
}
}
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>
${ABOUT.map(c => `
<div class="channel">
<div class="channel__title">${escapeHtml(c.title)}</div>
<p>${escapeHtml(c.text)}</p>
</div>
`).join('')}
<hr>
<a href="/log" data-link>Channel PlayLog</a>
</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.biba.live</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);