Ottimizzazione UI player, rotazione head tilt invertita, prefisso playlist in Home e implementazione Identity QR modal e interceptor API

This commit is contained in:
David Frassi
2026-06-12 17:13:46 +02:00
parent 4d1883a45d
commit d023bb7d3f
31 changed files with 1706 additions and 160 deletions
+43 -24
View File
@@ -7,7 +7,9 @@
<span class="app-name">{{ appName }}</span>
<span class="version-badge" (click)="checkForAppUpdate($event)" style="cursor: pointer; display: inline-flex; align-items: center; gap: 4px;">
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>
</div>
</div>
@@ -68,14 +70,13 @@
</div>
<ion-button
*ngIf="settingsService.showEditor()"
[fill]="showOnlyMine() ? 'solid' : 'outline'"
[fill]="activeFilterType() === 'lista_completa' || showValidati() || showNonValidati() ? 'solid' : 'outline'"
size="small"
(click)="toggleOnlyMine()"
color="secondary"
(click)="toggleFilterType('lista_completa')"
class="filter-chip">
Miei
<span class="close-icon-wrapper" *ngIf="showOnlyMine()" (click)="clearOnlyMine($event)">
{{ showValidati() ? 'Lista: Validati' : (showNonValidati() ? 'Lista: Non Validati' : 'Lista completa') }}
<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>
</span>
</ion-button>
@@ -85,7 +86,7 @@
size="small"
(click)="toggleFilterType('liturgico')"
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>
<span class="close-icon-wrapper" *ngIf="selectedLiturgico() !== null" (click)="clearLiturgico($event)">
<ion-icon slot="end" name="close-circle"></ion-icon>
@@ -97,7 +98,7 @@
size="small"
(click)="toggleFilterType('tematico')"
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>
<span class="close-icon-wrapper" *ngIf="selectedTematico() !== null" (click)="clearTematico($event)">
<ion-icon slot="end" name="close-circle"></ion-icon>
@@ -120,7 +121,7 @@
size="small"
(click)="playlistService.allPlaylists().length > 0 ? toggleFilterType('playlist') : playlistService.toggleSelectionMode()"
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="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)">
@@ -145,7 +146,7 @@
<!-- 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">
<!-- Selection Mode Actions -->
<div class="selection-pill glass" *ngIf="playlistService.selectionMode()">
@@ -162,7 +163,7 @@
</div>
<!-- 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-icon slot="icon-only" name="create-outline"></ion-icon>
</ion-button>
@@ -170,20 +171,37 @@
<ion-icon slot="icon-only" name="share-social-outline"></ion-icon>
</ion-button>
<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>
</div>
<!-- Filter chips -->
<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 *ngIf="activeFilterType() === 'lista_completa'">
<div
class="momento-chip glass"
[class.active]="showValidati()"
(click)="toggleValidati()">
Validati
</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>
</ion-toolbar>
</ion-header>
@@ -284,7 +302,7 @@
<div class="item-wrapper">
<!-- 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)">
{{ canto.id.startsWith('my_') ? getMySongNumber(canto) : canto.id_canti }}
</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;">
<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 *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>
<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>
+12
View File
@@ -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;
}
}
/* ==========================================================================
+421 -17
View File
@@ -33,8 +33,10 @@ export class HomePage implements OnDestroy {
public showOnlyMine = signal<boolean>(false);
public showTopTen = signal<boolean>(false);
public showSuggeriti = signal<boolean>(false);
public showValidati = signal<boolean>(false);
public showNonValidati = 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 version = VERSION;
public appName = environment.appName;
@@ -44,6 +46,7 @@ export class HomePage implements OnDestroy {
public showAndroidBanner = signal<boolean>(false);
public showIosTooltip = signal<boolean>(false);
public hasUpdateAvailable = signal<boolean>(false);
public fontSize = signal<number>(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<any[]>) {
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) {