feat: integrazione scalette comunità come playlist

- Aggiunto il parsing di lista_nome e lista_esecuzione dall'API parrocchiale in ComunitaService.
- Le scalette vengono trasformate in ComunitaScaletta[] e persistite in localStorage.
- PlaylistService espone comunitaPlaylists (computed) e allPlaylists (computed che unisce playlist personali e scalette comunità).
- Le scalette della comunità appaiono nel dropdown Playlist SOLO quando il filtro comunità è attivo; scompaiono automaticamente alla disattivazione.
- Le scalette sono read-only: i pulsanti Modifica e Elimina vengono nascosti per le playlist della comunità.
- Icona people-outline e bordo sinistro secondario distinguono visivamente le scalette della comunità dalle playlist personali.
This commit is contained in:
David Frassi
2026-05-20 16:17:34 +02:00
parent 8c9f9df11c
commit e18c1c120b
6 changed files with 100 additions and 8 deletions
+8 -6
View File
@@ -115,11 +115,11 @@
<ion-button
[fill]="activeFilterType() === 'playlist' || playlistService.activeListName() !== null ? 'solid' : 'outline'"
size="small"
(click)="playlistService.playlists().length > 0 ? toggleFilterType('playlist') : playlistService.toggleSelectionMode()"
(click)="playlistService.allPlaylists().length > 0 ? toggleFilterType('playlist') : playlistService.toggleSelectionMode()"
class="filter-chip">
{{ playlistService.activeListName() !== null ? playlistService.activeListName() : 'Playlist' }}
<ion-icon slot="end" name="chevron-down-outline" *ngIf="playlistService.activeListName() === null && playlistService.playlists().length > 0"></ion-icon>
<ion-icon slot="end" name="add-circle-outline" *ngIf="playlistService.activeListName() === null && playlistService.playlists().length === 0"></ion-icon>
<ion-icon slot="end" name="chevron-down-outline" *ngIf="playlistService.activeListName() === null && playlistService.allPlaylists().length > 0"></ion-icon>
<ion-icon slot="end" name="add-circle-outline" *ngIf="playlistService.activeListName() === null && playlistService.allPlaylists().length === 0"></ion-icon>
<span class="close-icon-wrapper" *ngIf="playlistService.activeListName() !== null" (click)="clearSpecialList($event)">
<ion-icon slot="end" name="close-circle"></ion-icon>
</span>
@@ -160,23 +160,25 @@
<!-- Active Playlist Actions -->
<div class="selection-pill glass" *ngIf="playlistService.activeListName() && !playlistService.selectionMode()">
<ion-button fill="clear" color="secondary" (click)="editPlaylist()" class="mini-action-btn">
<ion-button fill="clear" color="secondary" (click)="editPlaylist()" class="mini-action-btn" *ngIf="!isComunitaPlaylist()">
<ion-icon slot="icon-only" name="create-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" color="secondary" (click)="shareActivePlaylist()" class="mini-action-btn">
<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()">
<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-button>
</div>
<!-- Filter chips -->
<div
*ngFor="let item of (activeFilterType() === 'playlist' ? playlistService.playlists() : (activeFilterType() === 'liturgico' ? cantiService.indiceLiturgico() : (activeFilterType() === 'tematico' ? cantiService.indiceTematico() : [])))"
*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>
</div>
+11
View File
@@ -355,6 +355,17 @@ ion-title {
font-weight: 600;
border-color: var(--ion-color-secondary);
}
&.comunita-chip {
display: inline-flex;
align-items: center;
border-left: 3px solid rgba(var(--ion-color-secondary-rgb), 0.6);
background: rgba(var(--ion-color-secondary-rgb), 0.08);
&.active {
border-left-color: #000;
}
}
}
// Custom Item Layout
+5
View File
@@ -854,6 +854,11 @@ export class HomePage implements OnDestroy {
this.isAddingSongs.set(false);
}
isComunitaPlaylist(): boolean {
const id = this.playlistService.activePlaylistId();
return !!id && id.startsWith('comunita_');
}
clearSpecialList(event?: Event) {
if (event) event.stopPropagation();
this.playlistService.activeListIds.set([]);
+48
View File
@@ -23,6 +23,13 @@ export interface CantiSettingsItem {
tonalita: number;
}
export interface ComunitaScaletta {
id: string;
name: string;
date: string;
ids: string[];
}
export interface GetAllAppTablesResponse {
parrocchia?: { data: ParrocchiaItem[] };
parrocchia_canti?: { data: ParrocchiaCantiItem[] };
@@ -43,6 +50,7 @@ export class ComunitaService {
public comunitaCantiInfo = signal<ParrocchiaCantiItem[]>([]);
public comunitaCantiSettings = signal<CantiSettingsItem[]>([]);
public comunitaCantiPersonali = signal<Canto[]>([]);
public comunitaScalette = signal<ComunitaScaletta[]>([]);
public isFilterActive = signal<boolean>(false);
public loading = signal<boolean>(false);
public loadingProgress = signal<number>(0);
@@ -55,6 +63,7 @@ export class ComunitaService {
const savedInfo = localStorage.getItem('comunita-canti-info');
const savedSettings = localStorage.getItem('comunita-canti-settings');
const savedCantiPersonali = localStorage.getItem('comunita-canti-personali');
const savedScalette = localStorage.getItem('comunita-scalette');
const savedFilterActive = localStorage.getItem('comunita-filter-active') === 'true';
if (savedCode) {
@@ -85,6 +94,9 @@ export class ComunitaService {
if (savedCantiPersonali) {
try { this.comunitaCantiPersonali.set(JSON.parse(savedCantiPersonali)); } catch (e) {}
}
if (savedScalette) {
try { this.comunitaScalette.set(JSON.parse(savedScalette)); } catch (e) {}
}
this.isFilterActive.set(savedFilterActive);
effect(() => {
@@ -95,6 +107,7 @@ export class ComunitaService {
localStorage.setItem('comunita-canti-info', JSON.stringify(this.comunitaCantiInfo()));
localStorage.setItem('comunita-canti-settings', JSON.stringify(this.comunitaCantiSettings()));
localStorage.setItem('comunita-canti-personali', JSON.stringify(this.comunitaCantiPersonali()));
localStorage.setItem('comunita-scalette', JSON.stringify(this.comunitaScalette()));
localStorage.setItem('comunita-filter-active', this.isFilterActive().toString());
});
@@ -143,6 +156,7 @@ export class ComunitaService {
this.comunitaCantiInfo.set([]);
this.comunitaCantiSettings.set([]);
this.comunitaCantiPersonali.set([]);
this.comunitaScalette.set([]);
this.isFilterActive.set(false);
return true;
}
@@ -182,6 +196,39 @@ export class ComunitaService {
this.comunitaCantiIds.set(cantiList.map(c => c.id_canti));
this.comunitaCantiSettings.set(settingsList);
this.comunitaCantiPersonali.set(mappedCantiPersonali);
// Parse scalette (setlists) from lista_nome + lista_esecuzione
const listaNomelList = (data as any).lista_nome?.data || [];
const listaEsecuzioneList = (data as any).lista_esecuzione?.data || [];
// Group lista_esecuzione by id_lista and keep order by progr
const esecuzioniByLista = new Map<string, { id_canti: string; progr: number }[]>();
for (const item of listaEsecuzioneList) {
if (!esecuzioniByLista.has(item.id_lista)) {
esecuzioniByLista.set(item.id_lista, []);
}
esecuzioniByLista.get(item.id_lista)!.push({
id_canti: String(item.id_canti),
progr: Number(item.progr) || 0
});
}
// Build scalette array sorted by date (most recent first)
const scalette: ComunitaScaletta[] = listaNomelList
.map((ln: any) => {
const songs = esecuzioniByLista.get(ln.id_lista) || [];
songs.sort((a: any, b: any) => a.progr - b.progr);
return {
id: `comunita_${ln.id_lista}`,
name: ln.nome || 'Scaletta',
date: ln.progr || '',
ids: songs.map((s: any) => s.id_canti)
};
})
.filter((s: ComunitaScaletta) => s.ids.length > 0)
.sort((a: ComunitaScaletta, b: ComunitaScaletta) => b.date.localeCompare(a.date));
this.comunitaScalette.set(scalette);
this.isFilterActive.set(true);
this.loading.set(false);
return true;
@@ -218,6 +265,7 @@ export class ComunitaService {
this.comunitaCantiInfo.set(mockInfo);
this.comunitaCantiSettings.set([]);
this.comunitaCantiPersonali.set([]);
this.comunitaScalette.set([]);
this.isFilterActive.set(true);
this.loading.set(false);
return true;
+27 -1
View File
@@ -1,6 +1,7 @@
import { Injectable, signal, inject } from '@angular/core';
import { Injectable, signal, inject, computed } from '@angular/core';
import { Storage } from '@ionic/storage-angular';
import { Canto, CantiService } from './canti.service';
import { ComunitaService } from './comunita.service';
import * as QRCode from 'qrcode';
@Injectable({
@@ -9,6 +10,7 @@ import * as QRCode from 'qrcode';
export class PlaylistService {
private storage = inject(Storage);
private cantiService = inject(CantiService);
private comunitaService = inject(ComunitaService);
public selectionMode = signal<boolean>(false);
public selectedIds = signal<Set<string>>(new Set());
@@ -22,6 +24,30 @@ export class PlaylistService {
private _storage: Storage | null = null;
// Community scalette exposed as playlists (only when community filter is active)
public comunitaPlaylists = computed(() => {
if (!this.comunitaService.comunitaCode() || !this.comunitaService.isFilterActive()) {
return [];
}
return this.comunitaService.comunitaScalette().map(s => ({
id: s.id,
name: s.name,
ids: s.ids,
createdAt: s.date,
isComunita: true
}));
});
// Merged list: personal playlists + community scalette
public allPlaylists = computed(() => {
const community = this.comunitaPlaylists();
const personal = this.playlists();
if (community.length > 0) {
return [...community, ...personal.map(p => ({ ...p, isComunita: false }))];
}
return personal.map(p => ({ ...p, isComunita: false }));
});
constructor() {
this.init();
}
+1 -1
View File
@@ -1 +1 @@
export const VERSION = '2026.05.20.1245';
export const VERSION = '2026.05.20.1602';