Files
canti/src/app/app.component.ts
T
2026-06-17 01:04:27 +02:00

428 lines
15 KiB
TypeScript

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<boolean>(false);
public showInstallOverlay = signal<boolean>(false);
public redirectFailed = signal<boolean>(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<boolean> {
// 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<Response | null>((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<boolean>((resolve) => setTimeout(() => resolve(false), 8000))
]);
if (hasSwUpdate) {
console.log('[PWA-Update] SW aggiornamento disponibile, attivazione...');
await Promise.race([
this.swUpdate.activateUpdate(),
new Promise<void>((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<boolean>((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 });
}
}
};
}