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, NavigationStart } from '@angular/router'; import { ToastController, Platform } from '@ionic/angular'; import { SwUpdate, VersionReadyEvent } from '@angular/service-worker'; import { filter, first } from 'rxjs/operators'; import { App } from '@capacitor/app'; import { addIcons } from 'ionicons'; import { add, addCircleOutline, addOutline, analyticsOutline, arrowUpCircle, bookOutline, calendarOutline, cameraOutline, cameraReverseOutline, carOutline, chevronBack, chevronBackOutline, chevronDown, chevronDownOutline, chevronForward, chevronForwardOutline, chevronUp, chevronUpOutline, closeCircle, closeCircleOutline, closeOutline, cloudDownloadOutline, cloudOfflineOutline, cloudUploadOutline, contrastOutline, copyOutline, createOutline, documentAttachOutline, documentTextOutline, downloadOutline, eyeOutline, informationCircleOutline, keypadOutline, listOutline, logoApple, logoYoutube, mic, micOutline, musicalNote, musicalNotesOutline, pause, pauseSharp, peopleOutline, personOutline, phoneLandscapeOutline, play, playForwardOutline, playSharp, playSkipBackSharp, playSkipForwardSharp, pricetagsOutline, qrCodeOutline, refreshOutline, remove, removeCircleOutline, removeOutline, reorderTwoOutline, saveOutline, scanOutline, searchOutline, settingsOutline, shareOutline, shareSocialOutline, sparklesOutline, statsChartOutline, swapVerticalOutline, trashOutline, arrowUndoOutline, videocam, videocamOffOutline } from 'ionicons/icons'; @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 platform = inject(Platform); 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 isInstalling = signal(false); public isRedirecting = signal(false); public isPwaInstalled = 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. addIcons({ add, 'add-circle-outline': addCircleOutline, 'add-outline': addOutline, 'analytics-outline': analyticsOutline, 'arrow-up-circle': arrowUpCircle, 'book-outline': bookOutline, 'calendar-outline': calendarOutline, 'camera-outline': cameraOutline, 'camera-reverse-outline': cameraReverseOutline, 'car-outline': carOutline, 'chevron-back': chevronBack, 'chevron-back-outline': chevronBackOutline, 'chevron-down': chevronDown, 'chevron-down-outline': chevronDownOutline, 'chevron-forward': chevronForward, 'chevron-forward-outline': chevronForwardOutline, 'chevron-up': chevronUp, 'chevron-up-outline': chevronUpOutline, 'close-circle': closeCircle, 'close-circle-outline': closeCircleOutline, 'close-outline': closeOutline, 'cloud-download-outline': cloudDownloadOutline, 'cloud-offline-outline': cloudOfflineOutline, 'cloud-upload-outline': cloudUploadOutline, 'contrast-outline': contrastOutline, 'copy-outline': copyOutline, 'create-outline': createOutline, 'document-attach-outline': documentAttachOutline, 'document-text-outline': documentTextOutline, 'download-outline': downloadOutline, 'eye-outline': eyeOutline, 'information-circle-outline': informationCircleOutline, 'keypad-outline': keypadOutline, 'list-outline': listOutline, 'logo-apple': logoApple, 'logo-youtube': logoYoutube, mic, 'mic-outline': micOutline, 'musical-note': musicalNote, 'musical-notes-outline': musicalNotesOutline, pause, 'pause-sharp': pauseSharp, 'people-outline': peopleOutline, 'person-outline': personOutline, 'phone-landscape-outline': phoneLandscapeOutline, play, 'play-forward-outline': playForwardOutline, 'play-sharp': playSharp, 'play-skip-back-sharp': playSkipBackSharp, 'play-skip-forward-sharp': playSkipForwardSharp, 'pricetags-outline': pricetagsOutline, 'qr-code-outline': qrCodeOutline, 'refresh-outline': refreshOutline, remove, 'remove-circle-outline': removeCircleOutline, 'remove-outline': removeOutline, 'reorder-two-outline': reorderTwoOutline, 'save-outline': saveOutline, 'scan-outline': scanOutline, 'search-outline': searchOutline, 'settings-outline': settingsOutline, 'share-outline': shareOutline, 'share-social-outline': shareSocialOutline, 'sparkles-outline': sparklesOutline, 'stats-chart-outline': statsChartOutline, 'swap-vertical-outline': swapVerticalOutline, 'trash-outline': trashOutline, 'arrow-undo-outline': arrowUndoOutline, videocam, 'videocam-off-outline': videocamOffOutline }); effect(() => { this.checkLoaderDismissal(); }); } checkLoaderDismissal() { const ready = this.settingsService.isVersionCheckComplete() && this.cantiService.firstLoadCompleted(); if (!ready) { return; } // Se stiamo attivamente installando o reindirizzando, NON nascondiamo il loader if (this.isInstalling() || this.isRedirecting()) { return; } // Altrimenti, nascondiamo il loader per far entrare l'utente if ((window as any).PwaLoader) { (window as any).PwaLoader.hide(); } } async ngOnInit() { // Gestione tasto back per PWA/Browser (intercettando popstate di Angular Router) this.router.events.subscribe(event => { if (event instanceof NavigationStart && event.navigationTrigger === 'popstate') { const targetUrl = event.url.split('?')[0]; if (targetUrl !== '/home' && targetUrl !== '/') { this.router.navigate(['/home'], { replaceUrl: true }); } } }); // Gestione tasto back hardware per nativo (Capacitor/Cordova) this.platform.backButton.subscribeWithPriority(9999, () => { const currentUrl = this.router.url; const path = currentUrl.split('?')[0]; if (path !== '/home' && path !== '/') { this.router.navigate(['/home']); } else { App.exitApp(); } }); // 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); // Handle PWA Launch Queue if supported (focus-existing launch behavior) if ('launchQueue' in window) { (window as any).launchQueue.setConsumer((launchParams: any) => { if (launchParams.targetURL) { this.handleLaunchUrl(launchParams.targetURL); } }); } 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); } } }); window.addEventListener('appinstalled', () => { console.log('[AppComponent] PWA appinstalled event caught.'); localStorage.setItem('pwa-installed', 'true'); this.isPwaInstalled.set(true); this.showInstallOverlay.set(false); this.showRedirectOverlay.set(false); // Se la finestra è GIÀ stata trasformata in PWA standalone (es. Desktop Mac/Windows) const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (navigator as any).standalone; if (isStandalone) { this.isInstalling.set(false); this.isRedirecting.set(false); this.checkLoaderDismissal(); return; } // Su Android, l'evento 'appinstalled' scatta in 2-3 secondi, MA l'OS impiega 10-12s // per pacchettizzare e registrare il WebAPK nella schermata Home. // Calibriamo la progress bar spalmata su ~11 secondi reali prima del reindirizzamento. this.isInstalling.set(true); this.isRedirecting.set(false); if ((window as any).PwaLoader) { (window as any).PwaLoader.show(); (window as any).PwaLoader.update({ title: 'Installazione applicazione', phase: 'Fase: Registrazione', desc: 'Generazione e registrazione dell\'applicazione sul dispositivo in corso...', percent: 15 }); } const startTime = Date.now(); const TARGET_DURATION_MS = 11000; // 11 secondi reali per completare l'installazione WebAPK const timer = setInterval(() => { const elapsed = Date.now() - startTime; let pct = Math.min(100, Math.round(15 + (elapsed / TARGET_DURATION_MS) * 85)); if ((window as any).PwaLoader) { (window as any).PwaLoader.update({ percent: pct }); } if (pct >= 100) { clearInterval(timer); this.isInstalling.set(false); this.isRedirecting.set(true); if ((window as any).PwaLoader) { (window as any).PwaLoader.update({ title: 'Chiudi il browser', desc: 'Applicazione installata con successo! Chiudi il browser e continua sulla PWA.', isRedirect: true }); } if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { console.log('[AppComponent] Running on localhost - skipping protocol link redirect on appinstalled.'); this.isRedirecting.set(false); this.checkLoaderDismissal(); return; } setTimeout(() => { window.location.href = this.protocolLink; }, 1000); } }, 250); }); this.checkAndRedirectToPwa(); } handleLaunchUrl(urlStr: string) { try { const urlObj = new URL(urlStr); // 1. Check if it's a protocol link inside query params (e.g. /?url=web+canti://...) const protocolUrl = urlObj.searchParams.get('url'); if (protocolUrl && protocolUrl.startsWith('web+canti:')) { const cleanUrl = protocolUrl.replace('web+canti://', 'http://localhost/'); const innerUrlObj = new URL(cleanUrl); let targetPath = innerUrlObj.pathname; if (targetPath === '/open' || targetPath === '//open') { targetPath = '/'; } else if (targetPath.startsWith('/open/')) { targetPath = targetPath.substring(5); } const queryParams: any = {}; innerUrlObj.searchParams.forEach((value, key) => { queryParams[key] = value; }); console.log('[AppComponent] Launch queue routing (protocol) to:', targetPath, queryParams); this.router.navigate([targetPath], { queryParams, replaceUrl: true }); return; } // 2. Otherwise, route directly to the pathname and query params of the URL let targetPath = urlObj.pathname; const queryParams: any = {}; urlObj.searchParams.forEach((value, key) => { queryParams[key] = value; }); console.log('[AppComponent] Launch queue routing (direct) to:', targetPath, queryParams); this.router.navigate([targetPath], { queryParams, replaceUrl: true }); } catch (e) { console.error('Failed to parse launch url:', urlStr, e); } } async checkVersionSync(): Promise { // Verifica all'avvio se è disponibile una nuova versione remota (sia tramite version.json che SwUpdate) // Se disponibile, viene aggiornata e attivata automaticamente senza richiedere l'intervento dell'utente. 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}. Avvio aggiornamento automatico...`); // Scarica e attiva il nuovo Service Worker se abilitato 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 automatica...'); await Promise.race([ this.swUpdate.activateUpdate(), new Promise((resolve) => setTimeout(resolve, 5000)) ]); } } catch (e) { console.warn('[PWA-Update] SwUpdate durante mismatch fallito:', e); } } // Forziamo il controllo di aggiornamento della registrazione Service Worker 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); } } // Deregistra i vecchi Service Worker per applicare la nuova versione pulita if ('serviceWorker' in navigator) { const registrations = await navigator.serviceWorker.getRegistrations(); for (const registration of registrations) { await registration.unregister(); } } // Pulisci le cache del browser if ('caches' in window) { const keys = await caches.keys(); for (const key of keys) { await caches.delete(key); } } // Ricarica la pagina in modo trasparente 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); } // Fallback: Controlla direttamente tramite SwUpdate se non intercettato da version.json if (this.swUpdate.isEnabled) { try { console.log('[PWA-Update] Verifica aggiornamenti via SwUpdate all\'avvio...'); let activated = false; const activateAndReload = async () => { if (activated) return; activated = true; try { await this.swUpdate.activateUpdate(); } catch (e) { console.warn('[PWA-Update] activateUpdate fallito all\'avvio:', e); } 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); } } const url = new URL(window.location.href); url.searchParams.set('update_cb', Date.now().toString()); window.location.replace(url.toString()); }; 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, ricaricamento automatico...'); activateAndReload(); }); 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. Applicazione automatica...'); setTimeout(() => { console.log('[PWA-Update] Safety timeout raggiunto all\'avvio, procedo...'); sub.unsubscribe(); activateAndReload(); }, 15000); return true; } else { sub.unsubscribe(); } } catch (err) { console.warn('[PWA-Update] Controllo SwUpdate fallito all\'avvio:', err); } } return false; } async checkAndRedirectToPwa() { if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { console.log('[AppComponent] Running on localhost - skipping PWA redirect and install overlays.'); this.checkLoaderDismissal(); return; } 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}`; let isInstalled = false; if ('getInstalledRelatedApps' in navigator) { try { const relatedApps = await (navigator as any).getInstalledRelatedApps(); isInstalled = relatedApps.length > 0; } catch (e) { console.warn('Failed to check installed apps:', e); } } else { isInstalled = localStorage.getItem('pwa-installed') === 'true'; } // Se prima era salvata come installata ma l'utente riceve il prima possibile un beforeinstallprompt, // significa che l'app è stata disinstallata! if (this.settingsService.deferredPrompt() || this.settingsService.showInstallButton()) { isInstalled = false; localStorage.setItem('pwa-installed', 'false'); } this.isPwaInstalled.set(isInstalled); if (isInstalled) { if (sessionStorage.getItem('skip-pwa-redirect') === 'true') { this.showRedirectOverlay.set(false); this.checkLoaderDismissal(); return; } 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, mostriamo lo stato fallito per aprire manualmente o indicare la disinstallazione setTimeout(() => { if (this.showRedirectOverlay()) { this.redirectFailed.set(true); 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); } else { this.checkLoaderDismissal(); } } } 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); this.checkLoaderDismissal(); } stayInBrowserForceUninstallCheck() { localStorage.setItem('pwa-installed', 'false'); this.isPwaInstalled.set(false); this.stayInBrowser(); } closeInstallOverlay() { sessionStorage.setItem('skip-pwa-install', 'true'); this.showInstallOverlay.set(false); this.checkLoaderDismissal(); } async triggerInstall() { this.isInstalling.set(true); this.showInstallOverlay.set(false); if ((window as any).PwaLoader) { (window as any).PwaLoader.show(); (window as any).PwaLoader.update({ title: 'Installazione in corso...', desc: 'Completa l\'installazione tramite la finestra del browser.' }); } const outcome = await this.settingsService.installPwa(); if (outcome === 'accepted') { this.isInstalling.set(false); this.isRedirecting.set(true); if ((window as any).PwaLoader) { (window as any).PwaLoader.update({ title: 'Apertura Applicazione...', desc: 'Installazione completata! Chiudi il browser e continua sulla PWA.', isRedirect: true }); } } else { this.isInstalling.set(false); this.isRedirecting.set(false); this.showInstallOverlay.set(true); this.checkLoaderDismissal(); } } async triggerInstallFromRedirect() { this.isInstalling.set(true); this.showRedirectOverlay.set(false); if ((window as any).PwaLoader) { (window as any).PwaLoader.show(); (window as any).PwaLoader.update({ title: 'Installazione in corso...', desc: 'Completa l\'installazione tramite la finestra del browser.' }); } const outcome = await this.settingsService.installPwa(); if (outcome === 'accepted') { sessionStorage.setItem('skip-pwa-redirect', 'true'); this.isInstalling.set(false); this.isRedirecting.set(true); if ((window as any).PwaLoader) { (window as any).PwaLoader.update({ title: 'Apertura Applicazione...', desc: 'Installazione completata! Chiudi il browser e continua sulla PWA.', isRedirect: true }); } } else { this.isInstalling.set(false); this.isRedirecting.set(false); this.showRedirectOverlay.set(true); this.checkLoaderDismissal(); } } } 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 }); } } }; }