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,23 +3,20 @@ import {animate, query, state, style, transition, trigger} from '@angular/animat
export const fade = [
// the fade-in/fade-out animation.
trigger('fade', [
// the "in" style determines the "resting" state of the element when it is visible.
state('in', style({opacity: 1, transform: 'translateY(0px)'})),
// fade in when created. this could also be written as transition('void => *')
transition(':enter', [
style({opacity: 0, transform: 'translateY(-10px)'}),
animate(200)
]),
])
transition(':enter', [style({opacity: 0, transform: 'translateY(-10px)'}), animate(200)]),
]),
];
export const fader =
trigger('fader', [
transition('* <=> *', [
// Set a default style for enter and leave
query(':enter, :leave', [
export const fader = trigger('fader', [
transition('* <=> *', [
// Set a default style for enter and leave
query(
':enter, :leave',
[
style({
position: 'absolute',
left: 0,
@@ -27,10 +24,22 @@ export const fader =
opacity: 0,
transform: 'scale(1) translateY(-10px)',
}),
], {optional: true}),
// Animate the new page in
query(':enter', [
animate('200ms ease', style({opacity: 1, transform: 'scale(1) translateY(0)'})),
], {optional: true}),
]),
]);
],
{optional: true}
),
// Animate the new page in
query(
':enter',
[
animate(
'200ms ease',
style({
opacity: 1,
transform: 'scale(1) translateY(0)',
})
),
],
{optional: true}
),
]),
]);

View File

@@ -7,7 +7,7 @@ const routes: Routes = [
{
path: '',
redirectTo: 'songs',
pathMatch: 'full'
pathMatch: 'full',
},
{
path: 'songs',
@@ -15,8 +15,8 @@ const routes: Routes = [
canActivate: [AngularFireAuthGuard, RoleGuard],
data: {
authGuardPipe: () => redirectUnauthorizedTo(['user', 'login']),
requiredRoles: ['user']
}
requiredRoles: ['user'],
},
},
{
path: 'shows',
@@ -24,8 +24,8 @@ const routes: Routes = [
canActivate: [AngularFireAuthGuard, RoleGuard],
data: {
authGuardPipe: () => redirectUnauthorizedTo(['user', 'login']),
requiredRoles: ['leader']
}
requiredRoles: ['leader'],
},
},
{
path: 'presentation',
@@ -33,12 +33,12 @@ const routes: Routes = [
canActivate: [AngularFireAuthGuard, RoleGuard],
data: {
authGuardPipe: () => redirectUnauthorizedTo(['user', 'login']),
requiredRoles: ['presenter']
}
requiredRoles: ['presenter'],
},
},
{
path: 'user',
loadChildren: () => import('./modules/user/user.module').then(m => m.UserModule)
loadChildren: () => import('./modules/user/user.module').then(m => m.UserModule),
},
{
path: 'brand',
@@ -47,12 +47,16 @@ const routes: Routes = [
{
path: 'guest',
loadChildren: () => import('./modules/guest/guest.module').then(m => m.GuestModule),
}
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules, relativeLinkResolution: 'legacy' })],
exports: [RouterModule]
imports: [
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules,
relativeLinkResolution: 'legacy',
}),
],
exports: [RouterModule],
})
export class AppRoutingModule {
}
export class AppRoutingModule {}

View File

@@ -1,6 +1,10 @@
<app-navigation></app-navigation>
<perfect-scrollbar #scrollbar (psScrollY)="onScoll($event)" [perfectScrollbar]
class="scroll" style="max-height: calc(100vh); width: 100%; overflow: hidden;"
<perfect-scrollbar
#scrollbar
(psScrollY)="onScoll($event)"
[perfectScrollbar]
class="scroll"
style="max-height: calc(100vh); width: 100%; overflow: hidden"
>
<div [@fader]="o.isActivated ? o.activatedRoute : ''" class="content">
<router-outlet #o="outlet"></router-outlet>

View File

@@ -3,20 +3,17 @@ import {RouterTestingModule} from '@angular/router/testing';
import {AppComponent} from './app.component';
describe('AppComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
}));
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [AppComponent],
}).compileComponents();
})
);
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
void expect(fixture.debugElement.componentInstance).toBeTruthy();
});
});

View File

@@ -7,30 +7,24 @@ import {PerfectScrollbarComponent} from 'ngx-perfect-scrollbar';
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.less'],
animations: [fader]
animations: [fader],
})
export class AppComponent implements OnInit {
@ViewChild('scrollbar', {static: false}) public scrollbar: PerfectScrollbarComponent;
@ViewChild('scrollbar', {static: false}) scrollbar: PerfectScrollbarComponent;
constructor(private scrollService: ScrollService) {
public constructor(private scrollService: ScrollService) {
scrollService.restoreScrollPosition$.subscribe(pos => {
if (this.scrollbar && pos) {
// this.scrollbar.scrollTo(pos, 0);
this.scrollbar.directiveRef.scrollTo(0, pos, 300);
// debugger;
}
if (this.scrollbar && pos) this.scrollbar.directiveRef.scrollTo(0, pos, 300);
});
}
public static hideLoader = () => document.querySelector('#load-bg').classList.add('hidden');
public static hideLoader: () => void = () => document.querySelector('#load-bg').classList.add('hidden');
public ngOnInit(): void {
setTimeout(() => AppComponent.hideLoader(), 800);
}
onScoll($event: { srcElement: { scrollTop: number } }) {
public onScoll($event: {srcElement: {scrollTop: number}}): void {
this.scrollService.saveScrollPosition($event.srcElement.scrollTop);
}
}

View File

@@ -18,14 +18,14 @@ import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {PerfectScrollbarModule} from 'ngx-perfect-scrollbar';
@NgModule({
declarations: [
AppComponent
],
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production}),
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production,
}),
BrowserAnimationsModule,
ApplicationFrameModule,
@@ -38,13 +38,8 @@ import {PerfectScrollbarModule} from 'ngx-perfect-scrollbar';
AngularFireAuthGuardModule,
FontAwesomeModule,
PerfectScrollbarModule,
],
providers: [
{provide: MAT_DATE_LOCALE, useValue: 'de-DE'},
],
providers: [{provide: MAT_DATE_LOCALE, useValue: 'de-DE'}],
bootstrap: [AppComponent],
})
export class AppModule {
}
export class AppModule {}

View File

@@ -6,12 +6,13 @@ describe('BrandComponent', () => {
let component: BrandComponent;
let fixture: ComponentFixture<BrandComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [BrandComponent]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [BrandComponent],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(BrandComponent);
@@ -20,6 +21,6 @@ describe('BrandComponent', () => {
});
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-brand',
templateUrl: './brand.component.html',
styleUrls: ['./brand.component.less']
styleUrls: ['./brand.component.less'],
})
export class BrandComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}
export class BrandComponent {}

View File

