stremio/apps/tizen-tv/www/js/app.js
Jos Vooges | STH a0311c031e Add Samsung Tizen TV web client.
Focus-friendly VMC shell for Tizen with live TV and playback against the master API.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 19:16:30 +02:00

1346 lines
No EOL
42 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function (global) {
/* Zelfde set als Android ProfileAvatars.kt — emoji + naam (niet alleen letter) */
var AVATARS = {
fox: "🦊",
panda: "🐼",
robot: "🤖",
alien: "👽",
ninja: "🥷",
pirate: "🏴‍☠️",
wizard: "🧙",
dino: "🦕",
cat: "🐱",
rocket: "🚀",
dog: "🐶",
owl: "🦉",
star: "⭐",
game: "🎮",
music: "🎵",
kids: "🧒",
lion: "🦁",
};
function avatarEmoji(id) {
return AVATARS[id] || "🙂";
}
var state = {
screen: "pair",
tab: "home",
iptvEnabled: false,
home: null,
heroItem: null,
spotlight: [],
spotlightIndex: 0,
heroTimer: null,
detail: null,
detailMeta: null,
pendingProfile: null,
pin: "",
pollTimer: null,
browseSkip: {},
searchQ: "",
liveMode: false,
liveCtx: null,
progressCtx: null,
progressTimer: null,
mediaFileId: null,
playerMenu: null,
};
function $(id) { return document.getElementById(id); }
function escapeHtml(s) {
return String(s || "")
.replace(/&/g, "&amp;").replace(/</g, "&lt;")
.replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function showLoading(on) {
var el = $("loading");
if (!el) return;
if (on) el.classList.remove("hidden");
else el.classList.add("hidden");
}
function toast(msg) {
var el = $("toast");
if (!el) return;
el.textContent = msg;
el.classList.remove("hidden");
clearTimeout(toast._t);
toast._t = setTimeout(function () { el.classList.add("hidden"); }, 3200);
}
function showScreen(name) {
state.screen = name;
["pair", "profiles", "main", "detail", "player"].forEach(function (s) {
var el = $("screen-" + s);
if (!el) return;
if (s === name) el.classList.remove("hidden");
else el.classList.add("hidden");
});
setTimeout(function () {
if (window.McFocus) McFocus.focusFirst($("screen-" + name));
}, 40);
}
function poster(item) { return item.posterUrl || item.backdropUrl || ""; }
/* ---------- Pairing ---------- */
function startPairing() {
showScreen("pair");
refreshCode();
}
function refreshCode() {
clearInterval(state.pollTimer);
$("pair-status").textContent = "Code ophalen…";
McApi.deviceCode().then(function (data) {
$("pair-code").textContent = data.code;
$("pair-link").textContent = data.linkUrl || McApi.BASE + "/link";
$("pair-status").textContent = "Wachten op koppeling…";
var interval = (data.pollIntervalSeconds || 2) * 1000;
state.pollTimer = setInterval(function () {
McApi.devicePoll(data.code).then(function (res) {
if (res.status === "linked" && res.refreshToken) {
clearInterval(state.pollTimer);
McApi.setToken(res.refreshToken);
toast("TV gekoppeld");
afterAuth();
} else if (res.status === "expired") {
clearInterval(state.pollTimer);
$("pair-status").textContent = "Code verlopen";
}
}).catch(function () {});
}, interval);
}).catch(function (e) {
$("pair-status").textContent = e.message || "Code ophalen mislukt";
});
}
/* ---------- Profiles ---------- */
function afterAuth() {
showLoading(true);
McApi.me().then(function (me) {
state.iptvEnabled = !!(me.iptv && me.iptv.enabled);
var tvNav = $("nav-tv");
if (tvNav) {
if (state.iptvEnabled) tvNav.classList.remove("hidden");
else tvNav.classList.add("hidden");
}
return McApi.listProfiles();
}).then(function (data) {
showLoading(false);
if (data.activeProfileId) {
var active = (data.profiles || []).filter(function (p) {
return p.id === data.activeProfileId;
})[0];
if (active) McApi.setProfileCache(active);
enterMain();
} else {
renderProfiles(data.profiles || []);
}
}).catch(function (e) {
showLoading(false);
if (e.status === 401) { McApi.clearSession(); startPairing(); return; }
toast(e.message || "Sessie mislukt");
startPairing();
});
}
function renderProfiles(profiles) {
showScreen("profiles");
$("pin-panel").classList.add("hidden");
var grid = $("profile-grid");
grid.innerHTML = "";
profiles.forEach(function (p, idx) {
var btn = document.createElement("button");
btn.className = "profile-tile";
btn.type = "button";
btn.innerHTML =
'<div class="avatar">' + avatarEmoji(p.avatarId) + "</div>" +
'<div class="name">' + escapeHtml(p.name) + "</div>" +
'<div class="badge">' +
escapeHtml(p.isKids ? "Kids" : p.isOwner ? "Hoofd" : p.hasPin ? "PIN" : "") +
"</div>";
btn.onclick = function () {
if (p.hasPin) openPin(p);
else selectProfile(p, null);
};
grid.appendChild(btn);
if (idx === 0) setTimeout(function () { btn.focus(); }, 40);
});
}
function openPin(profile) {
state.pendingProfile = profile;
state.pin = "";
$("pin-panel").classList.remove("hidden");
$("pin-title").textContent = "PIN voor " + profile.name;
renderPinDots();
var pad = $("pin-pad");
pad.innerHTML = "";
["1","2","3","4","5","6","7","8","9","⌫","0","C"].forEach(function (k) {
var b = document.createElement("button");
b.className = "num-key";
b.type = "button";
b.textContent = k;
b.onclick = function () {
if (k === "⌫") state.pin = state.pin.slice(0, -1);
else if (k === "C") state.pin = "";
else if (state.pin.length < 4) state.pin += k;
renderPinDots();
if (state.pin.length === 4) selectProfile(state.pendingProfile, state.pin);
};
pad.appendChild(b);
});
McFocus.focusFirst($("pin-pad"));
}
function renderPinDots() {
var html = "";
for (var i = 0; i < 4; i++) html += '<span class="' + (i < state.pin.length ? "on" : "") + '"></span>';
$("pin-dots").innerHTML = html;
}
function selectProfile(profile, pin) {
showLoading(true);
McApi.selectProfile(profile.id, pin).then(function (res) {
showLoading(false);
McApi.setProfileCache(res.profile || profile);
$("pin-panel").classList.add("hidden");
enterMain();
}).catch(function (e) {
showLoading(false);
state.pin = "";
renderPinDots();
toast(e.message || "PIN onjuist");
});
}
function switchProfile() {
showLoading(true);
McApi.clearProfile().then(function () {
McApi.setProfileCache(null);
return McApi.listProfiles();
}).then(function (d) {
showLoading(false);
renderProfiles(d.profiles || []);
}).catch(function () {
showLoading(false);
McApi.listProfiles().then(function (d) { renderProfiles(d.profiles || []); });
});
}
/* ---------- Main hub ---------- */
function enterMain() {
showScreen("main");
updateNavProfile();
setTab(state.tab || "home");
}
function updateNavProfile() {
var p = McApi.getProfileCache();
var el = $("nav-profile");
if (!el) return;
if (!p) {
el.innerHTML = '<span class="nav-profile-name">Profiel</span>';
return;
}
el.innerHTML =
'<span class="nav-avatar" aria-hidden="true">' +
avatarEmoji(p.avatarId) +
'</span><span class="nav-profile-name">' +
escapeHtml(p.name) +
"</span>";
}
function setTab(tab) {
state.tab = tab;
var topnav = $("topnav");
var body = $("main-body");
if (topnav) {
if (tab === "home") topnav.classList.add("overlay");
else topnav.classList.remove("overlay");
}
if (body) {
if (tab === "home") body.classList.add("home-mode");
else body.classList.remove("home-mode");
}
if (tab !== "home") stopHeroRotate();
var tabs = document.querySelectorAll(".nav-tab");
for (var i = 0; i < tabs.length; i++) {
var t = tabs[i];
var on = t.getAttribute("data-tab") === tab;
if (on) t.classList.add("active");
else t.classList.remove("active");
}
["home","movies","series","tv","search","settings"].forEach(function (name) {
var pane = $("tab-" + name);
if (!pane) return;
if (name === tab) pane.classList.remove("hidden");
else pane.classList.add("hidden");
});
if (tab === "home") loadHome();
else if (tab === "movies") loadBrowse("movies", "Films", ["movies","recent-movies","4k-movies"]);
else if (tab === "series") loadBrowse("series", "Series", ["series","recent-series"]);
else if (tab === "tv") McLive.mount($("tab-tv"));
else if (tab === "search") renderSearch();
else if (tab === "settings") renderSettings();
}
/* ---------- Home (Android HomeScreen / SpotlightHero) ---------- */
function stopHeroRotate() {
if (state.heroTimer) {
clearInterval(state.heroTimer);
state.heroTimer = null;
}
}
function loadHome() {
showLoading(true);
McApi.home().then(function (home) {
showLoading(false);
state.home = home;
renderHome(home);
}).catch(function (e) {
showLoading(false);
if (e.status === 428 || e.code === "PROFILE_REQUIRED") {
switchProfile();
return;
}
if (e.status === 401) { McApi.clearSession(); startPairing(); return; }
toast(e.message || "Home laden mislukt");
});
}
function buildSpotlight(home) {
var list = (home && home.spotlight) || [];
if (list.length) return list.slice();
var fallback = [];
if (home && home.continueWatching && home.continueWatching[0]) {
fallback.push(home.continueWatching[0]);
}
if (home && home.shelves) {
for (var i = 0; i < home.shelves.length && fallback.length < 5; i++) {
var items = home.shelves[i].items || [];
if (items[0]) fallback.push(items[0]);
}
}
if (!fallback.length) {
fallback.push({ type: "movie", id: "", title: "Welkom bij VMC" });
}
return fallback;
}
function heroTypeLabel(item) {
if (!item) return "";
if (item.type === "series" || item.type === "episode") return "Serie";
return "Film";
}
function paintHero() {
var heroBg = $("hero-bg");
var content = $("hero-content");
if (!content) return;
var items = state.spotlight;
var idx = state.spotlightIndex;
if (!items.length) return;
if (idx < 0 || idx >= items.length) idx = 0;
var hero = items[idx];
state.heroItem = hero;
if (heroBg) {
var url = hero && (hero.backdropUrl || hero.posterUrl)
? String(hero.backdropUrl || hero.posterUrl).replace(/'/g, "%27")
: "";
if (url) {
heroBg.style.backgroundImage = "url('" + url + "')";
heroBg.style.backgroundSize = "cover";
heroBg.style.backgroundPosition = "center top";
heroBg.style.backgroundRepeat = "no-repeat";
heroBg.classList.remove("empty");
} else {
heroBg.style.backgroundImage = "none";
heroBg.classList.add("empty");
}
}
var badgeHtml = hero && hero.badge
? '<div class="hero-badge">' + escapeHtml(String(hero.badge).toUpperCase()) + "</div>"
: "";
var titleBlock;
if (hero && hero.logoUrl) {
titleBlock =
'<div class="hero-logo" style="background-image:url(\'' +
String(hero.logoUrl).replace(/'/g, "%27") +
"')\"></div>";
} else {
titleBlock =
'<div class="hero-title">' +
escapeHtml(hero && hero.title ? hero.title : "Welkom bij VMC") +
"</div>";
}
var metaParts = [];
if (hero && hero.year) metaParts.push(String(hero.year));
if (hero && hero.id) metaParts.push(heroTypeLabel(hero));
var metaHtml = metaParts.length
? '<div class="hero-meta">' + escapeHtml(metaParts.join(" · ")) + "</div>"
: "";
var dots = "";
if (items.length > 1) {
dots = '<div class="hero-dots">';
for (var d = 0; d < items.length; d++) {
dots += '<span class="hero-dot' + (d === idx ? " on" : "") + '"></span>';
}
dots += "</div>";
}
var hadPlayFocus = document.activeElement && document.activeElement.id === "hero-play";
var hadInfoFocus = document.activeElement && document.activeElement.id === "hero-info";
content.innerHTML =
badgeHtml +
titleBlock +
metaHtml +
'<div class="hero-actions">' +
'<button class="btn primary focusable" id="hero-play">Afspelen</button>' +
'<button class="btn focusable" id="hero-info">Meer info</button>' +
"</div>" +
dots;
var playBtn = $("hero-play");
var infoBtn = $("hero-info");
if (playBtn) playBtn.onclick = playHero;
if (infoBtn) {
infoBtn.onclick = function () {
openDetail(state.heroItem);
};
}
if (hadPlayFocus && playBtn) playBtn.focus();
else if (hadInfoFocus && infoBtn) infoBtn.focus();
}
function startHeroRotate() {
stopHeroRotate();
if (state.spotlight.length <= 1) return;
state.heroTimer = setInterval(function () {
if (state.tab !== "home" || state.screen !== "main") return;
state.spotlightIndex = (state.spotlightIndex + 1) % state.spotlight.length;
paintHero();
}, 7000);
}
function renderHome(home) {
var pane = $("tab-home");
state.spotlight = buildSpotlight(home);
state.spotlightIndex = 0;
state.heroItem = state.spotlight[0] || null;
/* Full-bleed bg-laag + zachte scrims (geen zware zwarte platen die de art tot een strip maken) */
pane.innerHTML =
'<div class="hero" id="home-hero">' +
'<div class="hero-bg" id="hero-bg"></div>' +
'<div class="hero-scrim"></div>' +
'<div class="hero-content" id="hero-content"></div>' +
"</div>" +
'<div class="rows" id="home-rows"></div>';
paintHero();
startHeroRotate();
var rows = $("home-rows");
function addRow(title, items) {
if (!items || !items.length) return;
var row = document.createElement("div");
row.className = "row";
row.innerHTML =
'<div class="row-head"><div class="row-title">' +
escapeHtml(title) +
"</div></div>";
var scroller = document.createElement("div");
scroller.className = "row-scroller";
items.forEach(function (item) {
scroller.appendChild(makeTile(item));
});
row.appendChild(scroller);
rows.appendChild(row);
}
if (home.continueWatching && home.continueWatching.length) {
addRow("Verder kijken", home.continueWatching);
}
(home.shelves || []).forEach(function (shelf) {
addRow(shelf.title || shelf.name || "Rij", shelf.items || []);
});
var playBtn = $("hero-play");
if (playBtn) playBtn.focus();
else McFocus.focusFirst(pane);
}
function makeTile(item) {
var tile = document.createElement("button");
tile.className = "tile focusable";
tile.type = "button";
var url = poster(item);
var progress = 0;
if (item.positionMs && item.durationMs && item.durationMs > 0) {
progress = Math.min(100, Math.round((item.positionMs / item.durationMs) * 100));
}
var letter = (item.title || "?").charAt(0).toUpperCase();
var posterHtml =
'<div class="poster">' +
'<div class="poster-ph">' + escapeHtml(letter) + "</div>" +
(url
? '<div class="poster-img" style="background-image:url(\'' +
String(url).replace(/'/g, "%27") +
"')\"></div>"
: "") +
(progress > 0
? '<div class="poster-bar"><i style="width:' + progress + '%"></i></div>'
: "") +
"</div>" +
'<div class="tile-title">' + escapeHtml(item.title || "") + "</div>";
tile.innerHTML = posterHtml;
tile.onclick = function () {
if (item.mediaFileId && item.type === "episode") {
playMedia(item.mediaFileId, item.title, {
mediaType: "EPISODE",
episodeId: item.id,
seriesId: item.seriesId,
startMs: item.positionMs || 0,
});
} else {
openDetail(item);
}
};
return tile;
}
function playHero() {
var item = state.heroItem;
if (!item) return;
if (item.mediaFileId) {
playMedia(item.mediaFileId, item.title, {
mediaType: item.type === "episode" ? "EPISODE" : "MOVIE",
movieId: item.type === "movie" ? item.id : null,
episodeId: item.type === "episode" ? item.id : null,
seriesId: item.seriesId || null,
startMs: item.positionMs || 0,
});
return;
}
openDetail(item);
}
/* ---------- Browse ---------- */
function loadBrowse(paneKey, title, catalogIds) {
var pane = $("tab-" + (paneKey === "movies" ? "movies" : "series"));
var primary = catalogIds[0];
pane.innerHTML =
'<div class="pane-title">' + escapeHtml(title) + "</div>" +
'<div class="subnav" id="browse-sub-' + paneKey + '"></div>' +
'<div class="grid" id="browse-grid-' + paneKey + '"></div>';
var labels = {
movies: "Alles",
"recent-movies": "Recent",
"4k-movies": "4K",
series: "Alles",
"recent-series": "Recent",
};
var sub = $("browse-sub-" + paneKey);
catalogIds.forEach(function (id, i) {
var chip = document.createElement("button");
chip.className = "cat-chip focusable" + (i === 0 ? " active" : "");
chip.type = "button";
chip.setAttribute("data-cat", id);
chip.innerHTML = escapeHtml(labels[id] || id) + '<span class="chip-line"></span>';
chip.onclick = function () {
var chips = sub.querySelectorAll(".cat-chip");
for (var j = 0; j < chips.length; j++) chips[j].classList.remove("active");
chip.classList.add("active");
fillCatalog(paneKey, id, { keepChipFocus: true, focusChip: chip });
};
sub.appendChild(chip);
});
fillCatalog(paneKey, primary, { focusGrid: true });
}
function fillCatalog(paneKey, catalogId, opts) {
opts = opts || {};
var grid = $("browse-grid-" + paneKey);
if (!grid) return;
showLoading(true);
McApi.catalog(catalogId, 0, 60).then(function (page) {
showLoading(false);
grid.innerHTML = "";
var items = page.items || [];
if (!items.length) {
grid.innerHTML = '<p class="muted" style="font-size:16px;padding:20px">Geen items</p>';
if (opts.focusChip) opts.focusChip.focus();
return;
}
items.forEach(function (item) { grid.appendChild(makeTile(item)); });
if (opts.keepChipFocus && opts.focusChip) {
opts.focusChip.focus();
} else if (opts.focusGrid !== false) {
McFocus.focusFirst(grid);
}
}).catch(function (e) {
showLoading(false);
toast(e.message || "Catalogus mislukt");
if (opts.focusChip) opts.focusChip.focus();
});
}
/* ---------- Search ---------- */
function renderSearch() {
var pane = $("tab-search");
pane.innerHTML =
'<div class="search-wrap">' +
'<div class="pane-title">Zoeken</div>' +
'<div class="search-query" id="search-q">' + escapeHtml(state.searchQ || " ") + "</div>" +
'<div class="keyboard" id="search-kb"></div>' +
'<div class="grid" id="search-grid" style="padding-left:0;padding-right:0"></div>' +
"</div>";
var kb = $("search-kb");
var keys = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split("");
keys.forEach(function (k) {
var b = document.createElement("button");
b.className = "kb-key focusable";
b.type = "button";
b.textContent = k;
b.onclick = function () {
state.searchQ = (state.searchQ || "") + k;
$("search-q").textContent = state.searchQ;
runSearch();
};
kb.appendChild(b);
});
[["␣", " "], ["⌫", "back"], ["Wis", "clear"]].forEach(function (pair) {
var b = document.createElement("button");
b.className = "kb-key wide focusable";
b.type = "button";
b.textContent = pair[0];
b.onclick = function () {
if (pair[1] === "back") state.searchQ = (state.searchQ || "").slice(0, -1);
else if (pair[1] === "clear") state.searchQ = "";
else state.searchQ = (state.searchQ || "") + " ";
$("search-q").textContent = state.searchQ || " ";
runSearch();
};
kb.appendChild(b);
});
if (state.searchQ) runSearch();
McFocus.focusFirst(kb);
}
function runSearch() {
clearTimeout(runSearch._t);
runSearch._t = setTimeout(function () {
var q = (state.searchQ || "").trim();
var grid = $("search-grid");
if (!grid) return;
if (q.length < 2) { grid.innerHTML = ""; return; }
McApi.search(q).then(function (res) {
grid.innerHTML = "";
var items = res.results || res || [];
items.forEach(function (item) { grid.appendChild(makeTile(item)); });
}).catch(function (e) { toast(e.message || "Zoeken mislukt"); });
}, 250);
}
/* ---------- Settings ---------- */
function renderSettings() {
var pane = $("tab-settings");
var p = McApi.getProfileCache();
pane.innerHTML =
'<div class="settings-wrap">' +
'<div class="pane-title">Instellingen</div>' +
'<p class="muted" style="margin-bottom:20px">Profiel: ' +
escapeHtml(p ? p.name : "—") + "</p>" +
'<button class="settings-row focusable" id="set-switch">Profiel wisselen</button>' +
'<button class="settings-row focusable" id="set-logout">Uitloggen / apparaat ontkoppelen</button>' +
"</div>";
$("set-switch").onclick = switchProfile;
$("set-logout").onclick = function () {
McApi.clearSession();
toast("Ontkoppeld");
startPairing();
};
McFocus.focusFirst(pane);
}
/* ---------- Detail ---------- */
function openDetail(item) {
if (!item) return;
var type = item.type;
var id = item.id;
if (type === "episode" && item.seriesId) {
type = "series";
id = item.seriesId;
}
showLoading(true);
var req = type === "series" ? McApi.series(id) : McApi.movie(id);
req.then(function (detail) {
showLoading(false);
state.detail = detail;
renderDetail(detail);
}).catch(function (e) {
showLoading(false);
toast(e.message || "Detail laden mislukt");
});
}
function renderDetail(d) {
showScreen("detail");
$("detail-bg").style.backgroundImage = d.backdropUrl || d.posterUrl
? 'url("' + (d.backdropUrl || d.posterUrl) + '")' : "";
var logoEl = $("detail-logo");
var titleEl = $("detail-title");
if (logoEl && d.logoUrl) {
logoEl.classList.remove("hidden");
logoEl.style.backgroundImage = "url('" + String(d.logoUrl).replace(/'/g, "%27") + "')";
if (titleEl) titleEl.classList.add("hidden");
} else {
if (logoEl) {
logoEl.classList.add("hidden");
logoEl.style.backgroundImage = "none";
}
if (titleEl) {
titleEl.classList.remove("hidden");
titleEl.textContent = d.title || "";
}
}
$("detail-meta").textContent = [
d.year,
d.type === "series" ? "Serie" : "Film",
(d.genres || []).slice(0, 3).join(", "),
].filter(Boolean).join(" · ");
$("detail-overview").textContent = d.overview || "";
var actions = $("detail-actions");
var episodes = $("detail-episodes");
actions.innerHTML = "";
episodes.innerHTML = "";
if (d.type === "movie") {
var files = (d.mediaFiles || []).slice().sort(function (a, b) {
var ao = a.nodeOnline === false ? 1 : 0;
var bo = b.nodeOnline === false ? 1 : 0;
return ao - bo;
});
if (!files.length) {
actions.innerHTML = '<span class="muted">Geen afspeelbare bestanden</span>';
} else {
files.forEach(function (f, i) {
var btn = document.createElement("button");
var online = f.nodeOnline !== false;
btn.className = "btn focusable" + (i === 0 && online ? " primary" : "");
btn.type = "button";
btn.textContent = f.label || f.resolution || "Afspelen";
if (!online) {
btn.textContent += " (offline)";
btn.disabled = true;
}
btn.onclick = function () {
playMedia(f.id, d.title, { mediaType: "MOVIE", movieId: d.id });
};
actions.appendChild(btn);
});
}
} else {
(d.seasons || []).forEach(function (season) {
var h = document.createElement("h3");
h.style.margin = "18px 0 10px";
h.textContent = "Seizoen " + season.seasonNumber;
episodes.appendChild(h);
(season.episodes || []).forEach(function (ep) {
var btn = document.createElement("button");
btn.className = "ep focusable" + (ep.available && ep.mediaFileId ? "" : " disabled");
btn.type = "button";
btn.disabled = !(ep.available && ep.mediaFileId);
btn.innerHTML =
'<div class="ep-title">Afl. ' + ep.episodeNumber + " · " + escapeHtml(ep.title || "") + "</div>" +
'<div class="ep-meta">' + escapeHtml((ep.overview || "").slice(0, 140)) + "</div>";
btn.onclick = function () {
playMedia(ep.mediaFileId, d.title + " S" + ep.seasonNumber + "E" + ep.episodeNumber, {
mediaType: "EPISODE",
episodeId: ep.id,
seriesId: d.id,
});
};
episodes.appendChild(btn);
});
});
}
McFocus.focusFirst($("screen-detail"));
}
/* ---------- Playback ---------- */
var playBusy = false;
function closePlayerMenu() {
state.playerMenu = null;
var menu = $("player-menu");
if (menu) menu.classList.add("hidden");
McPlayer.showUi(true);
}
function menuRow(label, sub, onClick, active) {
var b = document.createElement("button");
b.type = "button";
b.className = "player-menu-row focusable" + (active ? " is-active" : "");
var mark = document.createElement("span");
mark.className = "player-menu-mark";
b.appendChild(mark);
var main = document.createElement("span");
main.className = "player-menu-main";
main.textContent = label || "Ondertitel";
b.appendChild(main);
if (sub) {
var s = document.createElement("span");
s.className = "muted";
s.textContent = sub;
b.appendChild(s);
}
b.onclick = onClick;
return b;
}
function openPlayerMenu(kind) {
state.playerMenu = kind;
McPlayer.showUi(true);
var menu = $("player-menu");
var title = $("player-menu-title");
var list = $("player-menu-list");
if (!menu || !list) return;
menu.classList.remove("hidden");
list.innerHTML = "";
if (kind === "audio") {
title.textContent = "Audio";
var tracks = McPlayer.listAudioTracks();
if (!tracks.length) {
list.appendChild(menuRow("Geen audio-sporen", null, function () {}));
} else {
tracks.forEach(function (t) {
list.appendChild(
menuRow(t.label, null, function () {
McPlayer.selectAudio(t.index);
closePlayerMenu();
toast("Audio: " + t.label);
})
);
});
}
} else if (kind === "text") {
title.textContent = "Ondertiteling";
list.appendChild(
menuRow("Uit", null, function () {
McPlayer.disableText();
closePlayerMenu();
})
);
if (state.mediaFileId && !state.liveMode) {
list.appendChild(
menuRow("Zoeken op OpenSubtitles…", null, function () {
openOsMenu();
})
);
}
var texts = McPlayer.listTextTracks();
if (!texts.length) {
list.appendChild(menuRow("Geen ingebouwde ondertitels", null, function () {}));
} else {
texts.forEach(function (t) {
if (t.type === "EXTERNAL") {
list.appendChild(menuRow(t.label + " (actief)", null, function () {
closePlayerMenu();
}));
return;
}
list.appendChild(
menuRow(t.label, null, function () {
McPlayer.selectText(t.index);
closePlayerMenu();
toast("Ondertitel: " + t.label);
})
);
});
}
} else if (kind === "os") {
title.textContent = "OpenSubtitles";
list.appendChild(menuRow("Zoeken…", null, function () {}));
}
setTimeout(function () {
McFocus.focusFirst(list);
}, 40);
}
function openOsMenu() {
if (!state.mediaFileId) {
toast("Geen mediabestand voor ondertitels");
return;
}
state.playerMenu = "os";
var menu = $("player-menu");
var title = $("player-menu-title");
var list = $("player-menu-list");
if (!menu || !list) return;
menu.classList.remove("hidden");
title.textContent = "OpenSubtitles";
list.innerHTML = "";
list.appendChild(menuRow("Zoeken…", null, function () {}));
McApi.searchSubtitles(state.mediaFileId)
.then(function (res) {
list.innerHTML = "";
if (!res.configured) {
list.appendChild(menuRow("OpenSubtitles staat uit in de admin", null, function () {}));
} else if (!(res.results || []).length) {
list.appendChild(menuRow("Geen ondertitels gevonden", null, function () {}));
} else {
(res.results || []).forEach(function (hit) {
var lang =
McPlayer.langLabel(hit.language || hit.lang || "") ||
String(hit.language || hit.lang || "").toUpperCase() ||
"Ondertitel";
var bits = [];
var release = hit.release || hit.fileName || "";
if (release) bits.push(String(release).slice(0, 48));
if (hit.hearingImpaired) bits.push("SDH");
if (hit.source) bits.push(String(hit.source).toUpperCase());
if (hit.downloadCount) bits.push(hit.downloadCount + "×");
list.appendChild(
menuRow(lang, bits.join(" · ") || null, function () {
pickOsHit(hit);
})
);
});
}
list.appendChild(
menuRow("Terug", null, function () {
openPlayerMenu("text");
})
);
McFocus.focusFirst(list);
})
.catch(function (e) {
list.innerHTML = "";
list.appendChild(menuRow(e.message || "Zoeken mislukt", null, function () {}));
list.appendChild(
menuRow("Terug", null, function () {
openPlayerMenu("text");
})
);
McFocus.focusFirst(list);
});
}
function pickOsHit(hit) {
showLoading(true);
McApi.fetchSubtitle(hit.fileId, hit.source)
.then(function (fetched) {
showLoading(false);
var label = [
McPlayer.langLabel(hit.language || ""),
hit.release ? String(hit.release).slice(0, 40) : "",
]
.filter(Boolean)
.join(" · ");
fetched.label = label;
var n = McPlayer.applyExternalSubtitle(fetched);
closePlayerMenu();
toast("Ondertitel geladen (" + n + ")");
})
.catch(function (e) {
showLoading(false);
toast(e.message || "Download mislukt");
});
}
function playMedia(mediaFileId, title, meta, attempt) {
attempt = attempt || 1;
if (!mediaFileId) { toast("Geen mediabestand"); return; }
if (playBusy && attempt === 1) return;
playBusy = true;
showLoading(true);
McApi.play(mediaFileId).then(function (res) {
playBusy = false;
showLoading(false);
if (!res.streamUrl) throw new Error("Geen stream-URL");
state.liveMode = false;
state.liveCtx = null;
state.mediaFileId = mediaFileId;
state.progressCtx = meta || null;
state.detailMeta = meta;
closePlayerMenu();
showScreen("player");
var acts = $("player-actions");
if (acts) acts.classList.remove("hidden");
McPlayer.playUrl(res.streamUrl, title || "");
startProgressLoop();
}).catch(function (e) {
var msg = (e && e.message) || "";
var code = e && e.code;
var retriable =
code === "NODE_OFFLINE" ||
/register session|node|offline|503/i.test(msg);
if (retriable && attempt < 3) {
toast("Mediaserver bezet — opnieuw proberen (" + attempt + "/2)…");
setTimeout(function () {
playBusy = false;
playMedia(mediaFileId, title, meta, attempt + 1);
}, 1200 * attempt);
return;
}
playBusy = false;
showLoading(false);
if (retriable) {
toast("Mediaserver reageert niet. Kies andere kwaliteit of probeer later.");
} else {
toast(msg || "Afspelen mislukt");
}
});
}
function openLivePlayer(ctx) {
state.liveMode = true;
state.liveCtx = ctx;
state.progressCtx = null;
state.mediaFileId = null;
stopProgressLoop();
closePlayerMenu();
showScreen("player");
var acts = $("player-actions");
if (acts) acts.classList.add("hidden");
McPlayer.playUrl(ctx.url, ctx.title || "Live TV");
}
function startProgressLoop() {
stopProgressLoop();
state.progressTimer = setInterval(function () {
postProgress(false);
}, 20000);
}
function stopProgressLoop() {
if (state.progressTimer) clearInterval(state.progressTimer);
state.progressTimer = null;
}
function postProgress(force) {
var ctx = state.progressCtx;
if (!ctx || state.liveMode) return;
var pos = McPlayer.getPositionMs ? McPlayer.getPositionMs() : 0;
var dur = McPlayer.getDurationMs ? McPlayer.getDurationMs() : 0;
if (!force && pos < 5000) return;
var body = {
mediaType: ctx.mediaType || "MOVIE",
positionMs: pos,
durationMs: dur || 0,
};
if (ctx.movieId) body.movieId = ctx.movieId;
if (ctx.episodeId) body.episodeId = ctx.episodeId;
if (ctx.seriesId) body.seriesId = ctx.seriesId;
McApi.progress(body).catch(function () {});
}
function leavePlayer() {
closePlayerMenu();
postProgress(true);
stopProgressLoop();
McPlayer.stop();
state.liveMode = false;
state.mediaFileId = null;
if (state.detail) showScreen("detail");
else {
showScreen("main");
setTab(state.tab || "home");
}
}
function liveZap(delta) {
if (!state.liveMode) return;
var ch = McLive.zap(delta);
if (!ch) return;
showLoading(true);
McApi.iptvPlay(ch.streamId).then(function (info) {
showLoading(false);
state.liveCtx = {
title: info.name || ch.name,
url: info.streamUrl,
fallbackUrl: info.fallbackStreamUrl,
};
McPlayer.playUrl(info.streamUrl, state.liveCtx.title);
}).catch(function (e) {
showLoading(false);
toast(e.message || "Zap mislukt");
});
}
/* ---------- Keys ---------- */
function onKey(e) {
var code = e.keyCode || e.which;
var K = McFocus.keys;
if (state.screen === "player") {
var uiOn = McPlayer.isUiVisible && McPlayer.isUiVisible();
if (state.playerMenu) {
if (code === K.BACK || code === K.ESC) {
e.preventDefault();
if (state.playerMenu === "os") openPlayerMenu("text");
else closePlayerMenu();
return;
}
if (code === K.LEFT) { e.preventDefault(); McFocus.move("left"); return; }
if (code === K.RIGHT) { e.preventDefault(); McFocus.move("right"); return; }
if (code === K.UP) { e.preventDefault(); McFocus.move("up"); return; }
if (code === K.DOWN) { e.preventDefault(); McFocus.move("down"); return; }
if (code === K.ENTER) { return; }
return;
}
/* Controls zichtbaar: navigeren, niet spoelen */
if (uiOn && !state.liveMode) {
if (code === K.BACK || code === K.ESC) {
e.preventDefault();
McPlayer.showUi(false);
return;
}
if (code === K.LEFT) {
e.preventDefault();
McPlayer.showUi(true);
McFocus.move("left");
return;
}
if (code === K.RIGHT) {
e.preventDefault();
McPlayer.showUi(true);
McFocus.move("right");
return;
}
if (code === K.UP || code === K.DOWN) {
e.preventDefault();
McPlayer.showUi(true);
McFocus.move(code === K.UP ? "up" : "down");
return;
}
if (code === K.ENTER) {
var ae = document.activeElement;
if (ae && ae.className && String(ae.className).indexOf("pctrl") >= 0) {
return; /* laat click/default op knop */
}
e.preventDefault();
McPlayer.togglePause();
return;
}
if (code === K.PLAYPAUSE || code === K.PAUSE || code === K.PLAY) {
e.preventDefault();
McPlayer.togglePause();
return;
}
if (code === K.STOP) {
e.preventDefault();
leavePlayer();
return;
}
return;
}
/* Controls verborgen (of live): spoelen / zappen / UI tonen */
if (code === K.BACK || code === K.ESC || code === K.STOP) {
e.preventDefault(); leavePlayer(); return;
}
if (code === K.ENTER || code === K.PLAYPAUSE || code === K.PAUSE || code === K.PLAY) {
e.preventDefault();
if (!uiOn) {
McPlayer.showUi(true, { focusPlay: true });
return;
}
McPlayer.togglePause();
return;
}
if (code === K.RIGHT || code === K.FF) {
e.preventDefault();
if (state.liveMode) liveZap(1);
else McPlayer.seek(10000);
return;
}
if (code === K.LEFT || code === K.REW) {
e.preventDefault();
if (state.liveMode) liveZap(-1);
else McPlayer.seek(-10000);
return;
}
if (state.liveMode && (code === K.UP || code === K.DOWN)) {
e.preventDefault();
liveZap(code === K.UP ? -1 : 1);
return;
}
if (!state.liveMode && (code === K.UP || code === K.DOWN)) {
e.preventDefault();
McPlayer.showUi(true, { focusPlay: true });
return;
}
McPlayer.showUi(true, { focusPlay: true });
return;
}
if (code === K.BACK || code === K.ESC) {
e.preventDefault();
if (state.screen === "detail") {
showScreen("main");
return;
}
if (state.screen === "profiles" && !$("pin-panel").classList.contains("hidden")) {
$("pin-panel").classList.add("hidden");
return;
}
if (state.screen === "main" && state.tab !== "home") {
setTab("home");
return;
}
return;
}
if (code === K.LEFT) { e.preventDefault(); McFocus.move("left"); }
else if (code === K.RIGHT) { e.preventDefault(); McFocus.move("right"); }
else if (code === K.UP) { e.preventDefault(); McFocus.move("up"); }
else if (code === K.DOWN) { e.preventDefault(); McFocus.move("down"); }
}
function bind() {
$("pair-refresh").onclick = refreshCode;
$("detail-back").onclick = function () { showScreen("main"); };
$("pin-cancel").onclick = function () {
$("pin-panel").classList.add("hidden");
state.pendingProfile = null;
state.pin = "";
};
$("nav-profile").onclick = switchProfile;
var tabs = document.querySelectorAll(".nav-tab");
for (var i = 0; i < tabs.length; i++) {
(function (tab) {
tab.onclick = function () {
setTab(tab.getAttribute("data-tab"));
};
})(tabs[i]);
}
var btnAudio = $("btn-audio");
var btnSubs = $("btn-subs");
var btnClose = $("player-menu-close");
var btnPlay = $("btn-play");
var btnRew = $("btn-rew");
var btnFf = $("btn-ff");
if (btnAudio) {
btnAudio.onclick = function () {
openPlayerMenu("audio");
};
}
if (btnSubs) {
btnSubs.onclick = function () {
openPlayerMenu("text");
};
}
if (btnClose) {
btnClose.onclick = function () {
closePlayerMenu();
};
}
if (btnPlay) {
btnPlay.onclick = function () {
McPlayer.togglePause();
};
}
if (btnRew) {
btnRew.onclick = function () {
McPlayer.seek(-10000);
McPlayer.showUi(true);
};
}
if (btnFf) {
btnFf.onclick = function () {
McPlayer.seek(10000);
McPlayer.showUi(true);
};
}
document.addEventListener("keydown", onKey);
McPlayer.on("onEnded", function () {
if (state.liveMode) return;
leavePlayer();
});
}
global.McApp = {
toast: toast,
showLoading: showLoading,
openLivePlayer: openLivePlayer,
};
function boot() {
try {
var banner = document.getElementById("boot-banner");
if (banner) {
banner.style.display = "none";
banner.style.visibility = "hidden";
}
if (!window.McApi) throw new Error("api.js niet geladen");
if (!window.McFocus) throw new Error("focus.js niet geladen");
if (!window.McPlayer) throw new Error("player.js niet geladen");
McFocus.registerKeys();
bind();
if (McApi.getToken()) afterAuth();
else startPairing();
} catch (e) {
var banner2 = document.getElementById("boot-banner");
if (banner2) {
banner2.style.display = "block";
banner2.style.visibility = "visible";
banner2.style.background = "#12080a";
banner2.style.color = "#fff";
banner2.style.paddingTop = "30%";
banner2.innerHTML = "Startfout:<br/>" + (e.message || e);
}
var fatal = document.getElementById("fatal");
if (fatal) {
fatal.classList.remove("hidden");
fatal.textContent = "Startfout: " + (e.message || e);
}
}
}
var booted = false;
function bootOnce() {
if (booted) return;
booted = true;
boot();
}
window.onerror = function (msg, url, line) {
var fatal = document.getElementById("fatal");
if (fatal) {
fatal.classList.remove("hidden");
fatal.textContent = "JS-fout: " + msg + " @" + line;
}
var banner = document.getElementById("boot-banner");
if (banner && banner.style.display !== "none") {
banner.style.background = "#12080a";
banner.style.color = "#fff";
banner.innerHTML = "JS-fout:<br/>" + msg + " (regel " + line + ")";
}
return false;
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bootOnce);
}
bootOnce();
setTimeout(bootOnce, 100);
setTimeout(bootOnce, 500);
})(window);