update tslint -> eslint

This commit is contained in:
2021-05-21 20:17:26 +02:00
parent 80260df71f
commit a195fafa6b
252 changed files with 3080 additions and 2420 deletions

View File

@@ -3,10 +3,10 @@ import {TestBed} from '@angular/core/testing';
import {FileDataService} from './file-data.service';
describe('FileDataService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
beforeEach(() => void TestBed.configureTestingModule({}));
it('should be created', () => {
const service: FileDataService = TestBed.get(FileDataService);
expect(service).toBeTruthy();
const service: FileDataService = TestBed.inject(FileDataService);
void expect(service).toBeTruthy();
});
});

View File

@@ -5,12 +5,10 @@ import {FileServer} from './fileServer';
import {DbService} from '../../../services/db.service';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class FileDataService {
constructor(private db: DbService) {
}
public constructor(private db: DbService) {}
public async set(songId: string, file: FileServer): Promise<string> {
const songRef = this.db.doc('songs/' + songId);
@@ -27,8 +25,5 @@ export class FileDataService {
public read$(songId: string): Observable<File[]> {
const songRef = this.db.doc('songs/' + songId);
return songRef.collection<File>('files').valueChanges({idField: 'id'});
}
}

View File

@@ -6,11 +6,11 @@ describe('FileService', () => {
let service: FileService;
beforeEach(() => {
TestBed.configureTestingModule({});
void TestBed.configureTestingModule({});
service = TestBed.inject(FileService);
});
it('should be created', () => {
expect(service).toBeTruthy();
void expect(service).toBeTruthy();
});
});

View File

@@ -4,22 +4,17 @@ import {Observable} from 'rxjs';
import {FileDataService} from './file-data.service';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class FileService {
constructor(
private storage: AngularFireStorage,
private fileDataService: FileDataService
) {
}
public constructor(private storage: AngularFireStorage, private fileDataService: FileDataService) {}
public getDownloadUrl(path: string): Observable<string> {
const ref = this.storage.ref(path);
return ref.getDownloadURL();
return ref.getDownloadURL() as Observable<string>;
}
public async delete(path: string, songId: string, fileId: string) {
public async delete(path: string, songId: string, fileId: string): Promise<void> {
const ref = this.storage.ref(path);
await ref.delete().toPromise();
await this.fileDataService.delete(songId, fileId);

View File

@@ -1,5 +1,4 @@
export class FileBase {
protected basePath = '/attachments';
protected directory = (songId: string) => `${this.basePath}/${songId}`;
protected directory: (songId: string) => string = (songId: string) => `${this.basePath}/${songId}`;
}

View File

@@ -1,133 +1,156 @@
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_MAJOR_FLAT = [
'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H',
];
export const KEYS_MAJOR_B = [
'C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H',
];
export const KEYS_MINOR_FLAT = [
'c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'h',
];
export const KEYS_MINOR_B = [
'c', 'db', 'd', 'eb', 'e', 'f', 'gb', 'g', 'ab', 'a', 'b', 'h',
export const KEYS: string[] = [
'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_MAJOR_FLAT: string[] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H'];
export const KEYS_MAJOR_B: string[] = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H'];
export const KEYS_MINOR_FLAT: string[] = ['c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'h'];
export const KEYS_MINOR_B: string[] = ['c', 'db', 'd', 'eb', 'e', 'f', 'gb', 'g', 'ab', 'a', 'b', 'h'];
export type scale = 'b' | 'flat'
export type scale = 'b' | 'flat';
const scaleTypeAssignment: { [key: string]: string[][] } = {
'C': [KEYS_MAJOR_FLAT, KEYS_MINOR_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],
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],
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],
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],
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],
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],
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],
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],
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],
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,
'Db': KEYS_MAJOR_B,
'D': KEYS_MAJOR_B,
'D#': KEYS_MAJOR_FLAT,
'Eb': KEYS_MAJOR_B,
'E': KEYS_MAJOR_FLAT,
'F': KEYS_MAJOR_B,
'F#': KEYS_MAJOR_FLAT,
'Gb': KEYS_MAJOR_B,
'G': KEYS_MAJOR_FLAT,
'G#': KEYS_MAJOR_FLAT,
'Ab': KEYS_MAJOR_B,
'A': KEYS_MAJOR_FLAT,
'A#': KEYS_MAJOR_FLAT,
'B': KEYS_MAJOR_B,
'H': KEYS_MAJOR_FLAT,
'c': KEYS_MINOR_FLAT,
'c#': KEYS_MINOR_FLAT,
'db': KEYS_MINOR_B,
'd': KEYS_MINOR_B,
'd#': KEYS_MINOR_FLAT,
'eb': KEYS_MINOR_B,
'e': KEYS_MINOR_FLAT,
'f': KEYS_MINOR_B,
'f#': KEYS_MINOR_FLAT,
'gb': KEYS_MINOR_B,
'g': KEYS_MINOR_FLAT,
'g#': KEYS_MINOR_FLAT,
'ab': KEYS_MINOR_B,
'a': KEYS_MINOR_FLAT,
'a#': KEYS_MINOR_FLAT,
'b': KEYS_MINOR_B,
'h': KEYS_MINOR_FLAT,
b: [KEYS_MINOR_B, KEYS_MAJOR_B],
h: [KEYS_MINOR_FLAT, KEYS_MAJOR_FLAT],
};
export const scaleMapping = {
'C': 'C',
const scaleAssignment: {[key: string]: string[]} = {
C: KEYS_MAJOR_FLAT,
'C#': KEYS_MAJOR_FLAT,
Db: KEYS_MAJOR_B,
D: KEYS_MAJOR_B,
'D#': KEYS_MAJOR_FLAT,
Eb: KEYS_MAJOR_B,
E: KEYS_MAJOR_FLAT,
F: KEYS_MAJOR_B,
'F#': KEYS_MAJOR_FLAT,
Gb: KEYS_MAJOR_B,
G: KEYS_MAJOR_FLAT,
'G#': KEYS_MAJOR_FLAT,
Ab: KEYS_MAJOR_B,
A: KEYS_MAJOR_FLAT,
'A#': KEYS_MAJOR_FLAT,
B: KEYS_MAJOR_B,
H: KEYS_MAJOR_FLAT,
c: KEYS_MINOR_FLAT,
'c#': KEYS_MINOR_FLAT,
db: KEYS_MINOR_B,
d: KEYS_MINOR_B,
'd#': KEYS_MINOR_FLAT,
eb: KEYS_MINOR_B,
e: KEYS_MINOR_FLAT,
f: KEYS_MINOR_B,
'f#': KEYS_MINOR_FLAT,
gb: KEYS_MINOR_B,
g: KEYS_MINOR_FLAT,
'g#': KEYS_MINOR_FLAT,
ab: KEYS_MINOR_B,
a: KEYS_MINOR_FLAT,
'a#': KEYS_MINOR_FLAT,
b: KEYS_MINOR_B,
h: KEYS_MINOR_FLAT,
};
export const scaleMapping: {[key: string]: string} = {
C: 'C',
'C#': 'C♯',
'Db': 'D♭',
'D': 'D',
Db: 'D♭',
D: 'D',
'D#': 'D♯',
'Eb': 'E♭',
'E': 'E',
'F': 'F',
Eb: 'E♭',
E: 'E',
F: 'F',
'F#': 'F♯',
'Gb': 'D♭',
'G': 'G',
Gb: 'D♭',
G: 'G',
'G#': 'G♯',
'Ab': 'A♭',
'A': 'A',
Ab: 'A♭',
A: 'A',
'A#': 'A♯',
'B': 'B',
'H': 'H',
'c': 'c',
B: 'B',
H: 'H',
c: 'c',
'c#': 'c♯',
'db': 'd♭',
'd': 'd',
db: 'd♭',
d: 'd',
'd#': 'd♯',
'eb': 'e♭',
'e': 'e',
'f': 'f',
eb: 'e♭',
e: 'e',
f: 'f',
'f#': 'f♯',
'gb': 'g♭',
'g': 'g',
gb: 'g♭',
g: 'g',
'g#': 'g♯',
'ab': 'a♭',
'a': 'a',
ab: 'a♭',
a: 'a',
'a#': 'a♯',
'b': 'b',
'h': 'h',
b: 'b',
h: 'h',
};
export const getScale = (key: string): string[] => scaleAssignment[key];
export const getScaleType = (key: string): string[][] => scaleTypeAssignment[key];

View File

@@ -5,37 +5,35 @@ import {AngularFirestore} from '@angular/fire/firestore';
import {of} from 'rxjs';
describe('SongDataService', () => {
const songs = [
{title: 'title1'}
];
const songs = [{title: 'title1'}];
const angularFirestoreCollection = {
valueChanges: () => of(songs)
valueChanges: () => of(songs),
};
const mockAngularFirestore = {
collection: () => angularFirestoreCollection
collection: () => angularFirestoreCollection,
};
beforeEach(() => TestBed.configureTestingModule({
providers: [
{provide: AngularFirestore, useValue: mockAngularFirestore}
]
}));
beforeEach(
() =>
void TestBed.configureTestingModule({
providers: [{provide: AngularFirestore, useValue: mockAngularFirestore}],
})
);
it('should be created', () => {
const service: SongDataService = TestBed.get(SongDataService);
expect(service).toBeTruthy();
const service: SongDataService = TestBed.inject(SongDataService);
void expect(service).toBeTruthy();
});
it('should list songs', waitForAsync(() => {
const service: SongDataService = TestBed.get(SongDataService);
service.list$().subscribe(s => {
expect(s).toEqual([
{title: 'title1'}
] as any);
}
);
}));
it(
'should list songs',
waitForAsync(() => {
const service: SongDataService = TestBed.inject(SongDataService);
service.list$().subscribe(s => {
void expect(s[0].title).toEqual('title1');
});
})
);
});

View File

@@ -4,19 +4,16 @@ import {Observable} from 'rxjs';
import {DbService} from '../../../services/db.service';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class SongDataService {
private collection = 'songs';
constructor(private dbService: DbService) {
}
public constructor(private dbService: DbService) {}
public list$ = (): Observable<Song[]> => this.dbService.col$(this.collection);
public read$ = (songId: string): Observable<Song | undefined> => this.dbService.doc$(this.collection + '/' + songId);
public update$ = async (songId: string, data: Partial<Song>): Promise<void> => await this.dbService.doc(this.collection + '/' + songId).update(data);
public add = async (data: Partial<Song>): Promise<string> => (await this.dbService.col(this.collection).add(data)).id;
public delete = async (songId: string): Promise<void> => await this.dbService.doc(this.collection + '/' + songId).delete();
}

View File

@@ -5,32 +5,31 @@ import {SongDataService} from './song-data.service';
import {of} from 'rxjs';
describe('SongService', () => {
const songs = [
{title: 'title1'}
];
const songs = [{title: 'title1'}];
const mockSongDataService = {
list: () => of(songs)
list: () => of(songs),
};
beforeEach(() => TestBed.configureTestingModule({
providers: [
{provide: SongDataService, useValue: mockSongDataService}
]
}));
beforeEach(
() =>
void TestBed.configureTestingModule({
providers: [{provide: SongDataService, useValue: mockSongDataService}],
})
);
it('should be created', () => {
const service: SongService = TestBed.get(SongService);
expect(service).toBeTruthy();
const service: SongService = TestBed.inject(SongService);
void expect(service).toBeTruthy();
});
it('should list songs', waitForAsync(() => {
const service: SongService = TestBed.get(SongService);
service.list$().subscribe(s => {
expect(s).toEqual([
{title: 'title1'}
] as any);
});
}));
it(
'should list songs',
waitForAsync(() => {
const service: SongService = TestBed.inject(SongService);
service.list$().subscribe(s => {
void expect(s[0].title).toEqual('title1');
});
})
);
});

View File

@@ -2,31 +2,30 @@ import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {Song} from './song';
import {SongDataService} from './song-data.service';
import {first, tap} from 'rxjs/operators';
import {first} from 'rxjs/operators';
import {UserService} from '../../../services/user/user.service';
import * as firebase from 'firebase';
import Timestamp = firebase.firestore.Timestamp;
declare var importCCLI: any;
// declare let importCCLI: any;
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class SongService {
public static TYPES = ['Praise', 'Worship'];
public static STATUS = ['draft', 'set', 'final'];
public static LEGAL_OWNER = ['CCLI', 'other'];
public static LEGAL_TYPE = ['open', 'allowed'];
private list: Song[];
// private list: Song[];
constructor(private songDataService: SongDataService, private userService: UserService) {
importCCLI = (songs: Song[]) => this.updateFromCLI(songs);
public constructor(private songDataService: SongDataService, private userService: UserService) {
// importCCLI = (songs: Song[]) => this.updateFromCLI(songs);
}
public list$ = (): Observable<Song[]> => this.songDataService.list$().pipe(tap(_ => this.list = _));
public list$ = (): Observable<Song[]> => this.songDataService.list$(); //.pipe(tap(_ => (this.list = _)));
public read$ = (songId: string): Observable<Song | undefined> => this.songDataService.read$(songId);
public read = (songId: string): Promise<Song | undefined> => this.read$(songId).pipe(first()).toPromise();
@@ -38,38 +37,41 @@ export class SongService {
await this.songDataService.update$(songId, {...data, edits});
}
public async new(number: number, title: string): Promise<string> {
return await this.songDataService.add({number, title, status: 'draft', legalType: 'open'});
public async new(songNumber: number, title: string): Promise<string> {
return await this.songDataService.add({
number: songNumber,
title,
status: 'draft',
legalType: 'open',
});
}
public async delete(songId: string): Promise<void> {
await this.songDataService.delete(songId);
}
// https://www.csvjson.com/csv2json
private async updateFromCLI(songs: Song[]) {
const mapped = songs.map(_ => ({
number: _.number,
legalType: _.legalType === 'ja' ? 'allowed' : 'open',
legalOwner: _.legalOwner === 'ja' ? 'CCLI' : 'other',
title: _.title,
legalOwnerId: _.legalOwnerId,
origin: _.origin,
artist: _.artist,
comment: _.comment
}));
const promises = this.list.map(async _ => {
// tslint:disable-next-line:triple-equals
const mappedSongs = mapped.filter(f => f.number == _.number);
if (mappedSongs.length === 1) {
const mappedSong = mappedSongs[0];
const id = _.id;
return await this.update$(id, mappedSong);
}
});
await Promise.all(promises);
}
// https://www.csvjson.com/csv2json
// private async updateFromCLI(songs: Song[]) {
// const mapped = songs.map(_ => ({
// number: _.number,
// legalType: _.legalType === 'ja' ? 'allowed' : 'open',
// legalOwner: _.legalOwner === 'ja' ? 'CCLI' : 'other',
// title: _.title,
// legalOwnerId: _.legalOwnerId,
// origin: _.origin,
// artist: _.artist,
// comment: _.comment,
// }));
// const promises = this.list.map(async _ => {
// // eslint-disable-next-line eqeqeq
// const mappedSongs = mapped.filter(f => f.number == _.number);
// if (mappedSongs.length === 1) {
// const mappedSong = mappedSongs[0];
// const id = _.id;
// return await this.update$(id, mappedSong);
// }
// });
//
// await Promise.all(promises);
// }
}

View File

@@ -1,6 +1,7 @@
import {TestBed} from '@angular/core/testing';
import {LineType, SectionType, TextRenderingService} from './text-rendering.service';
import {TextRenderingService} from './text-rendering.service';
import {LineType} from './line-type';
import {SectionType} from './section-type';
describe('TextRenderingService', () => {
const testText = `Strophe
@@ -23,60 +24,59 @@ Bridge
Cool bridge without any chords
`;
beforeEach(() => TestBed.configureTestingModule({}));
beforeEach(() => void TestBed.configureTestingModule({}));
it('should be created', () => {
const service: TextRenderingService = TestBed.get(TextRenderingService);
expect(service).toBeTruthy();
const service: TextRenderingService = TestBed.inject(TextRenderingService);
void expect(service).toBeTruthy();
});
it('should parse section types', () => {
const service: TextRenderingService = TestBed.get(TextRenderingService);
const service: TextRenderingService = TestBed.inject(TextRenderingService);
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);
expect(sections[1].number).toBe(1);
expect(sections[2].type).toBe(SectionType.Chorus);
expect(sections[2].number).toBe(0);
expect(sections[3].type).toBe(SectionType.Bridge);
expect(sections[3].number).toBe(0);
void expect(sections[0].type).toBe(SectionType.Verse);
void expect(sections[0].number).toBe(0);
void expect(sections[1].type).toBe(SectionType.Verse);
void expect(sections[1].number).toBe(1);
void expect(sections[2].type).toBe(SectionType.Chorus);
void expect(sections[2].number).toBe(0);
void expect(sections[3].type).toBe(SectionType.Bridge);
void expect(sections[3].number).toBe(0);
});
it('should parse text lines', () => {
const service: TextRenderingService = TestBed.get(TextRenderingService);
const service: TextRenderingService = TestBed.inject(TextRenderingService);
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);
expect(sections[0].lines[3].text).toBe('Text Line 2-1');
expect(sections[1].lines[1].type).toBe(LineType.text);
expect(sections[1].lines[1].text).toBe('Text Line 1-2');
expect(sections[1].lines[3].type).toBe(LineType.text);
expect(sections[1].lines[3].text).toBe('Text Line 2-2');
expect(sections[2].lines[1].type).toBe(LineType.text);
expect(sections[2].lines[1].text).toBe('and the chorus');
expect(sections[3].lines[0].type).toBe(LineType.text);
expect(sections[3].lines[0].text).toBe('Cool bridge without any chords');
void expect(sections[0].lines[1].type).toBe(LineType.text);
void expect(sections[0].lines[1].text).toBe('Text Line 1-1');
void expect(sections[0].lines[3].type).toBe(LineType.text);
void expect(sections[0].lines[3].text).toBe('Text Line 2-1');
void expect(sections[1].lines[1].type).toBe(LineType.text);
void expect(sections[1].lines[1].text).toBe('Text Line 1-2');
void expect(sections[1].lines[3].type).toBe(LineType.text);
void expect(sections[1].lines[3].text).toBe('Text Line 2-2');
void expect(sections[2].lines[1].type).toBe(LineType.text);
void expect(sections[2].lines[1].text).toBe('and the chorus');
void expect(sections[3].lines[0].type).toBe(LineType.text);
void expect(sections[3].lines[0].text).toBe('Cool bridge without any chords');
});
it('should parse chord lines', () => {
const service: TextRenderingService = TestBed.inject(TextRenderingService);
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);
expect(sections[0].lines[2].text).toBe(' a d e f g a h c b');
expect(sections[1].lines[0].type).toBe(LineType.chord);
expect(sections[1].lines[0].text).toBe('C D E F G A H');
expect(sections[1].lines[2].type).toBe(LineType.chord);
expect(sections[1].lines[2].text).toBe(' a d e f g a h c b');
expect(sections[2].lines[0].type).toBe(LineType.chord);
expect(sections[2].lines[0].text).toBe('c c# db c7 cmaj7 c/e');
void expect(sections[0].lines[0].type).toBe(LineType.chord);
void expect(sections[0].lines[0].text).toBe('C D E F G A H');
void expect(sections[0].lines[2].type).toBe(LineType.chord);
void expect(sections[0].lines[2].text).toBe(' a d e f g a h c b');
void expect(sections[1].lines[0].type).toBe(LineType.chord);
void expect(sections[1].lines[0].text).toBe('C D E F G A H');
void expect(sections[1].lines[2].type).toBe(LineType.chord);
void expect(sections[1].lines[2].text).toBe(' a d e f g a h c b');
void expect(sections[2].lines[0].type).toBe(LineType.chord);
void expect(sections[2].lines[0].text).toBe('c c# db c7 cmaj7 c/e');
// c c# db c7 cmaj7 c/e
expect(sections[2].lines[0].chords).toEqual([
void expect(sections[2].lines[0].chords).toEqual([
{chord: 'c', length: 1, position: 0},
{chord: 'c#', length: 2, position: 2},
{chord: 'db', length: 2, position: 5},
@@ -92,10 +92,9 @@ Cool bridge without any chords
g# F# E g# F# E
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);
expect(sections[0].lines[1].text).toBe('text');
void expect(sections[0].lines[0].type).toBe(LineType.chord);
void expect(sections[0].lines[0].text).toBe('g# F# E g# F# E');
void expect(sections[0].lines[1].type).toBe(LineType.text);
void expect(sections[0].lines[1].text).toBe('text');
});
});

View File

@@ -8,13 +8,12 @@ import {Chord} from './chord';
import {Line} from './line';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class TextRenderingService {
private regexSection = /(Strophe|Refrain|Bridge)/;
constructor(private transposeService: TransposeService) {
}
public constructor(private transposeService: TransposeService) {}
public parse(text: string, transpose: TransposeMode): Section[] {
if (!text) {
@@ -28,12 +27,15 @@ export class TextRenderingService {
};
return arrayOfLines.reduce((array, line) => {
const type = this.getSectionTypeOfLine(line);
if (line.match(this.regexSection)) {
return [...array, {
type,
number: indices[type]++,
lines: []
}];
if (this.regexSection.exec(line)) {
return [
...array,
{
type,
number: indices[type]++,
lines: [],
},
];
}
array[array.length - 1].lines.push(this.getLineOfLineText(line, transpose));
return array;
@@ -49,36 +51,35 @@ export class TextRenderingService {
const type = hasMatches ? LineType.chord : LineType.text;
const line = {type, text, chords: hasMatches ? cords : undefined};
return transpose
? this.transposeService.transpose(line, transpose.baseKey, transpose.targetKey)
: this.transposeService.renderChords(line);
return transpose ? this.transposeService.transpose(line, transpose.baseKey, transpose.targetKey) : this.transposeService.renderChords(line);
}
private getSectionTypeOfLine(line: string): SectionType {
if (!line) {
return null;
}
const match = line.match(this.regexSection);
const match = this.regexSection.exec(line);
if (!match || match.length < 2) {
return null;
}
const typeString = match[1];
switch (typeString) {
case 'Strophe':
case 'Strophe':
return SectionType.Verse;
case 'Refrain':
case 'Refrain':
return SectionType.Chorus;
case 'Bridge':
case 'Bridge':
return SectionType.Bridge;
}
}
private readChords(chordLine: string): Chord[] {
let match;
let match: string[];
const chords: Chord[] = [];
// https://regex101.com/r/68jMB8/5
const regex = /(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)(\/(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))?(\d+|maj7)?/mg;
const regex =
/(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)(\/(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))?(\d+|maj7)?/gm;
while ((match = regex.exec(chordLine)) !== null) {
const chord: Chord = {
@@ -101,5 +102,4 @@ export class TextRenderingService {
const isChrod = chordCount * 1.2 > lineCount;
return isChrod ? chords : [];
}
}

View File

@@ -6,7 +6,7 @@ describe('TransposeService', () => {
let service: TransposeService;
beforeEach(() => {
TestBed.configureTestingModule({});
void TestBed.configureTestingModule({});
service = TestBed.inject(TransposeService);
});
@@ -15,6 +15,6 @@ describe('TransposeService', () => {
const map = service.getMap('D', distance);
console.log(map);
expect(service).toBeTruthy();
void expect(service).toBeTruthy();
});
});

View File

@@ -4,11 +4,12 @@ import {LineType} from './line-type';
import {Chord} from './chord';
import {Line} from './line';
type TransposeMap = {[key: string]: string};
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class TransposeService {
public transpose(line: Line, baseKey: string, targetKey: string): Line {
if (line.type !== LineType.chord) {
return line;
@@ -33,13 +34,10 @@ export class TransposeService {
public getDistance(baseKey: string, targetKey: string): number {
const scale = getScaleType(baseKey);
return scale ? (
(scale[0].indexOf(targetKey) - scale[0].indexOf(baseKey)) ??
(scale[1].indexOf(targetKey) - scale[1].indexOf(baseKey))
) % 12 : 0;
return scale ? (scale[0].indexOf(targetKey) - scale[0].indexOf(baseKey) ?? scale[1].indexOf(targetKey) - scale[1].indexOf(baseKey)) % 12 : 0;
}
public getMap(baseKey: string, difference: number) {
public getMap(baseKey: string, difference: number): TransposeMap | null {
const scale = getScaleType(baseKey);
if (!scale) {
return null;
@@ -59,10 +57,14 @@ export class TransposeService {
return map;
}
private transposeChord(chord: Chord, map: {}): Chord {
private transposeChord(chord: Chord, map: TransposeMap): Chord {
const translatedChord = map[chord.chord];
const translatedSlashChord = chord.slashChord ? map[chord.slashChord] : null;
return {...chord, chord: translatedChord, slashChord: translatedSlashChord};
return {
...chord,
chord: translatedChord,
slashChord: translatedSlashChord,
};
}
private renderLine(chords: Chord[]): string {
@@ -83,9 +85,6 @@ export class TransposeService {
}
private renderChord(chord: Chord) {
return (
scaleMapping[chord.chord] +
(chord.add ? chord.add : '') +
(chord.slashChord ? '/' + scaleMapping[chord.slashChord] : ''));
return scaleMapping[chord.chord] + (chord.add ? chord.add : '') + (chord.slashChord ? '/' + scaleMapping[chord.slashChord] : '');
}
}

View File

@@ -3,10 +3,10 @@ import {TestBed} from '@angular/core/testing';
import {UploadService} from './upload.service';
describe('UploadServiceService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
beforeEach(() => void TestBed.configureTestingModule({}));
it('should be created', () => {
const service: UploadService = TestBed.get(UploadService);
expect(service).toBeTruthy();
const service: UploadService = TestBed.inject(UploadService);
void expect(service).toBeTruthy();
});
});

View File

@@ -6,17 +6,15 @@ import {finalize} from 'rxjs/operators';
import {FileBase} from './fileBase';
import {FileServer} from './fileServer';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class UploadService extends FileBase {
constructor(private fileDataService: FileDataService, private angularFireStorage: AngularFireStorage) {
public constructor(private fileDataService: FileDataService, private angularFireStorage: AngularFireStorage) {
super();
}
public async pushUpload(songId: string, upload: Upload) {
public pushUpload(songId: string, upload: Upload): void {
const directory = this.directory(songId);
const filePath = `${directory}/${upload.file.name}`;
upload.path = directory;
@@ -24,20 +22,18 @@ export class UploadService extends FileBase {
const ref = this.angularFireStorage.ref(filePath);
const task = ref.put(upload.file);
task.percentageChanges().subscribe(percent => upload.progress = percent);
task.snapshotChanges().pipe(
finalize(() => {
this.saveFileData(songId, upload);
})
).subscribe();
task.percentageChanges().subscribe(percent => (upload.progress = percent));
task
.snapshotChanges()
.pipe(finalize(() => void this.saveFileData(songId, upload)))
.subscribe();
}
private async saveFileData(songId: string, upload: Upload) {
const file: FileServer = {
name: upload.file.name,
path: upload.path,
createdAt: new Date()
createdAt: new Date(),
};
await this.fileDataService.set(songId, file);
}

View File

@@ -1,13 +1,12 @@
export class Upload {
public $key: string;
public file: File;
public name: string;
public path: string;
public progress: number;
public createdAt: Date = new Date();
$key: string;
file: Upload;
name: string;
path: string;
progress: number;
createdAt: Date = new Date();
constructor(file: Upload) {
public constructor(file: File) {
this.file = file;
}
}

View File

@@ -1,8 +1,7 @@
<div [formGroup]="filterFormGroup">
<mat-form-field appearance="outline">
<mat-label>Titel oder Text</mat-label>
<input formControlName="q" matInput>
<input formControlName="q" matInput/>
</mat-form-field>
<div class="third">
@@ -10,16 +9,19 @@
<mat-label>Typ</mat-label>
<mat-select formControlName="type">
<mat-option [value]="null">- kein Filter -</mat-option>
<mat-option *ngFor="let type of types" [value]="type">{{type | songType}}</mat-option>
<mat-option *ngFor="let type of types" [value]="type">{{
type | songType
}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Tonart</mat-label>
<mat-select formControlName="key">
<mat-option [value]="null">- kein Filter -</mat-option>
<mat-option *ngFor="let key of keys" [value]="key">{{key|key}}</mat-option>
<mat-option *ngFor="let key of keys" [value]="key">{{
key | key
}}</mat-option>
</mat-select>
</mat-form-field>
@@ -27,7 +29,9 @@
<mat-label>Rechtlicher Status</mat-label>
<mat-select formControlName="legalType">
<mat-option [value]="null">- kein Filter -</mat-option>
<mat-option *ngFor="let key of legalType" [value]="key">{{key|legalType}}</mat-option>
<mat-option *ngFor="let key of legalType" [value]="key">{{
key | legalType
}}</mat-option>
</mat-select>
</mat-form-field>
@@ -35,10 +39,12 @@
<mat-label>Attribute</mat-label>
<mat-select formControlName="flag">
<mat-option [value]="null">- kein Filter -</mat-option>
<mat-option *ngFor="let flag of getFlags()" [value]="flag">{{flag}}</mat-option>
<mat-option *ngFor="let flag of getFlags()" [value]="flag">{{
flag
}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<i>Anzahl der Suchergebnisse: {{songs.length}}</i>
<i>Anzahl der Suchergebnisse: {{ songs.length }}</i>
</div>

View File

@@ -6,12 +6,13 @@ describe('FilterComponent', () => {
let component: FilterComponent;
let fixture: ComponentFixture<FilterComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [FilterComponent]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [FilterComponent],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(FilterComponent);
@@ -20,6 +21,6 @@ describe('FilterComponent', () => {
});
it('should create', () => {
expect(component).toBeTruthy();
void expect(component).toBeTruthy();
});
});

View File

@@ -1,4 +1,4 @@
import {Component, Input, OnInit} from '@angular/core';
import {Component, Input} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {FormBuilder, FormGroup} from '@angular/forms';
import {SongService} from '../../services/song.service';
@@ -9,18 +9,17 @@ import {KEYS} from '../../services/key.helper';
@Component({
selector: 'app-filter',
templateUrl: './filter.component.html',
styleUrls: ['./filter.component.less']
styleUrls: ['./filter.component.less'],
})
export class FilterComponent implements OnInit {
export class FilterComponent {
public filterFormGroup: FormGroup;
@Input() route: string;
@Input() songs: Song[];
@Input() public route: string;
@Input() public songs: Song[];
public types = SongService.TYPES;
public legalType = SongService.LEGAL_TYPE;
public keys = KEYS;
constructor(private router: Router, activatedRoute: ActivatedRoute, fb: FormBuilder) {
public constructor(private router: Router, activatedRoute: ActivatedRoute, fb: FormBuilder) {
this.filterFormGroup = fb.group({
q: '',
type: '',
@@ -30,32 +29,18 @@ export class FilterComponent implements OnInit {
});
activatedRoute.queryParams.subscribe((filterValues: FilterValues) => {
if (filterValues.q) {
this.filterFormGroup.controls.q.setValue(filterValues.q);
}
if (filterValues.type) {
this.filterFormGroup.controls.type.setValue(filterValues.type);
}
if (filterValues.key) {
this.filterFormGroup.controls.key.setValue(filterValues.key);
}
if (filterValues.legalType) {
this.filterFormGroup.controls.legalType.setValue(filterValues.legalType);
}
if (filterValues.flag) {
this.filterFormGroup.controls.flag.setValue(filterValues.flag);
}
if (filterValues.q) this.filterFormGroup.controls.q.setValue(filterValues.q);
if (filterValues.type) this.filterFormGroup.controls.type.setValue(filterValues.type);
if (filterValues.key) this.filterFormGroup.controls.key.setValue(filterValues.key);
if (filterValues.legalType) this.filterFormGroup.controls.legalType.setValue(filterValues.legalType);
if (filterValues.flag) this.filterFormGroup.controls.flag.setValue(filterValues.flag);
});
this.filterFormGroup.controls.q.valueChanges.subscribe(_ => this.filerValueChanged('q', _));
this.filterFormGroup.controls.key.valueChanges.subscribe(_ => this.filerValueChanged('key', _));
this.filterFormGroup.controls.type.valueChanges.subscribe(_ => this.filerValueChanged('type', _));
this.filterFormGroup.controls.legalType.valueChanges.subscribe(_ => this.filerValueChanged('legalType', _));
this.filterFormGroup.controls.flag.valueChanges.subscribe(_ => this.filerValueChanged('flag', _));
}
ngOnInit(): void {
this.filterFormGroup.controls.q.valueChanges.subscribe(_ => void this.filerValueChanged('q', _));
this.filterFormGroup.controls.key.valueChanges.subscribe(_ => void this.filerValueChanged('key', _));
this.filterFormGroup.controls.type.valueChanges.subscribe(_ => void this.filerValueChanged('type', _));
this.filterFormGroup.controls.legalType.valueChanges.subscribe(_ => void this.filerValueChanged('legalType', _));
this.filterFormGroup.controls.flag.valueChanges.subscribe(_ => void this.filerValueChanged('flag', _));
}
public getFlags(): string[] {
@@ -66,13 +51,14 @@ export class FilterComponent implements OnInit {
.reduce((pn, u) => [...pn, ...u], [])
.filter(_ => !!_);
const uqFlags = flags.filter((n, i) => flags.indexOf(n) === i);
return uqFlags;
return flags.filter((n, i) => flags.indexOf(n) === i);
}
private async filerValueChanged(key: string, value: string): Promise<void> {
const route = this.router.createUrlTree([this.route], {queryParams: {[key]: value}, queryParamsHandling: 'merge'});
const route = this.router.createUrlTree([this.route], {
queryParams: {[key]: value},
queryParamsHandling: 'merge',
});
await this.router.navigateByUrl(route);
}
}

View File

@@ -1,22 +1,40 @@
<div class="list-item">
<div class="number">{{song.number}}</div>
<div>{{song.title}}</div>
<div class="number">{{ song.number }}</div>
<div>{{ song.title }}</div>
<div>
<ng-container *appRole="['contributor']">
<span *ngIf="song.status==='draft' || !song.status" class="warning" matTooltip="Entwurf"
matTooltipPosition="before">
<span
*ngIf="song.status === 'draft' || !song.status"
class="warning"
matTooltip="Entwurf"
matTooltipPosition="before"
>
<fa-icon [icon]="faDraft"></fa-icon> &nbsp;
</span>
<span *ngIf="song.status==='set'" class="neutral" matTooltip="Entwurf" matTooltipPosition="before">
<span
*ngIf="song.status === 'set'"
class="neutral"
matTooltip="Entwurf"
matTooltipPosition="before"
>
<fa-icon [icon]="faDraft"></fa-icon> &nbsp;
</span>
<span *ngIf="song.status==='final'" class="success" matTooltip="Final" matTooltipPosition="before">
<span
*ngIf="song.status === 'final'"
class="success"
matTooltip="Final"
matTooltipPosition="before"
>
<fa-icon [icon]="faFinal"></fa-icon> &nbsp;
</span>
</ng-container>
<span *ngIf="song.legalType==='open'" class="warning" matTooltip="rechtlicher Status ist ungeklärt"
matTooltipPosition="before"><fa-icon [icon]="faLegal"></fa-icon> &nbsp;</span>
<span
*ngIf="song.legalType === 'open'"
class="warning"
matTooltip="rechtlicher Status ist ungeklärt"
matTooltipPosition="before"
><fa-icon [icon]="faLegal"></fa-icon> &nbsp;</span
>
</div>
<div>{{song.key}}</div>
<div>{{ song.key }}</div>
</div>

View File

@@ -6,12 +6,13 @@ describe('ListItemComponent', () => {
let component: ListItemComponent;
let fixture: ComponentFixture<ListItemComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ListItemComponent]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [ListItemComponent],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(ListItemComponent);
@@ -20,6 +21,6 @@ describe('ListItemComponent', () => {
});
it('should create', () => {
expect(component).toBeTruthy();
void expect(component).toBeTruthy();
});
});

View File

@@ -1,4 +1,4 @@
import {Component, Input, OnInit} from '@angular/core';
import {Component, Input} from '@angular/core';
import {Song} from '../../services/song';
import {faBalanceScaleRight} from '@fortawesome/free-solid-svg-icons/faBalanceScaleRight';
import {faPencilRuler} from '@fortawesome/free-solid-svg-icons/faPencilRuler';
@@ -7,18 +7,11 @@ import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck';
@Component({
selector: 'app-list-item',
templateUrl: './list-item.component.html',
styleUrls: ['./list-item.component.less']
styleUrls: ['./list-item.component.less'],
})
export class ListItemComponent implements OnInit {
export class ListItemComponent {
@Input() public song: Song;
public faLegal = faBalanceScaleRight;
public faDraft = faPencilRuler;
public faFinal = faCheck;
constructor() {
}
ngOnInit() {
}
}

View File

@@ -4,6 +4,10 @@
</app-list-header>
<app-card [padding]="false">
<app-list-item *ngFor="let song of songs" [routerLink]="song.id" [song]="song"></app-list-item>
<app-list-item
*ngFor="let song of songs"
[routerLink]="song.id"
[song]="song"
></app-list-item>
</app-card>
</div>

View File

@@ -9,24 +9,21 @@ describe('SongListComponent', () => {
let component: SongListComponent;
let fixture: ComponentFixture<SongListComponent>;
const songs = [
{title: 'title1'}
];
const songs = [{title: 'title1'}];
const mockSongService = {
list: () => of(songs)
list: () => of(songs),
};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SongListComponent],
providers: [
{provide: SongService, useValue: mockSongService}
],
schemas: [NO_ERRORS_SCHEMA]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [SongListComponent],
providers: [{provide: SongService, useValue: mockSongService}],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(SongListComponent);
@@ -35,13 +32,11 @@ describe('SongListComponent', () => {
});
it('should create', () => {
expect(component).toBeTruthy();
void expect(component).toBeTruthy();
});
it('should read songs from SongService', fakeAsync(() => {
tick();
expect(component.songs$).toEqual([
{title: 'title1'}
] as any);
void expect(component.songs$).toEqual([{title: 'title1'}]);
}));
});

View File

@@ -13,28 +13,21 @@ import {ScrollService} from '../../../services/scroll.service';
selector: 'app-songs',
templateUrl: './song-list.component.html',
styleUrls: ['./song-list.component.less'],
animations: [fade]
animations: [fade],
})
export class SongListComponent implements OnInit, OnDestroy {
public songs$: Observable<Song[]>;
public anyFilterActive = false;
constructor(
private songService: SongService,
private activatedRoute: ActivatedRoute,
private scrollService: ScrollService) {
}
public constructor(private songService: SongService, private activatedRoute: ActivatedRoute, private scrollService: ScrollService) {}
ngOnInit() {
public ngOnInit(): void {
const filter$ = this.activatedRoute.queryParams.pipe(
debounceTime(300),
map(_ => _ as FilterValues)
);
const songs$ = this.songService.list$().pipe(
map(songs => songs.sort((a, b) => a.number - b.number)),
);
const songs$ = this.songService.list$().pipe(map(songs => songs.sort((a, b) => a.number - b.number)));
this.songs$ = combineLatest([filter$, songs$]).pipe(
map(_ => {

View File

@@ -17,7 +17,6 @@ import {MatTooltipModule} from '@angular/material/tooltip';
import {RoleModule} from '../../../services/user/role.module';
import {KeyTranslatorModule} from '../../../widget-modules/pipes/key-translator/key-translator.module';
@NgModule({
declarations: [SongListComponent, ListItemComponent, FilterComponent],
exports: [SongListComponent],
@@ -37,7 +36,6 @@ import {KeyTranslatorModule} from '../../../widget-modules/pipes/key-translator/
MatTooltipModule,
RoleModule,
KeyTranslatorModule,
]
],
})
export class SongListModule {
}
export class SongListModule {}

View File

@@ -1,26 +1,29 @@
<app-card heading="Angehängte Dateien">
<div *ngIf="currentUpload">
<div class="progress">
<div [ngStyle]="{ 'width': currentUpload?.progress + '%' }" class="progress-bar progress-bar-animated"></div>
<div
[ngStyle]="{ width: currentUpload?.progress + '%' }"
class="progress-bar progress-bar-animated"
></div>
</div>
Progress: {{currentUpload?.name}} | {{currentUpload?.progress}}% Complete
Progress: {{ currentUpload?.name }} | {{ currentUpload?.progress }}%
Complete
</div>
<div class="upload">
<label>
<input (change)="detectFiles($event)" type="file">
<input (change)="detectFiles($event)" type="file"/>
</label>
<button (click)="uploadSingle()"
[disabled]="!selectedFiles"
mat-icon-button>
<button
(click)="uploadSingle()"
[disabled]="!selectedFiles"
mat-icon-button
>
<mat-icon>cloud_upload</mat-icon>
</button>
</div>
<p *ngFor="let file of (files$|async)">
<p *ngFor="let file of files$ | async">
<app-file [file]="file" [songId]="songId"></app-file>
</p>
</app-card>

View File

@@ -6,12 +6,13 @@ describe('EditFileComponent', () => {
let component: EditFileComponent;
let fixture: ComponentFixture<EditFileComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [EditFileComponent]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [EditFileComponent],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(EditFileComponent);
@@ -20,6 +21,6 @@ describe('EditFileComponent', () => {
});
it('should create', () => {
expect(component).toBeTruthy();
void expect(component).toBeTruthy();
});
});

View File

@@ -10,41 +10,33 @@ import {File} from '../../../services/file';
@Component({
selector: 'app-edit-file',
templateUrl: './edit-file.component.html',
styleUrls: ['./edit-file.component.less']
styleUrls: ['./edit-file.component.less'],
})
export class EditFileComponent {
public selectedFiles: FileList;
public currentUpload: Upload;
public songId: string;
public files$: Observable<File[]>;
constructor(
private activatedRoute: ActivatedRoute,
private uploadService: UploadService,
private fileService: FileDataService,
) {
this.activatedRoute.params.pipe(
map(param => param.songId),
).subscribe(songId => {
public constructor(private activatedRoute: ActivatedRoute, private uploadService: UploadService, private fileService: FileDataService) {
this.activatedRoute.params.pipe(map((param: {songId: string}) => param.songId)).subscribe(songId => {
this.songId = songId;
});
this.files$ = this.activatedRoute.params.pipe(
map(param => param.songId),
map((param: {songId: string}) => param.songId),
switchMap(songId => this.fileService.read$(songId))
);
}
detectFiles(event) {
this.selectedFiles = event.target.files;
public detectFiles(event: Event): void {
const target = event.target as HTMLInputElement;
this.selectedFiles = target.files;
}
public async uploadSingle() {
public uploadSingle(): void {
const file = this.selectedFiles.item(0);
this.currentUpload = new Upload(file as any);
await this.uploadService.pushUpload(this.songId, this.currentUpload);
this.currentUpload = new Upload(file);
this.uploadService.pushUpload(this.songId, this.currentUpload);
}
}

View File

@@ -2,4 +2,4 @@
<fa-icon [icon]="faTrash"></fa-icon>
</button>
<a [href]="url$|async" target="_blank">{{name}}</a>
<a [href]="url$ | async" target="_blank">{{ name }}</a>

View File

@@ -6,12 +6,13 @@ describe('FileComponent', () => {
let component: FileComponent;
let fixture: ComponentFixture<FileComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [FileComponent]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [FileComponent],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(FileComponent);
@@ -20,6 +21,6 @@ describe('FileComponent', () => {
});
it('should create', () => {
expect(component).toBeTruthy();
void expect(component).toBeTruthy();
});
});

View File

@@ -7,20 +7,20 @@ import {FileService} from '../../../../services/file.service';
@Component({
selector: 'app-file',
templateUrl: './file.component.html',
styleUrls: ['./file.component.less']
styleUrls: ['./file.component.less'],
})
export class FileComponent {
public url$: Observable<string>;
public name: string;
public faTrash = faTrashAlt;
@Input() songId: string;
@Input() public songId: string;
private fileId: string;
private path: string;
constructor(private fileService: FileService) {
}
public constructor(private fileService: FileService) {}
@Input() set file(file: File) {
@Input()
public set file(file: File) {
this.url$ = this.fileService.getDownloadUrl(file.path + '/' + file.name);
this.name = file.name;
this.fileId = file.id;

View File

@@ -6,11 +6,11 @@ describe('EditSongGuard', () => {
let guard: EditSongGuard;
beforeEach(() => {
TestBed.configureTestingModule({});
void TestBed.configureTestingModule({});
guard = TestBed.inject(EditSongGuard);
});
it('should be created', () => {
expect(guard).toBeTruthy();
void expect(guard).toBeTruthy();
});
});

View File

@@ -4,16 +4,15 @@ import {Observable} from 'rxjs';
import {EditComponent} from './edit.component';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class EditSongGuard implements CanDeactivate<unknown> {
canDeactivate(
public canDeactivate(
component: EditComponent,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
nextState?: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return component.editSongComponent.askForSave(nextState);
}
}

View File

@@ -1,48 +1,58 @@
<app-card *ngIf="song" [heading]="song.number + ' bearbeiten'" closeLink="../">
<form [formGroup]="form" class="form">
<mat-form-field appearance="outline">
<mat-label>Titel</mat-label>
<input formControlName="title" matInput>
<input formControlName="title" matInput/>
</mat-form-field>
<div class="fourth">
<mat-form-field appearance="outline">
<mat-label>Typ</mat-label>
<mat-select formControlName="type">
<mat-option *ngFor="let type of types" [value]="type">{{type | songType}}</mat-option>
<mat-option *ngFor="let type of types" [value]="type">{{
type | songType
}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Tonart</mat-label>
<mat-select formControlName="key">
<mat-option *ngFor="let key of keys" [value]="key">{{key|key}}</mat-option>
<mat-option *ngFor="let key of keys" [value]="key">{{
key | key
}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Tempo</mat-label>
<input formControlName="tempo" matInput>
<input formControlName="tempo" matInput/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Status</mat-label>
<mat-select formControlName="status">
<mat-option *ngFor="let status of status" [value]="status">{{status | status}}</mat-option>
<mat-option *ngFor="let status of status" [value]="status">{{
status | status
}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-form-field appearance="outline">
<mat-label>Songtext</mat-label>
<textarea (focus)="songtextFocus=true" (focusout)="songtextFocus=false" [mat-autosize]="true"
formControlName="text" matInput></textarea>
<textarea
(focus)="songtextFocus = true"
(focusout)="songtextFocus = false"
[mat-autosize]="true"
formControlName="text"
matInput
></textarea>
</mat-form-field>
<div *ngIf="songtextFocus" class="song-text-help">
<h3>Vorschau</h3>
<app-song-text [text]="form.value.text" chordMode="show"></app-song-text>
<h3>Hinweise zur Bearbeitung</h3>
<h4>Aufbau</h4>
Der Liedtext wird hintereinander weg geschrieben. Dabei werden Strophen, Refrain und Bridge jeweils durch
eine zusätzliche Zeile Markiert. z.B.
Der Liedtext wird hintereinander weg geschrieben. Dabei werden Strophen,
Refrain und Bridge jeweils durch eine zusätzliche Zeile Markiert. z.B.
<pre>
Strophe
Text der ersten Strophe
@@ -54,9 +64,10 @@
Und hier der Refrain
</pre>
<h3>Akkorde</h3>
Die Akktorde werden jeweils in der Zeile über dem jeweiligen Liedtext geschrieben.
Sie werden jeweils durch Leerzeichen an die entsprechende Position gebracht.
Bitte keine Tabulatoren verwenden! Folgende Schreibweisen sind erlaubt:
Die Akktorde werden jeweils in der Zeile über dem jeweiligen Liedtext
geschrieben. Sie werden jeweils durch Leerzeichen an die entsprechende
Position gebracht. Bitte keine Tabulatoren verwenden! Folgende
Schreibweisen sind erlaubt:
<pre>
Dur: C D E
Moll: c d e
@@ -76,21 +87,31 @@
<mat-form-field appearance="outline">
<mat-label>Kommentar</mat-label>
<textarea [mat-autosize]="true" formControlName="comment" matInput></textarea>
<textarea
[mat-autosize]="true"
formControlName="comment"
matInput
></textarea>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-chip-list #chipList>
<mat-chip (removed)="removeFlag(flag)" *ngFor="let flag of flags"
[removable]="true" [selectable]="false">
{{flag}}&nbsp;
<mat-chip
(removed)="removeFlag(flag)"
*ngFor="let flag of flags"
[removable]="true"
[selectable]="false"
>
{{ flag }}&nbsp;
<fa-icon (click)="removeFlag(flag)" [icon]="faRemove"></fa-icon>
</mat-chip>
<input (matChipInputTokenEnd)="addFlag($event)"
[matChipInputAddOnBlur]="true"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
placeholder="Attribute">
<input
(matChipInputTokenEnd)="addFlag($event)"
[matChipInputAddOnBlur]="true"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
placeholder="Attribute"
/>
</mat-chip-list>
</mat-form-field>
@@ -98,54 +119,62 @@
<mat-form-field appearance="outline">
<mat-label>Rechtlicher Status</mat-label>
<mat-select formControlName="legalType">
<mat-option *ngFor="let key of legalType" [value]="key">{{key|legalType}}</mat-option>
<mat-option *ngFor="let key of legalType" [value]="key">{{
key | legalType
}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Rechteinhaber</mat-label>
<mat-select formControlName="legalOwner">
<mat-option *ngFor="let key of legalOwner" [value]="key">{{key|legalOwner}}</mat-option>
<mat-option *ngFor="let key of legalOwner" [value]="key">{{
key | legalOwner
}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Rechteinhaber ID (z.B. CCLI Liednummer)</mat-label>
<input formControlName="legalOwnerId" matInput>
<a *ngIf="form.value.legalOwner==='CCLI'" class="link-ccli"
href="https://songselect.ccli.com/Songs/{{form.value.legalOwnerId}}"
matSuffix
matTooltip="CCLI Link: https://songselect.ccli.com/Songs/{{form.value.legalOwnerId}}"
matTooltipPosition="before" target="_blank">
<input formControlName="legalOwnerId" matInput/>
<a
*ngIf="form.value.legalOwner === 'CCLI'"
class="link-ccli"
href="https://songselect.ccli.com/Songs/{{ form.value.legalOwnerId }}"
matSuffix
matTooltip="CCLI Link: https://songselect.ccli.com/Songs/{{
form.value.legalOwnerId
}}"
matTooltipPosition="before"
target="_blank"
>
<fa-icon [icon]="faLink"></fa-icon>
</a>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Künstler</mat-label>
<input formControlName="artist" matInput>
<input formControlName="artist" matInput/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Verlag</mat-label>
<input formControlName="label" matInput>
<input formControlName="label" matInput/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Nutzungsbedingungen</mat-label>
<input formControlName="termsOfUse" matInput>
<input formControlName="termsOfUse" matInput/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>abweichende Quelle</mat-label>
<input formControlName="origin" matInput>
<input formControlName="origin" matInput/>
</mat-form-field>
</div>
</form>
<app-button-row>
<app-button (click)="onSave()" [icon]="faSave">Speichern</app-button>
</app-button-row>
</app-card>

View File

@@ -6,12 +6,13 @@ describe('EditSongComponent', () => {
let component: EditSongComponent;
let fixture: ComponentFixture<EditSongComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [EditSongComponent]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [EditSongComponent],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(EditSongComponent);
@@ -20,6 +21,6 @@ describe('EditSongComponent', () => {
});
it('should create', () => {
expect(component).toBeTruthy();
void expect(component).toBeTruthy();
});
});

View File

@@ -17,7 +17,7 @@ import {SaveDialogComponent} from './save-dialog/save-dialog.component';
@Component({
selector: 'app-edit-song',
templateUrl: './edit-song.component.html',
styleUrls: ['./edit-song.component.less']
styleUrls: ['./edit-song.component.less'],
})
export class EditSongComponent implements OnInit {
public song: Song;
@@ -28,36 +28,31 @@ export class EditSongComponent implements OnInit {
public legalOwner = SongService.LEGAL_OWNER;
public legalType = SongService.LEGAL_TYPE;
public flags: string[] = [];
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
public readonly separatorKeysCodes: number[] = [ENTER, COMMA];
public faRemove = faTimesCircle;
public faSave = faSave;
public faLink = faExternalLinkAlt;
public songtextFocus = false;
constructor(
private activatedRoute: ActivatedRoute,
private songService: SongService,
private editService: EditService,
private router: Router,
public dialog: MatDialog
) {
}
public constructor(private activatedRoute: ActivatedRoute, private songService: SongService, private editService: EditService, private router: Router, public dialog: MatDialog) {}
public ngOnInit(): void {
this.activatedRoute.params.pipe(
map(param => param.songId),
switchMap(songId => this.songService.read$(songId)),
first()
).subscribe(song => {
this.song = song;
this.form = this.editService.createSongForm(song);
this.form.controls.flags.valueChanges.subscribe(_ => this.onFlagsChanged(_));
this.onFlagsChanged(this.form.controls.flags.value);
});
this.activatedRoute.params
.pipe(
map((param: {songId: string}) => param.songId),
switchMap(songId => this.songService.read$(songId)),
first()
)
.subscribe(song => {
this.song = song;
this.form = this.editService.createSongForm(song);
this.form.controls.flags.valueChanges.subscribe(_ => this.onFlagsChanged(_));
this.onFlagsChanged(this.form.controls.flags.value);
});
}
public async onSave(): Promise<void> {
const data = this.form.value;
const data = this.form.value as Partial<Song>;
await this.songService.update$(this.song.id, data);
this.form.markAsPristine();
await this.router.navigateByUrl('songs/' + this.song.id);
@@ -83,6 +78,22 @@ export class EditSongComponent implements OnInit {
}
}
public askForSave(nextState?: RouterStateSnapshot): boolean {
if (!this.form.dirty) {
return true;
}
const dialogRef = this.dialog.open(SaveDialogComponent, {
width: '350px',
});
dialogRef.afterClosed().subscribe((save: boolean) => {
void this.onSaveDialogAfterClosed(save, nextState.url).then();
});
return false;
}
private onFlagsChanged(flagArray: string): void {
if (!flagArray) {
this.flags = [];
@@ -92,30 +103,13 @@ export class EditSongComponent implements OnInit {
this.flags = flagArray.split(';').filter(_ => !!_);
}
public askForSave(nextState?: RouterStateSnapshot): boolean {
if (!this.form.dirty) {
return true;
}
const dialogRef = this.dialog.open(SaveDialogComponent, {
width: '350px'
});
dialogRef.afterClosed().subscribe((save: boolean) => {
this.onSaveDialogAfterClosed(save, nextState.url).then();
});
return false;
}
private async onSaveDialogAfterClosed(save: boolean, url: string) {
if (save) {
const data = this.form.value;
const data = this.form.value as Partial<Song>;
await this.songService.update$(this.song.id, data);
}
this.form.markAsPristine();
await this.router.navigateByUrl(url);
}
}

View File

@@ -4,5 +4,7 @@
</div>
<div mat-dialog-actions>
<button [mat-dialog-close]="false" mat-button>Änderungen verwerfen</button>
<button [mat-dialog-close]="true" cdkFocusInitial mat-button>Speichern</button>
<button [mat-dialog-close]="true" cdkFocusInitial mat-button>
Speichern
</button>
</div>

View File

@@ -6,12 +6,13 @@ describe('SaveDialogComponent', () => {
let component: SaveDialogComponent;
let fixture: ComponentFixture<SaveDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SaveDialogComponent]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [SaveDialogComponent],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(SaveDialogComponent);
@@ -20,6 +21,6 @@ describe('SaveDialogComponent', () => {
});
it('should create', () => {
expect(component).toBeTruthy();
void expect(component).toBeTruthy();
});
});

View File

@@ -1,16 +1,8 @@
import {Component, OnInit} from '@angular/core';
import {Component} from '@angular/core';
@Component({
selector: 'app-save-dialog',
templateUrl: './save-dialog.component.html',
styleUrls: ['./save-dialog.component.less']
styleUrls: ['./save-dialog.component.less'],
})
export class SaveDialogComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}
export class SaveDialogComponent {}

View File

@@ -6,12 +6,13 @@ describe('EditComponent', () => {
let component: EditComponent;
let fixture: ComponentFixture<EditComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [EditComponent]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [EditComponent],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(EditComponent);
@@ -20,6 +21,6 @@ describe('EditComponent', () => {
});
it('should create', () => {
expect(component).toBeTruthy();
void expect(component).toBeTruthy();
});
});

View File

@@ -4,7 +4,7 @@ import {EditSongComponent} from './edit-song/edit-song.component';
@Component({
selector: 'app-edit',
templateUrl: './edit.component.html',
styleUrls: ['./edit.component.less']
styleUrls: ['./edit.component.less'],
})
export class EditComponent {
@ViewChild(EditSongComponent) public editSongComponent: EditSongComponent;

View File

@@ -27,7 +27,6 @@ import {MatDialogModule} from '@angular/material/dialog';
import {HistoryComponent} from './history/history.component';
import {SongTextModule} from '../../../../widget-modules/components/song-text/song-text.module';
@NgModule({
declarations: [EditComponent, EditSongComponent, EditFileComponent, FileComponent, SaveDialogComponent, HistoryComponent],
exports: [EditComponent],
@@ -56,8 +55,6 @@ import {SongTextModule} from '../../../../widget-modules/components/song-text/so
MatTooltipModule,
MatDialogModule,
SongTextModule,
]
],
})
export class EditModule {
}
export class EditModule {}

View File

@@ -3,10 +3,10 @@ import {TestBed} from '@angular/core/testing';
import {EditService} from './edit.service';
describe('EditService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
beforeEach(() => void TestBed.configureTestingModule({}));
it('should be created', () => {
const service: EditService = TestBed.get(EditService);
expect(service).toBeTruthy();
const service: EditService = TestBed.inject(EditService);
void expect(service).toBeTruthy();
});
});

View File

@@ -3,13 +3,9 @@ import {Song} from '../../services/song';
import {FormControl, FormGroup} from '@angular/forms';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class EditService {
constructor() {
}
public createSongForm(song: Song): FormGroup {
return new FormGroup({
text: new FormControl(song.text),

View File

@@ -1,6 +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>{{ edit.username }}</div>
<div>{{ edit.timestamp.toDate() | date: "dd.MM.yyyy" }}</div>
</div>
</app-card>

View File

@@ -6,12 +6,13 @@ describe('HistoryComponent', () => {
let component: HistoryComponent;
let fixture: ComponentFixture<HistoryComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [HistoryComponent]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [HistoryComponent],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(HistoryComponent);
@@ -20,6 +21,6 @@ describe('HistoryComponent', () => {
});
it('should create', () => {
expect(component).toBeTruthy();
void expect(component).toBeTruthy();
});
});

View File

@@ -7,25 +7,22 @@ import {Song} from '../../../services/song';
@Component({
selector: 'app-history',
templateUrl: './history.component.html',
styleUrls: ['./history.component.less']
styleUrls: ['./history.component.less'],
})
export class HistoryComponent implements OnInit {
public song: Song;
constructor(
private activatedRoute: ActivatedRoute,
private songService: SongService,
) {
}
public 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;
});
this.activatedRoute.params
.pipe(
map((param: {songId: string}) => param.songId),
switchMap(songId => this.songService.read$(songId)),
first()
)
.subscribe(song => {
this.song = song;
});
}
}

View File

@@ -1,3 +1,3 @@
<a [href]="url$|async" target="_blank">
{{name}}
<a [href]="url$ | async" target="_blank">
{{ name }}
</a>

View File

@@ -6,12 +6,13 @@ describe('FileComponent', () => {
let component: FileComponent;
let fixture: ComponentFixture<FileComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [FileComponent]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [FileComponent],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(FileComponent);
@@ -20,6 +21,6 @@ describe('FileComponent', () => {
});
it('should create', () => {
expect(component).toBeTruthy();
void expect(component).toBeTruthy();
});
});

View File

@@ -1,4 +1,4 @@
import {Component, Input, OnInit} from '@angular/core';
import {Component, Input} from '@angular/core';
import {File} from '../../services/file';
import {AngularFireStorage} from '@angular/fire/storage';
import {Observable} from 'rxjs';
@@ -6,24 +6,18 @@ import {Observable} from 'rxjs';
@Component({
selector: 'app-file',
templateUrl: './file.component.html',
styleUrls: ['./file.component.less']
styleUrls: ['./file.component.less'],
})
export class FileComponent implements OnInit {
export class FileComponent {
public url$: Observable<string>;
public name: string;
constructor(private storage: AngularFireStorage) {
}
@Input() set file(file: File) {
public constructor(private storage: AngularFireStorage) {}
@Input()
public set file(file: File) {
const ref = this.storage.ref(file.path + '/' + file.name);
this.url$ = ref.getDownloadURL();
this.url$ = ref.getDownloadURL() as Observable<string>;
this.name = file.name;
}
ngOnInit(): void {
}
}

View File

@@ -1,13 +1,12 @@
<app-card closeLink="../" heading="Neues Lied">
<div [formGroup]="form" class="split">
<mat-form-field appearance="outline">
<mat-label>Nummer</mat-label>
<input formControlName="number" matInput>
<input formControlName="number" matInput/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Titel</mat-label>
<input autofocus formControlName="title" matInput>
<input autofocus formControlName="title" matInput/>
</mat-form-field>
</div>
@@ -15,4 +14,3 @@
<app-button (click)="onSave()" [icon]="faSave">Anlegen</app-button>
</app-button-row>
</app-card>

View File

@@ -6,12 +6,13 @@ describe('NewComponent', () => {
let component: NewComponent;
let fixture: ComponentFixture<NewComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [NewComponent]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [NewComponent],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(NewComponent);
@@ -20,6 +21,6 @@ describe('NewComponent', () => {
});
it('should create', () => {
expect(component).toBeTruthy();
void expect(component).toBeTruthy();
});
});

View File

@@ -10,14 +10,13 @@ import {Router} from '@angular/router';
@Component({
selector: 'app-new',
templateUrl: './new.component.html',
styleUrls: ['./new.component.less']
styleUrls: ['./new.component.less'],
})
export class NewComponent implements OnInit {
public faSave = faSave;
public form: FormGroup;
constructor(private songService: SongService, private router: Router) {
}
public constructor(private songService: SongService, private router: Router) {}
public ngOnInit(): void {
this.form = new FormGroup({
@@ -25,23 +24,27 @@ export class NewComponent implements OnInit {
title: new FormControl(null, Validators.required),
});
this.songService.list$().pipe(autoComplete(this)).subscribe(songs => {
const freeSongnumber = this.getFreeSongNumber(songs);
this.form.controls.number.setValue(freeSongnumber);
});
this.songService
.list$()
.pipe(autoComplete(this))
.subscribe(songs => {
const freeSongnumber = this.getFreeSongNumber(songs);
this.form.controls.number.setValue(freeSongnumber);
});
}
public async onSave(): Promise<void> {
const number = this.form.value.number;
const title = this.form.value.title;
const newSongId = await this.songService.new(number, title);
const value = this.form.value as {number: number; title: string};
const songNumber = value.number;
const title = value.title;
const newSongId = await this.songService.new(songNumber, title);
await this.router.navigateByUrl('/songs/' + newSongId + '/edit');
}
private getFreeSongNumber(songs: Song[]): Number {
const numbers = songs.map(_ => _.number);
private getFreeSongNumber(songs: Song[]): number {
const songNumber = songs.map(_ => _.number);
for (let i = 1; i < Number.MAX_SAFE_INTEGER; i++) {
if (!numbers.some(_ => _ === i)) {
if (!songNumber.some(_ => _ === i)) {
return i;
}
}

View File

@@ -9,19 +9,8 @@ import {ButtonRowModule} from '../../../../widget-modules/components/button-row/
import {ButtonModule} from '../../../../widget-modules/components/button/button.module';
import {AutofocusModule} from '../../../../widget-modules/directives/autofocus/autofocus.module';
@NgModule({
declarations: [NewComponent],
imports: [
CommonModule,
CardModule,
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
ButtonRowModule,
ButtonModule,
AutofocusModule
]
imports: [CommonModule, CardModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, ButtonRowModule, ButtonModule, AutofocusModule],
})
export class NewModule {
}
export class NewModule {}

View File

@@ -1,47 +1,74 @@
<div class="split">
<app-card *ngIf="song$ | async as song" [heading]="song.number + ' - ' + song.title" closeLink="../">
<app-card
*ngIf="song$ | async as song"
[heading]="song.number + ' - ' + song.title"
closeLink="../"
>
<div class="song">
<div>
<div *appRole="['leader', 'contributor']" class="detail">
<div>Typ: {{song.type | songType}}</div>
<div>Tonart: {{song.key}}</div>
<div>Tempo: {{song.tempo}}</div>
<div>Status: {{(song.status|status) || 'entwurf'}}</div>
<div *ngIf="song.legalOwner">Rechteinhaber: {{song.legalOwner|legalOwner}}</div>
<div *ngIf="song.legalOwnerId && song.legalOwner==='CCLI'">
<a href="https://songselect.ccli.com/Songs/{{song.legalOwnerId}}" target="_blank">
CCLI Nummer: {{song.legalOwnerId}}
<div>Typ: {{ song.type | songType }}</div>
<div>Tonart: {{ song.key }}</div>
<div>Tempo: {{ song.tempo }}</div>
<div>Status: {{ (song.status | status) || "entwurf" }}</div>
<div *ngIf="song.legalOwner">
Rechteinhaber: {{ song.legalOwner | legalOwner }}
</div>
<div *ngIf="song.legalOwnerId && song.legalOwner === 'CCLI'">
<a
href="https://songselect.ccli.com/Songs/{{ song.legalOwnerId }}"
target="_blank"
>
CCLI Nummer: {{ song.legalOwnerId }}
</a>
</div>
<div *ngIf="song.legalOwnerId && song.legalOwner!=='CCLI'">Rechteinhaber ID: {{song.legalOwnerId}}</div>
<div *ngIf="song.artist">Künstler: {{song.artist}}</div>
<div *ngIf="song.label">Verlag: {{song.label}}</div>
<div *ngIf="song.origin">Quelle: {{song.origin}}</div>
<div *ngIf="song.origin">Quelle: {{song.origin}}</div>
<div *ngIf="song.legalOwnerId && song.legalOwner !== 'CCLI'">
Rechteinhaber ID: {{ song.legalOwnerId }}
</div>
<div *ngIf="song.artist">Künstler: {{ song.artist }}</div>
<div *ngIf="song.label">Verlag: {{ song.label }}</div>
<div *ngIf="song.origin">Quelle: {{ song.origin }}</div>
<div *ngIf="song.origin">Quelle: {{ song.origin }}</div>
</div>
</div>
<!-- <div class="text">{{song.text}}</div>-->
<app-song-text *ngIf="user$|async as user" [chordMode]="user.chordMode" [showSwitch]="true"
[text]="song.text"></app-song-text>
<app-song-text
*ngIf="user$ | async as user"
[chordMode]="user.chordMode"
[showSwitch]="true"
[text]="song.text"
></app-song-text>
<mat-chip-list *appRole="['leader', 'contributor']" aria-label="Attribute">
<mat-chip *ngFor="let flag of getFlags(song.flags)">{{flag}}</mat-chip>
<mat-chip-list
*appRole="['leader', 'contributor']"
aria-label="Attribute"
>
<mat-chip *ngFor="let flag of getFlags(song.flags)">{{
flag
}}</mat-chip>
</mat-chip-list>
<div *appRole="['leader', 'contributor']" class="text">{{song.comment}}</div>
<div *appRole="['leader', 'contributor']" class="text">
{{ song.comment }}
</div>
</div>
<app-button-row>
<app-button (click)="onDelete(song.id)" *appRole="['admin']" [icon]="faDelete">Löschen</app-button>
<app-button *appRole="['contributor']" [icon]="faEdit" routerLink="edit">Bearbeiten</app-button>
<app-button
(click)="onDelete(song.id)"
*appRole="['admin']"
[icon]="faDelete"
>Löschen
</app-button>
<app-button *appRole="['contributor']" [icon]="faEdit" routerLink="edit"
>Bearbeiten
</app-button>
</app-button-row>
</app-card>
<ng-container *ngIf="(files$|async) as files">
<app-card *ngIf="files.length>0" heading="Anhänge">
<p *ngFor="let file of (files$|async)">
<ng-container *ngIf="files$ | async as files">
<app-card *ngIf="files.length > 0" heading="Anhänge">
<p *ngFor="let file of files$ | async">
<app-file [file]="file"></app-file>
</p>
</app-card>

View File

@@ -9,18 +9,17 @@ describe('SongComponent', () => {
let fixture: ComponentFixture<SongComponent>;
const mockActivatedRoute = {
params: of({songId: '4711'})
params: of({songId: '4711'}),
};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SongComponent],
providers: [
{provide: ActivatedRoute, useValue: mockActivatedRoute}
]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [SongComponent],
providers: [{provide: ActivatedRoute, useValue: mockActivatedRoute}],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(SongComponent);
@@ -29,7 +28,6 @@ describe('SongComponent', () => {
});
it('should create', () => {
expect(component).toBeTruthy();
void expect(component).toBeTruthy();
});
});

View File

@@ -14,7 +14,7 @@ import {faTrash} from '@fortawesome/free-solid-svg-icons/faTrash';
@Component({
selector: 'app-song',
templateUrl: './song.component.html',
styleUrls: ['./song.component.less']
styleUrls: ['./song.component.less'],
})
export class SongComponent implements OnInit {
public song$: Observable<Song>;
@@ -23,24 +23,18 @@ export class SongComponent implements OnInit {
public faEdit = faEdit;
public faDelete = faTrash;
constructor(
private activatedRoute: ActivatedRoute,
private songService: SongService,
private fileService: FileDataService,
private userService: UserService,
private router: Router,
) {
public constructor(private activatedRoute: ActivatedRoute, private songService: SongService, private fileService: FileDataService, private userService: UserService, private router: Router) {
this.user$ = userService.user$;
}
public ngOnInit(): void {
this.song$ = this.activatedRoute.params.pipe(
map(param => param.songId),
map((param: {songId: string}) => param.songId),
switchMap(songId => this.songService.read$(songId))
);
this.files$ = this.activatedRoute.params.pipe(
map(param => param.songId),
map((param: {songId: string}) => param.songId),
switchMap(songId => this.fileService.read$(songId))
);
}
@@ -50,7 +44,7 @@ export class SongComponent implements OnInit {
return [];
}
return flags.split(';').filter(_ => !!_);
}
};
public async onDelete(songId: string): Promise<void> {
await this.songService.delete(songId);

View File

@@ -14,7 +14,6 @@ import {StatusTranslaterModule} from '../../../widget-modules/pipes/status-trans
import {ButtonModule} from '../../../widget-modules/components/button/button.module';
import {FileComponent} from './file/file.component';
@NgModule({
declarations: [SongComponent, FileComponent],
exports: [SongComponent],
@@ -32,7 +31,6 @@ import {FileComponent} from './file/file.component';
RoleModule,
StatusTranslaterModule,
ButtonModule,
]
],
})
export class SongModule {
}
export class SongModule {}

View File

@@ -6,31 +6,29 @@ import {EditComponent} from './song/edit/edit.component';
import {NewComponent} from './song/new/new.component';
import {EditSongGuard} from './song/edit/edit-song.guard';
const routes: Routes = [
{
path: '',
component: SongListComponent,
pathMatch: 'full'
pathMatch: 'full',
},
{
path: 'new',
component: NewComponent
component: NewComponent,
},
{
path: ':songId/edit',
component: EditComponent,
canDeactivate: [EditSongGuard]
canDeactivate: [EditSongGuard],
},
{
path: ':songId',
component: SongComponent
}
component: SongComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
exports: [RouterModule],
})
export class SongsRoutingModule {
}
export class SongsRoutingModule {}

View File

@@ -9,14 +9,6 @@ import {NewModule} from './song/new/new.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
SongsRoutingModule,
SongListModule,
SongModule,
EditModule,
NewModule,
]
imports: [CommonModule, SongsRoutingModule, SongListModule, SongModule, EditModule, NewModule],
})
export class SongsModule {
}
export class SongsModule {}