@@ -5,26 +5,20 @@ import {RouterModule, Routes} from '@angular/router';
import {LogoModule} from '../../widget-modules/components/logo/logo.module';
import {NewUserComponent} from './new-user/new-user.component';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: BrandComponent
component: BrandComponent,
},
{
path: 'new-user',
component: NewUserComponent
component: NewUserComponent,
},
];
@NgModule({
declarations: [BrandComponent, NewUserComponent],
imports: [
CommonModule,
RouterModule.forChild(routes),
LogoModule
]
imports: [CommonModule, RouterModule.forChild(routes), LogoModule],
})
export class BrandModule {
}
export class BrandModule {}

View File

@@ -1,8 +1,11 @@
<div class="frame">
<app-brand class="brand"></app-brand>
<div *ngIf="user$|async as user" class="text">
<div *ngIf="user$ | async as user" class="text">
<div class="welcome">WILLKOMMEN</div>
<div class="name">{{user.name}}</div>
<div class="roles">Es wurden noch keine Berechtigungen zugeteilt, bitte wende Dich an den Administrator!</div>
<div class="name">{{ user.name }}</div>
<div class="roles">
Es wurden noch keine Berechtigungen zugeteilt, bitte wende Dich an den
Administrator!
</div>
</div>
</div>

View File

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

View File

@@ -6,12 +6,12 @@ import {User} from '../../../services/user/user';
@Component({
selector: 'app-new-user',
templateUrl: './new-user.component.html',
styleUrls: ['./new-user.component.less']
styleUrls: ['./new-user.component.less'],
})
export class NewUserComponent {
public user$: Observable<User>;
constructor(private userService: UserService) {
public constructor(private userService: UserService) {
this.user$ = userService.user$;
}
}

View File

@@ -1,8 +1,11 @@
<div *ngIf="songs$|async as songs" class="view">
<div *ngIf="songs$ | async as songs" class="view">
<swiper>
<div *ngFor="let song of songs" class="song">
<app-song-text [showSwitch]="false" [text]="song|async"
chordMode="hide"></app-song-text>
<app-song-text
[showSwitch]="false"
[text]="song | async"
chordMode="hide"
></app-song-text>
</div>
</swiper>
</div>

View File

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

View File

@@ -8,28 +8,18 @@ import {ShowSongService} from '../shows/services/show-song.service';
@Component({
selector: 'app-guest',
templateUrl: './guest.component.html',
styleUrls: ['./guest.component.less']
styleUrls: ['./guest.component.less'],
})
export class GuestComponent implements OnInit {
public songs$: Observable<Observable<string>[]>;
constructor(
private songService: SongService,
private globalSettingsService: GlobalSettingsService,
private showSongService: ShowSongService,
) {
}
public constructor(private songService: SongService, private globalSettingsService: GlobalSettingsService, private showSongService: ShowSongService) {}
public ngOnInit(): void {
this.songs$ = this.globalSettingsService.get$.pipe(
map(_ => _.currentShow),
switchMap(_ => this.showSongService.list$(_)),
map(_ => _
.sort((x, y) => x.order - y.order)
.map(showSong => this.songService.read$(showSong.songId).pipe(map(song => song.text)))
)
map(_ => _.sort((x, y) => x.order - y.order).map(showSong => this.songService.read$(showSong.songId).pipe(map(song => song.text))))
);
}
}

View File

@@ -13,23 +13,16 @@ const DEFAULT_SWIPER_CONFIG: SwiperConfigInterface = {
scrollbar: false,
navigation: true,
pagination: false,
};
@NgModule({
declarations: [GuestComponent],
imports: [
CommonModule,
RouterModule.forChild([{path: '', component: GuestComponent}]),
SwiperModule,
SongTextModule
],
imports: [CommonModule, RouterModule.forChild([{path: '', component: GuestComponent}]), SwiperModule, SongTextModule],
providers: [
{
provide: SWIPER_CONFIG,
useValue: DEFAULT_SWIPER_CONFIG
}
]
useValue: DEFAULT_SWIPER_CONFIG,
},
],
})
export class GuestModule {
}
export class GuestModule {}

View File

@@ -1,10 +1,12 @@
<p *ngIf="song.artist">{{song.artist}}</p>
<p *ngIf="song.label">{{song.label}}</p>
<p *ngIf="song.termsOfUse" class="terms-of-use">{{song.termsOfUse}}</p>
<p *ngIf="song.origin">{{song.origin}}</p>
<p *ngIf="song.artist">{{ song.artist }}</p>
<p *ngIf="song.label">{{ song.label }}</p>
<p *ngIf="song.termsOfUse" class="terms-of-use">{{ song.termsOfUse }}</p>
<p *ngIf="song.origin">{{ song.origin }}</p>
<div *ngIf="song.legalOwnerId">
<p *ngIf="song.legalOwner==='CCLI' && config">CCLI-Liednummer {{song.legalOwnerId}},
CCLI-Lizenznummer {{config.ccliLicenseId}}</p>
<p *ngIf="song.legalOwner!=='CCLI'">Liednummer {{song.legalOwnerId}}</p>
<p *ngIf="song.legalOwner === 'CCLI' && config">
CCLI-Liednummer {{ song.legalOwnerId }}, CCLI-Lizenznummer
{{ config.ccliLicenseId }}
</p>
<p *ngIf="song.legalOwner !== 'CCLI'">Liednummer {{ song.legalOwnerId }}</p>
</div>

View File

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

View File

@@ -5,7 +5,7 @@ import {Config} from '../../../../services/config';
@Component({
selector: 'app-legal',
templateUrl: './legal.component.html',
styleUrls: ['./legal.component.less']
styleUrls: ['./legal.component.less'],
})
export class LegalComponent {
@Input() public song: Song;

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -6,12 +6,13 @@ describe('LogoComponent', () => {
let component: LogoComponent;
let fixture: ComponentFixture<LogoComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [LogoComponent]
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [LogoComponent],
}).compileComponents();
})
.compileComponents();
}));
);
beforeEach(() => {
fixture = TestBed.createComponent(LogoComponent);
@@ -20,6 +21,6 @@ describe('LogoComponent', () => {
});
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-logo',
templateUrl: './logo.component.html',
styleUrls: ['./logo.component.less']
styleUrls: ['./logo.component.less'],
})
export class LogoComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}
export class LogoComponent {}

View File

