+
{{ canto.id.startsWith('my_') ? getMySongNumber(canto) : canto.id_canti }}
@@ -297,7 +315,8 @@
{{ getCommunitySongNumber(canto) }}
{{ canto.titolo }}
- Non Validato
+ Remoto
+ Mio
{{ canto.autore || 'Autore sconosciuto' }}
diff --git a/src/app/home/home.page.scss b/src/app/home/home.page.scss
index 7ae63f8..07bd10e 100644
--- a/src/app/home/home.page.scss
+++ b/src/app/home/home.page.scss
@@ -946,12 +946,24 @@ ion-title {
margin-left: 6px;
display: inline-block;
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 {
background: rgba(231, 76, 60, 0.1) !important;
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;
+ }
}
/* ==========================================================================
diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts
index 43c21c9..d25bbb7 100644
--- a/src/app/home/home.page.ts
+++ b/src/app/home/home.page.ts
@@ -33,8 +33,10 @@ export class HomePage implements OnDestroy {
public showOnlyMine = signal(false);
public showTopTen = signal(false);
public showSuggeriti = signal(false);
+ public showValidati = signal(false);
+ public showNonValidati = signal(false);
public isMassCardExpanded = signal(false);
- public activeFilterType = signal<'liturgico' | 'tematico' | 'playlist' | null>(null);
+ public activeFilterType = signal<'liturgico' | 'tematico' | 'playlist' | 'lista_completa' | null>(null);
public loadedThumbs = new Set();
public version = VERSION;
public appName = environment.appName;
@@ -44,6 +46,7 @@ export class HomePage implements OnDestroy {
public showAndroidBanner = signal(false);
public showIosTooltip = signal(false);
+ public hasUpdateAvailable = signal(false);
public fontSize = signal(1.0);
public youtubePlayerService = inject(YoutubePlayerService);
@@ -73,6 +76,7 @@ export class HomePage implements OnDestroy {
private swUpdate = inject(SwUpdate);
private firstInteraction = true;
+ private updatePollInterval: any = null;
async checkForAppUpdate(event?: Event) {
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() {
if (this.firstInteraction) {
this.firstInteraction = false;
@@ -241,10 +263,11 @@ export class HomePage implements OnDestroy {
const comunitaIds = this.comunitaService.comunitaCantiIds();
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 = [
...this.cantiService.canti(),
...this.myCantiService.myCanti(),
+ ...this.playlistService.remoteCustomSongs(),
...this.comunitaService.comunitaCantiPersonali()
];
@@ -278,8 +301,12 @@ export class HomePage implements OnDestroy {
return numA - numB;
});
} else {
- // General context: only standard canti + my canti
- list = [...this.cantiService.canti(), ...this.myCantiService.myCanti()];
+ // General context: only standard canti + my canti + remote custom canti
+ list = [
+ ...this.cantiService.canti(),
+ ...this.myCantiService.myCanti(),
+ ...this.playlistService.remoteCustomSongs()
+ ];
}
// 2. Cumulative filtering
@@ -290,7 +317,7 @@ export class HomePage implements OnDestroy {
}
// A. Filter by Playlist (if activeIds is present)
- if (activeIds.length > 0 && !selectionMode) {
+ if (activeIds.length > 0 && (!selectionMode || this.playlistService.activePlaylistId() === null)) {
list = activeIds
.map(id => list.find(c => c.id === id))
.filter((c): c is any => !!c);
@@ -302,6 +329,16 @@ export class HomePage implements OnDestroy {
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
if (litId !== null) {
list = list.filter(c => c.id_momenti?.includes(litId));
@@ -377,6 +414,21 @@ export class HomePage implements OnDestroy {
});
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
const androidDismissed = localStorage.getItem('pwa-android-dismissed') === 'true';
const iosDismissed = localStorage.getItem('pwa-ios-dismissed') === 'true';
@@ -411,6 +463,8 @@ export class HomePage implements OnDestroy {
this.showOnlyMine.set(false);
this.showTopTen.set(false);
this.showSuggeriti.set(false);
+ this.showValidati.set(false);
+ this.showNonValidati.set(false);
this.playlistService.activeListIds.set([]);
this.playlistService.activeListName.set(null);
this.playlistService.activePlaylistId.set(null);
@@ -432,6 +486,12 @@ export class HomePage implements OnDestroy {
if (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;
@@ -443,7 +503,7 @@ export class HomePage implements OnDestroy {
if (selectionMode) {
if (!prevSelectionMode) {
- if (activeIds.length === 0) {
+ if (activeIds.length === 0 || this.playlistService.activePlaylistId() === null) {
this.isAddingSongs.set(true);
} else {
this.isAddingSongs.set(false);
@@ -476,7 +536,12 @@ export class HomePage implements OnDestroy {
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 {
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() {
}
ngOnDestroy() {
this.audioEngine.stopSearchRecognition();
+ if (this.updatePollInterval) {
+ clearInterval(this.updatePollInterval);
+ }
}
onSearch(event: any) {
@@ -619,8 +873,9 @@ export class HomePage implements OnDestroy {
return [
...this.cantiService.canti(),
...this.myCantiService.myCanti(),
+ ...this.playlistService.remoteCustomSongs(),
...this.comunitaService.comunitaCantiPersonali()
- ].find(c => c.id === id);
+ ].find(c => c.id === id || String(c.id_canti) === id);
}
getPlayingCanto() {
@@ -643,6 +898,7 @@ export class HomePage implements OnDestroy {
toggleOnlyMine() {
this.showOnlyMine.update(v => !v);
+ this.activeFilterType.set(null);
this.limit.set(10);
}
@@ -652,13 +908,24 @@ export class HomePage implements OnDestroy {
this.limit.set(10);
}
- toggleIndex(id: number, type: 'liturgico' | 'tematico' | 'playlist') {
+ toggleIndex(id: number, type: 'liturgico' | 'tematico' | 'playlist' | 'lista_completa') {
if (type === 'liturgico') {
this.selectedLiturgico.update(cur => cur === id ? null : id);
} else if (type === 'tematico') {
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);
}
@@ -734,6 +1001,8 @@ export class HomePage implements OnDestroy {
this.showOnlyMine() ||
this.showTopTen() ||
this.showSuggeriti() ||
+ this.showValidati() ||
+ this.showNonValidati() ||
this.playlistService.activeListName() !== null ||
this.searchQuery() !== '';
}
@@ -744,6 +1013,8 @@ export class HomePage implements OnDestroy {
this.showOnlyMine.set(false);
this.showTopTen.set(false);
this.showSuggeriti.set(false);
+ this.showValidati.set(false);
+ this.showNonValidati.set(false);
this.playlistService.activeListIds.set([]);
this.playlistService.activeListName.set(null);
this.playlistService.activePlaylistId.set(null);
@@ -752,9 +1023,23 @@ export class HomePage implements OnDestroy {
this.limit.set(10);
}
- toggleFilterType(type: 'liturgico' | 'tematico' | 'playlist') {
+ toggleFilterType(type: 'liturgico' | 'tematico' | 'playlist' | 'lista_completa') {
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 {
this.activeFilterType.set(type);
}
@@ -764,12 +1049,13 @@ export class HomePage implements OnDestroy {
this.playlistService.activeListIds.set(pl.ids);
this.playlistService.activeListName.set(pl.name);
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);
}
toggleTopTen() {
this.showTopTen.update(v => !v);
+ this.activeFilterType.set(null);
this.limit.set(30);
}
@@ -779,8 +1065,53 @@ export class HomePage implements OnDestroy {
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() {
this.showSuggeriti.update(v => !v);
+ this.activeFilterType.set(null);
this.limit.set(30);
}
@@ -996,6 +1327,42 @@ export class HomePage implements OnDestroy {
handleScannedData(data: string) {
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=')) {
try {
let base64 = '';
@@ -1115,6 +1482,11 @@ export class HomePage implements OnDestroy {
return !!id && id.startsWith('comunita_');
}
+ isRemotePlaylist(): boolean {
+ const id = this.playlistService.activePlaylistId();
+ return !!id && id.startsWith('remote_');
+ }
+
isActivePlaylistSaved(): boolean {
const id = this.playlistService.activePlaylistId();
if (!id) return false;
@@ -1164,16 +1536,22 @@ export class HomePage implements OnDestroy {
const name = this.playlistService.activeListName();
if (!id || !name) return;
+ const isRemote = this.isRemotePlaylist();
+
const alert = await this.alertCtrl.create({
- header: 'Elimina Playlist',
- message: `Vuoi davvero eliminare "${name}"?`,
+ header: isRemote ? 'Rimuovi Playlist' : 'Elimina Playlist',
+ message: isRemote ? `Vuoi davvero rimuovere la playlist "${name}"?` : `Vuoi davvero eliminare "${name}"?`,
buttons: [
{ text: 'Annulla', role: 'cancel' },
{
- text: 'Elimina',
+ text: isRemote ? 'Rimuovi' : 'Elimina',
role: 'destructive',
handler: () => {
- this.playlistService.deletePlaylist(id);
+ if (isRemote) {
+ this.playlistService.clearRemotePlaylist();
+ } else {
+ this.playlistService.deletePlaylist(id);
+ }
this.clearSpecialList();
}
}
@@ -1203,6 +1581,31 @@ export class HomePage implements OnDestroy {
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) {
const arr = [...this.reorderList()];
moveItemInArray(arr, event.previousIndex, event.currentIndex);
@@ -1233,6 +1636,7 @@ export class HomePage implements OnDestroy {
} else {
this.comunitaService.isFilterActive.update(v => !v);
}
+ this.activeFilterType.set(null);
}
editComunitaCode(event: Event) {
diff --git a/src/app/interceptors/api-auth.interceptor.ts b/src/app/interceptors/api-auth.interceptor.ts
new file mode 100644
index 0000000..a6b298e
--- /dev/null
+++ b/src/app/interceptors/api-auth.interceptor.ts
@@ -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, next: HttpHandler): Observable> {
+ 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);
+ }
+}
diff --git a/src/app/pages/player/player.page.html b/src/app/pages/player/player.page.html
index 0bfd35b..04a67b3 100644
--- a/src/app/pages/player/player.page.html
+++ b/src/app/pages/player/player.page.html
@@ -156,9 +156,16 @@
-
-
-
+
+
+
+
+
+
+ {{ faceDetector.currentTiltAngle() }}°
+
+
@@ -208,12 +215,5 @@
-
-
-
-
-
- {{ faceDetector.currentTiltAngle() }}°
-
-
-
+
+
diff --git a/src/app/pages/player/player.page.scss b/src/app/pages/player/player.page.scss
index f137f1c..02e40c8 100644
--- a/src/app/pages/player/player.page.scss
+++ b/src/app/pages/player/player.page.scss
@@ -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;
diff --git a/src/app/pages/player/player.page.ts b/src/app/pages/player/player.page.ts
index a7f9f51..634045b 100644
--- a/src/app/pages/player/player.page.ts
+++ b/src/app/pages/player/player.page.ts
@@ -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() {
diff --git a/src/app/pages/propose-canto/propose-canto.page.ts b/src/app/pages/propose-canto/propose-canto.page.ts
index e81dfc2..3ca3536 100644
--- a/src/app/pages/propose-canto/propose-canto.page.ts
+++ b/src/app/pages/propose-canto/propose-canto.page.ts
@@ -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();
+ }
+ }
}
}
diff --git a/src/app/pages/settings/settings.page.html b/src/app/pages/settings/settings.page.html
index c95afc5..42a2be6 100644
--- a/src/app/pages/settings/settings.page.html
+++ b/src/app/pages/settings/settings.page.html
@@ -156,6 +156,47 @@