diff --git a/config/ajaxModule.inc.php b/config/ajaxModule.inc.php index c5f1a41..81c9507 100644 --- a/config/ajaxModule.inc.php +++ b/config/ajaxModule.inc.php @@ -7,63 +7,23 @@ $is_ajax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTT // Se è una richiesta AJAX, carica solo il contenuto della pagina if ($is_ajax) { - switch ($page) { - case 'home': - include './pages/page/home.php'; - break; - case 'radio': - include './pages/page/radio.php'; - break; - case 'tv': - include './pages/page/tv.php'; - break; - case 'play': - $stationId = (int)$param; - $station = getRadioStation($stationId); - if ($station !== null) { - include './pages/page/player.php'; - } else { - include './pages/page/404.php'; - } - break; - case 'playtv': - $stationId = (int)$param; - $station = getTVStation($stationId); - if ($station !== null) { - include './pages/page/player_tv.php'; - } else { - include './pages/page/404.php'; - } - break; - case 'podcast': - include './pages/page/podcast.php'; - break; - case 'page': - switch ($param) { - case 'about': - include './pages/page/about.php'; - break; - case 'contact': - include './pages/page/contact.php'; - break; - case 'copyright': - include './pages/page/copyright.php'; - break; - case 'termini-condizioni': - include './pages/page/terminicondizioni.php'; - break; - case 'policy-privacy': - include './pages/page/policyprivacy.php'; - break; - case 'changelog': - include './pages/page/changelog.php'; - break; - default: - include './pages/page/404.php'; - } - break; - default: - include './pages/page/404.php'; + $filePath = getPageFilePath($page, $param); + + // Validazione specifica delle stazioni radio e tv per sicurezza + if ($page === 'play' && !empty($param)) { + $stationId = (int)$param; + $station = getRadioStation($stationId); + if ($station === null) { + $filePath = './pages/page/404.php'; + } + } elseif ($page === 'playtv' && !empty($param)) { + $stationId = (int)$param; + $station = getTVStation($stationId); + if ($station === null) { + $filePath = './pages/page/404.php'; + } } + + include $filePath; exit; // Termina l'esecuzione per le richieste AJAX } \ No newline at end of file diff --git a/config/getPage.inc.php b/config/getPage.inc.php index f372805..cd18a58 100644 --- a/config/getPage.inc.php +++ b/config/getPage.inc.php @@ -4,10 +4,6 @@ header('Content-Type: text/html; charset=UTF-8'); // File: config/getPage.inc.php -// Whitelist delle pagine valide -$validPages = ['home', 'radio', 'tv', 'play', 'playtv', 'page', 'podcast']; -$validSubPages = ['about', 'contact', 'copyright', 'termini-condizioni', 'policy-privacy', 'changelog']; - // Rileva se l'utente sta usando un dispositivo mobile function isMobile() { return preg_match("/(android|avantgo|blackberry|bolt|boost|cricket|docomo|fone|hiptop|mini|mobi|palm|phone|pie|tablet|up\.browser|up\.link|webos|wos)/i", $_SERVER["HTTP_USER_AGENT"]); @@ -18,8 +14,7 @@ function sanitizePageInput($input) { // Rimuovi caratteri pericolosi $input = preg_replace('/[^a-zA-Z0-9\-_]/', '', $input); // Previeni path traversal - $input = str_replace(['..', '/', '\\'], '', $input); - return $input; + return str_replace(['..', '/', '\\'], '', $input); } // Recupera l'URL richiesto @@ -40,45 +35,71 @@ if (isset($path_parts[0]) && $path_parts[0] == 'index.php') { array_shift($path_parts); } -// Determina la pagina da mostrare in base all'URL con validazione -$page = 'home'; // Default sicuro +// Determina la pagina e il parametro iniziale +$page = 'home'; $param = ''; if (isset($path_parts[0]) && !empty($path_parts[0])) { - $requestedPage = sanitizePageInput($path_parts[0]); - - // Verifica se la pagina è nella whitelist - if (in_array($requestedPage, $validPages)) { - $page = $requestedPage; - } else { - // Pagina non valida, redirect a 404 - $page = 'home'; - error_log("Tentativo di accesso a pagina non valida: " . $path_parts[0]); - } + $page = sanitizePageInput($path_parts[0]); } if (isset($path_parts[1]) && !empty($path_parts[1])) { - $requestedParam = sanitizePageInput($path_parts[1]); - - // Validazione specifica per tipo di pagina - if ($page === 'play' || $page === 'playtv') { - // Per play/playtv, il parametro deve essere un numero - if (ctype_digit($requestedParam)) { - $param = $requestedParam; - } else { - error_log("ID stazione non valido: " . $path_parts[1]); - $page = 'home'; + $param = sanitizePageInput($path_parts[1]); +} + +/** + * Risolve dinamicamente il percorso del file PHP corrispondente alla pagina e al parametro. + * Cerca automaticamente i file all'interno della cartella './pages/page/'. + * Se la pagina non esiste, restituisce il file di errore 404. + */ +function getPageFilePath($page, $param) { + $page = sanitizePageInput($page); + $param = sanitizePageInput($param); + + // Gestione stazioni radio e TV (player) + if ($page === 'play') { + if (ctype_digit($param)) { + return './pages/page/player.php'; } - } elseif ($page === 'page') { - // Per page, il parametro deve essere nella whitelist - if (in_array($requestedParam, $validSubPages)) { - $param = $requestedParam; - } else { - error_log("Sottopagina non valida: " . $path_parts[1]); - $page = 'home'; + return './pages/page/404.php'; + } + + if ($page === 'playtv') { + if (ctype_digit($param)) { + return './pages/page/player_tv.php'; + } + return './pages/page/404.php'; + } + + // Gestione sottopagine generiche /page/{param} + if ($page === 'page' && !empty($param)) { + $param_no_dash = str_replace('-', '', $param); + $paths = [ + "./pages/page/{$param}.php", + "./pages/page/{$param_no_dash}.php" + ]; + foreach ($paths as $path) { + if (file_exists($path)) { + return $path; + } + } + return './pages/page/404.php'; + } + + // Gestione pagine principali (es: /home, /radio, /tv, /podcast) + $page_no_dash = str_replace('-', '', $page); + $paths = [ + "./pages/page/{$page}.php", + "./pages/page/{$page_no_dash}.php" + ]; + foreach ($paths as $path) { + if (file_exists($path)) { + return $path; } } + + return './pages/page/404.php'; } // Debug (rimuovi in produzione) -error_log("Page: $page, Param: $param, Path: $path"); \ No newline at end of file +error_log("Page resolved: $page, Param: $param, Path resolved file: " . getPageFilePath($page, $param)); \ No newline at end of file diff --git a/css/style.css b/css/style.css index 7eb977f..2e649eb 100644 --- a/css/style.css +++ b/css/style.css @@ -418,4 +418,421 @@ button#formatToggleBtn { body.appBody>* { display: none !important; } -} \ No newline at end of file +} + +/* PWA STYLES (Banner, Nav Button, and Instructions Modal) */ + +/* Hide all PWA install indicators when running as standalone installed app */ +@media (display-mode: standalone) { + .pwa-install-btn, + .pwa-install-banner, + .pwa-install-btn-nav { + display: none !important; + } +} + +/* iOS navigation standalone fallback check */ +@media (display-mode: standalone) { + body.appBody { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + } +} + +/* PWA Install Banner Style */ +.pwa-install-banner { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%) translateY(120px); + width: 90%; + max-width: 500px; + background: rgba(24, 34, 92, 0.95); + backdrop-filter: blur(15px); + -webkit-backdrop-filter: blur(15px); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 24px; + padding: 16px; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); + z-index: 100000; + display: flex; + flex-direction: column; + transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.4s; + opacity: 0; + color: white; + box-sizing: border-box; +} + +.pwa-install-banner.show { + transform: translateX(-50%) translateY(0); + opacity: 1; +} + +.pwa-banner-content { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; +} + +.pwa-banner-logo { + width: 48px; + height: 48px; + border-radius: 12px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); + flex-shrink: 0; +} + +.pwa-banner-text { + flex: 1; + text-align: left; +} + +.pwa-banner-title { + font-weight: 700; + font-size: 1rem; + color: #fff; +} + +.pwa-banner-desc { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.85); + margin-top: 2px; + line-height: 1.3; +} + +.pwa-banner-actions { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +.pwa-banner-btn { + background: linear-gradient(135deg, #f7b835, #f5a623); + color: #10194b; + border: none; + padding: 8px 16px; + border-radius: 30px; + font-weight: 700; + font-size: 0.8rem; + cursor: pointer; + box-shadow: 0 4px 12px rgba(247, 184, 53, 0.3); + transition: transform 0.2s, box-shadow 0.2s, background 0.2s; + white-space: nowrap; +} + +.pwa-banner-btn:hover { + background: linear-gradient(135deg, #ffd05c, #ffb633); +} + +.pwa-banner-btn:active { + transform: scale(0.95); +} + +.pwa-banner-close-btn { + background: rgba(255, 255, 255, 0.1); + border: none; + color: white; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.2s; +} + +.pwa-banner-close-btn:hover { + background: rgba(255, 255, 255, 0.2); +} + +/* Nav Link Install Button */ +.pwa-install-btn-nav { + color: #f7b835 !important; + font-weight: 700; + border: 1px solid #f7b835; + padding: 4px 12px !important; + border-radius: 20px; + display: inline-flex !important; + align-items: center; + gap: 4px; + background: rgba(247, 184, 53, 0.1); + transition: all 0.3s ease; +} + +.pwa-install-btn-nav:hover { + background: #f7b835 !important; + color: #10194b !important; +} + +/* PWA Instructions Modal Overlay */ +.pwa-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + z-index: 110000; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s ease; + box-sizing: border-box; +} + +.pwa-modal-overlay.show { + opacity: 1; +} + +.pwa-modal-content { + background: #18225c; + color: white; + width: 90%; + max-width: 420px; + border-radius: 24px; + border: 1px solid rgba(255, 255, 255, 0.15); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); + transform: scale(0.9); + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + overflow: hidden; + box-sizing: border-box; +} + +.pwa-modal-overlay.show .pwa-modal-content { + transform: scale(1); +} + +.pwa-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.pwa-modal-header h3 { + font-size: 1.1rem; + margin: 0; + font-weight: 700; + color: white; +} + +.pwa-modal-close-btn-top { + background: none; + border: none; + color: rgba(255, 255, 255, 0.6); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + transition: color 0.2s; +} + +.pwa-modal-close-btn-top:hover { + color: white; +} + +.pwa-modal-body { + padding: 20px; + text-align: left; +} + +.pwa-modal-app-info { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + padding-bottom: 15px; +} + +.pwa-modal-logo { + width: 52px; + height: 52px; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + flex-shrink: 0; +} + +.pwa-modal-steps { + display: flex; + flex-direction: column; + gap: 16px; +} + +.pwa-step { + display: flex; + gap: 12px; + align-items: flex-start; +} + +.pwa-step-num { + background: #f7b835; + color: #10194b; + width: 22px; + height: 22px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 0.8rem; + flex-shrink: 0; + margin-top: 2px; +} + +.pwa-step-text { + font-size: 0.85rem; + line-height: 1.4; + color: rgba(255, 255, 255, 0.9); +} + +.pwa-step-text strong { + color: #f7b835; +} + +.pwa-inline-icon { + vertical-align: middle; + margin: 0 2px; + background: rgba(255, 255, 255, 0.12); + border-radius: 6px; + padding: 2px 4px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +@media (max-width: 480px) { + .pwa-install-banner { + width: 95%; + bottom: 10px; + padding: 12px; + } + + .pwa-banner-content { + gap: 8px; + } + + .pwa-banner-desc { + font-size: 0.7rem; + } + + .pwa-banner-btn { + padding: 6px 12px; + font-size: 0.75rem; + } +} + +/* OFFLINE SCREEN STYLES */ +.offline-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(16, 25, 75, 0.96); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + z-index: 999999; + display: flex; + justify-content: center; + align-items: center; + opacity: 0; + visibility: hidden; + transition: opacity 0.5s ease, visibility 0.5s ease; +} + +.offline-overlay.active { + opacity: 1; + visibility: visible; +} + +.offline-content { + text-align: center; + color: white; + padding: 40px; + max-width: 400px; + width: 90%; + font-family: 'Poppins', sans-serif; +} + +.offline-icon { + font-size: 80px; + color: #ff5e5e; + margin-bottom: 24px; + animation: pulse-offline 2s infinite ease-in-out; + display: inline-block; +} + +.offline-title { + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 12px; + letter-spacing: -0.5px; + color: white; +} + +.offline-desc { + font-size: 1rem; + color: rgba(255, 255, 255, 0.7); + margin-bottom: 30px; + line-height: 1.5; +} + +.offline-btn { + background: linear-gradient(135deg, #f7b835, #f5a623); + color: #10194b; + border: none; + padding: 12px 30px; + font-size: 0.95rem; + font-weight: 700; + border-radius: 50px; + cursor: pointer; + box-shadow: 0 4px 15px rgba(245, 166, 35, 0.4); + transition: transform 0.2s, box-shadow 0.2s; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + text-decoration: none; +} + +.offline-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(245, 166, 35, 0.6); + color: #10194b; +} + +.offline-btn:active { + transform: translateY(0); +} + +.offline-btn:disabled { + opacity: 0.8; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +@keyframes pulse-offline { + 0% { transform: scale(1); opacity: 0.8; } + 50% { transform: scale(1.08); opacity: 1; } + 100% { transform: scale(1); opacity: 0.8; } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.spinner-offline { + display: inline-block; +} \ No newline at end of file diff --git a/data/changelog.xml b/data/changelog.xml index ce882c7..9f1bf61 100644 --- a/data/changelog.xml +++ b/data/changelog.xml @@ -2,6 +2,15 @@ + + 2.6.0 + + Aggiornato il sistema di gestione della visualizzazione delle pagine. + Aggiunto il pulsante "guida" per il download dell'applicazione su iOS e Android. + Correzione e bugfix di problematiche varie. + + + 2.5.1 (a) diff --git a/js/app.js b/js/app.js index 5e7febf..9b3d634 100644 --- a/js/app.js +++ b/js/app.js @@ -1043,11 +1043,75 @@ document.addEventListener('DOMContentLoaded', function () { if (homeNav) homeNav.style.display = 'block'; }; + /** + * Gestione dello stato Offline + */ + function initOfflineDetection() { + let offlineOverlay = document.getElementById('offlineOverlay'); + if (!offlineOverlay) { + offlineOverlay = document.createElement('div'); + offlineOverlay.id = 'offlineOverlay'; + offlineOverlay.className = 'offline-overlay'; + offlineOverlay.innerHTML = ` +
+ wifi_off +

