transpose & history

This commit is contained in:
2020-06-13 17:41:53 +02:00
parent bcbd119fbd
commit 835ffa9e8e
19 changed files with 256 additions and 23 deletions

View File

@@ -2,7 +2,6 @@ export const KEYS = [
'C#', 'C', 'Db', 'D#', 'D', 'Eb', 'E', 'F#', 'F', 'Gb', 'G#', 'G', 'Ab', 'A#', 'A', 'B', 'H',
'c#', 'c', 'db', 'd#', 'd', 'eb', 'e', 'f#', 'f', 'gb', 'g#', 'g', 'ab', 'a#', 'a', 'b', 'h'
];
export const KEYS_REGEX = new RegExp('(' + KEYS.reduce((a, b) => a + '|' + b) + ')', 'gm');
export const KEYS_MAJOR_FLAT = [
'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H',
];
@@ -16,6 +15,45 @@ export const KEYS_MINOR_B = [
'c', 'db', 'd', 'eb', 'e', 'f', 'gb', 'g', 'ab', 'a', 'b', 'h',
];
export type scale = 'b' | 'flat'
const scaleTypeAssignment: { [key: string]: string[][] } = {
'C': [KEYS_MAJOR_FLAT, KEYS_MINOR_FLAT],
'C#': [KEYS_MAJOR_FLAT, KEYS_MINOR_FLAT],
'Db': [KEYS_MAJOR_B, KEYS_MINOR_B],
'D': [KEYS_MAJOR_B, KEYS_MINOR_B],
'D#': [KEYS_MAJOR_FLAT, KEYS_MINOR_FLAT],
'Eb': [KEYS_MAJOR_B, KEYS_MINOR_B],
'E': [KEYS_MAJOR_FLAT, KEYS_MINOR_FLAT],
'F': [KEYS_MAJOR_B, KEYS_MINOR_B],
'F#': [KEYS_MAJOR_FLAT, KEYS_MINOR_FLAT],
'Gb': [KEYS_MAJOR_B, KEYS_MINOR_B],
'G': [KEYS_MAJOR_FLAT, KEYS_MINOR_FLAT],
'G#': [KEYS_MAJOR_FLAT, KEYS_MINOR_FLAT],
'Ab': [KEYS_MAJOR_B, KEYS_MINOR_B],
'A': [KEYS_MAJOR_FLAT, KEYS_MINOR_FLAT],
'A#': [KEYS_MAJOR_FLAT, KEYS_MINOR_FLAT],
'B': [KEYS_MAJOR_B, KEYS_MINOR_B],
'H': [KEYS_MAJOR_FLAT, KEYS_MINOR_FLAT],
'c': [KEYS_MINOR_FLAT, KEYS_MAJOR_FLAT],
'c#': [KEYS_MINOR_FLAT, KEYS_MAJOR_FLAT],
'db': [KEYS_MINOR_B, KEYS_MAJOR_B],
'd': [KEYS_MINOR_B, KEYS_MAJOR_B],
'd#': [KEYS_MINOR_FLAT, KEYS_MAJOR_FLAT],
'eb': [KEYS_MINOR_B, KEYS_MAJOR_B],
'e': [KEYS_MINOR_FLAT, KEYS_MAJOR_FLAT],
'f': [KEYS_MINOR_B, KEYS_MAJOR_B],
'f#': [KEYS_MINOR_FLAT, KEYS_MAJOR_FLAT],
'gb': [KEYS_MINOR_B, KEYS_MAJOR_B],
'g': [KEYS_MINOR_FLAT, KEYS_MAJOR_FLAT],
'g#': [KEYS_MINOR_FLAT, KEYS_MAJOR_FLAT],
'ab': [KEYS_MINOR_B, KEYS_MAJOR_B],
'a': [KEYS_MINOR_FLAT, KEYS_MAJOR_FLAT],
'a#': [KEYS_MINOR_FLAT, KEYS_MAJOR_FLAT],
'b': [KEYS_MINOR_B, KEYS_MAJOR_B],
'h': [KEYS_MINOR_FLAT, KEYS_MAJOR_FLAT],
}
const scaleAssignment = {
'C': KEYS_MAJOR_FLAT,
'C#': KEYS_MAJOR_FLAT,
@@ -90,7 +128,6 @@ export const scaleMapping = {
'h': 'h',
};
export const getScale = (key: string): string[] => {
const scaleAssignmentElement = scaleAssignment[key];
return scaleAssignmentElement;
};
export const getScale = (key: string): string[] => scaleAssignment[key];
export const getScaleType = (key: string): string[][] => scaleTypeAssignment[key];

View File

@@ -3,6 +3,9 @@ import {Observable} from 'rxjs';
import {Song} from './song';
import {SongDataService} from './song-data.service';
import {first, tap} from 'rxjs/operators';
import {UserService} from '../../../services/user/user.service';
import * as firebase from 'firebase';
import Timestamp = firebase.firestore.Timestamp;
declare var importCCLI: any;
@@ -19,7 +22,7 @@ export class SongService {
private list: Song[];
constructor(private songDataService: SongDataService) {
constructor(private songDataService: SongDataService, private userService: UserService) {
importCCLI = (songs: Song[]) => this.updateFromCLI(songs);
}
@@ -28,7 +31,11 @@ export class SongService {
public read = (songId: string): Promise<Song | undefined> => this.read$(songId).pipe(first()).toPromise();
public async update$(songId: string, data: Partial<Song>): Promise<void> {
await this.songDataService.update$(songId, data);
const song = await this.read(songId);
const edits = song.edits ?? [];
const user = await this.userService.currentUser();
edits.push({username: user.name, timestamp: Timestamp.now()})
await this.songDataService.update$(songId, {...data, edits});
}
public async new(number: number, title: string): Promise<string> {

View File

@@ -1,3 +1,6 @@
import Timestamp = firebase.firestore.Timestamp;
import * as firebase from 'firebase';
export interface Song {
id: string;
comment: string;
@@ -19,4 +22,11 @@ export interface Song {
label: string;
termsOfUse: string;
origin: string;
edits: Edit[];
}
export interface Edit {
username: string;
timestamp: Timestamp;
}

View File

@@ -33,7 +33,7 @@ Cool bridge without any chords
it('should parse section types', () => {
const service: TextRenderingService = TestBed.get(TextRenderingService);
const sections = service.parse(testText);
const sections = service.parse(testText, null);
expect(sections[0].type).toBe(SectionType.Verse);
expect(sections[0].number).toBe(0);
expect(sections[1].type).toBe(SectionType.Verse);
@@ -46,7 +46,7 @@ Cool bridge without any chords
it('should parse text lines', () => {
const service: TextRenderingService = TestBed.get(TextRenderingService);
const sections = service.parse(testText);
const sections = service.parse(testText, null);
expect(sections[0].lines[1].type).toBe(LineType.text);
expect(sections[0].lines[1].text).toBe('Text Line 1-1');
expect(sections[0].lines[3].type).toBe(LineType.text);
@@ -63,7 +63,7 @@ Cool bridge without any chords
it('should parse chord lines', () => {
const service: TextRenderingService = TestBed.inject(TextRenderingService);
const sections = service.parse(testText);
const sections = service.parse(testText, null);
expect(sections[0].lines[0].type).toBe(LineType.chord);
expect(sections[0].lines[0].text).toBe('C D E F G A H');
expect(sections[0].lines[2].type).toBe(LineType.chord);
@@ -91,7 +91,7 @@ Cool bridge without any chords
const text = `Strophe
g# F# E g# F# E
text`
const sections = service.parse(text);
const sections = service.parse(text, null);
expect(sections[0].lines[0].type).toBe(LineType.chord);
expect(sections[0].lines[0].text).toBe('g# F# E g# F# E');
expect(sections[0].lines[1].type).toBe(LineType.text);

View File

@@ -1,4 +1,5 @@
import {Injectable} from '@angular/core';
import {TransposeMode, TransposeService} from './transpose.service';
export enum SectionType {
Verse,
@@ -22,7 +23,7 @@ export interface Chord {
export interface Line {
type: LineType;
text: string;
chords?: Chord[]
chords?: Chord[];
}
export interface Section {
@@ -37,10 +38,10 @@ export interface Section {
export class TextRenderingService {
private regexSection = /(Strophe|Refrain|Bridge)/;
constructor() {
constructor(private transposeService: TransposeService) {
}
public parse(text: string): Section[] {
public parse(text: string, transpose: TransposeMode): Section[] {
if (!text) return [];
const arrayOfLines = text.split(/\r?\n/).filter(_ => _);
const indices = {
@@ -55,18 +56,19 @@ export class TextRenderingService {
number: indices[type]++,
lines: []
}];
array[array.length - 1].lines.push(this.getLineOfLineText(line));
array[array.length - 1].lines.push(this.getLineOfLineText(line, transpose));
return array;
}, [] as Section[]);
}
private getLineOfLineText(text: string): Line {
private getLineOfLineText(text: string, transpose: TransposeMode): Line {
if (!text) return null;
const cords = this.readChords(text);
const hasMatches = cords.length > 0;
const type = hasMatches ? LineType.chord : LineType.text;
return {type, text, chords: hasMatches ? cords : undefined}
const line = {type, text, chords: hasMatches ? cords : undefined};
return transpose ? this.transposeService.transpose(line, transpose.baseKey, transpose.targetKey) : line;
}
private getSectionTypeOfLine(line: string): SectionType {

View File

@@ -0,0 +1,20 @@
import {TestBed} from '@angular/core/testing';
import {TransposeService} from './transpose.service';
describe('TransposeService', () => {
let service: TransposeService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(TransposeService);
});
it('should create map', () => {
const distance = service.getDistance('D', 'G');
const map = service.getMap('D', distance);
console.log(map);
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,80 @@
import {Injectable} from '@angular/core';
import {Chord, Line, LineType} from './text-rendering.service';
import {getScaleType, scaleMapping} from './key.helper';
export interface TransposeMode {
baseKey: string;
targetKey: string
}
@Injectable({
providedIn: 'root'
})
export class TransposeService {
public transpose(line: Line, baseKey: string, targetKey: string): Line {
if (line.type !== LineType.chord) return line;
const difference = this.getDistance(baseKey, targetKey);
const map = this.getMap(baseKey, difference);
const chords = line.chords.map(chord => this.transposeChord(chord, map));
const renderedLine = this.renderLine(chords);
return {...line, text: renderedLine, chords};
}
public getDistance(baseKey: string, targetKey: string): number {
const scale = getScaleType(baseKey);
return (
(scale[0].indexOf(targetKey) - scale[0].indexOf(baseKey)) ??
(scale[1].indexOf(targetKey) - scale[1].indexOf(baseKey))
) % 12;
}
public getMap(baseKey: string, difference: number) {
const scale = getScaleType(baseKey);
const map = {};
for (let i = 0; i < 12; i++) {
const source = scale[0][i];
const mappedIndex = (i + difference) % 12;
map[source] = scale[0][mappedIndex];
}
for (let i = 0; i < 12; i++) {
const source = scale[1][i];
const mappedIndex = (i + difference) % 12;
map[source] = scale[1][mappedIndex];
}
return map;
}
private transposeChord(chord: Chord, map: {}): Chord {
const translatedChord = map[chord.chord];
const translatedSlashChord = chord.slashChord ? map[chord.slashChord] : null;
return {...chord, chord: translatedChord, slashChord: translatedSlashChord};
}
private renderLine(chords: Chord[]): string {
let template = ' ';
chords.forEach(chord => {
const pos = chord.position;
const renderedChord = this.renderChord(chord);
const newLength = renderedChord.length;
const pre = template.substr(0, pos);
const post = template.substr(pos + newLength);
template = pre + renderedChord + post;
})
return template.trimRight();
}
private renderChord(chord: Chord) {
return (
scaleMapping[chord.chord] +
(chord.add ? chord.add : '') +
(chord.slashChord ? '/' + scaleMapping[chord.slashChord] : ''));
}
}

View File

@@ -1,4 +1,5 @@
<div>
<app-edit-song></app-edit-song>
<app-edit-file></app-edit-file>
<app-history></app-history>
</div>

View File

@@ -24,10 +24,11 @@ import {ButtonModule} from '../../../../widget-modules/components/button/button.
import {MatTooltipModule} from '@angular/material/tooltip';
import {SaveDialogComponent} from './edit-song/save-dialog/save-dialog.component';
import {MatDialogModule} from '@angular/material/dialog';
import {HistoryComponent} from './history/history.component';
@NgModule({
declarations: [EditComponent, EditSongComponent, EditFileComponent, FileComponent, SaveDialogComponent],
declarations: [EditComponent, EditSongComponent, EditFileComponent, FileComponent, SaveDialogComponent, HistoryComponent],
exports: [EditComponent],
bootstrap: [SaveDialogComponent],
imports: [

View File

@@ -0,0 +1,6 @@
<app-card *ngIf="song && song.edits" heading="letzte Änderungen">
<div *ngFor="let edit of song.edits" class="list">
<div>{{edit.username}}</div>
<div>{{edit.timestamp.toDate()|date:'dd.MM.yyyy'}}</div>
</div>
</app-card>

View File

@@ -0,0 +1,4 @@
.list {
display: grid;
grid-template-columns: 1fr 1fr;
}

View File

@@ -0,0 +1,25 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {HistoryComponent} from './history.component';
describe('HistoryComponent', () => {
let component: HistoryComponent;
let fixture: ComponentFixture<HistoryComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [HistoryComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HistoryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,31 @@
import {Component, OnInit} from '@angular/core';
import {first, map, switchMap} from 'rxjs/operators';
import {ActivatedRoute} from '@angular/router';
import {SongService} from '../../../services/song.service';
import {Song} from '../../../services/song';
@Component({
selector: 'app-history',
templateUrl: './history.component.html',
styleUrls: ['./history.component.less']
})
export class HistoryComponent implements OnInit {
public song: Song;
constructor(
private activatedRoute: ActivatedRoute,
private songService: SongService,
) {
}
public ngOnInit(): void {
this.activatedRoute.params.pipe(
map(param => param.songId),
switchMap(songId => this.songService.read$(songId)),
first()
).subscribe(song => {
this.song = song;
});
}
}