tasto back e bugfix editor

This commit is contained in:
David Frassi
2026-06-18 13:55:55 +02:00
parent d4a52a8fbf
commit 68d455289d
9 changed files with 471 additions and 19 deletions
@@ -31,9 +31,19 @@
</div>
</div>
<!-- Segment Control for mobile layout -->
<ion-segment [(ngModel)]="activeTab" class="custom-mobile-segment ion-margin-bottom" mode="md">
<ion-segment-button value="editor">
<ion-label>Editor</ion-label>
</ion-segment-button>
<ion-segment-button value="preview">
<ion-label>Anteprima</ion-label>
</ion-segment-button>
</ion-segment>
<div class="editor-preview-split">
<!-- Left Column: Inputs & Editor -->
<div class="editor-column">
<div class="editor-column" [class.hide-on-mobile]="activeTab === 'preview'">
<ion-list lines="none" class="input-list">
<ion-item class="custom-input-item">
<ion-label position="stacked">Titolo del Canto</ion-label>
@@ -44,7 +54,7 @@
<ion-item class="custom-input-item select-item">
<ion-label position="stacked">Momento Liturgico</ion-label>
<ion-select [(ngModel)]="selectedLiturgico" placeholder="Scegli momento" multiple="true" interface="popover">
<ion-select-option *ngFor="let lit of cantiService.indiceLiturgico()" [value]="lit.id">
<ion-select-option *ngFor="let lit of sortedLiturgico" [value]="lit.id">
{{ lit.tag_name }}
</ion-select-option>
</ion-select>
@@ -53,7 +63,7 @@
<ion-item class="custom-input-item select-item">
<ion-label position="stacked">Periodo / Tema</ion-label>
<ion-select [(ngModel)]="selectedTematico" placeholder="Scegli tema" multiple="true" interface="popover">
<ion-select-option *ngFor="let tem of cantiService.indiceTematico()" [value]="tem.id">
<ion-select-option *ngFor="let tem of sortedTematico" [value]="tem.id">
{{ tem.tag_name }}
</ion-select-option>
</ion-select>
@@ -103,13 +113,16 @@
<span>Editor Testo</span>
</div>
<div class="editor-actions">
<ion-button fill="clear" size="small" (click)="undo()" [disabled]="undoStack.length === 0">
<ion-button fill="clear" size="small" (click)="undo()" [disabled]="undoStack.length === 0" title="Annulla ultima modifica">
<ion-icon name="undo-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" size="small" (click)="takePhoto()">
<ion-button fill="clear" size="small" (click)="deduceChords()" [disabled]="!content" title="Deduci accordi per le altre strofe">
<ion-icon name="musical-notes-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" size="small" (click)="takePhoto()" title="Scatta foto">
<ion-icon name="camera-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" size="small" (click)="chooseFile()">
<ion-button fill="clear" size="small" (click)="chooseFile()" title="Allega file o PDF">
<ion-icon name="document-attach-outline"></ion-icon>
</ion-button>
</div>
@@ -142,7 +155,7 @@
</div>
<!-- Right Column: Active Preview Pane -->
<div class="preview-column">
<div class="preview-column" [class.hide-on-mobile]="activeTab === 'editor'">
<div class="preview-card" [class.hc]="isHighContrast">
<div class="preview-header">
<span class="preview-title">Anteprima Attiva</span>
@@ -702,3 +702,90 @@ body.high-contrast :host ::ng-deep {
}
}
}
/* CUSTOM STYLES FOR INTUITIVE MOBILE RESPONSIVENESS */
.custom-mobile-segment {
display: none;
--background: rgba(255, 255, 255, 0.05);
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 4px;
border: 1px solid rgba(255, 255, 255, 0.08);
margin-bottom: 16px;
ion-segment-button {
--color: rgba(255, 255, 255, 0.6);
--color-checked: #ffffff;
--indicator-color: var(--ion-color-secondary);
--border-radius: 8px;
font-weight: 700;
font-family: 'Outfit', sans-serif;
}
}
body.high-contrast :host ::ng-deep {
.custom-mobile-segment {
--background: #f0f0f0 !important;
background: #f0f0f0 !important;
border: 2px solid #000000 !important;
ion-segment-button {
--color: #333333 !important;
--color-checked: #000000 !important;
--indicator-color: #000000 !important;
}
}
}
@media (max-width: 991px) {
.custom-mobile-segment {
display: flex;
}
.hide-on-mobile {
display: none !important;
}
.horizontal-toolbar {
flex-wrap: wrap;
overflow-x: visible;
padding-bottom: 0;
ion-button {
flex-shrink: 1;
}
}
}
@media (max-width: 576px) {
:host ::ng-deep .category-selectors {
grid-template-columns: 1fr;
gap: 8px;
}
.editor-wrapper {
.editor-header {
padding: 6px 8px;
flex-wrap: wrap;
.editor-title-group {
width: 100%;
margin-bottom: 6px;
padding-left: 8px;
}
.editor-actions {
width: 100%;
display: flex;
justify-content: space-between;
ion-button {
flex: 1;
height: 40px;
margin: 0;
}
}
}
}
}
@@ -33,6 +33,7 @@ export class ProposeCantoPage implements OnInit {
private alertCtrl = inject(AlertController);
showChordsPreview: boolean = true;
activeTab: string = 'editor';
get parsedSections(): ParsedSection[] {
return this.lyricsParser.parseAccordi(this.content);
@@ -47,6 +48,15 @@ export class ProposeCantoPage implements OnInit {
youtubeLink: string = '';
selectedLiturgico: number[] = [];
selectedTematico: number[] = [];
get sortedLiturgico() {
return [...this.cantiService.indiceLiturgico()].sort((a, b) => a.tag_name.localeCompare(b.tag_name));
}
get sortedTematico() {
return [...this.cantiService.indiceTematico()].sort((a, b) => a.tag_name.localeCompare(b.tag_name));
}
editId: string | null = null;
private _content: string = '';
@@ -195,6 +205,194 @@ export class ProposeCantoPage implements OnInit {
this.fileInput.nativeElement.click();
}
async deduceChords() {
if (!this.content) return;
// Salva nello stack degli undo
this.saveToUndoStack(this.content);
const lines = this.content.split('\n');
interface SectionLine {
originalIndex: number;
text: string;
}
interface Section {
type: 'verse' | 'chorus' | 'other';
startTag: string | null;
endTag: string | null;
lines: SectionLine[];
hasChords: boolean;
}
const sections: Section[] = [];
let currentSection: Section = { type: 'other', startTag: null, endTag: null, lines: [], hasChords: false };
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
const startMatch = trimmed.match(/^\{(start_verse|start_chorus|start_verse_num|sov|soc)\}/);
const endMatch = trimmed === '{end_verse}' || trimmed === '{eov}' || trimmed === '{end_chorus}' || trimmed === '{eoc}' || trimmed === '{end_verse_num}';
if (startMatch) {
if (currentSection.lines.length > 0 || currentSection.startTag) {
sections.push(currentSection);
}
let type: 'verse' | 'chorus' | 'other' = 'other';
const tag = startMatch[1];
if (tag === 'start_verse' || tag === 'start_verse_num' || tag === 'sov') {
type = 'verse';
} else if (tag === 'start_chorus' || tag === 'soc') {
type = 'chorus';
}
currentSection = { type, startTag: line, endTag: null, lines: [], hasChords: false };
} else if (endMatch) {
currentSection.endTag = line;
sections.push(currentSection);
currentSection = { type: 'other', startTag: null, endTag: null, lines: [], hasChords: false };
} else {
if (trimmed === '' && !currentSection.startTag) {
if (currentSection.lines.length > 0) {
sections.push(currentSection);
}
sections.push({ type: 'other', startTag: null, endTag: null, lines: [{ originalIndex: i, text: '' }], hasChords: false });
currentSection = { type: 'other', startTag: null, endTag: null, lines: [], hasChords: false };
} else {
const hasChords = /\[[^\]]+\]/.test(line);
if (hasChords) {
currentSection.hasChords = true;
}
if (currentSection.lines.length === 0 && !currentSection.startTag && trimmed !== '') {
currentSection.type = 'verse';
}
const isComment = trimmed.startsWith('{c:') || trimmed.startsWith('{comment:');
if (!isComment) {
currentSection.lines.push({ originalIndex: i, text: line });
}
}
}
}
if (currentSection.lines.length > 0 || currentSection.startTag) {
sections.push(currentSection);
}
const templateVerse = sections.find(s => s.type === 'verse' && s.hasChords);
const templateChorus = sections.find(s => s.type === 'chorus' && s.hasChords);
let deducedCount = 0;
const getWords = (text: string) => {
const words: { text: string; start: number; end: number }[] = [];
const regex = /\S+/g;
let match;
while ((match = regex.exec(text)) !== null) {
words.push({
text: match[0],
start: match.index,
end: match.index + match[0].length
});
}
return words;
};
const alignChords = (templateLine: string, targetLine: string): string => {
if (/\[[^\]]+\]/.test(targetLine)) {
return targetLine;
}
const parsedTemplate = this.lyricsParser.parseChordLine(templateLine);
const templateClean = parsedTemplate.text;
const targetClean = targetLine.replace(/\[[^\]]*\]/g, '');
if (!templateClean.trim() || !targetClean.trim()) {
return targetLine;
}
const chords: { chord: string; charIndex: number }[] = [];
let charAcc = 0;
parsedTemplate.segments.forEach(seg => {
if (seg.chord) {
chords.push({ chord: seg.chord, charIndex: charAcc });
}
charAcc += seg.text.length;
});
if (chords.length === 0) {
return targetLine;
}
const templateWords = getWords(templateClean);
const targetWords = getWords(targetClean);
const insertions: { chord: string; index: number }[] = [];
chords.forEach(c => {
const ratio = c.charIndex / Math.max(1, templateClean.length);
const wordIdx = templateWords.findIndex(w => Math.abs(w.start - c.charIndex) <= 1);
let targetIndex = 0;
if (wordIdx !== -1 && templateWords.length > 1 && targetWords.length > 1) {
const wRatio = wordIdx / (templateWords.length - 1);
const targetWIdx = Math.round(wRatio * (targetWords.length - 1));
targetIndex = targetWords[targetWIdx].start;
} else {
targetIndex = Math.round(ratio * targetClean.length);
}
insertions.push({ chord: c.chord, index: targetIndex });
});
insertions.sort((a, b) => b.index - a.index);
let result = targetClean;
insertions.forEach(ins => {
result = result.substring(0, ins.index) + `[${ins.chord}]` + result.substring(ins.index);
});
return result;
};
const newLines = [...lines];
sections.forEach(sec => {
if (!sec.hasChords && sec.type !== 'other') {
const template = sec.type === 'verse' ? templateVerse : templateChorus;
if (template) {
for (let j = 0; j < sec.lines.length; j++) {
const targetSecLine = sec.lines[j];
if (j < template.lines.length) {
const templateSecLine = template.lines[j];
const aligned = alignChords(templateSecLine.text, targetSecLine.text);
if (aligned !== targetSecLine.text) {
newLines[targetSecLine.originalIndex] = aligned;
deducedCount++;
}
}
}
}
}
});
if (deducedCount > 0) {
this.content = newLines.join('\n');
const toast = await this.toastController.create({
message: `Accordi dedotti con successo in ${deducedCount} righe!`,
duration: 3000,
color: 'success'
});
toast.present();
} else {
const toast = await this.toastController.create({
message: 'Nessuna strofa compatibile trovata o accordi già presenti.',
duration: 3000,
color: 'warning'
});
toast.present();
}
}
onDragOver(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
@@ -804,11 +1002,11 @@ export class ProposeCantoPage implements OnInit {
return italianChordsCount > englishChordsCount;
}
isChordWord(text: string, isItalian: boolean): boolean {
isChordWord(text: string, isItalian: boolean, allowLowercase: boolean = false): boolean {
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)) {
// Skip common valid words written purely in lowercase unless allowLowercase is true
if (!allowLowercase && text[0] === lower[0] && ['la', 'mi', 're', 'do', 'si', 'fa', 'e'].includes(lower)) {
return false;
}
let clean = text.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
@@ -943,7 +1141,7 @@ export class ProposeCantoPage implements OnInit {
let hasLongNonChord = false;
line.forEach(w => {
if (this.isChordWord(w.text, isItalian)) {
if (this.isChordWord(w.text, isItalian, true)) {
chordCount++;
} else {
const clean = w.text.replace(/[.,:;!\?]/g, '').trim();
@@ -1047,10 +1245,11 @@ export class ProposeCantoPage implements OnInit {
}
// If we see a large vertical gap, close open blocks
const lastProcessedLine = classifiedLines[i];
const currentNext = (i + 1 < classifiedLines.length) ? classifiedLines[i + 1] : null;
if (currentNext) {
const gap = currentNext.yCenter - current.yCenter;
if (gap > avgHeight * 2.5) {
if (currentNext && lastProcessedLine) {
const gap = currentNext.yCenter - lastProcessedLine.yCenter;
if (gap > avgHeight * 3.5) {
if (inChorus) { processedLines.push('{end_chorus}'); inChorus = false; }
if (inVerse) { processedLines.push('{end_verse}'); inVerse = false; }
processedLines.push('');
@@ -1120,7 +1319,7 @@ export class ProposeCantoPage implements OnInit {
if (!part.trim()) return part;
let clean = part.toUpperCase().replace(/\s+/g, '').replace(/\((.*?)\)/g, '/$1').replace(/[\.\,\[\]]/g, '');
clean = this.sanitizeOcrChord(clean);
if (this.isChordWord(part, isItalian)) {
if (this.isChordWord(part, isItalian, true)) {
return `[${this.convertEnglishChordToItalian(clean, isItalian)}]`;
} else {
return part;
@@ -1234,7 +1433,7 @@ export class ProposeCantoPage implements OnInit {
let cleanChord = chord.text.replace(/[\(\)\[\]]/g, '').toUpperCase();
cleanChord = this.sanitizeOcrChord(cleanChord);
if (this.isChordWord(chord.text, isItalian)) {
if (this.isChordWord(chord.text, isItalian, true)) {
wordResult += wordText.substring(lastCharIndex, charIndex) + `[${this.convertEnglishChordToItalian(cleanChord, isItalian)}]`;
} else {
wordResult += wordText.substring(lastCharIndex, charIndex);