stremio/apps/tizen-tv/www/js/live.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

366 lines
12 KiB
JavaScript

(function (global) {
var state = {
categories: [],
channels: [],
categoryId: "__all__",
selected: null,
playList: [],
playIndex: -1,
mounted: false,
focusInList: false,
};
function $(id) { return document.getElementById(id); }
function toast(msg) {
if (global.McApp && McApp.toast) McApp.toast(msg);
}
function showLoading(on) {
if (global.McApp && McApp.showLoading) McApp.showLoading(on);
}
function escapeHtml(s) {
return String(s || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function safeUrl(u) {
return String(u || "").replace(/'/g, "%27");
}
function categoryList() {
var all = [{ id: "__all__", name: "Alles", channelCount: 0 }].concat(state.categories || []);
var favs = McApi.getFavs();
if (favs.length) {
all.splice(1, 0, { id: "__fav__", name: "Favorieten", channelCount: favs.length });
}
return all;
}
/** Bouw categoriechips één keer; update alleen active + behoud focus. */
function renderCategories(opts) {
opts = opts || {};
var el = $("tv-cats");
if (!el) return;
var all = categoryList();
var activeId = state.categoryId || "__all__";
var prevFocusId = null;
var ae = document.activeElement;
if (ae && ae.getAttribute && ae.getAttribute("data-cat-id")) {
prevFocusId = ae.getAttribute("data-cat-id");
}
el.innerHTML = "";
var focusTarget = null;
all.forEach(function (c) {
var b = document.createElement("button");
b.className = "cat-chip focusable" + (activeId === c.id ? " active" : "");
b.type = "button";
b.setAttribute("data-cat-id", c.id);
b.innerHTML =
escapeHtml(c.name) +
(c.channelCount ? " (" + c.channelCount + ")" : "") +
'<span class="chip-line"></span>';
b.addEventListener("click", function () {
if (state.categoryId === c.id) {
b.focus();
return;
}
state.categoryId = c.id;
state.focusInList = false;
setCategoryActive(c.id);
loadChannels(c.id, { keepCategoryFocus: true, focusChipId: c.id });
});
b.addEventListener("focus", function () {
state.focusInList = false;
});
el.appendChild(b);
if (c.id === (opts.focusChipId || prevFocusId || activeId)) {
focusTarget = b;
}
});
if (opts.autoFocus || opts.keepCategoryFocus) {
setTimeout(function () {
if (focusTarget) focusTarget.focus();
}, 30);
}
}
function setCategoryActive(id) {
var chips = document.querySelectorAll("#tv-cats .cat-chip");
for (var i = 0; i < chips.length; i++) {
var chip = chips[i];
if (chip.getAttribute("data-cat-id") === id) chip.classList.add("active");
else chip.classList.remove("active");
}
}
function renderChannels(channels, opts) {
opts = opts || {};
state.channels = channels || [];
var list = $("tv-list");
var detail = $("tv-detail");
if (!list) return;
list.innerHTML = "";
if (!state.channels.length) {
list.innerHTML = '<div class="muted" style="padding:28px">Geen zenders in deze categorie</div>';
if (detail) {
detail.innerHTML = '<p class="muted">Geen zenders</p>';
}
return;
}
state.channels.forEach(function (ch, idx) {
var row = document.createElement("button");
row.className = "ch-row focusable";
row.type = "button";
row.setAttribute("data-idx", String(idx));
var now = ch.nowTitle || "";
var fav = McApi.isFav(ch.streamId);
var logo = ch.logoUrl
? '<div class="ch-logo"><div class="ch-logo-img" style="background-image:url(\'' +
safeUrl(ch.logoUrl) +
"')\"></div></div>"
: '<div class="ch-logo"><div class="ch-logo-img"></div></div>';
row.innerHTML =
'<span class="ch-bar"></span>' +
logo +
'<div class="ch-meta">' +
'<div class="ch-name">' +
(fav ? "* " : "") +
escapeHtml(ch.name) +
"</div>" +
'<div class="ch-epg">' +
escapeHtml(now ? "Nu · " + now : "Geen programma-info") +
"</div></div>";
row.addEventListener("click", function () {
selectChannel(ch, idx);
});
row.addEventListener("focus", function () {
state.focusInList = true;
selectChannel(ch, idx, { skipRebuildActions: true });
});
row.addEventListener("keydown", function (e) {
if ((e.keyCode || e.which) === 13) playChannel(ch, idx);
});
list.appendChild(row);
});
var keepIdx = 0;
if (state.selected) {
for (var i = 0; i < state.channels.length; i++) {
if (state.channels[i].streamId === state.selected.streamId) {
keepIdx = i;
break;
}
}
}
selectChannel(state.channels[keepIdx], keepIdx, { skipRebuildActions: false });
if (opts.focusFirstChannel) {
var first = list.querySelector(".ch-row");
if (first) setTimeout(function () { first.focus(); }, 40);
}
}
function paintDetail(ch, idx) {
var detail = $("tv-detail");
if (!detail || !ch) return;
var logoBlock = ch.logoUrl
? '<div class="tv-detail-logo"><div class="tv-detail-logo-img" style="background-image:url(\'' +
safeUrl(ch.logoUrl) +
"')\"></div></div>"
: "";
detail.innerHTML =
logoBlock +
"<h2>" + escapeHtml(ch.name) + "</h2>" +
'<div class="epg-card">' +
'<div class="epg-label">Nu</div>' +
'<div class="epg-title" id="tv-epg-now">' +
escapeHtml(ch.nowTitle || "Geen EPG beschikbaar") +
"</div></div>" +
'<div class="epg-card">' +
'<div class="epg-label">Straks</div>' +
'<div class="epg-title" id="tv-epg-next">' +
escapeHtml(ch.nextTitle || "—") +
"</div></div>" +
'<div class="tv-actions">' +
'<button class="btn primary focusable" id="tv-play">Kijken</button>' +
'<button class="btn focusable" id="tv-fav">' +
(McApi.isFav(ch.streamId) ? "* Favoriet" : "Favoriet") +
"</button></div>" +
'<p class="tv-hint">OK = kijken · Favoriet toevoegen met Favoriet</p>';
var playBtn = $("tv-play");
var favBtn = $("tv-fav");
if (playBtn) playBtn.onclick = function () { playChannel(ch, idx); };
if (favBtn) {
favBtn.onclick = function () {
McApi.toggleFav(ch.streamId);
renderCategories({ keepCategoryFocus: true, focusChipId: state.categoryId });
loadChannels(state.categoryId || "__all__", {
keepCategoryFocus: true,
focusChipId: state.categoryId,
});
};
}
}
function selectChannel(ch, idx, opts) {
opts = opts || {};
state.selected = ch;
state.playIndex = idx;
var rows = document.querySelectorAll("#tv-list .ch-row");
for (var i = 0; i < rows.length; i++) {
if (i === idx) rows[i].classList.add("active");
else rows[i].classList.remove("active");
}
paintDetail(ch, idx);
McApi.iptvEpg(ch.streamId).then(function (epg) {
if (!state.selected || state.selected.streamId !== ch.streamId) return;
ch.nowTitle = epg.now && epg.now.title;
ch.nextTitle = epg.next && epg.next.title;
var nowEl = $("tv-epg-now");
var nextEl = $("tv-epg-next");
if (nowEl) nowEl.textContent = ch.nowTitle || "Geen EPG beschikbaar";
if (nextEl) nextEl.textContent = ch.nextTitle || "—";
var row = document.querySelector('#tv-list .ch-row[data-idx="' + idx + '"] .ch-epg');
if (row) {
row.textContent = ch.nowTitle ? "Nu · " + ch.nowTitle : "Geen programma-info";
}
}).catch(function () {});
}
function playChannel(ch, idx) {
if (!ch) return;
showLoading(true);
McApi.iptvPlay(ch.streamId)
.then(function (info) {
showLoading(false);
state.playList = state.channels.slice();
state.playIndex = idx;
if (global.McApp && McApp.openLivePlayer) {
McApp.openLivePlayer({
title: info.name || ch.name,
url: info.streamUrl,
fallbackUrl: info.fallbackStreamUrl,
channels: state.playList,
index: idx,
});
}
})
.catch(function (e) {
showLoading(false);
toast(e.message || "Live starten mislukt");
});
}
function loadChannels(categoryId, opts) {
opts = opts || {};
var id = categoryId || "__all__";
state.categoryId = id;
setCategoryActive(id);
showLoading(true);
var req;
if (id === "__fav__") {
req = McApi.iptvChannels(null, true).then(function (res) {
var all = (res && res.channels) || res || [];
return (all || []).filter(function (c) { return McApi.isFav(c.streamId); });
});
} else if (id === "__all__") {
req = McApi.iptvChannels(null, true).then(function (res) {
return (res && res.channels) || res || [];
});
} else {
req = McApi.iptvChannels(id, true).then(function (res) {
return (res && res.channels) || res || [];
});
}
req
.then(function (channels) {
showLoading(false);
renderChannels(channels, {
focusFirstChannel: !!opts.focusFirstChannel,
});
if (opts.keepCategoryFocus) {
var chip =
document.querySelector('#tv-cats .cat-chip[data-cat-id="' + (opts.focusChipId || id) + '"]');
if (chip) {
setTimeout(function () { chip.focus(); }, 40);
}
}
})
.catch(function (e) {
showLoading(false);
toast(e.message || "Zenders laden mislukt");
if (opts.keepCategoryFocus) {
var chip =
document.querySelector('#tv-cats .cat-chip[data-cat-id="' + (opts.focusChipId || id) + '"]');
if (chip) chip.focus();
}
});
}
function mount(container) {
state.mounted = true;
state.focusInList = false;
container.innerHTML =
'<div class="tv-layout">' +
'<div id="tv-cats" class="tv-cats"></div>' +
'<div class="tv-body">' +
'<div id="tv-list" class="tv-list"></div>' +
'<div id="tv-detail" class="tv-detail"><p class="muted">Kies een zender om te kijken</p></div>' +
"</div></div>";
if (state.categories.length) {
var id = state.categoryId || "__all__";
renderCategories({ autoFocus: true, focusChipId: id });
loadChannels(id, { keepCategoryFocus: true, focusChipId: id });
return;
}
state.categoryId = "__all__";
state.selected = null;
showLoading(true);
McApi.iptvCategories()
.then(function (res) {
state.categories = (res && res.categories) || res || [];
renderCategories({ autoFocus: true, focusChipId: "__all__" });
loadChannels("__all__", { keepCategoryFocus: true, focusChipId: "__all__" });
})
.catch(function (e) {
showLoading(false);
container.innerHTML =
'<p class="muted" style="padding:48px">' +
escapeHtml(e.message || "Live TV niet beschikbaar") +
"</p>";
});
}
function zap(delta) {
if (!state.playList.length) return null;
var next = state.playIndex + delta;
if (next < 0) next = state.playList.length - 1;
if (next >= state.playList.length) next = 0;
var ch = state.playList[next];
state.playIndex = next;
return ch;
}
global.McLive = {
mount: mount,
zap: zap,
playChannel: playChannel,
getPlayState: function () {
return { list: state.playList, index: state.playIndex };
},
};
})(window);