@@ -1,20 +1,31 @@
<div *ngIf="song" [style.font-size.px]="zoom" class="fullscreen background">
<div @songSwitch [class.blur]="songId==='title'" [class.hide]="songId!=='title' && songId!=='empty'"
class="start fullscreen logo">
<div
@songSwitch
[class.blur]="songId === 'title'"
[class.hide]="songId !== 'title' && songId !== 'empty'"
class="start fullscreen logo"
>
<app-logo></app-logo>
</div>
<div *ngIf="songId==='title'" @songSwitch class="start fullscreen">
<div>{{showType|showType}}</div>
<div class="date">{{date|date:'dd.MM.yyyy'}}</div>
<div *ngIf="songId === 'title'" @songSwitch class="start fullscreen">
<div>{{ showType | showType }}</div>
<div class="date">{{ date | date: "dd.MM.yyyy" }}</div>
</div>
<app-song-text *ngIf="songId!=='title' && songId!=='empty'" @songSwitch [fullscreen]="true" [index]="index"
[showSwitch]="false" [text]="song.text"
chordMode="hide"></app-song-text>
<app-legal *ngIf="songId!=='title' && songId!=='empty'" @songSwitch [config]="config$|async"
[song]="song"></app-legal>
<app-song-text
*ngIf="songId !== 'title' && songId !== 'empty'"
@songSwitch
[fullscreen]="true"
[index]="index"
[showSwitch]="false"
[text]="song.text"
chordMode="hide"
></app-song-text>
<app-legal
*ngIf="songId !== 'title' && songId !== 'empty'"
@songSwitch
[config]="config$ | async"
[song]="song"
></app-legal>
</div>

View File

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

View File

@@ -8,14 +8,14 @@ import {Config} from '../../../services/config';
import {Observable} from 'rxjs';
import {ConfigService} from '../../../services/config.service';
import {songSwitch} from '../../../widget-modules/components/song-text/animation';
import {Section} from '../../songs/services/section';
import {TextRenderingService} from '../../songs/services/text-rendering.service';
import {Show} from '../../shows/services/show';
@Component({
selector: 'app-monitor',
templateUrl: './monitor.component.html',
styleUrls: ['./monitor.component.less'],
animations: [songSwitch]
animations: [songSwitch],
})
export class MonitorComponent implements OnInit {
public song: Song;
@@ -25,35 +25,37 @@ export class MonitorComponent implements OnInit {
public index: number;
public showType: string;
public date: Date;
private sections: Section[];
public config$: Observable<Config>;
constructor(
// private sections: Section[];
public constructor(
private showService: ShowService,
private songService: SongService,
private textRenderingService: TextRenderingService,
private globalSettingsService: GlobalSettingsService,
private configService: ConfigService,
private configService: ConfigService
) {
this.config$ = configService.get$;
}
ngOnInit(): void {
this.globalSettingsService.get$.pipe(
map(_ => _.currentShow),
distinctUntilChanged(),
tap(_ => this.currentShowId = _),
switchMap(_ => this.showService.read$(_)),
tap(_ => this.showType = _.showType),
tap(_ => this.date = _.date.toDate()),
tap(_ => this.songId = _.presentationSongId),
tap(_ => this.index = _.presentationSection),
tap(_ => this.zoom = _.presentationZoom ?? 30),
switchMap(_ => this.songService.read$(_.presentationSongId))
).subscribe((_: Song) => {
this.song = _;
this.sections = this.textRenderingService.parse(_.text, null);
});
public ngOnInit(): void {
this.globalSettingsService.get$
.pipe(
map(_ => _.currentShow),
distinctUntilChanged(),
tap(_ => (this.currentShowId = _)),
switchMap(_ => this.showService.read$(_)),
tap(_ => (this.showType = _.showType)),
tap(_ => (this.date = _.date.toDate())),
tap(_ => (this.songId = _.presentationSongId)),
tap(_ => (this.index = _.presentationSection)),
tap(_ => (this.zoom = _.presentationZoom ?? 30)),
switchMap((_: Show) => this.songService.read$(_.presentationSongId))
)
.subscribe((_: Song) => {
this.song = _;
// this.sections = this.textRenderingService.parse(_.text, null);
});
}
}

View File

@@ -3,26 +3,24 @@ import {RouterModule, Routes} from '@angular/router';
import {RemoteComponent} from './remote/remote.component';
import {MonitorComponent} from './monitor/monitor.component';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
redirectTo: 'remote'
redirectTo: 'remote',
},
{
path: 'remote',
component: RemoteComponent
component: RemoteComponent,
},
{
path: 'monitor',
component: MonitorComponent
}
component: MonitorComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
exports: [RouterModule],
})
export class PresentationRoutingModule {
}
export class PresentationRoutingModule {}

View File

@@ -18,12 +18,9 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {AddSongModule} from '../../widget-modules/components/add-song/add-song.module';
import {LogoComponent} from './monitor/logo/logo.component';
@NgModule({
declarations: [MonitorComponent, RemoteComponent, LegalComponent, LogoComponent],
exports: [
RemoteComponent
],
exports: [RemoteComponent],
imports: [
CommonModule,
PresentationRoutingModule,
@@ -38,8 +35,7 @@ import {LogoComponent} from './monitor/logo/logo.component';
MatSliderModule,
FormsModule,
AddSongModule,
ReactiveFormsModule
]
ReactiveFormsModule,
],
})
export class PresentationModule {
}
export class PresentationModule {}

View File

@@ -1,41 +1,60 @@
<div *ngIf="shows$|async as shows">
<div *ngIf="shows$ | async as shows">
<app-card>
<p *ngIf="!shows.length" @fade>
Es ist derzeit keine Veranstaltung vorhanden
</p>
<p *ngIf="!shows.length" @fade>Es ist derzeit keine Veranstaltung vorhanden</p>
<mat-form-field *ngIf="shows.length>0" @fade appearance="outline">
<mat-form-field *ngIf="shows.length > 0" @fade appearance="outline">
<mat-label>Veranstaltung</mat-label>
<mat-select [formControl]="showControl">
<mat-option *ngFor="let show of shows" [value]="show.id">
{{show.showType|showType}}, {{show.date.toDate()|date:'dd.MM.yyyy'}}
{{ show.showType | showType }},
{{ show.date.toDate() | date: "dd.MM.yyyy" }}
</mat-option>
</mat-select>
</mat-form-field>
<ng-container *ngIf="!progress">
<div *ngIf="show" class="song-parts padding-bottom">
<div (click)="onSectionClick('title', -1)"
[class.active]="show.presentationSongId==='title'"
class="song-part">
<div
(click)="onSectionClick('title', -1)"
[class.active]="show.presentationSongId === 'title'"
class="song-part"
>
<div class="head">Veranstaltung</div>
</div>
<div (click)="onSectionClick('empty', -1)"
[class.active]="show.presentationSongId==='empty'"
class="song-part">
<div
(click)="onSectionClick('empty', -1)"
[class.active]="show.presentationSongId === 'empty'"
class="song-part"
>
<div class="head">Leer</div>
</div>
</div>
<div *ngFor="let song of presentationSongs" @fade class="song">
<div [class.active]="show.presentationSongId===song.id" class="title song-part">
<div (click)="onSectionClick(song.id, -1)" class="head">{{song.title}}</div>
<div
[class.active]="show.presentationSongId === song.id"
class="title song-part"
>
<div (click)="onSectionClick(song.id, -1)" class="head">
{{ song.title }}
</div>
</div>
<div *ngIf="show" class="song-parts">
<div (click)="onSectionClick(song.id, i)" *ngFor="let section of song.sections; index as i"
[class.active]="show.presentationSongId===song.id && show.presentationSection===i"
class="song-part">
<div class="head">{{section.type|sectionType}} {{section.number + 1}}</div>
<div class="fragment">{{getFirstLine(section)}}</div>
<div
(click)="onSectionClick(song.id, i)"
*ngFor="let section of song.sections; index as i"
[class.active]="
show.presentationSongId === song.id &&
show.presentationSection === i
"
class="song-part"
>
<div class="head">
{{ section.type | sectionType }} {{ section.number + 1 }}
</div>
<div class="fragment">{{ getFirstLine(section) }}</div>
</div>
</div>
</div>
@@ -56,11 +75,13 @@
</mat-slider>
</div>
<app-add-song *ngIf="show" [addedLive]="true" [showId]="currentShowId" [showSongs]="showSongs"
[songs]="songs"></app-add-song>
<app-add-song
*ngIf="show"
[addedLive]="true"
[showId]="currentShowId"
[showSongs]="showSongs"
[songs]="songs"
></app-add-song>
</ng-container>
</app-card>
</div>

