From 2d23dcae83e5586102d241e2dffad712a4ad534d Mon Sep 17 00:00:00 2001 From: David Frassi Date: Tue, 16 Jun 2026 16:14:20 +0200 Subject: [PATCH] Salvataggio modifiche correnti prima di implementare condivisione canto --- src/app/app.component.ts | 21 ++ src/app/home/home.page.html | 2 +- src/app/home/home.page.scss | 2 +- src/app/home/home.page.ts | 8 +- src/app/pages/player/player.page.html | 8 + src/app/pages/player/player.page.scss | 33 ++- src/app/pages/player/player.page.ts | 13 +- src/app/pages/playlist/playlist.page.ts | 4 +- .../propose-canto/propose-canto.page.spec.ts | 112 ++++++++- .../pages/propose-canto/propose-canto.page.ts | 237 ++++++++++++++---- src/app/pages/settings/settings.page.html | 8 + src/app/services/settings.service.ts | 20 ++ src/app/version.ts | 2 +- 13 files changed, 401 insertions(+), 69 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 729f1d9..40f6fec 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -58,6 +58,27 @@ export class AppComponent implements OnInit { } async ngOnInit() { + // Add global horizontal scroll support for wheel on horizontal containers + window.addEventListener('wheel', (event: WheelEvent) => { + if (Math.abs(event.deltaY) > 0 && Math.abs(event.deltaX) === 0) { + const path = event.composedPath(); + for (const target of path) { + if (target instanceof HTMLElement) { + const style = window.getComputedStyle(target); + const isHorizontalScroll = + (style.overflowX === 'auto' || style.overflowX === 'scroll') && + target.scrollWidth > target.clientWidth; + + if (isHorizontalScroll) { + target.scrollLeft += event.deltaY; + event.preventDefault(); + break; + } + } + } + } + }, { passive: false }); + // 1. Allineamento istantaneo alla versione remota if ((window as any).PwaLoader) { (window as any).PwaLoader.update({ diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index 1af03c9..c12bb49 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -203,7 +203,7 @@

- Per creare una playlist, seleziona il nr dei canti che vuoi inserire nella playlist, riordinali e salvala con nome + Per creare una playlist, seleziona il nr del canto che vuoi inserire nella playlist, riordinali e salvala con nome

diff --git a/src/app/home/home.page.scss b/src/app/home/home.page.scss index 7181c74..adffa4d 100644 --- a/src/app/home/home.page.scss +++ b/src/app/home/home.page.scss @@ -1318,7 +1318,7 @@ ion-title { font-size: 0.85rem; line-height: 1.4; color: rgba(255, 255, 255, 0.8); - text-align: center; + text-align: left; background: rgba(var(--ion-color-secondary-rgb), 0.08); border: 1px dashed rgba(var(--ion-color-secondary-rgb), 0.35); padding: 12px 16px; diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index 8216135..1e35309 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -1146,6 +1146,9 @@ export class HomePage implements OnDestroy { toggleSuggeriti() { this.showSuggeriti.update(v => !v); + if (this.showSuggeriti()) { + this.isMassCardExpanded.set(true); + } this.activeFilterType.set(null); this.limit.set(30); } @@ -1481,7 +1484,10 @@ export class HomePage implements OnDestroy { handler: async (data) => { if (data.name) { const ids = this.reorderList().map(s => s.id); - await this.playlistService.savePlaylist(data.name, ids); + const activeId = this.playlistService.activePlaylistId(); + const pl = activeId ? this.playlistService.playlists().find(p => p.id === activeId) : null; + const songSettings = pl ? pl.songSettings : undefined; + await this.playlistService.savePlaylist(data.name, ids, songSettings); const toast = await this.toastCtrl.create({ message: 'Playlist salvata!', diff --git a/src/app/pages/player/player.page.html b/src/app/pages/player/player.page.html index 2a1f69d..992c0ff 100644 --- a/src/app/pages/player/player.page.html +++ b/src/app/pages/player/player.page.html @@ -75,6 +75,10 @@ +
+ + {{ fontSize().toFixed(1) }} +
@@ -229,6 +233,10 @@ +
+ + {{ fontSize().toFixed(1) }} +
diff --git a/src/app/pages/player/player.page.scss b/src/app/pages/player/player.page.scss index eb0268f..e3b6a8b 100644 --- a/src/app/pages/player/player.page.scss +++ b/src/app/pages/player/player.page.scss @@ -265,6 +265,17 @@ ion-icon { font-size: 0.9rem; } } + .zoom-indicator { + display: flex; + align-items: center; + gap: 2px; + font-size: 0.8rem; + font-weight: 700; + color: var(--ion-color-secondary); + + ion-icon { font-size: 0.9rem; } + } + .camera-indicator { display: flex; align-items: center; @@ -306,7 +317,7 @@ 100% { transform: scale(0.9); opacity: 0.6; } } -@media (orientation: landscape) { +:host(.landscape-active) { // Always hide footer in landscape as we have side controls ion-footer { display: none !important; @@ -330,8 +341,8 @@ .landscape-side-controls { display: none !important; // Strict hidden in portrait - - @media (orientation: landscape) { + + :host(.landscape-active) & { display: flex !important; flex-direction: column; position: fixed; @@ -430,11 +441,11 @@ } ion-content.full-screen-content { + --offset-bottom: 48px !important; // Footer height +} + +:host(.landscape-active) ion-content.full-screen-content { --offset-bottom: 0px !important; - - @media (orientation: portrait) { - --offset-bottom: 48px !important; // Footer height - } } .mic-sensitivity-overlay { @@ -445,7 +456,7 @@ ion-content.full-screen-content { z-index: 1000; pointer-events: auto; - @media (orientation: landscape) { + :host(.landscape-active) & { right: 80px; // Prossimo ai controlli laterali } @@ -552,10 +563,6 @@ ion-content.full-screen-content { z-index: 1000; pointer-events: auto; - @media (orientation: landscape) { - left: 15px; - } - .slider-card { display: flex; flex-direction: column; @@ -846,7 +853,7 @@ ion-content.full-screen-content { backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); - @media (orientation: landscape) { + :host(.landscape-active) & { bottom: 15px; right: 80px; } diff --git a/src/app/pages/player/player.page.ts b/src/app/pages/player/player.page.ts index c8247e8..596a853 100644 --- a/src/app/pages/player/player.page.ts +++ b/src/app/pages/player/player.page.ts @@ -18,6 +18,9 @@ import { FaceDetectorService } from '../../services/face-detector.service'; selector: 'app-player', templateUrl: './player.page.html', styleUrls: ['./player.page.scss'], + host: { + '[class.landscape-active]': 'isLandscapeActive()' + }, standalone: false }) export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { @@ -40,7 +43,11 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { } - public isLandscapeActive = signal(window.innerWidth > window.innerHeight); + private windowLandscape = signal(window.innerWidth > window.innerHeight); + + public isLandscapeActive = computed(() => { + return this.windowLandscape() && this.settingsService.landscapeProjectionEnabled(); + }); /** true = show chords (accordi mode), false = text only */ public showChords = signal(false); @@ -318,12 +325,12 @@ export class PlayerPage implements OnInit, AfterViewInit, OnDestroy { @HostListener('window:resize', ['$event']) onResize(event: any) { - this.isLandscapeActive.set(window.innerWidth > window.innerHeight); + this.windowLandscape.set(window.innerWidth > window.innerHeight); this.checkLandscapeZoom(); } private isLandscape(): boolean { - return window.innerWidth > window.innerHeight; + return this.isLandscapeActive(); } private checkAndLimitFontSize(targetFont: number): number { diff --git a/src/app/pages/playlist/playlist.page.ts b/src/app/pages/playlist/playlist.page.ts index 65e0834..ef002db 100644 --- a/src/app/pages/playlist/playlist.page.ts +++ b/src/app/pages/playlist/playlist.page.ts @@ -90,7 +90,7 @@ export class PlaylistPage { handler: (data) => { if (data.name) { this.savedPlaylistName = data.name; - this.playlistService.savePlaylist(data.name, this.localSongs.map(s => s.id)); + this.playlistService.savePlaylist(data.name, this.localSongs.map(s => s.id), this.getPlaylistSongSettings()); this.showToast('Playlist salvata!'); this.router.navigate(['/settings']); return true; @@ -126,8 +126,8 @@ export class PlaylistPage { if (data.name) { this.savedPlaylistName = data.name; const ids = this.localSongs.map(s => s.id); - await this.playlistService.savePlaylist(data.name, ids); const songSettings = this.getPlaylistSongSettings(); + await this.playlistService.savePlaylist(data.name, ids, songSettings); this.qrCodeImage = await this.playlistService.generateQR(ids, data.name, songSettings); this.showToast('Playlist salvata!'); return true; diff --git a/src/app/pages/propose-canto/propose-canto.page.spec.ts b/src/app/pages/propose-canto/propose-canto.page.spec.ts index 645994d..785b451 100644 --- a/src/app/pages/propose-canto/propose-canto.page.spec.ts +++ b/src/app/pages/propose-canto/propose-canto.page.spec.ts @@ -1,17 +1,127 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ProposeCantoPage } from './propose-canto.page'; +import { ToastController, PopoverController, NavController, AngularDelegate } from '@ionic/angular'; +import { CantiService } from '../../services/canti.service'; +import { MyCantiService } from '../../services/my-canti.service'; +import { PlaylistService } from '../../services/playlist.service'; +import { ThemeService } from '../../services/theme.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { LyricsParserService } from '../../services/lyrics-parser.service'; +import { of } from 'rxjs'; describe('ProposeCantoPage', () => { let component: ProposeCantoPage; let fixture: ComponentFixture; beforeEach(() => { + const cantiServiceMock = { + indiceLiturgico: () => [], + indiceTematico: () => [], + canti: () => [] + }; + const myCantiServiceMock = { + myCanti: () => [] + }; + const playlistServiceMock = { + remoteCustomSongs: () => [], + activePlaylistId: () => null + }; + const themeServiceMock = { + highContrast: () => false + }; + const activatedRouteMock = { + queryParams: of({}) + }; + const routerMock = { + navigate: jasmine.createSpy('navigate') + }; + const lyricsParserMock = { + parseAccordi: () => [] + }; + const navCtrlMock = {}; + const toastControllerMock = {}; + const popoverControllerMock = {}; + const angularDelegateMock = {}; + + TestBed.configureTestingModule({ + providers: [ + { provide: CantiService, useValue: cantiServiceMock }, + { provide: MyCantiService, useValue: myCantiServiceMock }, + { provide: PlaylistService, useValue: playlistServiceMock }, + { provide: ThemeService, useValue: themeServiceMock }, + { provide: ActivatedRoute, useValue: activatedRouteMock }, + { provide: Router, useValue: routerMock }, + { provide: LyricsParserService, useValue: lyricsParserMock }, + { provide: NavController, useValue: navCtrlMock }, + { provide: ToastController, useValue: toastControllerMock }, + { provide: PopoverController, useValue: popoverControllerMock }, + { provide: AngularDelegate, useValue: angularDelegateMock } + ] + }); + fixture = TestBed.createComponent(ProposeCantoPage); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + describe('convertEnglishChordToItalian', () => { + it('should convert simple English chords to Italian', () => { + expect(component.convertEnglishChordToItalian('C')).toBe('DO'); + expect(component.convertEnglishChordToItalian('D')).toBe('RE'); + expect(component.convertEnglishChordToItalian('E')).toBe('MI'); + expect(component.convertEnglishChordToItalian('F')).toBe('FA'); + expect(component.convertEnglishChordToItalian('G')).toBe('SOL'); + expect(component.convertEnglishChordToItalian('A')).toBe('LA'); + expect(component.convertEnglishChordToItalian('B')).toBe('SI'); + }); + + it('should convert English chords with accidentals and modifiers', () => { + expect(component.convertEnglishChordToItalian('C#m7')).toBe('DO#m7'); + expect(component.convertEnglishChordToItalian('Bb')).toBe('SIb'); + expect(component.convertEnglishChordToItalian('F#m')).toBe('FA#m'); + expect(component.convertEnglishChordToItalian('Faug')).toBe('FAaug'); + expect(component.convertEnglishChordToItalian('Fadd9')).toBe('FAadd9'); + }); + + it('should keep Italian chords unchanged', () => { + expect(component.convertEnglishChordToItalian('DO')).toBe('DO'); + expect(component.convertEnglishChordToItalian('RE#m7')).toBe('RE#m7'); + expect(component.convertEnglishChordToItalian('FA#')).toBe('FA#'); + expect(component.convertEnglishChordToItalian('SIb')).toBe('SIb'); + }); + + it('should handle slash chords correctly', () => { + expect(component.convertEnglishChordToItalian('C/E')).toBe('DO/MI'); + expect(component.convertEnglishChordToItalian('D/F#')).toBe('RE/FA#'); + expect(component.convertEnglishChordToItalian('F#m7/A#')).toBe('FA#m7/LA#'); + }); + }); + + describe('sanitizeOcrChord', () => { + it('should convert H to # when following a chord root', () => { + expect(component.sanitizeOcrChord('CH')).toBe('C#'); + expect(component.sanitizeOcrChord('DHm7')).toBe('D#m7'); + expect(component.sanitizeOcrChord('FH#')).toBe('F#'); + expect(component.sanitizeOcrChord('FAH')).toBe('FA#'); + expect(component.sanitizeOcrChord('SOLH7')).toBe('SOL#7'); + expect(component.sanitizeOcrChord('FH#H')).toBe('F#'); + expect(component.sanitizeOcrChord('FH#H/AH#')).toBe('F#/A#'); + expect(component.sanitizeOcrChord('C#H-')).toBe('C#-'); + expect(component.sanitizeOcrChord('G#H#')).toBe('G#'); + }); + + it('should convert 0 to O for DO and SOL roots', () => { + expect(component.sanitizeOcrChord('D0')).toBe('DO'); + expect(component.sanitizeOcrChord('D0#')).toBe('DO#'); + expect(component.sanitizeOcrChord('S0L')).toBe('SOL'); + }); + + it('should handle slash chords with sanitization', () => { + expect(component.sanitizeOcrChord('CH/EH')).toBe('C#/E#'); + expect(component.sanitizeOcrChord('D0/FH#')).toBe('DO/F#'); + }); + }); }); diff --git a/src/app/pages/propose-canto/propose-canto.page.ts b/src/app/pages/propose-canto/propose-canto.page.ts index c1ff722..a76dd53 100644 --- a/src/app/pages/propose-canto/propose-canto.page.ts +++ b/src/app/pages/propose-canto/propose-canto.page.ts @@ -380,12 +380,95 @@ export class ProposeCantoPage implements OnInit { return this.parseSongSpatially(words); } + sanitizeOcrChord(text: string): string { + if (!text) return text; + if (text.includes('/')) { + return text.split('/').map(part => this.sanitizeOcrChord(part.trim())).join('/'); + } + let cleaned = text; + // Replace D0/d0 with DO/do + cleaned = cleaned.replace(/^D0/gi, 'DO'); + // Replace S0L/s0l with SOL/sol + cleaned = cleaned.replace(/^S0L/gi, 'SOL'); + // Clean H/sharp mismatches: + cleaned = cleaned.replace(/H#/gi, '#'); + cleaned = cleaned.replace(/#H/gi, '#'); + cleaned = cleaned.replace(/([CDEFGAB]|DO|RE|MI|FA|SOL|LA|SI)H/gi, '$1#'); + // Clean duplicate sharps (e.g. ## -> #) + cleaned = cleaned.replace(/##+/g, '#'); + return cleaned; + } + + convertEnglishChordToItalian(chord: string): string { + if (!chord) return chord; + if (chord.includes('/')) { + return chord.split('/').map(part => this.convertEnglishChordToItalian(part.trim())).join('/'); + } + const upper = chord.toUpperCase(); + if (upper.startsWith('DO')) { + return chord; + } + if (upper.startsWith('FA')) { + if (upper.startsWith('FAUG') || upper.startsWith('FADD') || upper.startsWith('FALT')) { + return 'FA' + chord.slice(1); + } + return chord; + } + if (upper.startsWith('C')) { + return 'DO' + chord.slice(1); + } + if (upper.startsWith('D')) { + return 'RE' + chord.slice(1); + } + if (upper.startsWith('E')) { + return 'MI' + chord.slice(1); + } + if (upper.startsWith('F')) { + return 'FA' + chord.slice(1); + } + if (upper.startsWith('G')) { + return 'SOL' + chord.slice(1); + } + if (upper.startsWith('A')) { + return 'LA' + chord.slice(1); + } + if (upper.startsWith('B')) { + return 'SI' + chord.slice(1); + } + return chord; + } + + isLabelLine(text: string): boolean { + return /^(Intro|Strofa|Rit|Special|Coro|Bridge|RIT|CHORUS|VERSE)/i.test(text.trim()); + } + parseSongSpatially(words: any[]): string { if (!words || words.length === 0) { console.warn('[OCR-Capture] Nessuna parola ricevuta dall\'OCR.'); return ''; } + // Preprocess words to split run-together chords like BA + const preprocessedWords: any[] = []; + words.forEach(w => { + if (!w.text) return; + const match = w.text.match(/^([ABCDEFG])([ABCDEFG])$/i); + if (match && !(match[1].toUpperCase() === 'F' && match[2].toUpperCase() === 'A')) { + const charWidth = (w.bbox.x1 - w.bbox.x0) / 2; + preprocessedWords.push({ + text: match[1], + bbox: { ...w.bbox, x1: w.bbox.x0 + charWidth } + }); + preprocessedWords.push({ + text: match[2], + bbox: { ...w.bbox, x0: w.bbox.x0 + charWidth } + }); + } else { + preprocessedWords.push(w); + } + }); + words = preprocessedWords; + console.log(`[OCR-Capture] Parole totali ricevute dall'OCR: ${words.length}`); const validWords = words.filter(w => w.text && w.text.trim().length > 0); console.log(`[OCR-Capture] Parole valide dopo filtraggio: ${validWords.length}`); @@ -436,6 +519,12 @@ export class ProposeCantoPage implements OnInit { let clean = text.toUpperCase().replace(/\s+/g, ''); clean = clean.replace(/\((.*?)\)/g, '/$1'); clean = clean.replace(/[\.\,]$/g, ''); + clean = this.sanitizeOcrChord(clean); + if (clean.includes('_')) { + const parts = clean.split('_').filter(p => p.length > 0); + if (parts.length === 0) return false; + return parts.every(p => chordRegex.test(p)); + } return chordRegex.test(clean); }; @@ -481,7 +570,8 @@ export class ProposeCantoPage implements OnInit { if (current.isChords) { const next = (i + 1 < classifiedLines.length) ? classifiedLines[i + 1] : null; - if (next && !next.isChords) { + const nextText = next ? next.words.map(w => w.text).join(' ') : ''; + if (next && !next.isChords && !this.isLabelLine(nextText)) { // Merge spatially! const merged = this.mergeChordsAndLyrics(current.words, next.words); const lineText = next.words.map(w => w.text).join(' '); @@ -502,21 +592,29 @@ export class ProposeCantoPage implements OnInit { const multiChordRegex = /((?:DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(?:#|B)?(?:MIN|MAJ|AUG|DIM|M|-)?(?:7|9|11|13)?(?:\/(?:DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(?:#|B)?)?)/gi; const expandedChords: string[] = []; current.words.forEach(w => { - let cleanText = w.text.toUpperCase().replace(/\s+/g, ''); - cleanText = cleanText.replace(/\((.*?)\)/g, '/$1'); - cleanText = cleanText.replace(/[\.\,]$/g, ''); - - if (chordRegex.test(cleanText)) { - expandedChords.push(`[${cleanText}]`); - } else { - const matches = [...cleanText.matchAll(multiChordRegex)]; - const fullMatchStr = matches.map(m => m[0]).join(''); - if (matches.length > 0 && fullMatchStr === cleanText) { - expandedChords.push(...matches.map((m: string) => `[${m[0].toUpperCase()}]`)); + const parts = w.text.split(/(_)/); + const processedParts = parts.map((part: string) => { + if (part === '_') return ' _ '; + if (!part.trim()) return part; + + let cleanText = part.toUpperCase().replace(/\s+/g, ''); + cleanText = cleanText.replace(/\((.*?)\)/g, '/$1'); + cleanText = cleanText.replace(/[\.\,]$/g, ''); + cleanText = this.sanitizeOcrChord(cleanText); + + if (chordRegex.test(cleanText)) { + return `[${this.convertEnglishChordToItalian(cleanText)}]`; } else { - expandedChords.push(w.text); // keep original text if it's not a pure chord merge + const matches = [...cleanText.matchAll(multiChordRegex)]; + const fullMatchStr = matches.map(m => m[0]).join(''); + if (matches.length > 0 && fullMatchStr === cleanText) { + return matches.map((m: any) => `[${this.convertEnglishChordToItalian(m[0].toUpperCase())}]`).join(' '); + } else { + return part; + } } - } + }); + expandedChords.push(processedParts.join('')); }); const wrapped = expandedChords.join(' '); if (wrapped) { @@ -535,7 +633,7 @@ export class ProposeCantoPage implements OnInit { inVerse = true; } - processedLines.push(lineText); + processedLines.push(this.wrapChords(lineText, chordRegex)); } // If we see a large vertical gap, close open blocks @@ -606,8 +704,14 @@ export class ProposeCantoPage implements OnInit { if (!textWords || textWords.length === 0) { return mergedChordWords.map(c => { - let clean = c.text.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, ''); - return `[${clean}]`; + const parts = c.text.split(/(_)/); + return parts.map((part: string) => { + if (part === '_') return ' _ '; + if (!part.trim()) return part; + let clean = part.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, ''); + clean = this.sanitizeOcrChord(clean); + return `[${this.convertEnglishChordToItalian(clean)}]`; + }).join(''); }).join(' '); } @@ -616,30 +720,54 @@ export class ProposeCantoPage implements OnInit { const expandedChordWords: any[] = []; mergedChordWords.forEach(chord => { - let originalText = chord.text.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, ''); - const matches = [...originalText.matchAll(multiChordRegex)]; + const parts = chord.text.split(/(_)/); + let currentX = chord.bbox.x0; + const totalLen = chord.text.length || 1; + const widthPerChar = (chord.bbox.x1 - chord.bbox.x0) / totalLen; - if (matches.length === 0) { - expandedChordWords.push(chord); - } else { - const fullMatchStr = matches.map(m => m[0]).join(''); - if (fullMatchStr === originalText) { - const charWidth = (chord.bbox.x1 - chord.bbox.x0) / Math.max(1, originalText.length); - matches.forEach(match => { - const matchIndex = match.index!; - const matchLength = match[0].length; - const newX0 = chord.bbox.x0 + matchIndex * charWidth; - const newX1 = chord.bbox.x0 + (matchIndex + matchLength) * charWidth; - - expandedChordWords.push({ - text: match[0], - bbox: { ...chord.bbox, x0: newX0, x1: newX1 } - }); + parts.forEach((part: string) => { + const partLen = part.length; + const partWidth = partLen * widthPerChar; + const partX0 = currentX; + const partX1 = currentX + partWidth; + currentX = partX1; + + if (part === '_' || !part.trim()) { + return; + } + + let originalText = part.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, ''); + originalText = this.sanitizeOcrChord(originalText); + const matches = [...originalText.matchAll(multiChordRegex)]; + + if (matches.length === 0) { + expandedChordWords.push({ + text: originalText, + bbox: { ...chord.bbox, x0: partX0, x1: partX1 } }); } else { - expandedChordWords.push(chord); + const fullMatchStr = matches.map(m => m[0]).join(''); + if (fullMatchStr === originalText) { + const matchCharWidth = (partX1 - partX0) / Math.max(1, originalText.length); + matches.forEach(match => { + const matchIndex = match.index!; + const matchLength = match[0].length; + const newX0 = partX0 + matchIndex * matchCharWidth; + const newX1 = partX0 + (matchIndex + matchLength) * matchCharWidth; + + expandedChordWords.push({ + text: match[0], + bbox: { ...chord.bbox, x0: newX0, x1: newX1 } + }); + }); + } else { + expandedChordWords.push({ + text: originalText, + bbox: { ...chord.bbox, x0: partX0, x1: partX1 } + }); + } } - } + }); }); const chordAssignments = new Map(); @@ -691,8 +819,9 @@ export class ProposeCantoPage implements OnInit { if (charIndex < 0) charIndex = 0; if (charIndex > wordText.length) charIndex = wordText.length; - const cleanChord = chord.text.replace(/[\(\)\[\]]/g, '').toUpperCase(); - wordResult += wordText.substring(lastCharIndex, charIndex) + `[${cleanChord}]`; + let cleanChord = chord.text.replace(/[\(\)\[\]]/g, '').toUpperCase(); + cleanChord = this.sanitizeOcrChord(cleanChord); + wordResult += wordText.substring(lastCharIndex, charIndex) + `[${this.convertEnglishChordToItalian(cleanChord)}]`; lastCharIndex = charIndex; }); @@ -704,10 +833,15 @@ export class ProposeCantoPage implements OnInit { } }); - const finalResult = result.replace(/\]\[/g, '] ['); + let finalResult = result.replace(/\]\[/g, '] ['); + finalResult = finalResult.replace(/\]_\[/g, '] _ ['); + finalResult = finalResult.replace(/\]_/g, '] _ '); + finalResult = finalResult.replace(/_\[/g, ' _ ['); + // Normalize any duplicate spaces around underscores: + finalResult = finalResult.replace(/\s*_\s*/g, ' _ '); + console.log(`[OCR-Debug] Linea generata: ${finalResult}`); - // Assicura che due accordi consecutivi abbiano sempre 3 spazi (es. [LA][MI] diventa [LA] [MI]) return finalResult; } @@ -750,11 +884,22 @@ export class ProposeCantoPage implements OnInit { } private wrapChords(line: string, regex: RegExp): string { - const chordsInLine = line.match(regex); - if (chordsInLine && chordsInLine.length > 0) { - return line.replace(regex, (match) => `[${match.toUpperCase()}]`); - } - return line; + const globalRegex = new RegExp(regex.source.replace(/^\^/, '').replace(/\$$/, ''), 'gi'); + const parts = line.split(/(_)/); + return parts.map((part: string) => { + if (part === '_') return ' _ '; + return part.replace(globalRegex, (match) => { + const lower = match.toLowerCase(); + if (match === 'a' || match === 'e' || match === 'o' || match === 'i') { + return match; + } + if (['la', 'mi', 're', 'do', 'si', 'fa', 'sol'].includes(lower) && match === lower) { + return match; + } + const sanitized = this.sanitizeOcrChord(match.toUpperCase()); + return `[${this.convertEnglishChordToItalian(sanitized)}]`; + }); + }).join(''); } diff --git a/src/app/pages/settings/settings.page.html b/src/app/pages/settings/settings.page.html index c307870..54d7769 100644 --- a/src/app/pages/settings/settings.page.html +++ b/src/app/pages/settings/settings.page.html @@ -128,6 +128,14 @@ + + + +

Vista orizzontale per proiezione

+

Adatta il layout in landscape per la proiezione (testo più grande, controlli dedicati)

+
+ +
diff --git a/src/app/services/settings.service.ts b/src/app/services/settings.service.ts index 331bb59..4db7955 100644 --- a/src/app/services/settings.service.ts +++ b/src/app/services/settings.service.ts @@ -53,6 +53,9 @@ export class SettingsService { /** Avanzamento a pagine del karaoke manuale: true = i tasti next/prev voltano la pagina */ public karaokePageScrollMode = signal(false); + /** Vista orizzontale per proiezione: true = attiva layout landscape per proiezione */ + public landscapeProjectionEnabled = signal(true); + /** Identificativo utente univoco per la gestione delle comunità */ public userUuid = signal(''); @@ -244,6 +247,13 @@ export class SettingsService { this.karaokePageScrollMode.set(false); } + const savedLandscapeProjectionEnabled = localStorage.getItem('landscape-projection-enabled'); + if (savedLandscapeProjectionEnabled !== null) { + this.landscapeProjectionEnabled.set(savedLandscapeProjectionEnabled === 'true'); + } else { + this.landscapeProjectionEnabled.set(true); + } + // Sync browser fullscreen state with listeners (supporting vendor prefixes) const updateFullscreenState = () => { const isFs = !!( @@ -297,6 +307,10 @@ export class SettingsService { localStorage.setItem('karaoke-page-scroll-mode', this.karaokePageScrollMode().toString()); }); + effect(() => { + localStorage.setItem('landscape-projection-enabled', this.landscapeProjectionEnabled().toString()); + }); + effect(() => { const active = this.keepScreenOn(); localStorage.setItem('keep-screen-on', active.toString()); @@ -426,6 +440,12 @@ export class SettingsService { localStorage.setItem('karaoke-page-scroll-mode', newValue.toString()); } + toggleLandscapeProjectionEnabled() { + const newValue = !this.landscapeProjectionEnabled(); + this.landscapeProjectionEnabled.set(newValue); + localStorage.setItem('landscape-projection-enabled', newValue.toString()); + } + setChordNotationPreference(val: 'diesis' | 'bemolle') { this.chordNotationPreference.set(val); } diff --git a/src/app/version.ts b/src/app/version.ts index b09cb27..eb86d9c 100644 --- a/src/app/version.ts +++ b/src/app/version.ts @@ -1 +1 @@ -export const VERSION = '2026.06.15.0021'; +export const VERSION = '2026.06.16.1611';