feat: show instructions when playlist is empty & fix high contrast contrast
This commit is contained in:
+87
-22
@@ -9,29 +9,71 @@
|
|||||||
<div style="margin-bottom: 25px; display: inline-block;">
|
<div style="margin-bottom: 25px; display: inline-block;">
|
||||||
<img src="assets/icon/favicon.png" alt="CantiCristiani" style="width: 80px; height: 80px; border-radius: 20px; box-shadow: 0 8px 24px rgba(230, 126, 34, 0.4); border: 2px solid rgba(230, 126, 34, 0.2);">
|
<img src="assets/icon/favicon.png" alt="CantiCristiani" style="width: 80px; height: 80px; border-radius: 20px; box-shadow: 0 8px 24px rgba(230, 126, 34, 0.4); border: 2px solid rgba(230, 126, 34, 0.2);">
|
||||||
</div>
|
</div>
|
||||||
<h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 8px; color: #ffffff; -webkit-font-smoothing: antialiased;">Apertura App in corso</h2>
|
|
||||||
<p style="font-size: 0.95rem; color: rgba(255, 255, 255, 0.65); margin-bottom: 25px; line-height: 1.5; -webkit-font-smoothing: antialiased;">
|
|
||||||
Stiamo aprendo la PWA installata per caricare la playlist ed evitare cache del browser obsoleta.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Premium CSS Spinner -->
|
|
||||||
<div style="display: inline-block; width: 40px; height: 40px; border: 3px solid rgba(230, 126, 34, 0.15); border-radius: 50%; border-top-color: #e67e22; animation: spin 1s ease-in-out infinite; margin-bottom: 30px;"></div>
|
|
||||||
<style>
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 12px; width: 100%;">
|
<!-- Fase di redirect normale (in attesa di apertura) -->
|
||||||
<button (click)="openPwaManual()"
|
<ng-container *ngIf="!redirectFailed()">
|
||||||
style="background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: #ffffff; border: none; padding: 14px 20px; border-radius: 12px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 15px rgba(230, 126, 34, 0.25);">
|
<h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 8px; color: #ffffff; -webkit-font-smoothing: antialiased;">Apertura App in corso</h2>
|
||||||
Apri Manualmente
|
<p style="font-size: 0.95rem; color: rgba(255, 255, 255, 0.65); margin-bottom: 25px; line-height: 1.5; -webkit-font-smoothing: antialiased;">
|
||||||
</button>
|
Stiamo aprendo la PWA installata per caricare la playlist ed evitare cache del browser obsoleta.
|
||||||
<button (click)="stayInBrowser()"
|
</p>
|
||||||
style="background: transparent; color: rgba(255, 255, 255, 0.7); border: 1px solid rgba(255, 255, 255, 0.15); padding: 12px 20px; border-radius: 12px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease;">
|
<div style="display: flex; flex-direction: column; gap: 12px; width: 100%;">
|
||||||
Rimani nel browser
|
<button (click)="openPwaManual()"
|
||||||
</button>
|
style="background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: #ffffff; border: none; padding: 14px 20px; border-radius: 12px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 15px rgba(230, 126, 34, 0.25);">
|
||||||
</div>
|
Apri Manualmente
|
||||||
|
</button>
|
||||||
|
<button (click)="stayInBrowser()"
|
||||||
|
style="background: transparent; color: rgba(255, 255, 255, 0.7); border: 1px solid rgba(255, 255, 255, 0.15); padding: 12px 20px; border-radius: 12px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease;">
|
||||||
|
Rimani nel browser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Se l'apertura automatica/manuale fallisce (PWA probabilmente non installata) -->
|
||||||
|
<ng-container *ngIf="redirectFailed()">
|
||||||
|
<!-- iOS Guide -->
|
||||||
|
<div *ngIf="settingsService.isIos()" style="text-align: left;">
|
||||||
|
<h2 style="font-size: 1.5rem; font-weight: 600; margin-bottom: 8px; color: #ffffff; text-align: center; -webkit-font-smoothing: antialiased;">Aggiungi a Home (iOS)</h2>
|
||||||
|
<p style="font-size: 0.9rem; color: rgba(255, 255, 255, 0.65); line-height: 1.4; text-align: center; margin-bottom: 20px; -webkit-font-smoothing: antialiased;">
|
||||||
|
Per aggiornamenti istantanei e uso offline, aggiungi l'app alla schermata Home di iOS:
|
||||||
|
</p>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 16px; margin-bottom: 25px; color: rgba(255, 255, 255, 0.85); font-size: 0.95rem;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 15px;">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: rgba(230, 126, 34, 0.15); border-radius: 50%; color: #e67e22; font-weight: bold; flex-shrink: 0;">1</div>
|
||||||
|
<div style="flex-grow: 1; -webkit-font-smoothing: antialiased;">
|
||||||
|
Tocca il pulsante <strong>Condividi</strong> <span style="display: inline-flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.1); border-radius: 6px; padding: 4px; font-size: 1.1rem; vertical-align: middle;">📤</span> in Safari.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 15px;">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: rgba(230, 126, 34, 0.15); border-radius: 50%; color: #e67e22; font-weight: bold; flex-shrink: 0;">2</div>
|
||||||
|
<div style="flex-grow: 1; -webkit-font-smoothing: antialiased;">
|
||||||
|
Scorri il menu e seleziona <strong>Aggiungi alla schermata Home</strong> ➕.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button (click)="stayInBrowser()"
|
||||||
|
style="background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: #ffffff; border: none; padding: 14px 20px; border-radius: 12px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 15px rgba(230, 126, 34, 0.25); width: 100%; text-align: center;">
|
||||||
|
Continua nel browser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Android / Desktop -->
|
||||||
|
<div *ngIf="!settingsService.isIos()" style="text-align: center;">
|
||||||
|
<h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 8px; color: #ffffff; -webkit-font-smoothing: antialiased;">Installa CantiCristiani</h2>
|
||||||
|
<p style="font-size: 0.95rem; color: rgba(255, 255, 255, 0.65); margin-bottom: 25px; line-height: 1.5; -webkit-font-smoothing: antialiased;">
|
||||||
|
Installa l'applicazione sul tuo dispositivo per evitare problemi di cache del browser ed usarla offline!
|
||||||
|
</p>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 12px; width: 100%;">
|
||||||
|
<button *ngIf="settingsService.showInstallButton() || settingsService.deferredPrompt()" (click)="triggerInstallFromRedirect()"
|
||||||
|
style="background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: #ffffff; border: none; padding: 14px 20px; border-radius: 12px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 15px rgba(230, 126, 34, 0.25);">
|
||||||
|
INSTALLA APP
|
||||||
|
</button>
|
||||||
|
<button (click)="stayInBrowser()"
|
||||||
|
style="background: transparent; color: rgba(255, 255, 255, 0.7); border: 1px solid rgba(255, 255, 255, 0.15); padding: 12px 20px; border-radius: 12px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; width: 100%;">
|
||||||
|
Continua nel browser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -97,5 +139,28 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop / Other Install Prompt -->
|
||||||
|
<div *ngIf="!settingsService.isAndroid() && !settingsService.isIos()"
|
||||||
|
style="text-align: center; padding: 30px; max-width: 420px; width: 100%; background: rgba(30, 30, 30, 0.75); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 24px; box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);">
|
||||||
|
<div style="margin-bottom: 25px; display: inline-block;">
|
||||||
|
<img src="assets/icon/favicon.png" alt="CantiCristiani" style="width: 80px; height: 80px; border-radius: 20px; box-shadow: 0 8px 24px rgba(230, 126, 34, 0.4); border: 2px solid rgba(230, 126, 34, 0.2);">
|
||||||
|
</div>
|
||||||
|
<h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 8px; color: #ffffff; -webkit-font-smoothing: antialiased;">Installa CantiCristiani</h2>
|
||||||
|
<p style="font-size: 0.95rem; color: rgba(255, 255, 255, 0.65); margin-bottom: 25px; line-height: 1.5; -webkit-font-smoothing: antialiased;">
|
||||||
|
Installa l'applicazione sul tuo computer per evitare problemi di cache del browser ed usarla offline!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 12px; width: 100%;">
|
||||||
|
<button (click)="triggerInstall()"
|
||||||
|
style="background: linear-gradient(135deg, #e67e22 0%, #d35400 100%); color: #ffffff; border: none; padding: 14px 20px; border-radius: 12px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 15px rgba(230, 126, 34, 0.25);">
|
||||||
|
INSTALLA APP
|
||||||
|
</button>
|
||||||
|
<button (click)="closeInstallOverlay()"
|
||||||
|
style="background: transparent; color: rgba(255, 255, 255, 0.7); border: 1px solid rgba(255, 255, 255, 0.15); padding: 12px 20px; border-radius: 12px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease;">
|
||||||
|
Continua nel browser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ion-app>
|
</ion-app>
|
||||||
|
|||||||
@@ -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 { ThemeService } from './services/theme.service';
|
||||||
import { CantiService } from './services/canti.service';
|
import { CantiService } from './services/canti.service';
|
||||||
import { SettingsService } from './services/settings.service';
|
import { SettingsService } from './services/settings.service';
|
||||||
@@ -26,12 +26,35 @@ export class AppComponent implements OnInit {
|
|||||||
|
|
||||||
public showRedirectOverlay = signal<boolean>(false);
|
public showRedirectOverlay = signal<boolean>(false);
|
||||||
public showInstallOverlay = signal<boolean>(false);
|
public showInstallOverlay = signal<boolean>(false);
|
||||||
|
public redirectFailed = signal<boolean>(false);
|
||||||
public protocolLink = '';
|
public protocolLink = '';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Gli aggiornamenti automatici e periodici sono stati rimossi.
|
// Gli aggiornamenti automatici e periodici sono stati rimossi.
|
||||||
// L'aggiornamento viene gestito esclusivamente in modo manuale
|
// L'aggiornamento viene gestito esclusivamente in modo manuale
|
||||||
// tramite il pulsante "Verifica Aggiornamenti App" in SettingsPage.
|
// 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() {
|
async ngOnInit() {
|
||||||
@@ -50,11 +73,6 @@ export class AppComponent implements OnInit {
|
|||||||
|
|
||||||
// Set signal indicating startup version check is complete
|
// Set signal indicating startup version check is complete
|
||||||
this.settingsService.isVersionCheckComplete.set(true);
|
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 => {
|
this.route.queryParams.subscribe(params => {
|
||||||
const protocolUrl = params['url'];
|
const protocolUrl = params['url'];
|
||||||
@@ -269,15 +287,26 @@ export class AppComponent implements OnInit {
|
|||||||
const skipRedirect = sessionStorage.getItem('skip-pwa-redirect') === 'true';
|
const skipRedirect = sessionStorage.getItem('skip-pwa-redirect') === 'true';
|
||||||
if (!skipRedirect) {
|
if (!skipRedirect) {
|
||||||
this.showRedirectOverlay.set(true);
|
this.showRedirectOverlay.set(true);
|
||||||
|
this.redirectFailed.set(false);
|
||||||
// Tentiamo il reindirizzamento automatico
|
// Tentiamo il reindirizzamento automatico
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = this.protocolLink;
|
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);
|
}, 800);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Se non è installata, proponiamo l'installazione immediata per evitare la cache del browser e avere un'esperienza ottimale
|
// 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 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);
|
this.showInstallOverlay.set(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,6 +314,13 @@ export class AppComponent implements OnInit {
|
|||||||
|
|
||||||
openPwaManual() {
|
openPwaManual() {
|
||||||
window.location.href = this.protocolLink;
|
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() {
|
stayInBrowser() {
|
||||||
@@ -301,6 +337,12 @@ export class AppComponent implements OnInit {
|
|||||||
await this.settingsService.installPwa();
|
await this.settingsService.installPwa();
|
||||||
this.closeInstallOverlay();
|
this.closeInstallOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async triggerInstallFromRedirect() {
|
||||||
|
await this.settingsService.installPwa();
|
||||||
|
sessionStorage.setItem('skip-pwa-redirect', 'true');
|
||||||
|
this.showRedirectOverlay.set(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showFullscreenUpdateOverlay() {
|
export function showFullscreenUpdateOverlay() {
|
||||||
|
|||||||
+18
-20
@@ -6,7 +6,7 @@
|
|||||||
<div class="header-text-group">
|
<div class="header-text-group">
|
||||||
<span class="app-name">{{ appName }}</span>
|
<span class="app-name">{{ appName }}</span>
|
||||||
<span class="version-badge" (click)="checkForAppUpdate($event)" style="cursor: pointer; display: inline-flex; align-items: center; gap: 4px;">
|
<span class="version-badge" (click)="checkForAppUpdate($event)" style="cursor: pointer; display: inline-flex; align-items: center; gap: 4px;">
|
||||||
v{{ version }}
|
v{{ version }}<span *ngIf="settingsService.userName()"> - {{ settingsService.userName() }}</span>
|
||||||
<span *ngIf="hasUpdateAvailable()" style="display: inline-flex; align-items: center; justify-content: center; padding: 4px; margin-left: 2px; background: rgba(var(--ion-color-secondary-rgb), 0.25); border-radius: 50%; border: 1px solid var(--ion-color-secondary); width: 24px; height: 24px; box-shadow: 0 0 8px rgba(var(--ion-color-secondary-rgb), 0.3);">
|
<span *ngIf="hasUpdateAvailable()" style="display: inline-flex; align-items: center; justify-content: center; padding: 4px; margin-left: 2px; background: rgba(var(--ion-color-secondary-rgb), 0.25); border-radius: 50%; border: 1px solid var(--ion-color-secondary); width: 24px; height: 24px; box-shadow: 0 0 8px rgba(var(--ion-color-secondary-rgb), 0.3);">
|
||||||
<ion-icon name="refresh-outline" style="font-size: 1.15rem; color: var(--ion-color-secondary); font-weight: bold;"></ion-icon>
|
<ion-icon name="refresh-outline" style="font-size: 1.15rem; color: var(--ion-color-secondary); font-weight: bold;"></ion-icon>
|
||||||
</span>
|
</span>
|
||||||
@@ -48,11 +48,10 @@
|
|||||||
<ion-button
|
<ion-button
|
||||||
[fill]="activeFilterType() === 'playlist' || playlistService.activeListName() !== null ? 'solid' : 'outline'"
|
[fill]="activeFilterType() === 'playlist' || playlistService.activeListName() !== null ? 'solid' : 'outline'"
|
||||||
size="small"
|
size="small"
|
||||||
(click)="playlistService.allPlaylists().length > 0 ? toggleFilterType('playlist') : playlistService.toggleSelectionMode()"
|
(click)="toggleFilterType('playlist')"
|
||||||
class="filter-chip">
|
class="filter-chip">
|
||||||
{{ playlistService.activeListName() !== null ? 'Playlist: ' + playlistService.activeListName() : 'Playlist' }}
|
{{ playlistService.activeListName() !== null ? 'Playlist: ' + playlistService.activeListName() : 'Playlist' }}
|
||||||
<ion-icon slot="end" name="chevron-down-outline" *ngIf="playlistService.activeListName() === null && playlistService.allPlaylists().length > 0"></ion-icon>
|
<ion-icon slot="end" name="chevron-down-outline" *ngIf="playlistService.activeListName() === null"></ion-icon>
|
||||||
<ion-icon slot="end" name="add-circle-outline" *ngIf="playlistService.activeListName() === null && playlistService.allPlaylists().length === 0"></ion-icon>
|
|
||||||
<span class="close-icon-wrapper" *ngIf="playlistService.activeListName() !== null" (click)="clearSpecialList($event)">
|
<span class="close-icon-wrapper" *ngIf="playlistService.activeListName() !== null" (click)="clearSpecialList($event)">
|
||||||
<ion-icon slot="end" name="close-circle"></ion-icon>
|
<ion-icon slot="end" name="close-circle"></ion-icon>
|
||||||
</span>
|
</span>
|
||||||
@@ -69,14 +68,6 @@
|
|||||||
<ion-icon slot="start" name="people-outline" style="font-size: 1.1rem; margin-right: 4px;"></ion-icon>
|
<ion-icon slot="start" name="people-outline" style="font-size: 1.1rem; margin-right: 4px;"></ion-icon>
|
||||||
{{ comunitaService.comunitaCode() ? comunitaService.comunitaNome() : 'Comunità' }}
|
{{ comunitaService.comunitaCode() ? comunitaService.comunitaNome() : 'Comunità' }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
<div
|
|
||||||
*ngIf="comunitaService.comunitaCode()"
|
|
||||||
(click)="editComunitaCode($event)"
|
|
||||||
style="position: absolute; top: -6px; right: -6px; z-index: 99999; background: var(--ion-color-secondary); color: white; border-radius: 50%; width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; border: 1.5px solid #1a1a1a; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"
|
|
||||||
class="floating-edit-badge">
|
|
||||||
<ion-icon name="create-outline" style="font-size: 0.85rem; pointer-events: none;"></ion-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ion-button
|
<ion-button
|
||||||
@@ -143,18 +134,18 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- Category Scroll (Dropdown style) -->
|
<!-- Category Scroll (Dropdown style) -->
|
||||||
<ion-toolbar class="bg-gradient momentos-toolbar" *ngIf="activeFilterType() || playlistService.selectionMode()">
|
<ion-toolbar class="bg-gradient momentos-toolbar" *ngIf="shouldShowSubSectionToolbar()">
|
||||||
<div class="momento-scroll">
|
<div class="momento-scroll" *ngIf="activeFilterType() !== 'playlist' || playlistService.allPlaylists().length > 0 || playlistService.selectionMode()">
|
||||||
<!-- Selection Mode Actions -->
|
<!-- Selection Mode Actions -->
|
||||||
<div class="selection-pill glass" *ngIf="playlistService.selectionMode()">
|
<div class="selection-pill glass" *ngIf="playlistService.selectionMode() && reorderList().length > 0">
|
||||||
<span class="selection-count">{{ playlistService.selectedIds().size }}</span>
|
<span class="selection-count">{{ reorderList().length }}</span>
|
||||||
<ion-button fill="clear" color="secondary" (click)="toggleAddingSongs()" class="mini-action-btn">
|
<ion-button fill="clear" color="secondary" (click)="toggleAddingSongs()" class="mini-action-btn" *ngIf="reorderList().length > 0">
|
||||||
<ion-icon slot="icon-only" [name]="isAddingSongs() ? 'list-outline' : 'add-circle-outline'"></ion-icon>
|
<ion-icon slot="icon-only" [name]="isAddingSongs() ? 'list-outline' : 'add-circle-outline'"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button fill="clear" color="secondary" (click)="finishSelection()" [disabled]="playlistService.selectedIds().size === 0" class="mini-action-btn">
|
<ion-button fill="clear" color="secondary" (click)="finishSelection()" [disabled]="reorderList().length === 0" class="mini-action-btn" *ngIf="reorderList().length > 0">
|
||||||
<ion-icon slot="icon-only" name="save-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="save-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button fill="clear" color="danger" (click)="cancelSelection()" class="mini-action-btn">
|
<ion-button fill="clear" color="danger" (click)="cancelSelection()" class="mini-action-btn" *ngIf="reorderList().length > 0">
|
||||||
<ion-icon slot="icon-only" name="close-circle-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="close-circle-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,7 +182,7 @@
|
|||||||
name="add-outline"
|
name="add-outline"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
routerLink="/propose-canto"
|
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">
|
||||||
</ion-icon>
|
</ion-icon>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -208,6 +199,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Instructions when playlist is empty -->
|
||||||
|
<div class="empty-playlist-container" *ngIf="activeFilterType() === 'playlist' && playlistService.allPlaylists().length === 0 && !playlistService.selectionMode()">
|
||||||
|
<p class="empty-playlist-instruction outfit-font">
|
||||||
|
Per creare una playlist, seleziona il nr dei canti che vuoi inserire nella playlist, riordinali e salvala con nome
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
|
|||||||
@@ -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 Layout
|
||||||
.custom-item {
|
.custom-item {
|
||||||
--padding-start: 16px;
|
--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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+19
-95
@@ -773,9 +773,15 @@ export class HomePage implements OnDestroy {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const remoteJson = await response.json();
|
const remoteJson = await response.json();
|
||||||
if (Array.isArray(remoteJson)) {
|
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
|
// Reconstruct custom songs
|
||||||
const customSongs = remoteJson
|
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) => ({
|
.map((item: any) => ({
|
||||||
id: `my_${item.id_canti}`,
|
id: `my_${item.id_canti}`,
|
||||||
id_canti: Number(item.id_canti),
|
id_canti: Number(item.id_canti),
|
||||||
@@ -1025,6 +1031,17 @@ export class HomePage implements OnDestroy {
|
|||||||
this.searchQuery() !== '';
|
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() {
|
clearAllFilters() {
|
||||||
this.selectedLiturgico.set(null);
|
this.selectedLiturgico.set(null);
|
||||||
this.selectedTematico.set(null);
|
this.selectedTematico.set(null);
|
||||||
@@ -1655,105 +1672,12 @@ export class HomePage implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleComunitaFilter() {
|
toggleComunitaFilter() {
|
||||||
if (!this.comunitaService.comunitaCode()) {
|
if (this.comunitaService.comunitaCode()) {
|
||||||
this.promptComunitaCode();
|
|
||||||
} else {
|
|
||||||
this.comunitaService.isFilterActive.update(v => !v);
|
this.comunitaService.isFilterActive.update(v => !v);
|
||||||
}
|
}
|
||||||
this.activeFilterType.set(null);
|
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 massCardStartX: number = 0;
|
||||||
private massCardStartY: number = 0;
|
private massCardStartY: number = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
private initialPinchDistance: number | null = null;
|
private initialPinchDistance: number | null = null;
|
||||||
private initialFontSize: number = 1.0;
|
private initialFontSize: number = 1.0;
|
||||||
|
public portraitFontSize: number = 1.0;
|
||||||
|
|
||||||
private lastMatchedTranscript: string = '';
|
private lastMatchedTranscript: string = '';
|
||||||
|
|
||||||
@@ -267,8 +268,10 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.autoscrollSpeed.set(playlistSongSetting.speed !== undefined ? playlistSongSetting.speed : 2);
|
this.autoscrollSpeed.set(playlistSongSetting.speed !== undefined ? playlistSongSetting.speed : 2);
|
||||||
if (playlistSongSetting.zoom !== undefined) {
|
if (playlistSongSetting.zoom !== undefined) {
|
||||||
this.fontSize.set(playlistSongSetting.zoom);
|
this.fontSize.set(playlistSongSetting.zoom);
|
||||||
|
this.portraitFontSize = playlistSongSetting.zoom;
|
||||||
} else {
|
} else {
|
||||||
this.fontSize.set(1.0);
|
this.fontSize.set(1.0);
|
||||||
|
this.portraitFontSize = 1.0;
|
||||||
}
|
}
|
||||||
} else if (this.comunitaService.comunitaCode() && this.comunitaService.isFilterActive()) {
|
} else if (this.comunitaService.comunitaCode() && this.comunitaService.isFilterActive()) {
|
||||||
const settings = this.comunitaService.comunitaCantiSettings();
|
const settings = this.comunitaService.comunitaCantiSettings();
|
||||||
@@ -289,9 +292,13 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.transposeAmount.set(0);
|
this.transposeAmount.set(0);
|
||||||
this.autoscrollSpeed.set(2);
|
this.autoscrollSpeed.set(2);
|
||||||
}
|
}
|
||||||
|
this.fontSize.set(1.0);
|
||||||
|
this.portraitFontSize = 1.0;
|
||||||
} else {
|
} else {
|
||||||
this.transposeAmount.set(0);
|
this.transposeAmount.set(0);
|
||||||
this.autoscrollSpeed.set(2);
|
this.autoscrollSpeed.set(2);
|
||||||
|
this.fontSize.set(1.0);
|
||||||
|
this.portraitFontSize = 1.0;
|
||||||
}
|
}
|
||||||
}, { allowSignalWrites: true });
|
}, { allowSignalWrites: true });
|
||||||
|
|
||||||
@@ -320,6 +327,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private checkAndLimitFontSize(targetFont: number): number {
|
private checkAndLimitFontSize(targetFont: number): number {
|
||||||
|
if (!this.isLandscape()) {
|
||||||
|
return targetFont;
|
||||||
|
}
|
||||||
const titleMain = this.el.nativeElement.querySelector('.title-main');
|
const titleMain = this.el.nativeElement.querySelector('.title-main');
|
||||||
if (!titleMain) return targetFont;
|
if (!titleMain) return targetFont;
|
||||||
|
|
||||||
@@ -388,9 +398,8 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}, 150);
|
}, 150);
|
||||||
} else {
|
} else {
|
||||||
const limitedFont = this.checkAndLimitFontSize(1.0);
|
this.fontSize.set(this.portraitFontSize);
|
||||||
this.fontSize.set(limitedFont);
|
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.portraitFontSize });
|
||||||
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: limitedFont });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,6 +527,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
if (Math.abs(limited - this.fontSize()) > 0.01) {
|
if (Math.abs(limited - this.fontSize()) > 0.01) {
|
||||||
this.fontSize.set(limited);
|
this.fontSize.set(limited);
|
||||||
|
if (!this.isLandscape()) {
|
||||||
|
this.portraitFontSize = limited;
|
||||||
|
}
|
||||||
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -582,6 +594,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
if (limited !== this.fontSize()) {
|
if (limited !== this.fontSize()) {
|
||||||
this.fontSize.set(limited);
|
this.fontSize.set(limited);
|
||||||
|
if (!this.isLandscape()) {
|
||||||
|
this.portraitFontSize = limited;
|
||||||
|
}
|
||||||
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
||||||
this.updatePlaylistSettings();
|
this.updatePlaylistSettings();
|
||||||
}
|
}
|
||||||
@@ -593,6 +608,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (this.fontSize() > minZ) {
|
if (this.fontSize() > minZ) {
|
||||||
const target = Math.max(this.fontSize() - this.FONT_STEP, minZ);
|
const target = Math.max(this.fontSize() - this.FONT_STEP, minZ);
|
||||||
this.fontSize.set(target);
|
this.fontSize.set(target);
|
||||||
|
if (!this.isLandscape()) {
|
||||||
|
this.portraitFontSize = target;
|
||||||
|
}
|
||||||
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
||||||
this.updatePlaylistSettings();
|
this.updatePlaylistSettings();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,14 +79,7 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-toggle slot="end" [checked]="settingsService.autoAdvance()" (ionChange)="settingsService.toggleAutoAdvance()" color="secondary"></ion-toggle>
|
<ion-toggle slot="end" [checked]="settingsService.autoAdvance()" (ionChange)="settingsService.toggleAutoAdvance()" color="secondary"></ion-toggle>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="transparent-item" lines="none">
|
|
||||||
<ion-icon name="sunny-outline" slot="start" color="secondary"></ion-icon>
|
|
||||||
<ion-label class="outfit-font">
|
|
||||||
<h2 class="settings-item-title">Schermo Sempre Acceso</h2>
|
|
||||||
<p class="settings-item-subtitle">Evita che lo schermo si spenga</p>
|
|
||||||
</ion-label>
|
|
||||||
<ion-toggle slot="end" [checked]="settingsService.keepScreenOn()" (ionChange)="settingsService.toggleKeepScreenOn()" color="secondary"></ion-toggle>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item class="transparent-item" lines="none">
|
<ion-item class="transparent-item" lines="none">
|
||||||
<ion-icon name="create-outline" slot="start" color="secondary"></ion-icon>
|
<ion-icon name="create-outline" slot="start" color="secondary"></ion-icon>
|
||||||
<ion-label class="outfit-font">
|
<ion-label class="outfit-font">
|
||||||
@@ -145,12 +138,50 @@
|
|||||||
<h2 class="settings-item-title">Comunità</h2>
|
<h2 class="settings-item-title">Comunità</h2>
|
||||||
<p class="settings-item-subtitle">Mostra il filtro Comunità nella home</p>
|
<p class="settings-item-subtitle">Mostra il filtro Comunità nella home</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-toggle slot="end" [checked]="settingsService.comunitaEnabled()" (ionChange)="settingsService.toggleComunitaEnabled()" color="secondary"></ion-toggle>
|
<ion-toggle slot="end" [checked]="settingsService.comunitaEnabled()" (ionChange)="onComunitaToggleChange($event)" color="secondary"></ion-toggle>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Community Details/Form when enabled -->
|
||||||
|
<div class="ion-padding-horizontal ion-padding-bottom" *ngIf="settingsService.comunitaEnabled()" style="border-top: 1px solid rgba(255,255,255,0.06); padding-top: 16px;">
|
||||||
|
<div *ngIf="comunitaService.comunitaCode()" class="glass" style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); padding: 12px; border-radius: 12px; display: flex; align-items: center; justify-content: space-between;">
|
||||||
|
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-right: 12px; flex-grow: 1;">
|
||||||
|
<div style="font-size: 0.72rem; text-transform: uppercase; color: var(--ion-color-secondary); font-weight: bold; letter-spacing: 0.05em; margin-bottom: 2px;">Comunità Attiva</div>
|
||||||
|
<div style="font-size: 0.9rem; font-weight: 500; color: var(--ion-text-color); overflow: hidden; text-overflow: ellipsis;">{{ comunitaService.comunitaNome() }}</div>
|
||||||
|
<div style="font-size: 0.75rem; opacity: 0.7; color: var(--ion-text-color); margin-top: 2px;">Codice: {{ comunitaService.comunitaCode() }}</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; flex-shrink: 0;">
|
||||||
|
<ion-button fill="clear" size="small" color="secondary" (click)="editComunita()" style="margin: 0; --padding-start: 4px; --padding-end: 4px;">
|
||||||
|
<ion-icon name="create-outline" slot="icon-only" style="font-size: 1.25rem;"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
<ion-button fill="clear" size="small" color="danger" (click)="removeComunita()" style="margin: 0; --padding-start: 4px; --padding-end: 4px;">
|
||||||
|
<ion-icon name="trash-outline" slot="icon-only" style="font-size: 1.25rem;"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!comunitaService.comunitaCode()" style="display: flex; flex-direction: column; gap: 12px;">
|
||||||
|
<p class="settings-item-subtitle outfit-font" style="margin: 0; font-size: 0.8rem; opacity: 0.85;">
|
||||||
|
Inserisci il codice parrocchiale/comunità per scaricare i canti e le scalette dedicate:
|
||||||
|
</p>
|
||||||
|
<div style="display: flex; gap: 10px; align-items: center;">
|
||||||
|
<div class="glass" style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); padding: 8px 12px; border-radius: 10px; flex-grow: 1; display: flex; align-items: center;">
|
||||||
|
<input type="text"
|
||||||
|
#comunitaCodeInput
|
||||||
|
placeholder="Es: 123456"
|
||||||
|
style="background: transparent; border: none; color: var(--ion-text-color); font-size: 0.85rem; width: 100%; outline: none;"
|
||||||
|
class="outfit-font"
|
||||||
|
(keyup.enter)="saveComunitaCode(comunitaCodeInput.value)">
|
||||||
|
</div>
|
||||||
|
<ion-button fill="solid" color="secondary" size="small" (click)="saveComunitaCode(comunitaCodeInput.value)" class="outfit-font" style="--border-radius: 10px; font-weight: bold; text-transform: none; margin: 0; height: 36px;">
|
||||||
|
Attiva
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Identità Utente e Ripristino -->
|
<!-- Identità Utente e Ripristino -->
|
||||||
<div class="settings-group glass ion-margin-bottom" *ngIf="settingsService.comunitaEnabled() || settingsService.showEditor()">
|
<div class="settings-group glass ion-margin-bottom">
|
||||||
<div class="group-header ion-padding-start ion-padding-top">
|
<div class="group-header ion-padding-start ion-padding-top">
|
||||||
<h2 class="outfit-font settings-group-title" style="color: var(--ion-color-secondary); font-weight: bold; font-size: 1.05rem;">
|
<h2 class="outfit-font settings-group-title" style="color: var(--ion-color-secondary); font-weight: bold; font-size: 1.05rem;">
|
||||||
Identità Utente e Ripristino
|
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.
|
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.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- Nome Associato all'Identità -->
|
||||||
|
<div class="glass" style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); padding: 12px; border-radius: 12px; margin-bottom: 12px;">
|
||||||
|
<div style="font-size: 0.72rem; text-transform: uppercase; color: var(--ion-color-secondary); font-weight: bold; letter-spacing: 0.05em; margin-bottom: 4px;">Nome</div>
|
||||||
|
<input type="text"
|
||||||
|
[value]="settingsService.userName()"
|
||||||
|
(input)="onNameChange($event)"
|
||||||
|
placeholder="Inserisci il tuo nome..."
|
||||||
|
style="background: transparent; border: none; color: var(--ion-text-color); font-size: 0.85rem; width: 100%; outline: none; padding: 4px 0;"
|
||||||
|
class="outfit-font">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="glass" style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); padding: 12px; border-radius: 12px; display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;">
|
<div class="glass" style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); padding: 12px; border-radius: 12px; display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;">
|
||||||
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-right: 12px; flex-grow: 1;">
|
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-right: 12px; flex-grow: 1;">
|
||||||
<div style="font-size: 0.72rem; text-transform: uppercase; color: var(--ion-color-secondary); font-weight: bold; letter-spacing: 0.05em; margin-bottom: 2px;">Codice ID</div>
|
<div style="font-size: 0.72rem; text-transform: uppercase; color: var(--ion-color-secondary); font-weight: bold; letter-spacing: 0.05em; margin-bottom: 2px;">Codice ID</div>
|
||||||
@@ -183,10 +225,21 @@
|
|||||||
</ion-button>
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ion-button *ngIf="settingsService.userUuid() !== settingsService.originalUserUuid()"
|
||||||
|
expand="block" fill="outline" color="warning" (click)="restoreOriginalIdentity()" class="outfit-font ion-margin-top" style="--border-radius: 10px; font-size: 0.85rem; font-weight: bold; text-transform: none; margin-left: 0; margin-right: 0; margin-bottom: 0; height: 42px;">
|
||||||
|
<ion-icon name="refresh-outline" slot="start"></ion-icon>
|
||||||
|
Ripristina ID Originario
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
<ion-button expand="block" fill="solid" color="secondary" (click)="syncLocalDataToServer()" class="outfit-font ion-margin-top" style="--border-radius: 10px; font-size: 0.85rem; font-weight: bold; text-transform: none; margin-left: 0; margin-right: 0; margin-bottom: 0; height: 42px;">
|
<ion-button expand="block" fill="solid" color="secondary" (click)="syncLocalDataToServer()" class="outfit-font ion-margin-top" style="--border-radius: 10px; font-size: 0.85rem; font-weight: bold; text-transform: none; margin-left: 0; margin-right: 0; margin-bottom: 0; height: 42px;">
|
||||||
<ion-icon name="cloud-upload-outline" slot="start"></ion-icon>
|
<ion-icon name="cloud-upload-outline" slot="start"></ion-icon>
|
||||||
Salva Backup sul Server
|
Salva Backup sul Server
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
|
<ion-button expand="block" fill="outline" color="secondary" (click)="restoreBackupFromServer()" class="outfit-font ion-margin-top" style="--border-radius: 10px; font-size: 0.85rem; font-weight: bold; text-transform: none; margin-left: 0; margin-right: 0; margin-bottom: 0; height: 42px;">
|
||||||
|
<ion-icon name="cloud-download-outline" slot="start"></ion-icon>
|
||||||
|
Ripristina Backup dal Server
|
||||||
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -254,44 +307,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="settings-group glass ion-margin-top" *ngIf="settingsService.showEditor()">
|
|
||||||
<ion-item class="transparent-item" lines="none" (click)="myCantiService.sendAllMyCanti()" detail="true" button *ngIf="myCantiService.myCanti().length > 0">
|
|
||||||
<ion-icon name="send-outline" slot="start" color="secondary"></ion-icon>
|
|
||||||
<ion-label class="outfit-font">
|
|
||||||
<h2 class="settings-item-title">Proponi i miei canti ({{ myCantiService.myCanti().length }})</h2>
|
|
||||||
<p class="settings-item-subtitle">Invia a {{ contactEmail }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-group glass ion-margin-top">
|
|
||||||
<ion-item class="transparent-item" lines="none" (click)="fullRefresh()" detail="true" button>
|
|
||||||
<ion-icon name="cloud-download-outline" slot="start" color="secondary"></ion-icon>
|
|
||||||
<ion-label class="outfit-font">
|
|
||||||
<h2 class="settings-item-title">Allinea con Server</h2>
|
|
||||||
<p class="settings-item-subtitle">Aggiorna canti e versione app</p>
|
|
||||||
</ion-label>
|
|
||||||
<ion-spinner slot="end" name="crescent" color="secondary" *ngIf="cantiService.loading()"></ion-spinner>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ion-item class="transparent-item" lines="none" (click)="checkForAppUpdate()" detail="true" button style="border-top: 1px solid rgba(255,255,255,0.06);">
|
|
||||||
<ion-icon name="sync-outline" slot="start" color="secondary"></ion-icon>
|
|
||||||
<ion-label class="outfit-font">
|
|
||||||
<h2 class="settings-item-title">Verifica Aggiornamenti App</h2>
|
|
||||||
<p class="settings-item-subtitle">Forza la ricerca di una nuova versione dell'applicazione</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<div class="sync-info ion-padding-bottom">
|
|
||||||
<p class="outfit-font settings-item-subtitle">
|
|
||||||
Versione: <strong>v{{ version }}</strong> •
|
|
||||||
Canti: <strong>{{ cantiService.canti().length }}</strong>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="sync-progress" *ngIf="cantiService.loading()">
|
|
||||||
<div class="progress-bar" [style.width.%]="cantiService.progress()"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Legenda Pulsanti Canto -->
|
<!-- Legenda Pulsanti Canto -->
|
||||||
<div class="settings-group glass ion-margin-top">
|
<div class="settings-group glass ion-margin-top">
|
||||||
|
|||||||
@@ -142,9 +142,15 @@ export class SettingsPage {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const remoteJson = await response.json();
|
const remoteJson = await response.json();
|
||||||
if (Array.isArray(remoteJson)) {
|
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
|
// Reconstruct custom songs
|
||||||
const customSongs = remoteJson
|
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) => ({
|
.map((item: any) => ({
|
||||||
id: `my_${item.id_canti}`,
|
id: `my_${item.id_canti}`,
|
||||||
id_canti: Number(item.id_canti),
|
id_canti: Number(item.id_canti),
|
||||||
@@ -215,6 +221,37 @@ export class SettingsPage {
|
|||||||
await alert.present();
|
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() {
|
async syncLocalDataToServer() {
|
||||||
const uid = this.settingsService.userUuid();
|
const uid = this.settingsService.userUuid();
|
||||||
if (!uid) {
|
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) {
|
onModeChange(event: any) {
|
||||||
this.settingsService.setShowChordsDefault(event.detail.value === 'chords');
|
this.settingsService.setShowChordsDefault(event.detail.value === 'chords');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onNameChange(event: any) {
|
||||||
|
this.settingsService.setUserName(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
onMassChange(event: any) {
|
onMassChange(event: any) {
|
||||||
this.cantiLettureService.setSelectedMass(event.detail.value);
|
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)
|
async onComunitaToggleChange(event: any) {
|
||||||
const updateAvailable = await this.performUpdateCheck();
|
const checked = event.detail.checked;
|
||||||
if (updateAvailable) {
|
if (checked) {
|
||||||
return; // updateAvailable already triggered the update/reload flow
|
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() {
|
async editComunita() {
|
||||||
if (!this.swUpdate.isEnabled) {
|
await this.comunitaService.setComunitaCode('');
|
||||||
const toast = await this.toastCtrl.create({
|
}
|
||||||
message: 'Aggiornamenti non supportati su questo browser.',
|
|
||||||
duration: 3000,
|
|
||||||
color: 'medium'
|
|
||||||
});
|
|
||||||
await toast.present();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toastLoading = await this.toastCtrl.create({
|
async saveComunitaCode(code: string) {
|
||||||
message: 'Ricerca aggiornamenti in corso...',
|
const trimmed = (code || '').trim();
|
||||||
duration: 1500,
|
if (!trimmed) return;
|
||||||
color: 'secondary'
|
|
||||||
|
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({
|
const toast = await this.toastCtrl.create({
|
||||||
message: 'L\'applicazione è già aggiornata all\'ultima versione.',
|
message: `Comunità attivata: ${this.comunitaService.comunitaNome()}`,
|
||||||
duration: 3000,
|
duration: 2000,
|
||||||
color: 'success'
|
color: 'success'
|
||||||
});
|
});
|
||||||
await toast.present();
|
await toast.present();
|
||||||
}
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<boolean> {
|
|
||||||
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<void> | undefined = undefined;
|
|
||||||
|
|
||||||
if (this.swUpdate.isEnabled) {
|
|
||||||
readyPromise = new Promise<void>((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);
|
|
||||||
const toast = await this.toastCtrl.create({
|
const toast = await this.toastCtrl.create({
|
||||||
message: 'Errore durante la ricerca di aggiornamenti.',
|
message: 'Codice non trovato o errore di connessione.',
|
||||||
duration: 3000,
|
duration: 2000,
|
||||||
color: 'danger'
|
color: 'danger'
|
||||||
});
|
});
|
||||||
await toast.present();
|
await toast.present();
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async applyUpdateAndReload(readyPromise?: Promise<void>, subscription?: any) {
|
async removeComunita() {
|
||||||
const overlay = showFullscreenUpdateOverlay();
|
await this.comunitaService.setComunitaCode('');
|
||||||
|
this.settingsService.comunitaEnabled.set(false);
|
||||||
let activated = false;
|
localStorage.setItem('comunita-enabled', 'false');
|
||||||
const activateAndReload = async () => {
|
const toast = await this.toastCtrl.create({
|
||||||
if (activated) return;
|
message: 'Comunità disattivata.',
|
||||||
activated = true;
|
duration: 2000,
|
||||||
try {
|
color: 'secondary'
|
||||||
if (this.swUpdate.isEnabled) {
|
});
|
||||||
await this.swUpdate.activateUpdate();
|
await toast.present();
|
||||||
}
|
|
||||||
} 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<boolean> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,9 +60,6 @@ export class CantiService {
|
|||||||
await this.loadFromStorage();
|
await this.loadFromStorage();
|
||||||
if (this.canti() && this.canti().length > 0) {
|
if (this.canti() && this.canti().length > 0) {
|
||||||
this.firstLoadCompleted.set(true);
|
this.firstLoadCompleted.set(true);
|
||||||
if (this.settingsService.isVersionCheckComplete() && (window as any).PwaLoader) {
|
|
||||||
(window as any).PwaLoader.hide();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// First boot or data cleared: show setup loader immediately
|
// First boot or data cleared: show setup loader immediately
|
||||||
if ((window as any).PwaLoader) {
|
if ((window as any).PwaLoader) {
|
||||||
@@ -159,19 +156,12 @@ export class CantiService {
|
|||||||
this.progress.set(100);
|
this.progress.set(100);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
this.firstLoadCompleted.set(true);
|
this.firstLoadCompleted.set(true);
|
||||||
|
|
||||||
if (this.settingsService.isVersionCheckComplete() && (window as any).PwaLoader) {
|
|
||||||
(window as any).PwaLoader.hide();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Failed to fetch canti', error);
|
console.error('Failed to fetch canti', error);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
this.firstLoadCompleted.set(true);
|
this.firstLoadCompleted.set(true);
|
||||||
if ((window as any).PwaLoader) {
|
|
||||||
(window as any).PwaLoader.hide();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -363,6 +363,16 @@ export class PlaylistService {
|
|||||||
|
|
||||||
const payload = [...customSongs, ...playlistSongs];
|
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', {
|
const response = await fetch('https://api.canticristiani.it/miei', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ export class SettingsService {
|
|||||||
/** Identificativo utente univoco per la gestione delle comunità */
|
/** Identificativo utente univoco per la gestione delle comunità */
|
||||||
public userUuid = signal<string>('');
|
public userUuid = signal<string>('');
|
||||||
|
|
||||||
|
/** Identificativo originario assegnato alla prima installazione */
|
||||||
|
public originalUserUuid = signal<string>('');
|
||||||
|
|
||||||
|
/** Nome associato all'identità utente */
|
||||||
|
public userName = signal<string>('');
|
||||||
|
|
||||||
private wakeLock: any = null;
|
private wakeLock: any = null;
|
||||||
|
|
||||||
// PWA installation signals
|
// PWA installation signals
|
||||||
@@ -83,6 +89,19 @@ export class SettingsService {
|
|||||||
}
|
}
|
||||||
this.userUuid.set(savedUuid);
|
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
|
// Detect PWA status
|
||||||
try {
|
try {
|
||||||
this.isStandalone.set(
|
this.isStandalone.set(
|
||||||
@@ -166,12 +185,8 @@ export class SettingsService {
|
|||||||
this.autoAdvance.set(true);
|
this.autoAdvance.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedKeepScreenOn = localStorage.getItem('keep-screen-on');
|
// Keep screen always on by default and always active
|
||||||
if (savedKeepScreenOn !== null) {
|
this.keepScreenOn.set(true);
|
||||||
this.keepScreenOn.set(savedKeepScreenOn === 'true');
|
|
||||||
} else {
|
|
||||||
this.keepScreenOn.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedComunitaEnabled = localStorage.getItem('comunita-enabled');
|
const savedComunitaEnabled = localStorage.getItem('comunita-enabled');
|
||||||
if (savedComunitaEnabled !== null) {
|
if (savedComunitaEnabled !== null) {
|
||||||
@@ -318,9 +333,6 @@ export class SettingsService {
|
|||||||
localStorage.setItem('show-editor', newValue.toString());
|
localStorage.setItem('show-editor', newValue.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleKeepScreenOn() {
|
|
||||||
this.keepScreenOn.update(v => !v);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleComunitaEnabled() {
|
toggleComunitaEnabled() {
|
||||||
const newValue = !this.comunitaEnabled();
|
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() {
|
async installPwa() {
|
||||||
const promptEvent = this.deferredPrompt();
|
const promptEvent = this.deferredPrompt();
|
||||||
if (!promptEvent) {
|
if (!promptEvent) {
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
export const VERSION = '2026.06.14.1936';
|
export const VERSION = '2026.06.15.0021';
|
||||||
|
|||||||
Reference in New Issue
Block a user