1107 lines
34 KiB
TypeScript
1107 lines
34 KiB
TypeScript
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;
|
||
}
|
||
}
|
||
|