View File

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

View File

@@ -25,7 +25,7 @@ export interface PresentationSong {
selector: 'app-remote',
templateUrl: './remote.component.html',
styleUrls: ['./remote.component.less'],
animations: [fade]
animations: [fade],
})
export class RemoteComponent {
public shows$: Observable<Show[]>;
@@ -39,24 +39,26 @@ export class RemoteComponent {
public faDesktop = faDesktop;
public showControl = new FormControl();
constructor(
public constructor(
private showService: ShowService,
private showSongService: ShowSongService,
private songService: SongService,
private textRenderingService: TextRenderingService,
private globalSettingsService: GlobalSettingsService,
private globalSettingsService: GlobalSettingsService
) {
this.shows$ = showService.list$(true);
songService.list$().subscribe(_ => this.songs = _);
songService.list$().subscribe(_ => (this.songs = _));
globalSettingsService.get$.pipe(
map(_ => _.currentShow),
distinctUntilChanged()
).subscribe(_ => {
this.showControl.setValue(_, {emitEvent: false});
this.onShowChanged(_, false);
});
this.showControl.valueChanges.subscribe(value => this.onShowChanged(value));
globalSettingsService.get$
.pipe(
map(_ => _.currentShow),
distinctUntilChanged()
)
.subscribe(_ => {
this.showControl.setValue(_, {emitEvent: false});
void this.onShowChanged(_, false);
});
this.showControl.valueChanges.subscribe((value: string) => void this.onShowChanged(value));
}
public async onShowChanged(change: string, updateShow = true): Promise<void> {
@@ -68,16 +70,14 @@ export class RemoteComponent {
await this.showService.update$(change, {presentationSongId: 'title'});
}
this.currentShowId = change;
this.showService.read$(change).subscribe(_ => this.show = _);
this.showService.read$(change).subscribe(_ => (this.show = _));
this.showSongService.list$(change).subscribe(_ => {
this.showSongs = _;
this.presentationSongs = _
.map(song => this.songs.filter(f => f.id == song.songId)[0])
.map(song => ({
id: song.id,
title: song.title,
sections: this.textRenderingService.parse(song.text, null)
}));
this.presentationSongs = _.map(song => this.songs.filter(f => f.id === song.songId)[0]).map(song => ({
id: song.id,
title: song.title,
sections: this.textRenderingService.parse(song.text, null),
}));
});
await delay(500);
this.progress = false;
@@ -90,13 +90,11 @@ export class RemoteComponent {
public async onSectionClick(id: string, index: number): Promise<void> {
await this.showService.update$(this.currentShowId, {
presentationSongId: id,
presentationSection: index
presentationSection: index,
});
}
public async onZoom(zoom: number) {
await this.showService.update$(this.currentShowId, {
presentationZoom: zoom,
});
public async onZoom(zoom: number): Promise<void> {
await this.showService.update$(this.currentShowId, {presentationZoom: zoom});
}
}

View File

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

View File

@@ -1,10 +1,6 @@
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class PresentationService {
constructor() {
}
}
export class PresentationService {}

View File

@@ -1,4 +1,4 @@
<div class="list-item">
<div>{{show.date.toDate()|date:'dd.MM.yyyy'}}</div>
<div>{{show.showType|showType}}</div>
<div>{{ show.date.toDate() | date: "dd.MM.yyyy" }}</div>
<div>{{ show.showType | showType }}</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

@@ -4,7 +4,7 @@ import {Show} from '../../services/show';
@Component({
selector: 'app-list-item',
templateUrl: './list-item.component.html',
styleUrls: ['./list-item.component.less']
styleUrls: ['./list-item.component.less'],
})
export class ListItemComponent {
@Input() public show: Show;

View File

@@ -2,15 +2,32 @@
<app-list-header></app-list-header>
<ng-container *ngIf="shows$ | async as shows">
<app-card *ngIf="getPrivateSongs(shows).length>0" [@fade] [padding]="false" heading="meine Veranstaltungen">
<app-list-item *ngFor="let show of getPrivateSongs(shows)" [routerLink]="show.id" [show]="show"></app-list-item>
<app-card
*ngIf="getPrivateSongs(shows).length > 0"
[@fade]
[padding]="false"
heading="meine Veranstaltungen"
>
<app-list-item
*ngFor="let show of getPrivateSongs(shows)"
[routerLink]="show.id"
[show]="show"
></app-list-item>
</app-card>
</ng-container>
<ng-container *ngIf="shows$ | async as shows">
<app-card *ngIf="getPublicShows(shows).length>0" [@fade] [padding]="false"
heading="veröffentlichte Veranstaltungen">
<app-list-item *ngFor="let show of getPublicShows(shows)" [routerLink]="show.id" [show]="show"></app-list-item>
<app-card
*ngIf="getPublicShows(shows).length > 0"
[@fade]
[padding]="false"
heading="veröffentlichte Veranstaltungen"
>
<app-list-item
*ngFor="let show of getPublicShows(shows)"
[routerLink]="show.id"
[show]="show"
></app-list-item>
</app-card>
</ng-container>
</div>

View File

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

View File

@@ -8,12 +8,12 @@ import {ShowService} from '../services/show.service';
selector: 'app-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.less'],
animations: [fade]
animations: [fade],
})
export class ListComponent {
public shows$: Observable<Show[]>;
constructor(showService: ShowService) {
public constructor(showService: ShowService) {
this.shows$ = showService.list$();
}
@@ -24,5 +24,4 @@ export class ListComponent {
public getPrivateSongs(songs: Show[]): Show[] {
return songs.filter(_ => !_.published);
}
}

View File

@@ -1,21 +1,24 @@
<div>
<app-card closeLink="/shows" heading="Neue Veranstaltung">
<div [formGroup]="form" class="split">
<mat-form-field appearance="outline">
<mat-label>Art der Veranstaltung</mat-label>
<mat-select formControlName="showType">
<mat-optgroup label="öffentlich">
<mat-option *ngFor="let key of showTypePublic" [value]="key">{{key|showType}}</mat-option>
<mat-option *ngFor="let key of showTypePublic" [value]="key">{{
key | showType
}}</mat-option>
</mat-optgroup>
<mat-optgroup label="privat">
<mat-option *ngFor="let key of showTypePrivate" [value]="key">{{key|showType}}</mat-option>
<mat-option *ngFor="let key of showTypePrivate" [value]="key">{{
key | showType
}}</mat-option>
</mat-optgroup>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Datum</mat-label>
<input [matDatepicker]="picker" formControlName="date" matInput>
<input [matDatepicker]="picker" formControlName="date" matInput/>
<mat-datepicker-toggle [for]="picker" matSuffix></mat-datepicker-toggle>
<mat-datepicker #picker touchUi></mat-datepicker>
</mat-form-field>

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,7 +10,7 @@ import {faSave} from '@fortawesome/free-solid-svg-icons/faSave';
@Component({
selector: 'app-new',
templateUrl: './new.component.html',
styleUrls: ['./new.component.less']
styleUrls: ['./new.component.less'],
})
export class NewComponent implements OnInit {
public shows$: Observable<Show[]>;
@@ -19,7 +19,7 @@ export class NewComponent implements OnInit {
public form: FormGroup;
public faSave = faSave;
constructor(private showService: ShowService, showDataService: ShowDataService, private router: Router) {
public constructor(private showService: ShowService, showDataService: ShowDataService, private router: Router) {
this.shows$ = showDataService.list$();
}
@@ -30,7 +30,7 @@ export class NewComponent implements OnInit {
});
}
public async onSave() {
public async onSave(): Promise<void> {
this.form.markAllAsTouched();
if (!this.form.valid) {
return;

View File

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

View File

@@ -24,29 +24,24 @@ export interface DownloadOptions {
}
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class DocxService {
constructor(
public constructor(
private showService: ShowService,
private showSongService: ShowSongService,
private songService: SongService,
private textRenderingService: TextRenderingService,
private userService: UserService,
private configService: ConfigService,
) {
}
private configService: ConfigService
) {}
public async create(showId: string, options: DownloadOptions = {}): Promise<any> {
public async create(showId: string, options: DownloadOptions = {}): Promise<void> {
const {show, songs, user, config} = await this.prepareData(showId);
const type = new ShowTypePipe().transform(show.showType);
const title = `${type} ${show.date.toDate().toLocaleDateString()}`;
const paragraphs = [
...this.renderTitle(title),
...this.renderSongs(songs, options, config),
];
const paragraphs = [...this.renderTitle(title), ...this.renderSongs(songs, options, config)];
const sections: ISectionOptions[] = [
{
@@ -56,11 +51,10 @@ export class DocxService {
},
},
children: paragraphs,
}
]
},
];
const document = this.prepareNewDocument(type, user.name, options, sections);
const blob = await Packer.toBlob(document);
// saveAs from FileSaver will download the file
@@ -72,7 +66,7 @@ export class DocxService {
creator: name,
title: type,
description: '... mit Beschreibung',
sections: sections,
sections,
styles: {
paragraphStyles: [
{
@@ -83,7 +77,8 @@ export class DocxService {
quickFormat: true,
run: options?.chordMode === 'hide' ? {} : {font: 'courier new'},
paragraph: {indent: {left: 0}},
}, {
},
{
id: 'licence',
name: 'Lizenz',
basedOn: 'Normal',
@@ -92,13 +87,13 @@ export class DocxService {
run: {size: 15, color: 'grey'},
paragraph: {indent: {left: 0}},
},
]
}
],
},
});
}
private renderSongs(songs: { showSong: ShowSong; song: Song; sections: Section[] }[], options: DownloadOptions, config: Config): Paragraph[] {
return songs.reduce((p, song) => [...p, ...this.renderSong(song.showSong, song.song, song.sections, options, config)], []);
private renderSongs(songs: {showSong: ShowSong; song: Song; sections: Section[]}[], options: DownloadOptions, config: Config): Paragraph[] {
return songs.reduce((p: Paragraph[], song) => [...p, ...this.renderSong(song.showSong, song.song, song.sections, options, config)], []);
}
private renderSong(showSong: ShowSong, song: Song, sections: Section[], options: DownloadOptions, config: Config): Paragraph[] {
@@ -106,15 +101,11 @@ export class DocxService {
const copyright = this.renderCopyright(song, options, config);
const songText = this.renderSongText(sections, options?.chordMode ?? showSong.chordMode);
return [
songTitle,
copyright,
...songText
].filter(_ => _);
return [songTitle, copyright, ...songText].filter(_ => _);
}
private renderSongText(sections: Section[], chordMode: ChordMode) {
return sections.reduce((p, section) => [...p, ...this.renderSection(section, chordMode)], []);
private renderSongText(sections: Section[], chordMode: ChordMode): Paragraph[] {
return sections.reduce((p: Paragraph[], section) => [...p, ...this.renderSection(section, chordMode)], []);
}
private renderSongTitle(song: Song): Paragraph {
@@ -137,9 +128,7 @@ export class DocxService {
const artist = song.artist ? song.artist + ', ' : '';
const termsOfUse = song.termsOfUse ? song.termsOfUse + ', ' : '';
const origin = song.origin ? song.origin + ', ' : '';
const licence = song.legalOwner === 'CCLI'
? 'CCLI-Liednummer: ' + song.legalOwnerId + ', CCLI-Lizenz: ' + config.ccliLicenseId
: 'CCLI-Liednummer: ' + song.legalOwnerId;
const licence = song.legalOwner === 'CCLI' ? 'CCLI-Liednummer: ' + song.legalOwnerId + ', CCLI-Lizenz: ' + config.ccliLicenseId : 'CCLI-Liednummer: ' + song.legalOwnerId;
return new Paragraph({
text: artist + label + termsOfUse + origin + licence,
@@ -147,7 +136,6 @@ export class DocxService {
});
}
private renderSection(section: Section, chordMode: ChordMode): Paragraph[] {
return section.lines
.filter(line => {
@@ -171,13 +159,11 @@ export class DocxService {
return new Paragraph({
text: line.text,
style: 'songtext',
spacing
spacing,
});
}
private renderTitle(type: string): Paragraph[] {
const songTitle = new Paragraph({
text: type,
heading: HeadingLevel.HEADING_1,
@@ -187,34 +173,39 @@ export class DocxService {
return [songTitle];
}
private async prepareData(showId: string): Promise<{ songs: ({ showSong: ShowSong, song: Song, sections: Section[] })[]; show: Show, user: User, config: Config }> {
private async prepareData(showId: string): Promise<{
songs: {showSong: ShowSong; song: Song; sections: Section[]}[];
show: Show;
user: User;
config: Config;
}> {
const show = await this.showService.read$(showId).pipe(first()).toPromise();
const user = await this.userService.getUserbyId(show.owner);
const config = await this.configService.get();
const showSongs = await this.showSongService.list(showId);
const songsAsync = await showSongs.map(async showSong => {
const songsAsync = showSongs.map(async showSong => {
const song = await this.songService.read(showSong.songId);
const sections = this.textRenderingService.parse(song.text, {
baseKey: showSong.keyOriginal,
targetKey: showSong.key
targetKey: showSong.key,
});
return {
showSong,
song,
sections
sections,
};
});
const songs = await Promise.all(songsAsync);
return {songs, show, user, config};
}
private saveAs(blob, fileName) {
const a = document.createElement('a') as any;
private saveAs(blob: Blob, fileName: string) {
const a = document.createElement('a');
document.body.appendChild(a);
a.setAttribute('target', '_self');
a.style = 'display: none';
a.style.display = 'none';
const url = window.URL.createObjectURL(blob);
a.href = url;

View File

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

View File

@@ -2,17 +2,17 @@ import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {DbService} from '../../../services/db.service';
import {Show} from './show';
import {QueryFn} from '@angular/fire/firestore/interfaces';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class ShowDataService {
private collection = 'shows';
constructor(private dbService: DbService) {
}
public constructor(private dbService: DbService) {}
public list$ = (queryFn?): Observable<Show[]> => this.dbService.col$(this.collection, queryFn);
public list$ = (queryFn?: QueryFn): Observable<Show[]> => this.dbService.col$(this.collection, queryFn);
public read$ = (showId: string): Observable<Show | undefined> => this.dbService.doc$(`${this.collection}/${showId}`);
public update = async (showId: string, data: Partial<Show>): Promise<void> => await this.dbService.doc(`${this.collection}/${showId}`).update(data);
public add = async (data: Partial<Show>): Promise<string> => (await this.dbService.col(this.collection).add(data)).id;

View File

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

View File

@@ -2,20 +2,21 @@ import {Injectable} from '@angular/core';
import {DbService} from '../../../services/db.service';
import {Observable} from 'rxjs';
import {ShowSong} from './show-song';
import {QueryFn} from '@angular/fire/firestore/interfaces';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class ShowSongDataService {
private collection = 'shows';
private subCollection = 'songs';
constructor(private dbService: DbService) {
}
public constructor(private dbService: DbService) {}
public list$ = (showId: string, queryFn?): Observable<ShowSong[]> => this.dbService.col$(`${this.collection}/${showId}/${this.subCollection}`, queryFn);
public list$ = (showId: string, queryFn?: QueryFn): Observable<ShowSong[]> => this.dbService.col$(`${this.collection}/${showId}/${this.subCollection}`, queryFn);
public read$ = (showId: string, songId: string): Observable<ShowSong | undefined> => this.dbService.doc$(`${this.collection}/${showId}/${this.subCollection}/${songId}`);
public update$ = async (showId: string, songId: string, data: Partial<ShowSong>): Promise<void> => await this.dbService.doc(`${this.collection}/${showId}/${this.subCollection}/${songId}`).update(data);
public update$ = async (showId: string, songId: string, data: Partial<ShowSong>): Promise<void> =>
await this.dbService.doc(`${this.collection}/${showId}/${this.subCollection}/${songId}`).update(data);
public delete = async (showId: string, songId: string): Promise<void> => await this.dbService.doc(`${this.collection}/${showId}/${this.subCollection}/${songId}`).delete();
public add = async (showId: string, data: Partial<ShowSong>): Promise<string> => (await this.dbService.col(`${this.collection}/${showId}/${this.subCollection}`).add(data)).id;
}

View File

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

View File

@@ -7,16 +7,10 @@ import {first, take} from 'rxjs/operators';
import {UserService} from '../../../services/user/user.service';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class ShowSongService {
constructor(
private showSongDataService: ShowSongDataService,
private songDataService: SongDataService,
private userService: UserService,
) {
}
public constructor(private showSongDataService: ShowSongDataService, private songDataService: SongDataService, private userService: UserService) {}
public async new$(showId: string, songId: string, order: number, addedLive = false): Promise<string> {
const song = await this.songDataService.read$(songId).pipe(take(1)).toPromise();
@@ -27,7 +21,7 @@ export class ShowSongService {
key: song.key,
keyOriginal: song.key,
chordMode: user.chordMode,
addedLive
addedLive,
};
return await this.showSongDataService.add(showId, data);
}

View File

@@ -5,33 +5,40 @@ import {ShowDataService} from './show-data.service';
describe('ShowService', () => {
const mockShowDataService = {add: Promise.resolve(null)};
beforeEach(() => TestBed.configureTestingModule({
providers: [
{provide: ShowDataService, useValue: mockShowDataService}
]
}));
beforeEach(
() =>
void TestBed.configureTestingModule({
providers: [{provide: ShowDataService, useValue: mockShowDataService}],
})
);
ShowService.SHOW_TYPE_PUBLIC.forEach(type => {
it('should calc public flag for ' + type, async () => {
const service: ShowService = TestBed.get(ShowService);
const service: ShowService = TestBed.inject(ShowService);
const addSpy = spyOn(TestBed.inject(ShowDataService), 'add').and.returnValue(Promise.resolve('id'));
const id = await service.new$({showType: type});
expect(id).toBe('id');
expect(addSpy).toHaveBeenCalledWith({showType: type, public: true});
void expect(id).toBe('id');
void expect(addSpy).toHaveBeenCalledWith({
showType: type,
public: true,
});
});
});
ShowService.SHOW_TYPE_PRIVATE.forEach(type => {
it('should calc private flag for ' + type, async () => {
const service: ShowService = TestBed.get(ShowService);
const service: ShowService = TestBed.inject(ShowService);
const addSpy = spyOn(TestBed.inject(ShowDataService), 'add').and.returnValue(Promise.resolve('id'));
const id = await service.new$({showType: type});
expect(id).toBe('id');
expect(addSpy).toHaveBeenCalledWith({showType: type, public: false});
void expect(id).toBe('id');
void expect(addSpy).toHaveBeenCalledWith({
showType: type,
public: false,
});
});
});
});

View File

@@ -7,31 +7,28 @@ import {map, switchMap} from 'rxjs/operators';
import {User} from '../../../services/user/user';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class ShowService {
public static SHOW_TYPE = ['service-worship', 'service-praise', 'home-group-big', 'home-group', 'prayer-group', 'teens-group', 'kids-group', 'misc-public', 'misc-private'];
public static SHOW_TYPE_PUBLIC = ['service-worship', 'service-praise', 'home-group-big', 'teens-group', 'kids-group', 'misc-public'];
public static SHOW_TYPE_PRIVATE = ['home-group', 'prayer-group', 'misc-private',];
public static SHOW_TYPE_PRIVATE = ['home-group', 'prayer-group', 'misc-private'];
private user: User;
constructor(private showDataService: ShowDataService, private userService: UserService) {
userService.user$.subscribe(_ => this.user = _);
public constructor(private showDataService: ShowDataService, private userService: UserService) {
userService.user$.subscribe(_ => (this.user = _));
}
public read$ = (showId: string): Observable<Show> => this.showDataService.read$(showId);
public list$(publishedOnly: boolean = false): Observable<Show[]> {
public list$(publishedOnly = false): Observable<Show[]> {
return this.userService.user$.pipe(
switchMap(_ => this.showDataService.list$(), (user: User, shows: Show[]) => ({user, shows})),
map(_ => _.shows
.filter(_ => !_.archived)
.filter(show => show.published || (show.owner === _.user.id && !publishedOnly))
)
switchMap(
() => this.showDataService.list$(),
(user: User, shows: Show[]) => ({user, shows})
),
map(s => s.shows.filter(_ => !_.archived).filter(show => show.published || (show.owner === s.user.id && !publishedOnly)))
);
}
public update$ = async (showId: string, data: Partial<Show>): Promise<void> => this.showDataService.update(showId, data);

View File

@@ -14,6 +14,4 @@ export interface Show {
presentationSongId: string;
presentationSection: number;
presentationZoom: number;
}

View File

@@ -1,49 +1,82 @@
<div *ngIf="(show$|async) as show">
<div *ngIf="show$ | async as show">
<app-card
closeLink="../"
heading="{{show.showType|showType}}, {{show.date.toDate()|date:'dd.MM.yyyy'}} - {{getStatus(show)}}">
<i *ngIf="show.public">öffentliche Veranstaltung von
heading="{{ show.showType | showType }}, {{
show.date.toDate() | date: 'dd.MM.yyyy'
}} - {{ getStatus(show) }}"
>
<i *ngIf="show.public"
>öffentliche Veranstaltung von
<app-user-name [userId]="show.owner"></app-user-name>
</i>
<i *ngIf="!show.public">geschlossene Veranstaltung von
<i *ngIf="!show.public"
>geschlossene Veranstaltung von
<app-user-name [userId]="show.owner"></app-user-name>
</i>
<p *ngIf="!show.published">
<mat-checkbox [(ngModel)]="showText">Text anzeigen</mat-checkbox>
</p>
<div *ngIf="showSongs && songs" class="song-list">
<app-song *ngFor="let song of showSongs" [showId]="showId"
[showSong]="song"
[showSongs]="showSongs"
[showText]="showText"
[show]="show"
[song]="getSong(song.songId)"
class="song-row"
<app-song
*ngFor="let song of showSongs"
[Song]="getSong(song.songId)"
[showId]="showId"
[showSong]="song"
[showSongs]="showSongs"
[showText]="showText"
[show]="show"
class="song-row"
></app-song>
</div>
<app-add-song *ngIf="songs && !show.published" [showId]="showId" [showSongs]="showSongs"
[songs]="songs"></app-add-song>
<app-add-song
*ngIf="songs && !show.published"
[showId]="showId"
[showSongs]="showSongs"
[songs]="songs"
></app-add-song>
<app-button-row>
<ng-container *appOwner="show.owner">
<app-button (click)="onArchive(true)" *ngIf="!show.archived" [icon]="faBox">
<app-button
(click)="onArchive(true)"
*ngIf="!show.archived"
[icon]="faBox"
>
Archivieren
</app-button>
<app-button (click)="onArchive(false)" *ngIf="show.archived" [icon]="faBoxOpen">
<app-button
(click)="onArchive(false)"
*ngIf="show.archived"
[icon]="faBoxOpen"
>
Wiederherstellen
</app-button>
<app-button (click)="onPublish(true)" *ngIf="!show.published" [icon]="faPublish">
<app-button
(click)="onPublish(true)"
*ngIf="!show.published"
[icon]="faPublish"
>
Veröffentlichen
</app-button>
<app-button (click)="onPublish(false)" *ngIf="show.published" [icon]="faUnpublish">
<app-button
(click)="onPublish(false)"
*ngIf="show.published"
[icon]="faUnpublish"
>
Veröffentlichung zurückziehen
</app-button>
</ng-container>
<app-button [icon]="faDownload" [matMenuTriggerFor]="menu">Herunterladen</app-button>
<app-button [icon]="faDownload" [matMenuTriggerFor]="menu"
>Herunterladen
</app-button>
<mat-menu #menu="matMenu">
<app-button (click)="onDownload()" [icon]="faUser">Ablauf für Lobpreisleiter</app-button>
<app-button (click)="onDownloadHandout()" [icon]="faUsers">Handout mit Copyright Infos</app-button>
<app-button (click)="onDownload()" [icon]="faUser"
>Ablauf für Lobpreisleiter
</app-button>
<app-button (click)="onDownloadHandout()" [icon]="faUsers"
>Handout mit Copyright Infos
</app-button>
</mat-menu>
</app-button-row>
</app-card>

View File

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

View File

@@ -20,7 +20,7 @@ import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers';
@Component({
selector: 'app-show',
templateUrl: './show.component.html',
styleUrls: ['./show.component.less']
styleUrls: ['./show.component.less'],
})
export class ShowComponent implements OnInit {
public show$: Observable<Show>;
@@ -37,29 +37,31 @@ export class ShowComponent implements OnInit {
public faUser = faUser;
public faUsers = faUsers;
constructor(
public constructor(
private activatedRoute: ActivatedRoute,
private showService: ShowService,
private songService: SongService,
private showSongService: ShowSongService,
private docxService: DocxService,
) {
}
private docxService: DocxService
) {}
ngOnInit(): void {
public ngOnInit(): void {
this.show$ = this.activatedRoute.params.pipe(
map(param => param.showId),
tap(_ => this.showId = _),
switchMap(showId => this.showService.read$(showId))
map((param: {showId: string}) => param.showId),
tap((_: string) => (this.showId = _)),
switchMap((showId: string) => this.showService.read$(showId))
);
this.activatedRoute.params.pipe(
map(param => param.showId),
switchMap(showId => this.showSongService.list$(showId)),
filter(_ => !!_)
).subscribe(_ => this.showSongs = _);
this.songService.list$().pipe(
filter(_ => !!_)
).subscribe(_ => this.songs = _);
this.activatedRoute.params
.pipe(
map((param: {showId: string}) => param.showId),
switchMap(showId => this.showSongService.list$(showId)),
filter(_ => !!_)
)
.subscribe(_ => (this.showSongs = _));
this.songService
.list$()
.pipe(filter(_ => !!_))
.subscribe(_ => (this.songs = _));
}
public getSong(songId: string): Song {
@@ -90,6 +92,9 @@ export class ShowComponent implements OnInit {
}
public async onDownloadHandout(): Promise<void> {
await this.docxService.create(this.showId, {chordMode: 'hide', copyright: true});
await this.docxService.create(this.showId, {
chordMode: 'hide',
copyright: true,
});
}
}

View File

@@ -1,21 +1,42 @@
<div *ngIf="_song">
<div *ngIf="show.published" class="title published">{{_song.title}}</div>
<div *ngIf="iSong">
<div *ngIf="show.published" class="title published">{{ iSong.title }}</div>
<div *ngIf="!show.published" class="song">
<app-menu-button (click)="reorder(true)" [icon]="faUp" class="btn-up btn-icon"></app-menu-button>
<app-menu-button (click)="reorder(false)" [icon]="faDown" class="btn-down btn-icon"></app-menu-button>
<span class="title">{{_song.title}}</span>
<app-menu-button
(click)="reorder(true)"
[icon]="faUp"
class="btn-up btn-icon"
></app-menu-button>
<app-menu-button
(click)="reorder(false)"
[icon]="faDown"
class="btn-down btn-icon"
></app-menu-button>
<span class="title">{{ iSong.title }}</span>
<span class="keys">
<span *ngIf="showSong.keyOriginal!==showSong.key">{{showSong.keyOriginal}}&nbsp;&nbsp;</span>
<mat-form-field *ngIf="keys" appearance="standard">
<span *ngIf="showSong.keyOriginal !== showSong.key"
>{{ showSong.keyOriginal }}&nbsp;&nbsp;</span
>
<mat-form-field *ngIf="keys" appearance="standard">
<mat-select [formControl]="keyFormControl">
<mat-option *ngFor="let key of keys" [value]="key">{{key}}</mat-option>
<mat-option *ngFor="let key of keys" [value]="key">{{
key
}}</mat-option>
</mat-select>
</mat-form-field>
</span>
<app-menu-button (click)="onDelete()" [icon]="faDelete" class="btn-delete btn-icon"></app-menu-button>
</span>
<app-menu-button
(click)="onDelete()"
[icon]="faDelete"
class="btn-delete btn-icon"
></app-menu-button>
</div>
<app-song-text (chordModeChanged)="onChordModeChanged($event)" *ngIf="showText || show.published"
[chordMode]="showSong.chordMode" [transpose]="{baseKey: showSong.keyOriginal, targetKey: showSong.key}"
[showSwitch]="!show.published" [text]="_song.text"></app-song-text>
<app-song-text
(chordModeChanged)="onChordModeChanged($event)"
*ngIf="showText || show.published"
[chordMode]="showSong.chordMode"
[showSwitch]="!show.published"
[text]="iSong.text"
[transpose]="{ baseKey: showSong.keyOriginal, targetKey: showSong.key }"
></app-song-text>
</div>

View File

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

View File

@@ -13,7 +13,7 @@ import {Show} from '../../services/show';
@Component({
selector: 'app-song',
templateUrl: './song.component.html',
styleUrls: ['./song.component.less']
styleUrls: ['./song.component.less'],
})
export class SongComponent implements OnInit {
@Input() public show: Show;
@@ -22,30 +22,25 @@ export class SongComponent implements OnInit {
@Input() public showId: string;
@Input() public showText: boolean;
public keys: string[];
public faDelete = faTrash;
public faUp = faCaretUp;
public faDown = faCaretDown;
public keyFormControl: FormControl;
public iSong: Song;
constructor(
private showSongService: ShowSongService,
) {
}
public _song: Song;
public constructor(private showSongService: ShowSongService) {}
@Input()
public set song(song: Song) {
this._song = song;
this.keys = !!song ? getScale(song.key) : [];
public set Song(song: Song) {
this.iSong = song;
this.keys = song ? getScale(song.key) : [];
}
public ngOnInit(): void {
this.keyFormControl = new FormControl(this.showSong.key);
this.keyFormControl.valueChanges.subscribe(async value => {
await this.showSongService.update$(this.showId, this.showSong.id, {key: value});
this.keyFormControl.valueChanges.subscribe((value: string) => {
void this.showSongService.update$(this.showId, this.showSong.id, {key: value});
});
}
@@ -53,7 +48,6 @@ export class SongComponent implements OnInit {
await this.showSongService.delete$(this.showId, this.showSong.id);
}
public async reorder(up: boolean): Promise<void> {
if (up) {
await this.reorderUp();
@@ -63,7 +57,7 @@ export class SongComponent implements OnInit {
}
public async reorderUp(): Promise<void> {
const index = this.showSongs.findIndex(_ => _.songId === this._song.id);
const index = this.showSongs.findIndex(_ => _.songId === this.iSong.id);
if (index === 0) {
return;
}
@@ -71,12 +65,16 @@ export class SongComponent implements OnInit {
const song = this.showSongs[index];
const toggleSong = this.showSongs[index - 1];
await this.showSongService.update$(this.showId, song.id, {order: toggleSong.order});
await this.showSongService.update$(this.showId, toggleSong.id, {order: song.order});
await this.showSongService.update$(this.showId, song.id, {
order: toggleSong.order,
});
await this.showSongService.update$(this.showId, toggleSong.id, {
order: song.order,
});
}
public async reorderDown(): Promise<void> {
const index = this.showSongs.findIndex(_ => _.songId === this._song.id);
const index = this.showSongs.findIndex(_ => _.songId === this.iSong.id);
if (index === this.showSongs.length - 1) {
return;
}
@@ -84,11 +82,17 @@ export class SongComponent implements OnInit {
const song = this.showSongs[index];
const toggleSong = this.showSongs[index + 1];
await this.showSongService.update$(this.showId, song.id, {order: toggleSong.order});
await this.showSongService.update$(this.showId, toggleSong.id, {order: song.order});
await this.showSongService.update$(this.showId, song.id, {
order: toggleSong.order,
});
await this.showSongService.update$(this.showId, toggleSong.id, {
order: song.order,
});
}
public async onChordModeChanged(value: ChordMode): Promise<void> {
await this.showSongService.update$(this.showId, this.showSong.id, {chordMode: value});
await this.showSongService.update$(this.showId, this.showSong.id, {
chordMode: value,
});
}
}

View File

@@ -4,26 +4,24 @@ import {NewComponent} from './new/new.component';
import {ListComponent} from './list/list.component';
import {ShowComponent} from './show/show.component';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: ListComponent
component: ListComponent,
},
{
path: 'new',
component: NewComponent
component: NewComponent,
},
{
path: ':showId',
component: ShowComponent
}
component: ShowComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
exports: [RouterModule],
})
export class ShowsRoutingModule {
}
export class ShowsRoutingModule {}

View File

@@ -29,7 +29,6 @@ import {OwnerModule} from '../../services/user/owner.module';
import {UserNameModule} from '../../services/user/user-name/user-name.module';
import {MatMenuModule} from '@angular/material/menu';
@NgModule({
declarations: [NewComponent, ListComponent, ListItemComponent, ShowComponent, SongComponent],
imports: [
@@ -57,7 +56,6 @@ import {MatMenuModule} from '@angular/material/menu';
OwnerModule,
UserNameModule,
MatMenuModule,
]
],
})
export class ShowsModule {
}
export class ShowsModule {}

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();
});
});

Some files were not shown because too many files have changed in this diff Show More