Style
This commit is contained in:
@@ -225,7 +225,7 @@
|
||||
"icon": "",
|
||||
"country": "",
|
||||
"language": "",
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"favorite": false,
|
||||
"number": 210,
|
||||
"group": "Österreich"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 = `
|
||||
<div class="now-channel">
|
||||
<span data-live-channel-name></span>
|
||||
<span data-live-favorite></span>
|
||||
<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-time" data-live-time></div>
|
||||
<div class="now-badges" data-live-badges></div>
|
||||
|
||||
<div class="progress">
|
||||
<div class="progress-bar" data-live-progress></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);
|
||||
@@ -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 => `<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) {
|
||||
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', () => {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 80">
|
||||
<rect width="120" height="80" rx="12" fill="#1f2937"/>
|
||||
<rect x="18" y="18" width="84" height="48" rx="6" fill="#111827" stroke="#334155" stroke-width="4"/>
|
||||
<path d="M44 32l24 8-24 8z" fill="#94a3b8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 276 B |
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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 => '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+10
-3
@@ -123,12 +123,13 @@ foreach ($dayProgrammes as $channel) {
|
||||
</div>
|
||||
|
||||
<?php foreach ($dayProgrammes as $channel): ?>
|
||||
<?php $channelLogoUrl = LogoService::getLogoUrl($channel['channel_id'] ?? $channel['id'] ?? ''); ?>
|
||||
<div class="timeline-row">
|
||||
|
||||
<div class="timeline-channel">
|
||||
<img
|
||||
class="channel-logo"
|
||||
src="<?= htmlspecialchars(LogoService::getLogoUrl($channel['channel_id'])) ?>"
|
||||
src="<?= htmlspecialchars($channelLogoUrl) ?>"
|
||||
alt="<?= htmlspecialchars($channel['channel_name']) ?>">
|
||||
|
||||
<span class="timeline-channel-name"><?= htmlspecialchars($channel['channel_name']) ?></span>
|
||||
@@ -149,7 +150,7 @@ foreach ($dayProgrammes as $channel) {
|
||||
|
||||
<?php foreach ($channel['programmes'] as $programme): ?>
|
||||
<div
|
||||
class="timeline-programme <?= $programme['is_current'] ? 'is-current' : '' ?> <?= $programme['is_past'] ? 'is-past' : '' ?>"
|
||||
class="timeline-programme <?= $programme['is_current'] ? 'is-current' : '' ?> <?= $programme['is_past'] ? 'is-past' : '' ?> genre-<?= htmlspecialchars(strtolower(preg_replace('/[^a-z0-9]+/i', '-', $programme['category'] ?: 'other'))) ?>"
|
||||
data-programme-id="<?= htmlspecialchars($programme['id']) ?>"
|
||||
data-live-programme
|
||||
data-live-start="<?= (int)$programme['start'] ?>"
|
||||
@@ -160,6 +161,11 @@ foreach ($dayProgrammes as $channel) {
|
||||
"
|
||||
title="<?= htmlspecialchars(date('H:i', $programme['start']) . ' - ' . date('H:i', $programme['stop']) . ' · ' . $programme['title']) ?>"
|
||||
>
|
||||
<?php if ($programme['is_current']): ?>
|
||||
<em class="live-badge" data-live-badge>LIVE</em>
|
||||
<?php else: ?>
|
||||
<em class="live-badge" data-live-badge hidden>LIVE</em>
|
||||
<?php endif; ?>
|
||||
<strong><?= htmlspecialchars($programme['title']) ?></strong>
|
||||
<span><?= date('H:i', $programme['start']) ?> - <?= date('H:i', $programme['stop']) ?></span>
|
||||
</div>
|
||||
@@ -173,13 +179,14 @@ foreach ($dayProgrammes as $channel) {
|
||||
|
||||
<div class="day-list">
|
||||
<?php foreach ($dayProgrammes as $channel): ?>
|
||||
<?php $channelLogoUrl = LogoService::getLogoUrl($channel['channel_id'] ?? $channel['id'] ?? ''); ?>
|
||||
<section class="day-channel">
|
||||
<div class="day-channel-header">
|
||||
<div>
|
||||
<span class="channel-number"><?= (int)$channel['channel_number'] ?></span>
|
||||
<img
|
||||
class="channel-logo"
|
||||
src="<?= htmlspecialchars(LogoService::getLogoUrl($channel['channel_id'])) ?>"
|
||||
src="<?= htmlspecialchars($channelLogoUrl) ?>"
|
||||
alt="<?= htmlspecialchars($channel['channel_name']) ?>">
|
||||
|
||||
<strong><?= htmlspecialchars($channel['channel_name']) ?></strong>
|
||||
|
||||
+85
-17
@@ -21,16 +21,59 @@ $isFavoritesView = ($view ?? '') === 'favorites';
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="now-grid" data-live-now-grid>
|
||||
<div class="now-grid now-grid-enhanced" data-live-now-grid>
|
||||
<?php foreach ($currentProgrammes as $item): ?>
|
||||
<?php $logoUrl = LogoService::getLogoUrl($item['channel_id']); ?>
|
||||
<article class="now-card">
|
||||
<div class="now-channel">
|
||||
<img class="mini-channel-logo" src="<?= htmlspecialchars($logoUrl) ?>" alt="">
|
||||
<span><?= htmlspecialchars($item['channel_name']) ?></span>
|
||||
<?php if ($item['favorite']): ?>
|
||||
<span class="favorite-star">⭐</span>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$logoUrl = LogoService::getLogoUrl($item['channel_id']);
|
||||
$badges = [];
|
||||
|
||||
if (!empty($item['category'])) {
|
||||
$badges[] = ($item['genre_icon'] ?? '▶') . ' ' . $item['category'];
|
||||
}
|
||||
|
||||
if (!empty($item['year'])) {
|
||||
$badges[] = $item['year'];
|
||||
}
|
||||
|
||||
if (!empty($item['rating'])) {
|
||||
$badges[] = 'FSK ' . $item['rating'];
|
||||
}
|
||||
|
||||
if (!empty($item['quality'])) {
|
||||
$badges[] = $item['quality'];
|
||||
}
|
||||
?>
|
||||
|
||||
<article class="now-card now-card-enhanced">
|
||||
|
||||
<div class="now-card-top">
|
||||
<div class="now-channel-logo-wrap">
|
||||
<img class="now-channel-logo"
|
||||
src=" <?= htmlspecialchars($logoUrl) ?>"
|
||||
alt="">
|
||||
</div>
|
||||
|
||||
<div class="now-channel-info">
|
||||
|
||||
<div class="now-channel-name">
|
||||
<?= htmlspecialchars($item['channel_name']) ?>
|
||||
|
||||
<?php if ($item['favorite']): ?>
|
||||
<span class="favorite-star">⭐ </span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="now-channel-number">
|
||||
Kanal <?= (int)$item['channel_number'] ?>
|
||||
</div>
|
||||
|
||||
<div class="now-time">
|
||||
<?= date('H:i', $item['start']) ?>
|
||||
–
|
||||
<?= date('H:i', $item['stop']) ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3><?= htmlspecialchars($item['title']) ?></h3>
|
||||
@@ -39,25 +82,50 @@ $isFavoritesView = ($view ?? '') === 'favorites';
|
||||
<div class="now-subtitle"><?= htmlspecialchars($item['subtitle']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item['category']): ?>
|
||||
<span class="category"><?= htmlspecialchars($item['category']) ?></span>
|
||||
<?php if ($badges): ?>
|
||||
<div class="now-badges">
|
||||
<?php foreach ($badges as $badge): ?>
|
||||
<span class="now-badge"><?= htmlspecialchars($badge) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-meta">
|
||||
<span><?= date('H:i', $item['start']) ?></span>
|
||||
<span><?= date('H:i', $item['stop']) ?></span>
|
||||
<span><?= (int)$item['percent'] ?>%</span>
|
||||
<span>noch <?= (int)$item['remaining_minutes'] ?> Minuten</span>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: <?= (int)$item['percent'] ?>%"></div>
|
||||
</div>
|
||||
|
||||
<div class="remaining">
|
||||
<?= (int)$item['percent'] ?>% · noch <?= (int)$item['remaining_minutes'] ?> Minuten
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($item['next_title'])): ?>
|
||||
|
||||
|
||||
<div class="now-next">
|
||||
<div class="now-next-label">Danach</div>
|
||||
|
||||
<div class="now-next-programme">
|
||||
<div class="now-next-time">
|
||||
<?= !empty($item['next_start']) ? date('H:i', $item['next_start']) : '' ?>
|
||||
</div>
|
||||
|
||||
<div class="now-next-info">
|
||||
<strong><?= htmlspecialchars($item['next_title']) ?></strong>
|
||||
|
||||
<?php if (!empty($item['next_subtitle'])): ?>
|
||||
<div class="now-next-subtitle">
|
||||
<?= htmlspecialchars($item['next_subtitle']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item['description']): ?>
|
||||
<p class="now-desc"><?= htmlspecialchars($item['description']) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
Reference in New Issue
Block a user