import { Component, OnInit, OnDestroy, signal, computed, effect, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { CantiService, Canto } from '../../services/canti.service'; import { LyricsParserService, ParsedSection } from '../../services/lyrics-parser.service'; @Component({ selector: 'app-display', templateUrl: './display.page.html', styleUrls: ['./display.page.scss'], standalone: false }) export class DisplayPage implements OnInit, OnDestroy { public canto = signal(null); public activeSongId = signal(null); public showChords = signal(false); public fontSize = signal(1.0); public currentLineIndex = signal(0); public parsedSections = computed(() => { const c = this.canto(); if (!c) return []; if (this.showChords() && c.accordi) { return this.lyricsParser.parseAccordi(c.accordi); } return this.lyricsParser.parseText(c.testo); }); /** Get the current line text and surrounding lines from flat index */ public displayLines = computed(() => { const sections = this.parsedSections(); const idx = this.currentLineIndex(); const allLines: { text: string; segments: any[]; sectionType: string }[] = []; for (const section of sections) { for (const line of section.lines) { allLines.push({ text: line.text, segments: line.segments, sectionType: section.type }); } } return { prev: idx > 0 ? allLines[idx - 1] : null, current: allLines[idx] || null, next: idx < allLines.length - 1 ? allLines[idx + 1] : null }; }); private channel = new BroadcastChannel('karaoke_sync'); private readonly MIN_FONT = 0.6; private readonly MAX_FONT = 5.0; private initialPinchDistance: number | null = null; private initialFontSize: number = 1.0; private route = inject(ActivatedRoute); private cantiService = inject(CantiService); private lyricsParser = inject(LyricsParserService); constructor() { effect(() => { const id = this.activeSongId(); const allCanti = this.cantiService.canti(); if (id) { const found = allCanti.find(c => c.id === id); if (found) { this.canto.set(found); } } }, { allowSignalWrites: true }); } ngOnInit() { this.route.queryParams.subscribe(params => { const id = params['id']; if (id) { this.activeSongId.set(id); } }); this.channel.onmessage = (event) => { if (event.data.type === 'SYNC_INDEX') { this.currentLineIndex.set(event.data.index); } if (event.data.type === 'SYNC_CANTO') { this.activeSongId.set(event.data.id); } if (event.data.type === 'SYNC_CHORDS') { this.showChords.set(event.data.showChords); } if (event.data.type === 'SYNC_FONT') { this.fontSize.set(event.data.fontSize); } }; } ngOnDestroy() { this.channel.close(); } // --- 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) { event.preventDefault(); // Prevent browser zoom/scroll 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); // Sync back to player if display is touched this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() }); } } } 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)); } // ---------------------------------------- }