Ottimizzazione UI player, rotazione head tilt invertita, prefisso playlist in Home e implementazione Identity QR modal e interceptor API
This commit is contained in:
@@ -156,9 +156,16 @@
|
||||
</div>
|
||||
|
||||
<!-- Camera Head Gestures Toggle in Portrait -->
|
||||
<ion-button fill="clear" size="small" (click)="toggleCameraNavigation()" [color]="enableCameraNavigation() ? 'success' : 'secondary'" *ngIf="settingsService.enableVisualAutoscroll()">
|
||||
<ion-icon slot="icon-only" [name]="enableCameraNavigation() ? 'videocam' : 'videocam-off-outline'"></ion-icon>
|
||||
</ion-button>
|
||||
<div class="group" *ngIf="settingsService.enableVisualAutoscroll()">
|
||||
<ion-button fill="clear" size="small" (click)="toggleCameraNavigation()" [color]="enableCameraNavigation() ? 'success' : 'secondary'" style="margin: 0;">
|
||||
<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) -->
|
||||
@@ -208,12 +215,5 @@
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
|
||||
<!-- Floating Camera Preview for Head Navigation -->
|
||||
<div class="camera-preview-floating glass" *ngIf="enableCameraNavigation()">
|
||||
<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>
|
||||
<!-- Hidden Camera Video for Head Navigation (needed for face detection API to run) -->
|
||||
<video *ngIf="enableCameraNavigation()" id="face-preview-video" muted playsinline autoplay style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none;"></video>
|
||||
|
||||
@@ -257,6 +257,28 @@
|
||||
|
||||
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
|
||||
@@ -741,16 +763,17 @@ ion-content.full-screen-content {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
right: 15px;
|
||||
width: 90px;
|
||||
height: 120px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
padding: 8px 14px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.4);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(18, 18, 18, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
|
||||
@media (orientation: landscape) {
|
||||
bottom: 15px;
|
||||
@@ -758,27 +781,24 @@ ion-content.full-screen-content {
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transform: scaleX(-1); // Mirror camera preview
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.camera-preview-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
padding: 2px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.angle-indicator {
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.tilted {
|
||||
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 { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AlertController, ToastController, GestureController } from '@ionic/angular';
|
||||
@@ -177,19 +177,40 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (!found) {
|
||||
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) {
|
||||
found = comunitaCantiPers.find(c => c.id === id);
|
||||
}
|
||||
|
||||
if (found) {
|
||||
const current = this.canto();
|
||||
// Avoid duplicate triggers for the same song
|
||||
if (!current || current.id !== found.id) {
|
||||
const current = untracked(() => this.canto());
|
||||
// Avoid duplicate triggers for the same song, but update if song content changed
|
||||
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.canto.set(found);
|
||||
this.currentLineIndex.set(0);
|
||||
this.lastScrollBlock.set('start');
|
||||
this.songStartTime = Date.now();
|
||||
this.cantiService.getStorage()?.set('last_song_id', 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();
|
||||
let playlistSongSetting: any = null;
|
||||
if (activePlaylistId) {
|
||||
const pl = this.playlistService.playlists().find(p => p.id === activePlaylistId);
|
||||
if (pl && pl.songSettings && pl.songSettings[c.id]) {
|
||||
playlistSongSetting = pl.songSettings[c.id];
|
||||
if (activePlaylistId.startsWith('remote_')) {
|
||||
const pl = this.playlistService.remotePlaylist();
|
||||
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) {
|
||||
this.transposeAmount.set(playlistSongSetting.tonalita !== undefined ? playlistSongSetting.tonalita : 0);
|
||||
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()) {
|
||||
const settings = this.comunitaService.comunitaCantiSettings();
|
||||
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() {
|
||||
this.initialPinchDistance = null;
|
||||
this.updatePlaylistSettings();
|
||||
}
|
||||
|
||||
private getDistance(t1: Touch, t2: Touch): number {
|
||||
@@ -334,9 +368,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.transposeAmount.set(0);
|
||||
}
|
||||
|
||||
transposeUp() {
|
||||
this.transposeAmount.update(v => v + 1);
|
||||
|
||||
private updatePlaylistSettings() {
|
||||
const c = this.canto();
|
||||
if (c) {
|
||||
const dbSpeed = this.autoscrollSpeed() * 100;
|
||||
@@ -344,30 +376,35 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
const activePlaylistId = this.playlistService.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() {
|
||||
this.transposeAmount.update(v => v - 1);
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
this.updatePlaylistSettings();
|
||||
}
|
||||
|
||||
zoomIn() {
|
||||
if (this.fontSize() < 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.updatePlaylistSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,6 +412,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.fontSize() > 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.updatePlaylistSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,30 +895,12 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
increaseAutoscrollSpeed() {
|
||||
this.autoscrollSpeed.update(s => Math.min(10, s + 1));
|
||||
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());
|
||||
}
|
||||
}
|
||||
this.updatePlaylistSettings();
|
||||
}
|
||||
|
||||
decreaseAutoscrollSpeed() {
|
||||
this.autoscrollSpeed.update(s => Math.max(1, s - 1));
|
||||
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());
|
||||
}
|
||||
}
|
||||
this.updatePlaylistSettings();
|
||||
}
|
||||
|
||||
private logPreviousSongTime() {
|
||||
|
||||
@@ -5,8 +5,9 @@ import { IonicModule, ToastController, IonTextarea, PopoverController, NavContro
|
||||
import { createWorker } from 'tesseract.js';
|
||||
import { CantiService } from '../../services/canti.service';
|
||||
import { MyCantiService } from '../../services/my-canti.service';
|
||||
import { PlaylistService } from '../../services/playlist.service';
|
||||
import { ThemeService } from '../../services/theme.service';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import { ActivatedRoute, RouterModule, Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-propose-canto',
|
||||
@@ -21,9 +22,11 @@ export class ProposeCantoPage implements OnInit {
|
||||
|
||||
public cantiService = inject(CantiService);
|
||||
private myCantiService = inject(MyCantiService);
|
||||
private playlistService = inject(PlaylistService);
|
||||
private navCtrl = inject(NavController);
|
||||
public themeService = inject(ThemeService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
|
||||
title: string = '';
|
||||
author: string = '';
|
||||
@@ -112,10 +115,11 @@ export class ProposeCantoPage implements OnInit {
|
||||
const editId = params['editId'];
|
||||
if (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 = [
|
||||
...this.cantiService.canti(),
|
||||
...this.myCantiService.myCanti()
|
||||
...this.myCantiService.myCanti(),
|
||||
...this.playlistService.remoteCustomSongs()
|
||||
].find(c => c.id === editId);
|
||||
|
||||
if (song) {
|
||||
@@ -128,8 +132,8 @@ export class ProposeCantoPage implements OnInit {
|
||||
const litIds = this.cantiService.indiceLiturgico().map(m => m.id);
|
||||
const temIds = this.cantiService.indiceTematico().map(m => m.id);
|
||||
|
||||
this.selectedLiturgico = song.id_momenti?.filter(id => litIds.includes(id)) || [];
|
||||
this.selectedTematico = song.id_momenti?.filter(id => temIds.includes(id)) || [];
|
||||
this.selectedLiturgico = song.id_momenti?.filter((id: number) => litIds.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
|
||||
const id_momenti = [...this.selectedLiturgico, ...this.selectedTematico];
|
||||
|
||||
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
|
||||
});
|
||||
const activePlaylistId = this.playlistService.activePlaylistId();
|
||||
const isRemotePlaylist = activePlaylistId && activePlaylistId.startsWith('remote_');
|
||||
|
||||
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>
|
||||
</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 -->
|
||||
<div class="settings-group glass ion-margin-bottom">
|
||||
<ion-item class="transparent-item" lines="none">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SettingsService } from '../../services/settings.service';
|
||||
import { CantiService } from '../../services/canti.service';
|
||||
import { ConnectivityService } from '../../services/connectivity.service';
|
||||
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 { PlaylistService } from '../../services/playlist.service';
|
||||
import { Router } from '@angular/router';
|
||||
@@ -15,6 +15,8 @@ import { CantiLettureService } from '../../services/canti-letture.service';
|
||||
import { ComunitaService } from '../../services/comunita.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
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({
|
||||
selector: 'app-settings',
|
||||
@@ -35,6 +37,7 @@ export class SettingsPage {
|
||||
private toastCtrl = inject(ToastController);
|
||||
private modalCtrl = inject(ModalController);
|
||||
private alertCtrl = inject(AlertController);
|
||||
private loadingCtrl = inject(LoadingController);
|
||||
private router = inject(Router);
|
||||
|
||||
public version = VERSION;
|
||||
@@ -52,6 +55,209 @@ export class SettingsPage {
|
||||
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) {
|
||||
this.settingsService.setShowChordsDefault(event.detail.value === 'chords');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user