Ottimizzazione UI player, rotazione head tilt invertita, prefisso playlist in Home e implementazione Identity QR modal e interceptor API
This commit is contained in:
@@ -2,3 +2,5 @@ FTP_PASSWORD=cantiDavid@72
|
|||||||
FTP_THREADS=30
|
FTP_THREADS=30
|
||||||
BASE_HREF=/ionic/
|
BASE_HREF=/ionic/
|
||||||
CONTACT_EMAIL=info@canticristiani.it
|
CONTACT_EMAIL=info@canticristiani.it
|
||||||
|
API_AUTH_USER=canti
|
||||||
|
API_AUTH_PASS=antani2026
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ fi
|
|||||||
FTP_HOST="ftp.canticristiani.it"
|
FTP_HOST="ftp.canticristiani.it"
|
||||||
FTP_USER="canticristiani.it"
|
FTP_USER="canticristiani.it"
|
||||||
FTP_PASS="$FTP_PASSWORD"
|
FTP_PASS="$FTP_PASSWORD"
|
||||||
SOURCE_URL="http://185.193.67.105:3000/cantiletture.json"
|
SOURCE_URL="https://api.canticristiani.it/cantiletture.json"
|
||||||
REMOTE_PATH="api/cantiletture.json"
|
REMOTE_PATH="api/cantiletture.json"
|
||||||
|
|
||||||
if [ -z "$FTP_PASS" ]; then
|
if [ -z "$FTP_PASS" ]; then
|
||||||
@@ -23,7 +23,7 @@ fi
|
|||||||
# --- Download del file ---
|
# --- Download del file ---
|
||||||
echo "⬇️ Scaricamento dati da $SOURCE_URL..."
|
echo "⬇️ Scaricamento dati da $SOURCE_URL..."
|
||||||
TEMP_FILE=$(mktemp)
|
TEMP_FILE=$(mktemp)
|
||||||
if ! curl -s -L "$SOURCE_URL" -o "$TEMP_FILE"; then
|
if ! curl -s -u "$API_AUTH_USER:$API_AUTH_PASS" -L "$SOURCE_URL" -o "$TEMP_FILE"; then
|
||||||
echo "❌ Errore nel download dei dati."
|
echo "❌ Errore nel download dei dati."
|
||||||
rm "$TEMP_FILE"
|
rm "$TEMP_FILE"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -17,6 +17,28 @@ VERSION=$(date +'%Y.%m.%d.%H%M')
|
|||||||
echo "export const VERSION = '$VERSION';" > src/app/version.ts
|
echo "export const VERSION = '$VERSION';" > src/app/version.ts
|
||||||
echo "🏷️ Versione aggiornata a: $VERSION"
|
echo "🏷️ Versione aggiornata a: $VERSION"
|
||||||
|
|
||||||
|
# --- Caricamento variabili d'ambiente ---
|
||||||
|
if [ -f .env ]; then
|
||||||
|
export $(grep -v '^#' .env | xargs)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Configurazione Email e API Parametriche ---
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const envEmail = process.env.CONTACT_EMAIL || 'info@canticristiani.it';
|
||||||
|
const apiUser = process.env.API_AUTH_USER || 'canti';
|
||||||
|
const apiPass = process.env.API_AUTH_PASS || 'antani2026';
|
||||||
|
['src/environments/environment.ts', 'src/environments/environment.prod.ts'].forEach(file => {
|
||||||
|
if (fs.existsSync(file)) {
|
||||||
|
let content = fs.readFileSync(file, 'utf8');
|
||||||
|
content = content.replace(/contactEmail:\s*'[^']*'/g, \`contactEmail: '\${envEmail}'\`);
|
||||||
|
content = content.replace(/apiAuthUser:\s*'[^']*'/g, \`apiAuthUser: '\${apiUser}'\`);
|
||||||
|
content = content.replace(/apiAuthPass:\s*'[^']*'/g, \`apiAuthPass: '\${apiPass}'\`);
|
||||||
|
fs.writeFileSync(file, content, 'utf8');
|
||||||
|
console.log(\`📧 Aggiornate variabili di ambiente in \${file}\`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
"
|
||||||
|
|
||||||
# 2. Build dell'applicazione
|
# 2. Build dell'applicazione
|
||||||
echo "📦 Compilazione in corso (Production Build)..."
|
echo "📦 Compilazione in corso (Production Build)..."
|
||||||
|
|||||||
+8
-3
@@ -9,7 +9,8 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Configurazione Parallelismo ---
|
# --- Configurazione Parallelismo ---
|
||||||
THREADS=${1:-${FTP_THREADS:-30}}
|
THREADS=${1:-${FTP_THREADS:-100}}
|
||||||
|
export FTP_THREADS=$THREADS
|
||||||
|
|
||||||
# --- Configurazione FTP per ROOT www.canticristiani.it ---
|
# --- Configurazione FTP per ROOT www.canticristiani.it ---
|
||||||
FTP_HOST="ftp.canticristiani.it"
|
FTP_HOST="ftp.canticristiani.it"
|
||||||
@@ -27,16 +28,20 @@ VERSION=$(date +'%Y.%m.%d.%H%M')
|
|||||||
echo "export const VERSION = '$VERSION';" > src/app/version.ts
|
echo "export const VERSION = '$VERSION';" > src/app/version.ts
|
||||||
echo "🏷️ Versione aggiornata a: $VERSION"
|
echo "🏷️ Versione aggiornata a: $VERSION"
|
||||||
|
|
||||||
# --- Configurazione Email Parametrica ---
|
# --- Configurazione Email e API Parametriche ---
|
||||||
node -e "
|
node -e "
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const envEmail = process.env.CONTACT_EMAIL || 'info@canticristiani.it';
|
const envEmail = process.env.CONTACT_EMAIL || 'info@canticristiani.it';
|
||||||
|
const apiUser = process.env.API_AUTH_USER || 'canti';
|
||||||
|
const apiPass = process.env.API_AUTH_PASS || 'antani2026';
|
||||||
['src/environments/environment.ts', 'src/environments/environment.prod.ts'].forEach(file => {
|
['src/environments/environment.ts', 'src/environments/environment.prod.ts'].forEach(file => {
|
||||||
if (fs.existsSync(file)) {
|
if (fs.existsSync(file)) {
|
||||||
let content = fs.readFileSync(file, 'utf8');
|
let content = fs.readFileSync(file, 'utf8');
|
||||||
content = content.replace(/contactEmail:\s*'[^']*'/g, \`contactEmail: '\${envEmail}'\`);
|
content = content.replace(/contactEmail:\s*'[^']*'/g, \`contactEmail: '\${envEmail}'\`);
|
||||||
|
content = content.replace(/apiAuthUser:\s*'[^']*'/g, \`apiAuthUser: '\${apiUser}'\`);
|
||||||
|
content = content.replace(/apiAuthPass:\s*'[^']*'/g, \`apiAuthPass: '\${apiPass}'\`);
|
||||||
fs.writeFileSync(file, content, 'utf8');
|
fs.writeFileSync(file, content, 'utf8');
|
||||||
console.log(\`📧 Aggiornata email di contatto in \${file} a: \${envEmail}\`);
|
console.log(\`📧 Aggiornate variabili di ambiente in \${file}\`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"theme_color": "#3880ff",
|
"theme_color": "#3880ff",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
|
"protocol_handlers": [
|
||||||
|
{
|
||||||
|
"protocol": "web+canti",
|
||||||
|
"url": "/?url=%s"
|
||||||
|
}
|
||||||
|
],
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "assets/icons/icon-72x72.png",
|
"src": "assets/icons/icon-72x72.png",
|
||||||
|
|||||||
@@ -1,4 +1,22 @@
|
|||||||
<ion-app>
|
<ion-app>
|
||||||
<ion-router-outlet></ion-router-outlet>
|
<ion-router-outlet></ion-router-outlet>
|
||||||
<div id="global-yt-player-container" style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; top: -100px;"></div>
|
<div id="global-yt-player-container" style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; top: -100px;"></div>
|
||||||
|
|
||||||
|
<!-- Unified Setup Loader Overlay -->
|
||||||
|
<div *ngIf="!cantiService.firstLoadCompleted()"
|
||||||
|
style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: #121212; color: #ffffff; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 99999; font-family: 'Outfit', sans-serif;">
|
||||||
|
<div style="text-align: center; padding: 20px; max-width: 400px; width: 100%;">
|
||||||
|
<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.3); border: 2px solid rgba(230, 126, 34, 0.2);">
|
||||||
|
</div>
|
||||||
|
<h2 style="font-size: 1.8rem; font-weight: 600; margin-bottom: 5px; color: #ffffff; -webkit-font-smoothing: antialiased;">Setup in corso</h2>
|
||||||
|
<p style="font-size: 1rem; color: rgba(255, 255, 255, 0.6); margin-bottom: 15px; -webkit-font-smoothing: antialiased;">Configurazione iniziale e caricamento canti...</p>
|
||||||
|
<div style="font-size: 0.85rem; color: rgba(255, 255, 255, 0.45); margin-bottom: 20px; -webkit-font-smoothing: antialiased; font-weight: 500;">Versione {{ version }}</div>
|
||||||
|
<div style="font-size: 0.95rem; font-weight: 600; color: #fdcb6e; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 1.5px;">Fase: Setup</div>
|
||||||
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 10px; height: 8px; width: 100%; overflow: hidden; margin-bottom: 15px;">
|
||||||
|
<div [style.width.%]="cantiService.progress()" style="background: #e67e22; height: 100%; transition: width 0.15s ease-out; border-radius: 10px;"></div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 1.2rem; font-weight: 700; color: #e67e22; -webkit-font-smoothing: antialiased;">{{ cantiService.progress() }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ion-app>
|
</ion-app>
|
||||||
|
|||||||
+118
-4
@@ -1,5 +1,9 @@
|
|||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
import { ThemeService } from './services/theme.service';
|
import { ThemeService } from './services/theme.service';
|
||||||
|
import { CantiService } from './services/canti.service';
|
||||||
|
import { VERSION } from './version';
|
||||||
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
|
import { ToastController } from '@ionic/angular';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -7,14 +11,100 @@ import { ThemeService } from './services/theme.service';
|
|||||||
styleUrls: ['app.component.scss'],
|
styleUrls: ['app.component.scss'],
|
||||||
standalone: false,
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent implements OnInit {
|
||||||
private themeService = inject(ThemeService); // Ensures theme is initialized at boot
|
private themeService = inject(ThemeService); // Ensures theme is initialized at boot
|
||||||
|
public cantiService = inject(CantiService);
|
||||||
|
public version = VERSION;
|
||||||
|
private router = inject(Router);
|
||||||
|
private route = inject(ActivatedRoute);
|
||||||
|
private toastCtrl = inject(ToastController);
|
||||||
|
|
||||||
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.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.route.queryParams.subscribe(params => {
|
||||||
|
const protocolUrl = params['url'];
|
||||||
|
if (protocolUrl && protocolUrl.startsWith('web+canti:')) {
|
||||||
|
try {
|
||||||
|
const cleanUrl = protocolUrl.replace('web+canti://', 'http://localhost/');
|
||||||
|
const urlObj = new URL(cleanUrl);
|
||||||
|
|
||||||
|
let targetPath = urlObj.pathname;
|
||||||
|
if (targetPath === '/open' || targetPath === '//open') {
|
||||||
|
targetPath = '/';
|
||||||
|
} else if (targetPath.startsWith('/open/')) {
|
||||||
|
targetPath = targetPath.substring(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams: any = {};
|
||||||
|
urlObj.searchParams.forEach((value, key) => {
|
||||||
|
queryParams[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.router.navigate([targetPath], { queryParams, replaceUrl: true });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse protocol url:', protocolUrl, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.checkAndRedirectToPwa();
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAndRedirectToPwa() {
|
||||||
|
const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (navigator as any).standalone;
|
||||||
|
if (isStandalone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have sharing/navigation query parameters
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const hasSharing = urlParams.has('import') || urlParams.has('playlist-uid') || urlParams.has('restore-uid') || urlParams.has('id');
|
||||||
|
|
||||||
|
if (hasSharing) {
|
||||||
|
let isInstalled = false;
|
||||||
|
if ('getInstalledRelatedApps' in navigator) {
|
||||||
|
try {
|
||||||
|
const relatedApps = await (navigator as any).getInstalledRelatedApps();
|
||||||
|
isInstalled = relatedApps.length > 0;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to check installed apps:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = window.location.search;
|
||||||
|
const path = window.location.pathname;
|
||||||
|
const protocolLink = `web+canti://open${path}${search}`;
|
||||||
|
|
||||||
|
if (isInstalled) {
|
||||||
|
window.location.href = protocolLink;
|
||||||
|
} else {
|
||||||
|
const toast = await this.toastCtrl.create({
|
||||||
|
header: 'Apri nell\'App CantiCristiani',
|
||||||
|
message: 'Usa la PWA installata per visualizzare questo contenuto ed evitare la cache del browser.',
|
||||||
|
position: 'top',
|
||||||
|
color: 'warning',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'APRI APP',
|
||||||
|
handler: () => {
|
||||||
|
window.location.href = protocolLink;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Nascondi',
|
||||||
|
role: 'cancel'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await toast.present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showFullscreenUpdateOverlay() {
|
export function showFullscreenUpdateOverlay() {
|
||||||
@@ -37,8 +127,13 @@ export function showFullscreenUpdateOverlay() {
|
|||||||
|
|
||||||
overlay.innerHTML = `
|
overlay.innerHTML = `
|
||||||
<div style="text-align: center; padding: 20px; max-width: 400px; width: 100%;">
|
<div style="text-align: center; padding: 20px; max-width: 400px; width: 100%;">
|
||||||
<h2 style="font-size: 1.8rem; font-weight: 600; margin-bottom: 10px; color: #ffffff; font-family: 'Outfit', sans-serif; -webkit-font-smoothing: antialiased;">Aggiornamento in corso</h2>
|
<div style="margin-bottom: 25px; display: inline-block;">
|
||||||
<p style="font-size: 1rem; color: rgba(255, 255, 255, 0.6); margin-bottom: 30px; font-family: 'Outfit', sans-serif; -webkit-font-smoothing: antialiased;">Installazione della nuova versione...</p>
|
<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.3); border: 2px solid rgba(230, 126, 34, 0.2);">
|
||||||
|
</div>
|
||||||
|
<h2 style="font-size: 1.8rem; font-weight: 600; margin-bottom: 5px; color: #ffffff; font-family: 'Outfit', sans-serif; -webkit-font-smoothing: antialiased;">Download aggiornamento</h2>
|
||||||
|
<p style="font-size: 1rem; color: rgba(255, 255, 255, 0.6); margin-bottom: 15px; font-family: 'Outfit', sans-serif; -webkit-font-smoothing: antialiased;">Scaricamento della nuova versione...</p>
|
||||||
|
<div id="pwa-update-version" style="font-size: 0.85rem; color: rgba(255, 255, 255, 0.45); margin-bottom: 20px; font-family: 'Outfit', sans-serif; -webkit-font-smoothing: antialiased; font-weight: 500;">Ricerca versione...</div>
|
||||||
|
<div style="font-size: 0.95rem; font-weight: 600; color: #fdcb6e; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 1.5px; font-family: 'Outfit', sans-serif;">Fase: Download</div>
|
||||||
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 10px; height: 8px; width: 100%; overflow: hidden; margin-bottom: 15px;">
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 10px; height: 8px; width: 100%; overflow: hidden; margin-bottom: 15px;">
|
||||||
<div id="pwa-update-bar" style="background: #e67e22; height: 100%; width: 0%; transition: width 0.1s ease; border-radius: 10px;"></div>
|
<div id="pwa-update-bar" style="background: #e67e22; height: 100%; width: 0%; transition: width 0.1s ease; border-radius: 10px;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,6 +142,25 @@ export function showFullscreenUpdateOverlay() {
|
|||||||
`;
|
`;
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
// Fetch remote version to display the version being downloaded
|
||||||
|
fetch(`/version.json?cb=${Date.now()}`)
|
||||||
|
.then(res => {
|
||||||
|
if (res.ok) return res.json();
|
||||||
|
throw new Error('Fallback');
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
const versionEl = document.getElementById('pwa-update-version');
|
||||||
|
if (data && data.version && versionEl) {
|
||||||
|
versionEl.textContent = 'Versione ' + data.version;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
const versionEl = document.getElementById('pwa-update-version');
|
||||||
|
if (versionEl) {
|
||||||
|
versionEl.textContent = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let percent = 0;
|
let percent = 0;
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (percent < 95) {
|
if (percent < 95) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NgModule, isDevMode } from '@angular/core';
|
import { NgModule, isDevMode } from '@angular/core';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { RouteReuseStrategy } from '@angular/router';
|
import { RouteReuseStrategy } from '@angular/router';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
import { IonicStorageModule } from '@ionic/storage-angular';
|
import { IonicStorageModule } from '@ionic/storage-angular';
|
||||||
|
|
||||||
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
|
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
|
||||||
@@ -9,6 +9,7 @@ import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
|
|||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||||
|
import { ApiAuthInterceptor } from './interceptors/api-auth.interceptor';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
@@ -24,7 +25,10 @@ import { ServiceWorkerModule } from '@angular/service-worker';
|
|||||||
registrationStrategy: 'registerImmediately'
|
registrationStrategy: 'registerImmediately'
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
|
providers: [
|
||||||
|
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: ApiAuthInterceptor, multi: true }
|
||||||
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<ion-header class="ion-no-border">
|
||||||
|
<ion-toolbar class="bg-gradient">
|
||||||
|
<ion-title class="outfit-font">QR Code di Ripristino</ion-title>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button (click)="dismiss()">
|
||||||
|
<ion-icon name="close-outline" slot="icon-only" color="secondary"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content class="bg-gradient ion-padding">
|
||||||
|
<div class="qr-container">
|
||||||
|
<p class="description outfit-font">
|
||||||
|
Salva questo QR Code (fai uno screenshot o scaricalo) per ripristinare il tuo account e le tue comunità se cambi dispositivo o reinstalli l'applicazione.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="qr-card glass">
|
||||||
|
<div class="qr-wrapper" *ngIf="qrCodeUrl">
|
||||||
|
<img [src]="qrCodeUrl" alt="QR Code di Ripristino" class="qr-image" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uuid-box">
|
||||||
|
<span class="uuid-label outfit-font">Codice Identificativo:</span>
|
||||||
|
<span class="uuid-text select-all outfit-font">{{ userUuid }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-wrapper">
|
||||||
|
<ion-button expand="block" fill="solid" color="secondary" class="outfit-font action-btn" (click)="copyToClipboard()">
|
||||||
|
<ion-icon name="copy-outline" slot="start"></ion-icon>
|
||||||
|
Copia Codice Testuale
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<a *ngIf="qrCodeUrl" [href]="qrCodeUrl" download="ripristino-identita-canti.png" class="download-btn outfit-font">
|
||||||
|
<ion-icon name="download-outline"></ion-icon>
|
||||||
|
Scarica Immagine QR
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ion-content>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
.qr-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px 8px;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--ion-text-color);
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-card {
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 340px;
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-wrapper {
|
||||||
|
background: white;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 220px;
|
||||||
|
height: 220px;
|
||||||
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.qr-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uuid-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uuid-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uuid-text {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--ion-text-color);
|
||||||
|
word-break: break-all;
|
||||||
|
opacity: 0.85;
|
||||||
|
font-family: monospace;
|
||||||
|
user-select: all;
|
||||||
|
background: rgba(var(--ion-text-color-rgb, 255, 255, 255), 0.04);
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
margin: 0;
|
||||||
|
--border-radius: 14px;
|
||||||
|
--box-shadow: 0 4px 16px rgba(var(--ion-color-secondary-rgb), 0.2);
|
||||||
|
font-weight: 600;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: var(--ion-text-color);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Component, OnInit, inject, Input } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule, ModalController, ToastController } from '@ionic/angular';
|
||||||
|
import * as QRCode from 'qrcode';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-identity-qr-modal',
|
||||||
|
templateUrl: './identity-qr-modal.component.html',
|
||||||
|
styleUrls: ['./identity-qr-modal.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, IonicModule]
|
||||||
|
})
|
||||||
|
export class IdentityQrModalComponent implements OnInit {
|
||||||
|
private modalCtrl = inject(ModalController);
|
||||||
|
private toastCtrl = inject(ToastController);
|
||||||
|
|
||||||
|
@Input() userUuid!: string;
|
||||||
|
public qrCodeUrl: string = '';
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.generateQr();
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateQr() {
|
||||||
|
try {
|
||||||
|
this.qrCodeUrl = await QRCode.toDataURL(this.userUuid, {
|
||||||
|
errorCorrectionLevel: 'H',
|
||||||
|
margin: 2,
|
||||||
|
width: 400,
|
||||||
|
color: {
|
||||||
|
dark: '#1e293b', // Slate 800 for premium dark aesthetic contrast
|
||||||
|
light: '#ffffff'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to generate QR Code:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyToClipboard() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(this.userUuid);
|
||||||
|
const toast = await this.toastCtrl.create({
|
||||||
|
message: 'Codice copiato negli appunti!',
|
||||||
|
duration: 2000,
|
||||||
|
color: 'success',
|
||||||
|
position: 'bottom'
|
||||||
|
});
|
||||||
|
await toast.present();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy text:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
this.modalCtrl.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
-24
@@ -7,7 +7,9 @@
|
|||||||
<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 }}
|
||||||
<ion-icon name="refresh-outline" style="font-size: 0.9em;"></ion-icon>
|
<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>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,14 +70,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ion-button
|
<ion-button
|
||||||
*ngIf="settingsService.showEditor()"
|
[fill]="activeFilterType() === 'lista_completa' || showValidati() || showNonValidati() ? 'solid' : 'outline'"
|
||||||
[fill]="showOnlyMine() ? 'solid' : 'outline'"
|
|
||||||
size="small"
|
size="small"
|
||||||
(click)="toggleOnlyMine()"
|
(click)="toggleFilterType('lista_completa')"
|
||||||
color="secondary"
|
|
||||||
class="filter-chip">
|
class="filter-chip">
|
||||||
Miei
|
{{ showValidati() ? 'Lista: Validati' : (showNonValidati() ? 'Lista: Non Validati' : 'Lista completa') }}
|
||||||
<span class="close-icon-wrapper" *ngIf="showOnlyMine()" (click)="clearOnlyMine($event)">
|
<ion-icon slot="end" name="chevron-down-outline" *ngIf="!showValidati() && !showNonValidati()"></ion-icon>
|
||||||
|
<span class="close-icon-wrapper" *ngIf="showValidati() || showNonValidati()" (click)="clearListaCompleta($event)">
|
||||||
<ion-icon slot="end" name="close-circle"></ion-icon>
|
<ion-icon slot="end" name="close-circle"></ion-icon>
|
||||||
</span>
|
</span>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
@@ -85,7 +86,7 @@
|
|||||||
size="small"
|
size="small"
|
||||||
(click)="toggleFilterType('liturgico')"
|
(click)="toggleFilterType('liturgico')"
|
||||||
class="filter-chip">
|
class="filter-chip">
|
||||||
{{ selectedLiturgico() !== null ? getSelectedLiturgicoLabel() : 'Liturgia' }}
|
{{ selectedLiturgico() !== null ? 'Liturgia: ' + getSelectedLiturgicoLabel() : 'Liturgia' }}
|
||||||
<ion-icon slot="end" name="chevron-down-outline" *ngIf="selectedLiturgico() === null"></ion-icon>
|
<ion-icon slot="end" name="chevron-down-outline" *ngIf="selectedLiturgico() === null"></ion-icon>
|
||||||
<span class="close-icon-wrapper" *ngIf="selectedLiturgico() !== null" (click)="clearLiturgico($event)">
|
<span class="close-icon-wrapper" *ngIf="selectedLiturgico() !== null" (click)="clearLiturgico($event)">
|
||||||
<ion-icon slot="end" name="close-circle"></ion-icon>
|
<ion-icon slot="end" name="close-circle"></ion-icon>
|
||||||
@@ -97,7 +98,7 @@
|
|||||||
size="small"
|
size="small"
|
||||||
(click)="toggleFilterType('tematico')"
|
(click)="toggleFilterType('tematico')"
|
||||||
class="filter-chip">
|
class="filter-chip">
|
||||||
{{ selectedTematico() !== null ? getSelectedTematicoLabel() : 'Periodo' }}
|
{{ selectedTematico() !== null ? 'Periodo: ' + getSelectedTematicoLabel() : 'Periodo' }}
|
||||||
<ion-icon slot="end" name="chevron-down-outline" *ngIf="selectedTematico() === null"></ion-icon>
|
<ion-icon slot="end" name="chevron-down-outline" *ngIf="selectedTematico() === null"></ion-icon>
|
||||||
<span class="close-icon-wrapper" *ngIf="selectedTematico() !== null" (click)="clearTematico($event)">
|
<span class="close-icon-wrapper" *ngIf="selectedTematico() !== null" (click)="clearTematico($event)">
|
||||||
<ion-icon slot="end" name="close-circle"></ion-icon>
|
<ion-icon slot="end" name="close-circle"></ion-icon>
|
||||||
@@ -120,7 +121,7 @@
|
|||||||
size="small"
|
size="small"
|
||||||
(click)="playlistService.allPlaylists().length > 0 ? toggleFilterType('playlist') : playlistService.toggleSelectionMode()"
|
(click)="playlistService.allPlaylists().length > 0 ? toggleFilterType('playlist') : playlistService.toggleSelectionMode()"
|
||||||
class="filter-chip">
|
class="filter-chip">
|
||||||
{{ playlistService.activeListName() !== null ? 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 && playlistService.allPlaylists().length > 0"></ion-icon>
|
||||||
<ion-icon slot="end" name="add-circle-outline" *ngIf="playlistService.activeListName() === null && playlistService.allPlaylists().length === 0"></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)">
|
||||||
@@ -145,7 +146,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- Category Scroll (Dropdown style) -->
|
<!-- Category Scroll (Dropdown style) -->
|
||||||
<ion-toolbar class="bg-gradient momentos-toolbar" *ngIf="activeFilterType() || playlistService.selectionMode() || (playlistService.activeListName() && !playlistService.selectionMode())">
|
<ion-toolbar class="bg-gradient momentos-toolbar" *ngIf="activeFilterType() || playlistService.selectionMode()">
|
||||||
<div class="momento-scroll">
|
<div class="momento-scroll">
|
||||||
<!-- Selection Mode Actions -->
|
<!-- Selection Mode Actions -->
|
||||||
<div class="selection-pill glass" *ngIf="playlistService.selectionMode()">
|
<div class="selection-pill glass" *ngIf="playlistService.selectionMode()">
|
||||||
@@ -162,7 +163,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Active Playlist Actions -->
|
<!-- Active Playlist Actions -->
|
||||||
<div class="selection-pill glass" *ngIf="playlistService.activeListName() && !playlistService.selectionMode()">
|
<div class="selection-pill glass" *ngIf="activeFilterType() === 'playlist' && playlistService.activePlaylistId() && !playlistService.selectionMode()">
|
||||||
<ion-button fill="clear" color="secondary" (click)="editPlaylist()" class="mini-action-btn" *ngIf="!isComunitaPlaylist() && isActivePlaylistSaved()">
|
<ion-button fill="clear" color="secondary" (click)="editPlaylist()" class="mini-action-btn" *ngIf="!isComunitaPlaylist() && isActivePlaylistSaved()">
|
||||||
<ion-icon slot="icon-only" name="create-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="create-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
@@ -170,20 +171,37 @@
|
|||||||
<ion-icon slot="icon-only" name="share-social-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="share-social-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button fill="clear" color="danger" (click)="deleteActivePlaylist()" class="mini-action-btn" *ngIf="playlistService.activePlaylistId() && !isComunitaPlaylist()">
|
<ion-button fill="clear" color="danger" (click)="deleteActivePlaylist()" class="mini-action-btn" *ngIf="playlistService.activePlaylistId() && !isComunitaPlaylist()">
|
||||||
<ion-icon slot="icon-only" name="trash-outline"></ion-icon>
|
<ion-icon name="trash-outline" slot="icon-only"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter chips -->
|
<!-- Filter chips -->
|
||||||
<div
|
<ng-container *ngIf="activeFilterType() === 'lista_completa'">
|
||||||
*ngFor="let item of (activeFilterType() === 'playlist' ? playlistService.allPlaylists() : (activeFilterType() === 'liturgico' ? cantiService.indiceLiturgico() : (activeFilterType() === 'tematico' ? cantiService.indiceTematico() : [])))"
|
<div
|
||||||
class="momento-chip glass"
|
class="momento-chip glass"
|
||||||
[class.active]="activeFilterType() === 'playlist' ? playlistService.activePlaylistId() === item.id : isIndexSelected(item.id)"
|
[class.active]="showValidati()"
|
||||||
[class.comunita-chip]="activeFilterType() === 'playlist' && item.isComunita"
|
(click)="toggleValidati()">
|
||||||
(click)="activeFilterType() === 'playlist' ? selectPlaylist(item) : toggleIndex(item.id, activeFilterType()!)">
|
Validati
|
||||||
<ion-icon *ngIf="activeFilterType() === 'playlist' && item.isComunita" name="people-outline" style="font-size: 0.85rem; margin-right: 4px; vertical-align: middle;"></ion-icon>
|
</div>
|
||||||
{{ activeFilterType() === 'playlist' ? item.name : item.tag_name }}
|
<div
|
||||||
</div>
|
class="momento-chip glass"
|
||||||
|
[class.active]="showNonValidati()"
|
||||||
|
(click)="toggleNonValidati()">
|
||||||
|
Non Validati
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="activeFilterType() !== 'lista_completa'">
|
||||||
|
<div
|
||||||
|
*ngFor="let item of (activeFilterType() === 'playlist' ? playlistService.allPlaylists() : (activeFilterType() === 'liturgico' ? cantiService.indiceLiturgico() : (activeFilterType() === 'tematico' ? cantiService.indiceTematico() : [])))"
|
||||||
|
class="momento-chip glass"
|
||||||
|
[class.active]="activeFilterType() === 'playlist' ? playlistService.activePlaylistId() === item.id : isIndexSelected(item.id)"
|
||||||
|
[class.comunita-chip]="activeFilterType() === 'playlist' && item.isComunita"
|
||||||
|
(click)="activeFilterType() === 'playlist' ? selectPlaylist(item) : toggleIndex(item.id, activeFilterType()!)">
|
||||||
|
<ion-icon *ngIf="activeFilterType() === 'playlist' && item.isComunita" name="people-outline" style="font-size: 0.85rem; margin-right: 4px; vertical-align: middle;"></ion-icon>
|
||||||
|
{{ activeFilterType() === 'playlist' ? item.name : item.tag_name }}
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
@@ -284,7 +302,7 @@
|
|||||||
|
|
||||||
<div class="item-wrapper">
|
<div class="item-wrapper">
|
||||||
<!-- Selection Area -->
|
<!-- Selection Area -->
|
||||||
<div class="selection-column" (click)="playlistService.toggleSongSelection(canto.id); $event.stopPropagation()">
|
<div class="selection-column" (click)="toggleSongSelection(canto.id); $event.stopPropagation()">
|
||||||
<span class="canto-number" [class.selected-number]="playlistService.selectedIds().has(canto.id)">
|
<span class="canto-number" [class.selected-number]="playlistService.selectedIds().has(canto.id)">
|
||||||
{{ canto.id.startsWith('my_') ? getMySongNumber(canto) : canto.id_canti }}
|
{{ canto.id.startsWith('my_') ? getMySongNumber(canto) : canto.id_canti }}
|
||||||
</span>
|
</span>
|
||||||
@@ -297,7 +315,8 @@
|
|||||||
<h2 class="outfit-font" style="font-weight: 500; color: var(--ion-color-secondary); display: flex; align-items: center; flex-wrap: wrap; gap: 4px;">
|
<h2 class="outfit-font" style="font-weight: 500; color: var(--ion-color-secondary); display: flex; align-items: center; flex-wrap: wrap; gap: 4px;">
|
||||||
<span *ngIf="getCommunitySongNumber(canto)" style="color: var(--ion-color-secondary); font-weight: 600; margin-right: 8px; opacity: 0.9;">{{ getCommunitySongNumber(canto) }}</span>
|
<span *ngIf="getCommunitySongNumber(canto)" style="color: var(--ion-color-secondary); font-weight: 600; margin-right: 8px; opacity: 0.9;">{{ getCommunitySongNumber(canto) }}</span>
|
||||||
<span>{{ canto.titolo }}</span>
|
<span>{{ canto.titolo }}</span>
|
||||||
<span *ngIf="canto.nonValidato" class="non-validato-badge">Non Validato</span>
|
<span *ngIf="canto.nonValidato && !canto.id.startsWith('my_')" class="non-validato-badge remote-badge">Remoto</span>
|
||||||
|
<span *ngIf="canto.id.startsWith('my_')" class="non-validato-badge mio-badge">Mio</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p style="color: rgba(255,255,255,0.6); margin-bottom: 2px; display: flex; align-items: center; flex-wrap: wrap; gap: 8px;">
|
<p style="color: rgba(255,255,255,0.6); margin-bottom: 2px; display: flex; align-items: center; flex-wrap: wrap; gap: 8px;">
|
||||||
<span>{{ canto.autore || 'Autore sconosciuto' }}</span>
|
<span>{{ canto.autore || 'Autore sconosciuto' }}</span>
|
||||||
|
|||||||
@@ -946,12 +946,24 @@ ion-title {
|
|||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
|
&.mio-badge {
|
||||||
|
background: rgba(var(--ion-color-secondary-rgb), 0.15);
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
border-color: rgba(var(--ion-color-secondary-rgb), 0.35);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(body.high-contrast) .non-validato-badge {
|
:host-context(body.high-contrast) .non-validato-badge {
|
||||||
background: rgba(231, 76, 60, 0.1) !important;
|
background: rgba(231, 76, 60, 0.1) !important;
|
||||||
color: #c0392b !important;
|
color: #c0392b !important;
|
||||||
border-color: #c0392b !important;
|
border-color: #c0392b !important;
|
||||||
|
|
||||||
|
&.mio-badge {
|
||||||
|
background: rgba(var(--ion-color-secondary-rgb), 0.1) !important;
|
||||||
|
color: var(--ion-color-secondary-shade, #007bb6) !important;
|
||||||
|
border-color: var(--ion-color-secondary-shade, #007bb6) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
|
|||||||
+421
-17
@@ -33,8 +33,10 @@ export class HomePage implements OnDestroy {
|
|||||||
public showOnlyMine = signal<boolean>(false);
|
public showOnlyMine = signal<boolean>(false);
|
||||||
public showTopTen = signal<boolean>(false);
|
public showTopTen = signal<boolean>(false);
|
||||||
public showSuggeriti = signal<boolean>(false);
|
public showSuggeriti = signal<boolean>(false);
|
||||||
|
public showValidati = signal<boolean>(false);
|
||||||
|
public showNonValidati = signal<boolean>(false);
|
||||||
public isMassCardExpanded = signal<boolean>(false);
|
public isMassCardExpanded = signal<boolean>(false);
|
||||||
public activeFilterType = signal<'liturgico' | 'tematico' | 'playlist' | null>(null);
|
public activeFilterType = signal<'liturgico' | 'tematico' | 'playlist' | 'lista_completa' | null>(null);
|
||||||
public loadedThumbs = new Set<string>();
|
public loadedThumbs = new Set<string>();
|
||||||
public version = VERSION;
|
public version = VERSION;
|
||||||
public appName = environment.appName;
|
public appName = environment.appName;
|
||||||
@@ -44,6 +46,7 @@ export class HomePage implements OnDestroy {
|
|||||||
|
|
||||||
public showAndroidBanner = signal<boolean>(false);
|
public showAndroidBanner = signal<boolean>(false);
|
||||||
public showIosTooltip = signal<boolean>(false);
|
public showIosTooltip = signal<boolean>(false);
|
||||||
|
public hasUpdateAvailable = signal<boolean>(false);
|
||||||
|
|
||||||
public fontSize = signal<number>(1.0);
|
public fontSize = signal<number>(1.0);
|
||||||
public youtubePlayerService = inject(YoutubePlayerService);
|
public youtubePlayerService = inject(YoutubePlayerService);
|
||||||
@@ -73,6 +76,7 @@ export class HomePage implements OnDestroy {
|
|||||||
private swUpdate = inject(SwUpdate);
|
private swUpdate = inject(SwUpdate);
|
||||||
|
|
||||||
private firstInteraction = true;
|
private firstInteraction = true;
|
||||||
|
private updatePollInterval: any = null;
|
||||||
|
|
||||||
async checkForAppUpdate(event?: Event) {
|
async checkForAppUpdate(event?: Event) {
|
||||||
if (event) event.stopPropagation();
|
if (event) event.stopPropagation();
|
||||||
@@ -187,6 +191,24 @@ export class HomePage implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkUpdateStatus() {
|
||||||
|
try {
|
||||||
|
if (this.swUpdate.isEnabled) {
|
||||||
|
const swFoundUpdate = await this.swUpdate.checkForUpdate().catch(() => false);
|
||||||
|
if (swFoundUpdate) {
|
||||||
|
this.hasUpdateAvailable.set(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mismatch = await this.checkVersionJson();
|
||||||
|
if (mismatch) {
|
||||||
|
this.hasUpdateAvailable.set(true);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to check for updates on startup:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onInteraction() {
|
onInteraction() {
|
||||||
if (this.firstInteraction) {
|
if (this.firstInteraction) {
|
||||||
this.firstInteraction = false;
|
this.firstInteraction = false;
|
||||||
@@ -241,10 +263,11 @@ export class HomePage implements OnDestroy {
|
|||||||
const comunitaIds = this.comunitaService.comunitaCantiIds();
|
const comunitaIds = this.comunitaService.comunitaCantiIds();
|
||||||
|
|
||||||
if (comunitaCode && this.comunitaService.isFilterActive()) {
|
if (comunitaCode && this.comunitaService.isFilterActive()) {
|
||||||
// Base is standard canti + my canti + community custom canti
|
// Base is standard canti + my canti + community custom canti + remote custom canti
|
||||||
list = [
|
list = [
|
||||||
...this.cantiService.canti(),
|
...this.cantiService.canti(),
|
||||||
...this.myCantiService.myCanti(),
|
...this.myCantiService.myCanti(),
|
||||||
|
...this.playlistService.remoteCustomSongs(),
|
||||||
...this.comunitaService.comunitaCantiPersonali()
|
...this.comunitaService.comunitaCantiPersonali()
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -278,8 +301,12 @@ export class HomePage implements OnDestroy {
|
|||||||
return numA - numB;
|
return numA - numB;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// General context: only standard canti + my canti
|
// General context: only standard canti + my canti + remote custom canti
|
||||||
list = [...this.cantiService.canti(), ...this.myCantiService.myCanti()];
|
list = [
|
||||||
|
...this.cantiService.canti(),
|
||||||
|
...this.myCantiService.myCanti(),
|
||||||
|
...this.playlistService.remoteCustomSongs()
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Cumulative filtering
|
// 2. Cumulative filtering
|
||||||
@@ -290,7 +317,7 @@ export class HomePage implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A. Filter by Playlist (if activeIds is present)
|
// A. Filter by Playlist (if activeIds is present)
|
||||||
if (activeIds.length > 0 && !selectionMode) {
|
if (activeIds.length > 0 && (!selectionMode || this.playlistService.activePlaylistId() === null)) {
|
||||||
list = activeIds
|
list = activeIds
|
||||||
.map(id => list.find(c => c.id === id))
|
.map(id => list.find(c => c.id === id))
|
||||||
.filter((c): c is any => !!c);
|
.filter((c): c is any => !!c);
|
||||||
@@ -302,6 +329,16 @@ export class HomePage implements OnDestroy {
|
|||||||
list = list.filter(c => myIds.has(c.id));
|
list = list.filter(c => myIds.has(c.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by Validati (non personali e non contrassegnati come non validati)
|
||||||
|
if (this.showValidati()) {
|
||||||
|
list = list.filter(c => !c.nonValidato && !c.id.startsWith('my_'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by Non-Validati (personali o esplicitamente non validati)
|
||||||
|
if (this.showNonValidati()) {
|
||||||
|
list = list.filter(c => c.nonValidato || c.id.startsWith('my_'));
|
||||||
|
}
|
||||||
|
|
||||||
// C. Filter by Liturgical Moment
|
// C. Filter by Liturgical Moment
|
||||||
if (litId !== null) {
|
if (litId !== null) {
|
||||||
list = list.filter(c => c.id_momenti?.includes(litId));
|
list = list.filter(c => c.id_momenti?.includes(litId));
|
||||||
@@ -377,6 +414,21 @@ export class HomePage implements OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// Check if there is an update available
|
||||||
|
this.checkUpdateStatus();
|
||||||
|
if (this.swUpdate.isEnabled) {
|
||||||
|
this.swUpdate.versionUpdates.subscribe(evt => {
|
||||||
|
if (evt.type === 'VERSION_READY') {
|
||||||
|
this.hasUpdateAvailable.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polling setup: check for updates every 30 seconds
|
||||||
|
this.updatePollInterval = setInterval(() => {
|
||||||
|
this.checkUpdateStatus();
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
// Check if install prompts should be visible
|
// Check if install prompts should be visible
|
||||||
const androidDismissed = localStorage.getItem('pwa-android-dismissed') === 'true';
|
const androidDismissed = localStorage.getItem('pwa-android-dismissed') === 'true';
|
||||||
const iosDismissed = localStorage.getItem('pwa-ios-dismissed') === 'true';
|
const iosDismissed = localStorage.getItem('pwa-ios-dismissed') === 'true';
|
||||||
@@ -411,6 +463,8 @@ export class HomePage implements OnDestroy {
|
|||||||
this.showOnlyMine.set(false);
|
this.showOnlyMine.set(false);
|
||||||
this.showTopTen.set(false);
|
this.showTopTen.set(false);
|
||||||
this.showSuggeriti.set(false);
|
this.showSuggeriti.set(false);
|
||||||
|
this.showValidati.set(false);
|
||||||
|
this.showNonValidati.set(false);
|
||||||
this.playlistService.activeListIds.set([]);
|
this.playlistService.activeListIds.set([]);
|
||||||
this.playlistService.activeListName.set(null);
|
this.playlistService.activeListName.set(null);
|
||||||
this.playlistService.activePlaylistId.set(null);
|
this.playlistService.activePlaylistId.set(null);
|
||||||
@@ -432,6 +486,12 @@ export class HomePage implements OnDestroy {
|
|||||||
if (params['import']) {
|
if (params['import']) {
|
||||||
this.handleImport(params['import']);
|
this.handleImport(params['import']);
|
||||||
}
|
}
|
||||||
|
if (params['playlist-uid']) {
|
||||||
|
this.handleRemotePlaylistImport(params['playlist-uid'], params['playlist-id']);
|
||||||
|
}
|
||||||
|
if (params['restore-uid']) {
|
||||||
|
this.handleRemoteRestore(params['restore-uid']);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let prevSelectionMode = false;
|
let prevSelectionMode = false;
|
||||||
@@ -443,7 +503,7 @@ export class HomePage implements OnDestroy {
|
|||||||
|
|
||||||
if (selectionMode) {
|
if (selectionMode) {
|
||||||
if (!prevSelectionMode) {
|
if (!prevSelectionMode) {
|
||||||
if (activeIds.length === 0) {
|
if (activeIds.length === 0 || this.playlistService.activePlaylistId() === null) {
|
||||||
this.isAddingSongs.set(true);
|
this.isAddingSongs.set(true);
|
||||||
} else {
|
} else {
|
||||||
this.isAddingSongs.set(false);
|
this.isAddingSongs.set(false);
|
||||||
@@ -476,7 +536,12 @@ export class HomePage implements OnDestroy {
|
|||||||
updatedList = [...updatedList, ...newSongs];
|
updatedList = [...updatedList, ...newSongs];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reorderList.set(updatedList);
|
// Previeni cicli infiniti se il contenuto della lista non è effettivamente cambiato
|
||||||
|
const currentIdsStr = currentIds.join(',');
|
||||||
|
const updatedIdsStr = updatedList.map(c => c.id).join(',');
|
||||||
|
if (currentIdsStr !== updatedIdsStr) {
|
||||||
|
this.reorderList.set(updatedList);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
prevSelectionMode = false;
|
prevSelectionMode = false;
|
||||||
@@ -580,11 +645,200 @@ export class HomePage implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleRemotePlaylistImport(uid: string, pid?: string) {
|
||||||
|
const loading = await this.loadingCtrl.create({
|
||||||
|
message: 'Scaricamento playlist da remoto...'
|
||||||
|
});
|
||||||
|
await loading.present();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.canticristiani.it/${uid}.json?cb=${Date.now()}`, { cache: 'no-store' });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Risposta del server non valida: ${response.status}`);
|
||||||
|
}
|
||||||
|
const remoteJson = await response.json();
|
||||||
|
if (Array.isArray(remoteJson)) {
|
||||||
|
// 1. Reconstruct custom songs
|
||||||
|
const customSongs = remoteJson
|
||||||
|
.filter((item: any) => item.momenti && !item.momenti.includes('Playlist'))
|
||||||
|
.map((item: any) => ({
|
||||||
|
id: `my_${item.id_canti}`,
|
||||||
|
id_canti: Number(item.id_canti),
|
||||||
|
titolo: item.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)) || []
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 2. 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: `remote_${uid}_${item.id_canti}`,
|
||||||
|
name: `[Remote] ${item.titolo}`,
|
||||||
|
ids: item.testo ? item.testo.split(',') : [],
|
||||||
|
songSettings: songSettings,
|
||||||
|
createdAt: new Date(),
|
||||||
|
isRemote: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (playlists.length === 0) {
|
||||||
|
throw new Error('Nessuna playlist trovata in questa identità.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the selected one
|
||||||
|
let selectedPl = playlists[0];
|
||||||
|
if (pid) {
|
||||||
|
const targetId = `remote_${uid}_${pid}`;
|
||||||
|
const found = playlists.find(p => p.id === targetId || p.id.endsWith(`_${pid}`));
|
||||||
|
if (found) {
|
||||||
|
selectedPl = found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.playlistService.saveRemotePlaylist(selectedPl, customSongs);
|
||||||
|
|
||||||
|
// Clear other filters to avoid confusion
|
||||||
|
this.selectedLiturgico.set(null);
|
||||||
|
this.selectedTematico.set(null);
|
||||||
|
|
||||||
|
// Select it immediately
|
||||||
|
this.selectPlaylist(selectedPl);
|
||||||
|
|
||||||
|
await loading.dismiss();
|
||||||
|
|
||||||
|
const toast = await this.toastCtrl.create({
|
||||||
|
message: `Playlist "${selectedPl.name}" caricata per consultazione!`,
|
||||||
|
duration: 3000,
|
||||||
|
color: 'success'
|
||||||
|
});
|
||||||
|
await toast.present();
|
||||||
|
} else {
|
||||||
|
throw new Error('Formato dati non valido.');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
await loading.dismiss();
|
||||||
|
console.error('Failed to import remote playlist:', err);
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
header: 'Errore Importazione',
|
||||||
|
message: 'Impossibile scaricare la playlist da remoto. Controlla la connessione o il codice.',
|
||||||
|
buttons: ['OK']
|
||||||
|
});
|
||||||
|
await alert.present();
|
||||||
|
} finally {
|
||||||
|
this.router.navigate([], { queryParams: { 'playlist-uid': null, 'playlist-id': null }, queryParamsHandling: 'merge' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRemoteRestore(uid: string) {
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
header: 'Ripristina Identità',
|
||||||
|
message: 'Sei sicuro di voler scaricare e ripristinare i dati di questa identità? L\'ID attuale del dispositivo verrà sovrascritto.',
|
||||||
|
buttons: [
|
||||||
|
{ text: 'Annulla', role: 'cancel' },
|
||||||
|
{
|
||||||
|
text: 'Ripristina',
|
||||||
|
role: 'destructive',
|
||||||
|
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/${uid}.json?cb=${Date.now()}`, { cache: 'no-store' });
|
||||||
|
if (response.ok) {
|
||||||
|
const remoteJson = await response.json();
|
||||||
|
if (Array.isArray(remoteJson)) {
|
||||||
|
// Reconstruct custom songs
|
||||||
|
const customSongs = remoteJson
|
||||||
|
.filter((item: any) => !item.momenti || !item.momenti.includes('Playlist'))
|
||||||
|
.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)) || []
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (customSongs.length > 0) {
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (playlists.length > 0) {
|
||||||
|
this.playlistService.playlists.set(playlists);
|
||||||
|
if (this.playlistService['_storage']) {
|
||||||
|
const key = this.playlistService.getPlaylistsStorageKey();
|
||||||
|
await this.playlistService['_storage'].set(key, playlists);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to restore remote backup:', err);
|
||||||
|
} finally {
|
||||||
|
await loading.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.settingsService.setUserUuid(uid);
|
||||||
|
|
||||||
|
const toast = await this.toastCtrl.create({
|
||||||
|
message: 'Dati ripristinati con successo! Ricaricamento...',
|
||||||
|
duration: 2000,
|
||||||
|
color: 'success'
|
||||||
|
});
|
||||||
|
await toast.present();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.replace(window.location.origin + window.location.pathname);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await alert.present();
|
||||||
|
}
|
||||||
|
|
||||||
ionViewWillLeave() {
|
ionViewWillLeave() {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.audioEngine.stopSearchRecognition();
|
this.audioEngine.stopSearchRecognition();
|
||||||
|
if (this.updatePollInterval) {
|
||||||
|
clearInterval(this.updatePollInterval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearch(event: any) {
|
onSearch(event: any) {
|
||||||
@@ -619,8 +873,9 @@ export class HomePage implements OnDestroy {
|
|||||||
return [
|
return [
|
||||||
...this.cantiService.canti(),
|
...this.cantiService.canti(),
|
||||||
...this.myCantiService.myCanti(),
|
...this.myCantiService.myCanti(),
|
||||||
|
...this.playlistService.remoteCustomSongs(),
|
||||||
...this.comunitaService.comunitaCantiPersonali()
|
...this.comunitaService.comunitaCantiPersonali()
|
||||||
].find(c => c.id === id);
|
].find(c => c.id === id || String(c.id_canti) === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlayingCanto() {
|
getPlayingCanto() {
|
||||||
@@ -643,6 +898,7 @@ export class HomePage implements OnDestroy {
|
|||||||
|
|
||||||
toggleOnlyMine() {
|
toggleOnlyMine() {
|
||||||
this.showOnlyMine.update(v => !v);
|
this.showOnlyMine.update(v => !v);
|
||||||
|
this.activeFilterType.set(null);
|
||||||
this.limit.set(10);
|
this.limit.set(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,13 +908,24 @@ export class HomePage implements OnDestroy {
|
|||||||
this.limit.set(10);
|
this.limit.set(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleIndex(id: number, type: 'liturgico' | 'tematico' | 'playlist') {
|
toggleIndex(id: number, type: 'liturgico' | 'tematico' | 'playlist' | 'lista_completa') {
|
||||||
if (type === 'liturgico') {
|
if (type === 'liturgico') {
|
||||||
this.selectedLiturgico.update(cur => cur === id ? null : id);
|
this.selectedLiturgico.update(cur => cur === id ? null : id);
|
||||||
} else if (type === 'tematico') {
|
} else if (type === 'tematico') {
|
||||||
this.selectedTematico.update(cur => cur === id ? null : id);
|
this.selectedTematico.update(cur => cur === id ? null : id);
|
||||||
}
|
}
|
||||||
this.activeFilterType.set(null);
|
|
||||||
|
// Mantieni aperto il menu di secondo livello se c'è una selezione attiva per quel tipo
|
||||||
|
let hasSelection = false;
|
||||||
|
if (type === 'liturgico' && this.selectedLiturgico() !== null) {
|
||||||
|
hasSelection = true;
|
||||||
|
} else if (type === 'tematico' && this.selectedTematico() !== null) {
|
||||||
|
hasSelection = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSelection) {
|
||||||
|
this.activeFilterType.set(null);
|
||||||
|
}
|
||||||
this.limit.set(10);
|
this.limit.set(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,6 +1001,8 @@ export class HomePage implements OnDestroy {
|
|||||||
this.showOnlyMine() ||
|
this.showOnlyMine() ||
|
||||||
this.showTopTen() ||
|
this.showTopTen() ||
|
||||||
this.showSuggeriti() ||
|
this.showSuggeriti() ||
|
||||||
|
this.showValidati() ||
|
||||||
|
this.showNonValidati() ||
|
||||||
this.playlistService.activeListName() !== null ||
|
this.playlistService.activeListName() !== null ||
|
||||||
this.searchQuery() !== '';
|
this.searchQuery() !== '';
|
||||||
}
|
}
|
||||||
@@ -744,6 +1013,8 @@ export class HomePage implements OnDestroy {
|
|||||||
this.showOnlyMine.set(false);
|
this.showOnlyMine.set(false);
|
||||||
this.showTopTen.set(false);
|
this.showTopTen.set(false);
|
||||||
this.showSuggeriti.set(false);
|
this.showSuggeriti.set(false);
|
||||||
|
this.showValidati.set(false);
|
||||||
|
this.showNonValidati.set(false);
|
||||||
this.playlistService.activeListIds.set([]);
|
this.playlistService.activeListIds.set([]);
|
||||||
this.playlistService.activeListName.set(null);
|
this.playlistService.activeListName.set(null);
|
||||||
this.playlistService.activePlaylistId.set(null);
|
this.playlistService.activePlaylistId.set(null);
|
||||||
@@ -752,9 +1023,23 @@ export class HomePage implements OnDestroy {
|
|||||||
this.limit.set(10);
|
this.limit.set(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleFilterType(type: 'liturgico' | 'tematico' | 'playlist') {
|
toggleFilterType(type: 'liturgico' | 'tematico' | 'playlist' | 'lista_completa') {
|
||||||
if (this.activeFilterType() === type) {
|
if (this.activeFilterType() === type) {
|
||||||
this.activeFilterType.set(null);
|
// Chiudi solo se non ci sono selezioni di secondo livello attive per questo tipo
|
||||||
|
let hasSelection = false;
|
||||||
|
if (type === 'liturgico' && this.selectedLiturgico() !== null) {
|
||||||
|
hasSelection = true;
|
||||||
|
} else if (type === 'tematico' && this.selectedTematico() !== null) {
|
||||||
|
hasSelection = true;
|
||||||
|
} else if (type === 'playlist' && this.playlistService.activePlaylistId() !== null) {
|
||||||
|
hasSelection = true;
|
||||||
|
} else if (type === 'lista_completa' && (this.showValidati() || this.showNonValidati())) {
|
||||||
|
hasSelection = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSelection) {
|
||||||
|
this.activeFilterType.set(null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.activeFilterType.set(type);
|
this.activeFilterType.set(type);
|
||||||
}
|
}
|
||||||
@@ -764,12 +1049,13 @@ export class HomePage implements OnDestroy {
|
|||||||
this.playlistService.activeListIds.set(pl.ids);
|
this.playlistService.activeListIds.set(pl.ids);
|
||||||
this.playlistService.activeListName.set(pl.name);
|
this.playlistService.activeListName.set(pl.name);
|
||||||
this.playlistService.activePlaylistId.set(pl.id);
|
this.playlistService.activePlaylistId.set(pl.id);
|
||||||
this.activeFilterType.set(null);
|
// Mantieni il menu aperto poiché la playlist è ora selezionata ed attiva
|
||||||
this.limit.set(50);
|
this.limit.set(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTopTen() {
|
toggleTopTen() {
|
||||||
this.showTopTen.update(v => !v);
|
this.showTopTen.update(v => !v);
|
||||||
|
this.activeFilterType.set(null);
|
||||||
this.limit.set(30);
|
this.limit.set(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -779,8 +1065,53 @@ export class HomePage implements OnDestroy {
|
|||||||
this.limit.set(30);
|
this.limit.set(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleValidati() {
|
||||||
|
this.showValidati.update(v => !v);
|
||||||
|
this.showNonValidati.set(false);
|
||||||
|
if (!this.showValidati()) {
|
||||||
|
this.activeFilterType.set(null);
|
||||||
|
} else {
|
||||||
|
this.activeFilterType.set('lista_completa');
|
||||||
|
}
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearValidati(event?: Event) {
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
this.showValidati.set(false);
|
||||||
|
this.activeFilterType.set(null);
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleNonValidati() {
|
||||||
|
this.showNonValidati.update(v => !v);
|
||||||
|
this.showValidati.set(false);
|
||||||
|
if (!this.showNonValidati()) {
|
||||||
|
this.activeFilterType.set(null);
|
||||||
|
} else {
|
||||||
|
this.activeFilterType.set('lista_completa');
|
||||||
|
}
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearNonValidati(event?: Event) {
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
this.showNonValidati.set(false);
|
||||||
|
this.activeFilterType.set(null);
|
||||||
|
this.limit.set(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearListaCompleta(event?: Event) {
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
this.showValidati.set(false);
|
||||||
|
this.showNonValidati.set(false);
|
||||||
|
this.activeFilterType.set(null);
|
||||||
|
this.limit.set(10);
|
||||||
|
}
|
||||||
|
|
||||||
toggleSuggeriti() {
|
toggleSuggeriti() {
|
||||||
this.showSuggeriti.update(v => !v);
|
this.showSuggeriti.update(v => !v);
|
||||||
|
this.activeFilterType.set(null);
|
||||||
this.limit.set(30);
|
this.limit.set(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -996,6 +1327,42 @@ export class HomePage implements OnDestroy {
|
|||||||
handleScannedData(data: string) {
|
handleScannedData(data: string) {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
|
if (data.includes('playlist-uid=')) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(data);
|
||||||
|
const uid = urlObj.searchParams.get('playlist-uid');
|
||||||
|
const pid = urlObj.searchParams.get('playlist-id');
|
||||||
|
if (uid) {
|
||||||
|
this.handleRemotePlaylistImport(uid, pid || undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const uidMatch = data.match(/[?&]playlist-uid=([^&]+)/);
|
||||||
|
const pidMatch = data.match(/[?&]playlist-id=([^&]+)/);
|
||||||
|
if (uidMatch && uidMatch[1]) {
|
||||||
|
this.handleRemotePlaylistImport(uidMatch[1], pidMatch ? pidMatch[1] : undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.includes('restore-uid=')) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(data);
|
||||||
|
const uid = urlObj.searchParams.get('restore-uid');
|
||||||
|
if (uid) {
|
||||||
|
this.handleRemoteRestore(uid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const uidMatch = data.match(/[?&]restore-uid=([^&]+)/);
|
||||||
|
if (uidMatch && uidMatch[1]) {
|
||||||
|
this.handleRemoteRestore(uidMatch[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (data.includes('import=')) {
|
if (data.includes('import=')) {
|
||||||
try {
|
try {
|
||||||
let base64 = '';
|
let base64 = '';
|
||||||
@@ -1115,6 +1482,11 @@ export class HomePage implements OnDestroy {
|
|||||||
return !!id && id.startsWith('comunita_');
|
return !!id && id.startsWith('comunita_');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isRemotePlaylist(): boolean {
|
||||||
|
const id = this.playlistService.activePlaylistId();
|
||||||
|
return !!id && id.startsWith('remote_');
|
||||||
|
}
|
||||||
|
|
||||||
isActivePlaylistSaved(): boolean {
|
isActivePlaylistSaved(): boolean {
|
||||||
const id = this.playlistService.activePlaylistId();
|
const id = this.playlistService.activePlaylistId();
|
||||||
if (!id) return false;
|
if (!id) return false;
|
||||||
@@ -1164,16 +1536,22 @@ export class HomePage implements OnDestroy {
|
|||||||
const name = this.playlistService.activeListName();
|
const name = this.playlistService.activeListName();
|
||||||
if (!id || !name) return;
|
if (!id || !name) return;
|
||||||
|
|
||||||
|
const isRemote = this.isRemotePlaylist();
|
||||||
|
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Elimina Playlist',
|
header: isRemote ? 'Rimuovi Playlist' : 'Elimina Playlist',
|
||||||
message: `Vuoi davvero eliminare "${name}"?`,
|
message: isRemote ? `Vuoi davvero rimuovere la playlist "${name}"?` : `Vuoi davvero eliminare "${name}"?`,
|
||||||
buttons: [
|
buttons: [
|
||||||
{ text: 'Annulla', role: 'cancel' },
|
{ text: 'Annulla', role: 'cancel' },
|
||||||
{
|
{
|
||||||
text: 'Elimina',
|
text: isRemote ? 'Rimuovi' : 'Elimina',
|
||||||
role: 'destructive',
|
role: 'destructive',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
this.playlistService.deletePlaylist(id);
|
if (isRemote) {
|
||||||
|
this.playlistService.clearRemotePlaylist();
|
||||||
|
} else {
|
||||||
|
this.playlistService.deletePlaylist(id);
|
||||||
|
}
|
||||||
this.clearSpecialList();
|
this.clearSpecialList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1203,6 +1581,31 @@ export class HomePage implements OnDestroy {
|
|||||||
this.playlistService.activeListName.set(name);
|
this.playlistService.activeListName.set(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleSongSelection(id: string) {
|
||||||
|
if (!this.playlistService.selectionMode()) {
|
||||||
|
const activeIds = this.playlistService.activeListIds();
|
||||||
|
const activeName = this.playlistService.activeListName();
|
||||||
|
|
||||||
|
if (activeIds.length > 0) {
|
||||||
|
// Se selezioniamo i numerini di una playlist attiva, non andiamo in modifica di quella playlist
|
||||||
|
// ma avviamo la selezione partendo da questa lista per creare una nuova playlist.
|
||||||
|
this.playlistService.activePlaylistId.set(null);
|
||||||
|
|
||||||
|
const clickedCanto = this.findCanto(id);
|
||||||
|
if (clickedCanto) {
|
||||||
|
this.playlistService.selectedIds.set(new Set([clickedCanto.id]));
|
||||||
|
this.reorderList.set([clickedCanto]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playlistService.selectionMode.set(true);
|
||||||
|
this.isAddingSongs.set(true); // Rimaniamo sulla lista filtrata per selezionare altri canti
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playlistService.toggleSongSelection(id);
|
||||||
|
}
|
||||||
|
|
||||||
drop(event: CdkDragDrop<any[]>) {
|
drop(event: CdkDragDrop<any[]>) {
|
||||||
const arr = [...this.reorderList()];
|
const arr = [...this.reorderList()];
|
||||||
moveItemInArray(arr, event.previousIndex, event.currentIndex);
|
moveItemInArray(arr, event.previousIndex, event.currentIndex);
|
||||||
@@ -1233,6 +1636,7 @@ export class HomePage implements OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
this.comunitaService.isFilterActive.update(v => !v);
|
this.comunitaService.isFilterActive.update(v => !v);
|
||||||
}
|
}
|
||||||
|
this.activeFilterType.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
editComunitaCode(event: Event) {
|
editComunitaCode(event: Event) {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiAuthInterceptor implements HttpInterceptor {
|
||||||
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
if (req.url.includes('api.canticristiani.it')) {
|
||||||
|
const authHeader = 'Basic ' + btoa(`${environment.apiAuthUser}:${environment.apiAuthPass}`);
|
||||||
|
const authReq = req.clone({
|
||||||
|
setHeaders: {
|
||||||
|
Authorization: authHeader
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return next.handle(authReq);
|
||||||
|
}
|
||||||
|
return next.handle(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -156,9 +156,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Camera Head Gestures Toggle in Portrait -->
|
<!-- Camera Head Gestures Toggle in Portrait -->
|
||||||
<ion-button fill="clear" size="small" (click)="toggleCameraNavigation()" [color]="enableCameraNavigation() ? 'success' : 'secondary'" *ngIf="settingsService.enableVisualAutoscroll()">
|
<div class="group" *ngIf="settingsService.enableVisualAutoscroll()">
|
||||||
<ion-icon slot="icon-only" [name]="enableCameraNavigation() ? 'videocam' : 'videocam-off-outline'"></ion-icon>
|
<ion-button fill="clear" size="small" (click)="toggleCameraNavigation()" [color]="enableCameraNavigation() ? 'success' : 'secondary'" style="margin: 0;">
|
||||||
</ion-button>
|
<ion-icon slot="icon-only" [name]="enableCameraNavigation() ? 'videocam' : 'videocam-off-outline'"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
<div class="camera-indicator" *ngIf="enableCameraNavigation()" [class.tilted]="faceDetector.isTilted()">
|
||||||
|
<ion-icon name="person-outline"
|
||||||
|
[style.transform]="'rotate(' + (-faceDetector.currentTiltAngle()) + 'deg)'"></ion-icon>
|
||||||
|
<span class="val">{{ faceDetector.currentTiltAngle() }}°</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Transposition (Only in chords mode) -->
|
<!-- Transposition (Only in chords mode) -->
|
||||||
@@ -208,12 +215,5 @@
|
|||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-footer>
|
</ion-footer>
|
||||||
|
|
||||||
<!-- Floating Camera Preview for Head Navigation -->
|
<!-- Hidden Camera Video for Head Navigation (needed for face detection API to run) -->
|
||||||
<div class="camera-preview-floating glass" *ngIf="enableCameraNavigation()">
|
<video *ngIf="enableCameraNavigation()" id="face-preview-video" muted playsinline autoplay style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none;"></video>
|
||||||
<video id="face-preview-video" muted playsinline autoplay></video>
|
|
||||||
<div class="camera-preview-overlay">
|
|
||||||
<div class="angle-indicator" [class.tilted]="faceDetector.isTilted()">
|
|
||||||
{{ faceDetector.currentTiltAngle() }}°
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -257,6 +257,28 @@
|
|||||||
|
|
||||||
ion-icon { font-size: 0.9rem; }
|
ion-icon { font-size: 0.9rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.camera-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ion-color-secondary);
|
||||||
|
padding: 0 4px;
|
||||||
|
transition: color 0.15s ease-out;
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
transition: transform 0.15s ease-out;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tilted {
|
||||||
|
color: var(--ion-color-success, #2ed573);
|
||||||
|
text-shadow: 0 0 5px rgba(46, 213, 115, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transcript area
|
// Transcript area
|
||||||
@@ -741,16 +763,17 @@ ion-content.full-screen-content {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 60px;
|
bottom: 60px;
|
||||||
right: 15px;
|
right: 15px;
|
||||||
width: 90px;
|
padding: 8px 14px;
|
||||||
height: 120px;
|
border-radius: 20px;
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.4);
|
border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.4);
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
background: #000;
|
justify-content: center;
|
||||||
|
background: rgba(18, 18, 18, 0.85);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
@media (orientation: landscape) {
|
||||||
bottom: 15px;
|
bottom: 15px;
|
||||||
@@ -758,27 +781,24 @@ ion-content.full-screen-content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
video {
|
video {
|
||||||
width: 100%;
|
position: absolute;
|
||||||
height: 100%;
|
width: 1px;
|
||||||
object-fit: cover;
|
height: 1px;
|
||||||
transform: scaleX(-1); // Mirror camera preview
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.camera-preview-overlay {
|
.camera-preview-overlay {
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.6);
|
|
||||||
padding: 2px 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.angle-indicator {
|
.angle-indicator {
|
||||||
font-size: 0.75rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
&.tilted {
|
&.tilted {
|
||||||
color: #2ed573;
|
color: #2ed573;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit, OnDestroy, signal, computed, effect, inject, ElementRef, AfterViewInit } from '@angular/core';
|
import { Component, OnInit, OnDestroy, signal, computed, effect, inject, ElementRef, AfterViewInit, untracked } from '@angular/core';
|
||||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { AlertController, ToastController, GestureController } from '@ionic/angular';
|
import { AlertController, ToastController, GestureController } from '@ionic/angular';
|
||||||
@@ -177,19 +177,40 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (!found) {
|
if (!found) {
|
||||||
found = myCantiList.find(c => c.id === id);
|
found = myCantiList.find(c => c.id === id);
|
||||||
}
|
}
|
||||||
|
if (!found) {
|
||||||
|
found = this.playlistService.remoteCustomSongs().find(c => c.id === id || String(c.id_canti) === id);
|
||||||
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
found = comunitaCantiPers.find(c => c.id === id);
|
found = comunitaCantiPers.find(c => c.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found) {
|
if (found) {
|
||||||
const current = this.canto();
|
const current = untracked(() => this.canto());
|
||||||
// Avoid duplicate triggers for the same song
|
// Avoid duplicate triggers for the same song, but update if song content changed
|
||||||
if (!current || current.id !== found.id) {
|
const contentChanged = !current ||
|
||||||
|
current.id !== found.id ||
|
||||||
|
current.titolo !== found.titolo ||
|
||||||
|
current.autore !== found.autore ||
|
||||||
|
current.link_youtube !== found.link_youtube ||
|
||||||
|
current.testo !== found.testo ||
|
||||||
|
current.accordi !== found.accordi ||
|
||||||
|
JSON.stringify(current.id_momenti) !== JSON.stringify(found.id_momenti);
|
||||||
|
|
||||||
|
if (contentChanged) {
|
||||||
this.logPreviousSongTime();
|
this.logPreviousSongTime();
|
||||||
this.canto.set(found);
|
this.canto.set(found);
|
||||||
|
this.currentLineIndex.set(0);
|
||||||
|
this.lastScrollBlock.set('start');
|
||||||
this.songStartTime = Date.now();
|
this.songStartTime = Date.now();
|
||||||
this.cantiService.getStorage()?.set('last_song_id', id);
|
this.cantiService.getStorage()?.set('last_song_id', id);
|
||||||
this.channel.postMessage({ type: 'SYNC_CANTO', id: found.id });
|
this.channel.postMessage({ type: 'SYNC_CANTO', id: found.id });
|
||||||
|
this.channel.postMessage({ type: 'SYNC_INDEX', index: 0 });
|
||||||
|
setTimeout(() => {
|
||||||
|
const scrollEl = this.el.nativeElement.querySelector('.lyrics-container');
|
||||||
|
if (scrollEl) {
|
||||||
|
scrollEl.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,15 +224,27 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
const activePlaylistId = this.playlistService.activePlaylistId();
|
const activePlaylistId = this.playlistService.activePlaylistId();
|
||||||
let playlistSongSetting: any = null;
|
let playlistSongSetting: any = null;
|
||||||
if (activePlaylistId) {
|
if (activePlaylistId) {
|
||||||
const pl = this.playlistService.playlists().find(p => p.id === activePlaylistId);
|
if (activePlaylistId.startsWith('remote_')) {
|
||||||
if (pl && pl.songSettings && pl.songSettings[c.id]) {
|
const pl = this.playlistService.remotePlaylist();
|
||||||
playlistSongSetting = pl.songSettings[c.id];
|
if (pl && pl.songSettings && pl.songSettings[c.id]) {
|
||||||
|
playlistSongSetting = pl.songSettings[c.id];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pl = this.playlistService.playlists().find(p => p.id === activePlaylistId);
|
||||||
|
if (pl && pl.songSettings && pl.songSettings[c.id]) {
|
||||||
|
playlistSongSetting = pl.songSettings[c.id];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playlistSongSetting) {
|
if (playlistSongSetting) {
|
||||||
this.transposeAmount.set(playlistSongSetting.tonalita !== undefined ? playlistSongSetting.tonalita : 0);
|
this.transposeAmount.set(playlistSongSetting.tonalita !== undefined ? playlistSongSetting.tonalita : 0);
|
||||||
this.autoscrollSpeed.set(playlistSongSetting.speed !== undefined ? playlistSongSetting.speed : 2);
|
this.autoscrollSpeed.set(playlistSongSetting.speed !== undefined ? playlistSongSetting.speed : 2);
|
||||||
|
if (playlistSongSetting.zoom !== undefined) {
|
||||||
|
this.fontSize.set(playlistSongSetting.zoom);
|
||||||
|
} else {
|
||||||
|
this.fontSize.set(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();
|
||||||
const songSetting = settings.find(s => s.id_canti === c.id_canti || s.id_canti === Number(c.id));
|
const songSetting = settings.find(s => s.id_canti === c.id_canti || s.id_canti === Number(c.id));
|
||||||
@@ -322,6 +355,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
onTouchEnd() {
|
onTouchEnd() {
|
||||||
this.initialPinchDistance = null;
|
this.initialPinchDistance = null;
|
||||||
|
this.updatePlaylistSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDistance(t1: Touch, t2: Touch): number {
|
private getDistance(t1: Touch, t2: Touch): number {
|
||||||
@@ -334,9 +368,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.transposeAmount.set(0);
|
this.transposeAmount.set(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
transposeUp() {
|
private updatePlaylistSettings() {
|
||||||
this.transposeAmount.update(v => v + 1);
|
|
||||||
|
|
||||||
const c = this.canto();
|
const c = this.canto();
|
||||||
if (c) {
|
if (c) {
|
||||||
const dbSpeed = this.autoscrollSpeed() * 100;
|
const dbSpeed = this.autoscrollSpeed() * 100;
|
||||||
@@ -344,30 +376,35 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
const activePlaylistId = this.playlistService.activePlaylistId();
|
const activePlaylistId = this.playlistService.activePlaylistId();
|
||||||
if (activePlaylistId) {
|
if (activePlaylistId) {
|
||||||
this.playlistService.updatePlaylistSongSettings(activePlaylistId, c.id, this.transposeAmount(), this.autoscrollSpeed());
|
if (activePlaylistId.startsWith('remote_')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.playlistService.updatePlaylistSongSettings(
|
||||||
|
activePlaylistId,
|
||||||
|
c.id,
|
||||||
|
this.transposeAmount(),
|
||||||
|
this.autoscrollSpeed(),
|
||||||
|
this.fontSize()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transposeUp() {
|
||||||
|
this.transposeAmount.update(v => v + 1);
|
||||||
|
this.updatePlaylistSettings();
|
||||||
|
}
|
||||||
|
|
||||||
transposeDown() {
|
transposeDown() {
|
||||||
this.transposeAmount.update(v => v - 1);
|
this.transposeAmount.update(v => v - 1);
|
||||||
|
this.updatePlaylistSettings();
|
||||||
const c = this.canto();
|
|
||||||
if (c) {
|
|
||||||
const dbSpeed = this.autoscrollSpeed() * 100;
|
|
||||||
this.statsService.updateSongSettings(c.id_canti, this.transposeAmount(), dbSpeed);
|
|
||||||
|
|
||||||
const activePlaylistId = this.playlistService.activePlaylistId();
|
|
||||||
if (activePlaylistId) {
|
|
||||||
this.playlistService.updatePlaylistSongSettings(activePlaylistId, c.id, this.transposeAmount(), this.autoscrollSpeed());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
zoomIn() {
|
zoomIn() {
|
||||||
if (this.fontSize() < this.MAX_FONT) {
|
if (this.fontSize() < this.MAX_FONT) {
|
||||||
this.fontSize.update(v => Math.min(v + this.FONT_STEP, this.MAX_FONT));
|
this.fontSize.update(v => Math.min(v + this.FONT_STEP, this.MAX_FONT));
|
||||||
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
||||||
|
this.updatePlaylistSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,6 +412,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
if (this.fontSize() > this.MIN_FONT) {
|
if (this.fontSize() > this.MIN_FONT) {
|
||||||
this.fontSize.update(v => Math.max(v - this.FONT_STEP, this.MIN_FONT));
|
this.fontSize.update(v => Math.max(v - this.FONT_STEP, this.MIN_FONT));
|
||||||
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
|
||||||
|
this.updatePlaylistSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,30 +895,12 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
increaseAutoscrollSpeed() {
|
increaseAutoscrollSpeed() {
|
||||||
this.autoscrollSpeed.update(s => Math.min(10, s + 1));
|
this.autoscrollSpeed.update(s => Math.min(10, s + 1));
|
||||||
const c = this.canto();
|
this.updatePlaylistSettings();
|
||||||
if (c) {
|
|
||||||
const dbSpeed = this.autoscrollSpeed() * 100;
|
|
||||||
this.statsService.updateSongSettings(c.id_canti, this.transposeAmount(), dbSpeed);
|
|
||||||
|
|
||||||
const activePlaylistId = this.playlistService.activePlaylistId();
|
|
||||||
if (activePlaylistId) {
|
|
||||||
this.playlistService.updatePlaylistSongSettings(activePlaylistId, c.id, this.transposeAmount(), this.autoscrollSpeed());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
decreaseAutoscrollSpeed() {
|
decreaseAutoscrollSpeed() {
|
||||||
this.autoscrollSpeed.update(s => Math.max(1, s - 1));
|
this.autoscrollSpeed.update(s => Math.max(1, s - 1));
|
||||||
const c = this.canto();
|
this.updatePlaylistSettings();
|
||||||
if (c) {
|
|
||||||
const dbSpeed = this.autoscrollSpeed() * 100;
|
|
||||||
this.statsService.updateSongSettings(c.id_canti, this.transposeAmount(), dbSpeed);
|
|
||||||
|
|
||||||
const activePlaylistId = this.playlistService.activePlaylistId();
|
|
||||||
if (activePlaylistId) {
|
|
||||||
this.playlistService.updatePlaylistSongSettings(activePlaylistId, c.id, this.transposeAmount(), this.autoscrollSpeed());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private logPreviousSongTime() {
|
private logPreviousSongTime() {
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { IonicModule, ToastController, IonTextarea, PopoverController, NavContro
|
|||||||
import { createWorker } from 'tesseract.js';
|
import { createWorker } from 'tesseract.js';
|
||||||
import { CantiService } from '../../services/canti.service';
|
import { CantiService } from '../../services/canti.service';
|
||||||
import { MyCantiService } from '../../services/my-canti.service';
|
import { MyCantiService } from '../../services/my-canti.service';
|
||||||
|
import { PlaylistService } from '../../services/playlist.service';
|
||||||
import { ThemeService } from '../../services/theme.service';
|
import { ThemeService } from '../../services/theme.service';
|
||||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
import { ActivatedRoute, RouterModule, Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-propose-canto',
|
selector: 'app-propose-canto',
|
||||||
@@ -21,9 +22,11 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
|
|
||||||
public cantiService = inject(CantiService);
|
public cantiService = inject(CantiService);
|
||||||
private myCantiService = inject(MyCantiService);
|
private myCantiService = inject(MyCantiService);
|
||||||
|
private playlistService = inject(PlaylistService);
|
||||||
private navCtrl = inject(NavController);
|
private navCtrl = inject(NavController);
|
||||||
public themeService = inject(ThemeService);
|
public themeService = inject(ThemeService);
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
|
private router = inject(Router);
|
||||||
|
|
||||||
title: string = '';
|
title: string = '';
|
||||||
author: string = '';
|
author: string = '';
|
||||||
@@ -112,10 +115,11 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
const editId = params['editId'];
|
const editId = params['editId'];
|
||||||
if (editId) {
|
if (editId) {
|
||||||
this.editId = editId;
|
this.editId = editId;
|
||||||
// Find the song in standard canti or personal canti list
|
// Find the song in standard canti, personal canti, or remote custom canti list
|
||||||
const song = [
|
const song = [
|
||||||
...this.cantiService.canti(),
|
...this.cantiService.canti(),
|
||||||
...this.myCantiService.myCanti()
|
...this.myCantiService.myCanti(),
|
||||||
|
...this.playlistService.remoteCustomSongs()
|
||||||
].find(c => c.id === editId);
|
].find(c => c.id === editId);
|
||||||
|
|
||||||
if (song) {
|
if (song) {
|
||||||
@@ -128,8 +132,8 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
const litIds = this.cantiService.indiceLiturgico().map(m => m.id);
|
const litIds = this.cantiService.indiceLiturgico().map(m => m.id);
|
||||||
const temIds = this.cantiService.indiceTematico().map(m => m.id);
|
const temIds = this.cantiService.indiceTematico().map(m => m.id);
|
||||||
|
|
||||||
this.selectedLiturgico = song.id_momenti?.filter(id => litIds.includes(id)) || [];
|
this.selectedLiturgico = song.id_momenti?.filter((id: number) => litIds.includes(id)) || [];
|
||||||
this.selectedTematico = song.id_momenti?.filter(id => temIds.includes(id)) || [];
|
this.selectedTematico = song.id_momenti?.filter((id: number) => temIds.includes(id)) || [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -749,16 +753,62 @@ export class ProposeCantoPage implements OnInit {
|
|||||||
// Combine lit and tematico for id_momenti
|
// Combine lit and tematico for id_momenti
|
||||||
const id_momenti = [...this.selectedLiturgico, ...this.selectedTematico];
|
const id_momenti = [...this.selectedLiturgico, ...this.selectedTematico];
|
||||||
|
|
||||||
await this.myCantiService.saveCanto({
|
const activePlaylistId = this.playlistService.activePlaylistId();
|
||||||
id: this.editId || undefined,
|
const isRemotePlaylist = activePlaylistId && activePlaylistId.startsWith('remote_');
|
||||||
titolo: this.title,
|
|
||||||
autore: this.author,
|
|
||||||
link_youtube: this.youtubeLink,
|
|
||||||
testo: this.content,
|
|
||||||
accordi: this.content, // Save to both fields for compatibility
|
|
||||||
id_momenti: id_momenti
|
|
||||||
});
|
|
||||||
|
|
||||||
this.navCtrl.back();
|
if (isRemotePlaylist) {
|
||||||
|
// 1. Clone the song (generate a brand new my_... ID)
|
||||||
|
const savedCanto = await this.myCantiService.saveCanto({
|
||||||
|
titolo: this.title,
|
||||||
|
autore: this.author,
|
||||||
|
link_youtube: this.youtubeLink,
|
||||||
|
testo: this.content,
|
||||||
|
accordi: this.content,
|
||||||
|
id_momenti: id_momenti
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Clone/convert remote playlist to local personal playlist
|
||||||
|
const remotePl = this.playlistService.remotePlaylist();
|
||||||
|
if (remotePl) {
|
||||||
|
const originalIds = remotePl.ids || [];
|
||||||
|
const updatedIds = originalIds.map((id: string) => id === this.editId ? savedCanto.id : id);
|
||||||
|
|
||||||
|
const songSettings = { ...(remotePl.songSettings || {}) };
|
||||||
|
if (this.editId && songSettings[this.editId]) {
|
||||||
|
songSettings[savedCanto.id] = { ...songSettings[this.editId] };
|
||||||
|
delete songSettings[this.editId];
|
||||||
|
}
|
||||||
|
|
||||||
|
const localName = remotePl.name.replace('[Remote] ', '');
|
||||||
|
|
||||||
|
// Force save as a new local playlist
|
||||||
|
this.playlistService.activePlaylistId.set(null);
|
||||||
|
await this.playlistService.savePlaylist(localName, updatedIds, songSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to the player with the new cloned song ID immediately
|
||||||
|
this.router.navigate(['/player'], { queryParams: { id: savedCanto.id }, replaceUrl: true });
|
||||||
|
} else {
|
||||||
|
// Standard path
|
||||||
|
const savedCanto = await this.myCantiService.saveCanto({
|
||||||
|
id: this.editId || undefined,
|
||||||
|
titolo: this.title,
|
||||||
|
autore: this.author,
|
||||||
|
link_youtube: this.youtubeLink,
|
||||||
|
testo: this.content,
|
||||||
|
accordi: this.content, // Save to both fields for compatibility
|
||||||
|
id_momenti: id_momenti
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.editId && !this.editId.startsWith('my_') && savedCanto && savedCanto.id) {
|
||||||
|
await this.playlistService.replaceSongIdInPlaylists(this.editId, savedCanto.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editId && savedCanto && savedCanto.id && this.editId !== savedCanto.id) {
|
||||||
|
this.router.navigate(['/player'], { queryParams: { id: savedCanto.id }, replaceUrl: true });
|
||||||
|
} else {
|
||||||
|
this.navCtrl.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,47 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Identità Utente e Ripristino -->
|
||||||
|
<div class="settings-group glass ion-margin-bottom" *ngIf="settingsService.comunitaEnabled() || settingsService.showEditor()">
|
||||||
|
<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;">
|
||||||
|
Identità Utente e Ripristino
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ion-padding-horizontal ion-padding-bottom">
|
||||||
|
<p class="settings-item-subtitle outfit-font" style="margin: 0 0 16px 0; font-size: 0.85rem; line-height: 1.45; opacity: 0.85; color: var(--ion-text-color);">
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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="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-family: monospace; font-size: 0.78rem; opacity: 0.9; color: var(--ion-text-color);" class="select-all">{{ settingsService.userUuid() }}</div>
|
||||||
|
</div>
|
||||||
|
<ion-button fill="clear" size="small" (click)="showBackupQrCode()" color="secondary" style="margin: 0; --padding-start: 4px; --padding-end: 4px;">
|
||||||
|
<ion-icon name="qr-code-outline" slot="icon-only" style="font-size: 1.35rem;"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||||
|
<ion-button expand="block" fill="outline" color="secondary" (click)="scanRestoreQrCode()" class="outfit-font" style="--border-radius: 10px; font-size: 0.8rem; font-weight: bold; text-transform: none; margin: 0; height: 38px;">
|
||||||
|
<ion-icon name="scan-outline" slot="start"></ion-icon>
|
||||||
|
Scansiona QR
|
||||||
|
</ion-button>
|
||||||
|
<ion-button expand="block" fill="outline" color="secondary" (click)="manualRestore()" class="outfit-font" style="--border-radius: 10px; font-size: 0.8rem; font-weight: bold; text-transform: none; margin: 0; height: 38px;">
|
||||||
|
<ion-icon name="keypad-outline" slot="start"></ion-icon>
|
||||||
|
Inserisci Codice
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
Salva Backup sul Server
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Invio Dati Statistici -->
|
<!-- Invio Dati Statistici -->
|
||||||
<div class="settings-group glass ion-margin-bottom">
|
<div class="settings-group glass ion-margin-bottom">
|
||||||
<ion-item class="transparent-item" lines="none">
|
<ion-item class="transparent-item" lines="none">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { SettingsService } from '../../services/settings.service';
|
|||||||
import { CantiService } from '../../services/canti.service';
|
import { CantiService } from '../../services/canti.service';
|
||||||
import { ConnectivityService } from '../../services/connectivity.service';
|
import { ConnectivityService } from '../../services/connectivity.service';
|
||||||
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
|
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
|
||||||
import { ToastController, ModalController, AlertController } from '@ionic/angular';
|
import { ToastController, ModalController, AlertController, LoadingController } from '@ionic/angular';
|
||||||
import { VERSION } from '../../version';
|
import { VERSION } from '../../version';
|
||||||
import { PlaylistService } from '../../services/playlist.service';
|
import { PlaylistService } from '../../services/playlist.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -15,6 +15,8 @@ import { CantiLettureService } from '../../services/canti-letture.service';
|
|||||||
import { ComunitaService } from '../../services/comunita.service';
|
import { ComunitaService } from '../../services/comunita.service';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { showFullscreenUpdateOverlay } from '../../app.component';
|
import { showFullscreenUpdateOverlay } from '../../app.component';
|
||||||
|
import { QrScannerComponent } from '../../components/qr-scanner/qr-scanner.component';
|
||||||
|
import { IdentityQrModalComponent } from '../../components/identity-qr-modal/identity-qr-modal.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings',
|
selector: 'app-settings',
|
||||||
@@ -35,6 +37,7 @@ export class SettingsPage {
|
|||||||
private toastCtrl = inject(ToastController);
|
private toastCtrl = inject(ToastController);
|
||||||
private modalCtrl = inject(ModalController);
|
private modalCtrl = inject(ModalController);
|
||||||
private alertCtrl = inject(AlertController);
|
private alertCtrl = inject(AlertController);
|
||||||
|
private loadingCtrl = inject(LoadingController);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
|
|
||||||
public version = VERSION;
|
public version = VERSION;
|
||||||
@@ -52,6 +55,209 @@ export class SettingsPage {
|
|||||||
this.showIosInstructions = !this.showIosInstructions;
|
this.showIosInstructions = !this.showIosInstructions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async showBackupQrCode() {
|
||||||
|
const modal = await this.modalCtrl.create({
|
||||||
|
component: IdentityQrModalComponent,
|
||||||
|
componentProps: {
|
||||||
|
userUuid: this.settingsService.userUuid()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return await modal.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanRestoreQrCode() {
|
||||||
|
const modal = await this.modalCtrl.create({
|
||||||
|
component: QrScannerComponent
|
||||||
|
});
|
||||||
|
await modal.present();
|
||||||
|
|
||||||
|
const { data } = await modal.onWillDismiss();
|
||||||
|
if (data) {
|
||||||
|
this.confirmRestore(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async manualRestore() {
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
header: 'Ripristina con Codice',
|
||||||
|
message: 'Digita o incolla il tuo codice identificativo univoco.',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
name: 'code',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx',
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
],
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Annulla',
|
||||||
|
role: 'cancel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Conferma',
|
||||||
|
handler: (data) => {
|
||||||
|
if (data.code && data.code.trim()) {
|
||||||
|
this.confirmRestore(data.code.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await alert.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async confirmRestore(code: string) {
|
||||||
|
// Basic validation for UUID
|
||||||
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
|
if (!uuidRegex.test(code)) {
|
||||||
|
const errorAlert = await this.alertCtrl.create({
|
||||||
|
header: 'Codice Non Valido',
|
||||||
|
message: 'Il codice inserito non sembra essere un identificativo univoco valido. Riprova.',
|
||||||
|
buttons: ['OK']
|
||||||
|
});
|
||||||
|
await errorAlert.present();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
header: 'Ripristina Identità',
|
||||||
|
message: 'Sei sicuro di voler ripristinare questa identità? L\'ID attuale del dispositivo verrà sovrascritto permanentemente ed eventuali canti e playlist remoti associati al nuovo ID verranno scaricati.',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Annulla',
|
||||||
|
role: 'cancel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Ripristina',
|
||||||
|
role: 'destructive',
|
||||||
|
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 custom songs
|
||||||
|
const customSongs = remoteJson
|
||||||
|
.filter((item: any) => !item.momenti || !item.momenti.includes('Playlist'))
|
||||||
|
.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)) || []
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (customSongs.length > 0) {
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (playlists.length > 0) {
|
||||||
|
this.playlistService.playlists.set(playlists);
|
||||||
|
if (this.playlistService['_storage']) {
|
||||||
|
const key = this.playlistService.getPlaylistsStorageKey();
|
||||||
|
await this.playlistService['_storage'].set(key, playlists);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to restore backup data:', err);
|
||||||
|
} finally {
|
||||||
|
await loading.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.settingsService.setUserUuid(code);
|
||||||
|
const toast = await this.toastCtrl.create({
|
||||||
|
message: 'Identità e 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await alert.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncLocalDataToServer() {
|
||||||
|
const uid = this.settingsService.userUuid();
|
||||||
|
if (!uid) {
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
header: 'Errore',
|
||||||
|
message: 'Nessun identificativo utente (UID) trovato.',
|
||||||
|
buttons: ['OK']
|
||||||
|
});
|
||||||
|
await alert.present();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastLoading = await this.toastCtrl.create({
|
||||||
|
message: 'Invio backup in corso...',
|
||||||
|
duration: 1500,
|
||||||
|
color: 'secondary'
|
||||||
|
});
|
||||||
|
await toastLoading.present();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.playlistService.syncLocalDataToServer();
|
||||||
|
await toastLoading.dismiss();
|
||||||
|
|
||||||
|
const toastSuccess = await this.toastCtrl.create({
|
||||||
|
message: 'Backup sincronizzato con successo!',
|
||||||
|
duration: 3000,
|
||||||
|
color: 'success'
|
||||||
|
});
|
||||||
|
await toastSuccess.present();
|
||||||
|
} catch (err: any) {
|
||||||
|
try {
|
||||||
|
await toastLoading.dismiss();
|
||||||
|
} catch (e) {}
|
||||||
|
console.error('Failed to backup to server:', err);
|
||||||
|
const alertError = await this.alertCtrl.create({
|
||||||
|
header: 'Errore di sincronizzazione',
|
||||||
|
message: 'Impossibile inviare il backup al server. Controlla la connessione e riprova.',
|
||||||
|
buttons: ['OK']
|
||||||
|
});
|
||||||
|
await alertError.present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onModeChange(event: any) {
|
onModeChange(event: any) {
|
||||||
this.settingsService.setShowChordsDefault(event.detail.value === 'chords');
|
this.settingsService.setShowChordsDefault(event.detail.value === 'chords');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export class CantiLettureService {
|
|||||||
public suggestionsMap = signal<Map<number, number>>(new Map()); // id_canto -> peso
|
public suggestionsMap = signal<Map<number, number>>(new Map()); // id_canto -> peso
|
||||||
public suggestionsMomentsMap = signal<Map<number, string[]>>(new Map()); // id_canto -> moments[]
|
public suggestionsMomentsMap = signal<Map<number, string[]>>(new Map()); // id_canto -> moments[]
|
||||||
|
|
||||||
private JSON_URL = 'http://185.193.67.105:3000/cantiletture.json';
|
private JSON_URL = 'https://api.canticristiani.it/cantiletture.json';
|
||||||
private SECURE_JSON_URL = '/api/cantiletture.json';
|
private SECURE_JSON_URL = '/api/cantiletture.json';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export class CantiService {
|
|||||||
public momenti = signal<Indice[]>([]);
|
public momenti = signal<Indice[]>([]);
|
||||||
public loading = signal<boolean>(false);
|
public loading = signal<boolean>(false);
|
||||||
public progress = signal<number>(0);
|
public progress = signal<number>(0);
|
||||||
|
public firstLoadCompleted = signal<boolean>(false);
|
||||||
|
|
||||||
private API_URL = 'https://www.canticristiani.it/api/canti.json';
|
private API_URL = 'https://www.canticristiani.it/api/canti.json';
|
||||||
|
|
||||||
@@ -55,6 +56,9 @@ export class CantiService {
|
|||||||
const storage = await this.storage.create();
|
const storage = await this.storage.create();
|
||||||
this._storage = storage;
|
this._storage = storage;
|
||||||
await this.loadFromStorage();
|
await this.loadFromStorage();
|
||||||
|
if (this.canti() && this.canti().length > 0) {
|
||||||
|
this.firstLoadCompleted.set(true);
|
||||||
|
}
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,11 +136,13 @@ export class CantiService {
|
|||||||
}
|
}
|
||||||
this.progress.set(100);
|
this.progress.set(100);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
|
this.firstLoadCompleted.set(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Injectable, signal, inject } from '@angular/core';
|
import { Injectable, signal, inject, Injector } from '@angular/core';
|
||||||
import { Storage } from '@ionic/storage-angular';
|
import { Storage } from '@ionic/storage-angular';
|
||||||
import { Canto, CantiService } from './canti.service';
|
import { Canto, CantiService } from './canti.service';
|
||||||
import { ToastController } from '@ionic/angular';
|
import { ToastController } from '@ionic/angular';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
|
import { PlaylistService } from './playlist.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -11,6 +12,8 @@ export class MyCantiService {
|
|||||||
private storage = inject(Storage);
|
private storage = inject(Storage);
|
||||||
private cantiService = inject(CantiService);
|
private cantiService = inject(CantiService);
|
||||||
private toastController = inject(ToastController);
|
private toastController = inject(ToastController);
|
||||||
|
private injector = inject(Injector);
|
||||||
|
private playlistService!: PlaylistService;
|
||||||
|
|
||||||
private _storage: Storage | null = null;
|
private _storage: Storage | null = null;
|
||||||
public myCanti = signal<Canto[]>([]);
|
public myCanti = signal<Canto[]>([]);
|
||||||
@@ -56,9 +59,11 @@ export class MyCantiService {
|
|||||||
});
|
});
|
||||||
// Fallback if not found in list (should not happen normally)
|
// Fallback if not found in list (should not happen normally)
|
||||||
if (!updated.some(c => c.id === canto.id)) {
|
if (!updated.some(c => c.id === canto.id)) {
|
||||||
|
const numericId = canto.id ? Number(canto.id.replace('my_', '')) : NaN;
|
||||||
|
const idCanti = isNaN(numericId) ? Date.now() : numericId;
|
||||||
targetCanto = {
|
targetCanto = {
|
||||||
id: canto.id,
|
id: canto.id,
|
||||||
id_canti: canto.id_canti || Date.now(),
|
id_canti: canto.id_canti || idCanti,
|
||||||
titolo: canto.titolo || 'Senza Titolo',
|
titolo: canto.titolo || 'Senza Titolo',
|
||||||
testo: canto.testo || '',
|
testo: canto.testo || '',
|
||||||
accordi: canto.accordi,
|
accordi: canto.accordi,
|
||||||
@@ -86,6 +91,12 @@ export class MyCantiService {
|
|||||||
this.myCanti.set(updated);
|
this.myCanti.set(updated);
|
||||||
await this._storage?.set('my-canti', updated);
|
await this._storage?.set('my-canti', updated);
|
||||||
|
|
||||||
|
// Sincronizza automaticamente in background
|
||||||
|
if (!this.playlistService) {
|
||||||
|
this.playlistService = this.injector.get(PlaylistService);
|
||||||
|
}
|
||||||
|
this.playlistService.syncLocalDataToServer();
|
||||||
|
|
||||||
const toast = await this.toastController.create({
|
const toast = await this.toastController.create({
|
||||||
message: 'Canto salvato nei "Miei Canti"!',
|
message: 'Canto salvato nei "Miei Canti"!',
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
@@ -100,6 +111,12 @@ export class MyCantiService {
|
|||||||
const updated = this.myCanti().filter(c => c.id !== id);
|
const updated = this.myCanti().filter(c => c.id !== id);
|
||||||
this.myCanti.set(updated);
|
this.myCanti.set(updated);
|
||||||
await this._storage?.set('my-canti', updated);
|
await this._storage?.set('my-canti', updated);
|
||||||
|
|
||||||
|
// Sincronizza automaticamente in background
|
||||||
|
if (!this.playlistService) {
|
||||||
|
this.playlistService = this.injector.get(PlaylistService);
|
||||||
|
}
|
||||||
|
this.playlistService.syncLocalDataToServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendAllMyCanti() {
|
async sendAllMyCanti() {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Injectable, signal, inject, computed, effect } from '@angular/core';
|
import { Injectable, signal, inject, computed, effect, Injector } from '@angular/core';
|
||||||
import { Storage } from '@ionic/storage-angular';
|
import { Storage } from '@ionic/storage-angular';
|
||||||
import { Canto, CantiService } from './canti.service';
|
import { Canto, CantiService } from './canti.service';
|
||||||
import { ComunitaService } from './comunita.service';
|
import { ComunitaService } from './comunita.service';
|
||||||
import * as QRCode from 'qrcode';
|
import * as QRCode from 'qrcode';
|
||||||
import { ToastController } from '@ionic/angular';
|
import { ToastController, AlertController } from '@ionic/angular';
|
||||||
|
import { SettingsService } from './settings.service';
|
||||||
|
import { MyCantiService } from './my-canti.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -13,6 +15,10 @@ export class PlaylistService {
|
|||||||
private cantiService = inject(CantiService);
|
private cantiService = inject(CantiService);
|
||||||
private comunitaService = inject(ComunitaService);
|
private comunitaService = inject(ComunitaService);
|
||||||
private toastCtrl = inject(ToastController);
|
private toastCtrl = inject(ToastController);
|
||||||
|
private alertCtrl = inject(AlertController);
|
||||||
|
private settingsService = inject(SettingsService);
|
||||||
|
private injector = inject(Injector);
|
||||||
|
private myCantiService!: MyCantiService;
|
||||||
|
|
||||||
public selectionMode = signal<boolean>(false);
|
public selectionMode = signal<boolean>(false);
|
||||||
public selectedIds = signal<Set<string>>(new Set());
|
public selectedIds = signal<Set<string>>(new Set());
|
||||||
@@ -24,6 +30,9 @@ export class PlaylistService {
|
|||||||
public activeListName = signal<string | null>(null);
|
public activeListName = signal<string | null>(null);
|
||||||
public activePlaylistId = signal<string | null>(null);
|
public activePlaylistId = signal<string | null>(null);
|
||||||
|
|
||||||
|
public remotePlaylist = signal<any | null>(null);
|
||||||
|
public remoteCustomSongs = signal<any[]>([]);
|
||||||
|
|
||||||
private _storage: Storage | null = null;
|
private _storage: Storage | null = null;
|
||||||
private initPromise!: Promise<void>;
|
private initPromise!: Promise<void>;
|
||||||
|
|
||||||
@@ -41,14 +50,20 @@ export class PlaylistService {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Merged list: personal playlists + community scalette
|
// Merged list: personal playlists + community scalette + remote playlist
|
||||||
public allPlaylists = computed(() => {
|
public allPlaylists = computed(() => {
|
||||||
const community = this.comunitaPlaylists();
|
const community = this.comunitaPlaylists();
|
||||||
const personal = this.playlists();
|
const personal = this.playlists();
|
||||||
|
const remote = this.remotePlaylist();
|
||||||
|
|
||||||
|
let list = [...personal.map(p => ({ ...p, isComunita: false, isRemote: false }))];
|
||||||
if (community.length > 0) {
|
if (community.length > 0) {
|
||||||
return [...community, ...personal.map(p => ({ ...p, isComunita: false }))];
|
list = [...community, ...list];
|
||||||
}
|
}
|
||||||
return personal.map(p => ({ ...p, isComunita: false }));
|
if (remote) {
|
||||||
|
list = [{ ...remote, isComunita: false, isRemote: true }, ...list];
|
||||||
|
}
|
||||||
|
return list;
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -65,7 +80,7 @@ export class PlaylistService {
|
|||||||
}, { allowSignalWrites: true });
|
}, { allowSignalWrites: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPlaylistsStorageKey(): string {
|
getPlaylistsStorageKey(): string {
|
||||||
const code = this.comunitaService.comunitaCode();
|
const code = this.comunitaService.comunitaCode();
|
||||||
const isCommunityActive = this.comunitaService.isFilterActive();
|
const isCommunityActive = this.comunitaService.isFilterActive();
|
||||||
if (code && isCommunityActive) {
|
if (code && isCommunityActive) {
|
||||||
@@ -89,6 +104,78 @@ export class PlaylistService {
|
|||||||
const lastKey = `lastPlaylist_${key}`;
|
const lastKey = `lastPlaylist_${key}`;
|
||||||
const last = await this._storage.get(lastKey);
|
const last = await this._storage.get(lastKey);
|
||||||
this.lastPlaylist.set(last || null);
|
this.lastPlaylist.set(last || null);
|
||||||
|
|
||||||
|
// Carica la playlist remota persistita e i relativi canti personalizzati
|
||||||
|
const remotePl = await this._storage.get('remote_playlist');
|
||||||
|
if (remotePl) {
|
||||||
|
this.remotePlaylist.set(remotePl);
|
||||||
|
}
|
||||||
|
const remoteSongs = await this._storage.get('remote_custom_songs');
|
||||||
|
if (remoteSongs) {
|
||||||
|
this.remoteCustomSongs.set(remoteSongs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggiorna in background la playlist remota per sincronizzare eventuali modifiche
|
||||||
|
if (remotePl) {
|
||||||
|
this.refreshRemotePlaylist();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshRemotePlaylist() {
|
||||||
|
const remotePl = this.remotePlaylist();
|
||||||
|
if (!remotePl) return;
|
||||||
|
|
||||||
|
const parts = remotePl.id.split('_');
|
||||||
|
if (parts.length < 3) return;
|
||||||
|
const uid = parts[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.canticristiani.it/${uid}.json?cb=${Date.now()}`, { cache: 'no-store' });
|
||||||
|
if (!response.ok) return;
|
||||||
|
const remoteJson = await response.json();
|
||||||
|
if (Array.isArray(remoteJson)) {
|
||||||
|
const customSongs = remoteJson
|
||||||
|
.filter((item: any) => !item.momenti || !item.momenti.includes('Playlist'))
|
||||||
|
.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)) || []
|
||||||
|
}));
|
||||||
|
|
||||||
|
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: `remote_${uid}_${item.id_canti}`,
|
||||||
|
name: `[Remote] ${item.titolo}`,
|
||||||
|
ids: item.testo ? item.testo.split(',') : [],
|
||||||
|
songSettings: songSettings,
|
||||||
|
createdAt: new Date(),
|
||||||
|
isRemote: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedPl = playlists.find(p => p.id === remotePl.id);
|
||||||
|
if (updatedPl) {
|
||||||
|
this.remotePlaylist.set(updatedPl);
|
||||||
|
this.remoteCustomSongs.set(customSongs);
|
||||||
|
await this._storage?.set('remote_playlist', updatedPl);
|
||||||
|
await this._storage?.set('remote_custom_songs', customSongs);
|
||||||
|
console.log('[Remote-Sync] Remote playlist and custom songs updated successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[Remote-Sync] Failed to refresh remote playlist:', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSelectionMode() {
|
toggleSelectionMode() {
|
||||||
@@ -154,14 +241,46 @@ export class PlaylistService {
|
|||||||
this.selectedIds.set(new Set());
|
this.selectedIds.set(new Set());
|
||||||
this.selectionMode.set(false);
|
this.selectionMode.set(false);
|
||||||
this.activePlaylistId.set(newPlaylist.id);
|
this.activePlaylistId.set(newPlaylist.id);
|
||||||
|
|
||||||
|
// Sincronizza automaticamente in background
|
||||||
|
this.syncLocalDataToServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePlaylistSongSettings(playlistId: string, songId: string, tonalita: number, speed: number) {
|
async replaceSongIdInPlaylists(oldId: string, newId: string) {
|
||||||
|
await this.initPromise;
|
||||||
|
this.playlists.update(p => p.map(pl => {
|
||||||
|
if (pl.ids && pl.ids.includes(oldId)) {
|
||||||
|
const updatedIds = pl.ids.map((id: string) => id === oldId ? newId : id);
|
||||||
|
|
||||||
|
// Copia anche i parametri di tonalità/zoom/velocità della canzone se presenti
|
||||||
|
const songSettings = { ...(pl.songSettings || {}) };
|
||||||
|
if (songSettings[oldId]) {
|
||||||
|
songSettings[newId] = { ...songSettings[oldId] };
|
||||||
|
delete songSettings[oldId];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...pl, ids: updatedIds, songSettings };
|
||||||
|
}
|
||||||
|
return pl;
|
||||||
|
}));
|
||||||
|
|
||||||
|
const key = this.getPlaylistsStorageKey();
|
||||||
|
await this._storage?.set(key, this.playlists());
|
||||||
|
|
||||||
|
// Sincronizza automaticamente in background
|
||||||
|
this.syncLocalDataToServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePlaylistSongSettings(playlistId: string, songId: string, tonalita: number, speed: number, zoom?: number) {
|
||||||
await this.initPromise;
|
await this.initPromise;
|
||||||
this.playlists.update(p => p.map(pl => {
|
this.playlists.update(p => p.map(pl => {
|
||||||
if (pl.id === playlistId) {
|
if (pl.id === playlistId) {
|
||||||
const songSettings = { ...(pl.songSettings || {}) };
|
const songSettings = { ...(pl.songSettings || {}) };
|
||||||
songSettings[songId] = { tonalita, speed };
|
songSettings[songId] = {
|
||||||
|
tonalita,
|
||||||
|
speed,
|
||||||
|
zoom: zoom !== undefined ? zoom : songSettings[songId]?.zoom
|
||||||
|
};
|
||||||
return { ...pl, songSettings };
|
return { ...pl, songSettings };
|
||||||
}
|
}
|
||||||
return pl;
|
return pl;
|
||||||
@@ -170,6 +289,9 @@ export class PlaylistService {
|
|||||||
const key = this.getPlaylistsStorageKey();
|
const key = this.getPlaylistsStorageKey();
|
||||||
await this._storage?.set(key, this.playlists());
|
await this._storage?.set(key, this.playlists());
|
||||||
|
|
||||||
|
// Sincronizza automaticamente in background
|
||||||
|
this.syncLocalDataToServer();
|
||||||
|
|
||||||
// Also update lastPlaylist if it is the current one
|
// Also update lastPlaylist if it is the current one
|
||||||
const lastKey = `lastPlaylist_${key}`;
|
const lastKey = `lastPlaylist_${key}`;
|
||||||
const last = this.lastPlaylist();
|
const last = this.lastPlaylist();
|
||||||
@@ -182,9 +304,82 @@ export class PlaylistService {
|
|||||||
|
|
||||||
async deletePlaylist(id: string) {
|
async deletePlaylist(id: string) {
|
||||||
await this.initPromise;
|
await this.initPromise;
|
||||||
|
if (id.startsWith('remote_')) {
|
||||||
|
await this.clearRemotePlaylist();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const key = this.getPlaylistsStorageKey();
|
const key = this.getPlaylistsStorageKey();
|
||||||
this.playlists.update(p => p.filter(pl => pl.id !== id));
|
this.playlists.update(p => p.filter(pl => pl.id !== id));
|
||||||
await this._storage?.set(key, this.playlists());
|
await this._storage?.set(key, this.playlists());
|
||||||
|
|
||||||
|
// Sincronizza automaticamente in background
|
||||||
|
this.syncLocalDataToServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveRemotePlaylist(pl: any, customSongs: any[]) {
|
||||||
|
await this.initPromise;
|
||||||
|
this.remotePlaylist.set(pl);
|
||||||
|
this.remoteCustomSongs.set(customSongs);
|
||||||
|
await this._storage?.set('remote_playlist', pl);
|
||||||
|
await this._storage?.set('remote_custom_songs', customSongs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearRemotePlaylist() {
|
||||||
|
await this.initPromise;
|
||||||
|
this.remotePlaylist.set(null);
|
||||||
|
this.remoteCustomSongs.set([]);
|
||||||
|
await this._storage?.remove('remote_playlist');
|
||||||
|
await this._storage?.remove('remote_custom_songs');
|
||||||
|
|
||||||
|
// Sincronizza automaticamente in background
|
||||||
|
this.syncLocalDataToServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncLocalDataToServer() {
|
||||||
|
await this.initPromise;
|
||||||
|
const uid = this.settingsService.userUuid();
|
||||||
|
if (!uid) return;
|
||||||
|
|
||||||
|
if (!this.myCantiService) {
|
||||||
|
this.myCantiService = this.injector.get(MyCantiService);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const customSongs = this.myCantiService.myCanti().map(c => ({
|
||||||
|
id_canti: c.id_canti || Date.now(),
|
||||||
|
titolo: c.titolo || 'Senza Titolo',
|
||||||
|
momenti: c.id_momenti?.map(id => String(id)) || [],
|
||||||
|
periodi: [] as string[],
|
||||||
|
testo: c.testo || ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
const playlistSongs = this.playlists().map(pl => ({
|
||||||
|
id_canti: Number(pl.id) || Date.now(),
|
||||||
|
titolo: pl.name,
|
||||||
|
momenti: ['Playlist'],
|
||||||
|
periodi: pl.songSettings ? [JSON.stringify(pl.songSettings)] : [],
|
||||||
|
testo: pl.ids.join(',')
|
||||||
|
}));
|
||||||
|
|
||||||
|
const payload = [...customSongs, ...playlistSongs];
|
||||||
|
|
||||||
|
const response = await fetch('https://api.canticristiani.it/miei', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-user-uid': uid
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn(`[Auto-Sync] Server returned code: ${response.status}`);
|
||||||
|
} else {
|
||||||
|
console.log('[Auto-Sync] Local playlists and canti backed up successfully.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Auto-Sync] Auto-synchronization failed:', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateQR(ids: string[], name: string, songSettings?: any): Promise<string> {
|
async generateQR(ids: string[], name: string, songSettings?: any): Promise<string> {
|
||||||
@@ -262,8 +457,67 @@ export class PlaylistService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sharePlaylistQR(ids: string[], name: string, songSettings?: any) {
|
async sharePlaylistQR(ids: string[], name: string, songSettings?: any) {
|
||||||
const qrImage = await this.generateQR(ids, name, songSettings);
|
// Sincronizza istantaneamente sul server prima di generare/condividere la playlist
|
||||||
const shareLink = this.getShareLink(ids, name, songSettings);
|
await this.syncLocalDataToServer();
|
||||||
|
|
||||||
|
const uid = this.settingsService.userUuid();
|
||||||
|
const activeId = this.activePlaylistId() || Date.now().toString();
|
||||||
|
const isRemote = activeId.startsWith('remote_');
|
||||||
|
|
||||||
|
const buttons: any[] = [
|
||||||
|
{
|
||||||
|
text: isRemote ? 'Condividi (Sola Lettura)' : 'Sola Lettura (Consultazione)',
|
||||||
|
handler: () => {
|
||||||
|
let shareLink = `https://www.canticristiani.it/?playlist-uid=${uid}&playlist-id=${activeId}`;
|
||||||
|
if (isRemote) {
|
||||||
|
let remoteUid = uid;
|
||||||
|
let remotePid = activeId;
|
||||||
|
const parts = activeId.split('_');
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
remoteUid = parts[1];
|
||||||
|
remotePid = parts[2];
|
||||||
|
}
|
||||||
|
shareLink = `https://www.canticristiani.it/?playlist-uid=${remoteUid}&playlist-id=${remotePid}`;
|
||||||
|
}
|
||||||
|
this.executeShare(shareLink, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isRemote) {
|
||||||
|
buttons.push({
|
||||||
|
text: 'Modifica (Collaborazione / Backup)',
|
||||||
|
handler: () => {
|
||||||
|
const shareLink = `https://www.canticristiani.it/?restore-uid=${uid}`;
|
||||||
|
this.executeShare(shareLink, name + ' (Editor)');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
text: 'Annulla',
|
||||||
|
role: 'cancel'
|
||||||
|
});
|
||||||
|
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
header: 'Condividi Playlist',
|
||||||
|
message: isRemote ? 'Condividi questa playlist in sola lettura:' : 'Scegli la modalità di condivisione della playlist:',
|
||||||
|
buttons: buttons
|
||||||
|
});
|
||||||
|
await alert.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeShare(shareLink: string, name: string) {
|
||||||
|
// Generate QR using the link
|
||||||
|
const qrImage = await QRCode.toDataURL(shareLink, {
|
||||||
|
width: 400,
|
||||||
|
margin: 2,
|
||||||
|
color: {
|
||||||
|
dark: '#2d3436',
|
||||||
|
light: '#ffffff'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const fileName = `${name.toLowerCase().replace(/\s+/g, '_')}_qr.png`;
|
const fileName = `${name.toLowerCase().replace(/\s+/g, '_')}_qr.png`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ export class SettingsService {
|
|||||||
/** Avanzamento a pagine del karaoke manuale: true = i tasti next/prev voltano la pagina */
|
/** Avanzamento a pagine del karaoke manuale: true = i tasti next/prev voltano la pagina */
|
||||||
public karaokePageScrollMode = signal<boolean>(false);
|
public karaokePageScrollMode = signal<boolean>(false);
|
||||||
|
|
||||||
|
/** Identificativo utente univoco per la gestione delle comunità */
|
||||||
|
public userUuid = signal<string>('');
|
||||||
|
|
||||||
private wakeLock: any = null;
|
private wakeLock: any = null;
|
||||||
|
|
||||||
// PWA installation signals
|
// PWA installation signals
|
||||||
@@ -63,6 +66,22 @@ export class SettingsService {
|
|||||||
public isAndroid = signal<boolean>(false);
|
public isAndroid = signal<boolean>(false);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// Gestione/Generazione ID utente univoco
|
||||||
|
let savedUuid = localStorage.getItem('user-uuid');
|
||||||
|
if (!savedUuid) {
|
||||||
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||||
|
savedUuid = crypto.randomUUID();
|
||||||
|
} else {
|
||||||
|
savedUuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
localStorage.setItem('user-uuid', savedUuid);
|
||||||
|
}
|
||||||
|
this.userUuid.set(savedUuid);
|
||||||
|
|
||||||
// Detect PWA status
|
// Detect PWA status
|
||||||
try {
|
try {
|
||||||
this.isStandalone.set(
|
this.isStandalone.set(
|
||||||
@@ -117,6 +136,7 @@ export class SettingsService {
|
|||||||
localStorage.setItem(migrationKey, 'true');
|
localStorage.setItem(migrationKey, 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const savedChords = localStorage.getItem('show-chords-default');
|
const savedChords = localStorage.getItem('show-chords-default');
|
||||||
if (savedChords !== null) {
|
if (savedChords !== null) {
|
||||||
this.showChordsDefault.set(savedChords === 'true');
|
this.showChordsDefault.set(savedChords === 'true');
|
||||||
@@ -397,6 +417,14 @@ export class SettingsService {
|
|||||||
this.chordNotationPreference.set(val);
|
this.chordNotationPreference.set(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setUserUuid(uuid: string) {
|
||||||
|
const trimmed = uuid.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
this.userUuid.set(trimmed);
|
||||||
|
localStorage.setItem('user-uuid', 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.06.0038';
|
export const VERSION = '2026.06.12.1712';
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
contactEmail: 'info@canticristiani.it',
|
contactEmail: 'info@canticristiani.it',
|
||||||
appName: 'CantiCristiani'
|
appName: 'CantiCristiani',
|
||||||
|
apiAuthUser: 'canti',
|
||||||
|
apiAuthPass: 'antani2026'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
contactEmail: 'info@canticristiani.it',
|
contactEmail: 'info@canticristiani.it',
|
||||||
appName: 'CantiCristiani'
|
appName: 'CantiCristiani',
|
||||||
|
apiAuthUser: 'canti',
|
||||||
|
apiAuthPass: 'antani2026'
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
+37
-5
@@ -37,8 +37,13 @@
|
|||||||
<app-root>
|
<app-root>
|
||||||
<div id="pwa-boot-loader" style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: #121212; color: #ffffff; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 99999; font-family: 'Outfit', sans-serif;">
|
<div id="pwa-boot-loader" style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: #121212; color: #ffffff; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 99999; font-family: 'Outfit', sans-serif;">
|
||||||
<div style="text-align: center; padding: 20px; max-width: 400px; width: 100%;">
|
<div style="text-align: center; padding: 20px; max-width: 400px; width: 100%;">
|
||||||
<h2 id="pwa-boot-title" style="font-size: 1.8rem; font-weight: 600; margin-bottom: 10px; color: #ffffff; font-family: 'Outfit', sans-serif; -webkit-font-smoothing: antialiased;">Avvio in corso</h2>
|
<div style="margin-bottom: 25px; display: inline-block;">
|
||||||
<p id="pwa-boot-desc" style="font-size: 1rem; color: rgba(255, 255, 255, 0.6); margin-bottom: 30px; font-family: 'Outfit', sans-serif; -webkit-font-smoothing: antialiased;">Caricamento dell'applicazione...</p>
|
<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.3); border: 2px solid rgba(230, 126, 34, 0.2);">
|
||||||
|
</div>
|
||||||
|
<h2 id="pwa-boot-title" style="font-size: 1.8rem; font-weight: 600; margin-bottom: 5px; color: #ffffff; font-family: 'Outfit', sans-serif; -webkit-font-smoothing: antialiased;">Avvio in corso</h2>
|
||||||
|
<p id="pwa-boot-desc" style="font-size: 1rem; color: rgba(255, 255, 255, 0.6); margin-bottom: 15px; font-family: 'Outfit', sans-serif; -webkit-font-smoothing: antialiased;">Caricamento dei componenti dell'applicazione...</p>
|
||||||
|
<div id="pwa-boot-version" style="font-size: 0.85rem; color: rgba(255, 255, 255, 0.45); margin-bottom: 20px; font-family: 'Outfit', sans-serif; -webkit-font-smoothing: antialiased; font-weight: 500;">Ricerca versione...</div>
|
||||||
|
<div id="pwa-boot-phase" style="font-size: 0.95rem; font-weight: 600; color: #fdcb6e; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 1.5px; font-family: 'Outfit', sans-serif;">Fase: Avvio</div>
|
||||||
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 10px; height: 8px; width: 100%; overflow: hidden; margin-bottom: 15px;">
|
<div style="background: rgba(255, 255, 255, 0.1); border-radius: 10px; height: 8px; width: 100%; overflow: hidden; margin-bottom: 15px;">
|
||||||
<div id="pwa-boot-bar" style="background: #e67e22; height: 100%; width: 0%; transition: width 0.1s ease; border-radius: 10px;"></div>
|
<div id="pwa-boot-bar" style="background: #e67e22; height: 100%; width: 0%; transition: width 0.1s ease; border-radius: 10px;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,11 +55,38 @@
|
|||||||
const isUpdate = window.location.search.includes('update');
|
const isUpdate = window.location.search.includes('update');
|
||||||
const titleEl = document.getElementById('pwa-boot-title');
|
const titleEl = document.getElementById('pwa-boot-title');
|
||||||
const descEl = document.getElementById('pwa-boot-desc');
|
const descEl = document.getElementById('pwa-boot-desc');
|
||||||
if (isUpdate && titleEl && descEl) {
|
const phaseEl = document.getElementById('pwa-boot-phase');
|
||||||
titleEl.textContent = 'Aggiornamento completato';
|
|
||||||
descEl.textContent = 'Ottimizzazione e avvio della nuova versione...';
|
// Detect if it is the first installation (no active service worker controlling the page yet)
|
||||||
|
const isFirstInstall = 'serviceWorker' in navigator && !navigator.serviceWorker.controller;
|
||||||
|
|
||||||
|
if (isUpdate) {
|
||||||
|
if (titleEl) titleEl.textContent = 'Aggiornamento completato';
|
||||||
|
if (descEl) descEl.textContent = 'Ottimizzazione e avvio della nuova versione...';
|
||||||
|
if (phaseEl) phaseEl.textContent = 'Fase: Avvio';
|
||||||
|
} else if (isFirstInstall) {
|
||||||
|
if (titleEl) titleEl.textContent = 'Download in corso';
|
||||||
|
if (descEl) descEl.textContent = 'Scaricamento dei componenti dell\'applicazione...';
|
||||||
|
if (phaseEl) phaseEl.textContent = 'Fase: Download';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const versionEl = document.getElementById('pwa-boot-version');
|
||||||
|
fetch('/version.json?cb=' + Date.now())
|
||||||
|
.then(res => {
|
||||||
|
if (res.ok) return res.json();
|
||||||
|
throw new Error('Fallback');
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data && data.version && versionEl) {
|
||||||
|
versionEl.textContent = 'Versione ' + data.version;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
if (versionEl) {
|
||||||
|
versionEl.textContent = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let percent = 0;
|
let percent = 0;
|
||||||
const bar = document.getElementById('pwa-boot-bar');
|
const bar = document.getElementById('pwa-boot-bar');
|
||||||
const pctText = document.getElementById('pwa-boot-percent');
|
const pctText = document.getElementById('pwa-boot-percent');
|
||||||
|
|||||||
+22
@@ -8,5 +8,27 @@ if (environment.production) {
|
|||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Patch window.fetch to automatically inject basic auth header for api.canticristiani.it
|
||||||
|
const originalFetch = window.fetch;
|
||||||
|
window.fetch = function(input: RequestInfo | URL, init?: RequestInit) {
|
||||||
|
const url = typeof input === 'string' ? input : (input instanceof URL ? input.href : input.url);
|
||||||
|
if (url.includes('api.canticristiani.it')) {
|
||||||
|
init = init || {};
|
||||||
|
init.headers = init.headers || {};
|
||||||
|
const authHeader = 'Basic ' + btoa(`${environment.apiAuthUser}:${environment.apiAuthPass}`);
|
||||||
|
if (init.headers instanceof Headers) {
|
||||||
|
init.headers.set('Authorization', authHeader);
|
||||||
|
} else if (Array.isArray(init.headers)) {
|
||||||
|
const hasAuth = init.headers.some(([key]) => key.toLowerCase() === 'authorization');
|
||||||
|
if (!hasAuth) {
|
||||||
|
init.headers.push(['Authorization', authHeader]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(init.headers as Record<string, string>)['Authorization'] = authHeader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originalFetch(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
.catch(err => console.log(err));
|
.catch(err => console.log(err));
|
||||||
|
|||||||
Reference in New Issue
Block a user