Feat: Adding Magazine mode to export.
This commit is contained in:
+120
-5
@@ -119,6 +119,47 @@ function export_hour_slots(DateTimeImmutable $start, DateTimeImmutable $end): ar
|
|||||||
return $slots;
|
return $slots;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function export_programme_weight(array $programme): int {
|
||||||
|
$duration = max(1, (int) round(($programme['stop']->getTimestamp() - $programme['start']->getTimestamp()) / 60));
|
||||||
|
$weight = min($duration, 240);
|
||||||
|
if (!empty($programme['subtitle'])) $weight += 10;
|
||||||
|
if (!empty($programme['categories'])) $weight += 8;
|
||||||
|
if (!empty($programme['desc'])) $weight += min(40, (int) floor(strlen($programme['desc']) / 20));
|
||||||
|
return $weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function export_build_magazine(array $channels): array {
|
||||||
|
$all = [];
|
||||||
|
foreach ($channels as $channel) {
|
||||||
|
foreach ($channel['programmes'] as $programme) {
|
||||||
|
$all[] = [
|
||||||
|
'channel_name' => $channel['name'],
|
||||||
|
'channel_icon' => $channel['icon'],
|
||||||
|
'programme' => $programme,
|
||||||
|
'weight' => export_programme_weight($programme),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($all, static function ($a, $b) {
|
||||||
|
return $b['weight'] <=> $a['weight']
|
||||||
|
?: $a['programme']['start'] <=> $b['programme']['start']
|
||||||
|
?: strcmp($a['channel_name'], $b['channel_name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
$highlights = array_slice($all, 0, 5);
|
||||||
|
$by_channel = [];
|
||||||
|
foreach ($channels as $channel) {
|
||||||
|
$by_channel[] = [
|
||||||
|
'name' => $channel['name'],
|
||||||
|
'icon' => $channel['icon'],
|
||||||
|
'programmes' => array_slice($channel['programmes'], 0, 6),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['highlights' => $highlights, 'channels' => $by_channel];
|
||||||
|
}
|
||||||
|
|
||||||
$sources = array_values($config['epg_sources'] ?? []);
|
$sources = array_values($config['epg_sources'] ?? []);
|
||||||
$source_index = max(0, min((int) ($_GET['source'] ?? 0), max(count($sources) - 1, 0)));
|
$source_index = max(0, min((int) ($_GET['source'] ?? 0), max(count($sources) - 1, 0)));
|
||||||
$source = $sources[$source_index] ?? null;
|
$source = $sources[$source_index] ?? null;
|
||||||
@@ -126,7 +167,7 @@ $target_day = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) ($_GET['day'] ?? ''))
|
|||||||
$layout_input = isset($_GET['layout']) ? (string) $_GET['layout'] : 'cards';
|
$layout_input = isset($_GET['layout']) ? (string) $_GET['layout'] : 'cards';
|
||||||
$limit_input = isset($_GET['limit']) ? (string) $_GET['limit'] : '8';
|
$limit_input = isset($_GET['limit']) ? (string) $_GET['limit'] : '8';
|
||||||
$window_input = isset($_GET['window']) ? (string) $_GET['window'] : 'full';
|
$window_input = isset($_GET['window']) ? (string) $_GET['window'] : 'full';
|
||||||
$layout = in_array($layout_input, ['cards', 'timeline'], true) ? $layout_input : 'cards';
|
$layout = in_array($layout_input, ['cards', 'timeline', 'magazine'], true) ? $layout_input : 'cards';
|
||||||
$limit = in_array($limit_input, ['4', '8', 'all'], true) ? $limit_input : '8';
|
$limit = in_array($limit_input, ['4', '8', 'all'], true) ? $limit_input : '8';
|
||||||
$windows = export_window_definitions($target_day);
|
$windows = export_window_definitions($target_day);
|
||||||
$window_key = array_key_exists($window_input, $windows) ? $window_input : 'full';
|
$window_key = array_key_exists($window_input, $windows) ? $window_input : 'full';
|
||||||
@@ -148,7 +189,8 @@ if ($limit !== 'all') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$day_label = (new DateTimeImmutable($target_day))->format('l d F Y');
|
$day_label = (new DateTimeImmutable($target_day))->format('l d F Y');
|
||||||
$layout_label = $layout === 'cards' ? 'Readable cards' : 'Dense timeline';
|
$layout_label = $layout === 'cards' ? 'Readable cards' : ($layout === 'timeline' ? 'Dense timeline' : 'TV magazine');
|
||||||
|
$magazine = export_build_magazine($visible_channels);
|
||||||
?><!DOCTYPE html>
|
?><!DOCTYPE html>
|
||||||
<html lang="<?= htmlspecialchars($locale) ?>">
|
<html lang="<?= htmlspecialchars($locale) ?>">
|
||||||
<head>
|
<head>
|
||||||
@@ -192,6 +234,26 @@ $layout_label = $layout === 'cards' ? 'Readable cards' : 'Dense timeline';
|
|||||||
.prog-title{font-size:16px;font-weight:700;line-height:1.08}
|
.prog-title{font-size:16px;font-weight:700;line-height:1.08}
|
||||||
.prog-sub,.prog-cats,.prog-desc{font-size:12px;color:var(--ink-soft);line-height:1.3;margin-top:2px}
|
.prog-sub,.prog-cats,.prog-desc{font-size:12px;color:var(--ink-soft);line-height:1.3;margin-top:2px}
|
||||||
.prog-desc{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
.prog-desc{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||||||
|
.magazine{display:grid;gap:18px}
|
||||||
|
.mag-hero{display:grid;grid-template-columns:1.3fr .9fr;gap:18px}
|
||||||
|
.mag-lead,.mag-side,.mag-channel{background:rgba(255,255,255,.35);border:1px solid var(--line);padding:16px}
|
||||||
|
.mag-kicker{font-family:'Share Tech Mono',monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--accent-2);margin-bottom:8px}
|
||||||
|
.mag-title{font-family:'IM Fell English',serif;font-size:34px;line-height:1.02;margin-bottom:10px}
|
||||||
|
.mag-meta{font-family:'Share Tech Mono',monospace;font-size:11px;color:var(--ink-soft);letter-spacing:.04em;margin-bottom:10px}
|
||||||
|
.mag-desc{font-size:14px;line-height:1.45;color:var(--ink-soft)}
|
||||||
|
.mag-side-list,.mag-channels{display:grid;gap:14px}
|
||||||
|
.mag-mini-title{font-size:18px;font-weight:700;line-height:1.08}
|
||||||
|
.mag-mini{padding-top:12px;border-top:1px dashed var(--line)}
|
||||||
|
.mag-mini:first-child{padding-top:0;border-top:none}
|
||||||
|
.mag-channels{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||||
|
.mag-channel-head{display:flex;gap:10px;align-items:center;border-bottom:2px solid var(--accent-2);padding-bottom:8px;margin-bottom:10px}
|
||||||
|
.mag-channel-head img{width:38px;height:38px;object-fit:contain;background:#fff;border:1px solid var(--line);padding:4px}
|
||||||
|
.mag-channel-name{font-size:20px;font-weight:700;line-height:1.05}
|
||||||
|
.mag-list{display:grid;gap:8px}
|
||||||
|
.mag-item{display:grid;grid-template-columns:74px 1fr;gap:10px;padding-top:8px;border-top:1px dashed var(--line)}
|
||||||
|
.mag-item:first-child{padding-top:0;border-top:none}
|
||||||
|
.mag-time{font-family:'Share Tech Mono',monospace;font-size:11px;color:var(--accent-2)}
|
||||||
|
.mag-item-title{font-size:15px;font-weight:700;line-height:1.08}
|
||||||
.timeline-wrap{display:grid;gap:14px}
|
.timeline-wrap{display:grid;gap:14px}
|
||||||
.timeline-row{display:grid;grid-template-columns:180px 1fr;gap:14px;align-items:stretch}
|
.timeline-row{display:grid;grid-template-columns:180px 1fr;gap:14px;align-items:stretch}
|
||||||
.timeline-channel{border-top:2px solid var(--accent-2);padding-top:8px}
|
.timeline-channel{border-top:2px solid var(--accent-2);padding-top:8px}
|
||||||
@@ -203,7 +265,7 @@ $layout_label = $layout === 'cards' ? 'Readable cards' : 'Dense timeline';
|
|||||||
.mini-title{font-size:13px;font-weight:700;line-height:1.05}
|
.mini-title{font-size:13px;font-weight:700;line-height:1.05}
|
||||||
.mini-meta{font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--ink-soft);margin-top:3px}
|
.mini-meta{font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--ink-soft);margin-top:3px}
|
||||||
@media print{body{background:#fff}.page{padding:0}.toolbar{display:none}.sheet{box-shadow:none;border:none;width:auto;margin:0}.sheet-wrap{overflow:visible}.cards-grid{grid-template-columns:1fr 1fr}}
|
@media print{body{background:#fff}.page{padding:0}.toolbar{display:none}.sheet{box-shadow:none;border:none;width:auto;margin:0}.sheet-wrap{overflow:visible}.cards-grid{grid-template-columns:1fr 1fr}}
|
||||||
@media (max-width:1200px){.sheet{width:100%}.cards-grid{grid-template-columns:1fr}.timeline-row{grid-template-columns:1fr}.timeline-grid{overflow:auto}}
|
@media (max-width:1200px){.sheet{width:100%}.cards-grid,.mag-channels,.mag-hero{grid-template-columns:1fr}.timeline-row{grid-template-columns:1fr}.timeline-grid{overflow:auto}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -233,6 +295,7 @@ $layout_label = $layout === 'cards' ? 'Readable cards' : 'Dense timeline';
|
|||||||
</select>
|
</select>
|
||||||
<select name="layout">
|
<select name="layout">
|
||||||
<option value="cards"<?= $layout === 'cards' ? ' selected' : '' ?>>Readable cards</option>
|
<option value="cards"<?= $layout === 'cards' ? ' selected' : '' ?>>Readable cards</option>
|
||||||
|
<option value="magazine"<?= $layout === 'magazine' ? ' selected' : '' ?>>TV magazine</option>
|
||||||
<option value="timeline"<?= $layout === 'timeline' ? ' selected' : '' ?>>Dense timeline</option>
|
<option value="timeline"<?= $layout === 'timeline' ? ' selected' : '' ?>>Dense timeline</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="ghost">Refresh</button>
|
<button type="submit" class="ghost">Refresh</button>
|
||||||
@@ -300,7 +363,7 @@ $cats = $programme['categories'] ? implode(' · ', $programme['categories']) : '
|
|||||||
</section>
|
</section>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php elseif ($layout === 'timeline'): ?>
|
||||||
<div class="timeline-wrap">
|
<div class="timeline-wrap">
|
||||||
<?php foreach ($visible_channels as $channel): ?>
|
<?php foreach ($visible_channels as $channel): ?>
|
||||||
<div class="timeline-row">
|
<div class="timeline-row">
|
||||||
@@ -336,9 +399,61 @@ $items = array_values(array_filter($channel['programmes'], static fn($p) => $p['
|
|||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="magazine">
|
||||||
|
<div class="mag-hero">
|
||||||
|
<section class="mag-lead">
|
||||||
|
<div class="mag-kicker">Cover story</div>
|
||||||
|
<?php if (!empty($magazine['highlights'][0])): $lead = $magazine['highlights'][0]; $p = $lead['programme']; ?>
|
||||||
|
<div class="mag-title"><?= htmlspecialchars($p['title']) ?></div>
|
||||||
|
<div class="mag-meta"><?= htmlspecialchars($lead['channel_name']) ?> · <?= htmlspecialchars($p['start']->format('H:i') . ' - ' . $p['stop']->format('H:i')) ?><?= $p['rating'] ? ' · ' . htmlspecialchars($p['rating']) : '' ?></div>
|
||||||
|
<?php if ($p['subtitle']): ?><div class="mag-desc" style="font-style:italic;margin-bottom:8px"><?= htmlspecialchars($p['subtitle']) ?></div><?php endif; ?>
|
||||||
|
<div class="mag-desc"><?= htmlspecialchars($p['desc'] ?: 'Programme mis en avant pour cette plage horaire.') ?></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="empty">No highlight available for this selection.</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
<aside class="mag-side">
|
||||||
|
<div class="mag-kicker">Tonight’s highlights</div>
|
||||||
|
<div class="mag-side-list">
|
||||||
|
<?php foreach (array_slice($magazine['highlights'], 1) as $entry): $p = $entry['programme']; ?>
|
||||||
|
<div class="mag-mini">
|
||||||
|
<div class="mag-mini-title"><?= htmlspecialchars($p['title']) ?></div>
|
||||||
|
<div class="mag-meta"><?= htmlspecialchars($entry['channel_name']) ?> · <?= htmlspecialchars($p['start']->format('H:i') . ' - ' . $p['stop']->format('H:i')) ?></div>
|
||||||
|
<?php if ($p['desc']): ?><div class="mag-desc"><?= htmlspecialchars(mb_strimwidth($p['desc'], 0, 180, '…')) ?></div><?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mag-kicker">Channel picks</div>
|
||||||
|
<div class="mag-channels">
|
||||||
|
<?php foreach ($magazine['channels'] as $channel): ?>
|
||||||
|
<section class="mag-channel">
|
||||||
|
<div class="mag-channel-head">
|
||||||
|
<?php if ($channel['icon']): ?><img src="<?= htmlspecialchars($channel['icon']) ?>" alt=""><?php endif; ?>
|
||||||
|
<div class="mag-channel-name"><?= htmlspecialchars($channel['name']) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="mag-list">
|
||||||
|
<?php foreach ($channel['programmes'] as $programme): ?>
|
||||||
|
<div class="mag-item">
|
||||||
|
<div class="mag-time"><?= htmlspecialchars($programme['start']->format('H:i')) ?><br><?= htmlspecialchars($programme['stop']->format('H:i')) ?></div>
|
||||||
|
<div>
|
||||||
|
<div class="mag-item-title"><?= htmlspecialchars($programme['title']) ?></div>
|
||||||
|
<?php if ($programme['subtitle']): ?><div class="prog-sub"><?= htmlspecialchars($programme['subtitle']) ?></div><?php endif; ?>
|
||||||
|
<?php if ($programme['categories']): ?><div class="prog-cats"><?= htmlspecialchars(implode(' · ', $programme['categories'])) ?></div><?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="note">Astuce: pour un rendu vraiment propre, choisis “Readable cards” avec 4 ou 8 chaînes, puis exporte en PDF.</div>
|
<div class="note">Astuce: “TV magazine” est parfait pour une belle feuille éditoriale. “Readable cards” reste le plus propre pour une impression exhaustive.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user