chore: save current changes

This commit is contained in:
David Frassi
2026-06-06 08:19:08 +02:00
parent e7e43468b3
commit 4d1883a45d
23 changed files with 1028 additions and 327 deletions
+261 -16
View File
@@ -6,13 +6,14 @@ import { createWorker } from 'tesseract.js';
import { CantiService } from '../../services/canti.service';
import { MyCantiService } from '../../services/my-canti.service';
import { ThemeService } from '../../services/theme.service';
import { ActivatedRoute, RouterModule } from '@angular/router';
@Component({
selector: 'app-propose-canto',
templateUrl: './propose-canto.page.html',
styleUrls: ['./propose-canto.page.scss'],
standalone: true,
imports: [CommonModule, FormsModule, IonicModule]
imports: [CommonModule, FormsModule, IonicModule, RouterModule]
})
export class ProposeCantoPage implements OnInit {
@ViewChild('contentTextarea', { static: false }) contentTextarea!: IonTextarea;
@@ -22,12 +23,14 @@ export class ProposeCantoPage implements OnInit {
private myCantiService = inject(MyCantiService);
private navCtrl = inject(NavController);
public themeService = inject(ThemeService);
private route = inject(ActivatedRoute);
title: string = '';
author: string = '';
youtubeLink: string = '';
selectedLiturgico: number[] = [];
selectedTematico: number[] = [];
editId: string | null = null;
private _content: string = '';
get content(): string { return this._content; }
@@ -41,6 +44,7 @@ export class ProposeCantoPage implements OnInit {
undoStack: string[] = [];
isProcessingOCR: boolean = false;
ocrProgress: number = 0;
isDraggingOver: boolean = false;
get isHighContrast(): boolean { return this.themeService.highContrast(); }
groupedChords = [
@@ -104,6 +108,31 @@ export class ProposeCantoPage implements OnInit {
constructor(private toastController: ToastController, private popoverController: PopoverController) { }
ngOnInit() {
this.route.queryParams.subscribe(params => {
const editId = params['editId'];
if (editId) {
this.editId = editId;
// Find the song in standard canti or personal canti list
const song = [
...this.cantiService.canti(),
...this.myCantiService.myCanti()
].find(c => c.id === editId);
if (song) {
this.title = song.titolo;
this.author = song.autore || '';
this.youtubeLink = song.link_youtube || '';
this.content = song.accordi || song.testo || '';
// Pre-populate liturgico and tematico lists
const litIds = this.cantiService.indiceLiturgico().map(m => m.id);
const temIds = this.cantiService.indiceTematico().map(m => m.id);
this.selectedLiturgico = song.id_momenti?.filter(id => litIds.includes(id)) || [];
this.selectedTematico = song.id_momenti?.filter(id => temIds.includes(id)) || [];
}
}
});
}
async insertText(tag: string) {
@@ -143,13 +172,70 @@ export class ProposeCantoPage implements OnInit {
this.cameraInput.nativeElement.click();
}
onDragOver(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
// Mostra l'overlay solo se si sta trascinando un file
if (event.dataTransfer?.types.includes('Files')) {
this.isDraggingOver = true;
}
}
onDragLeave(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
this.isDraggingOver = false;
}
async onDrop(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
this.isDraggingOver = false;
if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) {
const file = event.dataTransfer.files[0];
if (file.type.indexOf('image') !== -1) {
await this.processImageFile(file);
} else {
const toast = await this.toastController.create({
message: 'Per favore, trascina un file immagine valido.',
duration: 3000,
color: 'warning'
});
toast.present();
}
}
}
async onFileSelected(event: any, isCamera: boolean) {
const file = event.target.files[0];
if (!file) {
console.log('[OCR-Capture] Nessun file selezionato.');
return;
}
await this.processImageFile(file);
event.target.value = '';
}
async onPaste(event: ClipboardEvent) {
const items = event.clipboardData?.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
event.preventDefault(); // Prevent pasting the image representation as text
const blob = items[i].getAsFile();
if (blob) {
const file = new File([blob], 'pasted-image.png', { type: blob.type });
await this.processImageFile(file);
}
break;
}
}
}
async processImageFile(file: File) {
console.log(`[OCR-Capture] File selezionato: nome="${file.name}", tipo="${file.type}", dimensione=${(file.size / (1024 * 1024)).toFixed(2)} MB`);
this.isProcessingOCR = true;
@@ -187,7 +273,6 @@ export class ProposeCantoPage implements OnInit {
} finally {
this.isProcessingOCR = false;
this.ocrProgress = 0;
event.target.value = '';
}
}
@@ -293,7 +378,7 @@ export class ProposeCantoPage implements OnInit {
// Calculate average word height to set vertical tolerance
const heights = validWords.map(w => w.bbox.y1 - w.bbox.y0);
const avgHeight = heights.reduce((sum, h) => sum + h, 0) / heights.length;
const verticalTolerance = avgHeight * 0.6;
const verticalTolerance = avgHeight * 0.85;
console.log(`[OCR-Capture] Altezza media carattere: ${avgHeight.toFixed(1)}px, tolleranza verticale: ${verticalTolerance.toFixed(1)}px`);
// 2. Group words into horizontal lines
@@ -324,16 +409,43 @@ export class ProposeCantoPage implements OnInit {
});
// 3. Classify lines as Chords vs. Text
const chordRegex = /^(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(#|B)?(M|-|MIN|MAJ|AUG|DIM)?(7|9|11|13)?$/i;
const chordRegex = /^(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(#|B)?(M|-|MIN|MAJ|AUG|DIM)?(7|9|11|13)?(\/(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(#|B)?)?$/i;
const isChordWord = (text: string): boolean => {
const clean = text.replace(/[\[\]\(\)\.\,\-\+]/g, '').trim().toUpperCase();
if (!text || text.length === 0) return false;
const lower = text.toLowerCase();
// Skip common valid words written purely in lowercase
if (text[0] === lower[0] && ['la', 'mi', 're', 'do', 'si', 'fa', 'e'].includes(lower)) {
return false;
}
let clean = text.toUpperCase().replace(/\s+/g, '');
clean = clean.replace(/\((.*?)\)/g, '/$1');
clean = clean.replace(/[\.\,]$/g, '');
return chordRegex.test(clean);
};
const classifiedLines = lines.map(line => {
const chordCount = line.filter(w => isChordWord(w.text)).length;
let chordCount = 0;
let hasLongNonChord = false;
line.forEach(w => {
if (isChordWord(w.text)) {
chordCount++;
} else {
const clean = w.text.replace(/[.,:;!\?]/g, '').trim();
if (clean.length > 5) {
hasLongNonChord = true;
}
}
});
const ratio = line.length > 0 ? chordCount / line.length : 0;
const isChords = ratio >= 0.4 && line.length <= 10;
let isChords = false;
if (ratio >= 0.4 && line.length <= 10) {
if (!hasLongNonChord || ratio >= 0.75) {
isChords = true;
}
}
return {
words: line,
@@ -371,8 +483,29 @@ export class ProposeCantoPage implements OnInit {
i++; // Skip next line because we consumed it!
} else {
// Chord line but no text below it: just wrap and print
const wrapped = current.words.map(w => `[${w.text.replace(/[\(\)\[\]]/g, '').toUpperCase()}]`).join(' ');
processedLines.push(wrapped);
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()}]`));
} else {
expandedChords.push(w.text); // keep original text if it's not a pure chord merge
}
}
});
const wrapped = expandedChords.join(' ');
if (wrapped) {
processedLines.push(wrapped);
}
}
} else {
const lineText = current.words.map(w => w.text).join(' ');
@@ -408,17 +541,106 @@ export class ProposeCantoPage implements OnInit {
}
mergeChordsAndLyrics(chordWords: any[], textWords: any[]): string {
let result = '';
// Pre-process chordWords to merge fragmented bass notes like 'Re', '(f', 'fa#)'
let mergedChordWords: any[] = [];
for (let i = 0; i < chordWords.length; i++) {
let cw = chordWords[i];
if (cw.text.startsWith('(') && mergedChordWords.length > 0) {
let prev = mergedChordWords[mergedChordWords.length - 1];
prev.text += cw.text;
prev.bbox.x1 = Math.max(prev.bbox.x1, cw.bbox.x1);
if (!prev.text.includes(')')) {
let j = i + 1;
while (j < chordWords.length) {
prev.text += chordWords[j].text;
prev.bbox.x1 = Math.max(prev.bbox.x1, chordWords[j].bbox.x1);
if (chordWords[j].text.includes(')')) {
i = j;
break;
}
j++;
}
}
} else {
let text = cw.text;
let bbox = { ...cw.bbox };
if (text.includes('(') && !text.includes(')')) {
let j = i + 1;
while (j < chordWords.length) {
text += chordWords[j].text;
bbox.x1 = Math.max(bbox.x1, chordWords[j].bbox.x1);
if (chordWords[j].text.includes(')')) {
i = j;
break;
}
j++;
}
}
mergedChordWords.push({ text, bbox });
}
}
// Sanitize common OCR errors in chords (e.g., 'Re(f fa#)' -> 'Re(fa#)')
mergedChordWords.forEach(cw => {
cw.text = cw.text.replace(/f\s*fa#/gi, 'fa#');
cw.text = cw.text.replace(/ff/gi, 'f');
cw.text = cw.text.replace(/m\s*mi/gi, 'mi');
cw.text = cw.text.replace(/mm/gi, 'm');
});
if (!textWords || textWords.length === 0) {
return mergedChordWords.map(c => {
let clean = c.text.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
return `[${clean}]`;
}).join(' ');
}
const chordRegex = /^(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(#|B)?(M|-|MIN|MAJ|AUG|DIM)?(7|9|11|13)?(\/(DO|RE|MI|FA|SOL|LA|SI|C|D|E|F|G|A|B)(#|B)?)?$/i;
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 expandedChordWords: any[] = [];
mergedChordWords.forEach(chord => {
let originalText = chord.text.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
const matches = [...originalText.matchAll(multiChordRegex)];
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 }
});
});
} else {
expandedChordWords.push(chord);
}
}
});
const chordAssignments = new Map<any, any[]>();
chordWords.forEach(chord => {
expandedChordWords.forEach(chord => {
const chordX = (chord.bbox.x0 + chord.bbox.x1) / 2;
let closestWord: any = null;
let minDistance = Infinity;
textWords.forEach(textWord => {
const wordXCenter = (textWord.bbox.x0 + textWord.bbox.x1) / 2;
const dist = Math.abs(chordX - wordXCenter);
let dist = 0;
if (chordX < textWord.bbox.x0) {
dist = textWord.bbox.x0 - chordX;
} else if (chordX > textWord.bbox.x1) {
dist = chordX - textWord.bbox.x1;
}
if (dist < minDistance) {
minDistance = dist;
closestWord = textWord;
@@ -433,22 +655,44 @@ export class ProposeCantoPage implements OnInit {
}
});
let result = '';
textWords.forEach((textWord, index) => {
const assignedChords = chordAssignments.get(textWord) || [];
assignedChords.sort((a, b) => a.bbox.x0 - b.bbox.x0);
const wordText = textWord.text;
let charWidth = (textWord.bbox.x1 - textWord.bbox.x0) / Math.max(1, wordText.length);
if (charWidth <= 0) charWidth = 6; // safe fallback
let lastCharIndex = 0;
let wordResult = '';
assignedChords.forEach(chord => {
const chordX = (chord.bbox.x0 + chord.bbox.x1) / 2;
let charIndex = Math.round((chordX - textWord.bbox.x0) / charWidth);
if (charIndex < 0) charIndex = 0;
if (charIndex > wordText.length) charIndex = wordText.length;
const cleanChord = chord.text.replace(/[\(\)\[\]]/g, '').toUpperCase();
result += `[${cleanChord}]`;
wordResult += wordText.substring(lastCharIndex, charIndex) + `[${cleanChord}]`;
lastCharIndex = charIndex;
});
result += textWord.text;
wordResult += wordText.substring(lastCharIndex);
result += wordResult;
if (index < textWords.length - 1) {
result += ' ';
}
});
return result;
const finalResult = result.replace(/\]\[/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;
}
@@ -506,6 +750,7 @@ export class ProposeCantoPage implements OnInit {
const id_momenti = [...this.selectedLiturgico, ...this.selectedTematico];
await this.myCantiService.saveCanto({
id: this.editId || undefined,
titolo: this.title,
autore: this.author,
link_youtube: this.youtubeLink,