validate chords

This commit is contained in:
2026-03-11 16:18:36 +01:00
parent 03fb395605
commit ae4459f5ce
12 changed files with 538 additions and 13 deletions

View File

@@ -1,4 +1,4 @@
<button mat-button>
<button [disabled]="disabled" mat-button>
@if (icon) {
<span
><fa-icon [icon]="icon"></fa-icon><span class="content">&nbsp;</span></span

View File

@@ -11,5 +11,6 @@ import {FaIconComponent} from '@fortawesome/angular-fontawesome';
imports: [MatButton, FaIconComponent],
})
export class ButtonComponent {
@Input() public disabled = false;
@Input() public icon: IconProp | null = null;
}

View File

@@ -26,7 +26,10 @@
[class.comment]="isComment(line.text)"
[class.disabled]="checkDisabled(i)"
class="line"
>{{ transform(line.text) }}
>
@for (segment of getDisplaySegments(line); track $index) {
<span [class.invalid-chord-token]="segment.invalid">{{ segment.text }}</span>
}
</div>
}
</div>
@@ -72,7 +75,10 @@
[class.chord]="line.type === 0"
[class.disabled]="checkDisabled(i)"
class="line"
>{{ line.text.trim() }}
>
@for (segment of getDisplaySegments(line, true); track $index) {
<span [class.invalid-chord-token]="segment.invalid">{{ segment.text }}</span>
}
</div>
}
</div>

View File

@@ -34,6 +34,11 @@
}
}
.invalid-chord-token {
color: #b42318;
text-shadow: none;
}
.menu {
position: absolute;
right: -10px;

View File

@@ -7,12 +7,18 @@ import {SectionType} from '../../../modules/songs/services/section-type';
import {LineType} from '../../../modules/songs/services/line-type';
import {Section} from '../../../modules/songs/services/section';
import {Line} from '../../../modules/songs/services/line';
import {ChordValidationIssue} from '../../../modules/songs/services/chord';
import {MatIconButton} from '@angular/material/button';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
export type ChordMode = 'show' | 'hide' | 'onlyFirst';
interface DisplaySegment {
text: string;
invalid: boolean;
}
@Component({
selector: 'app-song-text',
templateUrl: './song-text.component.html',
@@ -36,6 +42,7 @@ export class SongTextComponent implements OnInit {
public faLines = faGripLines;
public offset = 0;
public iChordMode: ChordMode = 'hide';
private invalidChordIssuesByLine = new Map<number, ChordValidationIssue[]>();
private iText = '';
private iTranspose: TransposeMode | null = null;
@@ -56,6 +63,18 @@ export class SongTextComponent implements OnInit {
this.render();
}
@Input()
public set invalidChordIssues(value: ChordValidationIssue[] | null) {
const issuesByLine = new Map<number, ChordValidationIssue[]>();
(value ?? []).forEach(issue => {
const lineIssues = issuesByLine.get(issue.lineNumber) ?? [];
lineIssues.push(issue);
issuesByLine.set(issue.lineNumber, lineIssues);
});
this.invalidChordIssuesByLine = issuesByLine;
this.cRef.markForCheck();
}
public ngOnInit(): void {
setInterval(() => {
if (!this.fullscreen || this.index === -1 || !this.viewSections?.toArray()[this.index]) {
@@ -106,6 +125,50 @@ export class SongTextComponent implements OnInit {
return text;
}
public getDisplaySegments(line: Line, trim = false): DisplaySegment[] {
const text = trim ? line.text.trim() : this.transform(line.text);
const lineNumber = line.lineNumber;
if (!lineNumber) {
return [{text, invalid: false}];
}
const issues = this.invalidChordIssuesByLine.get(lineNumber);
if (!issues || issues.length === 0) {
return [{text, invalid: false}];
}
const ranges: Array<{start: number; end: number}> = [];
let searchStart = 0;
issues.forEach(issue => {
const index = text.indexOf(issue.token, searchStart);
if (index === -1) {
return;
}
ranges.push({start: index, end: index + issue.token.length});
searchStart = index + issue.token.length;
});
if (ranges.length === 0) {
return [{text, invalid: false}];
}
const segments: DisplaySegment[] = [];
let cursor = 0;
ranges.forEach(range => {
if (range.start > cursor) {
segments.push({text: text.slice(cursor, range.start), invalid: false});
}
segments.push({text: text.slice(range.start, range.end), invalid: true});
cursor = range.end;
});
if (cursor < text.length) {
segments.push({text: text.slice(cursor), invalid: false});
}
return segments;
}
public isComment(text: string) {
return text.startsWith('#');
}