428 lines
15 KiB
TypeScript
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 });
|
|
}
|
|
}
|
|
};
|
|
}
|