chore: save current changes
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user