From 2df690ba1189404ac38a2754dca950ab1c9c9acc Mon Sep 17 00:00:00 2001 From: Gogs Date: Thu, 9 Jul 2026 14:36:47 +0200 Subject: [PATCH] Style --- app/config/channels.json | 2 +- app/public/assets/css/main.css | 283 ++++++++++++++++++++++++++ app/public/assets/css/timeline.css | 109 ++++++++++ app/public/assets/js/live-day.js | 38 +++- app/public/assets/js/live-now.js | 126 ++++++++++-- app/public/assets/logos/default.svg | 5 + app/src/Services/LogoService.php | 6 + app/src/Services/ProgrammeService.php | 76 ++++++- app/templates/day.php | 13 +- app/templates/now.php | 102 ++++++++-- 10 files changed, 724 insertions(+), 36 deletions(-) create mode 100644 app/public/assets/logos/default.svg diff --git a/app/config/channels.json b/app/config/channels.json index 4edb1ed..f545396 100644 --- a/app/config/channels.json +++ b/app/config/channels.json @@ -225,7 +225,7 @@ "icon": "", "country": "", "language": "", - "enabled": false, + "enabled": true, "favorite": false, "number": 210, "group": "Österreich" diff --git a/app/public/assets/css/main.css b/app/public/assets/css/main.css index 4a58ab4..620026f 100644 --- a/app/public/assets/css/main.css +++ b/app/public/assets/css/main.css @@ -186,3 +186,286 @@ align-items: center; gap: 0.5rem; } + +.now-card-enhanced { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.now-card-top { + display: flex; + align-items: center; + gap: 0.85rem; +} + +.now-channel-logo { + width: 64px; + height: 44px; + object-fit: contain; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 0.55rem; + padding: 4px; + flex: 0 0 64px; +} + +.now-channel-info { + min-width: 0; +} + +.now-channel-name { + display: flex; + align-items: center; + gap: 0.4rem; + font-weight: 700; +} + +.now-time { + color: var(--muted); + font-size: 0.9rem; + margin-top: 0.15rem; +} + +.now-badges { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +.now-badge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + border: 1px solid var(--border); + background: var(--panel-2); + color: var(--text); + border-radius: 999px; + padding: 0.2rem 0.55rem; + font-size: 0.82rem; +} + +.now-next { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 0.5rem; + align-items: center; + border-top: 1px solid var(--border); + padding-top: 0.65rem; + color: var(--muted); + font-size: 0.9rem; +} + +.now-next strong { + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.now-next-label { + color: var(--muted); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.now-channel-logo { + display: block; + width: 64px; + height: 44px; + min-width: 64px; + object-fit: contain; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 0.55rem; + padding: 4px; +} + +.progress-wrap { + width: 100%; +} + +.progress-bar { + width: 100%; + height: 10px; + background: var(--panel-2); + border-radius: 999px; + overflow: hidden; + border: 1px solid var(--border); +} + +.progress-fill { + display: block; + height: 100%; + min-width: 2px; + background: var(--accent); + border-radius: 999px; +} + +.progress-meta { + display: flex; + justify-content: space-between; + color: var(--muted); + font-size: 0.85rem; + margin-bottom: 0.35rem; +} + +.now-grid-enhanced { + gap: 1.5rem; +} + +.now-card-enhanced { + padding: 1.35rem; + border-radius: 14px; + gap: 0.9rem; +} + +.now-card-top { + margin-bottom: 0.35rem; +} + +.now-channel-logo { + width: 76px; + height: 52px; + flex-basis: 76px; +} + +.now-channel-name { + font-size: 1rem; +} + +.now-time { + font-size: 0.92rem; +} + +.now-card-enhanced h3 { + font-size: 1.35rem; + margin: 0.1rem 0 0; +} + +.now-subtitle { + color: var(--muted); + margin-top: -0.35rem; +} + +.now-badges { + margin-top: 0.1rem; +} + +.progress-wrap { + margin-top: 0.25rem; +} + +.progress-bar { + height: 12px; +} + +.progress-meta { + margin-bottom: 0.4rem; +} + +.now-next { + margin-top: 0.45rem; + padding-top: 0.85rem; + border-top: 1px solid var(--border); + background: rgba(255, 255, 255, 0.025); + border-radius: 0 0 10px 10px; +} + +.now-next-label { + font-size: 0.75rem; +} + +.now-next strong { + font-size: 0.95rem; +} + +.now-next { + display: block; + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid var(--border); + background: transparent; + border-radius: 0; + color: var(--muted); +} + +.now-next-programme { + display: flex; + gap: 1rem; + align-items: flex-start; + margin-top: 0.35rem; +} + +.now-next-time { + min-width: 54px; + font-weight: 700; + color: var(--accent); +} + +.now-next-info { + flex: 1; + min-width: 0; +} + +.now-next-info strong { + display: block; + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.now-next-subtitle { + margin-top: 0.15rem; + font-size: 0.9rem; + color: var(--muted); +} + +.now-card-top { + display: flex; + gap: 1rem; + align-items: center; +} + +.now-channel-logo-wrap { + width: 92px; + height: 64px; + flex: 0 0 92px; + + display: flex; + align-items: center; + justify-content: center; + + background: var(--panel-2); + border: 1px solid var(--border); + border-radius: .8rem; +} + +.now-channel-logo { + width: 82px; + height: 52px; + object-fit: contain; +} + +.now-channel-info { + flex: 1; +} + +.now-channel-name { + font-size: 1.15rem; + font-weight: 700; +} + +.now-channel-number { + margin-top: .15rem; + color: var(--muted); + font-size: .9rem; +} + +.now-time { + margin-top: .25rem; + color: var(--muted); + font-size: .95rem; +} diff --git a/app/public/assets/css/timeline.css b/app/public/assets/css/timeline.css index 58d9c01..fc66913 100644 --- a/app/public/assets/css/timeline.css +++ b/app/public/assets/css/timeline.css @@ -189,3 +189,112 @@ background: var(--accent); border-color: var(--accent); } + +cat >> app/public/assets/css/timeline.css <<'CSS' + +.timeline-channel { + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + gap: 6px; +} + +.timeline-channel .channel-logo { + width: 72px; + height: 48px; + flex: 0 0 auto; + padding: 4px; +} + +.timeline-channel-name { + font-size: 0.82rem; + line-height: 1.15; + max-width: 145px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.timeline-programme { + border-left: 5px solid rgba(148, 163, 184, 0.45); +} + +.timeline-programme.is-current { + opacity: 1; + border-color: var(--accent); + box-shadow: 0 0 0 1px var(--accent-soft), 0 8px 22px rgba(0, 0, 0, 0.45); + z-index: 10; +} + +.live-badge { + display: inline-flex; + margin-bottom: 3px; + padding: 1px 6px; + border-radius: 999px; + background: var(--accent); + color: white; + font-size: 0.65rem; + font-style: normal; + font-weight: 900; + letter-spacing: 0.04em; +} + +.genre-film, +.genre-movie, +.genre-kino { + border-left-color: #3b82f6; +} + +.genre-serie, +.genre-series { + border-left-color: #8b5cf6; +} + +.genre-sport, +.genre-fussball, +.genre-fu-ball { + border-left-color: #22c55e; +} + +.genre-news, +.genre-nachrichten, +.genre-journal { + border-left-color: #ef4444; +} + +.genre-doku, +.genre-documentary { + border-left-color: #14b8a6; +} + +.genre-kinder, +.genre-kids, +.genre-children { + border-left-color: #f97316; +} + +.genre-musik, +.genre-music { + border-left-color: #eab308; +} + +.timeline-programme:hover::after { + content: attr(title); + position: absolute; + left: 0; + top: calc(100% + 8px); + width: max-content; + max-width: 420px; + white-space: normal; + background: var(--panel-2); + border: 1px solid var(--border); + color: var(--text); + padding: 10px 12px; + border-radius: 10px; + box-shadow: 0 12px 28px rgba(0,0,0,.45); + z-index: 50; + font-size: 0.85rem; + line-height: 1.35; +} +CSS diff --git a/app/public/assets/js/live-day.js b/app/public/assets/js/live-day.js index 690d807..0b06428 100644 --- a/app/public/assets/js/live-day.js +++ b/app/public/assets/js/live-day.js @@ -15,9 +15,44 @@ function dayUpdateProgrammeStates() { document.querySelectorAll('[data-live-programme]').forEach(programme => { const start = Number(programme.dataset.liveStart); const stop = Number(programme.dataset.liveStop); + const isCurrent = now >= start && now < stop; - programme.classList.toggle('is-current', now >= start && now < stop); + programme.classList.toggle('is-current', isCurrent); programme.classList.toggle('is-past', now >= stop); + + const badge = programme.querySelector('[data-live-badge]'); + if (badge) { + badge.hidden = !isCurrent; + } + }); +} + +function daySetupZoomMemory() { + const wrap = document.querySelector('.timeline-wrap'); + const buttons = document.querySelectorAll('.timeline-zoom button'); + + if (!wrap || !buttons.length) return; + + const savedZoom = localStorage.getItem('tvguide.timelineZoom'); + + if (savedZoom) { + wrap.style.setProperty('--minute-width', `${savedZoom}px`); + + buttons.forEach(button => { + button.classList.toggle('active', button.dataset.zoom === savedZoom); + }); + } + + buttons.forEach(button => { + button.addEventListener('click', () => { + const zoom = button.dataset.zoom; + + wrap.style.setProperty('--minute-width', `${zoom}px`); + localStorage.setItem('tvguide.timelineZoom', zoom); + + buttons.forEach(b => b.classList.remove('active')); + button.classList.add('active'); + }); }); } @@ -27,6 +62,7 @@ function dayTick() { } document.addEventListener('DOMContentLoaded', () => { + daySetupZoomMemory(); dayTick(); setInterval(dayTick, 1000); }); diff --git a/app/public/assets/js/live-now.js b/app/public/assets/js/live-now.js index 5d52f90..e2aad63 100644 --- a/app/public/assets/js/live-now.js +++ b/app/public/assets/js/live-now.js @@ -1,22 +1,67 @@ +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'; + article.className = 'now-card now-card-enhanced'; article.innerHTML = ` -
- - +
+ + +
+
+ + + +
+ +
+

-
+
-
-
+
+
+ + +
+ +
+
+
+ +
+
Danach
+ +
+
+ +
+ +
+
+
+
+ +

`; nowUpdateCard(article, item); @@ -24,23 +69,78 @@ function nowCreateCard(item) { } function nowUpdateCard(card, item) { - card.querySelector('[data-live-channel-name]').textContent = item.channel_name; + 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-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 => `${nowEscape(badge)}`).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) { - card.querySelector('[data-live-time]').textContent = - `${TVGuideLive.formatTime(item.start)} – ${TVGuideLive.formatTime(item.stop)} · noch ${TVGuideLive.calcRemainingMinutes(item.stop)} Min.`; + const percent = TVGuideLive.calcPercent(item.start, item.stop); + const remaining = TVGuideLive.calcRemainingMinutes(item.stop); - card.querySelector('[data-live-progress]').style.width = - `${TVGuideLive.calcPercent(item.start, 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', () => { diff --git a/app/public/assets/logos/default.svg b/app/public/assets/logos/default.svg new file mode 100644 index 0000000..434614b --- /dev/null +++ b/app/public/assets/logos/default.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/Services/LogoService.php b/app/src/Services/LogoService.php index a287376..15c32a6 100644 --- a/app/src/Services/LogoService.php +++ b/app/src/Services/LogoService.php @@ -6,6 +6,12 @@ class LogoService public static function getLogoUrl(string $channelId): string { + $channelId = trim($channelId); + + if ($channelId === '') { + return '/assets/logos/default.svg'; + } + if (isset(self::$cache[$channelId])) { return self::$cache[$channelId]; } diff --git a/app/src/Services/ProgrammeService.php b/app/src/Services/ProgrammeService.php index d36816b..3bf252c 100644 --- a/app/src/Services/ProgrammeService.php +++ b/app/src/Services/ProgrammeService.php @@ -62,7 +62,13 @@ class ProgrammeService continue; } - foreach ($channel['programmes'] ?? [] as $p) { + $programmes = $channel['programmes'] ?? []; + + usort($programmes, function ($a, $b) { + return ((int)($a['start'] ?? 0)) <=> ((int)($b['start'] ?? 0)); + }); + + foreach ($programmes as $index => $p) { $start = (int)($p['start'] ?? 0); $stop = (int)($p['stop'] ?? 0); @@ -71,21 +77,36 @@ class ProgrammeService $elapsed = max(0, $now - $start); $percent = min(100, round(($elapsed / $duration) * 100)); + $next = $programmes[$index + 1] ?? null; + $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'] ?? '', 'subtitle' => $p['subtitle'] ?? '', 'description' => $p['description'] ?? '', 'category' => $p['category'] ?? '', + 'genre_icon' => $this->getGenreIcon($p['category'] ?? ''), 'metadata' => $p['metadata'] ?? [], + 'year' => $p['year'] ?? '', + 'rating' => $p['rating'] ?? '', + 'episode' => $p['episode'] ?? '', + 'quality' => $this->detectQuality($p), + 'start' => $start, 'stop' => $stop, 'percent' => $percent, 'remaining_minutes' => max(0, (int)ceil(($stop - $now) / 60)), + + 'next_title' => $next['title'] ?? '', + 'next_subtitle' => $next['subtitle'] ?? '', + 'next_start' => isset($next['start']) ? (int)$next['start'] : null, + 'next_stop' => isset($next['stop']) ? (int)$next['stop'] : null, + 'next_category' => $next['category'] ?? '', ]; break; @@ -191,4 +212,57 @@ class ProgrammeService return $items; } + + private function getGenreIcon(string $category): string + { + $category = mb_strtolower($category); + + return match (true) { + str_contains($category, 'film'), + str_contains($category, 'movie'), + str_contains($category, 'kino') => '🎬', + + str_contains($category, 'serie'), + str_contains($category, 'series') => '📺', + + str_contains($category, 'sport'), + str_contains($category, 'football'), + str_contains($category, 'fußball'), + str_contains($category, 'fussball') => '⚽', + + str_contains($category, 'news'), + str_contains($category, 'nachrichten'), + str_contains($category, 'journal') => '📰', + + str_contains($category, 'doku'), + str_contains($category, 'documentary') => '🌍', + + str_contains($category, 'kinder'), + str_contains($category, 'kids'), + str_contains($category, 'children') => '🧸', + + str_contains($category, 'musik'), + str_contains($category, 'music') => '🎵', + + default => '▶', + }; + } + + private function detectQuality(array $programme): string + { + $text = mb_strtolower( + ($programme['title'] ?? '') . ' ' . + ($programme['subtitle'] ?? '') . ' ' . + ($programme['description'] ?? '') + ); + + return match (true) { + str_contains($text, 'uhd'), + str_contains($text, '4k') => 'UHD', + + str_contains($text, 'hd') => 'HD', + + default => '', + }; + } } diff --git a/app/templates/day.php b/app/templates/day.php index 77fc46c..80ae5e0 100644 --- a/app/templates/day.php +++ b/app/templates/day.php @@ -123,12 +123,13 @@ foreach ($dayProgrammes as $channel) {
+
@@ -149,7 +150,7 @@ foreach ($dayProgrammes as $channel) {
+ + LIVE + + + -
@@ -173,13 +179,14 @@ foreach ($dayProgrammes as $channel) {
+
diff --git a/app/templates/now.php b/app/templates/now.php index 88e267b..8148022 100644 --- a/app/templates/now.php +++ b/app/templates/now.php @@ -21,16 +21,59 @@ $isFavoritesView = ($view ?? '') === 'favorites';
-
+
- -
-
- - - - - + + +
+ +
+
+ +
+ +
+ +
+ + + + + +
+ +
+ Kanal +
+ +
+ + – + +
+ +

@@ -39,25 +82,50 @@ $isFavoritesView = ($view ?? '') === 'favorites';
- - + +
+ + + +
- - + % + noch Minuten
- -
- % · noch Minuten -
+ + + +
+
Danach
+ +
+
+ +
+ +
+ + + +
+ +
+ +
+
+
+ + +