From 1b6e8a1832ce22113e7553c20ffc16f2083f5de6 Mon Sep 17 00:00:00 2001 From: David Frassi Date: Mon, 15 Jun 2026 00:24:27 +0200 Subject: [PATCH] feat: show instructions when playlist is empty & fix high contrast contrast --- src/app/app.component.html | 109 +++++-- src/app/app.component.ts | 56 +++- src/app/home/home.page.html | 38 ++- src/app/home/home.page.scss | 53 ++++ src/app/home/home.page.ts | 114 ++----- src/app/pages/player/player.page.ts | 24 +- src/app/pages/settings/settings.page.html | 110 ++++--- src/app/pages/settings/settings.page.ts | 362 ++++++++++++---------- src/app/services/canti.service.ts | 10 - src/app/services/playlist.service.ts | 10 + src/app/services/settings.service.ts | 36 ++- src/app/version.ts | 2 +- 12 files changed, 554 insertions(+), 370 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index 72b09b9..02cdbc4 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -9,29 +9,71 @@
CantiCristiani
-

Apertura App in corso

-

- Stiamo aprendo la PWA installata per caricare la playlist ed evitare cache del browser obsoleta. -

- - -
- -
- - -
+ + +

Apertura App in corso

+

+ Stiamo aprendo la PWA installata per caricare la playlist ed evitare cache del browser obsoleta. +

+
+ + +
+
+ + + + +
+

Aggiungi a Home (iOS)

+

+ Per aggiornamenti istantanei e uso offline, aggiungi l'app alla schermata Home di iOS: +

+
+
+
1
+
+ Tocca il pulsante Condividi 📤 in Safari. +
+
+
+
2
+
+ Scorri il menu e seleziona Aggiungi alla schermata Home ➕. +
+
+
+ +
+ + +
+

Installa CantiCristiani

+

+ Installa l'applicazione sul tuo dispositivo per evitare problemi di cache del browser ed usarla offline! +

+
+ + +
+
+
@@ -97,5 +139,28 @@ + + +
+
+ CantiCristiani +
+

Installa CantiCristiani

+

+ Installa l'applicazione sul tuo computer per evitare problemi di cache del browser ed usarla offline! +

