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(''); public selectedLiturgico = signal(null); public selectedTematico = signal(null); public showOnlyMine = signal(false); public showTopTen = signal(false); public showSuggeriti = signal(false); public isMassCardExpanded = signal(false); public activeFilterType = signal<'liturgico' | 'tematico' | 'playlist' | null>(null); public loadedThumbs = new Set(); public version = VERSION; public isAddingSongs = signal(false); public reorderList = signal([]); public limit = signal(30); public fontSize = signal(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(); 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) { 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; } }