diff --git a/src/app/modules/presentation/monitor/monitor.component.ts b/src/app/modules/presentation/monitor/monitor.component.ts index 0e285da..3e32b9f 100644 --- a/src/app/modules/presentation/monitor/monitor.component.ts +++ b/src/app/modules/presentation/monitor/monitor.component.ts @@ -51,7 +51,7 @@ export class MonitorComponent implements OnInit { switchMap(_ => this.songService.read$(_.presentationSongId)) ).subscribe((_: Song) => { this.song = _; - this.sections = this.textRenderingService.parse(_.text); + this.sections = this.textRenderingService.parse(_.text, null); }); } diff --git a/src/app/modules/presentation/remote/remote.component.ts b/src/app/modules/presentation/remote/remote.component.ts index 46235d7..a823127 100644 --- a/src/app/modules/presentation/remote/remote.component.ts +++ b/src/app/modules/presentation/remote/remote.component.ts @@ -75,7 +75,7 @@ export class RemoteComponent { .map(song => ({ id: song.id, title: song.title, - sections: this.textRenderingService.parse(song.text) + sections: this.textRenderingService.parse(song.text, null) })) }); await delay(500); diff --git a/src/app/modules/shows/services/docx.service.ts b/src/app/modules/shows/services/docx.service.ts index 5ae7ac2..0d3f70d 100644 --- a/src/app/modules/shows/services/docx.service.ts +++ b/src/app/modules/shows/services/docx.service.ts @@ -180,7 +180,10 @@ export class DocxService { const showSongs = await this.showSongService.list(showId); const songsAsync = await showSongs.map(async showSong => { const song = await this.songService.read(showSong.songId); - const sections = this.textRenderingService.parse(song.text); + const sections = this.textRenderingService.parse(song.text, { + baseKey: showSong.keyOriginal, + targetKey: showSong.key + }); return { showSong, song, diff --git a/src/app/modules/shows/show/song/song.component.html b/src/app/modules/shows/show/song/song.component.html index 80572b7..ca51ee5 100644 --- a/src/app/modules/shows/show/song/song.component.html +++ b/src/app/modules/shows/show/song/song.component.html @@ -16,6 +16,6 @@ diff --git a/src/app/modules/songs/services/key.helper.ts b/src/app/modules/songs/services/key.helper.ts index e89b3d5..009ed66 100644 --- a/src/app/modules/songs/services/key.helper.ts +++ b/src/app/modules/songs/services/key.helper.ts @@ -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]; + diff --git a/src/app/modules/songs/services/song.service.ts b/src/app/modules/songs/services/song.service.ts index be1b619..b4397d7 100644 --- a/src/app/modules/songs/services/song.service.ts +++ b/src/app/modules/songs/services/song.service.ts @@ -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 => this.read$(songId).pipe(first()).toPromise(); public async update$(songId: string, data: Partial): Promise { - 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 { diff --git a/src/app/modules/songs/services/song.ts b/src/app/modules/songs/services/song.ts index edc25fb..b29faf0 100644 --- a/src/app/modules/songs/services/song.ts +++ b/src/app/modules/songs/services/song.ts @@ -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; } diff --git a/src/app/modules/songs/services/text-rendering.service.spec.ts b/src/app/modules/songs/services/text-rendering.service.spec.ts index 12c4c91..986b14d 100644 --- a/src/app/modules/songs/services/text-rendering.service.spec.ts +++ b/src/app/modules/songs/services/text-rendering.service.spec.ts @@ -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); diff --git a/src/app/modules/songs/services/text-rendering.service.ts b/src/app/modules/songs/services/text-rendering.service.ts index 1cdea64..3574fb4 100644 --- a/src/app/modules/songs/services/text-rendering.service.ts +++ b/src/app/modules/songs/services/text-rendering.service.ts @@ -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 { diff --git a/src/app/modules/songs/services/transpose.service.spec.ts b/src/app/modules/songs/services/transpose.service.spec.ts new file mode 100644 index 0000000..1616e46 --- /dev/null +++ b/src/app/modules/songs/services/transpose.service.spec.ts @@ -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(); + }); +}); diff --git a/src/app/modules/songs/services/transpose.service.ts b/src/app/modules/songs/services/transpose.service.ts new file mode 100644 index 0000000..65a7020 --- /dev/null +++ b/src/app/modules/songs/services/transpose.service.ts @@ -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] : '')); + } +} diff --git a/src/app/modules/songs/song/edit/edit.component.html b/src/app/modules/songs/song/edit/edit.component.html index be84ccf..70748b3 100644 --- a/src/app/modules/songs/song/edit/edit.component.html +++ b/src/app/modules/songs/song/edit/edit.component.html @@ -1,4 +1,5 @@
+
diff --git a/src/app/modules/songs/song/edit/edit.module.ts b/src/app/modules/songs/song/edit/edit.module.ts index 9d9c7de..6a09387 100644 --- a/src/app/modules/songs/song/edit/edit.module.ts +++ b/src/app/modules/songs/song/edit/edit.module.ts @@ -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: [ diff --git a/src/app/modules/songs/song/edit/history/history.component.html b/src/app/modules/songs/song/edit/history/history.component.html new file mode 100644 index 0000000..318cfc5 --- /dev/null +++ b/src/app/modules/songs/song/edit/history/history.component.html @@ -0,0 +1,6 @@ + +
+
{{edit.username}}
+
{{edit.timestamp.toDate()|date:'dd.MM.yyyy'}}
+
+
diff --git a/src/app/modules/songs/song/edit/history/history.component.less b/src/app/modules/songs/song/edit/history/history.component.less new file mode 100644 index 0000000..600f8ac --- /dev/null +++ b/src/app/modules/songs/song/edit/history/history.component.less @@ -0,0 +1,4 @@ +.list { + display: grid; + grid-template-columns: 1fr 1fr; +} diff --git a/src/app/modules/songs/song/edit/history/history.component.spec.ts b/src/app/modules/songs/song/edit/history/history.component.spec.ts new file mode 100644 index 0000000..bae2d6b --- /dev/null +++ b/src/app/modules/songs/song/edit/history/history.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [HistoryComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HistoryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/modules/songs/song/edit/history/history.component.ts b/src/app/modules/songs/song/edit/history/history.component.ts new file mode 100644 index 0000000..00032d3 --- /dev/null +++ b/src/app/modules/songs/song/edit/history/history.component.ts @@ -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; + }); + } +} diff --git a/src/app/services/user/user.service.ts b/src/app/services/user/user.service.ts index 94a06c5..6c1b9c6 100644 --- a/src/app/services/user/user.service.ts +++ b/src/app/services/user/user.service.ts @@ -31,6 +31,10 @@ export class UserService { return this._user$.pipe(filter(_ => !!_)); } + public async currentUser(): Promise { + return this.user$.pipe(first()).toPromise(); + } + public getUserbyId(userId: string): Promise { return this.getUserbyId$(userId).pipe(first()).toPromise(); } diff --git a/src/app/widget-modules/components/song-text/song-text.component.ts b/src/app/widget-modules/components/song-text/song-text.component.ts index 6079b3c..de46801 100644 --- a/src/app/widget-modules/components/song-text/song-text.component.ts +++ b/src/app/widget-modules/components/song-text/song-text.component.ts @@ -8,6 +8,7 @@ import { } from '../../../modules/songs/services/text-rendering.service'; import {faGripLines} from '@fortawesome/free-solid-svg-icons/faGripLines'; import {songSwitch} from './animation'; +import {TransposeMode} from '../../../modules/songs/services/transpose.service'; export type ChordMode = 'show' | 'hide' | 'onlyFirst' @@ -22,6 +23,7 @@ export class SongTextComponent implements OnInit { @Input() public index = -1; @Input() public fullscreen = false; @Input() public showSwitch = false; + @Input() public transpose: TransposeMode = null; @Output() public chordModeChanged = new EventEmitter(); @ViewChildren('section') viewSections: QueryList; public faLines = faGripLines; @@ -42,7 +44,7 @@ export class SongTextComponent implements OnInit { this.sections = null; this.offset = 0; setTimeout(() => - this.sections = this.textRenderingService.parse(value).sort((a, b) => a.type - b.type), 100); + this.sections = this.textRenderingService.parse(value, this.transpose).sort((a, b) => a.type - b.type), 100); }