// ── 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(); }