diff --git a/allinealetture.sh b/allinealetture.sh new file mode 100755 index 0000000..941d757 --- /dev/null +++ b/allinealetture.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# --- Caricamento variabili d'ambiente --- +if [ -f .env ]; then + export $(grep -v '^#' .env | xargs) +else + echo "❌ Errore: File .env non trovato." + exit 1 +fi + +# --- Configurazione FTP --- +FTP_HOST="ftp.canticristiani.it" +FTP_USER="canticristiani.it" +FTP_PASS="$FTP_PASSWORD" +SOURCE_URL="http://185.193.67.105:3000/cantiletture.json" +REMOTE_PATH="api/cantiletture.json" + +if [ -z "$FTP_PASS" ]; then + echo "❌ Errore: FTP_PASSWORD non definita nel file .env" + exit 1 +fi + +# --- Download del file --- +echo "⬇️ Scaricamento dati da $SOURCE_URL..." +TEMP_FILE=$(mktemp) +if ! curl -s -L "$SOURCE_URL" -o "$TEMP_FILE"; then + echo "❌ Errore nel download dei dati." + rm "$TEMP_FILE" + exit 1 +fi + +# --- Upload via FTP --- +echo "🚀 Caricamento nuovo file su $FTP_HOST/$REMOTE_PATH..." +if curl -s -u "$FTP_USER:$FTP_PASS" --ftp-pasv --ftp-create-dirs -T "$TEMP_FILE" "ftp://$FTP_HOST/$REMOTE_PATH"; then + echo "✅ Allineamento completato con successo!" +else + echo "❌ Errore durante il caricamento FTP." + rm "$TEMP_FILE" + exit 1 +fi + +# Cleanup +rm "$TEMP_FILE" diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 64a0fa4..cd6f74a 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -27,10 +27,6 @@ const routes: Routes = [ path: 'propose-canto', loadComponent: () => import('./pages/propose-canto/propose-canto.page').then( m => m.ProposeCantoPage) }, - { - path: 'liturgia', - loadChildren: () => import('./pages/liturgia/liturgia.module').then( m => m.LiturgiaPageModule) - }, ]; @NgModule({ diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index d3c3278..8ac9fec 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -10,10 +10,6 @@ -
- -
- @@ -83,6 +79,17 @@ + + + Suggeriti + + + + + + + Top Ten + + + + @@ -108,6 +126,9 @@
{{ playlistService.selectedIds().size }} + + + @@ -162,6 +183,32 @@ (cdkDropListDropped)="drop($event)" class="transparent-list"> + +
+
+
+
+ SINTESI MESSA + {{ getSelectedMassFormattedDate() }} +
+ +
+ +
+
+ {{ getSelectedMassFormattedDate() }} +
+

{{ getSelectedMassTitle() }}

+

+ {{ getSelectedMassSummary() }} +

+
+
+
+ {{ canto.titolo }} -

{{ canto.autore || 'Autore sconosciuto' }}

+

+ {{ canto.autore || 'Autore sconosciuto' }} + + + {{ getEsecuzioniCount(canto.id) }} esecuzioni + + + + Attinenza {{ getMassSuggestionWeight(canto.id_canti) }}% + +

