21c9c206e1
- 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.
249 lines
12 KiB
HTML
249 lines
12 KiB
HTML
<ion-header [translucent]="true" class="ion-no-border">
|
|
<ion-toolbar class="bg-gradient top-toolbar">
|
|
<ion-buttons slot="end">
|
|
<div class="offline-badge-header" *ngIf="!connectivityService.isOnline()">
|
|
<ion-icon name="cloud-offline-outline"></ion-icon>
|
|
</div>
|
|
<ion-button fill="clear" (click)="toggleChords()" style="margin: 0; --padding-start: 6px; --padding-end: 6px;">
|
|
<ion-icon slot="icon-only"
|
|
[name]="showChords() ? 'musical-notes-outline' : 'text-outline'"
|
|
[color]="showChords() ? 'secondary' : 'medium'"
|
|
style="font-size: 1.3rem;">
|
|
</ion-icon>
|
|
</ion-button>
|
|
</ion-buttons>
|
|
<ion-title class="outfit-font wrapped-title">
|
|
<div class="title-main" [style.fontSize.rem]="fontSize() * 1.1">
|
|
<span class="canto-number" *ngIf="canto()?.id_canti" [style.color]="getCommunitySongNumber(canto()) !== null ? 'var(--ion-color-secondary)' : ''">
|
|
{{ getCommunitySongNumber(canto()) || canto()?.id_canti }}
|
|
</span>
|
|
<span class="title-text">{{ canto()?.titolo || 'Player' }}</span>
|
|
</div>
|
|
</ion-title>
|
|
<ion-buttons slot="start">
|
|
<ion-back-button text="" defaultHref="/home" color="secondary"></ion-back-button>
|
|
</ion-buttons>
|
|
</ion-toolbar>
|
|
|
|
<!-- Audio Toolbar Removed -->
|
|
</ion-header>
|
|
|
|
<ion-content class="bg-gradient" [class.full-screen-content]="settingsService.fullscreenMode()">
|
|
<div class="lyrics-container"
|
|
[style.fontSize.rem]="fontSize()"
|
|
[class.full-screen-container]="settingsService.fullscreenMode()"
|
|
(touchstart)="onTouchStart($event)"
|
|
(touchmove)="onTouchMove($event)"
|
|
(touchend)="onTouchEnd()">
|
|
|
|
|
|
<!-- 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()">
|
|
<ion-icon name="arrow-up-circle" color="secondary"></ion-icon>
|
|
</ion-button>
|
|
<ion-button fill="clear" (click)="prev()" [disabled]="currentLineIndex() === 0">
|
|
<ion-icon name="chevron-up" color="secondary"></ion-icon>
|
|
</ion-button>
|
|
<ion-button fill="clear" (click)="next()" [disabled]="currentLineIndex() === getTotalLines() - 1">
|
|
<ion-icon name="chevron-down" color="secondary"></ion-icon>
|
|
</ion-button>
|
|
|
|
<!-- Zoom -->
|
|
<ion-button fill="clear" (click)="zoomIn()" [disabled]="fontSize() >= 5.0">
|
|
<ion-icon name="add-circle-outline" color="secondary"></ion-icon>
|
|
</ion-button>
|
|
<ion-button fill="clear" (click)="zoomOut()" [disabled]="fontSize() <= 0.6">
|
|
<ion-icon name="remove-circle-outline" color="secondary"></ion-icon>
|
|
</ion-button>
|
|
|
|
<!-- Karaoke Toggle (Moved down) -->
|
|
<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>
|
|
|
|
<!-- Youtube Link -->
|
|
<ion-button *ngIf="connectivityService.isOnline() && canto()?.link_youtube && canto()?.link_youtube!.length > 5" fill="clear" (click)="openYoutube()">
|
|
<ion-icon name="logo-youtube" color="danger"></ion-icon>
|
|
</ion-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sections rendering -->
|
|
<div class="lyrics-view">
|
|
<div *ngFor="let section of parsedSections(); let si = index"
|
|
class="section"
|
|
[class.chorus]="section.type === 'chorus'"
|
|
[class.verse-num]="section.type === 'verse_num'">
|
|
|
|
<!-- Section label for chorus -->
|
|
<div *ngIf="section.type === 'chorus'" class="section-label">Rit.</div>
|
|
<div *ngIf="section.type === 'verse_num' && section.verseNumber" class="section-label verse-num-label">{{ section.verseNumber }}.</div>
|
|
|
|
<div *ngFor="let line of section.lines; let li = index"
|
|
class="lyric-line"
|
|
[class.active]="isActiveLine(si, li)">
|
|
|
|
<!-- Chord mode: show chords above text -->
|
|
<ng-container *ngIf="showChords(); else textOnly">
|
|
<span *ngFor="let seg of line.segments" class="chord-segment">
|
|
<span *ngIf="seg.chord" class="chord">{{ seg.chord }}</span>
|
|
<span class="seg-text">{{ seg.text }}</span>
|
|
</span>
|
|
</ng-container>
|
|
|
|
<!-- Text only mode -->
|
|
<ng-template #textOnly>
|
|
{{ line.text }}
|
|
</ng-template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</ion-content>
|
|
|
|
<ion-footer class="ion-no-border">
|
|
<!-- Audio Player Toolbar -->
|
|
<ion-toolbar class="global-player-toolbar glass" *ngIf="youtubePlayerService.isPlayerSupported() && canto()?.link_youtube && canto()?.link_youtube!.length > 5 && !settingsService.fullscreenMode()">
|
|
<div class="player-content">
|
|
<div class="controls-row">
|
|
<ion-button fill="clear" color="secondary" (click)="prevSong()" class="skip-btn">
|
|
<ion-icon slot="icon-only" name="play-skip-back-sharp"></ion-icon>
|
|
</ion-button>
|
|
|
|
<ion-button fill="clear" color="secondary" (click)="toggleAudio()" class="play-btn">
|
|
<ion-icon slot="icon-only" [name]="youtubePlayerService.isPlaying() ? 'pause-sharp' : 'play-sharp'"></ion-icon>
|
|
</ion-button>
|
|
|
|
<ion-range
|
|
[min]="0"
|
|
[max]="youtubePlayerService.videoDuration()"
|
|
[value]="youtubePlayerService.videoProgress()"
|
|
(ionChange)="onSeek($event)"
|
|
color="secondary"
|
|
class="global-range">
|
|
</ion-range>
|
|
|
|
<ion-button fill="clear" color="secondary" (click)="nextSong()" class="skip-btn">
|
|
<ion-icon slot="icon-only" name="play-skip-forward-sharp"></ion-icon>
|
|
</ion-button>
|
|
|
|
<ion-button fill="clear" color="medium" (click)="stopVideo($event)" class="close-btn">
|
|
<ion-icon slot="icon-only" name="close-circle-outline"></ion-icon>
|
|
</ion-button>
|
|
</div>
|
|
</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>
|
|
</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 *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>
|
|
|
|
|
|
<!-- Transposition (Only in chords mode) -->
|
|
<div class="group" *ngIf="showChords()">
|
|
<ion-button fill="clear" size="small" (click)="transposeDown()">
|
|
<ion-icon slot="icon-only" name="remove"></ion-icon>
|
|
</ion-button>
|
|
<div class="transpose-indicator">
|
|
<ion-icon name="musical-note" color="secondary"></ion-icon>
|
|
<span class="val" *ngIf="transposeAmount() !== 0">{{ transposeAmount() > 0 ? '+' : '' }}{{ transposeAmount() }}</span>
|
|
</div>
|
|
<ion-button fill="clear" size="small" (click)="transposeUp()">
|
|
<ion-icon slot="icon-only" name="add"></ion-icon>
|
|
</ion-button>
|
|
</div>
|
|
|
|
<!-- Navigation -->
|
|
<div class="group">
|
|
<ion-button fill="clear" size="small" (click)="restart()">
|
|
<ion-icon slot="icon-only" name="arrow-up-circle" color="secondary"></ion-icon>
|
|
</ion-button>
|
|
<ion-button fill="clear" size="small" (click)="prev()" [disabled]="currentLineIndex() === 0">
|
|
<ion-icon slot="icon-only" name="chevron-back" color="secondary"></ion-icon>
|
|
</ion-button>
|
|
<ion-button fill="clear" size="small" (click)="next()" [disabled]="currentLineIndex() === getTotalLines() - 1">
|
|
<ion-icon slot="icon-only" name="chevron-forward" color="secondary"></ion-icon>
|
|
</ion-button>
|
|
</div>
|
|
|
|
<!-- Zoom -->
|
|
<div class="group">
|
|
<ion-button fill="clear" size="small" (click)="zoomOut()" [disabled]="fontSize() <= 0.6">
|
|
<ion-icon slot="icon-only" name="remove-circle-outline" color="secondary"></ion-icon>
|
|
</ion-button>
|
|
<ion-button fill="clear" size="small" (click)="zoomIn()" [disabled]="fontSize() >= 5.0">
|
|
<ion-icon slot="icon-only" name="add-circle-outline" color="secondary"></ion-icon>
|
|
</ion-button>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="group" *ngIf="connectivityService.isOnline()">
|
|
<ion-button *ngIf="canto()?.link_youtube && canto()?.link_youtube!.length > 5" fill="clear" size="small" (click)="openYoutube()">
|
|
<ion-icon slot="icon-only" name="logo-youtube" color="danger"></ion-icon>
|
|
</ion-button>
|
|
</div>
|
|
</div>
|
|
</ion-toolbar>
|
|
</ion-footer>
|
|
|
|
<!-- Vertical Sensitivity Slider Overlay -->
|
|
<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>
|
|
</ion-button>
|
|
<div class="slider-wrapper"
|
|
(touchstart)="handleSensitivityTouch($event)"
|
|
(touchmove)="handleSensitivityTouch($event)">
|
|
<div class="custom-vertical-slider">
|
|
<div class="slider-track"></div>
|
|
<div class="slider-fill" [style.height.%]="(audioEngine.sensitivity() - 50) * 2"></div>
|
|
<div class="slider-knob" [style.bottom.%]="(audioEngine.sensitivity() - 50) * 2"></div>
|
|
</div>
|
|
</div>
|
|
<span class="sensitivity-label outfit-font">{{ audioEngine.sensitivity() }}%</span>
|
|
</div>
|
|
</div>
|