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 @@
= maxZoom()">
+
+
+ {{ fontSize().toFixed(1) }}
+
@@ -229,6 +233,10 @@
+
+
+ {{ fontSize().toFixed(1) }}
+
= maxZoom()">
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';