vers. 2.6.0

This commit is contained in:
2026-07-14 04:33:26 +02:00
parent 42eebc9484
commit 189a9114ff
10 changed files with 964 additions and 169 deletions
+11 -51
View File
@@ -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':
$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) {
include './pages/page/player.php';
} else {
include './pages/page/404.php';
if ($station === null) {
$filePath = './pages/page/404.php';
}
break;
case 'playtv':
} elseif ($page === 'playtv' && !empty($param)) {
$stationId = (int)$param;
$station = getTVStation($stationId);
if ($station !== null) {
include './pages/page/player_tv.php';
} else {
include './pages/page/404.php';
if ($station === null) {
$filePath = './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';
}
include $filePath;
exit; // Termina l'esecuzione per le richieste AJAX
}
+56 -35
View File
@@ -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]);
$param = 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';
/**
* 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");
error_log("Page resolved: $page, Param: $param, Path resolved file: " . getPageFilePath($page, $param));
+417
View File
@@ -419,3 +419,420 @@ button#formatToggleBtn {
display: none !important;
}
}
/* 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;
}
+9
View File
@@ -2,6 +2,15 @@
<changelog>
<version>
<number>2.6.0</number>
<logs>
<log>Aggiornato il sistema di gestione della visualizzazione delle pagine.</log>
<log>Aggiunto il pulsante "guida" per il download dell'applicazione su iOS e Android.</log>
<log>Correzione e bugfix di problematiche varie.</log>
</logs>
</version>
<version>
<number>2.5.1 (a)</number>
<logs>
+64
View File
@@ -1043,12 +1043,76 @@ 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 = `
<div class="offline-content">
<span class="material-icons offline-icon">wifi_off</span>
<h2 class="offline-title">Nessuna Connessione</h2>
<p class="offline-desc">Sembra che tu sia offline. Controlla la tua connessione internet per continuare ad ascoltare RPIGroup Play.</p>
<button class="offline-btn" id="offlineRetryBtn">
<span class="material-icons">refresh</span> Riprova
</button>
</div>
`;
document.body.appendChild(offlineOverlay);
const retryBtn = document.getElementById('offlineRetryBtn');
if (retryBtn) {
retryBtn.addEventListener('click', function() {
retryBtn.disabled = true;
retryBtn.innerHTML = '<span class="material-icons spinner-offline" style="animation: spin 1s infinite linear; vertical-align: middle;">sync</span> 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 = '<span class="material-icons">refresh</span> 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();
if (document.querySelector('.container') || document.querySelector('.container-fluid')) {
+236
View File
@@ -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 = `
<div class="pwa-step">
<div class="pwa-step-num">1</div>
<div class="pwa-step-text">Tocca l'icona di <strong>Condividi</strong> <span class="pwa-inline-icon"><i class="material-icons" style="font-size:18px; color:#f7b835;">share</i></span> (il quadrato con la freccia verso l'alto) nella barra in basso di Safari.</div>
</div>
<div class="pwa-step">
<div class="pwa-step-num">2</div>
<div class="pwa-step-text">Scorri l'elenco delle opzioni verso il basso e seleziona <strong>Aggiungi alla schermata Home</strong> <span class="pwa-inline-icon"><i class="material-icons" style="font-size:18px; color:#f7b835;">add_box</i></span>.</div>
</div>
<div class="pwa-step">
<div class="pwa-step-num">3</div>
<div class="pwa-step-text">Tocca <strong>Aggiungi</strong> nell'angolo in alto a destra per inserire l'icona sul tuo telefono.</div>
</div>
`;
} else {
content.innerHTML = `
<div class="pwa-step">
<div class="pwa-step-num">1</div>
<div class="pwa-step-text">Apri il menu delle impostazioni del browser toccando i <strong>tre puntini</strong> <span class="pwa-inline-icon"><i class="material-icons" style="font-size:18px; color:#f7b835;">more_vert</i></span> in alto a destra.</div>
</div>
<div class="pwa-step">
<div class="pwa-step-num">2</div>
<div class="pwa-step-text">Seleziona la voce <strong>Installa applicazione</strong> o <strong>Aggiungi a schermata Home</strong>.</div>
</div>
<div class="pwa-step">
<div class="pwa-step-num">3</div>
<div class="pwa-step-text">Conferma per scaricare la PWA sul tuo telefono.</div>
</div>
`;
}
modal.style.display = 'flex';
setTimeout(() => modal.classList.add('show'), 10);
}
// Run on DOM loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initPwaUI);
} else {
initPwaUI();
}
})();
+53 -67
View File
@@ -7,7 +7,7 @@
<div class="logo-section">
<img src="<?=$base_path?>/img/RpiGroupPlayWHITE.png" alt="Logo">
</div>
<div class="menu-section">
<div class="menu-section" style="display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 5px;">
<a href="<?php echo $base_path; ?>/" data-page="home" class="navLink <?php echo $page == 'home' ? 'active' : ''; ?>">Home</a>
<a href="<?php echo $base_path; ?>/radio" data-page="radio" class="navLink <?php echo ($page == 'radio' || $page == 'play') ? 'active' : ''; ?>">Radio</a>
<a href="<?php echo $base_path; ?>/tv" data-page="tv" class="navLink <?php echo ($page == 'tv' || $page == 'playtv') ? 'active' : ''; ?>">TV</a>
@@ -17,79 +17,24 @@
<main class="container-fluid" id="content">
<?php
// Carica il contenuto iniziale in base all'URL
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':
if (!empty($param)) {
$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) {
include './pages/page/player.php';
} else {
include './pages/page/404.php';
if ($station === null) {
$filePath = './pages/page/404.php';
}
} else {
// Se non c'è ID, torna alla home
include './pages/page/home.php';
}
break;
case 'playtv':
if (!empty($param)) {
} elseif ($page === 'playtv' && !empty($param)) {
$stationId = (int)$param;
$station = getTVStation($stationId);
if ($station !== null) {
include './pages/page/player_tv.php';
} else {
include './pages/page/404.php';
if ($station === null) {
$filePath = './pages/page/404.php';
}
} else {
// Se non c'è ID, torna alla home
include './pages/page/home.php';
}
break;
case 'podcast':
include './pages/page/podcast.php';
break;
case 'page':
if (!empty($param)) {
switch ($param) {
case 'about':
include './pages/page/about.php';
break;
case 'copyright':
include './pages/page/copyright.php';
break;
case 'contact':
include './pages/page/contact.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';
}
} else {
include './pages/page/404.php';
}
break;
default:
include './pages/page/home.php';
}
include $filePath;
?>
</main>
@@ -109,3 +54,44 @@
</div>
<div class="copyright-section" <?php if($is_mobile){ ?> style="padding: 10px 0 25px;" <?php } ?>>© 2025 RPIGroup • Versione: <?php echo $version_app; ?></div>
</div>
<!-- PWA Install Banner (Mobile view bottom prompt) -->
<div id="pwa-install-banner" class="pwa-install-banner" style="display: none;">
<div class="pwa-banner-content">
<img src="<?=$base_path?>/img/logoapp.png" alt="App Logo" class="pwa-banner-logo">
<div class="pwa-banner-text">
<div class="pwa-banner-title">RPIGroup Play</div>
<div class="pwa-banner-desc">Installa l'app per ascoltare musica ovunque e a schermo intero!</div>
</div>
<div class="pwa-banner-actions">
<button id="pwa-banner-install-btn" class="pwa-banner-btn">SCARICA APP</button>
<button id="pwa-banner-close-btn" class="pwa-banner-close-btn">
<i class="material-icons" style="font-size: 18px; vertical-align: middle;">close</i>
</button>
</div>
</div>
</div>
<!-- PWA Instructions Modal (iOS Safari & Fallback) -->
<div id="pwa-instructions-modal" class="pwa-modal-overlay" style="display: none;">
<div class="pwa-modal-content">
<div class="pwa-modal-header">
<h3>Installa l'applicazione</h3>
<button id="pwa-modal-close-btn" class="pwa-modal-close-btn-top">
<i class="material-icons">close</i>
</button>
</div>
<div class="pwa-modal-body">
<div class="pwa-modal-app-info">
<img src="<?=$base_path?>/img/logoapp.png" alt="App Logo" class="pwa-modal-logo">
<div>
<h4 style="color: white; font-weight: 700; margin: 0;">RPIGroup Play</h4>
<p style="color: rgba(255,255,255,0.7); font-size: 0.8rem; margin: 2px 0 0;">Ascolta le tue stazioni radio preferite</p>
</div>
</div>
<div id="pwa-instructions-content" class="pwa-modal-steps">
<!-- Will be populated dynamically by pwa-install.js -->
</div>
</div>
</div>
</div>
+1
View File
@@ -1,4 +1,5 @@
<script src="<?php echo $base_path; ?>/js/pwa-install.js?v=<?=time()?>"></script>
<script src="<?php echo $base_path; ?>/js/app.js?v=<?=time()?>"></script>
</body>
</html>
+4 -2
View File
@@ -25,10 +25,12 @@ header('Referrer-Policy: strict-origin-when-cross-origin');
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="<?php echo $title_site; ?>">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="<?php echo $title_site; ?>">
<meta name="theme-color" content="#2a377e">
<meta name="apple-mobile-web-app-status-bar-style" content="#2a377e">
<link rel="apple-touch-icon" href="<?=$base_path?>/img/logoapp.png">
<link rel="apple-touch-icon" sizes="152x152" href="<?=$base_path?>/img/icons/icon-152x152.png">
<link rel="apple-touch-icon" sizes="192x192" href="<?=$base_path?>/img/icons/icon-192x192.png">
<meta name="screen-orientation" content="portrait">
<meta name="x5-orientation" content="portrait">
<meta name="x5-fullscreen" content="true">
+99
View File
@@ -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;
});
})
);
});