Reminder, Watchlist

This commit is contained in:
Gogs
2026-07-10 14:06:25 +02:00
parent 8795d9b607
commit 8b68ba2e9b
10 changed files with 1170 additions and 38 deletions
+293
View File
@@ -498,3 +498,296 @@
font: inherit;
font-weight: 800;
}
.sidebar-nav a {
position: relative;
}
.sidebar-count {
min-width: 22px;
margin-left: auto;
padding: 2px 7px;
color: #cbd5e1;
background: rgba(51, 65, 85, 0.8);
border-radius: 999px;
font-size: 0.72rem;
line-height: 1.35;
text-align: center;
}
.sidebar-nav a.active .sidebar-count {
color: white;
background: rgba(255, 255, 255, 0.2);
}
.watchlist-notifications {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 22px;
padding: 18px 20px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 16px;
}
.watchlist-notifications strong {
display: block;
margin-bottom: 5px;
}
.watchlist-notifications p {
margin: 0;
color: var(--muted);
font-size: 0.88rem;
}
.watchlist-notifications button {
flex: 0 0 auto;
padding: 10px 15px;
color: white;
background: var(--accent);
border: 1px solid var(--accent);
border-radius: 11px;
font-weight: 750;
cursor: pointer;
}
.watchlist-grid {
display: grid;
grid-template-columns:
repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
.watchlist-card {
position: relative;
min-width: 0;
display: flex;
align-items: stretch;
background: var(--panel);
border: 1px solid var(--border);
border-left-width: 5px;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
overflow: hidden;
transition:
border-color 0.16s ease,
background 0.16s ease,
transform 0.16s ease,
box-shadow 0.16s ease;
}
.watchlist-card:hover {
background: var(--panel-2);
transform: translateY(-2px);
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.22);
}
.watchlist-card-current {
border-left-color: #22c55e;
}
.watchlist-card-soon {
border-left-color: #f59e0b;
}
.watchlist-card-future {
border-left-color: #3b82f6;
}
.watchlist-card-past {
border-left-color: #64748b;
opacity: 0.72;
}
.watchlist-card-unknown {
border-left-color: var(--border);
}
.watchlist-card-open {
min-width: 0;
display: flex;
align-items: center;
flex: 1;
gap: 14px;
padding: 16px 46px 16px 16px;
color: var(--text);
background: transparent;
border: 0;
font: inherit;
text-align: left;
cursor: pointer;
}
.watchlist-logo {
flex: 0 0 auto;
width: 64px;
height: 48px;
padding: 5px;
object-fit: contain;
background: rgba(255, 255, 255, 0.94);
border-radius: 10px;
}
.watchlist-card-body {
min-width: 0;
display: flex;
flex: 1;
flex-direction: column;
gap: 5px;
}
.watchlist-card-heading {
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
}
.watchlist-card-heading strong {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.watchlist-subtitle,
.watchlist-details {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.watchlist-subtitle {
color: #cbd5e1;
font-size: 0.88rem;
}
.watchlist-details {
color: var(--muted);
font-size: 0.8rem;
}
.watchlist-status {
flex: 0 0 auto;
padding: 3px 7px;
border-radius: 999px;
font-size: 0.66rem;
font-weight: 800;
line-height: 1.25;
white-space: nowrap;
}
.watchlist-status-current {
color: #bbf7d0;
background: rgba(34, 197, 94, 0.18);
border: 1px solid rgba(34, 197, 94, 0.42);
}
.watchlist-status-soon {
color: #fde68a;
background: rgba(245, 158, 11, 0.18);
border: 1px solid rgba(245, 158, 11, 0.42);
}
.watchlist-status-future {
color: #bfdbfe;
background: rgba(59, 130, 246, 0.18);
border: 1px solid rgba(59, 130, 246, 0.42);
}
.watchlist-status-past,
.watchlist-status-unknown {
color: #cbd5e1;
background: rgba(100, 116, 139, 0.18);
border: 1px solid rgba(100, 116, 139, 0.42);
}
.watchlist-remove-button {
position: absolute;
top: 10px;
right: 10px;
width: 30px;
height: 30px;
padding: 0;
color: var(--muted);
background: rgba(15, 23, 42, 0.76);
border: 1px solid var(--border);
border-radius: 999px;
font-size: 1.2rem;
line-height: 1;
cursor: pointer;
}
.watchlist-remove-button:hover {
color: white;
background: rgba(239, 68, 68, 0.75);
border-color: rgba(239, 68, 68, 0.85);
}
@media (max-width: 620px) {
.watchlist-notifications {
align-items: stretch;
flex-direction: column;
}
.watchlist-notifications button {
width: 100%;
}
.watchlist-grid {
grid-template-columns: 1fr;
}
.watchlist-status {
display: none;
}
}
.watchlist-card-body strong,
.watchlist-card-body span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.watchlist-card-body strong {
font-size: 1rem;
}
+43
View File
@@ -111,3 +111,46 @@
font-size: 1.05rem;
font-weight: 700;
}
.programme-modal-actions {
display: flex;
justify-content: flex-end;
margin-top: 22px;
padding-top: 18px;
border-top: 1px solid var(--border);
}
.programme-watchlist-button {
padding: 10px 16px;
color: var(--text);
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 12px;
font-weight: 750;
cursor: pointer;
transition:
background 0.16s ease,
border-color 0.16s ease,
transform 0.16s ease;
}
.programme-watchlist-button:hover {
background: var(--accent-soft);
border-color: var(--accent);
}
.programme-watchlist-button:active {
transform: scale(0.98);
}
.programme-watchlist-button.is-saved {
color: #fef3c7;
background: rgba(245, 158, 11, 0.16);
border-color: rgba(245, 158, 11, 0.55);
}
+6 -1
View File
@@ -1,10 +1,15 @@
import { initNowLine } from "./modules/now.js";
import { initWatchlist } from "./modules/watchlist.js";
import { initProgrammeModal } from "./modules/modal.js";
import { initGuideSearch } from "./modules/search.js";
import { initTimelineZoom, initTimelineDrag } from "./modules/timeline.js";
import {
initTimelineZoom,
initTimelineDrag
} from "./modules/timeline.js";
document.addEventListener("DOMContentLoaded", () => {
initNowLine();
initWatchlist();
initProgrammeModal();
initGuideSearch();
initTimelineZoom();
+110 -37
View File
@@ -13,43 +13,79 @@ export function initProgrammeModal() {
const meta = document.getElementById("modalMeta");
const logo = document.getElementById("modalLogo");
const channel = document.getElementById("modalChannel");
const watchlistButton =
document.getElementById("modalWatchlistButton");
let currentProgramme = null;
const updateWatchlistButton = () => {
if (!watchlistButton || !currentProgramme) {
return;
}
const saved =
window.TVGuideWatchlist?.isSaved(currentProgramme) ||
false;
watchlistButton.classList.toggle("is-saved", saved);
watchlistButton.textContent = saved
? "★ Aus Merkliste entfernen"
: "☆ Zur Merkliste hinzufügen";
watchlistButton.setAttribute(
"aria-pressed",
saved ? "true" : "false"
);
};
const openProgramme = (programme) => {
if (!programme) {
return;
}
const metadata = programme.metadata || {};
currentProgramme =
window.TVGuideWatchlist?.normalize(programme) ||
programme;
title.textContent = programme.title || "";
const metadata = currentProgramme.metadata || {};
title.textContent =
currentProgramme.title || "";
subtitle.textContent =
metadata.episode_title ||
programme.subtitle ||
currentProgramme.subtitle ||
"";
subtitle.hidden = !subtitle.textContent;
description.textContent =
programme.description ||
currentProgramme.description ||
"Keine Beschreibung vorhanden.";
category.textContent = programme.category || "";
category.textContent =
currentProgramme.category || "";
category.hidden = !category.textContent;
const channelName =
programme.channel ||
programme.channel_name ||
currentProgramme.channel ||
currentProgramme.channel_name ||
"";
const channelNumber = programme.channel_number;
const channelNumber =
currentProgramme.channel_number;
channel.textContent = channelNumber && channelNumber !== 999999
? `Kanal ${channelNumber} · ${channelName}`
: channelName;
channel.textContent =
channelNumber && channelNumber !== 999999
? `Kanal ${channelNumber} · ${channelName}`
: channelName;
logo.src =
`/assets/logos/${encodeURIComponent(programme.channel_id || "")}.svg`;
`/assets/logos/${encodeURIComponent(
currentProgramme.channel_id || ""
)}.svg`;
logo.onerror = () => {
logo.onerror = null;
@@ -71,23 +107,26 @@ export function initProgrammeModal() {
if (metadata.country) {
metaItems.push(metadata.country);
} else if (programme.country) {
metaItems.push(programme.country);
} else if (currentProgramme.country) {
metaItems.push(currentProgramme.country);
}
if (metadata.year) {
metaItems.push(metadata.year);
} else if (programme.year) {
metaItems.push(programme.year);
} else if (currentProgramme.year) {
metaItems.push(currentProgramme.year);
}
if (metadata.age_rating) {
metaItems.push(`FSK ${metadata.age_rating}`);
} else if (programme.rating) {
metaItems.push(`FSK ${programme.rating}`);
} else if (currentProgramme.rating) {
metaItems.push(`FSK ${currentProgramme.rating}`);
}
if (Array.isArray(metadata.genres) && metadata.genres.length) {
if (
Array.isArray(metadata.genres) &&
metadata.genres.length
) {
metaItems.push(metadata.genres.join(", "));
}
@@ -95,18 +134,21 @@ export function initProgrammeModal() {
meta.hidden = metaItems.length === 0;
const start =
programme.start_time ||
programme.start ||
currentProgramme.start_time ||
currentProgramme.start ||
"";
const stop =
programme.stop_time ||
programme.stop ||
currentProgramme.stop_time ||
currentProgramme.stop ||
"";
time.textContent = start && stop
? `${start} ${stop}`
: "";
time.textContent =
start && stop
? `${start} ${stop}`
: "";
updateWatchlistButton();
modal.hidden = false;
document.body.classList.add("modal-open");
@@ -114,7 +156,8 @@ export function initProgrammeModal() {
const openProgrammeElement = (programmeElement) => {
const id = programmeElement.dataset.programmeId;
const programme = window.TVGUIDE_PROGRAMMES?.[id];
const programme =
window.TVGUIDE_PROGRAMMES?.[id];
openProgramme(programme);
};
@@ -122,12 +165,31 @@ export function initProgrammeModal() {
const closeModal = () => {
modal.hidden = true;
meta.textContent = "";
currentProgramme = null;
document.body.classList.remove("modal-open");
};
/*
* Öffentliche Schnittstelle für die globale Suche.
*/
watchlistButton?.addEventListener("click", () => {
if (
!currentProgramme ||
!window.TVGuideWatchlist
) {
return;
}
window.TVGuideWatchlist.toggle(
currentProgramme
);
updateWatchlistButton();
});
document.addEventListener(
"tvguide:watchlist-changed",
updateWatchlistButton
);
window.TVGuideProgrammeModal = {
open: openProgramme,
close: closeModal
@@ -145,13 +207,24 @@ export function initProgrammeModal() {
openProgrammeElement(programme);
});
modal.querySelectorAll("[data-modal-close]").forEach((element) => {
element.addEventListener("click", closeModal);
});
modal
.querySelectorAll("[data-modal-close]")
.forEach((element) => {
element.addEventListener(
"click",
closeModal
);
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !modal.hidden) {
closeModal();
document.addEventListener(
"keydown",
(event) => {
if (
event.key === "Escape" &&
!modal.hidden
) {
closeModal();
}
}
});
);
}
+677
View File
@@ -0,0 +1,677 @@
const STORAGE_KEY = "tvguide.watchlist.v1";
const NOTIFIED_KEY = "tvguide.watchlist.notified.v1";
const REMINDER_MINUTES = 10;
const UPCOMING_MINUTES = 30;
const CLEANUP_AFTER_HOURS = 24;
function readJson(key, fallback) {
try {
const value = JSON.parse(
localStorage.getItem(key) || JSON.stringify(fallback)
);
return value;
} catch (error) {
console.error(`LocalStorage konnte nicht gelesen werden: ${key}`, error);
return fallback;
}
}
function writeJson(key, value) {
localStorage.setItem(
key,
JSON.stringify(value)
);
}
function readItems() {
const items = readJson(STORAGE_KEY, []);
return Array.isArray(items) ? items : [];
}
function writeItems(items) {
writeJson(STORAGE_KEY, items);
document.dispatchEvent(
new CustomEvent("tvguide:watchlist-changed", {
detail: {
items
}
})
);
}
function readNotifiedIds() {
const ids = readJson(NOTIFIED_KEY, []);
return Array.isArray(ids) ? ids : [];
}
function writeNotifiedIds(ids) {
writeJson(NOTIFIED_KEY, ids);
}
function getTimestamp(programme, type) {
const directTimestamp =
programme[`${type}_timestamp`] ??
programme[`${type}_ts`] ??
(
typeof programme[type] === "number"
? programme[type]
: 0
);
const parsedDirect = Number(directTimestamp);
if (parsedDirect > 0) {
return parsedDirect;
}
/*
* Fallback für Programme mit Datum und Uhrzeit,
* aber ohne Unix-Zeitstempel.
*/
const date = programme.date || "";
const time =
programme[`${type}_time`] ||
(
typeof programme[type] === "string"
? programme[type]
: ""
);
if (!date || !time) {
return 0;
}
const parsedDate = new Date(`${date}T${time}:00`);
if (Number.isNaN(parsedDate.getTime())) {
return 0;
}
return Math.floor(parsedDate.getTime() / 1000);
}
function normalizeProgramme(programme) {
const startTimestamp =
getTimestamp(programme, "start");
const stopTimestamp =
getTimestamp(programme, "stop");
const channelName =
programme.channel_name ||
programme.channel ||
"";
const startTime =
programme.start_time ||
(
typeof programme.start === "string"
? programme.start
: ""
);
const stopTime =
programme.stop_time ||
(
typeof programme.stop === "string"
? programme.stop
: ""
);
const id =
programme.id ||
[
programme.channel_id || "",
startTimestamp || startTime,
programme.title || ""
].join("|");
return {
id,
channel_id: programme.channel_id || "",
channel_name: channelName,
channel: channelName,
channel_number: programme.channel_number ?? "",
channel_icon: programme.channel_icon || "",
favorite: Boolean(programme.favorite),
title: programme.title || "Ohne Titel",
subtitle: programme.subtitle || "",
description: programme.description || "",
category: programme.category || "",
metadata: programme.metadata || {},
year: programme.year || "",
rating: programme.rating || "",
country: programme.country || "",
date: programme.date || "",
start: startTime,
stop: stopTime,
start_time: startTime,
stop_time: stopTime,
start_timestamp: startTimestamp,
stop_timestamp: stopTimestamp
};
}
function cleanupExpiredItems() {
const now = Math.floor(Date.now() / 1000);
const cleanupDelay =
CLEANUP_AFTER_HOURS * 60 * 60;
const items = readItems();
const remainingItems = items.filter((item) => {
const stop = Number(item.stop_timestamp) || 0;
/*
* Sendungen ohne Endzeit nicht automatisch entfernen.
*/
if (!stop) {
return true;
}
return now < stop + cleanupDelay;
});
if (remainingItems.length !== items.length) {
writeItems(remainingItems);
}
/*
* Auch die Liste bereits gesendeter Erinnerungen aufräumen.
*/
const remainingIds = new Set(
remainingItems.map((item) => item.id)
);
const notifiedIds = readNotifiedIds().filter(
(id) => remainingIds.has(id)
);
writeNotifiedIds(notifiedIds);
}
function getItems() {
cleanupExpiredItems();
return readItems().sort((a, b) => {
const startA =
Number(a.start_timestamp) ||
Number.MAX_SAFE_INTEGER;
const startB =
Number(b.start_timestamp) ||
Number.MAX_SAFE_INTEGER;
return startA - startB;
});
}
function isSaved(programmeOrId) {
const id =
typeof programmeOrId === "string"
? programmeOrId
: normalizeProgramme(programmeOrId).id;
return readItems().some(
(item) => item.id === id
);
}
function add(programme) {
const normalized =
normalizeProgramme(programme);
const items = readItems();
if (!items.some((item) => item.id === normalized.id)) {
items.push(normalized);
writeItems(items);
}
return normalized;
}
function remove(programmeOrId) {
const id =
typeof programmeOrId === "string"
? programmeOrId
: normalizeProgramme(programmeOrId).id;
const items = readItems().filter(
(item) => item.id !== id
);
writeItems(items);
const notifiedIds = readNotifiedIds().filter(
(notifiedId) => notifiedId !== id
);
writeNotifiedIds(notifiedIds);
}
function toggle(programme) {
const normalized =
normalizeProgramme(programme);
if (isSaved(normalized.id)) {
remove(normalized.id);
return false;
}
add(normalized);
return true;
}
function logoUrl(channelId) {
return channelId
? `/assets/logos/${encodeURIComponent(channelId)}.svg`
: "/assets/logos/default.svg";
}
function formatDate(item) {
if (!item.date) {
return "";
}
const date = new Date(`${item.date}T12:00:00`);
if (Number.isNaN(date.getTime())) {
return item.date;
}
return date.toLocaleDateString("de-AT", {
weekday: "short",
day: "2-digit",
month: "2-digit",
year: "numeric"
});
}
function getProgrammeStatus(item) {
const now = Math.floor(Date.now() / 1000);
const start = Number(item.start_timestamp) || 0;
const stop = Number(item.stop_timestamp) || 0;
if (!start || !stop) {
return {
key: "unknown",
label: "Zeit unbekannt"
};
}
if (now >= start && now < stop) {
const remainingMinutes =
Math.max(1, Math.ceil((stop - now) / 60));
return {
key: "current",
label: `Läuft gerade · noch ${remainingMinutes} Min.`
};
}
if (now < start) {
const minutes =
Math.ceil((start - now) / 60);
if (minutes <= UPCOMING_MINUTES) {
return {
key: "soon",
label: `Beginnt in ${minutes} Min.`
};
}
return {
key: "future",
label: "Vorgemerkt"
};
}
return {
key: "past",
label: "Bereits vorbei"
};
}
function createStatusBadge(item) {
const status = getProgrammeStatus(item);
const badge = document.createElement("span");
badge.className =
`watchlist-status watchlist-status-${status.key}`;
badge.textContent = status.label;
return badge;
}
function renderWatchlistPage() {
const grid =
document.getElementById("watchlistGrid");
const empty =
document.getElementById("watchlistEmpty");
if (!grid || !empty) {
return;
}
const items = getItems();
grid.innerHTML = "";
empty.hidden = items.length > 0;
items.forEach((item) => {
const card = document.createElement("article");
const status = getProgrammeStatus(item);
card.className =
`watchlist-card watchlist-card-${status.key}`;
const openButton = document.createElement("button");
openButton.type = "button";
openButton.className = "watchlist-card-open";
const logo = document.createElement("img");
logo.className = "watchlist-logo";
logo.src = logoUrl(item.channel_id);
logo.alt = "";
logo.onerror = () => {
logo.onerror = null;
logo.src = "/assets/logos/default.svg";
};
const body = document.createElement("span");
body.className = "watchlist-card-body";
const headingRow = document.createElement("span");
headingRow.className =
"watchlist-card-heading";
const title = document.createElement("strong");
title.textContent = item.title;
const badge = createStatusBadge(item);
headingRow.append(title, badge);
const subtitle = document.createElement("span");
subtitle.className = "watchlist-subtitle";
subtitle.textContent = item.subtitle;
subtitle.hidden = !item.subtitle;
const details = document.createElement("span");
details.className = "watchlist-details";
const date = formatDate(item);
const time =
item.start_time && item.stop_time
? `${item.start_time}${item.stop_time}`
: "";
details.textContent = [
item.channel_name,
date,
time
].filter(Boolean).join(" · ");
body.append(
headingRow,
subtitle,
details
);
openButton.append(logo, body);
openButton.addEventListener("click", () => {
window.TVGuideProgrammeModal?.open(item);
});
const removeButton =
document.createElement("button");
removeButton.type = "button";
removeButton.className =
"watchlist-remove-button";
removeButton.textContent = "×";
removeButton.title = "Aus Merkliste entfernen";
removeButton.setAttribute(
"aria-label",
`${item.title} aus Merkliste entfernen`
);
removeButton.addEventListener("click", () => {
remove(item.id);
});
card.append(
openButton,
removeButton
);
grid.appendChild(card);
});
}
function updateSidebarCount() {
const count =
document.getElementById("watchlistCount");
if (!count) {
return;
}
count.textContent =
String(getItems().length);
}
function updateNotificationButton() {
const button =
document.getElementById("enableWatchlistNotifications");
const status =
document.getElementById("watchlistNotificationStatus");
if (!button || !status) {
return;
}
if (!("Notification" in window)) {
button.hidden = true;
status.textContent =
"Dieser Browser unterstützt keine Systembenachrichtigungen.";
return;
}
if (Notification.permission === "granted") {
button.hidden = true;
status.textContent =
`Erinnerungen sind aktiv: ${REMINDER_MINUTES} Minuten vor Beginn.`;
return;
}
if (Notification.permission === "denied") {
button.hidden = true;
status.textContent =
"Benachrichtigungen wurden im Browser blockiert.";
return;
}
button.hidden = false;
status.textContent =
`Erinnerung ${REMINDER_MINUTES} Minuten vor Beginn aktivieren.`;
}
async function requestNotificationPermission() {
if (!("Notification" in window)) {
return;
}
try {
await Notification.requestPermission();
} catch (error) {
console.error(
"Benachrichtigungsfreigabe fehlgeschlagen:",
error
);
}
updateNotificationButton();
checkReminders();
}
function showReminder(item) {
const time =
item.start_time
? ` um ${item.start_time}`
: "";
const channel =
item.channel_name
? ` auf ${item.channel_name}`
: "";
const notification = new Notification(
`${item.title} beginnt bald`,
{
body:
`${item.title} beginnt${time}${channel}.`,
icon: logoUrl(item.channel_id),
tag: `tvguide-${item.id}`
}
);
notification.onclick = () => {
window.focus();
window.TVGuideProgrammeModal?.open(item);
notification.close();
};
}
function checkReminders() {
if (
!("Notification" in window) ||
Notification.permission !== "granted"
) {
return;
}
const now = Math.floor(Date.now() / 1000);
const reminderSeconds =
REMINDER_MINUTES * 60;
const notifiedIds =
readNotifiedIds();
const notifiedSet =
new Set(notifiedIds);
let changed = false;
getItems().forEach((item) => {
const start =
Number(item.start_timestamp) || 0;
if (!start || notifiedSet.has(item.id)) {
return;
}
const secondsUntilStart =
start - now;
/*
* Erinnerung innerhalb des Erinnerungsfensters.
* Bis zwei Minuten nach Sendungsbeginn wird ebenfalls
* erinnert, falls die Seite verspätet geöffnet wurde.
*/
if (
secondsUntilStart <= reminderSeconds &&
secondsUntilStart >= -120
) {
showReminder(item);
notifiedSet.add(item.id);
changed = true;
}
});
if (changed) {
writeNotifiedIds(
Array.from(notifiedSet)
);
}
}
function refreshWatchlist() {
cleanupExpiredItems();
renderWatchlistPage();
updateSidebarCount();
checkReminders();
}
export function initWatchlist() {
window.TVGuideWatchlist = {
getItems,
normalize: normalizeProgramme,
isSaved,
add,
remove,
toggle,
cleanup: cleanupExpiredItems,
checkReminders
};
const notificationButton =
document.getElementById(
"enableWatchlistNotifications"
);
notificationButton?.addEventListener(
"click",
requestNotificationPermission
);
refreshWatchlist();
updateNotificationButton();
document.addEventListener(
"tvguide:watchlist-changed",
() => {
renderWatchlistPage();
updateSidebarCount();
checkReminders();
}
);
/*
* Statusanzeigen und Erinnerungen regelmäßig aktualisieren.
*/
window.setInterval(
refreshWatchlist,
30_000
);
}
+1
View File
@@ -66,6 +66,7 @@ class HomeController
'now' => '/var/www/templates/now.php',
'favorites' => '/var/www/templates/now.php',
'day' => '/var/www/templates/day.php',
'watchlist' => '/var/www/templates/watchlist.php',
default => '/var/www/templates/home.php',
};
+3
View File
@@ -57,6 +57,9 @@ foreach ($dayProgrammes as $channel) {
'year' => $programme['metadata']['year'] ?? '',
'rating' => $programme['metadata']['age_rating'] ?? '',
'country' => $programme['metadata']['country'] ?? '',
'date' => $selectedDate,
'start_timestamp' => $programme['start'],
'stop_timestamp' => $programme['stop'],
'start' => date('H:i', $programme['start']),
'stop' => date('H:i', $programme['stop']),
];
@@ -18,5 +18,13 @@
<div id="modalCategory" class="programme-modal-category"></div>
<div id="modalMeta" class="programme-modal-meta"></div>
<p id="modalDescription"></p>
<div class="programme-modal-actions">
<button
id="modalWatchlistButton"
class="programme-watchlist-button"
type="button">
Zur Merkliste hinzufügen
</button>
</div>
</div>
</div>
+1
View File
@@ -4,6 +4,7 @@
<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=watchlist" class="<?= ($view ?? '') === 'watchlist' ? 'active' : '' ?>"> 🔖 Merkliste<span id="watchlistCount" class="sidebar-count">0</span></a>
<a href="/?view=channels" class="<?= ($view ?? '') === 'channels' ? 'active' : '' ?>">📺 Sender</a>
<a href="/?view=settings" class="<?= ($view ?? '') === 'settings' ? 'active' : '' ?>"> Einstellungen</a>
</nav>
+28
View File
@@ -0,0 +1,28 @@
<section class="page-heading">
<h2>Merkliste</h2>
<p>Sendungen, die du später ansehen möchtest.</p>
</section>
<section class="watchlist-notifications">
<div>
<strong>🔔 Erinnerungen</strong>
<p id="watchlistNotificationStatus">
Erinnerung vor Sendungsbeginn aktivieren.
</p>
</div>
<button
id="enableWatchlistNotifications"
type="button">
Erinnerungen aktivieren
</button>
</section>
<div id="watchlistEmpty" class="empty" hidden>
Deine Merkliste ist noch leer.
</div>
<div id="watchlistGrid" class="watchlist-grid"></div>
<?php require __DIR__ . '/partials/programme-modal.php'; ?>