feat: allinea lo swipe e lo scroll con l'avanzamento karaoke, aggiungi frecce di navigazione canti nel titolo

This commit is contained in:
David Frassi
2026-06-04 19:09:46 +02:00
parent 960c73fbd0
commit 92e955915e
12 changed files with 820 additions and 151 deletions
+25 -5
View File
@@ -8,9 +8,17 @@
<span class="canto-number" *ngIf="canto()?.id_canti">
{{ canto()?.id?.startsWith('my_') ? 'M' : canto()?.id_canti }}
</span>
<span class="title-text" style="display: inline-flex; align-items: center; flex-wrap: wrap;">
<span class="title-text" style="display: inline-flex; align-items: center; flex-wrap: wrap; gap: 8px;">
<span *ngIf="getCommunitySongNumber(canto())" style="color: var(--ion-color-secondary); font-weight: 600; margin-right: 8px; opacity: 0.9;">{{ getCommunitySongNumber(canto()) }}</span>
<span>{{ canto()?.titolo || 'Player' }}</span>
<span style="display: inline-flex; align-items: center; gap: 4px; margin-left: 8px;">
<ion-button fill="clear" (click)="prevSong()" style="margin: 0; --padding-start: 4px; --padding-end: 4px; height: 28px; width: 28px; min-height: 28px;">
<ion-icon name="chevron-back" color="secondary" style="font-size: 1.1rem;"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="nextSong()" style="margin: 0; --padding-start: 4px; --padding-end: 4px; height: 28px; width: 28px; min-height: 28px;">
<ion-icon name="chevron-forward" color="secondary" style="font-size: 1.1rem;"></ion-icon>
</ion-button>
</span>
</span>
</div>
</ion-title>
@@ -37,7 +45,8 @@
[class.full-screen-container]="settingsService.fullscreenMode()"
(touchstart)="onTouchStart($event)"
(touchmove)="onTouchMove($event)"
(touchend)="onTouchEnd()">
(touchend)="onTouchEnd()"
(wheel)="onWheel($event)">
<!-- Landscape Side Controls (Scrollable) -->
@@ -88,6 +97,8 @@
</ng-template>
</div>
</div>
<!-- Spazio bianco finale per consentire lo scorrimento a pagine perfetto dell'ultima riga in cima allo schermo -->
<div class="bottom-spacer" *ngIf="settingsService.karaokePageScrollMode()" style="height: 75vh; width: 100%; pointer-events: none;"></div>
</div>
</div>
@@ -126,9 +137,18 @@
</div>
</ion-toolbar>
<!-- Voice Activity Visualizer (Slim overlay) -->
<div class="transcript-area slim" *ngIf="settingsService.enableAcousticAutoscroll() && audioEngine.isListening()">
<div class="energy-bar" [style.width.%]="math.min(100, audioEngine.energyLevel() * 3)"></div>
<!-- Visualizzatore Vocale & Log del Riconoscimento in Tempo Reale -->
<div class="transcript-area" *ngIf="settingsService.enableAcousticAutoscroll() && audioEngine.isListening()" style="background: rgba(0, 0, 0, 0.55); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); padding: 10px 14px; border-top: 1px solid rgba(255,255,255,0.08); display: flex; flex-direction: column; gap: 8px; align-items: center; justify-content: center; position: relative;">
<div class="energy-bar-container" style="width: 100%; height: 5px; background: rgba(255,255,255,0.12); border-radius: 3px; overflow: hidden;">
<div class="energy-bar" [style.width.%]="math.min(100, audioEngine.energyLevel() * 3)" style="height: 100%; background: linear-gradient(90deg, var(--ion-color-secondary) 0%, #20e9ff 100%); border-radius: 3px; transition: width 0.08s ease-out;"></div>
</div>
<div class="speech-log-row" style="display: flex; align-items: center; justify-content: center; width: 100%;">
<div class="acoustic-counter-badge" style="display: inline-flex; align-items: center; gap: 6px; background: rgba(32, 233, 255, 0.12); border: 1px solid rgba(32, 233, 255, 0.25); color: #20e9ff; padding: 4px 10px; border-radius: 20px; font-size: 0.75rem; font-weight: 700;">
<span style="width: 6px; height: 6px; background: #20e9ff; border-radius: 50%; display: inline-block; box-shadow: 0 0 8px #20e9ff; animation: pulse 1.5s infinite;"></span>
<span>Parole Rilevate: {{ wordsSpokenInCurrentPage }} / {{ getPageTargetWordCount() }}</span>
</div>
</div>
</div>
<ion-toolbar class="bg-gradient slim-toolbar">
+54 -2
View File
@@ -258,9 +258,7 @@
// Transcript area
.transcript-area {
height: 4px;
width: 100%;
background: rgba(0,0,0,0.2);
.energy-bar {
height: 100%;
@@ -270,6 +268,12 @@
}
}
@keyframes pulse {
0% { transform: scale(0.9); opacity: 0.6; }
50% { transform: scale(1.15); opacity: 1; }
100% { transform: scale(0.9); opacity: 0.6; }
}
// Fullscreen and Orientation
@media (orientation: landscape) {
// Always hide footer in landscape as we have side controls
@@ -560,3 +564,51 @@ ion-content.full-screen-content {
padding-inline-start: 8px;
}
}
.autoscroll-sync-status {
position: fixed;
top: 70px;
left: 50%;
transform: translateX(-50%);
display: inline-flex;
align-items: center;
gap: 8px;
background: rgba(212, 136, 0, 0.18) !important;
border: 1px solid rgba(253, 203, 110, 0.5);
padding: 6px 14px;
border-radius: 16px;
z-index: 999;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
animation: slideDownSync 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
pointer-events: none;
span {
font-size: 0.8rem;
font-weight: 700;
color: #fdcb6e;
letter-spacing: 0.3px;
}
.spin-animation {
animation: spinSync 2s linear infinite;
font-size: 1.1rem;
}
}
@keyframes spinSync {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes slideDownSync {
from {
opacity: 0;
transform: translate(-50%, -20px);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
}
+406 -22
View File
@@ -83,6 +83,8 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
public youtubePlayerService = inject(YoutubePlayerService);
private channel = new BroadcastChannel('karaoke_sync');
// Rimossi stati di freeze fragili per garantire fluidità e stabilità 100% dell'avanzamento
private readonly MIN_FONT = 0.6;
private readonly MAX_FONT = 5.0;
private readonly FONT_STEP = 0.15;
@@ -96,7 +98,8 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
private lastAdvanceTimestamp: number = 0; // For safety cooldown
private readonly ADVANCE_COOLDOWN = 1500; // 1.5 seconds min
private wordsSpokenInCurrentLine: number = 0;
public wordsSpokenInCurrentPage: number = 0;
public lastScrollBlock = signal<'start' | 'center'>('start');
private lastProcessedTranscript: string = '';
private initialStartTime: number = 0;
@@ -132,11 +135,20 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
this.showChords.set(this.settingsService.showChordsDefault());
}, { allowSignalWrites: true });
// Automatic advancement logic based on line detection
// Avanzamento guidato dai picchi acustici con conteggio parole della riga in corso
effect(() => {
const count = this.audioEngine.linesDetected();
if (count > 0) {
this.next();
if (count > 0 && this.audioEngine.isListening()) {
this.handleAcousticPeak();
}
});
// Avanzamento in tempo reale tramite riconoscimento vocale di prossimità (driver primario veloce)
effect(() => {
const transcript = this.audioEngine.backgroundTranscript();
const isSpeechActive = this.audioEngine.speechRecognitionActive();
if (transcript && this.audioEngine.isListening() && isSpeechActive) {
this.checkWordAdvancement();
}
});
@@ -147,7 +159,10 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
setTimeout(() => {
const activeElem = document.querySelector('.lyric-line.active');
if (activeElem) {
activeElem.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Usa l'allineamento calcolato dinamico (start per manuale a pagine, center per automatico e standard)
// Usiamo behavior: 'auto' poiché il container CSS ha già 'scroll-behavior: smooth', evitando blocchi/conflitti nativi Chromium
const scrollBlock = this.lastScrollBlock();
activeElem.scrollIntoView({ behavior: 'auto', block: scrollBlock });
}
}, 100);
});
@@ -241,26 +256,45 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
}
ngAfterViewInit() {
const gesture = this.gestureCtrl.create({
const gestureX = this.gestureCtrl.create({
el: this.el.nativeElement,
direction: 'x',
gestureName: 'swipe-song',
gestureName: 'swipe-song-x',
canStart: (ev) => {
// Prevent swipe when touching the bottom toolbar or other interactive elements
const target = ev.event.target as HTMLElement;
return !target.closest('ion-footer');
return !target.closest('ion-footer') && !target.closest('ion-header');
},
onEnd: (ev) => {
if (Math.abs(ev.deltaX) > 60) {
if (ev.deltaX > 0) {
this.prevSong();
this.prev();
} else {
this.nextSong();
this.next();
}
}
}
});
gesture.enable();
gestureX.enable();
const gestureY = this.gestureCtrl.create({
el: this.el.nativeElement,
direction: 'y',
gestureName: 'swipe-song-y',
canStart: (ev) => {
const target = ev.event.target as HTMLElement;
return !target.closest('ion-footer') && !target.closest('ion-header');
},
onEnd: (ev) => {
if (Math.abs(ev.deltaY) > 50) {
if (ev.deltaY > 0) {
this.prev();
} else {
this.next();
}
}
}
});
gestureY.enable();
}
@@ -327,6 +361,25 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
this.initialPinchDistance = null;
}
private lastWheelTime = 0;
private readonly WHEEL_COOLDOWN = 400; // ms to avoid too rapid advancement
onWheel(event: WheelEvent) {
event.preventDefault();
const now = Date.now();
if (now - this.lastWheelTime < this.WHEEL_COOLDOWN) {
return;
}
if (Math.abs(event.deltaY) > 5) {
if (event.deltaY > 0) {
this.next();
} else {
this.prev();
}
this.lastWheelTime = now;
}
}
private getDistance(t1: Touch, t2: Touch): number {
return Math.sqrt(Math.pow(t1.clientX - t2.clientX, 2) + Math.pow(t1.clientY - t2.clientY, 2));
}
@@ -442,25 +495,356 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
this.audioEngine.resetWordCount();
this.youtubePlayerService.seekTo(0);
this.channel.postMessage({ type: 'SYNC_INDEX', index: 0 });
this.wordsSpokenInCurrentPage = 0;
this.lastScrollBlock.set('center');
}
next() {
this.lastAdvanceTimestamp = Date.now();
this.audioEngine.resetWordCount();
public calculatePageStepSize(): number {
try {
const container = document.querySelector('.lyrics-container') as HTMLElement;
if (!container) return 3; // Ritorno generico se il contenitore non è pronto
const totalLines = this.getTotalLines();
if (this.currentLineIndex() < totalLines - 1) {
this.currentLineIndex.set(this.currentLineIndex() + 1);
this.channel.postMessage({ type: 'SYNC_INDEX', index: this.currentLineIndex() });
const containerHeight = container.clientHeight;
// Calcoliamo la reale altezza visibile sottraendo l'eventuale footer in sovraimpressione
let visibleHeight = containerHeight;
const footer = document.querySelector('ion-footer') as HTMLElement;
if (footer && footer.offsetHeight > 0) {
const computedStyle = window.getComputedStyle(footer);
if (computedStyle.display !== 'none') {
visibleHeight -= footer.offsetHeight;
}
}
const lineElems = document.querySelectorAll('.lyric-line');
if (lineElems.length === 0) return 3;
let totalHeight = 0;
lineElems.forEach((el: any) => {
totalHeight += el.clientHeight;
});
const avgLineHeight = totalHeight / lineElems.length;
if (avgLineHeight <= 0) return 3;
// Quante righe entrano effettivamente nella vista REALE non oscurata dello schermo
const linesPerPage = Math.floor(visibleHeight / avgLineHeight);
// Passo di scorrimento: una pagina intera pulita (zero sovrapposizione) per far sparire il testo precedente
const pageStep = Math.max(1, linesPerPage);
console.log('[PageScroll] Calcolo dinamico dello scorrimento a pagina (Area visibile depurata):', {
containerHeight,
visibleHeight,
avgLineHeight,
linesPerPage,
pageStep
});
return pageStep;
} catch (e) {
console.warn('[PageScroll] Impossibile calcolare dinamicamente lo step di pagina:', e);
return 3; // Fallback di emergenza
}
}
private getLineWords(lineIndex: number): string[] {
const allLines = this.getAllLines();
const lineText = allLines[lineIndex]?.text || '';
return lineText
.toLowerCase()
.split(/\s+/)
.map((w: string) => w.replace(/[^a-zA-Z0-9àèìòùáéíóú]/g, ''))
.filter((w: string) => w.length >= 3);
}
public getPageWords(startIndex: number, pageSize: number): string[] {
const allWords: string[] = [];
const totalLines = this.getTotalLines();
const end = Math.min(totalLines, startIndex + pageSize);
for (let i = startIndex; i < end; i++) {
allWords.push(...this.getLineWords(i));
}
return allWords;
}
public getVisibleLineIndices(): number[] {
try {
const container = document.querySelector('.lyrics-container') as HTMLElement;
if (!container) return [];
const containerRect = container.getBoundingClientRect();
const visibleTop = containerRect.top;
let visibleBottom = containerRect.bottom;
// Sottrai l'altezza dell'eventuale footer sovrapposto ad alto z-index
const footer = document.querySelector('ion-footer') as HTMLElement;
if (footer) {
const footerRect = footer.getBoundingClientRect();
if (footerRect.height > 0 && window.getComputedStyle(footer).display !== 'none') {
visibleBottom = Math.min(visibleBottom, footerRect.top);
}
}
const lineElems = document.querySelectorAll('.lyric-line');
const visibleIndices: number[] = [];
for (let i = 0; i < lineElems.length; i++) {
const el = lineElems[i] as HTMLElement;
const rect = el.getBoundingClientRect();
// Consideriamo la riga visibile se la sua metà verticale è all'interno dei limiti visibili reali
const lineMiddle = (rect.top + rect.bottom) / 2;
if (lineMiddle >= visibleTop && lineMiddle <= visibleBottom) {
visibleIndices.push(i);
}
}
return visibleIndices;
} catch (e) {
console.warn('[PageScroll] Errore nel recupero degli indici delle righe visibili:', e);
return [];
}
}
public getPageTargetWordCount(): number {
const visibleIndices = this.getVisibleLineIndices();
let totalRawWords = 0;
const currentIdx = this.currentLineIndex();
if (visibleIndices.length > 0) {
if (currentIdx === 0) {
// Sulla prima pagina, contiamo solo la prima metà delle righe visibili
// per far avanzare l'applicazione non appena si arriva a metà della prima schermata
const halfLength = Math.max(1, Math.ceil(visibleIndices.length / 2));
for (let i = 0; i < halfLength; i++) {
const idx = visibleIndices[i];
totalRawWords += this.getLineRawWordCount(idx);
}
} else {
// Dalla seconda pagina in poi, contiamo solo le righe attive e future (escludendo il testo già cantato in alto)
for (const idx of visibleIndices) {
if (idx >= currentIdx) {
totalRawWords += this.getLineRawWordCount(idx);
}
}
}
} else {
// Fallback: calcolo teorico basato sulla pagina corrente
const pageSize = this.calculatePageStepSize();
const totalLines = this.getTotalLines();
let end: number;
if (currentIdx === 0) {
// Fallback prima pagina: metà delle righe della pagina teorica
const halfPageSize = Math.max(1, Math.ceil(pageSize / 2));
end = Math.min(totalLines, currentIdx + halfPageSize);
} else {
end = Math.min(totalLines, currentIdx + pageSize);
}
for (let i = currentIdx; i < end; i++) {
totalRawWords += this.getLineRawWordCount(i);
}
}
// Soglia al 70% delle parole contate nella schermata visibile
return Math.max(1, Math.ceil(totalRawWords * 0.70));
}
private checkWordAdvancement() {
const now = Date.now();
// Cooldown per prevenire avanzamenti multipli troppo veloci
if (now - this.lastAdvanceTimestamp < this.ADVANCE_COOLDOWN) {
return;
}
const currentIdx = this.currentLineIndex();
let nextPageIdx: number;
const totalLines = this.getTotalLines();
if (currentIdx === 0) {
// Sulla prima pagina, la "pagina successiva" è in realtà la seconda metà della prima schermata
const visibleIndices = this.getVisibleLineIndices();
if (visibleIndices.length > 0) {
const halfLength = Math.max(1, Math.ceil(visibleIndices.length / 2));
nextPageIdx = visibleIndices[halfLength] !== undefined ? visibleIndices[halfLength] : halfLength;
} else {
const pageSize = this.calculatePageStepSize();
const halfPageSize = Math.max(1, Math.ceil(pageSize / 2));
nextPageIdx = currentIdx + halfPageSize;
}
} else {
nextPageIdx = this.calculateNextPageStartIndex();
}
if (nextPageIdx >= totalLines) return;
const pageSize = this.calculatePageStepSize();
// Raccoglie le parole del blocco successivo (metà pagina se siamo all'inizio, altrimenti intera pagina)
const nextPageWords = this.getPageWords(nextPageIdx, currentIdx === 0 ? Math.max(1, Math.ceil(pageSize / 2)) : pageSize);
if (nextPageWords.length === 0) return;
// Parole rilevate di recente (con tolleranza di ritardo della Speech API)
const recentWords = this.audioEngine.getRecentWordsSince(this.lastAdvanceTimestamp);
// Controlla se l'utente ha cantato parole della pagina successiva -> Avanzamento istantaneo immediato
const matchNext = nextPageWords.some(word => recentWords.includes(word));
if (matchNext) {
console.log(`[SmartKaraoke] Parola chiave del blocco successivo rilevata! Avanzamento immediato a: ${nextPageIdx}`);
this.lastScrollBlock.set('center');
this.currentLineIndex.set(nextPageIdx);
this.channel.postMessage({ type: 'SYNC_INDEX', index: nextPageIdx });
this.lastAdvanceTimestamp = now;
this.audioEngine.resetWordCount();
this.wordsSpokenInCurrentPage = 0;
}
}
public getLineRawWordCount(lineIndex: number): number {
const allLines = this.getAllLines();
const lineText = allLines[lineIndex]?.text || '';
const words = lineText.trim().split(/\s+/).filter((w: string) => w.length > 0);
return words.length;
}
public getLineTargetWordCount(lineIndex: number): number {
const rawCount = this.getLineRawWordCount(lineIndex);
// Soglia proporzionale tollerante (65% del conteggio parole effettivo) per compensare legature melodiche
return Math.max(1, Math.ceil(rawCount * 0.65));
}
private handleAcousticPeak() {
const targetWordCount = this.getPageTargetWordCount();
this.wordsSpokenInCurrentPage++;
console.log(`[PeakAdvance] Peak detected. Spoken words on page: ${this.wordsSpokenInCurrentPage}/${targetWordCount}`);
if (this.wordsSpokenInCurrentPage >= targetWordCount) {
console.log(`[PeakAdvance] Target page word count reached (${targetWordCount}). Advancing page.`);
this.next(true);
}
}
private calculateNextPageStartIndex(): number {
try {
const container = document.querySelector('.lyrics-container') as HTMLElement;
if (!container) return this.currentLineIndex() + 1;
const containerRect = container.getBoundingClientRect();
let visibleBottom = containerRect.bottom;
// Trova la posizione del footer o di qualunque elemento sovrapposto in fondo
const footer = document.querySelector('ion-footer') as HTMLElement;
if (footer) {
const footerRect = footer.getBoundingClientRect();
if (footerRect.height > 0 && window.getComputedStyle(footer).display !== 'none') {
visibleBottom = Math.min(visibleBottom, footerRect.top);
}
}
// Sottrai un piccolo margine di tolleranza di 8px
const visibleLimitY = visibleBottom - 8;
const lineElems = document.querySelectorAll('.lyric-line');
const totalLines = this.getTotalLines();
const currentIdx = this.currentLineIndex();
// Scansiona le righe successive a quella attiva e trova la prima riga che non era COMPLETAMENTE visibile
let foundNextIdx = -1;
for (let i = currentIdx + 1; i < lineElems.length; i++) {
const el = lineElems[i] as HTMLElement;
const rect = el.getBoundingClientRect();
// Se la parte inferiore della riga ricade sotto l'area di visualizzazione utile (coperta da footer/log)
if (rect.bottom > visibleLimitY) {
foundNextIdx = i;
break;
}
}
if (foundNextIdx !== -1) {
console.log('[PageScroll] Riga coperta/tagliata in fondo rilevata. Diventerà la prima riga della nuova pagina:', foundNextIdx);
return foundNextIdx;
}
// Se tutto era perfettamente visibile, avanza dello step di pagina calcolato standard
const step = this.calculatePageStepSize();
return Math.min(totalLines - 1, currentIdx + step);
} catch (e) {
console.warn('[PageScroll] Errore nel calcolo dinamico dell\'indice della pagina successiva:', e);
return this.currentLineIndex() + 1;
}
}
next(isAutomatic: boolean = false) {
this.lastAdvanceTimestamp = Date.now();
this.audioEngine.resetWordCount();
this.wordsSpokenInCurrentPage = 0;
const totalLines = this.getTotalLines();
const prevIdx = this.currentLineIndex();
if (prevIdx < totalLines - 1) {
let nextIdx: number;
if (isAutomatic) {
if (prevIdx === 0) {
// Sulla prima pagina, avanziamo alla metà della pagina (inizio della seconda metà)
const visibleIndices = this.getVisibleLineIndices();
if (visibleIndices.length > 0) {
const halfLength = Math.max(1, Math.ceil(visibleIndices.length / 2));
nextIdx = Math.min(totalLines - 1, visibleIndices[halfLength] !== undefined ? visibleIndices[halfLength] : halfLength);
} else {
const pageSize = this.calculatePageStepSize();
const halfPageSize = Math.max(1, Math.ceil(pageSize / 2));
nextIdx = Math.min(totalLines - 1, prevIdx + halfPageSize);
}
} else {
// Avanzamento acustico automatico: avanza l'intera pagina lasciando centrale la riga di riferimento
nextIdx = this.calculateNextPageStartIndex();
}
this.lastScrollBlock.set('center');
} else if (this.settingsService.karaokePageScrollMode()) {
// Modalità manuale a pagine: calcolo analitico preciso per non perdere righe coperte
nextIdx = this.calculateNextPageStartIndex();
this.lastScrollBlock.set('start');
} else {
// Avanzamento riga per riga standard manuale
nextIdx = prevIdx + 1;
this.lastScrollBlock.set('center');
}
this.currentLineIndex.set(nextIdx);
this.channel.postMessage({ type: 'SYNC_INDEX', index: nextIdx });
}
}
// Metodi di validazione e freeze rimossi per migliorare affidabilità e prevenire blocchi permanenti
prev() {
this.lastAdvanceTimestamp = Date.now();
this.audioEngine.resetWordCount();
if (this.currentLineIndex() > 0) {
this.currentLineIndex.set(this.currentLineIndex() - 1);
this.channel.postMessage({ type: 'SYNC_INDEX', index: this.currentLineIndex() });
this.wordsSpokenInCurrentPage = 0;
const prevIdx = this.currentLineIndex();
if (prevIdx > 0) {
const isPageMode = this.settingsService.karaokePageScrollMode();
const stepSize = isPageMode ? this.calculatePageStepSize() : 1;
const nextIdx = Math.max(0, prevIdx - stepSize);
if (nextIdx === 0) {
// Se torniamo all'inizio, allineiamo in alto
this.lastScrollBlock.set('start');
} else if (isPageMode) {
this.lastScrollBlock.set('start');
} else {
this.lastScrollBlock.set('center');
}
this.currentLineIndex.set(nextIdx);
this.channel.postMessage({ type: 'SYNC_INDEX', index: nextIdx });
}
}
@@ -134,6 +134,14 @@
</ion-label>
<ion-toggle slot="end" [checked]="settingsService.enableAcousticAutoscroll()" (ionChange)="settingsService.toggleAcousticAutoscroll()" color="secondary"></ion-toggle>
</ion-item>
<ion-item class="transparent-item" lines="none" style="border-top: 1px solid rgba(255,255,255,0.03);">
<ion-icon name="book-outline" slot="start" color="secondary"></ion-icon>
<ion-label class="outfit-font">
<h2 class="settings-item-title">Avanzamento manuale a pagine</h2>
<p class="settings-item-subtitle">I tasti Avanti/Indietro (Karaoke) voltano la pagina intera anziché riga per riga</p>
</ion-label>
<ion-toggle slot="end" [checked]="settingsService.karaokePageScrollMode()" (ionChange)="settingsService.toggleKaraokePageScrollMode()" color="secondary"></ion-toggle>
</ion-item>
</div>
<!-- Comunità -->
+22 -2
View File
@@ -43,6 +43,26 @@ export class SettingsPage {
constructor() {}
private async forceBypassCacheAndCheck(): Promise<boolean> {
try {
// 1. Forza il browser mobile a controllare la rete per aggiornamenti al Service Worker nativo
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) {
await registration.update();
console.log('[PWA-Update] Native Service Worker updated from settings');
}
}
// 2. Forza il caricamento di ngsw.json bypassando le cache intermedie e locali
await fetch(`/ngsw.json?cb=${Date.now()}`, { cache: 'no-store' });
await fetch('/ngsw.json', { cache: 'reload' });
console.log('[PWA-Update] Caches successfully busted for ngsw.json');
} catch (e) {
console.warn('[PWA-Update] Failed to bust cache for ngsw.json:', e);
}
return await this.swUpdate.checkForUpdate();
}
async installApp() {
await this.settingsService.installPwa();
}
@@ -83,7 +103,7 @@ export class SettingsPage {
// 4. Check for Service Worker updates
if (this.swUpdate.isEnabled) {
try {
const updateFound = await this.swUpdate.checkForUpdate();
const updateFound = await this.forceBypassCacheAndCheck();
if (updateFound) {
const toast = await this.toastCtrl.create({
message: 'Nuova versione disponibile! Aggiornamento in corso...',
@@ -143,7 +163,7 @@ export class SettingsPage {
await toastLoading.present();
try {
const updateFound = await this.swUpdate.checkForUpdate();
const updateFound = await this.forceBypassCacheAndCheck();
if (updateFound) {
const toast = await this.toastCtrl.create({
message: 'Nuova versione trovata! Installazione e attivazione in corso...',