Search
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
Reference in New Issue
Block a user