Nessuna Connessione

+

Sembra che tu sia offline. Controlla la tua connessione internet per continuare ad ascoltare RPIGroup Play.

+ +
+ `; + document.body.appendChild(offlineOverlay); + + const retryBtn = document.getElementById('offlineRetryBtn'); + if (retryBtn) { + retryBtn.addEventListener('click', function() { + retryBtn.disabled = true; + retryBtn.innerHTML = 'sync Verifica in corso...'; + + fetch(BASE_PATH + '/robots.txt', { cache: 'no-store', mode: 'no-cors' }) + .then(() => { + offlineOverlay.classList.remove('active'); + window.location.reload(); + }) + .catch(() => { + setTimeout(() => { + retryBtn.disabled = false; + retryBtn.innerHTML = 'refresh Riprova'; + }, 1000); + }); + }); + } + } + + function showOfflineOverlay() { + offlineOverlay.classList.add('active'); + } + + function hideOfflineOverlay() { + offlineOverlay.classList.remove('active'); + } + + window.addEventListener('offline', showOfflineOverlay); + window.addEventListener('online', () => { + hideOfflineOverlay(); + console.log('Dispositivo tornato online. Ripristino...'); + }); + + if (!navigator.onLine) { + showOfflineOverlay(); + } + } + /** * Inizializzazione principale */ function init() { console.log('Inizializzazione app...'); + + // Rilevamento stato offline + initOfflineDetection(); setupOpenAppButton(); diff --git a/js/pwa-install.js b/js/pwa-install.js new file mode 100644 index 0000000..cce5a9f --- /dev/null +++ b/js/pwa-install.js @@ -0,0 +1,236 @@ +/** + * PWA Install and Service Worker Helper + * Powered by Antigravity AI + */ + +(function () { + const BASE_PATH = window.BASE_PATH || ''; + + // Register Service Worker + if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + const swUrl = `${BASE_PATH}/sw.js`; + navigator.serviceWorker.register(swUrl) + .then((registration) => { + console.log('[PWA] Service Worker registrato con successo:', registration.scope); + }) + .catch((error) => { + console.error('[PWA] Registrazione Service Worker fallita:', error); + }); + }); + } + + // Detectors + const isStandalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true; + const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; + const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + + // Se l'app viene aperta in modalità standalone (PWA) su desktop, reindirizza all'interno dell'app + if (isStandalone && !isMobile) { + const urlParams = new URLSearchParams(window.location.search); + if (!urlParams.has('app') || urlParams.get('app') !== 'true') { + const currentPath = window.location.pathname; + const pathWithoutBase = currentPath.replace(BASE_PATH, '').replace(/^\//, ''); + const redirectParam = pathWithoutBase ? '&redirect=' + encodeURIComponent(pathWithoutBase) : ''; + window.location.href = BASE_PATH + '/?app=true' + redirectParam; + } + } + + // Global variables + window.deferredPrompt = null; + + // Check if banner was dismissed in the last 7 days + function isBannerDismissed() { + const dismissedTime = localStorage.getItem('pwa-banner-dismissed'); + if (!dismissedTime) return false; + + const sevenDays = 7 * 24 * 60 * 60 * 1000; + return (Date.now() - parseInt(dismissedTime, 10)) < sevenDays; + } + + // Dismiss banner + function dismissBanner() { + localStorage.setItem('pwa-banner-dismissed', Date.now().toString()); + const banner = document.getElementById('pwa-install-banner'); + if (banner) { + banner.classList.remove('show'); + setTimeout(() => { + banner.style.display = 'none'; + }, 400); + } + } + + // Initialize UI components and listeners + function initPwaUI() { + // If already in standalone mode, hide all promotional components + if (isStandalone) { + console.log('[PWA] Applicazione avviata in modalità Standalone. Nascondo elementi PWA.'); + return; + } + + const headerBtn = document.getElementById('pwa-header-install-btn'); + const desktopBtn = document.getElementById('desktopPwaInstallBtn'); + const banner = document.getElementById('pwa-install-banner'); + const bannerInstallBtn = document.getElementById('pwa-banner-install-btn'); + const bannerCloseBtn = document.getElementById('pwa-banner-close-btn'); + + // On iOS, show the installation buttons immediately (as they don't support beforeinstallprompt) + if (isIOS) { + console.log('[PWA] Rilevato dispositivo iOS'); + if (headerBtn) headerBtn.style.display = 'inline-flex'; + if (desktopBtn) desktopBtn.style.display = 'inline-flex'; + + if (banner && !isBannerDismissed()) { + banner.style.display = 'flex'; + setTimeout(() => banner.classList.add('show'), 100); + } + } + + // Attach click listeners to all PWA installation buttons + const triggerInstall = (e) => { + e.preventDefault(); + + if (window.deferredPrompt) { + // Show native install prompt (Android / Desktop Chrome / Edge) + window.deferredPrompt.prompt(); + window.deferredPrompt.userChoice.then((choiceResult) => { + if (choiceResult.outcome === 'accepted') { + console.log('[PWA] Utente ha installato la PWA'); + dismissBanner(); + } else { + console.log('[PWA] Utente ha annullato l\'installazione'); + } + window.deferredPrompt = null; + }); + } else { + // Fallback for iOS Safari or other unsupported prompts + showInstructionsModal(); + } + }; + + if (headerBtn) headerBtn.addEventListener('click', triggerInstall); + if (desktopBtn) desktopBtn.addEventListener('click', triggerInstall); + if (bannerInstallBtn) bannerInstallBtn.addEventListener('click', triggerInstall); + + if (bannerCloseBtn) { + bannerCloseBtn.addEventListener('click', (e) => { + e.preventDefault(); + dismissBanner(); + }); + } + + // Modal Close logic + const modal = document.getElementById('pwa-instructions-modal'); + const modalCloseBtnTop = document.getElementById('pwa-modal-close-btn'); + + const closeModal = () => { + if (modal) { + modal.classList.remove('show'); + setTimeout(() => { + modal.style.display = 'none'; + }, 300); + } + }; + + if (modalCloseBtnTop) modalCloseBtnTop.addEventListener('click', closeModal); + if (modal) { + modal.addEventListener('click', (e) => { + if (e.target === modal) closeModal(); + }); + } + } + + // Listen for beforeinstallprompt (Chrome / Android / Edge) + window.addEventListener('beforeinstallprompt', (e) => { + // Prevent Chrome 67 and earlier from automatically showing the prompt + e.preventDefault(); + + // Se siamo su desktop, non salviamo il prompt e non mostriamo nessun banner promozionale PWA + if (!isMobile) { + console.log('[PWA] Installazione via browser intercettata su desktop. Ignorata per requisiti.'); + return; + } + + // Stash the event so it can be triggered later. + window.deferredPrompt = e; + + console.log('[PWA] Evento beforeinstallprompt intercettato.'); + + // Show custom buttons and banner (if not dismissed) + const headerBtn = document.getElementById('pwa-header-install-btn'); + const desktopBtn = document.getElementById('desktopPwaInstallBtn'); + const banner = document.getElementById('pwa-install-banner'); + + if (headerBtn) headerBtn.style.display = 'inline-flex'; + if (desktopBtn) desktopBtn.style.display = 'inline-flex'; + + if (banner && !isBannerDismissed()) { + banner.style.display = 'flex'; + setTimeout(() => banner.classList.add('show'), 100); + } + }); + + // Track successful installation + window.addEventListener('appinstalled', () => { + console.log('[PWA] L\'applicazione è stata installata con successo.'); + window.deferredPrompt = null; + + // Hide all promotional components + const headerBtn = document.getElementById('pwa-header-install-btn'); + const desktopBtn = document.getElementById('desktopPwaInstallBtn'); + const banner = document.getElementById('pwa-install-banner'); + + if (headerBtn) headerBtn.style.display = 'none'; + if (desktopBtn) desktopBtn.style.display = 'none'; + if (banner) banner.style.display = 'none'; + }); + + // Show instructions modal (iOS / Fallback) + function showInstructionsModal() { + const modal = document.getElementById('pwa-instructions-modal'); + const content = document.getElementById('pwa-instructions-content'); + if (!modal || !content) return; + + if (isIOS) { + content.innerHTML = ` +
+
1
+
Tocca l'icona di Condividi share (il quadrato con la freccia verso l'alto) nella barra in basso di Safari.
+
+
+
2
+
Scorri l'elenco delle opzioni verso il basso e seleziona Aggiungi alla schermata Home add_box.
+
+
+
3
+
Tocca Aggiungi nell'angolo in alto a destra per inserire l'icona sul tuo telefono.
+
+ `; + } else { + content.innerHTML = ` +
+
1
+
Apri il menu delle impostazioni del browser toccando i tre puntini more_vert in alto a destra.
+
+
+
2
+
Seleziona la voce Installa applicazione o Aggiungi a schermata Home.
+
+
+
3
+
Conferma per scaricare la PWA sul tuo telefono.
+
+ `; + } + + modal.style.display = 'flex'; + setTimeout(() => modal.classList.add('show'), 10); + } + + // Run on DOM loaded + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initPwaUI); + } else { + initPwaUI(); + } +})(); diff --git a/pages/mobile.php b/pages/mobile.php index 6368862..4c56836 100644 --- a/pages/mobile.php +++ b/pages/mobile.php @@ -7,7 +7,7 @@
Logo
- + + + + + + \ No newline at end of file diff --git a/static/footer.php b/static/footer.php index 0b0f8f0..c5c1a99 100644 --- a/static/footer.php +++ b/static/footer.php @@ -1,4 +1,5 @@ + diff --git a/static/head.php b/static/head.php index 9efc6db..09d700f 100644 --- a/static/head.php +++ b/static/head.php @@ -25,10 +25,12 @@ header('Referrer-Policy: strict-origin-when-cross-origin'); - + - + + + diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..ecb4c91 --- /dev/null +++ b/sw.js @@ -0,0 +1,99 @@ +const CACHE_NAME = 'rpigroupplay-v2'; +const ASSETS = [ + './', + './index.php', + './manifest.json', + './css/style.css', + './css/bootstrap.css', + './css/animation.css', + './js/app.js', + './js/bootstrap.js', + './img/RpiGroupPlayWHITE.png', + './img/logoapp.png' +]; + +// Install Event +self.addEventListener('install', (e) => { + e.waitUntil( + caches.open(CACHE_NAME) + .then((cache) => { + console.log('[Service Worker] Caching static shell'); + return cache.addAll(ASSETS).catch(err => { + console.warn('[Service Worker] Asset caching warning:', err); + }); + }) + .then(() => self.skipWaiting()) + ); +}); + +// Activate Event +self.addEventListener('activate', (e) => { + e.waitUntil( + caches.keys().then((keys) => { + return Promise.all( + keys.map((key) => { + if (key !== CACHE_NAME) { + console.log('[Service Worker] Removing old cache', key); + return caches.delete(key); + } + }) + ); + }).then(() => self.clients.claim()) + ); +}); + +// Fetch Event +self.addEventListener('fetch', (e) => { + const url = e.request.url; + + // Skip non-GET requests + if (e.request.method !== 'GET') { + return; + } + + // Bypass streaming content, APIs, and external domains (like radio/tv streams) + if ( + url.includes('stream') || + url.includes('.mp3') || + url.includes('.aac') || + url.includes('.m3u8') || + url.includes('.ts') || + url.includes('icecast') || + url.includes('shoutcast') || + url.includes('radiocitta105') || + url.includes('radiodiffusionelibera') || + !url.startsWith(self.location.origin) + ) { + return; // Let the browser handle standard streaming/external fetch requests directly + } + + e.respondWith( + caches.match(e.request).then((cachedResponse) => { + if (cachedResponse) { + // Return cached resource immediately and fetch fresh copy in background (Stale-While-Revalidate) + fetch(e.request).then((networkResponse) => { + if (networkResponse.status === 200) { + caches.open(CACHE_NAME).then((cache) => { + cache.put(e.request, networkResponse); + }); + } + }).catch(() => { + // Silent catch network failures + }); + return cachedResponse; + } + + // Fallback to network + return fetch(e.request).then((response) => { + // Cache standard successful requests + if (response.status === 200) { + const responseClone = response.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(e.request, responseClone); + }); + } + return response; + }); + }) + ); +});