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