- +
- +
-
- -
+ + +
+ +
+
@@ -249,7 +312,7 @@ - +
diff --git a/src/app/home/home.page.scss b/src/app/home/home.page.scss index 02adce7..31062d0 100644 --- a/src/app/home/home.page.scss +++ b/src/app/home/home.page.scss @@ -31,7 +31,7 @@ .search-wrapper-group { display: flex; flex-direction: column; - padding: 8px 16px 12px 16px; // Added top padding for extra clearance + padding: 2px 16px 12px 16px; // Reduced top padding gap: 12px; } @@ -174,7 +174,7 @@ ion-title { align-items: center; justify-content: flex-start; gap: 12px; - padding: 20px 0 16px 24px; // Increased padding for mobile spacing + padding: 16px 0 4px 24px; // Reduced padding to bring search bar closer } .header-logo { @@ -661,7 +661,7 @@ ion-title { justify-content: center; padding: 0 12px 0 0; cursor: grab; - color: rgba(255, 255, 255, 0.3); + color: rgba(255, 255, 255, 0.5); ion-icon { font-size: 1.8rem; @@ -748,3 +748,143 @@ ion-title { .transparent-list { padding-bottom: 100px !important; } + +.suggeriti-header-card { + border-radius: 18px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + overflow: hidden; + margin-left: 0; + margin-right: 0; + margin-bottom: 12px; + backdrop-filter: blur(15px); + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: background 0.3s ease, border-color 0.3s ease; + + &:active { + background: rgba(255, 255, 255, 0.08); + } + + .card-inner { + display: flex; + flex-direction: column; + gap: 0; + } + + .card-header-row { + display: flex; + justify-content: space-between; + align-items: center; + transition: padding-bottom 0.3s ease, border-bottom-color 0.3s ease; + } + + &.expanded { + .card-header-row { + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + padding-bottom: 6px; + } + } + + .card-header-left { + display: flex; + align-items: center; + } + + .expand-icon { + font-size: 1.25rem; + color: var(--ion-color-secondary); + transition: transform 0.3s ease; + } + + .card-label { + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.1em; + color: var(--ion-color-secondary); + } + + .card-date-compact { + font-size: 0.75rem; + font-weight: 600; + color: rgba(255, 255, 255, 0.6); + margin-left: 8px; + } + + .expandable-content { + max-height: 0; + opacity: 0; + overflow: hidden; + transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.35s ease, margin-top 0.35s ease; + + &.show { + max-height: 400px; /* High enough to fit summary */ + opacity: 1; + margin-top: 10px; + } + } + + .card-date-row { + margin-bottom: 6px; + } + + .card-date { + font-size: 0.75rem; + font-weight: 600; + color: rgba(255, 255, 255, 0.5); + } + + .card-title { + font-size: 1.25rem; + font-weight: 700; + color: white; + margin: 4px 0 0 0; + } + + .card-summary { + font-size: 0.85rem; + line-height: 1.45; + color: rgba(255, 255, 255, 0.75); + margin: 6px 0 0 0; + font-weight: 400; + } +} + +:host-context(body.high-contrast) .suggeriti-header-card { + background: #ffffff !important; + border-color: #000000 !important; + box-shadow: none !important; + + .card-date-compact { + color: black !important; + background: transparent !important; + } + + &.expanded { + .card-header-row { + border-bottom-color: rgba(0, 0, 0, 0.15); + } + } + + .card-label { + color: black !important; + } + + .card-date { + color: black !important; + } + + .card-title { + color: black !important; + } + + .card-summary { + color: black !important; + } +} + +:host-context(body.high-contrast) { + .drag-handle { + color: rgba(0, 0, 0, 0.6) !important; + } +} diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index c6e39fc..9be0c68 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -13,6 +13,7 @@ import { ModalController, AlertController, ToastController } from '@ionic/angula import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { MyCantiService } from '../services/my-canti.service'; import { QrScannerComponent } from '../components/qr-scanner/qr-scanner.component'; +import { CantiLettureService } from '../services/canti-letture.service'; @Component({ selector: 'app-home', @@ -25,6 +26,9 @@ export class HomePage implements OnDestroy { public selectedLiturgico = signal(null); public selectedTematico = signal(null); public showOnlyMine = signal(false); + public showTopTen = signal(false); + public showSuggeriti = signal(false); + public isMassCardExpanded = signal(false); public activeFilterType = signal<'liturgico' | 'tematico' | 'playlist' | null>(null); public loadedThumbs = new Set(); public version = VERSION; @@ -49,6 +53,7 @@ export class HomePage implements OnDestroy { public myCantiService = inject(MyCantiService); public playlistService = inject(PlaylistService); public settingsService = inject(SettingsService); + public cantiLettureService = inject(CantiLettureService); private router = inject(Router); private route = inject(ActivatedRoute); private modalCtrl = inject(ModalController); @@ -93,6 +98,8 @@ export class HomePage implements OnDestroy { const litId = this.selectedLiturgico(); const temId = this.selectedTematico(); const onlyMine = this.showOnlyMine(); + const topTen = this.showTopTen(); + const suggeriti = this.showSuggeriti(); let list = [...this.cantiService.canti(), ...this.myCantiService.myCanti()]; @@ -107,12 +114,17 @@ export class HomePage implements OnDestroy { list = list.filter(c => c.id_momenti?.includes(temId)); } + if (suggeriti) { + const suggMap = this.cantiLettureService.suggestionsMap(); + list = list.filter(c => suggMap.has(c.id_canti)); + } + // Special List handling (from QR Code or Saved Playlists) const activeIds = this.playlistService.activeListIds(); const selectionMode = this.playlistService.selectionMode(); - // If we are in selection mode, only show restricted list if NOT adding songs AND we have a base list to reorder - if (selectionMode && !this.isAddingSongs() && activeIds.length > 0) { + // If we are in selection mode, only show restricted list if NOT adding songs + if (selectionMode && !this.isAddingSongs()) { return this.reorderList(); } @@ -124,6 +136,25 @@ export class HomePage implements OnDestroy { .filter((c): c is any => !!c); } + if (suggeriti) { + const suggMap = this.cantiLettureService.suggestionsMap(); + list = list.sort((a, b) => { + const pesoA = suggMap.get(a.id_canti) || 0; + const pesoB = suggMap.get(b.id_canti) || 0; + return pesoB - pesoA; + }); + } else if (topTen) { + const eseguiti = this.cantiService.cantiEseguiti(); + const eseguitiMap = new Map(); + eseguiti.forEach(x => eseguitiMap.set(x.id_canti, x.num)); + + list = list.sort((a, b) => { + const numA = eseguitiMap.get(a.id_canti) || 0; + const numB = eseguitiMap.get(b.id_canti) || 0; + return numB - numA; + }); + } + if (!query) return list; // Converte "uno" in "1" per facilitare la ricerca vocale (es. "numero uno") @@ -170,15 +201,22 @@ export class HomePage implements OnDestroy { } }); + let prevSelectionMode = false; + effect(() => { const selectionMode = this.playlistService.selectionMode(); const selectedIds = this.playlistService.selectedIds(); const activeIds = this.playlistService.activeListIds(); if (selectionMode) { - if (activeIds.length === 0 && !this.isAddingSongs()) { - this.isAddingSongs.set(true); + if (!prevSelectionMode) { + if (activeIds.length === 0) { + this.isAddingSongs.set(true); + } else { + this.isAddingSongs.set(false); + } } + prevSelectionMode = true; const currentIds = this.reorderList().map(c => c.id); const newIds = Array.from(selectedIds); @@ -208,6 +246,7 @@ export class HomePage implements OnDestroy { this.reorderList.set(updatedList); } } else { + prevSelectionMode = false; this.isAddingSongs.set(false); } }, { allowSignalWrites: true }); @@ -267,11 +306,6 @@ export class HomePage implements OnDestroy { } goToCanto(id: string) { - if (this.playlistService.selectionMode()) { - this.playlistService.toggleSongSelection(id); - return; - } - const params: any = { id }; const isPlaying = this.youtubePlayerService.isPlaying(); @@ -379,6 +413,8 @@ export class HomePage implements OnDestroy { return this.selectedLiturgico() !== null || this.selectedTematico() !== null || this.showOnlyMine() || + this.showTopTen() || + this.showSuggeriti() || this.playlistService.activeListName() !== null || this.searchQuery() !== ''; } @@ -387,6 +423,8 @@ export class HomePage implements OnDestroy { this.selectedLiturgico.set(null); this.selectedTematico.set(null); this.showOnlyMine.set(false); + this.showTopTen.set(false); + this.showSuggeriti.set(false); this.playlistService.activeListIds.set([]); this.playlistService.activeListName.set(null); this.playlistService.activePlaylistId.set(null); @@ -415,6 +453,73 @@ export class HomePage implements OnDestroy { this.showOnlyMine.set(false); } + toggleTopTen() { + this.showTopTen.update(v => !v); + this.limit.set(30); + } + + clearTopTen(event?: Event) { + if (event) event.stopPropagation(); + this.showTopTen.set(false); + this.limit.set(30); + } + + toggleSuggeriti() { + this.showSuggeriti.update(v => !v); + this.limit.set(30); + } + + clearSuggeriti(event?: Event) { + if (event) event.stopPropagation(); + this.showSuggeriti.set(false); + this.limit.set(30); + } + + getMassSuggestionWeight(id_canti: number): number | null { + const weight = this.cantiLettureService.suggestionsMap().get(id_canti); + return weight !== undefined ? weight : null; + } + + getMassSuggestionMoment(id_canti: number): string | null { + const moments = this.cantiLettureService.suggestionsMomentsMap().get(id_canti); + return moments && moments.length > 0 ? moments.join(', ') : null; + } + + getSelectedMassFormattedDate(): string | null { + const dateStr = this.cantiLettureService.selectedMassDate(); + if (!dateStr) return null; + const parts = dateStr.split('-'); + if (parts.length === 3) { + return `${parts[2]}/${parts[1]}/${parts[0]}`; + } + return dateStr; + } + + getSelectedMassTitle(): string | null { + const dateStr = this.cantiLettureService.selectedMassDate(); + const data = this.cantiLettureService.data(); + if (dateStr && data && data.masses[dateStr]) { + return data.masses[dateStr].title; + } + return null; + } + + getSelectedMassSummary(): string | null { + const dateStr = this.cantiLettureService.selectedMassDate(); + const data = this.cantiLettureService.data(); + if (dateStr && data && data.masses[dateStr]) { + return data.masses[dateStr].summary || null; + } + return null; + } + + getEsecuzioniCount(id: string): number | null { + const numId = parseInt(id, 10); + if (isNaN(numId)) return null; + const es = this.cantiService.cantiEseguiti().find(x => x.id_canti === numId); + return es ? es.num : null; + } + onImgLoad(id: string) { this.loadedThumbs.add(id); } @@ -429,6 +534,9 @@ export class HomePage implements OnDestroy { playVideo(event: Event, id: string) { event.stopPropagation(); + if (!this.youtubePlayerService.isPlayerSupported()) { + return; + } this.youtubePlayerService.setMediaSessionCallbacks( () => this.playNextPreview(), () => this.playPrevPreview() @@ -577,14 +685,30 @@ export class HomePage implements OnDestroy { handleScannedData(data: string) { if (!data) return; - if (data.includes('?import=')) { + if (data.includes('import=')) { try { - const base64 = data.split('?import=')[1]; + let base64 = ''; + if (data.includes('?import=')) { + base64 = data.split('?import=')[1]; + } else if (data.includes('&import=')) { + base64 = data.split('&import=')[1]; + } + + if (base64.includes('&')) { + base64 = base64.split('&')[0]; + } + if (base64.includes('#')) { + base64 = base64.split('#')[0]; + } + this.handleImport(base64); + return; } catch (e) { console.error('Failed to parse scanned link', e); } - } else if (data.startsWith('canti:')) { + } + + if (data.startsWith('canti:')) { const parts = data.replace('canti:', '').split(':'); let idsStr = ''; if (parts.length > 1) { diff --git a/src/app/pages/liturgia/liturgia.module.ts b/src/app/pages/liturgia/liturgia.module.ts deleted file mode 100644 index 0828855..0000000 --- a/src/app/pages/liturgia/liturgia.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { IonicModule } from '@ionic/angular'; -import { RouterModule } from '@angular/router'; -import { LiturgiaPage } from './liturgia.page'; - -@NgModule({ - imports: [ - CommonModule, - FormsModule, - IonicModule, - RouterModule.forChild([ - { - path: '', - component: LiturgiaPage - } - ]) - ], - declarations: [LiturgiaPage] -}) -export class LiturgiaPageModule {} diff --git a/src/app/pages/liturgia/liturgia.page.html b/src/app/pages/liturgia/liturgia.page.html deleted file mode 100644 index 3b1360e..0000000 --- a/src/app/pages/liturgia/liturgia.page.html +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - Liturgia del Giorno - - - - -
- -
-
-
- - - -

{{ getTodayString() }}

- - - -
-
-
- -
- -

Caricamento letture...

-
- -
- - - - - - - Prima Lettura - -
-
- - - - - Seconda Lettura - -
-
- - - - - Vangelo - -
-
- -
- - -
- -
-

Non è stato possibile caricare le letture per oggi.

- Riprova -
- -
-
diff --git a/src/app/pages/liturgia/liturgia.page.scss b/src/app/pages/liturgia/liturgia.page.scss deleted file mode 100644 index d6f6c10..0000000 --- a/src/app/pages/liturgia/liturgia.page.scss +++ /dev/null @@ -1,144 +0,0 @@ -.bg-gradient { - --background: transparent; - background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); -} - -.liturgia-container { - max-width: 800px; - margin: 0 auto; -} - -.glass { - background: rgba(255, 255, 255, 0.05); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 16px; - padding: 16px; -} - -.date-header { - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; - - .date-navigation { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - - h2 { - margin: 0; - font-size: 1.1rem; - color: var(--ion-color-secondary); - text-transform: capitalize; - text-align: center; - } - } -} - -.keywords-container { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-bottom: 16px; -} - -.moment-group { - .moment-header { - margin-bottom: 8px; - ion-badge { - font-size: 0.9rem; - padding: 6px 12px; - border-radius: 8px; - } - } -} - -.glass-accordion { - background: rgba(255, 255, 255, 0.05); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 16px; - overflow: hidden; - - ion-accordion { - --background: transparent; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - - &:last-child { - border-bottom: none; - } - - .reading-title { - color: var(--ion-color-secondary); - font-size: 1.1rem; - font-weight: 600; - margin: 0; - } - - .reading-text { - color: rgba(255, 255, 255, 0.9); - line-height: 1.6; - font-size: 1rem; - white-space: pre-wrap; - background: rgba(0, 0, 0, 0.2); - - ::ng-deep br { - display: block; - content: ""; - margin-top: 10px; - } - } - } -} - -.section-title { - color: #fff; - font-size: 1.2rem; - margin-bottom: 16px; -} - -.song-card { - padding: 0; - transition: transform 0.2s ease; - - &:active { - transform: scale(0.98); - } - - .song-title { - color: #fff; - font-weight: 600; - } - - .song-author { - color: rgba(255, 255, 255, 0.5); - font-size: 0.85rem; - } -} - -.transparent-item { - --background: transparent; - --color: #fff; - --padding-start: 16px; - --padding-end: 16px; -} - -/* Alto Contrasto per Liturgia */ -:host-context(body.high-contrast) { - .glass-accordion { - // Mantengo il look glass per gli header come richiesto - - ion-accordion { - .reading-text { - color: #000 !important; // Testo nero - background: #fff !important; // Sfondo bianco solido per massimo contrasto - padding: 20px !important; - } - } - } -} diff --git a/src/app/pages/liturgia/liturgia.page.ts b/src/app/pages/liturgia/liturgia.page.ts deleted file mode 100644 index 4b67ca1..0000000 --- a/src/app/pages/liturgia/liturgia.page.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Component, OnInit, inject, signal } from '@angular/core'; -import { LiturgyService } from '../../services/liturgy.service'; -import { Router } from '@angular/router'; -import { Canto } from '../../services/canti.service'; - -@Component({ - selector: 'app-liturgia', - templateUrl: './liturgia.page.html', - styleUrls: ['./liturgia.page.scss'], - standalone: false -}) -export class LiturgiaPage implements OnInit { - public liturgyService = inject(LiturgyService); - private router = inject(Router); - - ngOnInit() { - this.liturgyService.fetchTodayLiturgy(); - } - - suggest() { - this.liturgyService.suggestSongs(); - } - - onDateChange(event: any) { - const newDate = event.detail.value.split('T')[0]; - this.liturgyService.fetchTodayLiturgy(newDate); - } - - openSong(song: Canto) { - this.router.navigate(['/player'], { queryParams: { id: song.id } }); - } - - getTodayString(): string { - const date = new Date(this.liturgyService.selectedDate()); - return date.toLocaleDateString('it-IT', { - weekday: 'long', - day: 'numeric', - month: 'long', - year: 'numeric' - }); - } -} diff --git a/src/app/pages/player/player.page.html b/src/app/pages/player/player.page.html index 6be5fe0..add023d 100644 --- a/src/app/pages/player/player.page.html +++ b/src/app/pages/player/player.page.html @@ -1,6 +1,6 @@ - +
@@ -57,7 +57,7 @@ - +
@@ -100,7 +100,7 @@ - +
@@ -182,7 +182,7 @@
-
+
diff --git a/src/app/pages/player/player.page.scss b/src/app/pages/player/player.page.scss index 1ba4df5..ac422b8 100644 --- a/src/app/pages/player/player.page.scss +++ b/src/app/pages/player/player.page.scss @@ -208,19 +208,31 @@ .slim-controls { display: flex; - justify-content: space-between; + justify-content: flex-start; align-items: center; width: 100%; - padding: 0 8px; + padding: 4px 12px; + overflow-x: auto; + gap: 16px; + -webkit-overflow-scrolling: touch; + + &::-webkit-scrollbar { + display: none; + } + + .mic-btn-portrait { + flex-shrink: 0; + } .group { - display: flex; + display: inline-flex; align-items: center; gap: 4px; background: rgba(255, 255, 255, 0.05); padding: 2px 6px; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.1); + flex-shrink: 0; } .page-indicator { diff --git a/src/app/pages/player/player.page.ts b/src/app/pages/player/player.page.ts index 25ca17e..4e271c5 100644 --- a/src/app/pages/player/player.page.ts +++ b/src/app/pages/player/player.page.ts @@ -447,6 +447,9 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { } private initPlayer(id: string) { + if (!this.youtubePlayerService.isPlayerSupported()) { + return; + } const startT = this.initialStartTime; this.initialStartTime = 0; // Reset diff --git a/src/app/pages/settings/settings.page.html b/src/app/pages/settings/settings.page.html index 568b878..451c9b0 100644 --- a/src/app/pages/settings/settings.page.html +++ b/src/app/pages/settings/settings.page.html @@ -59,13 +59,37 @@ - - - -

Liturgia del Giorno

-

Letture e suggerimenti canti

-
-
+
+ + +
+
+

+ Messa di Riferimento (Suggeriti) +

+
+ +
+
+ +
+

Seleziona Messa

+

Scegli la messa per i canti consigliati

+
+
+ +
+ + + {{ mass.formattedLabel }} + + +
+
@@ -123,6 +147,95 @@
+ +
+
+

+ Legenda Pulsanti del Canto +

+
+ +
+

+ All'interno della schermata di lettura del canto trovi vari pulsanti per controllare la visualizzazione e lo scorrimento: +

+ +
+
+
+ +
+
+

Scroll Acustico (Karaoke)

+

+ Attiva lo scorrimento vocale intelligente. L'app ascolta il canto o lo strumento e fa scorrere testo e accordi a tempo di musica, senza bisogno di toccare lo schermo. Uno slider verticale permette di regolare la sensibilità. +

+
+
+ +
+
+ +
+
+

Trasposizione Tonalità (+ / -)

+

+ Disponibile in modalità "Accordi". Alza o abbassa la tonalità del canto di semitono in semitono, ricalcolando gli accordi all'istante. +

+
+
+ +
+
+ +
+
+

Riavvia Canto

+

+ Riporta la visualizzazione all'inizio del testo e azzera il tracciamento vocale dello scroll acustico. +

+
+
+ +
+
+ +
+
+

Navigazione Righe

+

+ Permette di avanzare o tornare indietro manualmente tra le righe del testo del canto. +

+
+
+ +
+
+ +
+
+

Zoom Caratteri

+

+ Ingrandisce o rimpicciolisce il testo e gli accordi (puoi usare anche il gesto "pinch-to-zoom" sullo schermo). +

+
+
+ +
+
+ +
+
+

YouTube Esterno

+

+ Apre la traccia o il video ufficiale del canto direttamente sull'applicazione esterna di YouTube (quando online). +

+
+
+
+
+
+