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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user