feat: autoscroll standard, visualizzazione tag/date e OCR spaziale avanzato

- Aggiunto autoscroll standard con controllo velocità (V1-V10) e relativo toggle in settings.
- Implementata la visualizzazione opzionale di tag e data aggiornamento nella lista canti.
- Rinnovata la pagina 'Proponi Canto' con tastiera accordi (fondamentale + variazioni) e OCR spaziale potenziato per l'allineamento automatico degli accordi con il testo.
- Ottimizzata la persistenza della modalità schermo intero.
This commit is contained in:
David Frassi
2026-05-20 11:26:59 +02:00
parent af12a1f2da
commit 21c9c206e1
15 changed files with 970 additions and 225 deletions
+32 -4
View File
@@ -40,6 +40,20 @@
<!-- Landscape Side Controls (Scrollable) -->
<div class="landscape-side-controls" [class.active-fullscreen]="settingsService.fullscreenMode()">
<div class="side-scroll-container">
<!-- Autoscroll group in Landscape Side -->
<div class="side-group" *ngIf="settingsService.enableStandardAutoscroll()" style="gap: 4px; padding: 4px 0; background: rgba(255,255,255,0.05); border-radius: 12px; border: 1px solid rgba(255,255,255,0.1); width: 44px; margin: 0 auto; display: flex; flex-direction: column; align-items: center; justify-content: center;">
<ion-button fill="clear" size="small" (click)="increaseAutoscrollSpeed()" [disabled]="autoscrollSpeed() >= 10" style="height: 32px; margin: 0;">
<ion-icon name="add" style="font-size: 1.2rem; color: var(--ion-color-secondary);"></ion-icon>
</ion-button>
<div (click)="toggleAutoscroll()" style="cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 2px;">
<ion-icon [name]="isAutoscrolling() ? 'pause' : 'play'" [color]="isAutoscrolling() ? 'danger' : 'secondary'" style="font-size: 1.4rem;"></ion-icon>
<span style="font-size: 0.65rem; font-weight: 700; color: var(--ion-color-secondary);">V{{ autoscrollSpeed() }}</span>
</div>
<ion-button fill="clear" size="small" (click)="decreaseAutoscrollSpeed()" [disabled]="autoscrollSpeed() <= 1" style="height: 32px; margin: 0;">
<ion-icon name="remove" style="font-size: 1.2rem; color: var(--ion-color-secondary);"></ion-icon>
</ion-button>
</div>
<div class="side-group equidistant-group">
<!-- Navigation -->
<ion-button fill="clear" (click)="restart()">
@@ -61,7 +75,7 @@
</ion-button>
<!-- Karaoke Toggle (Moved down) -->
<ion-button fill="clear" (click)="toggleListening()" [color]="audioEngine.isListening() ? 'danger' : 'secondary'">
<ion-button *ngIf="settingsService.enableAcousticAutoscroll()" fill="clear" (click)="toggleListening()" [color]="audioEngine.isListening() ? 'danger' : 'secondary'">
<ion-icon [name]="audioEngine.isListening() ? 'mic' : 'mic-off'"></ion-icon>
</ion-button>
@@ -141,14 +155,28 @@
</ion-toolbar>
<!-- Voice Activity Visualizer (Slim overlay) -->
<div class="transcript-area slim" *ngIf="audioEngine.isListening()">
<div class="transcript-area slim" *ngIf="settingsService.enableAcousticAutoscroll() && audioEngine.isListening()">
<div class="energy-bar" [style.width.%]="math.min(100, audioEngine.energyLevel() * 3)"></div>
</div>
<ion-toolbar class="bg-gradient slim-toolbar">
<div class="slim-controls">
<!-- Autoscroll Standard in Portrait Footer -->
<div class="group" *ngIf="settingsService.enableStandardAutoscroll()">
<ion-button fill="clear" size="small" (click)="decreaseAutoscrollSpeed()" [disabled]="autoscrollSpeed() <= 1">
<ion-icon slot="icon-only" name="remove"></ion-icon>
</ion-button>
<div class="autoscroll-indicator" (click)="toggleAutoscroll()" style="display: flex; align-items: center; gap: 4px; padding: 0 4px; cursor: pointer;">
<ion-icon [name]="isAutoscrolling() ? 'pause' : 'play'" [color]="isAutoscrolling() ? 'danger' : 'secondary'"></ion-icon>
<span style="font-size: 0.75rem; font-weight: 700; color: var(--ion-color-secondary); min-width: 32px; text-align: center;">V{{ autoscrollSpeed() }}</span>
</div>
<ion-button fill="clear" size="small" (click)="increaseAutoscrollSpeed()" [disabled]="autoscrollSpeed() >= 10">
<ion-icon slot="icon-only" name="add"></ion-icon>
</ion-button>
</div>
<!-- Karaoke Toggle in Portrait -->
<ion-button fill="clear" size="small" (click)="toggleListening()" [color]="audioEngine.isListening() ? 'danger' : 'secondary'" class="mic-btn-portrait">
<ion-button *ngIf="settingsService.enableAcousticAutoscroll()" fill="clear" size="small" (click)="toggleListening()" [color]="audioEngine.isListening() ? 'danger' : 'secondary'" class="mic-btn-portrait">
<ion-icon slot="icon-only" [name]="audioEngine.isListening() ? 'mic' : 'mic-off'"></ion-icon>
</ion-button>
@@ -201,7 +229,7 @@
</ion-footer>
<!-- Vertical Sensitivity Slider Overlay -->
<div class="mic-sensitivity-overlay" *ngIf="showSensitivitySlider() && audioEngine.isListening()">
<div class="mic-sensitivity-overlay" *ngIf="settingsService.enableAcousticAutoscroll() && showSensitivitySlider() && audioEngine.isListening()">
<div class="slider-card glass">
<ion-button fill="clear" color="secondary" (click)="toggleSensitivitySlider($event)" class="close-slider-btn">
<ion-icon name="close-outline"></ion-icon>
+83
View File
@@ -449,3 +449,86 @@ ion-content.full-screen-content {
transform: translateX(0);
}
}
.floating-autoscroll-bar {
position: fixed;
bottom: 120px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 12px;
background: rgba(18, 18, 18, 0.85) !important;
backdrop-filter: blur(20px);
border: 1px solid rgba(var(--ion-color-secondary-rgb), 0.3);
padding: 6px 16px;
border-radius: 30px;
z-index: 999;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
animation: slideUp 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
pointer-events: auto;
&.fullscreen-active {
bottom: 24px !important;
}
&.high-contrast {
background: #ffffff !important;
border-color: #555555 !important;
box-shadow: none !important;
.autoscroll-control-center {
background: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.15);
.speed-badge {
color: #000000 !important;
}
}
}
ion-button {
--padding-start: 4px;
--padding-end: 4px;
margin: 0;
ion-icon {
font-size: 1.4rem;
}
}
.autoscroll-control-center {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
background: rgba(255, 255, 255, 0.05);
padding: 4px 10px;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.2s ease;
&:active {
transform: scale(0.95);
}
.speed-badge {
font-size: 0.75rem;
font-weight: 700;
color: var(--ion-color-secondary);
letter-spacing: 0.5px;
}
}
}
:host-context(body.high-contrast) {
.slim-controls .group {
background: rgba(0, 0, 0, 0.05) !important;
border: 1px solid rgba(0, 0, 0, 0.15) !important;
}
.landscape-side-controls .side-scroll-container .side-group {
background: rgba(0, 0, 0, 0.05) !important;
border: 1px solid rgba(0, 0, 0, 0.15) !important;
}
}
+47
View File
@@ -27,6 +27,11 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
public showChords = signal<boolean>(false);
public showSensitivitySlider = signal<boolean>(false);
/** Autoscroll standard */
public isAutoscrolling = signal<boolean>(false);
public autoscrollSpeed = signal<number>(2);
private autoscrollTimer: any = null;
/** Font size scale factor (1.0 = default) */
public fontSize = signal<number>(1.0);
@@ -524,6 +529,47 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
return info && info.num_canto ? info.num_canto.toString() : null;
}
toggleAutoscroll() {
if (this.isAutoscrolling()) {
this.stopAutoscroll();
} else {
this.startAutoscroll();
}
}
startAutoscroll() {
this.isAutoscrolling.set(true);
if (this.autoscrollTimer) clearInterval(this.autoscrollTimer);
const scrollEl = this.el.nativeElement.querySelector('.lyrics-container');
if (!scrollEl) return;
this.autoscrollTimer = setInterval(() => {
if (!this.isAutoscrolling()) {
clearInterval(this.autoscrollTimer);
return;
}
const step = this.autoscrollSpeed() * 0.25;
scrollEl.scrollTop += step;
}, 40);
}
stopAutoscroll() {
this.isAutoscrolling.set(false);
if (this.autoscrollTimer) {
clearInterval(this.autoscrollTimer);
this.autoscrollTimer = null;
}
}
increaseAutoscrollSpeed() {
this.autoscrollSpeed.update(s => Math.min(10, s + 1));
}
decreaseAutoscrollSpeed() {
this.autoscrollSpeed.update(s => Math.max(1, s - 1));
}
private logPreviousSongTime() {
const c = this.canto();
if (c && this.songStartTime > 0) {
@@ -534,6 +580,7 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy {
}
ngOnDestroy() {
this.stopAutoscroll();
this.logPreviousSongTime();
this.audioEngine.stopListening();
this.channel.close();