transpose & history
This commit is contained in:
@@ -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];
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
20
src/app/modules/songs/services/transpose.service.spec.ts
Normal file
20
src/app/modules/songs/services/transpose.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
80
src/app/modules/songs/services/transpose.service.ts
Normal file
80
src/app/modules/songs/services/transpose.service.ts
Normal 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] : ''));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<div>
|
||||
<app-edit-song></app-edit-song>
|
||||
<app-edit-file></app-edit-file>
|
||||
<app-history></app-history>
|
||||
</div>
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
.list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
31
src/app/modules/songs/song/edit/history/history.component.ts
Normal file
31
src/app/modules/songs/song/edit/history/history.component.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user