(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, "&").replace(//g, ">").replace(/"/g, """); } 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 = '
' + avatarEmoji(p.avatarId) + "
" + '
' + escapeHtml(p.name) + "
" + '
' + escapeHtml(p.isKids ? "Kids" : p.isOwner ? "Hoofd" : p.hasPin ? "PIN" : "") + "
"; 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 += ''; $("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 = 'Profiel'; return; } el.innerHTML = '' + escapeHtml(p.name) + ""; } 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 ? '
' + escapeHtml(String(hero.badge).toUpperCase()) + "
" : ""; var titleBlock; if (hero && hero.logoUrl) { titleBlock = '"; } else { titleBlock = '
' + escapeHtml(hero && hero.title ? hero.title : "Welkom bij VMC") + "
"; } var metaParts = []; if (hero && hero.year) metaParts.push(String(hero.year)); if (hero && hero.id) metaParts.push(heroTypeLabel(hero)); var metaHtml = metaParts.length ? '
' + escapeHtml(metaParts.join(" · ")) + "
" : ""; var dots = ""; if (items.length > 1) { dots = '
'; for (var d = 0; d < items.length; d++) { dots += ''; } dots += "
"; } var hadPlayFocus = document.activeElement && document.activeElement.id === "hero-play"; var hadInfoFocus = document.activeElement && document.activeElement.id === "hero-info"; content.innerHTML = badgeHtml + titleBlock + metaHtml + '
' + '' + '' + "
" + 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 = '
' + '
' + '
' + '
' + "
" + '
'; 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 = '
' + escapeHtml(title) + "
"; 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 = '
' + '
' + escapeHtml(letter) + "
" + (url ? '
" : "") + (progress > 0 ? '
' : "") + "
" + '
' + escapeHtml(item.title || "") + "
"; 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 = '
' + escapeHtml(title) + "
" + '' + '
'; 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) + ''; 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 = '

Geen items

'; 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 = '
' + '
Zoeken
' + '
' + escapeHtml(state.searchQ || " ") + "
" + '
' + '
' + "
"; 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 = '
' + '
Instellingen
' + '

Profiel: ' + escapeHtml(p ? p.name : "—") + "

" + '' + '' + "
"; $("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 = 'Geen afspeelbare bestanden'; } 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 = '
Afl. ' + ep.episodeNumber + " · " + escapeHtml(ep.title || "") + "
" + '
' + escapeHtml((ep.overview || "").slice(0, 140)) + "
"; 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:
" + (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:
" + msg + " (regel " + line + ")"; } return false; }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", bootOnce); } bootOnce(); setTimeout(bootOnce, 100); setTimeout(bootOnce, 500); })(window);