import { Component, inject, OnInit, signal, effect } from '@angular/core'; import { ThemeService } from './services/theme.service'; import { CantiService } from './services/canti.service'; import { SettingsService } from './services/settings.service'; import { VERSION } from './version'; import { Router, ActivatedRoute } from '@angular/router'; import { ToastController } from '@ionic/angular'; import { SwUpdate, VersionReadyEvent } from '@angular/service-worker'; import { filter, first } from 'rxjs/operators'; @Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrls: ['app.component.scss'], standalone: false, }) export class AppComponent implements OnInit { private themeService = inject(ThemeService); // Ensures theme is initialized at boot public cantiService = inject(CantiService); public settingsService = inject(SettingsService); public version = VERSION; private router = inject(Router); private route = inject(ActivatedRoute); private toastCtrl = inject(ToastController); private swUpdate = inject(SwUpdate); public showRedirectOverlay = signal(false); public showInstallOverlay = signal(false); public redirectFailed = signal(false); public protocolLink = ''; constructor() { // Gli aggiornamenti automatici e periodici sono stati rimossi. // L'aggiornamento viene gestito esclusivamente in modo manuale // tramite il pulsante "Verifica Aggiornamenti App" in SettingsPage. effect(() => { this.checkLoaderDismissal(); }); } checkLoaderDismissal() { const ready = this.settingsService.isVersionCheckComplete() && this.cantiService.firstLoadCompleted(); if (!ready) { return; } // Se l'overlay di redirect o di installazione è mostrato, NON nascondiamo il loader iniziale // per rimanere nella welcome page mentre l'utente sceglie. if (this.showRedirectOverlay() || this.showInstallOverlay()) { return; } // Altrimenti, nascondiamo il loader per far entrare l'utente nell'app if ((window as any).PwaLoader) { (window as any).PwaLoader.hide(); } } async ngOnInit() { // Add global horizontal scroll support for wheel on horizontal containers window.addEventListener('wheel', (event: WheelEvent) => { if (Math.abs(event.deltaY) > 0 && Math.abs(event.deltaX) === 0) { const path = event.composedPath(); for (const target of path) { if (target instanceof HTMLElement) { const style = window.getComputedStyle(target); const isHorizontalScroll = (style.overflowX === 'auto' || style.overflowX === 'scroll') && target.scrollWidth > target.clientWidth; if (isHorizontalScroll) { target.scrollLeft += event.deltaY; event.preventDefault(); break; } } } } }, { passive: false }); // 1. Allineamento istantaneo alla versione remota if ((window as any).PwaLoader) { (window as any).PwaLoader.update({ phase: 'Fase: Verifica Versione', desc: 'Verifica della versione più recente in corso...' }); } const updated = await this.checkVersionSync(); if (updated) { return; // Reloading, skip further setup } // Set signal indicating startup version check is complete this.settingsService.isVersionCheckComplete.set(true); this.route.queryParams.subscribe(params => { const protocolUrl = params['url']; if (protocolUrl && protocolUrl.startsWith('web+canti:')) { try { const cleanUrl = protocolUrl.replace('web+canti://', 'http://localhost/'); const urlObj = new URL(cleanUrl); let targetPath = urlObj.pathname; if (targetPath === '/open' || targetPath === '//open') { targetPath = '/'; } else if (targetPath.startsWith('/open/')) { targetPath = targetPath.substring(5); } const queryParams: any = {}; urlObj.searchParams.forEach((value, key) => { queryParams[key] = value; }); this.router.navigate([targetPath], { queryParams, replaceUrl: true }); } catch (e) { console.error('Failed to parse protocol url:', protocolUrl, e); } } }); this.checkAndRedirectToPwa(); } async checkVersionSync(): Promise { // Controlla SEMPRE version.json per primo — è il modo più affidabile per // rilevare un disallineamento di versione, indipendentemente dallo stato del SW. // Su mobile, checkForUpdate() può essere lento o inaffidabile. try { console.log(`[PWA-Update] Verifica version.json all'avvio (locale=${VERSION})...`); const response = await Promise.race([ fetch(`/version.json?cb=${Date.now()}`, { cache: 'no-store' }), new Promise((resolve) => setTimeout(() => resolve(null), 5000)) ]); if (response && response.ok) { const data = await response.json(); if (data && data.version && data.version !== VERSION) { console.log(`[PWA-Update] Mismatch rilevato: locale=${VERSION}, remota=${data.version}. Forza aggiornamento...`); const overlay = showFullscreenUpdateOverlay(); // Prova ad attivare tramite SwUpdate se abilitato (scarica il nuovo bundle SW) if (this.swUpdate.isEnabled) { try { const hasSwUpdate = await Promise.race([ this.swUpdate.checkForUpdate(), new Promise((resolve) => setTimeout(() => resolve(false), 8000)) ]); if (hasSwUpdate) { console.log('[PWA-Update] SW aggiornamento disponibile, attivazione...'); await Promise.race([ this.swUpdate.activateUpdate(), new Promise((resolve) => setTimeout(resolve, 5000)) ]); } } catch (e) { console.warn('[PWA-Update] SwUpdate durante mismatch fallito:', e); } } // Aggiorna anche la registrazione SW direttamente (doppia sicurezza) if ('serviceWorker' in navigator) { try { const registration = await navigator.serviceWorker.ready; await registration.update(); } catch (e) { console.warn('[PWA-Update] SW registration.update fallito:', e); } } // Disattiva service worker attivi per forzare il refresh completo if ('serviceWorker' in navigator) { const registrations = await navigator.serviceWorker.getRegistrations(); for (const registration of registrations) { await registration.unregister(); } } // Cancella le cache del browser if ('caches' in window) { const keys = await caches.keys(); for (const key of keys) { await caches.delete(key); } } overlay.finish(); // Ricarica con parametro cache-busting per forzare l'allineamento remoto const url = new URL(window.location.href); url.searchParams.set('update_cb', Date.now().toString()); window.location.replace(url.toString()); return true; } else { console.log('[PWA-Update] Versione allineata, nessun aggiornamento necessario.'); } } } catch (e) { console.warn('[PWA-Update] version.json check fallito:', e); } // 2. Fallback: Prova SwUpdate nel caso in cui il controllo version.json sia fallito o sia stato servito dalla cache if (this.swUpdate.isEnabled) { try { console.log('[PWA-Update] Verifica aggiornamenti via SwUpdate all\'avvio...'); let activated = false; let overlay: any = null; const activateAndReload = async () => { if (activated) return; activated = true; try { await this.swUpdate.activateUpdate(); } catch (e) { console.warn('[PWA-Update] activateUpdate fallito all\'avvio:', e); } // Deregistra i vecchi SW e cancella le cache per un ricaricamento pulito if ('serviceWorker' in navigator) { const registrations = await navigator.serviceWorker.getRegistrations(); for (const reg of registrations) { await reg.unregister(); } } if ('caches' in window) { const keys = await caches.keys(); for (const key of keys) { await caches.delete(key); } } if (overlay) overlay.finish(); setTimeout(() => { const url = new URL(window.location.href); url.searchParams.set('update_cb', Date.now().toString()); window.location.replace(url.toString()); }, 600); }; // Sottoscrivi PRIMA di verificare l'aggiornamento per evitare race condition const sub = this.swUpdate.versionUpdates .pipe( filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'), first() ) .subscribe(() => { console.log('[PWA-Update] VERSION_READY ricevuto all\'avvio'); activateAndReload(); }); // Concedi fino a 8 secondi al controllo SW — le connessioni mobili possono essere lente const hasUpdate = await Promise.race([ this.swUpdate.checkForUpdate(), new Promise((resolve) => setTimeout(() => resolve(false), 8000)) ]); if (hasUpdate) { console.log('[PWA-Update] Aggiornamento rilevato via SwUpdate. Avvio download...'); overlay = showFullscreenUpdateOverlay(); // Timeout di sicurezza di 25 secondi: se VERSION_READY non arriva, attiva comunque setTimeout(() => { console.log('[PWA-Update] Safety timeout raggiunto all\'avvio, procedo...'); sub.unsubscribe(); activateAndReload(); }, 25000); return true; // Attendi il ricaricamento } else { sub.unsubscribe(); } } catch (err) { console.warn('[PWA-Update] Controllo SwUpdate fallito all\'avvio:', err); } } return false; } async checkAndRedirectToPwa() { const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (navigator as any).standalone; if (isStandalone) { localStorage.setItem('pwa-installed', 'true'); return; } const search = window.location.search; const path = window.location.pathname; this.protocolLink = `web+canti://open${path}${search}`; // Controlliamo se abbiamo già salvato che l'app è installata o se possiamo verificarlo let isInstalled = localStorage.getItem('pwa-installed') === 'true'; if (!isInstalled && 'getInstalledRelatedApps' in navigator) { try { const relatedApps = await (navigator as any).getInstalledRelatedApps(); isInstalled = relatedApps.length > 0; if (isInstalled) { localStorage.setItem('pwa-installed', 'true'); } } catch (e) { console.warn('Failed to check installed apps:', e); } } if (isInstalled) { const skipRedirect = sessionStorage.getItem('skip-pwa-redirect') === 'true'; if (!skipRedirect) { this.showRedirectOverlay.set(true); this.redirectFailed.set(false); if ((window as any).PwaLoader) { (window as any).PwaLoader.update({ isRedirect: true }); } // Tentiamo il reindirizzamento automatico setTimeout(() => { window.location.href = this.protocolLink; // Se dopo 2 secondi l'utente è ancora qui, probabilmente l'app non è installata setTimeout(() => { if (this.showRedirectOverlay()) { this.redirectFailed.set(true); localStorage.setItem('pwa-installed', 'false'); if ((window as any).PwaLoader) { (window as any).PwaLoader.hide(); } } }, 2000); }, 800); } } else { // Se non è installata, proponiamo l'installazione immediata per evitare la cache del browser e avere un'esperienza ottimale const skipInstall = sessionStorage.getItem('skip-pwa-install') === 'true'; const isMobile = this.settingsService.isAndroid() || this.settingsService.isIos(); const hasDesktopPrompt = this.settingsService.showInstallButton() || this.settingsService.deferredPrompt(); if (!skipInstall && (isMobile || hasDesktopPrompt)) { this.showInstallOverlay.set(true); } } } openPwaManual() { window.location.href = this.protocolLink; if ((window as any).PwaLoader) { (window as any).PwaLoader.show(); (window as any).PwaLoader.update({ isRedirect: true }); } // Se dopo 2 secondi l'utente è ancora qui, probabilmente l'app non è installata setTimeout(() => { if (this.showRedirectOverlay()) { this.redirectFailed.set(true); localStorage.setItem('pwa-installed', 'false'); if ((window as any).PwaLoader) { (window as any).PwaLoader.hide(); } } }, 2000); } stayInBrowser() { sessionStorage.setItem('skip-pwa-redirect', 'true'); this.showRedirectOverlay.set(false); } closeInstallOverlay() { sessionStorage.setItem('skip-pwa-install', 'true'); this.showInstallOverlay.set(false); } async triggerInstall() { await this.settingsService.installPwa(); this.closeInstallOverlay(); } async triggerInstallFromRedirect() { await this.settingsService.installPwa(); sessionStorage.setItem('skip-pwa-redirect', 'true'); this.showRedirectOverlay.set(false); } } export function showFullscreenUpdateOverlay() { if ((window as any).PwaLoader) { (window as any).PwaLoader.show(); (window as any).PwaLoader.update({ title: 'Download aggiornamento', phase: 'Fase: Download', desc: 'Scaricamento della nuova versione...', percent: 0 }); } // Fetch remote version to display the version being downloaded fetch(`/version.json?cb=${Date.now()}`) .then(res => { if (res.ok) return res.json(); throw new Error('Fallback'); }) .then(data => { if (data && data.version && (window as any).PwaLoader) { (window as any).PwaLoader.update({ version: 'Versione ' + data.version }); } }) .catch(() => {}); let percent = 0; const interval = setInterval(() => { if (percent < 95) { percent += Math.floor(Math.random() * 5) + 2; if (percent > 95) percent = 95; if ((window as any).PwaLoader) { (window as any).PwaLoader.update({ percent }); } } }, 150); return { finish: () => { clearInterval(interval); if ((window as any).PwaLoader) { (window as any).PwaLoader.update({ percent: 100 }); } } }; }