Miglioramenti generali layout player landscape/portrait, traduzione pulsanti navigazione, gestione aggiornamenti PWA e modifiche al posizionamento del pulsante aggiungi canto

This commit is contained in:
David Frassi
2026-06-14 22:14:41 +02:00
parent d023bb7d3f
commit b4b7e10c5f
20 changed files with 1554 additions and 535 deletions
+226 -20
View File
@@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, signal, computed, effect, inject, ElementRef, AfterViewInit, untracked } from '@angular/core';
import { Component, OnInit, OnDestroy, signal, computed, effect, inject, ElementRef, AfterViewInit, untracked, HostListener } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { AlertController, ToastController, GestureController } from '@ionic/angular';
@@ -24,6 +24,24 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
public canto = signal<Canto | null>(null);
public activeSongId = signal<string | null>(null);
@HostListener('window:keydown', ['$event'])
handleKeyDown(event: KeyboardEvent) {
const target = event.target as HTMLElement;
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) {
return;
}
if (event.key === 'ArrowUp') {
this.prevSong();
event.preventDefault();
} else if (event.key === 'ArrowDown') {
this.nextSong();
event.preventDefault();
}
}
public isLandscapeActive = signal<boolean>(window.innerWidth > window.innerHeight);
/** true = show chords (accordi mode), false = text only */
public showChords = signal<boolean>(false);
@@ -44,7 +62,10 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
// For local songs, if accordi is missing but testo has chords, use parseAccordi on testo
const hasChordsInText = !c.accordi && c.testo?.includes('[');
if ((this.showChords() && c.accordi) || (this.showChords() && hasChordsInText)) {
// Force text-only mode in landscape
const activeShowChords = this.showChords() && !this.isLandscapeActive();
if ((activeShowChords && c.accordi) || (activeShowChords && hasChordsInText)) {
sections = this.lyricsParser.parseAccordi(c.accordi || c.testo);
} else {
sections = this.lyricsParser.parseText(c.testo);
@@ -68,7 +89,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
}
// Apply transposition if in chords mode
if (this.showChords()) {
if (activeShowChords) {
return this.lyricsParser.transposeSections(sections, this.transposeAmount());
}
@@ -88,6 +109,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
private readonly MAX_FONT = 5.0;
private readonly FONT_STEP = 0.15;
public minZoom = computed(() => this.isLandscapeActive() ? 2.0 : this.MIN_FONT);
public maxZoom = computed(() => this.isLandscapeActive() ? 3.0 : this.MAX_FONT);
private initialPinchDistance: number | null = null;
private initialFontSize: number = 1.0;
@@ -104,7 +128,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
private route = inject(ActivatedRoute);
public router = inject(Router);
public cantiService = inject(CantiService);
private lyricsParser = inject(LyricsParserService);
public lyricsParser = inject(LyricsParserService);
private alertCtrl = inject(AlertController);
private toastCtrl = inject(ToastController);
public themeService = inject(ThemeService);
@@ -210,6 +234,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
if (scrollEl) {
scrollEl.scrollTop = 0;
}
this.checkLandscapeZoom();
}, 100);
}
}
@@ -269,9 +294,147 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
this.autoscrollSpeed.set(2);
}
}, { allowSignalWrites: true });
// Fullscreen control in landscape mode
effect(() => {
const isLandscape = this.isLandscapeActive();
if (isLandscape) {
this.enterFullscreen();
} else {
this.exitFullscreen();
}
});
}
public isBlackScreen = signal<boolean>(false);
@HostListener('window:resize', ['$event'])
onResize(event: any) {
this.isLandscapeActive.set(window.innerWidth > window.innerHeight);
this.checkLandscapeZoom();
}
private isLandscape(): boolean {
return window.innerWidth > window.innerHeight;
}
private checkAndLimitFontSize(targetFont: number): number {
const titleMain = this.el.nativeElement.querySelector('.title-main');
if (!titleMain) return targetFont;
const originalStyle = titleMain.style.fontSize;
let bestFont = targetFont;
// We want to find the largest font <= targetFont where the title stays on one line.
// Let's iterate down from targetFont to 0.6 in steps of 0.05.
for (let f = targetFont; f >= 0.6; f -= 0.05) {
titleMain.style.fontSize = `${f * 1.1}rem`;
// Force layout calculation
const height = titleMain.clientHeight;
// Get computed line height
const computedStyle = window.getComputedStyle(titleMain);
const lineHeightVal = computedStyle.lineHeight;
let lh = parseFloat(lineHeightVal);
// If line-height is 'normal', fallback to a reasonable estimate based on font-size
if (isNaN(lh) || lineHeightVal === 'normal') {
const fs = parseFloat(computedStyle.fontSize) || (f * 1.1 * 16);
lh = fs * 1.25;
}
// If the actual height is less than 1.5 * line-height, it fits on one line!
if (height <= lh * 1.5) {
bestFont = f;
break;
}
bestFont = f; // Fallback
}
// Restore original style
titleMain.style.fontSize = originalStyle;
return Math.max(0.6, parseFloat(bestFont.toFixed(2)));
}
public checkLandscapeZoom() {
if (this.isLandscape()) {
setTimeout(() => {
const container = this.el.nativeElement.querySelector('.lyrics-container');
if (!container) return;
const visibleHeight = container.clientHeight;
const lineElems = this.el.nativeElement.querySelectorAll('.lyric-line');
if (lineElems.length > 0 && visibleHeight > 0) {
let totalHeight = 0;
lineElems.forEach((el: any) => {
totalHeight += el.getBoundingClientRect().height;
});
const avgLineHeight = totalHeight / lineElems.length;
if (avgLineHeight > 0) {
const currentFont = this.fontSize();
const margin = 16; // 1rem in pixels
const targetLineHeight = Math.max(10, (visibleHeight / 3.1) - margin);
const avgLineHeightAtFont1 = avgLineHeight / currentFont;
const newFont = targetLineHeight / avgLineHeightAtFont1;
const targetFont = Math.max(2.0, Math.min(3.0, newFont));
let limitedFont = this.checkAndLimitFontSize(targetFont);
if (limitedFont < 2.0) {
limitedFont = 2.0;
}
this.fontSize.set(limitedFont);
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
}
}
}, 150);
} else {
const limitedFont = this.checkAndLimitFontSize(1.0);
this.fontSize.set(limitedFont);
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: limitedFont });
}
}
deactivateBlackScreen() {
this.isBlackScreen.set(false);
}
private async enterFullscreen() {
try {
const docEl = document.documentElement;
if (docEl.requestFullscreen) {
await docEl.requestFullscreen();
} else if ((docEl as any).webkitRequestFullscreen) {
await (docEl as any).webkitRequestFullscreen();
} else if ((docEl as any).msRequestFullscreen) {
await (docEl as any).msRequestFullscreen();
}
} catch (e) {
console.warn('[PlayerPage] Failed to enter fullscreen:', e);
}
}
private async exitFullscreen() {
try {
if (document.exitFullscreen) {
if (document.fullscreenElement) {
await document.exitFullscreen();
}
} else if ((document as any).webkitExitFullscreen) {
if ((document as any).webkitFullscreenElement) {
await (document as any).webkitExitFullscreen();
}
} else if ((document as any).msExitFullscreen) {
if ((document as any).msFullscreenElement) {
await (document as any).msExitFullscreen();
}
}
} catch (e) {
console.warn('[PlayerPage] Failed to exit fullscreen:', e);
}
}
ngAfterViewInit() {
this.checkLandscapeZoom();
const gestureX = this.gestureCtrl.create({
el: this.el.nativeElement,
direction: 'x',
@@ -291,6 +454,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
}
});
gestureX.enable();
}
@@ -345,9 +509,15 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
const currentDistance = this.getDistance(event.touches[0], event.touches[1]);
const ratio = currentDistance / this.initialPinchDistance;
let newSize = this.initialFontSize * ratio;
newSize = Math.max(this.MIN_FONT, Math.min(this.MAX_FONT, newSize));
if (Math.abs(newSize - this.fontSize()) > 0.01) {
this.fontSize.set(newSize);
const minZ = this.minZoom();
const maxZ = this.maxZoom();
newSize = Math.max(minZ, Math.min(maxZ, newSize));
let limited = this.checkAndLimitFontSize(newSize);
if (this.isLandscape() && limited < 2.0) {
limited = 2.0;
}
if (Math.abs(limited - this.fontSize()) > 0.01) {
this.fontSize.set(limited);
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
}
}
@@ -358,6 +528,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
this.updatePlaylistSettings();
}
private getDistance(t1: Touch, t2: Touch): number {
return Math.sqrt(Math.pow(t1.clientX - t2.clientX, 2) + Math.pow(t1.clientY - t2.clientY, 2));
}
@@ -366,6 +537,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
this.showChords.update(v => !v);
this.channel.postMessage({ type: 'SYNC_CHORDS', showChords: this.showChords() });
this.transposeAmount.set(0);
this.checkLandscapeZoom();
}
private updatePlaylistSettings() {
@@ -401,16 +573,26 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
}
zoomIn() {
if (this.fontSize() < this.MAX_FONT) {
this.fontSize.update(v => Math.min(v + this.FONT_STEP, this.MAX_FONT));
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
this.updatePlaylistSettings();
const maxZ = this.maxZoom();
if (this.fontSize() < maxZ) {
const target = Math.min(this.fontSize() + this.FONT_STEP, maxZ);
let limited = this.checkAndLimitFontSize(target);
if (this.isLandscape() && limited < 2.0) {
limited = 2.0;
}
if (limited !== this.fontSize()) {
this.fontSize.set(limited);
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
this.updatePlaylistSettings();
}
}
}
zoomOut() {
if (this.fontSize() > this.MIN_FONT) {
this.fontSize.update(v => Math.max(v - this.FONT_STEP, this.MIN_FONT));
const minZ = this.minZoom();
if (this.fontSize() > minZ) {
const target = Math.max(this.fontSize() - this.FONT_STEP, minZ);
this.fontSize.set(target);
this.channel.postMessage({ type: 'SYNC_FONT', fontSize: this.fontSize() });
this.updatePlaylistSettings();
}
@@ -442,7 +624,13 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
this.currentLineIndex.set(0);
this.youtubePlayerService.seekTo(0);
this.channel.postMessage({ type: 'SYNC_INDEX', index: 0 });
this.lastScrollBlock.set('center');
this.lastScrollBlock.set('start');
setTimeout(() => {
const scrollEl = this.el.nativeElement.querySelector('.lyrics-container');
if (scrollEl) {
scrollEl.scrollTop = 0;
}
}, 100);
}
public calculatePageStepSize(): number {
@@ -600,13 +788,16 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
}
if (foundNextIdx !== -1) {
console.log('[PageScroll] Riga coperta/tagliata in fondo rilevata. Diventerà la prima riga della nuova pagina:', foundNextIdx);
return foundNextIdx;
// Overlap by 1 line to ensure the last visible line is repeated at the top of the next page
const targetIdx = Math.max(currentIdx + 1, foundNextIdx - 1);
console.log('[PageScroll] Riga coperta/tagliata in fondo rilevata. Nuova pagina inizierà con overlap a:', targetIdx);
return targetIdx;
}
// Se tutto era perfettamente visibile, avanza dello step di pagina calcolato standard
// Se tutto era perfettamente visibile, avanza dello step di pagina calcolato standard con 1 riga di overlap
const step = this.calculatePageStepSize();
return Math.min(totalLines - 1, currentIdx + step);
const targetIdx = Math.max(currentIdx + 1, currentIdx + step - 1);
return Math.min(totalLines - 1, targetIdx);
} catch (e) {
console.warn('[PageScroll] Errore nel calcolo dinamico dell\'indice della pagina successiva:', e);
return this.currentLineIndex() + 1;
@@ -614,6 +805,10 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
}
next(isAutomatic: boolean = false, isVisual: boolean = false) {
if (this.isBlackScreen()) {
this.deactivateBlackScreen();
return;
}
this.lastAdvanceTimestamp = Date.now();
const totalLines = this.getTotalLines();
@@ -638,7 +833,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
nextIdx = this.calculateNextPageStartIndex();
}
this.lastScrollBlock.set('center');
} else if (this.settingsService.karaokePageScrollMode() || isVisual) {
} else if ((this.settingsService.karaokePageScrollMode() || isVisual) && !this.isLandscapeActive()) {
// Modalità manuale a pagine (o trigger visuale): calcolo analitico preciso per non perdere righe coperte
nextIdx = this.calculateNextPageStartIndex();
this.lastScrollBlock.set('start');
@@ -656,11 +851,15 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
// Metodi di validazione e freeze rimossi per migliorare affidabilità e prevenire blocchi permanenti
prev(isVisual: boolean = false) {
if (this.isBlackScreen()) {
this.deactivateBlackScreen();
return;
}
this.lastAdvanceTimestamp = Date.now();
const prevIdx = this.currentLineIndex();
if (prevIdx > 0) {
const isPageMode = this.settingsService.karaokePageScrollMode() || isVisual;
const isPageMode = (this.settingsService.karaokePageScrollMode() || isVisual) && !this.isLandscapeActive();
const stepSize = isPageMode ? this.calculatePageStepSize() : 1;
const nextIdx = Math.max(0, prevIdx - stepSize);
@@ -690,6 +889,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
const index = list.indexOf(currentId);
if (index >= 0 && index < list.length - 1) {
const nextId = list[index + 1];
if (this.isLandscapeActive()) {
this.isBlackScreen.set(true);
}
this.router.navigate(['/player'], { queryParams: { id: nextId } });
} else {
this.playlistService.autoPlayPlaylist.set(false);
@@ -708,6 +910,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
const index = list.indexOf(currentId);
if (index > 0) {
const prevId = list[index - 1];
if (this.isLandscapeActive()) {
this.isBlackScreen.set(true);
}
this.router.navigate(['/player'], { queryParams: { id: prevId } });
}
}
@@ -949,6 +1154,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
}
ngOnDestroy() {
this.exitFullscreen();
this.stopAutoscroll();
this.logPreviousSongTime();
this.stopCameraNavigation();