Files
canti/src/app/app.component.ts
T

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