286 lines
8.3 KiB
JavaScript
286 lines
8.3 KiB
JavaScript
function nowEscape(value) {
|
||
return String(value ?? '').replace(/[&<>"']/g, char => ({
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
}[char]));
|
||
}
|
||
|
||
function nowLogoUrl(channelId) {
|
||
return `/assets/logos/${encodeURIComponent(channelId)}.svg`;
|
||
}
|
||
|
||
function nowCreateCard(item) {
|
||
const article = document.createElement('article');
|
||
article.className = 'now-card now-card-enhanced';
|
||
|
||
article.innerHTML = `
|
||
<div class="now-card-top">
|
||
<img class="now-channel-logo" data-live-logo alt="">
|
||
|
||
<div class="now-channel-info">
|
||
<div class="now-channel-name">
|
||
<span class="channel-number" data-live-channel-number></span>
|
||
<span data-live-channel-name></span>
|
||
<span data-live-favorite></span>
|
||
</div>
|
||
|
||
<div class="now-time" data-live-time-range></div>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 data-live-title></h3>
|
||
|
||
<div class="now-subtitle" data-live-subtitle></div>
|
||
|
||
<div class="now-badges" data-live-badges></div>
|
||
|
||
<div class="progress-wrap">
|
||
<div class="progress-meta">
|
||
<span data-live-percent></span>
|
||
<span data-live-remaining></span>
|
||
</div>
|
||
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" data-live-progress></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="now-next" data-live-next>
|
||
<div class="now-next-label">Danach</div>
|
||
|
||
<div class="now-next-programme">
|
||
<div class="now-next-time" data-live-next-time></div>
|
||
|
||
<div class="now-next-info">
|
||
<strong data-live-next-title></strong>
|
||
<div class="now-next-subtitle" data-live-next-subtitle></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="now-desc" data-live-description></p>
|
||
`;
|
||
|
||
nowUpdateCard(article, item);
|
||
return article;
|
||
}
|
||
|
||
function nowUpdateCard(card, item) {
|
||
const logo = card.querySelector('[data-live-logo]');
|
||
logo.src = nowLogoUrl(item.channel_id);
|
||
logo.onerror = () => {
|
||
logo.onerror = null;
|
||
logo.src = '/assets/logos/default.svg';
|
||
};
|
||
|
||
card.querySelector('[data-live-channel-number]').textContent = item.channel_number ?? '';
|
||
card.querySelector('[data-live-channel-name]').textContent = item.channel_name ?? '';
|
||
card.querySelector('[data-live-favorite]').textContent = item.favorite ? '⭐' : '';
|
||
card.querySelector('[data-live-title]').textContent = item.title ?? '';
|
||
card.querySelector('[data-live-next-subtitle]').textContent =
|
||
item.next_subtitle || '';
|
||
|
||
card.querySelector('[data-live-next-subtitle]').style.display =
|
||
item.next_subtitle ? '' : 'none';
|
||
|
||
const subtitle = card.querySelector('[data-live-subtitle]');
|
||
subtitle.textContent = item.subtitle || '';
|
||
subtitle.style.display = item.subtitle ? '' : 'none';
|
||
|
||
const badges = [];
|
||
|
||
if (item.category) {
|
||
badges.push(`${item.genre_icon || '▶'} ${item.category}`);
|
||
}
|
||
|
||
if (item.year) {
|
||
badges.push(item.year);
|
||
}
|
||
|
||
if (item.rating) {
|
||
badges.push(`FSK ${item.rating}`);
|
||
}
|
||
|
||
if (item.quality) {
|
||
badges.push(item.quality);
|
||
}
|
||
|
||
const badgesEl = card.querySelector('[data-live-badges]');
|
||
badgesEl.innerHTML = badges.map(badge => `<span class="now-badge">${nowEscape(badge)}</span>`).join('');
|
||
badgesEl.style.display = badges.length ? '' : 'none';
|
||
|
||
const next = card.querySelector('[data-live-next]');
|
||
if (item.next_title) {
|
||
next.style.display = '';
|
||
card.querySelector('[data-live-next-title]').textContent = item.next_title;
|
||
card.querySelector('[data-live-next-time]').textContent = item.next_start
|
||
? TVGuideLive.formatTime(item.next_start)
|
||
: '';
|
||
} else {
|
||
next.style.display = 'none';
|
||
}
|
||
|
||
const desc = card.querySelector('[data-live-description]');
|
||
desc.textContent = item.description || '';
|
||
desc.style.display = item.description ? '' : 'none';
|
||
|
||
nowTickCard(card, item);
|
||
}
|
||
|
||
function nowTickCard(card, item) {
|
||
const percent = TVGuideLive.calcPercent(item.start, item.stop);
|
||
const remaining = TVGuideLive.calcRemainingMinutes(item.stop);
|
||
|
||
card.querySelector('[data-live-time-range]').textContent =
|
||
`${TVGuideLive.formatTime(item.start)} – ${TVGuideLive.formatTime(item.stop)}`;
|
||
|
||
card.querySelector('[data-live-percent]').textContent = `${percent}%`;
|
||
card.querySelector('[data-live-remaining]').textContent = `noch ${remaining} Minuten`;
|
||
|
||
card.querySelector('[data-live-progress]').style.width = `${percent}%`;
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
if (!window.LiveEngine) return;
|
||
|
||
const liveNow = new LiveEngine({
|
||
url: '/api/live-now.php',
|
||
versionUrl: '/api/version.php',
|
||
interval: 30000,
|
||
gridSelector: '[data-live-now-grid]',
|
||
itemKey: item => item.channel_id,
|
||
createItem: nowCreateCard,
|
||
updateItem: nowUpdateCard,
|
||
tickItem: nowTickCard,
|
||
emptyHtml: '<div class="empty">Aktuell wurden keine laufenden Sendungen gefunden.</div>'
|
||
});
|
||
|
||
liveNow.start();
|
||
});
|
||
|
||
function dashboardCreateCard(item) {
|
||
const button = document.createElement('button');
|
||
button.className = 'dashboard-programme-card dashboard-programme';
|
||
button.type = 'button';
|
||
button.dataset.programmeId = `${item.channel_id}-${item.start}-${item.title}`;
|
||
button.dataset.dashboardKey = item.channel_id;
|
||
|
||
button.innerHTML = `
|
||
<img src="${item.logo_url || '/assets/logos/default.svg'}" alt="">
|
||
<strong></strong>
|
||
<span></span>
|
||
<div class="dashboard-progress">
|
||
<div></div>
|
||
</div>
|
||
`;
|
||
|
||
dashboardUpdateCard(button, item);
|
||
return button;
|
||
}
|
||
|
||
function dashboardUpdateCard(card, item) {
|
||
card.querySelector('img').src = item.logo_url || '/assets/logos/default.svg';
|
||
card.querySelector('strong').textContent = item.title || '';
|
||
card.querySelector('span').textContent =
|
||
`${item.channel_name || ''} · noch ${TVGuideLive.calcRemainingMinutes(item.stop)} Min.`;
|
||
|
||
const bar = card.querySelector('.dashboard-progress div');
|
||
if (bar) {
|
||
bar.style.width = `${TVGuideLive.calcPercent(item.start, item.stop)}%`;
|
||
}
|
||
|
||
const id = `${item.channel_id}-${item.start}-${item.title}`;
|
||
card.dataset.programmeId = id;
|
||
|
||
window.TVGUIDE_PROGRAMMES = window.TVGUIDE_PROGRAMMES || {};
|
||
window.TVGUIDE_PROGRAMMES[id] = {
|
||
id,
|
||
channel_id: item.channel_id,
|
||
channel: item.channel_name,
|
||
channel_number: item.channel_number,
|
||
favorite: item.favorite,
|
||
title: item.title,
|
||
subtitle: item.subtitle || '',
|
||
description: item.description || '',
|
||
category: item.category || '',
|
||
metadata: item.metadata || {},
|
||
year: item.year || '',
|
||
rating: item.rating || '',
|
||
country: item.country || '',
|
||
start: TVGuideLive.formatTime(item.start),
|
||
stop: TVGuideLive.formatTime(item.stop)
|
||
};
|
||
}
|
||
|
||
function dashboardRender(grid, items) {
|
||
if (!grid) return;
|
||
|
||
// Beim ersten Live-Refresh vorhandene serverseitige Karten entfernen
|
||
if (!grid.dataset.dashboardInitialized) {
|
||
grid.innerHTML = '';
|
||
grid.dataset.dashboardInitialized = '1';
|
||
}
|
||
|
||
const seen = new Set();
|
||
|
||
items.slice(0, 6).forEach(item => {
|
||
const key = item.channel_id;
|
||
seen.add(key);
|
||
|
||
let card = grid.querySelector(`[data-dashboard-key="${CSS.escape(key)}"]`);
|
||
|
||
if (!card) {
|
||
card = dashboardCreateCard(item);
|
||
grid.appendChild(card);
|
||
} else {
|
||
dashboardUpdateCard(card, item);
|
||
}
|
||
});
|
||
|
||
grid.querySelectorAll('[data-dashboard-key]').forEach(card => {
|
||
if (!seen.has(card.dataset.dashboardKey)) {
|
||
card.remove();
|
||
}
|
||
});
|
||
|
||
if (items.length === 0) {
|
||
grid.innerHTML = '<div class="empty compact">Aktuell keine Sendungen.</div>';
|
||
}
|
||
}
|
||
|
||
async function dashboardRefreshNow() {
|
||
const nowGrid = document.querySelector('[data-dashboard-now]');
|
||
const favGrid = document.querySelector('[data-dashboard-favorites]');
|
||
|
||
if (!nowGrid && !favGrid) return;
|
||
|
||
try {
|
||
const data = await TVGuideLive.fetchJson('/api/live-now.php');
|
||
const items = data.items || [];
|
||
|
||
dashboardRender(nowGrid, items);
|
||
|
||
dashboardRender(
|
||
favGrid,
|
||
items.filter(item => item.favorite)
|
||
);
|
||
} catch (error) {
|
||
console.error('Dashboard konnte nicht aktualisiert werden:', error);
|
||
}
|
||
}
|
||
|
||
function dashboardTick() {
|
||
document.querySelectorAll('[data-dashboard-key]').forEach(card => {
|
||
const key = card.dataset.dashboardKey;
|
||
// vollständiger Refresh kommt alle 30 Sekunden; hier nur Balken/Restzeit über vorhandene DOM-Daten nicht möglich
|
||
});
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
dashboardRefreshNow();
|
||
setInterval(dashboardRefreshNow, 30000);
|
||
});
|