Files
canti/src/app/home/home.page.ts
T
2026-05-18 16:07:37 +02:00

1107 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Component, computed, signal, inject, effect, OnDestroy } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { CantiService } from '../services/canti.service';
import { Router, ActivatedRoute } from '@angular/router';
import { AudioEngineService } from '../services/audio-engine.service';
import { ThemeService } from '../services/theme.service';
import { ConnectivityService } from '../services/connectivity.service';
import { PlaylistService } from '../services/playlist.service';
import { SettingsService } from '../services/settings.service';
import { VERSION } from '../version';
import { YoutubePlayerService } from '../services/youtube-player.service';
import { ModalController, AlertController, ToastController, LoadingController } from '@ionic/angular';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { MyCantiService } from '../services/my-canti.service';
import { QrScannerComponent } from '../components/qr-scanner/qr-scanner.component';
import { CantiLettureService } from '../services/canti-letture.service';
import { ComunitaService } from '../services/comunita.service';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
standalone: false,
})
export class HomePage implements OnDestroy {
public searchQuery = signal<string>('');
public selectedLiturgico = signal<number | null>(null);
public selectedTematico = signal<number | null>(null);
public showOnlyMine = signal<boolean>(false);
public showTopTen = signal<boolean>(false);
public showSuggeriti = signal<boolean>(false);
public isMassCardExpanded = signal<boolean>(false);
public activeFilterType = signal<'liturgico' | 'tematico' | 'playlist' | null>(null);
public loadedThumbs = new Set<string>();
public version = VERSION;
public isAddingSongs = signal<boolean>(false);
public reorderList = signal<any[]>([]);
public limit = signal<number>(30);
public fontSize = signal<number>(1.0);
public youtubePlayerService = inject(YoutubePlayerService);
public comunitaService = inject(ComunitaService);
public isSeeking = false;
private readonly MIN_FONT = 0.6;
private readonly MAX_FONT = 5.0;
private initialPinchDistance: number | null = null;
private initialFontSize: number = 1.0;
private sanitizer = inject(DomSanitizer);
public audioEngine = inject(AudioEngineService);
public themeService = inject(ThemeService);
public connectivityService = inject(ConnectivityService);
public cantiService = inject(CantiService);
public myCantiService = inject(MyCantiService);
public playlistService = inject(PlaylistService);
public settingsService = inject(SettingsService);
public cantiLettureService = inject(CantiLettureService);
private router = inject(Router);
private route = inject(ActivatedRoute);
private modalCtrl = inject(ModalController);
private alertCtrl = inject(AlertController);
private toastCtrl = inject(ToastController);
private loadingCtrl = inject(LoadingController);
private firstInteraction = true;
onInteraction() {
if (this.firstInteraction) {
this.firstInteraction = false;
}
}
private scrollEffect = effect(() => {
const activeId = this.youtubePlayerService.currentCantoId();
const list = this.filteredCanti();
if (activeId) {
const index = list.findIndex(c => c.id === activeId);
if (index !== -1 && index >= this.limit()) {
this.limit.set(index + 15);
}
// Delay slightly to ensure DOM is updated and classes are applied
setTimeout(() => {
const element = document.getElementById('canto-' + activeId);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 500);
}
}, { allowSignalWrites: true });
private normalize(str: string | undefined): string {
if (!str) return '';
return str
.toLowerCase()
.replace(/['‘’´`]/g, "'")
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/hallelujah/g, 'alleluia')
.replace(/halleluja/g, 'alleluia')
.replace(/alleluja/g, 'alleluia');
}
public filteredCanti = computed(() => {
const query = this.normalize(this.searchQuery());
const litId = this.selectedLiturgico();
const temId = this.selectedTematico();
const onlyMine = this.showOnlyMine();
const topTen = this.showTopTen();
const suggeriti = this.showSuggeriti();
let list = [...this.cantiService.canti(), ...this.myCantiService.myCanti()];
// Filter by Community if code is set and filter is active
const comunitaCode = this.comunitaService.comunitaCode();
const comunitaIds = this.comunitaService.comunitaCantiIds();
if (comunitaCode && this.comunitaService.isFilterActive()) {
list = list.filter(c => {
return comunitaIds.includes(c.id_canti) || comunitaIds.includes(c.id);
});
// Sort list by community song number (num_canto) ASC!
const cantiInfo = this.comunitaService.comunitaCantiInfo();
list.sort((a, b) => {
const infoA = cantiInfo.find(x => x.id_canti === a.id_canti || x.id_canti === Number(a.id));
const infoB = cantiInfo.find(x => x.id_canti === b.id_canti || x.id_canti === Number(b.id));
const numA = infoA ? Number(infoA.num_canto) : 999999;
const numB = infoB ? Number(infoB.num_canto) : 999999;
return numA - numB;
});
}
if (onlyMine) {
list = this.myCantiService.myCanti();
}
if (litId !== null) {
list = list.filter(c => c.id_momenti?.includes(litId));
}
if (temId !== null) {
list = list.filter(c => c.id_momenti?.includes(temId));
}
if (suggeriti) {
const suggMap = this.cantiLettureService.suggestionsMap();
list = list.filter(c => suggMap.has(c.id_canti));
}
// Special List handling (from QR Code or Saved Playlists)
const activeIds = this.playlistService.activeListIds();
const selectionMode = this.playlistService.selectionMode();
// If we are in selection mode, only show restricted list if NOT adding songs
if (selectionMode && !this.isAddingSongs()) {
return this.reorderList();
}
// If we have an active playlist and NOT in selection mode, show ONLY those songs
if (activeIds.length > 0 && !selectionMode) {
// Filter only songs in the special list and KEEP THE ORDER
return activeIds
.map(id => list.find(c => c.id === id))
.filter((c): c is any => !!c);
}
if (suggeriti) {
const suggMap = this.cantiLettureService.suggestionsMap();
list = list.sort((a, b) => {
const pesoA = suggMap.get(a.id_canti) || 0;
const pesoB = suggMap.get(b.id_canti) || 0;
return pesoB - pesoA;
});
} else if (topTen) {
const eseguiti = this.cantiService.cantiEseguiti();
const eseguitiMap = new Map<number, number>();
eseguiti.forEach(x => eseguitiMap.set(x.id_canti, x.num));
list = list.sort((a, b) => {
const numA = eseguitiMap.get(a.id_canti) || 0;
const numB = eseguitiMap.get(b.id_canti) || 0;
return numB - numA;
});
}
if (!query) return list;
// Converte "uno" in "1" per facilitare la ricerca vocale (es. "numero uno")
const processedQuery = query.replace(/\buno\b/g, '1');
// Se la ricerca inizia con "numero" o "nr", filtra esattamente per id_canti/comunita number
const numberMatchPattern = processedQuery.match(/^(?:numero|nr\.?)\s*(\d+)$/);
if (numberMatchPattern) {
const targetId = numberMatchPattern[1];
return list.filter(c => {
const commNum = this.getCommunitySongNumber(c);
return commNum ? commNum === targetId : c.id_canti.toString() === targetId;
});
}
return list.filter(c => {
const titleMatch = this.normalize(c.titolo).includes(query);
const authorMatch = this.normalize(c.autore).includes(query);
const commNum = this.getCommunitySongNumber(c);
const numberMatch = commNum ? commNum.includes(query) : c.id_canti.toString().includes(query);
// Strip tags like {Chorus} and newlines from lyrics before searching
const lyricsPlain = (c.testo || '')
.replace(/{.*?}/g, '')
.replace(/\n/g, ' ');
const lyricsMatch = this.normalize(lyricsPlain).includes(query);
return titleMatch || authorMatch || lyricsMatch || numberMatch;
});
});
public visibleCanti = computed(() => {
return this.filteredCanti().slice(0, this.limit());
});
constructor() {
// Sync speech recognition results to search query
effect(() => {
const transcript = this.audioEngine.searchTranscript();
if (transcript) {
this.searchQuery.set(transcript);
}
});
this.route.queryParams.subscribe(params => {
if (params['import']) {
this.handleImport(params['import']);
}
});
let prevSelectionMode = false;
effect(() => {
const selectionMode = this.playlistService.selectionMode();
const selectedIds = this.playlistService.selectedIds();
const activeIds = this.playlistService.activeListIds();
if (selectionMode) {
if (!prevSelectionMode) {
if (activeIds.length === 0) {
this.isAddingSongs.set(true);
} else {
this.isAddingSongs.set(false);
}
}
prevSelectionMode = true;
const currentIds = this.reorderList().map(c => c.id);
const newIds = Array.from(selectedIds);
// Use a set of IDs for faster lookup
const currentIdSet = new Set(currentIds);
const newIdSet = new Set(newIds);
// Check if there are differences in the SET of IDs
const added = newIds.filter(id => !currentIdSet.has(id));
const removed = currentIds.filter(id => !newIdSet.has(id));
if (added.length > 0 || removed.length > 0) {
let updatedList = [...this.reorderList()];
// Remove songs no longer selected
if (removed.length > 0) {
updatedList = updatedList.filter(c => newIdSet.has(c.id));
}
// Add newly selected songs at the end
if (added.length > 0) {
const newSongs = added.map(id => this.findCanto(id)).filter((c): c is any => !!c);
updatedList = [...updatedList, ...newSongs];
}
this.reorderList.set(updatedList);
}
} else {
prevSelectionMode = false;
this.isAddingSongs.set(false);
}
}, { allowSignalWrites: true });
}
handleImport(base64: string) {
try {
const decoded = decodeURIComponent(escape(atob(base64)));
const json = JSON.parse(decoded);
if (this.playlistService.processImportJson(json)) {
this.router.navigate([], { queryParams: { import: null }, queryParamsHandling: 'merge' });
}
} catch (e) {
console.error('Failed to import playlist', e);
}
}
ionViewWillLeave() {
}
ngOnDestroy() {
this.audioEngine.stopSearchRecognition();
}
onSearch(event: any) {
this.searchQuery.set(event.detail.value);
this.limit.set(30);
}
async deleteMyCanto(id: string, event: Event) {
event.stopPropagation();
const alert = await this.alertCtrl.create({
header: 'Elimina Canto',
message: 'Sei sicuro di voler eliminare questo canto dai tuoi brani personali?',
buttons: [
{ text: 'Annulla', role: 'cancel' },
{
text: 'Elimina',
role: 'destructive',
handler: () => {
this.myCantiService.deleteCanto(id);
}
}
]
});
await alert.present();
}
findCanto(id: string) {
return [...this.cantiService.canti(), ...this.myCantiService.myCanti()].find(c => c.id === id);
}
getPlayingCanto() {
const id = this.youtubePlayerService.currentCantoId();
if (!id) return null;
return this.findCanto(id);
}
goToCanto(id: string) {
const params: any = { id };
const isPlaying = this.youtubePlayerService.isPlaying();
if (this.youtubePlayerService.currentCantoId() === id && isPlaying) {
params.t = Math.floor(this.youtubePlayerService.videoProgress());
}
this.playlistService.autoPlayPlaylist.set(isPlaying);
this.router.navigate(['/player'], { queryParams: params });
}
toggleOnlyMine() {
this.showOnlyMine.update(v => !v);
if (this.showOnlyMine()) {
this.selectedLiturgico.set(null);
this.selectedTematico.set(null);
}
this.limit.set(10);
}
clearOnlyMine(event: Event) {
event.stopPropagation();
this.showOnlyMine.set(false);
this.limit.set(10);
}
toggleIndex(id: number, type: 'liturgico' | 'tematico' | 'playlist') {
if (type === 'liturgico') {
this.selectedLiturgico.update(cur => cur === id ? null : id);
} else if (type === 'tematico') {
this.selectedTematico.update(cur => cur === id ? null : id);
}
this.showOnlyMine.set(false);
this.activeFilterType.set(null);
this.limit.set(10);
}
clearLiturgico(event: Event) {
event.stopPropagation();
this.selectedLiturgico.set(null);
this.activeFilterType.set(null);
this.limit.set(10);
}
clearTematico(event: Event) {
event.stopPropagation();
this.selectedTematico.set(null);
this.activeFilterType.set(null);
this.limit.set(10);
}
isIndexSelected(id: number): boolean {
return this.selectedLiturgico() === id || this.selectedTematico() === id;
}
clearFilters() {
const type = this.activeFilterType();
if (this.playlistService.activeListIds().length > 0) {
this.clearSpecialList();
}
if (type === 'liturgico') {
if (this.selectedLiturgico() === null) {
this.selectedTematico.set(null);
} else {
this.selectedLiturgico.set(null);
}
} else if (type === 'tematico') {
if (this.selectedTematico() === null) {
this.selectedLiturgico.set(null);
} else {
this.selectedTematico.set(null);
}
} else {
this.selectedLiturgico.set(null);
this.selectedTematico.set(null);
}
this.showOnlyMine.set(false);
this.searchQuery.set('');
this.audioEngine.clearSearchTranscript();
}
activeFiltersCount(): number {
let count = 0;
if (this.selectedLiturgico() !== null) count++;
if (this.selectedTematico() !== null) count++;
return count;
}
getSelectedLiturgicoLabel(): string | null {
const id = this.selectedLiturgico();
if (id === null) return null;
return this.cantiService.indiceLiturgico().find(m => m.id === id)?.tag_name || null;
}
getSelectedTematicoLabel(): string | null {
const id = this.selectedTematico();
if (id === null) return null;
return this.cantiService.indiceTematico().find(m => m.id === id)?.tag_name || null;
}
hasActiveFilters(): boolean {
return this.selectedLiturgico() !== null ||
this.selectedTematico() !== null ||
this.showOnlyMine() ||
this.showTopTen() ||
this.showSuggeriti() ||
this.playlistService.activeListName() !== null ||
this.searchQuery() !== '';
}
clearAllFilters() {
this.selectedLiturgico.set(null);
this.selectedTematico.set(null);
this.showOnlyMine.set(false);
this.showTopTen.set(false);
this.showSuggeriti.set(false);
this.playlistService.activeListIds.set([]);
this.playlistService.activeListName.set(null);
this.playlistService.activePlaylistId.set(null);
this.searchQuery.set('');
this.activeFilterType.set(null);
this.limit.set(10);
}
toggleFilterType(type: 'liturgico' | 'tematico' | 'playlist') {
if (this.activeFilterType() === type) {
this.activeFilterType.set(null);
} else {
this.activeFilterType.set(type);
}
}
selectPlaylist(pl: any) {
this.playlistService.activeListIds.set(pl.ids);
this.playlistService.activeListName.set(pl.name);
this.playlistService.activePlaylistId.set(pl.id);
this.activeFilterType.set(null);
this.limit.set(50);
// Clear other filters
this.selectedLiturgico.set(null);
this.selectedTematico.set(null);
this.showOnlyMine.set(false);
}
toggleTopTen() {
this.showTopTen.update(v => !v);
this.limit.set(30);
}
clearTopTen(event?: Event) {
if (event) event.stopPropagation();
this.showTopTen.set(false);
this.limit.set(30);
}
toggleSuggeriti() {
this.showSuggeriti.update(v => !v);
this.limit.set(30);
}
clearSuggeriti(event?: Event) {
if (event) event.stopPropagation();
this.showSuggeriti.set(false);
this.limit.set(30);
}
getMassSuggestionWeight(id_canti: number): number | null {
const weight = this.cantiLettureService.suggestionsMap().get(id_canti);
return weight !== undefined ? weight : null;
}
getMassSuggestionMoment(id_canti: number): string | null {
const moments = this.cantiLettureService.suggestionsMomentsMap().get(id_canti);
return moments && moments.length > 0 ? moments.join(', ') : null;
}
getSelectedMassFormattedDate(): string | null {
const dateStr = this.cantiLettureService.selectedMassDate();
if (!dateStr) return null;
const parts = dateStr.split('-');
if (parts.length === 3) {
return `${parts[2]}/${parts[1]}/${parts[0]}`;
}
return dateStr;
}
getSelectedMassTitle(): string | null {
const dateStr = this.cantiLettureService.selectedMassDate();
const data = this.cantiLettureService.data();
if (dateStr && data && data.masses[dateStr]) {
return data.masses[dateStr].title;
}
return null;
}
getSelectedMassSummary(): string | null {
const dateStr = this.cantiLettureService.selectedMassDate();
const data = this.cantiLettureService.data();
if (dateStr && data && data.masses[dateStr]) {
return data.masses[dateStr].summary || null;
}
return null;
}
getEsecuzioniCount(id: string): number | null {
const numId = parseInt(id, 10);
if (isNaN(numId)) return null;
const es = this.cantiService.cantiEseguiti().find(x => x.id_canti === numId);
return es ? es.num : null;
}
onImgLoad(id: string) {
this.loadedThumbs.add(id);
}
openYoutube(event: Event, link: string) {
event.stopPropagation();
const videoId = this.cantiService.getYoutubeId(link);
if (videoId) {
window.open(`https://www.youtube.com/watch?v=${videoId}`, '_blank');
}
}
playVideo(event: Event, id: string) {
event.stopPropagation();
if (!this.youtubePlayerService.isPlayerSupported()) {
return;
}
this.youtubePlayerService.setMediaSessionCallbacks(
() => this.playNextPreview(),
() => this.playPrevPreview()
);
this.youtubePlayerService.initPlayer(
id,
0,
() => {
if (this.settingsService.autoAdvance()) {
this.playNextPreview();
} else {
this.youtubePlayerService.stop();
}
},
() => {
if (this.settingsService.autoAdvance()) {
setTimeout(() => this.playNextPreview(), 500);
} else {
this.youtubePlayerService.stop();
}
}
);
}
stopVideo(event: Event) {
if (event) event.stopPropagation();
this.youtubePlayerService.stop();
this.playlistService.autoPlayPlaylist.set(false);
}
playNextPreview(event?: Event) {
if (event) event.stopPropagation();
const currentId = this.youtubePlayerService.currentCantoId();
if (!currentId) return;
const list = this.visibleCanti();
const currentIndex = list.findIndex(c => c.id === currentId);
for (let i = currentIndex + 1; i < list.length; i++) {
if (list[i].link_youtube) {
this.playVideo(new Event('skip'), list[i].id);
setTimeout(() => {
const el = document.getElementById('canto-' + list[i].id);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 300);
return;
}
}
this.youtubePlayerService.stop();
}
playPrevPreview(event?: Event) {
if (event) event.stopPropagation();
const currentId = this.youtubePlayerService.currentCantoId();
if (!currentId) return;
const list = this.visibleCanti();
const currentIndex = list.findIndex(c => c.id === currentId);
for (let i = currentIndex - 1; i >= 0; i--) {
if (list[i].link_youtube) {
this.playVideo(new Event('skip'), list[i].id);
setTimeout(() => {
const el = document.getElementById('canto-' + list[i].id);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 300);
return;
}
}
}
onSeek(event: any) {
this.youtubePlayerService.seekTo(event.detail.value);
}
onSeekStart() {
this.isSeeking = true;
}
onSeekEnd() {
this.isSeeking = false;
}
getMomentiForCanto(ids: number[] | undefined): any[] {
if (!ids) return [];
return this.cantiService.momenti()
.filter(m => ids.includes(m.id));
}
filterByMomento(event: Event, id: number) {
event.stopPropagation();
// Use liturgico as default for momento tags
this.toggleIndex(id, 'liturgico');
// Scroll to top to show results
const content = document.querySelector('ion-content');
if (content) (content as any).scrollToTop(300);
}
loadData(event: any) {
setTimeout(() => {
this.limit.update(l => l + 10);
event.target.complete();
}, 500);
}
// --- Touch Gestures for Pinch-to-Zoom ---
onTouchStart(event: TouchEvent) {
if (event.touches.length === 2) {
this.initialPinchDistance = this.getDistance(event.touches[0], event.touches[1]);
this.initialFontSize = this.fontSize();
}
}
onTouchMove(event: TouchEvent) {
if (event.touches.length === 2 && this.initialPinchDistance !== null) {
// We don't preventDefault here to allow scrolling if needed,
// but usually pinch should be captured.
const currentDistance = this.getDistance(event.touches[0], event.touches[1]);
const ratio = currentDistance / this.initialPinchDistance;
let newSize = this.initialFontSize * ratio;
// Clamp values
newSize = Math.max(this.MIN_FONT, Math.min(this.MAX_FONT, newSize));
if (Math.abs(newSize - this.fontSize()) > 0.01) {
this.fontSize.set(newSize);
}
}
}
onTouchEnd() {
this.initialPinchDistance = null;
}
private getDistance(t1: Touch, t2: Touch): number {
return Math.sqrt(Math.pow(t1.clientX - t2.clientX, 2) + Math.pow(t1.clientY - t2.clientY, 2));
}
handleScannedData(data: string) {
if (!data) return;
if (data.includes('import=')) {
try {
let base64 = '';
if (data.includes('?import=')) {
base64 = data.split('?import=')[1];
} else if (data.includes('&import=')) {
base64 = data.split('&import=')[1];
}
if (base64.includes('&')) {
base64 = base64.split('&')[0];
}
if (base64.includes('#')) {
base64 = base64.split('#')[0];
}
this.handleImport(base64);
return;
} catch (e) {
console.error('Failed to parse scanned link', e);
}
}
if (data.startsWith('canti:')) {
const parts = data.replace('canti:', '').split(':');
let idsStr = '';
if (parts.length > 1) {
this.playlistService.activeListName.set(parts[0]);
idsStr = parts[1];
} else {
this.playlistService.activeListName.set('Lista Parrocchiale');
idsStr = parts[0];
}
const ids = idsStr.split(',').map(id => id.trim());
this.playlistService.activeListIds.set(ids);
this.limit.set(50); // Show more initially for special lists
// Clear other filters to avoid confusion
this.selectedLiturgico.set(null);
this.selectedTematico.set(null);
}
}
toggleVoiceSearch() {
if (this.audioEngine.isSearching()) {
this.audioEngine.stopSearchRecognition();
} else {
this.audioEngine.startSearchRecognition();
}
}
async finishSelection() {
const alert = await this.alertCtrl.create({
header: 'Salva Playlist',
inputs: [
{
name: 'name',
type: 'text',
placeholder: 'Nome della playlist',
value: this.playlistService.activeListName() || ''
}
],
buttons: [
{
text: 'Annulla',
role: 'cancel'
},
{
text: 'Salva',
handler: async (data) => {
if (data.name) {
const ids = this.reorderList().map(s => s.id);
await this.playlistService.savePlaylist(data.name, ids);
const toast = await this.toastCtrl.create({
message: 'Playlist salvata!',
duration: 2000,
color: 'success'
});
await toast.present();
this.isAddingSongs.set(false);
this.playlistService.selectionMode.set(false);
this.playlistService.selectedIds.set(new Set());
return true;
}
return false;
}
}
]
});
await alert.present();
}
cancelSelection() {
const id = this.playlistService.activePlaylistId();
if (id) {
const pl = this.playlistService.playlists().find(p => p.id === id);
if (pl) {
this.playlistService.activeListIds.set(pl.ids);
this.playlistService.activeListName.set(pl.name);
}
}
this.playlistService.selectionMode.set(false);
this.playlistService.selectedIds.set(new Set());
this.isAddingSongs.set(false);
}
clearSpecialList(event?: Event) {
if (event) event.stopPropagation();
this.playlistService.activeListIds.set([]);
this.playlistService.activeListName.set(null);
this.playlistService.activePlaylistId.set(null);
this.limit.set(30);
}
playPlaylist() {
const ids = this.playlistService.activeListIds();
if (ids.length > 0) {
this.playlistService.autoPlayPlaylist.set(true);
this.router.navigate(['/player'], { queryParams: { id: ids[0] } });
}
}
shareActivePlaylist() {
const ids = this.playlistService.activeListIds();
const name = this.playlistService.activeListName() || 'Playlist';
this.playlistService.sharePlaylistQR(ids, name);
}
async importPlaylist() {
const modal = await this.modalCtrl.create({
component: QrScannerComponent
});
await modal.present();
const { data } = await modal.onWillDismiss();
if (!data) return;
this.handleScannedData(data);
}
async deleteActivePlaylist() {
const id = this.playlistService.activePlaylistId();
const name = this.playlistService.activeListName();
if (!id || !name) return;
const alert = await this.alertCtrl.create({
header: 'Elimina Playlist',
message: `Vuoi davvero eliminare "${name}"?`,
buttons: [
{ text: 'Annulla', role: 'cancel' },
{
text: 'Elimina',
role: 'destructive',
handler: () => {
this.playlistService.deletePlaylist(id);
this.clearSpecialList();
}
}
]
});
await alert.present();
}
editPlaylist() {
const ids = this.playlistService.activeListIds();
const name = this.playlistService.activeListName();
// Prova a recuperare l'ID se stiamo editando una playlist salvata
const found = this.playlistService.playlists().find(p => p.name === name);
if (found) {
this.playlistService.activePlaylistId.set(found.id);
}
this.playlistService.selectedIds.set(new Set(ids));
const songs = ids.map(id => this.findCanto(id)).filter((c): c is any => !!c);
this.reorderList.set(songs);
this.playlistService.selectionMode.set(true);
this.isAddingSongs.set(false); // Default to reorder view
// Svuota la playlist attiva per mostrare tutto l'elenco in modalità selezione
this.playlistService.activeListIds.set([]);
this.playlistService.activeListName.set(name);
}
drop(event: CdkDragDrop<any[]>) {
const arr = [...this.reorderList()];
moveItemInArray(arr, event.previousIndex, event.currentIndex);
this.reorderList.set(arr);
}
toggleAddingSongs() {
this.isAddingSongs.update(v => !v);
this.limit.set(10);
}
getCommunitySongNumber(canto: any): string | null {
if (!this.comunitaService.comunitaCode() || !this.comunitaService.isFilterActive()) return null;
const cantiInfo = this.comunitaService.comunitaCantiInfo();
const info = cantiInfo.find(x => x.id_canti === canto.id_canti || x.id_canti === Number(canto.id));
return info && info.num_canto ? info.num_canto.toString() : null;
}
toggleComunitaFilter() {
if (!this.comunitaService.comunitaCode()) {
this.promptComunitaCode();
} else {
this.comunitaService.isFilterActive.update(v => !v);
}
}
editComunitaCode(event: Event) {
event.stopPropagation();
this.promptComunitaCode();
}
async promptComunitaCode() {
const alert = await this.alertCtrl.create({
header: 'Imposta Comunità',
subHeader: 'Inserisci il codice parrocchiale/comunità per attivare il libretto dedicato:',
cssClass: 'premium-alert',
inputs: [
{
name: 'code',
type: 'text',
placeholder: 'Es: 123456',
value: this.comunitaService.comunitaCode()
}
],
buttons: [
{
text: 'Annulla',
role: 'cancel'
},
{
text: 'Rimuovi',
role: 'destructive',
cssClass: 'alert-button-delete',
handler: async () => {
await this.comunitaService.setComunitaCode('');
const toast = await this.toastCtrl.create({
message: 'Comunità disattivata.',
duration: 2000,
color: 'secondary'
});
await toast.present();
}
},
{
text: 'Salva',
handler: async (data) => {
const trimmed = (data.code || '').trim();
if (!trimmed) {
await this.comunitaService.setComunitaCode('');
return;
}
// Show loading overlay with progress
const loading = await this.loadingCtrl.create({
message: 'Caricamento 0%',
cssClass: 'premium-loading',
spinner: 'crescent'
});
await loading.present();
// Subscribe to progress updates
let progressInterval: any = null;
progressInterval = setInterval(() => {
const pct = this.comunitaService.loadingProgress();
loading.message = `Caricamento ${pct}%`;
if (pct >= 100) {
clearInterval(progressInterval);
}
}, 100);
const success = await this.comunitaService.setComunitaCode(trimmed);
clearInterval(progressInterval);
await loading.dismiss();
if (success) {
const toast = await this.toastCtrl.create({
message: `Comunità attivata: ${this.comunitaService.comunitaNome()}`,
duration: 2000,
color: 'success'
});
await toast.present();
} else {
const toast = await this.toastCtrl.create({
message: 'Codice non trovato o errore di connessione.',
duration: 2000,
color: 'danger'
});
await toast.present();
setTimeout(() => this.promptComunitaCode(), 500);
}
}
}
]
});
await alert.present();
}
private massCardStartX: number = 0;
private massCardStartY: number = 0;
onMassCardTouchStart(event: TouchEvent) {
if (event.touches.length === 1) {
this.massCardStartX = event.touches[0].clientX;
this.massCardStartY = event.touches[0].clientY;
}
}
onMassCardTouchEnd(event: TouchEvent) {
if (event.changedTouches.length === 1) {
const endX = event.changedTouches[0].clientX;
const endY = event.changedTouches[0].clientY;
const diffX = endX - this.massCardStartX;
const diffY = endY - this.massCardStartY;
// Ensure it is a horizontal swipe (diffX is much larger than diffY)
if (Math.abs(diffX) > 60 && Math.abs(diffY) < 40) {
// Prevent triggering click expansion
event.stopPropagation();
event.preventDefault();
if (diffX > 0) {
// Swiped right -> go to PREVIOUS date
this.navigateMassDate(-1);
} else {
// Swiped left -> go to NEXT date
this.navigateMassDate(1);
}
}
}
}
async navigateMassDate(direction: number) {
const list = this.cantiLettureService.availableMasses();
if (list.length === 0) return;
const currentDate = this.cantiLettureService.selectedMassDate();
if (!currentDate) {
this.cantiLettureService.selectedMassDate.set(list[0].date);
return;
}
const currentIndex = list.findIndex(m => m.date === currentDate);
if (currentIndex === -1) {
this.cantiLettureService.selectedMassDate.set(list[0].date);
return;
}
const nextIndex = currentIndex + direction;
if (nextIndex >= 0 && nextIndex < list.length) {
// Set the next/prev date
this.cantiLettureService.selectedMassDate.set(list[nextIndex].date);
const toast = await this.toastCtrl.create({
message: `Messa: ${list[nextIndex].day} ${this.formatCompactDate(list[nextIndex].date)}`,
duration: 1500,
color: 'secondary',
position: 'bottom'
});
await toast.present();
} else {
// "quando le messe sono finite rimani sull'ultima valorizzata, non dare errore"
const targetIndex = direction > 0 ? list.length - 1 : 0;
this.cantiLettureService.selectedMassDate.set(list[targetIndex].date);
const toast = await this.toastCtrl.create({
message: direction > 0 ? 'Nessun\'altra messa futura disponibile.' : 'Nessun\'altra messa passata disponibile.',
duration: 1500,
color: 'medium',
position: 'bottom'
});
await toast.present();
}
}
private formatCompactDate(dateStr: string): string {
const parts = dateStr.split('-');
if (parts.length === 3) {
return `${parts[2]}/${parts[1]}`;
}
return dateStr;
}
}