203 lines
7.3 KiB
JavaScript
203 lines
7.3 KiB
JavaScript
// ── PROGRAM REMINDERS ────────────────────────────────────────────────────────
|
|
let currentReminderProgram = null;
|
|
let reminderTimers = {};
|
|
|
|
function reminderProgramId(ch, p) {
|
|
return [currentEpgUrl || 'default', ch.id || ch.name, p.start?.getTime() || 0, p.title || ''].join('::');
|
|
}
|
|
|
|
function reminderPayload(ch, p) {
|
|
return {
|
|
id: reminderProgramId(ch, p),
|
|
source: currentEpgUrl || '',
|
|
channelId: ch.id || '',
|
|
channelName: ch.name || '',
|
|
title: p.title || '',
|
|
subtitle: p.subtitle || '',
|
|
start: p.start?.getTime() || 0,
|
|
stop: p.stop?.getTime() || 0,
|
|
notifyAt: Math.max(Date.now() + 5000, (p.start?.getTime() || 0) - REMINDER_OFFSET_MS)
|
|
};
|
|
}
|
|
|
|
function getStoredReminders() {
|
|
try {
|
|
const parsed = JSON.parse(localStorage.getItem(REMINDER_STORAGE_KEY) || '[]');
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch (_) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function saveStoredReminders(reminders) {
|
|
localStorage.setItem(REMINDER_STORAGE_KEY, JSON.stringify(reminders));
|
|
}
|
|
|
|
function findStoredReminder(id) {
|
|
return getStoredReminders().find(item => item.id === id) || null;
|
|
}
|
|
|
|
function upsertReminder(reminder) {
|
|
const reminders = getStoredReminders().filter(item => item.id !== reminder.id);
|
|
reminders.push(reminder);
|
|
saveStoredReminders(reminders);
|
|
}
|
|
|
|
function removeReminder(id) {
|
|
if (reminderTimers[id]) {
|
|
clearTimeout(reminderTimers[id]);
|
|
delete reminderTimers[id];
|
|
}
|
|
saveStoredReminders(getStoredReminders().filter(item => item.id !== id));
|
|
updateReminderPanel();
|
|
}
|
|
|
|
function pruneExpiredReminders() {
|
|
const now = Date.now();
|
|
const reminders = getStoredReminders().filter(item => (item.stop || item.start || 0) > now);
|
|
saveStoredReminders(reminders);
|
|
return reminders;
|
|
}
|
|
|
|
async function ensureReminderPermission() {
|
|
if (!('Notification' in window)) {
|
|
throw new Error(L.reminder_error_unsupported || 'Notifications are not supported on this browser.');
|
|
}
|
|
if (Notification.permission === 'granted') return true;
|
|
if (Notification.permission === 'denied') {
|
|
throw new Error(L.reminder_error_denied || 'Notifications were blocked in this browser.');
|
|
}
|
|
const result = await Notification.requestPermission();
|
|
if (result !== 'granted') {
|
|
throw new Error(L.reminder_error_denied || 'Notifications were blocked in this browser.');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function showReminderNotification(reminder) {
|
|
const title = L.reminder_notification_title || 'Program reminder';
|
|
const body = (L.reminder_notification_body || '“{title}” starts at {time} on {channel}.')
|
|
.replace('{title}', reminder.title || '')
|
|
.replace('{time}', fmtTime(new Date(reminder.start)))
|
|
.replace('{channel}', reminder.channelName || '');
|
|
|
|
try {
|
|
if ('serviceWorker' in navigator) {
|
|
const registration = await navigator.serviceWorker.ready;
|
|
await registration.showNotification(title, {
|
|
body,
|
|
tag: reminder.id,
|
|
icon: '/assets/icon-192.png',
|
|
badge: '/assets/icon-192.png',
|
|
data: {
|
|
url: '/index.php',
|
|
reminderId: reminder.id
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
} catch (_) {}
|
|
|
|
if ('Notification' in window && Notification.permission === 'granted') {
|
|
new Notification(title, { body, icon: '/assets/icon-192.png', tag: reminder.id });
|
|
}
|
|
}
|
|
|
|
function scheduleReminder(reminder) {
|
|
if (!reminder?.id) return;
|
|
if (reminderTimers[reminder.id]) clearTimeout(reminderTimers[reminder.id]);
|
|
const delay = reminder.notifyAt - Date.now();
|
|
if (delay <= 0) {
|
|
showReminderNotification(reminder).finally(() => removeReminder(reminder.id));
|
|
return;
|
|
}
|
|
reminderTimers[reminder.id] = setTimeout(() => {
|
|
showReminderNotification(reminder).finally(() => removeReminder(reminder.id));
|
|
}, delay);
|
|
}
|
|
|
|
function refreshReminderSchedules() {
|
|
Object.keys(reminderTimers).forEach(id => {
|
|
clearTimeout(reminderTimers[id]);
|
|
delete reminderTimers[id];
|
|
});
|
|
pruneExpiredReminders().forEach(scheduleReminder);
|
|
updateReminderPanel();
|
|
}
|
|
|
|
function reminderTimingText(p) {
|
|
const startMs = p && typeof p.start === 'number'
|
|
? p.start
|
|
: (p?.start?.getTime ? p.start.getTime() : 0);
|
|
const notifyAt = Math.max(Date.now() + 5000, startMs - REMINDER_OFFSET_MS);
|
|
const deltaMinutes = Math.max(0, Math.round((startMs - notifyAt) / 60000));
|
|
if (deltaMinutes <= 1) return L.reminder_when_soon || 'This program starts soon. The reminder will fire almost immediately.';
|
|
return (L.reminder_when || 'A browser notification will be sent {minutes} minutes before broadcast.')
|
|
.replace('{minutes}', String(deltaMinutes));
|
|
}
|
|
|
|
function updateReminderPanel(message) {
|
|
const panel = document.getElementById('pm-reminder-panel');
|
|
const submenu = document.getElementById('pm-reminder-submenu');
|
|
const text = document.getElementById('pm-reminder-text');
|
|
const status = document.getElementById('pm-reminder-status');
|
|
const removeBtn = document.getElementById('pm-reminder-remove');
|
|
const saveBtn = document.getElementById('pm-reminder-save');
|
|
if (!panel || !submenu || !text || !status || !removeBtn || !saveBtn) return;
|
|
|
|
if (!currentReminderProgram) {
|
|
panel.style.display = 'none';
|
|
submenu.classList.remove('visible');
|
|
return;
|
|
}
|
|
|
|
panel.style.display = 'block';
|
|
const reminder = findStoredReminder(currentReminderProgram.id);
|
|
const hasStarted = currentReminderProgram.start <= Date.now();
|
|
|
|
text.textContent = reminderTimingText(currentReminderProgram);
|
|
status.textContent = message || (reminder
|
|
? (L.reminder_status_set || 'Reminder active for {time}.').replace('{time}', fmtTime(new Date(reminder.notifyAt)))
|
|
: (hasStarted ? (L.reminder_status_started || 'This program is already airing.') : (L.reminder_status_idle || 'No reminder active.')));
|
|
status.className = 'pm-reminder-status' + (reminder ? ' active' : '') + (hasStarted ? ' muted' : '');
|
|
removeBtn.style.display = reminder ? 'inline-flex' : 'none';
|
|
saveBtn.disabled = hasStarted;
|
|
}
|
|
|
|
async function saveCurrentReminder() {
|
|
if (!currentReminderProgram) return;
|
|
try {
|
|
await ensureReminderPermission();
|
|
upsertReminder(currentReminderProgram);
|
|
scheduleReminder(currentReminderProgram);
|
|
updateReminderPanel(L.reminder_saved || 'Reminder enabled.');
|
|
} catch (error) {
|
|
updateReminderPanel(error.message || (L.reminder_error_generic || 'Unable to enable reminder.'));
|
|
}
|
|
}
|
|
|
|
function toggleReminderSubmenu() {
|
|
const submenu = document.getElementById('pm-reminder-submenu');
|
|
if (!submenu || !currentReminderProgram) return;
|
|
submenu.classList.toggle('visible');
|
|
updateReminderPanel();
|
|
}
|
|
|
|
function bindReminderPanel(ch, p) {
|
|
currentReminderProgram = reminderPayload(ch, p);
|
|
const toggleBtn = document.getElementById('pm-reminder-toggle');
|
|
const saveBtn = document.getElementById('pm-reminder-save');
|
|
const removeBtn = document.getElementById('pm-reminder-remove');
|
|
const submenu = document.getElementById('pm-reminder-submenu');
|
|
if (!toggleBtn || !saveBtn || !removeBtn || !submenu) return;
|
|
|
|
submenu.classList.remove('visible');
|
|
toggleBtn.onclick = () => toggleReminderSubmenu();
|
|
saveBtn.onclick = () => saveCurrentReminder();
|
|
removeBtn.onclick = () => {
|
|
if (currentReminderProgram) removeReminder(currentReminderProgram.id);
|
|
updateReminderPanel(L.reminder_removed || 'Reminder removed.');
|
|
};
|
|
updateReminderPanel();
|
|
}
|