+ +
+ + +
+
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index ee57c63..729f1d9 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, OnInit, signal } from '@angular/core'; +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'; @@ -26,12 +26,35 @@ export class AppComponent implements OnInit { 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() { @@ -50,11 +73,6 @@ export class AppComponent implements OnInit { // Set signal indicating startup version check is complete this.settingsService.isVersionCheckComplete.set(true); - - // Hide loader if canti are also already loaded - if (this.cantiService.firstLoadCompleted() && (window as any).PwaLoader) { - (window as any).PwaLoader.hide(); - } this.route.queryParams.subscribe(params => { const protocolUrl = params['url']; @@ -269,15 +287,26 @@ export class AppComponent implements OnInit { const skipRedirect = sessionStorage.getItem('skip-pwa-redirect') === 'true'; if (!skipRedirect) { this.showRedirectOverlay.set(true); + this.redirectFailed.set(false); // 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'); + } + }, 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'; - if (!skipInstall && (this.settingsService.isAndroid() || this.settingsService.isIos())) { + const isMobile = this.settingsService.isAndroid() || this.settingsService.isIos(); + const hasDesktopPrompt = this.settingsService.showInstallButton() || this.settingsService.deferredPrompt(); + if (!skipInstall && (isMobile || hasDesktopPrompt)) { this.showInstallOverlay.set(true); } } @@ -285,6 +314,13 @@ export class AppComponent implements OnInit { openPwaManual() { 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'); + } + }, 2000); } stayInBrowser() { @@ -301,6 +337,12 @@ export class AppComponent implements OnInit { 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() { diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index 3df667d..1af03c9 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -6,7 +6,7 @@
{{ appName }} - v{{ version }} + v{{ version }} - {{ settingsService.userName() }} @@ -48,11 +48,10 @@ {{ playlistService.activeListName() !== null ? 'Playlist: ' + playlistService.activeListName() : 'Playlist' }} - - + @@ -69,14 +68,6 @@ {{ comunitaService.comunitaCode() ? comunitaService.comunitaNome() : 'Comunità' }} - -
- -
- -
+ +
-
- {{ playlistService.selectedIds().size }} - +
+ {{ reorderList().length }} + - + - +
@@ -191,7 +182,7 @@ name="add-outline" (click)="$event.stopPropagation()" routerLink="/propose-canto" - style="font-size: 1.1rem; color: #000; font-weight: bold; background: rgba(0,0,0,0.1); border-radius: 50%; padding: 2px;"> + class="non-validati-add-icon">
@@ -208,6 +199,13 @@
+ + +
+

+ Per creare una playlist, seleziona il nr dei canti che vuoi inserire nella playlist, riordinali e salvala con nome +

+
diff --git a/src/app/home/home.page.scss b/src/app/home/home.page.scss index 07bd10e..7181c74 100644 --- a/src/app/home/home.page.scss +++ b/src/app/home/home.page.scss @@ -368,6 +368,24 @@ ion-title { } } +.non-validati-add-icon { + font-size: 1.1rem; + color: #000; + font-weight: bold; + background: rgba(0, 0, 0, 0.1); + border-radius: 50%; + padding: 2px; + display: inline-block; + vertical-align: middle; +} + +:host-context(body.high-contrast) { + .non-validati-add-icon { + color: #ffffff !important; + background: rgba(255, 255, 255, 0.2) !important; + } +} + // Custom Item Layout .custom-item { --padding-start: 16px; @@ -1285,3 +1303,38 @@ ion-title { } } } + +.empty-playlist-container { + padding: 12px 16px; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + box-sizing: border-box; +} + +.empty-playlist-instruction { + margin: 0; + font-size: 0.85rem; + line-height: 1.4; + color: rgba(255, 255, 255, 0.8); + text-align: center; + background: rgba(var(--ion-color-secondary-rgb), 0.08); + border: 1px dashed rgba(var(--ion-color-secondary-rgb), 0.35); + padding: 12px 16px; + border-radius: 12px; + max-width: 480px; + width: 100%; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +:host-context(body.high-contrast) { + .empty-playlist-instruction { + color: #000000 !important; + background: rgba(var(--ion-color-secondary-rgb), 0.12) !important; + border-color: var(--ion-color-secondary) !important; + box-shadow: none !important; + } +} + + diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index 3ed1fd9..8216135 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -773,9 +773,15 @@ export class HomePage implements OnDestroy { if (response.ok) { const remoteJson = await response.json(); if (Array.isArray(remoteJson)) { + // Reconstruct user name/metadata + const userMetadata = remoteJson.find((item: any) => item.momenti && item.momenti.includes('UserMetadata')); + if (userMetadata && userMetadata.titolo) { + this.settingsService.setUserName(userMetadata.titolo); + } + // Reconstruct custom songs const customSongs = remoteJson - .filter((item: any) => !item.momenti || !item.momenti.includes('Playlist')) + .filter((item: any) => (!item.momenti || !item.momenti.includes('Playlist')) && (!item.momenti || !item.momenti.includes('UserMetadata'))) .map((item: any) => ({ id: `my_${item.id_canti}`, id_canti: Number(item.id_canti), @@ -1025,6 +1031,17 @@ export class HomePage implements OnDestroy { this.searchQuery() !== ''; } + shouldShowSubSectionToolbar(): boolean { + if (this.playlistService.selectionMode() && this.reorderList().length > 0) { + return true; + } + const filterType = this.activeFilterType(); + if (filterType) { + return true; + } + return false; + } + clearAllFilters() { this.selectedLiturgico.set(null); this.selectedTematico.set(null); @@ -1655,105 +1672,12 @@ export class HomePage implements OnDestroy { } toggleComunitaFilter() { - if (!this.comunitaService.comunitaCode()) { - this.promptComunitaCode(); - } else { + if (this.comunitaService.comunitaCode()) { this.comunitaService.isFilterActive.update(v => !v); } this.activeFilterType.set(null); } - editComunitaCode(event: Event) { - event.stopPropagation(); - this.promptComunitaCode(); - } - - async promptComunitaCode() { - const alert = await this.alertCtrl.create({ - header: 'Imposta Comunità', - subHeader: 'Inserisci il codice parrocchiale/comunità per attivare il libretto dedicato:', - cssClass: 'premium-alert', - inputs: [ - { - name: 'code', - type: 'text', - placeholder: 'Es: 123456', - value: this.comunitaService.comunitaCode() - } - ], - buttons: [ - { - text: 'Annulla', - role: 'cancel' - }, - { - text: 'Rimuovi', - role: 'destructive', - cssClass: 'alert-button-delete', - handler: async () => { - await this.comunitaService.setComunitaCode(''); - const toast = await this.toastCtrl.create({ - message: 'Comunità disattivata.', - duration: 2000, - color: 'secondary' - }); - await toast.present(); - } - }, - { - text: 'Salva', - handler: async (data) => { - const trimmed = (data.code || '').trim(); - if (!trimmed) { - await this.comunitaService.setComunitaCode(''); - return; - } - - // Show loading overlay with progress - const loading = await this.loadingCtrl.create({ - message: 'Caricamento 0%', - cssClass: 'premium-loading', - spinner: 'crescent' - }); - await loading.present(); - - // Subscribe to progress updates - let progressInterval: any = null; - progressInterval = setInterval(() => { - const pct = this.comunitaService.loadingProgress(); - loading.message = `Caricamento ${pct}%`; - if (pct >= 100) { - clearInterval(progressInterval); - } - }, 100); - - const success = await this.comunitaService.setComunitaCode(trimmed); - clearInterval(progressInterval); - await loading.dismiss(); - - if (success) { - const toast = await this.toastCtrl.create({ - message: `Comunità attivata: ${this.comunitaService.comunitaNome()}`, - duration: 2000, - color: 'success' - }); - await toast.present(); - } else { - const toast = await this.toastCtrl.create({ - message: 'Codice non trovato o errore di connessione.', - duration: 2000, - color: 'danger' - }); - await toast.present(); - setTimeout(() => this.promptComunitaCode(), 500); - } - } - } - ] - }); - await alert.present(); - } - private massCardStartX: number = 0; private massCardStartY: number = 0; diff --git a/src/app/pages/player/player.page.ts b/src/app/pages/player/player.page.ts index 4959655..c8247e8 100644 --- a/src/app/pages/player/player.page.ts +++ b/src/app/pages/player/player.page.ts @@ -114,6 +114,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { private initialPinchDistance: number | null = null; private initialFontSize: number = 1.0; + public portraitFontSize: number = 1.0; private lastMatchedTranscript: string = ''; @@ -267,8 +268,10 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { this.autoscrollSpeed.set(playlistSongSetting.speed !== undefined ? playlistSongSetting.speed : 2); if (playlistSongSetting.zoom !== undefined) { this.fontSize.set(playlistSongSetting.zoom); + this.portraitFontSize = playlistSongSetting.zoom; } else { this.fontSize.set(1.0); + this.portraitFontSize = 1.0; } } else if (this.comunitaService.comunitaCode() && this.comunitaService.isFilterActive()) { const settings = this.comunitaService.comunitaCantiSettings(); @@ -289,9 +292,13 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { this.transposeAmount.set(0); this.autoscrollSpeed.set(2); } + this.fontSize.set(1.0); + this.portraitFontSize = 1.0; } else { this.transposeAmount.set(0); this.autoscrollSpeed.set(2); + this.fontSize.set(1.0); + this.portraitFontSize = 1.0; } }, { allowSignalWrites: true }); @@ -320,6 +327,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { } private checkAndLimitFontSize(targetFont: number): number { + if (!this.isLandscape()) { + return targetFont; + } const titleMain = this.el.nativeElement.querySelector('.title-main'); if (!titleMain) return targetFont; @@ -388,9 +398,8 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { } }, 150); } else { - const limitedFont = this.checkAndLimitFontSize(1.0); - this.fontSize.set(limitedFont); - this.channel.postMessage({ type: 'SYNC_FONT', fontSize: limitedFont }); + this.fontSize.set(this.portraitFontSize); + this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.portraitFontSize }); } } @@ -518,6 +527,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { } if (Math.abs(limited - this.fontSize()) > 0.01) { this.fontSize.set(limited); + if (!this.isLandscape()) { + this.portraitFontSize = limited; + } this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() }); } } @@ -582,6 +594,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { } if (limited !== this.fontSize()) { this.fontSize.set(limited); + if (!this.isLandscape()) { + this.portraitFontSize = limited; + } this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() }); this.updatePlaylistSettings(); } @@ -593,6 +608,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { if (this.fontSize() > minZ) { const target = Math.max(this.fontSize() - this.FONT_STEP, minZ); this.fontSize.set(target); + if (!this.isLandscape()) { + this.portraitFontSize = target; + } this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() }); this.updatePlaylistSettings(); } diff --git a/src/app/pages/settings/settings.page.html b/src/app/pages/settings/settings.page.html index a3267df..c307870 100644 --- a/src/app/pages/settings/settings.page.html +++ b/src/app/pages/settings/settings.page.html @@ -79,14 +79,7 @@ - - - -

Schermo Sempre Acceso

-

Evita che lo schermo si spenga

-
- -
+ @@ -145,12 +138,50 @@

Comunità

Mostra il filtro Comunità nella home

- +
+ + +
+
+
+
Comunità Attiva
+
{{ comunitaService.comunitaNome() }}
+
Codice: {{ comunitaService.comunitaCode() }}
+
+
+ + + + + + +
+
+ +
+

+ Inserisci il codice parrocchiale/comunità per scaricare i canti e le scalette dedicate: +

+
+
+ +
+ + Attiva + +
+
+
-
+

Identità Utente e Ripristino @@ -162,6 +193,17 @@ Questo codice univoco identifica in modo anonimo il tuo dispositivo e ti consente di gestire comunità e canti. Salva il codice o il QR code per ripristinare il tuo profilo su un nuovo dispositivo.

+ +
+
Nome
+ +
+
Codice ID
@@ -183,10 +225,21 @@
+ + + Ripristina ID Originario + + Salva Backup sul Server + + + + Ripristina Backup dal Server +

@@ -254,44 +307,7 @@ -
- - - -

Proponi i miei canti ({{ myCantiService.myCanti().length }})

-

Invia a {{ contactEmail }}

-
-
-
-
- - - -

Allinea con Server

-

Aggiorna canti e versione app

-
- -
- - - - -

Verifica Aggiornamenti App

-

Forza la ricerca di una nuova versione dell'applicazione

-
-
- -
-

- Versione: v{{ version }} • - Canti: {{ cantiService.canti().length }} -

-
-
-
-
-
diff --git a/src/app/pages/settings/settings.page.ts b/src/app/pages/settings/settings.page.ts index 4a32a7a..ebebbe7 100644 --- a/src/app/pages/settings/settings.page.ts +++ b/src/app/pages/settings/settings.page.ts @@ -142,9 +142,15 @@ export class SettingsPage { if (response.ok) { const remoteJson = await response.json(); if (Array.isArray(remoteJson)) { + // Reconstruct user name/metadata + const userMetadata = remoteJson.find((item: any) => item.momenti && item.momenti.includes('UserMetadata')); + if (userMetadata && userMetadata.titolo) { + this.settingsService.setUserName(userMetadata.titolo); + } + // Reconstruct custom songs const customSongs = remoteJson - .filter((item: any) => !item.momenti || !item.momenti.includes('Playlist')) + .filter((item: any) => (!item.momenti || !item.momenti.includes('Playlist')) && (!item.momenti || !item.momenti.includes('UserMetadata'))) .map((item: any) => ({ id: `my_${item.id_canti}`, id_canti: Number(item.id_canti), @@ -215,6 +221,37 @@ export class SettingsPage { await alert.present(); } + async restoreOriginalIdentity() { + const original = this.settingsService.originalUserUuid(); + if (!original) { + const alert = await this.alertCtrl.create({ + header: 'Errore', + message: 'Nessun identificativo originario trovato.', + buttons: ['OK'] + }); + await alert.present(); + return; + } + + const alert = await this.alertCtrl.create({ + header: 'Ripristina ID Originario', + message: `Sei sicuro di voler ripristinare il codice ID originario assegnato alla prima installazione? L'ID attuale del dispositivo verrà sovrascritto e verranno scaricati eventuali canti e playlist associati all'ID originario.`, + buttons: [ + { + text: 'Annulla', + role: 'cancel' + }, + { + text: 'Ripristina', + handler: () => { + this.confirmRestore(original); + } + } + ] + }); + await alert.present(); + } + async syncLocalDataToServer() { const uid = this.settingsService.userUuid(); if (!uid) { @@ -258,188 +295,201 @@ export class SettingsPage { } } + async restoreBackupFromServer() { + const code = this.settingsService.userUuid(); + if (!code) { + const alert = await this.alertCtrl.create({ + header: 'Errore', + message: 'Nessun identificativo utente (UID) trovato.', + buttons: ['OK'] + }); + await alert.present(); + return; + } + + const alert = await this.alertCtrl.create({ + header: 'Ripristina Backup', + message: 'Sei sicuro di voler ripristinare i dati dal server? I tuoi canti personali e le tue scalette locali verranno allineati con l\'ultimo backup presente sul server.', + buttons: [ + { + text: 'Annulla', + role: 'cancel' + }, + { + text: 'Ripristina', + handler: async () => { + const loading = await this.loadingCtrl.create({ + message: 'Scaricamento dati da remoto...' + }); + await loading.present(); + + try { + const response = await fetch(`https://api.canticristiani.it/${code}.json?cb=${Date.now()}`, { cache: 'no-store' }); + if (response.ok) { + const remoteJson = await response.json(); + if (Array.isArray(remoteJson)) { + // Reconstruct user name/metadata + const userMetadata = remoteJson.find((item: any) => item.momenti && item.momenti.includes('UserMetadata')); + if (userMetadata && userMetadata.titolo) { + this.settingsService.setUserName(userMetadata.titolo); + } + + // Reconstruct custom songs + const customSongs = remoteJson + .filter((item: any) => (!item.momenti || !item.momenti.includes('Playlist')) && (!item.momenti || !item.momenti.includes('UserMetadata'))) + .map((item: any) => ({ + id: `my_${item.id_canti}`, + id_canti: Number(item.id_canti), + titolo: item.titolo || 'Senza Titolo', + testo: item.testo || '', + accordi: item.testo?.includes('[') ? item.testo : undefined, + id_momenti: item.momenti?.map((m: any) => Number(m)).filter((m: any) => !isNaN(m)) || [] + })); + + this.myCantiService.myCanti.set(customSongs); + const storage = this.cantiService.getStorage(); + if (storage) { + await storage.set('my-canti', customSongs); + } + + // Reconstruct playlists + const playlists = remoteJson + .filter((item: any) => item.momenti && item.momenti.includes('Playlist')) + .map((item: any) => { + let songSettings = {}; + if (item.periodi && item.periodi.length > 0) { + try { + songSettings = JSON.parse(item.periodi[0]); + } catch (e) {} + } + return { + id: String(item.id_canti), + name: item.titolo, + ids: item.testo ? item.testo.split(',') : [], + songSettings: songSettings, + createdAt: new Date() + }; + }); + + this.playlistService.playlists.set(playlists); + if (this.playlistService['_storage']) { + const key = this.playlistService.getPlaylistsStorageKey(); + await this.playlistService['_storage'].set(key, playlists); + } + + const toast = await this.toastCtrl.create({ + message: 'Dati ripristinati con successo! Ricaricamento...', + duration: 2000, + color: 'success', + position: 'bottom' + }); + await toast.present(); + + setTimeout(() => { + window.location.replace(window.location.origin + window.location.pathname); + }, 1500); + } else { + throw new Error('Formato dati del backup non valido.'); + } + } else { + throw new Error('Nessun backup trovato sul server per questo ID.'); + } + } catch (err: any) { + console.error('Failed to restore backup data:', err); + const errorAlert = await this.alertCtrl.create({ + header: 'Errore Ripristino', + message: err.message || 'Impossibile scaricare il backup dal server. Verifica la connessione.', + buttons: ['OK'] + }); + await errorAlert.present(); + } finally { + await loading.dismiss(); + } + } + } + ] + }); + await alert.present(); + } + onModeChange(event: any) { this.settingsService.setShowChordsDefault(event.detail.value === 'chords'); } + onNameChange(event: any) { + this.settingsService.setUserName(event.target.value); + } + onMassChange(event: any) { this.cantiLettureService.setSelectedMass(event.detail.value); } - /** - * Performs a full data refresh + checks for app updates. - * Uses both the Angular SW and the version.json fallback. - */ - async fullRefresh() { - // 1. Refresh JSON data - this.cantiService.refresh(); - - // 2. Refresh liturgical readings JSON - try { - await this.cantiLettureService.fetchData(); - } catch (err) { - console.error('Failed to refresh liturgical readings:', err); - } - // 3. Refresh community data if a code is active - const comunitaCode = this.comunitaService.comunitaCode(); - if (comunitaCode) { - try { - await this.comunitaService.setComunitaCode(comunitaCode); - } catch (err) { - console.error('Failed to refresh community data:', err); - } - } - // 4. Check for app updates (SW + version.json fallback) - const updateAvailable = await this.performUpdateCheck(); - if (updateAvailable) { - return; // updateAvailable already triggered the update/reload flow + async onComunitaToggleChange(event: any) { + const checked = event.detail.checked; + if (checked) { + this.settingsService.comunitaEnabled.set(true); + localStorage.setItem('comunita-enabled', 'true'); + } else { + this.settingsService.comunitaEnabled.set(false); + localStorage.setItem('comunita-enabled', 'false'); } - - const toast = await this.toastCtrl.create({ - message: 'Dati aggiornati correttamente!', - duration: 2000, - color: 'success' - }); - await toast.present(); } - async checkForAppUpdate() { - if (!this.swUpdate.isEnabled) { - const toast = await this.toastCtrl.create({ - message: 'Aggiornamenti non supportati su questo browser.', - duration: 3000, - color: 'medium' - }); - await toast.present(); - return; - } + async editComunita() { + await this.comunitaService.setComunitaCode(''); + } - const toastLoading = await this.toastCtrl.create({ - message: 'Ricerca aggiornamenti in corso...', - duration: 1500, - color: 'secondary' + async saveComunitaCode(code: string) { + const trimmed = (code || '').trim(); + if (!trimmed) return; + + const loading = await this.loadingCtrl.create({ + message: 'Caricamento 0%', + cssClass: 'premium-loading', + spinner: 'crescent' }); - await toastLoading.present(); + await loading.present(); - const updateAvailable = await this.performUpdateCheck(); + let progressInterval = setInterval(() => { + const pct = this.comunitaService.loadingProgress(); + loading.message = `Caricamento ${pct}%`; + if (pct >= 100) { + clearInterval(progressInterval); + } + }, 100); - if (!updateAvailable) { + const success = await this.comunitaService.setComunitaCode(trimmed); + clearInterval(progressInterval); + await loading.dismiss(); + + if (success) { const toast = await this.toastCtrl.create({ - message: 'L\'applicazione è già aggiornata all\'ultima versione.', - duration: 3000, + message: `Comunità attivata: ${this.comunitaService.comunitaNome()}`, + duration: 2000, color: 'success' }); await toast.present(); - } - } - - /** - * Shared update check logic: tries Angular SW first, falls back to version.json. - * Returns true if an update was found and the reload flow was initiated. - */ - private async performUpdateCheck(): Promise { - try { - // Layer 1: Force the browser to re-fetch the SW script - if ('serviceWorker' in navigator) { - const registration = await navigator.serviceWorker.ready; - await registration.update(); - } - - let sub: any = null; - let readyPromise: Promise | undefined = undefined; - - if (this.swUpdate.isEnabled) { - readyPromise = new Promise((resolve) => { - sub = this.swUpdate.versionUpdates - .pipe( - filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'), - first() - ) - .subscribe(() => { - resolve(); - }); - }); - } - - // Layer 2: Ask Angular SW to check - if (this.swUpdate.isEnabled) { - const swFoundUpdate = await this.swUpdate.checkForUpdate(); - if (swFoundUpdate) { - await this.applyUpdateAndReload(readyPromise, sub); - return true; - } - } - - if (sub) sub.unsubscribe(); - - // Layer 3: Fallback — check version.json - const versionMismatch = await this.checkVersionJson(); - if (versionMismatch) { - console.log('[PWA-Update] version.json mismatch detected from settings'); - await this.applyUpdateAndReload(); - return true; - } - - return false; - } catch (err) { - console.error('[PWA-Update] Update check failed from settings:', err); + } else { const toast = await this.toastCtrl.create({ - message: 'Errore durante la ricerca di aggiornamenti.', - duration: 3000, + message: 'Codice non trovato o errore di connessione.', + duration: 2000, color: 'danger' }); await toast.present(); - return false; } } - private async applyUpdateAndReload(readyPromise?: Promise, subscription?: any) { - const overlay = showFullscreenUpdateOverlay(); - - let activated = false; - const activateAndReload = async () => { - if (activated) return; - activated = true; - try { - if (this.swUpdate.isEnabled) { - await this.swUpdate.activateUpdate(); - } - } catch (e) { - console.warn('[PWA-Update] activateUpdate failed:', e); - } - overlay.finish(); - setTimeout(() => { - window.location.replace(window.location.origin + window.location.pathname + '?update=' + Date.now()); - }, 600); - }; - - if (readyPromise) { - Promise.race([ - readyPromise, - new Promise((resolve) => setTimeout(resolve, 25000)) - ]).then(() => { - if (subscription) subscription.unsubscribe(); - activateAndReload(); - }); - } else { - setTimeout(() => { - activateAndReload(); - }, 1000); - } - } - - private async checkVersionJson(): Promise { - try { - const response = await fetch(`/version.json?cb=${Date.now()}`, { cache: 'no-store' }); - if (!response.ok) return false; - const data = await response.json(); - console.log(`[PWA-Update] Settings version check: local=${VERSION}, remote=${data.version}`); - return data.version !== VERSION; - } catch (err) { - console.warn('[PWA-Update] version.json check failed:', err); - return false; - } + async removeComunita() { + await this.comunitaService.setComunitaCode(''); + this.settingsService.comunitaEnabled.set(false); + localStorage.setItem('comunita-enabled', 'false'); + const toast = await this.toastCtrl.create({ + message: 'Comunità disattivata.', + duration: 2000, + color: 'secondary' + }); + await toast.present(); } } diff --git a/src/app/services/canti.service.ts b/src/app/services/canti.service.ts index afb7ead..56da78c 100644 --- a/src/app/services/canti.service.ts +++ b/src/app/services/canti.service.ts @@ -60,9 +60,6 @@ export class CantiService { await this.loadFromStorage(); if (this.canti() && this.canti().length > 0) { this.firstLoadCompleted.set(true); - if (this.settingsService.isVersionCheckComplete() && (window as any).PwaLoader) { - (window as any).PwaLoader.hide(); - } } else { // First boot or data cleared: show setup loader immediately if ((window as any).PwaLoader) { @@ -159,19 +156,12 @@ export class CantiService { this.progress.set(100); this.loading.set(false); this.firstLoadCompleted.set(true); - - if (this.settingsService.isVersionCheckComplete() && (window as any).PwaLoader) { - (window as any).PwaLoader.hide(); - } } }, error: (error) => { console.error('Failed to fetch canti', error); this.loading.set(false); this.firstLoadCompleted.set(true); - if ((window as any).PwaLoader) { - (window as any).PwaLoader.hide(); - } } }); } diff --git a/src/app/services/playlist.service.ts b/src/app/services/playlist.service.ts index 92f0eac..ea41c54 100644 --- a/src/app/services/playlist.service.ts +++ b/src/app/services/playlist.service.ts @@ -363,6 +363,16 @@ export class PlaylistService { const payload = [...customSongs, ...playlistSongs]; + if (this.settingsService.userName()) { + payload.push({ + id_canti: 999999, + titolo: this.settingsService.userName(), + momenti: ['UserMetadata'], + periodi: [], + testo: '' + }); + } + const response = await fetch('https://api.canticristiani.it/miei', { method: 'POST', headers: { diff --git a/src/app/services/settings.service.ts b/src/app/services/settings.service.ts index def04a3..331bb59 100644 --- a/src/app/services/settings.service.ts +++ b/src/app/services/settings.service.ts @@ -56,6 +56,12 @@ export class SettingsService { /** Identificativo utente univoco per la gestione delle comunità */ public userUuid = signal(''); + /** Identificativo originario assegnato alla prima installazione */ + public originalUserUuid = signal(''); + + /** Nome associato all'identità utente */ + public userName = signal(''); + private wakeLock: any = null; // PWA installation signals @@ -83,6 +89,19 @@ export class SettingsService { } this.userUuid.set(savedUuid); + // Salvataggio dell'ID originario (alla prima installazione) se non è già presente + let originalUuid = localStorage.getItem('original-user-uuid'); + if (!originalUuid) { + originalUuid = savedUuid; + localStorage.setItem('original-user-uuid', originalUuid); + } + this.originalUserUuid.set(originalUuid); + + const savedName = localStorage.getItem('user-name'); + if (savedName) { + this.userName.set(savedName); + } + // Detect PWA status try { this.isStandalone.set( @@ -166,12 +185,8 @@ export class SettingsService { this.autoAdvance.set(true); } - const savedKeepScreenOn = localStorage.getItem('keep-screen-on'); - if (savedKeepScreenOn !== null) { - this.keepScreenOn.set(savedKeepScreenOn === 'true'); - } else { - this.keepScreenOn.set(true); - } + // Keep screen always on by default and always active + this.keepScreenOn.set(true); const savedComunitaEnabled = localStorage.getItem('comunita-enabled'); if (savedComunitaEnabled !== null) { @@ -318,9 +333,6 @@ export class SettingsService { localStorage.setItem('show-editor', newValue.toString()); } - toggleKeepScreenOn() { - this.keepScreenOn.update(v => !v); - } toggleComunitaEnabled() { const newValue = !this.comunitaEnabled(); @@ -426,6 +438,12 @@ export class SettingsService { } } + setUserName(name: string) { + const trimmed = name.trim(); + this.userName.set(trimmed); + localStorage.setItem('user-name', trimmed); + } + async installPwa() { const promptEvent = this.deferredPrompt(); if (!promptEvent) { diff --git a/src/app/version.ts b/src/app/version.ts index ef20d01..b09cb27 100644 --- a/src/app/version.ts +++ b/src/app/version.ts @@ -1 +1 @@ -export const VERSION = '2026.06.14.1936'; +export const VERSION = '2026.06.15.0021';