This commit is contained in:
Gogs
2026-07-07 07:55:55 +02:00
parent e2e6fb1f3c
commit cce22e3f0f
10 changed files with 959 additions and 3 deletions
Vendored Regular → Executable
View File
+2 -2
View File
@@ -2,7 +2,7 @@
"orf1.at": {
"uuid": "58a965b21f48319d1a4051476667f5e2",
"name": "ORF 1",
"icon": "\/logos\/orf1.png",
"icon": "\/logos\/orf1.png.svg",
"country": "",
"language": "",
"enabled": true,
@@ -13,7 +13,7 @@
"orf2.at": {
"uuid": "4c9dba34a821b5b25c8e1e2b65abc4df",
"name": "ORF 2",
"icon": "\/logos\/orf2.png",
"icon": "\/logos\/orf2.png.svg",
"country": "",
"language": "",
"enabled": true,
+461
View File
@@ -401,3 +401,464 @@ button {
.favorite-star {
color: #facc15;
}
.page-heading {
margin-bottom: 22px;
}
.page-heading h2 {
margin: 0;
font-size: 1.8rem;
}
.page-heading p {
color: var(--muted);
margin: 6px 0 0;
}
.day-toolbar {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
margin-bottom: 22px;
flex-wrap: wrap;
}
.day-nav {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.button-link {
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--panel-2);
color: var(--text);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 14px;
text-decoration: none;
font-weight: 800;
}
.button-link:hover {
background: var(--accent-soft);
}
.day-filter {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.day-filter input,
.day-filter select {
background: #020617;
color: var(--text);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 12px;
}
.day-list {
display: grid;
gap: 20px;
}
.day-channel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 18px;
overflow: hidden;
}
.day-channel-header {
background: var(--panel-2);
padding: 15px 18px;
display: flex;
align-items: center;
justify-content: space-between;
}
.channel-number {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 34px;
height: 26px;
margin-right: 10px;
border-radius: 999px;
background: var(--panel-3);
color: #dbeafe;
font-size: 0.85rem;
font-weight: 800;
}
.day-programme {
display: grid;
grid-template-columns: 92px 1fr;
gap: 16px;
padding: 16px 18px;
border-top: 1px solid var(--border);
}
.day-programme.is-current {
background: rgba(34, 197, 94, 0.12);
border-left: 5px solid var(--success);
}
.day-programme.is-past {
opacity: 0.55;
}
.day-time {
color: var(--muted);
font-weight: 800;
display: flex;
flex-direction: column;
gap: 4px;
}
.day-programme-body h3 {
margin: 0;
font-size: 1.08rem;
}
.day-programme-body p {
color: #cbd5e1;
margin: 10px 0 0;
line-height: 1.45;
}
@media (max-width: 700px) {
.day-programme {
grid-template-columns: 1fr;
gap: 8px;
}
.day-time {
flex-direction: row;
}
.day-time span:first-child::after {
content: "";
margin-left: 8px;
}
}
.timeline-wrap {
--minute-width: 2px;
margin-bottom: 28px;
overflow-x: auto;
border: 1px solid var(--border);
border-radius: 18px;
background: var(--panel);
}
.timeline {
min-width: calc(180px + 1440 * var(--minute-width));
}
.timeline-header,
.timeline-row {
display: grid;
grid-template-columns: 180px calc(1440 * var(--minute-width));
}
.timeline-header {
position: sticky;
top: 0;
z-index: 5;
background: var(--panel-2);
border-bottom: 1px solid var(--border);
}
.timeline-channel-head,
.timeline-channel {
position: sticky;
left: 0;
z-index: 4;
background: var(--panel-2);
border-right: 1px solid var(--border);
}
.timeline-channel-head {
padding: 12px 14px;
font-weight: 800;
color: var(--muted);
}
.timeline-hours {
position: relative;
height: 46px;
background:
repeating-linear-gradient(
to right,
transparent 0,
transparent calc(60 * var(--minute-width) - 1px),
rgba(148, 163, 184, 0.25) calc(60 * var(--minute-width)),
rgba(148, 163, 184, 0.25) calc(60 * var(--minute-width))
);
}
.timeline-hour {
position: absolute;
top: 12px;
transform: translateX(6px);
color: var(--muted);
font-size: 0.8rem;
font-weight: 800;
}
.timeline-row {
min-height: 72px;
border-bottom: 1px solid var(--border);
}
.timeline-channel {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 14px;
font-weight: 800;
}
.timeline-programmes {
position: relative;
min-height: 72px;
background:
repeating-linear-gradient(
to right,
transparent 0,
transparent calc(60 * var(--minute-width) - 1px),
rgba(148, 163, 184, 0.12) calc(60 * var(--minute-width)),
rgba(148, 163, 184, 0.12) calc(60 * var(--minute-width))
);
}
.timeline-programme {
position: absolute;
top: 9px;
left: calc(var(--start) * var(--minute-width));
width: calc(var(--duration) * var(--minute-width));
height: 54px;
overflow: hidden;
border-radius: 12px;
padding: 8px 10px;
background: var(--panel-3);
border: 1px solid rgba(148, 163, 184, 0.25);
color: var(--text);
}
.timeline-programme strong {
display: block;
font-size: 0.85rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.timeline-programme span {
display: block;
margin-top: 3px;
color: var(--muted);
font-size: 0.75rem;
}
.timeline-programme.is-current {
background: rgba(34, 197, 94, 0.22);
border-color: var(--success);
}
.timeline-programme.is-past {
opacity: 0.55;
}
.timeline-now-line {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background: #ef4444;
z-index: 100;
pointer-events: none;
}
.timeline-now-line::before {
content: "";
position: absolute;
left: -5px;
top: -5px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #ef4444;
}
.channel-logo {
width: 34px;
height: 24px;
object-fit: contain;
flex: 0 0 auto;
background: rgba(255, 255, 255, 0.08);
border-radius: 6px;
padding: 2px;
}
.timeline-channel-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.timeline-programme {
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
}
.timeline-programme:hover {
transform: scale(1.015);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
z-index: 20;
}
.timeline-channel {
justify-content: flex-start;
}
.timeline-channel .channel-number {
margin-right: 0;
}
.programme-modal[hidden] {
display: none;
}
.programme-modal {
position: fixed;
inset: 0;
z-index: 999;
}
.programme-modal-backdrop {
position: absolute;
inset: 0;
background: rgba(2, 6, 23, 0.78);
backdrop-filter: blur(3px);
}
.programme-modal-card {
position: relative;
width: min(620px, calc(100vw - 32px));
margin: 10vh auto;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 22px;
padding: 28px;
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.55);
}
.programme-modal-close {
position: absolute;
top: 14px;
right: 14px;
width: 38px;
height: 38px;
padding: 0;
border-radius: 999px;
font-size: 1.5rem;
line-height: 1;
}
.programme-modal-time {
color: var(--muted);
font-weight: 800;
margin-bottom: 8px;
}
.programme-modal-card h2 {
margin: 0 46px 8px 0;
font-size: 1.7rem;
}
.programme-modal-subtitle {
color: #cbd5e1;
font-size: 1.05rem;
margin-bottom: 12px;
}
.programme-modal-category {
display: inline-block;
margin-bottom: 14px;
padding: 5px 10px;
border-radius: 999px;
background: var(--panel-3);
color: #dbeafe;
font-size: 0.85rem;
font-weight: 700;
}
.programme-modal-category:empty,
.programme-modal-subtitle:empty {
display: none;
}
.programme-modal-card p {
color: #dbe4ef;
line-height: 1.55;
margin: 0;
}
.guide-search {
position: relative;
margin-bottom: 22px;
}
.guide-search input {
width: 100%;
background: #020617;
color: var(--text);
border: 1px solid var(--border);
border-radius: 14px;
padding: 13px 16px;
font-size: 1rem;
}
.guide-search-results {
position: absolute;
z-index: 200;
top: calc(100% + 8px);
left: 0;
right: 0;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 20px 50px rgba(0,0,0,.45);
}
.guide-search-result {
padding: 13px 16px;
border-bottom: 1px solid var(--border);
cursor: pointer;
}
.guide-search-result:hover {
background: var(--accent-soft);
}
.guide-search-result strong {
display: block;
}
.guide-search-result span {
display: block;
margin-top: 4px;
color: var(--muted);
font-size: .88rem;
}
+137
View File
@@ -1 +1,138 @@
document.addEventListener("DOMContentLoaded", () => {
const wrap = document.querySelector(".timeline-wrap");
const line = document.querySelector(".timeline-now-line");
if (!wrap || !line)
return;
wrap.scrollLeft = Math.max(
0,
line.offsetLeft - wrap.clientWidth / 3
);
});
document.addEventListener("DOMContentLoaded", () => {
const modal = document.getElementById("programmeModal");
if (!modal) {
return;
}
const title = document.getElementById("modalTitle");
const subtitle = document.getElementById("modalSubtitle");
const description = document.getElementById("modalDescription");
const category = document.getElementById("modalCategory");
const time = document.getElementById("modalTime");
const openModal = (programmeElement) => {
const id = programmeElement.dataset.programmeId;
const programme = window.TVGUIDE_PROGRAMMES?.[id];
if (!programme) {
return;
}
title.textContent = programme.title || "";
subtitle.textContent = programme.subtitle || "";
description.textContent = programme.description || "Keine Beschreibung vorhanden.";
category.textContent = programme.category || "";
time.textContent = `${programme.channel || ""} · ${programme.start || ""} ${programme.stop || ""}`;
modal.hidden = false;
document.body.classList.add("modal-open");
};
const closeModal = () => {
modal.hidden = true;
document.body.classList.remove("modal-open");
};
document.querySelectorAll(".timeline-programme").forEach((programme) => {
programme.addEventListener("click", () => openModal(programme));
});
modal.querySelectorAll("[data-modal-close]").forEach((element) => {
element.addEventListener("click", closeModal);
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !modal.hidden) {
closeModal();
}
});
});
document.addEventListener("DOMContentLoaded", () => {
const search = document.getElementById("guideSearch");
const results = document.getElementById("guideSearchResults");
const programmes = window.TVGUIDE_PROGRAMMES || {};
if (!search || !results) {
return;
}
const openProgrammeById = (id) => {
const element = document.querySelector(`[data-programme-id="${CSS.escape(id)}"]`);
if (element) {
element.click();
}
};
search.addEventListener("input", () => {
const query = search.value.trim().toLowerCase();
if (query.length < 2) {
results.hidden = true;
results.innerHTML = "";
return;
}
const matches = Object.values(programmes)
.filter((programme) => {
return [
programme.title,
programme.subtitle,
programme.description,
programme.category,
programme.channel
].some((value) => (value || "").toLowerCase().includes(query));
})
.slice(0, 12);
if (!matches.length) {
results.innerHTML = `<div class="guide-search-result"><strong>Nichts gefunden</strong></div>`;
results.hidden = false;
return;
}
results.innerHTML = matches.map((programme) => `
<div class="guide-search-result" data-search-programme-id="${programme.id}">
<strong>${programme.title || "Ohne Titel"}</strong>
<span>${programme.channel || ""} · ${programme.start || ""} ${programme.stop || ""}</span>
</div>
`).join("");
results.hidden = false;
});
results.addEventListener("click", (event) => {
const item = event.target.closest("[data-search-programme-id]");
if (!item) {
return;
}
openProgrammeById(item.dataset.searchProgrammeId);
results.hidden = true;
search.value = "";
});
document.addEventListener("click", (event) => {
if (!event.target.closest(".guide-search")) {
results.hidden = true;
}
});
});
console.log('TVGuide loaded');
+27
View File
@@ -44,9 +44,31 @@ if ($view === 'channels' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$currentProgrammes = $xmltv->getCurrentProgrammes($config);
$selectedDate = $_GET['date'] ?? date('Y-m-d');
$selectedChannel = $_GET['channel'] ?? null;
if ($selectedChannel === '') {
$selectedChannel = null;
}
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $selectedDate)) {
$selectedDate = date('Y-m-d');
}
$dayProgrammes = $xmltv->getDayProgrammes($config, $selectedDate, $selectedChannel);
$isToday = ($selectedDate === date('Y-m-d'));
$nowMinute = null;
if ($isToday) {
$nowMinute = ((int)date('H') * 60) + (int)date('i');
}
$contentTemplate = match ($view) {
'channels' => '/var/www/templates/channels.php',
'now' => '/var/www/templates/now.php',
'day' => '/var/www/templates/day.php',
default => '/var/www/templates/home.php',
};
@@ -59,4 +81,9 @@ View::render('layout', [
'channelConfig' => $config->getChannels(),
'saved' => isset($_GET['saved']),
'currentProgrammes' => $currentProgrammes,
'selectedDate' => $selectedDate,
'selectedChannel' => $selectedChannel,
'dayProgrammes' => $dayProgrammes,
'isToday' => $isToday,
'nowMinute' => $nowMinute,
]);
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="80" viewBox="0 0 160 80">
<rect width="160" height="80" rx="14" fill="#ef4444"/>
<text x="80" y="52" text-anchor="middle" font-family="Arial, sans-serif" font-size="38" font-weight="700" fill="white">ORF 1</text>
</svg>

After

Width:  |  Height:  |  Size: 284 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="80" viewBox="0 0 160 80">
<rect width="160" height="80" rx="14" fill="#2563eb"/>
<text x="80" y="52" text-anchor="middle" font-family="Arial, sans-serif" font-size="38" font-weight="700" fill="white">ORF 2a</text>
</svg>

After

Width:  |  Height:  |  Size: 285 B

+94 -1
View File
@@ -43,7 +43,6 @@ class XmlTv
];
} else {
$storedChannels[$id]['name'] = $channel['name'] ?? $storedChannels[$id]['name'];
$storedChannels[$id]['icon'] = $channel['icon'] ?? ($storedChannels[$id]['icon'] ?? '');
}
}
@@ -211,6 +210,7 @@ class XmlTv
$items[] = [
'channel_id' => $id,
'channel_name' => $cfg['name'] ?? $channel['name'] ?? $id,
'channel_icon' => $cfg['icon'] ?? $channel['icon'] ?? '',
'channel_number' => (int)($cfg['number'] ?? 999999),
'favorite' => (bool)($cfg['favorite'] ?? false),
'title' => $p['title'] ?? '',
@@ -258,4 +258,97 @@ class XmlTv
$dt->setTimezone(new DateTimeZone($this->timezone));
return $dt;
}
public function getDayProgrammes(Config $config, string $date, ?string $channelId = null): array
{
$data = $this->getCachedData();
$channelConfig = $config->getChannels();
$tz = new DateTimeZone($this->timezone);
$dayStart = new DateTime($date . ' 00:00:00', $tz);
$dayEnd = clone $dayStart;
$dayEnd->modify('+1 day');
$startTs = $dayStart->getTimestamp();
$endTs = $dayEnd->getTimestamp();
$items = [];
foreach ($data['channels'] ?? [] as $id => $channel) {
if ($channelId !== null && $id !== $channelId) {
continue;
}
$cfg = $channelConfig[$id] ?? [];
if (($cfg['enabled'] ?? true) !== true) {
continue;
}
$programmes = [];
foreach ($channel['programmes'] ?? [] as $p) {
$start = (int)($p['start'] ?? 0);
$stop = (int)($p['stop'] ?? 0);
if ($start < $endTs && $stop > $startTs) {
$visibleStart = max($start, $startTs);
$visibleStop = min($stop, $endTs);
$startDt = (new DateTime())->setTimestamp($visibleStart)->setTimezone($tz);
$stopDt = (new DateTime())->setTimestamp($visibleStop)->setTimezone($tz);
$startMinute = ((int)$startDt->format('H') * 60) + (int)$startDt->format('i');
$stopMinute = ((int)$stopDt->format('H') * 60) + (int)$stopDt->format('i');
if ($visibleStop >= $endTs) {
$stopMinute = 1440;
}
$durationMinutes = max(1, (int)(($visibleStop - $visibleStart) / 60));
$programmes[] = [
'id' => md5($id . '|' . $start . '|' . $stop . '|' . ($p['title'] ?? '')),
'title' => $p['title'] ?? '',
'subtitle' => $p['subtitle'] ?? '',
'description' => $p['description'] ?? '',
'category' => $p['category'] ?? '',
'start' => $start,
'stop' => $stop,
'visible_start' => $visibleStart,
'visible_stop' => $visibleStop,
'start_minute' => $startMinute,
'stop_minute' => $stopMinute,
'duration_minutes' => $durationMinutes,
'is_current' => time() >= $start && time() < $stop,
'is_past' => $stop < time(),
];
}
}
if (!$programmes) {
continue;
}
usort($programmes, function ($a, $b) {
return $a['start'] <=> $b['start'];
});
$items[] = [
'channel_id' => $id,
'channel_name' => $cfg['name'] ?? $channel['name'] ?? $id,
'channel_icon' => $cfg['icon'] ?? $channel['icon'] ?? '',
'channel_number' => (int)($cfg['number'] ?? 999999),
'favorite' => (bool)($cfg['favorite'] ?? false),
'programmes' => $programmes,
];
}
usort($items, function ($a, $b) {
return $a['channel_number'] <=> $b['channel_number'];
});
return $items;
}
}
+229
View File
@@ -0,0 +1,229 @@
<?php
$tz = new DateTimeZone('Europe/Vienna');
$dateObj = new DateTime($selectedDate, $tz);
$prevDate = (clone $dateObj)->modify('-1 day')->format('Y-m-d');
$nextDate = (clone $dateObj)->modify('+1 day')->format('Y-m-d');
$today = (new DateTime('today', $tz))->format('Y-m-d');
$wochentage = [
'Monday' => 'Montag',
'Tuesday' => 'Dienstag',
'Wednesday' => 'Mittwoch',
'Thursday' => 'Donnerstag',
'Friday' => 'Freitag',
'Saturday' => 'Samstag',
'Sunday' => 'Sonntag',
];
$monate = [
1 => 'Jänner',
2 => 'Februar',
3 => 'März',
4 => 'April',
5 => 'Mai',
6 => 'Juni',
7 => 'Juli',
8 => 'August',
9 => 'September',
10 => 'Oktober',
11 => 'November',
12 => 'Dezember',
];
$anzeigeDatum =
$wochentage[$dateObj->format('l')] . ', ' .
$dateObj->format('j') . '. ' .
$monate[(int)$dateObj->format('n')] . ' ' .
$dateObj->format('Y');
$programmeIndex = [];
foreach ($dayProgrammes as $channel) {
foreach ($channel['programmes'] as $programme) {
$programmeIndex[$programme['id']] = [
'id' => $programme['id'],
'channel' => $channel['channel_name'],
'title' => $programme['title'],
'subtitle' => $programme['subtitle'],
'description' => $programme['description'],
'category' => $programme['category'],
'start' => date('H:i', $programme['start']),
'stop' => date('H:i', $programme['stop']),
];
}
}
?>
<section class="page-heading">
<h2>TV Guide</h2>
<p><?= htmlspecialchars($anzeigeDatum) ?></p>
</section>
<div class="day-toolbar">
<div class="day-nav">
<a class="button-link" href="/?view=day&date=<?= urlencode($prevDate) ?>&channel=<?= urlencode($selectedChannel ?? '') ?>">◀ Vorheriger Tag</a>
<a class="button-link" href="/?view=day&date=<?= urlencode($today) ?>&channel=<?= urlencode($selectedChannel ?? '') ?>">Heute</a>
<a class="button-link" href="/?view=day&date=<?= urlencode($nextDate) ?>&channel=<?= urlencode($selectedChannel ?? '') ?>">Nächster Tag ▶</a>
</div>
<form method="get" class="day-filter">
<input type="hidden" name="view" value="day">
<input type="date" name="date" value="<?= htmlspecialchars($selectedDate) ?>">
<select name="channel">
<option value="">Alle Sender</option>
<?php foreach ($channels as $id => $channel): ?>
<option value="<?= htmlspecialchars($id) ?>" <?= $selectedChannel === $id ? 'selected' : '' ?>>
<?= htmlspecialchars(($channel['number'] ?? '') . ' · ' . $channel['name']) ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit">Anzeigen</button>
</form>
</div>
<div class="guide-search">
<input
type="search"
id="guideSearch"
placeholder="Sendung suchen …"
autocomplete="off">
<div class="guide-search-results" id="guideSearchResults" hidden></div>
</div>
<?php if (!$dayProgrammes): ?>
<div class="empty">Für diesen Tag wurden keine Sendungen gefunden.</div>
<?php endif; ?>
<?php if ($dayProgrammes): ?>
<div class="timeline-wrap">
<div class="timeline">
<div class="timeline-header">
<div class="timeline-channel-head">Sender</div>
<div class="timeline-hours">
<?php for ($hour = 0; $hour <= 24; $hour++): ?>
<div class="timeline-hour" style="left: calc(<?= $hour * 60 ?> * var(--minute-width));">
<?= str_pad((string)$hour, 2, '0', STR_PAD_LEFT) ?>:00
</div>
<?php endfor; ?>
</div>
</div>
<?php foreach ($dayProgrammes as $channel): ?>
<div class="timeline-row">
<div class="timeline-channel">
<?php if (!empty($channel['channel_icon'])): ?>
<img
class="channel-logo"
src="<?= htmlspecialchars($channel['channel_icon']) ?>"
alt="<?= htmlspecialchars($channel['channel_name']) ?>">
<?php else: ?>
<span class="channel-number"><?= (int)$channel['channel_number'] ?></span>
<?php endif; ?>
<span class="timeline-channel-name"><?= htmlspecialchars($channel['channel_name']) ?></span>
<?php if ($channel['favorite']): ?>
<span class="favorite-star">⭐</span>
<?php endif; ?>
</div>
<div class="timeline-programmes">
<?php if ($isToday): ?>
<div
class="timeline-now-line"
style="left: calc(<?= (int)$nowMinute ?> * var(--minute-width));">
</div>
<?php endif; ?>
<?php foreach ($channel['programmes'] as $programme): ?>
<div
class="timeline-programme <?= $programme['is_current'] ? 'is-current' : '' ?> <?= $programme['is_past'] ? 'is-past' : '' ?>"
data-programme-id="<?= htmlspecialchars($programme['id']) ?>"
style="
--start: <?= (int)$programme['start_minute'] ?>;
--duration: <?= (int)$programme['duration_minutes'] ?>;
"
title="<?= htmlspecialchars(date('H:i', $programme['start']) . ' - ' . date('H:i', $programme['stop']) . ' · ' . $programme['title']) ?>"
>
<strong><?= htmlspecialchars($programme['title']) ?></strong>
<span><?= date('H:i', $programme['start']) ?> - <?= date('H:i', $programme['stop']) ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div class="day-list">
<?php foreach ($dayProgrammes as $channel): ?>
<section class="day-channel">
<div class="day-channel-header">
<div>
<span class="channel-number"><?= (int)$channel['channel_number'] ?></span>
<?php if (!empty($channel['channel_icon'])): ?>
<img
class="channel-logo"
src="<?= htmlspecialchars($channel['channel_icon']) ?>"
alt="<?= htmlspecialchars($channel['channel_name']) ?>">
<?php endif; ?>
<strong><?= htmlspecialchars($channel['channel_name']) ?></strong>
<?php if ($channel['favorite']): ?>
<span class="favorite-star">⭐</span>
<?php endif; ?>
</div>
</div>
<?php foreach ($channel['programmes'] as $programme): ?>
<article class="day-programme <?= $programme['is_current'] ? 'is-current' : '' ?> <?= $programme['is_past'] ? 'is-past' : '' ?>">
<div class="day-time">
<span><?= date('H:i', $programme['start']) ?></span>
<span><?= date('H:i', $programme['stop']) ?></span>
</div>
<div class="day-programme-body">
<h3><?= htmlspecialchars($programme['title']) ?></h3>
<?php if ($programme['subtitle']): ?>
<div class="now-subtitle"><?= htmlspecialchars($programme['subtitle']) ?></div>
<?php endif; ?>
<?php if ($programme['category']): ?>
<span class="category"><?= htmlspecialchars($programme['category']) ?></span>
<?php endif; ?>
<?php if ($programme['description']): ?>
<p><?= htmlspecialchars($programme['description']) ?></p>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
</section>
<?php endforeach; ?>
</div>
<script>
window.TVGUIDE_PROGRAMMES = <?= json_encode(
$programmeIndex,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
) ?>;
</script>
<div class="programme-modal" id="programmeModal" hidden>
<div class="programme-modal-backdrop" data-modal-close></div>
<div class="programme-modal-card">
<button class="programme-modal-close" type="button" data-modal-close>×</button>
<div class="programme-modal-time" id="modalTime"></div>
<h2 id="modalTitle"></h2>
<div class="programme-modal-subtitle" id="modalSubtitle"></div>
<div class="programme-modal-category" id="modalCategory"></div>
<p id="modalDescription"></p>
</div>
</div>
+1
View File
@@ -1,6 +1,7 @@
<aside class="sidebar">
<nav class="sidebar-nav">
<a href="/" class="<?= ($view ?? '') === 'today' ? 'active' : '' ?>">🏠 Heute</a>
<a href="/?view=day" class="<?= ($view ?? '') === 'day' ? 'active' : '' ?>">📅 Tagesansicht</a>
<a href="/?view=now" class="<?= ($view ?? '') === 'now' ? 'active' : '' ?>"> Jetzt</a>
<a href="/?view=favorites" class="<?= ($view ?? '') === 'favorites' ? 'active' : '' ?>"> Favoriten</a>
<a href="/?view=channels" class="<?= ($view ?? '') === 'channels' ? 'active' : '' ?>">📺 Sender</a>