This commit is contained in:
2026-03-15 12:50:33 +01:00
parent dd68a6b21d
commit d907c89eb6
36 changed files with 309 additions and 286 deletions

View File

@@ -43,6 +43,7 @@ export class MonitorComponent implements OnInit, OnDestroy {
public config$: Observable<Config | null>;
public presentationBackground: PresentationBackground = 'none';
private destroy$ = new Subject<void>();
private songSwitchTimeoutId: ReturnType<typeof setTimeout> | null = null;
public constructor() {
const configService = this.configService;
@@ -97,7 +98,10 @@ export class MonitorComponent implements OnInit, OnDestroy {
if (this.songId !== presentationSongId) {
this.songId = 'empty';
}
setTimeout(() => {
if (this.songSwitchTimeoutId) {
clearTimeout(this.songSwitchTimeoutId);
}
this.songSwitchTimeoutId = setTimeout(() => {
this.songId = presentationSongId;
this.cRef.markForCheck();
}, 600);
@@ -113,6 +117,9 @@ export class MonitorComponent implements OnInit, OnDestroy {
}
public ngOnDestroy(): void {
if (this.songSwitchTimeoutId) {
clearTimeout(this.songSwitchTimeoutId);
}
this.destroy$.next();
this.destroy$.complete();
}

View File

@@ -120,9 +120,11 @@ export class RemoteComponent implements OnDestroy {
});
this.presentationDynamicCaptionChanged$
.pipe(debounceTime(1000))
.pipe(debounceTime(1000), takeUntil(this.destroy$))
.subscribe(_ => void this.showService.update$(_.showId, {presentationDynamicCaption: _.presentationDynamicCaption}));
this.presentationDynamicTextChanged$.pipe(debounceTime(1000)).subscribe(_ => void this.showService.update$(_.showId, {presentationDynamicText: _.presentationDynamicText}));
this.presentationDynamicTextChanged$
.pipe(debounceTime(1000), takeUntil(this.destroy$))
.subscribe(_ => void this.showService.update$(_.showId, {presentationDynamicText: _.presentationDynamicText}));
}
public trackBy(index: number, item: PresentationSong): string {

View File

@@ -19,7 +19,7 @@ export interface ShareDialogData {
export class ShareDialogComponent {
public data = inject<ShareDialogData>(MAT_DIALOG_DATA);
public qrCode: string;
public qrCode = '';
public constructor() {
const data = this.data;
@@ -35,10 +35,10 @@ export class ShareDialogComponent {
light: '#ffffff',
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-return
}).then(_ => (this.qrCode = _));
}).then((qrCode: string) => (this.qrCode = qrCode));
}
public async share() {
public async share(): Promise<void> {
if (navigator.clipboard) await navigator.clipboard.writeText(this.data.url);
if (navigator.share)

View File

@@ -87,10 +87,15 @@ export class EditComponent implements OnInit {
return;
}
await this.showService.update$(this.form.value.id, {
date: Timestamp.fromDate(this.form.value.date),
showType: this.form.value.showType,
const {id, date, showType} = this.form.getRawValue();
if (!id || !date || !showType) {
return;
}
await this.showService.update$(id, {
date: Timestamp.fromDate(date),
showType,
} as Partial<Show>);
await this.router.navigateByUrl(`/shows/${this.form.value.id ?? ''}`);
await this.router.navigateByUrl(`/shows/${id}`);
}
}

View File

@@ -1,6 +1,7 @@
import {Component, Input, inject} from '@angular/core';
import {Component, DestroyRef, Input, inject} from '@angular/core';
import {KeyValue} from '@angular/common';
import {ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup} from '@angular/forms';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {FilterValues} from './filter-values';
import {Show} from '../../services/show';
import {ShowService} from '../../services/show.service';
@@ -24,13 +25,18 @@ export class FilterComponent {
private showService = inject(ShowService);
private userService = inject(UserService);
private filterStore = inject(FilterStoreService);
private destroyRef = inject(DestroyRef);
@Input() public shows: Show[] = [];
public showTypePublic = ShowService.SHOW_TYPE_PUBLIC;
public showTypePrivate = ShowService.SHOW_TYPE_PRIVATE;
public filterFormGroup: UntypedFormGroup;
public filterFormGroup: FormGroup<{
time: FormControl<number>;
owner: FormControl<string | null>;
showType: FormControl<string | null>;
}>;
public times: KeyValue<number, string>[] = [
{key: 1, value: 'letzter Monat'},
{key: 3, value: 'letztes Quartal'},
@@ -41,15 +47,15 @@ export class FilterComponent {
public owners: {key: string; value: string}[] = [];
public constructor() {
const fb = inject(UntypedFormBuilder);
const fb = inject(FormBuilder);
this.filterFormGroup = fb.group({
time: 1,
owner: null,
showType: null,
time: fb.nonNullable.control(1),
owner: fb.control<string | null>(null),
showType: fb.control<string | null>(null),
});
this.filterStore.showFilter$.subscribe(filterValues => {
this.filterStore.showFilter$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(filterValues => {
this.filterFormGroup.patchValue(
{
time: filterValues.time,
@@ -60,11 +66,13 @@ export class FilterComponent {
);
});
this.filterFormGroup.controls.time.valueChanges.subscribe(_ => this.filterValueChanged('time', (_ as number) ?? 1));
this.filterFormGroup.controls.owner.valueChanges.subscribe(_ => this.filterValueChanged('owner', (_ as string | null) ?? ''));
this.filterFormGroup.controls.showType.valueChanges.subscribe(_ => this.filterValueChanged('showType', (_ as string | null) ?? ''));
this.filterFormGroup.controls.time.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('time', value));
this.filterFormGroup.controls.owner.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('owner', value ?? ''));
this.filterFormGroup.controls.showType.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('showType', value ?? ''));
this.owners$().subscribe(owners => (this.owners = owners));
this.owners$()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(owners => (this.owners = owners));
}
public owners$ = (): Observable<{key: string; value: string}[]> => {
@@ -85,17 +93,15 @@ export class FilterComponent {
this.userService.getUserbyId$(ownerId).pipe(
map(user => ({
key: ownerId,
value: user?.name,
value: user?.name ?? ownerId,
}))
)
)
);
}),
map(owners => {
return owners.sort(dynamicSort('value'));
}),
map(owners => owners.sort(dynamicSort<{key: string; value: string}>('value'))),
distinctUntilChanged((left, right) => this.sameOwners(left, right)),
map(_ => _ as {key: string; value: string}[])
map(owners => owners as {key: string; value: string}[])
);
};

View File

@@ -3,7 +3,7 @@ import {ShowDataService} from '../services/show-data.service';
import {Observable} from 'rxjs';
import {Show} from '../services/show';
import {ShowService} from '../services/show.service';
import {ReactiveFormsModule, UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {Router} from '@angular/router';
import {faSave} from '@fortawesome/free-solid-svg-icons';
import {CardComponent} from '../../../widget-modules/components/card/card.component';
@@ -46,9 +46,9 @@ export class NewComponent implements OnInit {
public shows$: Observable<Show[]>;
public showTypePublic = ShowService.SHOW_TYPE_PUBLIC;
public showTypePrivate = ShowService.SHOW_TYPE_PRIVATE;
public form: UntypedFormGroup = new UntypedFormGroup({
date: new UntypedFormControl(null, Validators.required),
showType: new UntypedFormControl(null, Validators.required),
public form = new FormGroup({
date: new FormControl<Date | null>(null, Validators.required),
showType: new FormControl<string | null>(null, Validators.required),
});
public faSave = faSave;
@@ -68,7 +68,11 @@ export class NewComponent implements OnInit {
return;
}
const id = await this.showService.new$(this.form.value as Partial<Show>);
const {date, showType} = this.form.getRawValue();
const id = await this.showService.new$({
date,
showType,
} as unknown as Partial<Show>);
await this.router.navigateByUrl(`/shows/${id ?? ''}`);
}
}

View File

@@ -3,6 +3,7 @@ import {filter, map, shareReplay, switchMap, tap} from 'rxjs/operators';
import {ActivatedRoute, Router} from '@angular/router';
import {ShowService} from '../services/show.service';
import {Observable, of, Subscription} from 'rxjs';
import {take} from 'rxjs/operators';
import {Show} from '../services/show';
import {SongService} from '../../songs/services/song.service';
import {Song} from '../../songs/services/song';
@@ -112,12 +113,13 @@ export class ShowComponent implements OnInit, OnDestroy {
public faRestore = faMinimize;
public faMaximize = faMaximize;
public faNextSong = faChevronRight;
public currentTime: Date;
public currentTime!: Date;
private subs: Subscription[] = [];
private clockIntervalId: ReturnType<typeof setInterval> | null = null;
public ngOnInit(): void {
this.currentTime = new Date();
setInterval(() => {
this.clockIntervalId = setInterval(() => {
this.currentTime = new Date();
}, 10000);
this.show$ = this.activatedRoute.params.pipe(
@@ -155,6 +157,9 @@ export class ShowComponent implements OnInit, OnDestroy {
public ngOnDestroy(): void {
this.subs.forEach(_ => _.unsubscribe());
if (this.clockIntervalId) {
clearInterval(this.clockIntervalId);
}
}
public onZoomIn() {
@@ -172,7 +177,7 @@ export class ShowComponent implements OnInit, OnDestroy {
width: '350px',
});
dialogRef.afterClosed().subscribe((archive: boolean) => {
dialogRef.afterClosed().pipe(take(1)).subscribe((archive: boolean) => {
if (archive && this.showId != null) void this.showService.update$(this.showId, {archived});
});
}

View File

@@ -1,8 +1,9 @@
import {Component, Input, OnInit, ViewChild, inject} from '@angular/core';
import {Component, DestroyRef, Input, OnInit, ViewChild, inject} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {ShowSongService} from '../../services/show-song.service';
import {ShowSong} from '../../services/show-song';
import {getScale} from '../../../songs/services/key.helper';
import {ReactiveFormsModule, UntypedFormControl} from '@angular/forms';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {ChordMode, SongTextComponent} from '../../../../widget-modules/components/song-text/song-text.component';
import {Show} from '../../services/show';
import {faEraser, faPenToSquare, faSave, faTrash} from '@fortawesome/free-solid-svg-icons';
@@ -42,6 +43,7 @@ import {CdkDragHandle} from '@angular/cdk/drag-drop';
})
export class SongComponent implements OnInit {
private showSongService = inject(ShowSongService);
private destroyRef = inject(DestroyRef);
@Input() public show: Show | null = null;
@Input() public showId: string | null = null;
@@ -54,11 +56,11 @@ export class SongComponent implements OnInit {
public faEdit = faPenToSquare;
public faSave = faSave;
public faEraser = faEraser;
public keyFormControl: UntypedFormControl = new UntypedFormControl();
public keyFormControl = new FormControl<string>('', {nonNullable: true});
public iSong: ShowSong | null = null;
public edit = false;
public editSongControl = new UntypedFormControl();
@ViewChild('option') private keyOptions: MatSelect;
public editSongControl = new FormControl<string | null>(null);
@ViewChild('option') private keyOptions!: MatSelect;
@Input()
public set showSong(song: ShowSong) {
@@ -68,8 +70,8 @@ export class SongComponent implements OnInit {
public ngOnInit(): void {
if (!this.iSong) return;
this.keyFormControl = new UntypedFormControl(this.iSong.key);
this.keyFormControl.valueChanges.subscribe((value: string) => {
this.keyFormControl = new FormControl<string>(this.iSong.key, {nonNullable: true});
this.keyFormControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
if (!this.showId || !this.iSong) return;
void this.showSongService.update$(this.showId, this.iSong.id, {key: value});
});

View File

@@ -96,10 +96,11 @@ export class TextRenderingService {
return [];
}
const indices = {
const indices: Record<SectionType, number> = {
[SectionType.Bridge]: 0,
[SectionType.Chorus]: 0,
[SectionType.Verse]: 0,
[SectionType.Comment]: 0,
};
const sections: Section[] = [];

View File

@@ -1,5 +1,6 @@
import {Component, Input, inject} from '@angular/core';
import {ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup} from '@angular/forms';
import {Component, DestroyRef, Input, inject} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {SongService} from '../../services/song.service';
import {FilterValues} from './filter-values';
import {Song} from '../../services/song';
@@ -22,17 +23,24 @@ import {SongTypePipe} from '../../../../widget-modules/pipes/song-type-translate
})
export class FilterComponent {
private filterStore = inject(FilterStoreService);
private destroyRef = inject(DestroyRef);
public filterFormGroup: UntypedFormGroup;
public filterFormGroup: FormGroup<{
q: FormControl<string>;
type: FormControl<string>;
key: FormControl<string>;
legalType: FormControl<string>;
flag: FormControl<string>;
}>;
@Input() public songs: Song[] = [];
public types = SongService.TYPES;
public legalType = SongService.LEGAL_TYPE;
public keys = KEYS;
public constructor() {
const fb = inject(UntypedFormBuilder);
const fb = inject(FormBuilder);
this.filterFormGroup = fb.group({
this.filterFormGroup = fb.nonNullable.group({
q: '',
type: '',
key: '',
@@ -40,15 +48,15 @@ export class FilterComponent {
flag: '',
});
this.filterStore.songFilter$.subscribe(filterValues => {
this.filterStore.songFilter$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(filterValues => {
this.filterFormGroup.patchValue(filterValues, {emitEvent: false});
});
this.filterFormGroup.controls.q.valueChanges.subscribe(_ => this.filterValueChanged('q', (_ as string) ?? ''));
this.filterFormGroup.controls.key.valueChanges.subscribe(_ => this.filterValueChanged('key', (_ as string) ?? ''));
this.filterFormGroup.controls.type.valueChanges.subscribe(_ => this.filterValueChanged('type', (_ as string) ?? ''));
this.filterFormGroup.controls.legalType.valueChanges.subscribe(_ => this.filterValueChanged('legalType', (_ as string) ?? ''));
this.filterFormGroup.controls.flag.valueChanges.subscribe(_ => this.filterValueChanged('flag', (_ as string) ?? ''));
this.filterFormGroup.controls.q.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('q', value));
this.filterFormGroup.controls.key.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('key', value));
this.filterFormGroup.controls.type.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('type', value));
this.filterFormGroup.controls.legalType.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('legalType', value));
this.filterFormGroup.controls.flag.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('flag', value));
}
public getFlags(): string[] {

View File

@@ -1,4 +1,5 @@
import {Component, inject} from '@angular/core';
import {Component, DestroyRef, inject} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {Upload} from '../../../services/upload';
import {UploadService} from '../../../services/upload.service';
import {ActivatedRoute} from '@angular/router';
@@ -22,6 +23,7 @@ export class EditFileComponent {
private activatedRoute = inject(ActivatedRoute);
private uploadService = inject(UploadService);
private fileService = inject(FileDataService);
private destroyRef = inject(DestroyRef);
public selectedFiles: FileList | null = null;
public currentUpload: Upload | null = null;
@@ -32,7 +34,8 @@ export class EditFileComponent {
this.activatedRoute.params
.pipe(
map(param => param as {songId: string}),
map(param => param.songId)
map(param => param.songId),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(songId => {
this.songId = songId;

View File

@@ -1,9 +1,10 @@
import {Component, OnInit, inject} from '@angular/core';
import {Component, DestroyRef, inject, OnInit} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {Song} from '../../../services/song';
import {ReactiveFormsModule, UntypedFormGroup} from '@angular/forms';
import {ReactiveFormsModule} from '@angular/forms';
import {ActivatedRoute, Router, RouterStateSnapshot} from '@angular/router';
import {SongService} from '../../../services/song.service';
import {EditService} from '../edit.service';
import {EditService, SongFormGroup} from '../edit.service';
import {first, map, switchMap} from 'rxjs/operators';
import {startWith} from 'rxjs';
import {KEYS} from '../../../services/key.helper';
@@ -62,15 +63,9 @@ import {StatusPipe} from '../../../../../widget-modules/pipes/status-translater/
],
})
export class EditSongComponent implements OnInit {
private activatedRoute = inject(ActivatedRoute);
private songService = inject(SongService);
private editService = inject(EditService);
private router = inject(Router);
private textRenderingService = inject(TextRenderingService);
public dialog = inject(MatDialog);
public song: Song | null = null;
public form: UntypedFormGroup = new UntypedFormGroup({});
public form = {} as SongFormGroup;
public keys = KEYS;
public types = SongService.TYPES;
public status = SongService.STATUS;
@@ -83,6 +78,12 @@ export class EditSongComponent implements OnInit {
public faLink = faExternalLinkAlt;
public songtextFocus = false;
public chordValidationIssues: ChordValidationIssue[] = [];
private activatedRoute = inject(ActivatedRoute);
private songService = inject(SongService);
private editService = inject(EditService);
private router = inject(Router);
private textRenderingService = inject(TextRenderingService);
private destroyRef = inject(DestroyRef);
public ngOnInit(): void {
this.activatedRoute.params
@@ -90,23 +91,24 @@ export class EditSongComponent implements OnInit {
map(param => param as {songId: string}),
map(param => param.songId),
switchMap(songId => this.songService.read$(songId)),
first()
first(),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(song => {
this.song = song;
if (!song) return;
this.form = this.editService.createSongForm(song);
this.form.controls.flags.valueChanges.subscribe(_ => this.onFlagsChanged(_ as string));
this.form.controls.text.valueChanges.pipe(startWith(this.form.controls.text.value)).subscribe(text => {
this.updateChordValidation(text as string);
this.form.controls.flags.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.onFlagsChanged(value));
this.form.controls.text.valueChanges.pipe(startWith(this.form.controls.text.value), takeUntilDestroyed(this.destroyRef)).subscribe(text => {
this.updateChordValidation(text);
});
this.onFlagsChanged(this.form.controls.flags.value as string);
this.onFlagsChanged(this.form.controls.flags.value);
});
}
public async onSave(): Promise<void> {
if (!this.song || this.form.invalid) return;
const data = this.form.value as Partial<Song>;
const data = this.form.getRawValue() as Partial<Song>;
await this.songService.update$(this.song.id, data);
this.form.markAsPristine();
await this.router.navigateByUrl('songs/' + this.song.id);
@@ -121,7 +123,6 @@ export class EditSongComponent implements OnInit {
const input = event.input;
const value = event.value;
// Add our fruit
if ((value || '').trim()) {
const flags = [...this.flags, value.trim()];
this.form.controls.flags.setValue(flags.join(';'));
@@ -174,7 +175,7 @@ export class EditSongComponent implements OnInit {
private async onSaveDialogAfterClosed(save: boolean, url: string) {
if (save && this.song && !this.form.invalid) {
const data = this.form.value as Partial<Song>;
const data = this.form.getRawValue() as Partial<Song>;
await this.songService.update$(this.song.id, data);
}

View File

@@ -1,30 +1,48 @@
import {Injectable} from '@angular/core';
import {Song} from '../../services/song';
import {UntypedFormControl, UntypedFormGroup} from '@angular/forms';
import {FormControl, FormGroup} from '@angular/forms';
export type SongFormGroup = FormGroup<{
text: FormControl<string>;
title: FormControl<string>;
comment: FormControl<string>;
flags: FormControl<string>;
key: FormControl<string>;
tempo: FormControl<number>;
type: FormControl<Song['type']>;
status: FormControl<Song['status']>;
legalType: FormControl<Song['legalType']>;
legalOwner: FormControl<Song['legalOwner']>;
legalOwnerId: FormControl<string>;
artist: FormControl<string>;
label: FormControl<string>;
termsOfUse: FormControl<string>;
origin: FormControl<string>;
}>;
@Injectable({
providedIn: 'root',
})
export class EditService {
public createSongForm(song: Song): UntypedFormGroup {
return new UntypedFormGroup({
text: new UntypedFormControl(song.text),
title: new UntypedFormControl(song.title),
comment: new UntypedFormControl(song.comment),
flags: new UntypedFormControl(song.flags),
key: new UntypedFormControl(song.key),
tempo: new UntypedFormControl(song.tempo),
type: new UntypedFormControl(song.type),
status: new UntypedFormControl(song.status ?? 'draft'),
public createSongForm(song: Song): SongFormGroup {
return new FormGroup({
text: new FormControl(song.text, {nonNullable: true}),
title: new FormControl(song.title, {nonNullable: true}),
comment: new FormControl(song.comment, {nonNullable: true}),
flags: new FormControl(song.flags, {nonNullable: true}),
key: new FormControl(song.key, {nonNullable: true}),
tempo: new FormControl(song.tempo, {nonNullable: true}),
type: new FormControl(song.type, {nonNullable: true}),
status: new FormControl(song.status ?? 'draft', {nonNullable: true}),
legalType: new UntypedFormControl(song.legalType),
legalOwner: new UntypedFormControl(song.legalOwner),
legalOwnerId: new UntypedFormControl(song.legalOwnerId),
legalType: new FormControl(song.legalType, {nonNullable: true}),
legalOwner: new FormControl(song.legalOwner, {nonNullable: true}),
legalOwnerId: new FormControl(song.legalOwnerId, {nonNullable: true}),
artist: new UntypedFormControl(song.artist),
label: new UntypedFormControl(song.label),
termsOfUse: new UntypedFormControl(song.termsOfUse),
origin: new UntypedFormControl(song.origin),
artist: new FormControl(song.artist, {nonNullable: true}),
label: new FormControl(song.label, {nonNullable: true}),
termsOfUse: new FormControl(song.termsOfUse, {nonNullable: true}),
origin: new FormControl(song.origin, {nonNullable: true}),
});
}
}

View File

@@ -1,10 +1,11 @@
import {Component, OnDestroy, OnInit, inject} from '@angular/core';
import {Component, OnInit, inject} from '@angular/core';
import {faSave} from '@fortawesome/free-solid-svg-icons';
import {ReactiveFormsModule, UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms';
import {DestroyRef} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {SongService} from '../../services/song.service';
import {Song} from '../../services/song';
import {Router} from '@angular/router';
import {Subscription} from 'rxjs';
import {take} from 'rxjs/operators';
import {CardComponent} from '../../../../widget-modules/components/card/card.component';
import {MatFormField, MatLabel} from '@angular/material/form-field';
@@ -18,39 +19,34 @@ import {ButtonComponent} from '../../../../widget-modules/components/button/butt
styleUrls: ['./new.component.less'],
imports: [CardComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, ButtonRowComponent, ButtonComponent],
})
export class NewComponent implements OnInit, OnDestroy {
export class NewComponent implements OnInit {
private songService = inject(SongService);
private router = inject(Router);
private destroyRef = inject(DestroyRef);
public faSave = faSave;
public form: UntypedFormGroup = new UntypedFormGroup({
number: new UntypedFormControl(null, Validators.required),
title: new UntypedFormControl(null, Validators.required),
public form = new FormGroup({
number: new FormControl<number | null>(null, Validators.required),
title: new FormControl<string>('', {nonNullable: true, validators: [Validators.required]}),
});
private subs: Subscription[] = [];
public ngOnInit(): void {
this.form.reset();
this.subs.push(
this.songService
.list$()
.pipe(take(1))
.subscribe(songs => {
const freeSongnumber = this.getFreeSongNumber(songs);
this.form.controls.number.setValue(freeSongnumber);
})
);
}
public ngOnDestroy(): void {
this.subs.forEach(_ => _.unsubscribe());
this.songService
.list$()
.pipe(take(1), takeUntilDestroyed(this.destroyRef))
.subscribe(songs => {
const freeSongnumber = this.getFreeSongNumber(songs);
this.form.controls.number.setValue(freeSongnumber);
});
}
public async onSave(): Promise<void> {
const value = this.form.value as {number: number; title: string};
const songNumber = value.number;
const title = value.title;
const {number: songNumber, title} = this.form.getRawValue();
if (songNumber == null) {
return;
}
const newSongId = await this.songService.new(songNumber, title);
await this.router.navigateByUrl('/songs/' + newSongId + '/edit');
}

View File

@@ -75,11 +75,12 @@ export class SongComponent implements OnInit {
}
public ngOnInit(): void {
this.song$ = this.activatedRoute.params.pipe(
const song$ = this.activatedRoute.params.pipe(
map(param => param as {songId: string}),
map(param => param.songId),
switchMap(songId => this.songService.read$(songId))
);
this.song$ = song$;
this.files$ = this.activatedRoute.params.pipe(
map(param => param as {songId: string}),
@@ -87,7 +88,7 @@ export class SongComponent implements OnInit {
switchMap(songId => this.fileService.read$(songId))
);
this.songCount$ = combineLatest([this.user$, this.song$]).pipe(
this.songCount$ = combineLatest([this.userService.user$, song$]).pipe(
map(([user, song]) => {
if (!song) {
return 0;
@@ -111,10 +112,9 @@ export class SongComponent implements OnInit {
await this.router.navigateByUrl('/songs');
}
public async addSongToShow(show: Show, song: Song) {
if (!show) return;
const newId = await this.showSongService.new$(show?.id, song.id, false);
await this.showService.update$(show?.id, {order: [...show.order, newId ?? '']});
public async addSongToShow(show: Show, song: Song): Promise<void> {
const newId = await this.showSongService.new$(show.id, song.id, false);
await this.showService.update$(show.id, {order: [...show.order, newId ?? '']});
await this.router.navigateByUrl('/shows/' + show.id);
}
}

View File

@@ -8,6 +8,14 @@ export class AuthMessagePipe implements PipeTransform {
return 'Benutzer wurde nicht gefunden';
case 'auth/wrong-password':
return 'Passwort ist falsch';
case 'auth/email-already-in-use':
return 'Die E-Mail-Adresse wird bereits verwendet';
case 'auth/invalid-email':
return 'Die E-Mail-Adresse ist ungueltig';
case 'auth/weak-password':
return 'Das Passwort ist zu schwach';
case 'unknown_error':
return 'Unbekannter Fehler';
default:
return code;
}

View File

@@ -1,5 +1,5 @@
import {Component, OnInit, inject} from '@angular/core';
import {ReactiveFormsModule, UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {Router, RouterLink} from '@angular/router';
import {UserService} from '../../../services/user/user.service';
import {faSignInAlt, faUserPlus} from '@fortawesome/free-solid-svg-icons';
@@ -20,9 +20,9 @@ export class LoginComponent implements OnInit {
private userService = inject(UserService);
private router = inject(Router);
public form: UntypedFormGroup = new UntypedFormGroup({
user: new UntypedFormControl(null, [Validators.required, Validators.email]),
pass: new UntypedFormControl(null, [Validators.required]),
public form = new FormGroup({
user: new FormControl<string>('', {nonNullable: true, validators: [Validators.required, Validators.email]}),
pass: new FormControl<string>('', {nonNullable: true, validators: [Validators.required]}),
});
public errorMessage = '';
public faSignIn = faSignInAlt;
@@ -36,12 +36,19 @@ export class LoginComponent implements OnInit {
this.form.updateValueAndValidity();
if (this.form.valid) {
try {
const value = this.form.value as {user: string; pass: string};
await this.userService.login(value.user, value.pass);
await this.userService.login(this.form.controls.user.value, this.form.controls.pass.value);
await this.router.navigateByUrl('/');
} catch ({code}) {
this.errorMessage = code as string;
} catch (error) {
this.errorMessage = this.errorCode(error);
}
}
}
private errorCode(error: unknown): string {
if (typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string') {
return error.code;
}
return 'unknown_error';
}
}

View File

@@ -11,6 +11,9 @@
<mat-label>Passwort</mat-label>
<input formControlName="password" matInput type="password" />
</mat-form-field>
@if (errorMessage) {
<p class="error">{{ errorMessage | authMessage }}</p>
}
<app-button-row>
<app-button (click)="onCreate()" [icon]="faNewUser"

View File

@@ -1,5 +1,5 @@
import {Component, OnInit, inject} from '@angular/core';
import {ReactiveFormsModule, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms';
import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms';
import {UserService} from '../../../services/user/user.service';
import {faUserPlus} from '@fortawesome/free-solid-svg-icons';
import {CardComponent} from '../../../widget-modules/components/card/card.component';
@@ -7,23 +7,25 @@ import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input';
import {ButtonRowComponent} from '../../../widget-modules/components/button-row/button-row.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
import {AuthMessagePipe} from '../login/auth-message.pipe';
@Component({
selector: 'app-new',
templateUrl: './new.component.html',
styleUrls: ['./new.component.less'],
imports: [CardComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, ButtonRowComponent, ButtonComponent],
imports: [CardComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, ButtonRowComponent, ButtonComponent, AuthMessagePipe],
})
export class NewComponent implements OnInit {
private fb = inject(UntypedFormBuilder);
private fb = inject(FormBuilder);
private userService = inject(UserService);
public form: UntypedFormGroup = this.fb.group({
email: new UntypedFormControl(null, [Validators.required, Validators.email]),
name: new UntypedFormControl(null, [Validators.required]),
password: new UntypedFormControl(null, [Validators.required, Validators.minLength(6)]),
public form = this.fb.nonNullable.group({
email: ['', [Validators.required, Validators.email]],
name: ['', [Validators.required]],
password: ['', [Validators.required, Validators.minLength(6)]],
});
public faNewUser = faUserPlus;
public errorMessage = '';
public ngOnInit(): void {
this.form.reset();
@@ -33,15 +35,20 @@ export class NewComponent implements OnInit {
this.form.updateValueAndValidity();
if (this.form.valid) {
try {
const value = this.form.value as {
email: string;
name: string;
password: string;
};
await this.userService.createNewUser(value.email, value.name, value.password);
this.errorMessage = '';
const {email, name, password} = this.form.getRawValue();
await this.userService.createNewUser(email, name, password);
} catch (ex) {
console.error(ex);
this.errorMessage = this.errorCode(ex);
}
}
}
private errorCode(ex: unknown): string {
if (typeof ex === 'object' && ex !== null && 'code' in ex && typeof ex.code === 'string') {
return ex.code;
}
return 'unknown_error';
}
}

View File

@@ -1,5 +1,5 @@
import {Component, OnInit, inject} from '@angular/core';
import {ReactiveFormsModule, UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {Router} from '@angular/router';
import {UserService} from '../../../services/user/user.service';
import {faWindowRestore} from '@fortawesome/free-solid-svg-icons';
@@ -21,8 +21,8 @@ export class PasswordComponent implements OnInit {
public userService = inject(UserService);
private router = inject(Router);
public form: UntypedFormGroup = new UntypedFormGroup({
user: new UntypedFormControl(null, [Validators.required, Validators.email]),
public form = new FormGroup({
user: new FormControl<string>('', {nonNullable: true, validators: [Validators.required, Validators.email]}),
});
public errorMessage = '';
@@ -36,12 +36,19 @@ export class PasswordComponent implements OnInit {
this.form.updateValueAndValidity();
if (this.form.valid) {
try {
const value = this.form.value as {user: string};
await this.userService.changePassword(value.user);
await this.userService.changePassword(this.form.controls.user.value);
await this.router.navigateByUrl('/user/password-send');
} catch ({code}) {
this.errorMessage = code as string;
} catch (error) {
this.errorMessage = this.errorCode(error);
}
}
}
private errorCode(error: unknown): string {
if (typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string') {
return error.code;
}
return 'unknown_error';
}
}

View File

@@ -16,17 +16,17 @@ function normalize(input: string): string {
export const onlyUnique = <T>(value: T, index: number, array: T[]) => array.indexOf(value) === index;
export function dynamicSort(property: string) {
export function dynamicSort<T extends Record<string, string | number>>(property: keyof T | `-${string & keyof T}`) {
let sortOrder = 1;
if (property[0] === '-') {
let resolvedProperty = property as string;
if (resolvedProperty[0] === '-') {
sortOrder = -1;
property = property.substr(1);
resolvedProperty = resolvedProperty.slice(1);
}
return function (a: unknown, b: unknown) {
/* next line works with strings and numbers,
* and you may want to customize it to your needs
*/
const result = a[property] < b[property] ? -1 : a[property] > b[property] ? 1 : 0;
return function (a: T, b: T): number {
const left = a[resolvedProperty as keyof T];
const right = b[resolvedProperty as keyof T];
const result = left < right ? -1 : left > right ? 1 : 0;
return result * sortOrder;
};
}

View File

@@ -1,12 +1,13 @@
import {Directive, ElementRef, Input, OnInit, TemplateRef, ViewContainerRef, inject} from '@angular/core';
import {Directive, DestroyRef, Input, OnInit, TemplateRef, ViewContainerRef, inject} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {UserService} from './user.service';
@Directive({selector: '[appOwner]'})
export class OwnerDirective implements OnInit {
private element = inject(ElementRef);
private templateRef = inject<TemplateRef<unknown>>(TemplateRef);
private viewContainer = inject(ViewContainerRef);
private userService = inject(UserService);
private destroyRef = inject(DestroyRef);
private currentUserId: string | null = null;
private iAppOwner: string | null = null;
@@ -18,14 +19,14 @@ export class OwnerDirective implements OnInit {
}
public ngOnInit(): void {
this.userService.userId$.subscribe(user => {
this.userService.userId$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
this.currentUserId = user;
this.updateView();
});
this.updateView();
}
private updateView() {
private updateView(): void {
this.viewContainer.clear();
if (this.currentUserId === this.iAppOwner) {
this.viewContainer.createEmbeddedView(this.templateRef);

View File

@@ -1,4 +1,5 @@
import {ChangeDetectorRef, Directive, ElementRef, Input, OnInit, TemplateRef, ViewContainerRef, inject} from '@angular/core';
import {ChangeDetectorRef, DestroyRef, Directive, Input, OnInit, TemplateRef, ViewContainerRef, inject} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {roles} from './roles';
import {UserService} from './user.service';
import {User} from './user';
@@ -6,11 +7,11 @@ import {combineLatest} from 'rxjs';
@Directive({selector: '[appRole]'})
export class RoleDirective implements OnInit {
private element = inject(ElementRef);
private templateRef = inject<TemplateRef<unknown>>(TemplateRef);
private viewContainer = inject(ViewContainerRef);
private userService = inject(UserService);
private changeDetection = inject(ChangeDetectorRef);
private destroyRef = inject(DestroyRef);
@Input() public appRole: roles[] = [];
private currentUser: User | null = null;
@@ -18,14 +19,16 @@ export class RoleDirective implements OnInit {
private currentViewState = false;
public ngOnInit(): void {
combineLatest([this.userService.user$, this.userService.loggedIn$()]).subscribe(_ => {
this.currentUser = _[0];
this.loggedIn = _[1];
this.updateView();
});
combineLatest([this.userService.user$, this.userService.loggedIn$()])
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(([user, loggedIn]) => {
this.currentUser = user;
this.loggedIn = loggedIn;
this.updateView();
});
}
private updateView() {
private updateView(): void {
const viewState = this.loggedIn && this.checkPermission();
if (this.currentViewState !== viewState) {
if (!viewState) this.viewContainer.clear();
@@ -35,7 +38,7 @@ export class RoleDirective implements OnInit {
}
}
private checkPermission() {
private checkPermission(): boolean {
if (this.currentUser && this.currentUser.role) {
if (this.currentUser.role === 'admin') {
return true;

View File

@@ -1,5 +1,5 @@
import {ChangeDetectionStrategy, Component, Input, inject} from '@angular/core';
import {ReactiveFormsModule, UntypedFormControl} from '@angular/forms';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {filterSong} from '../../../services/filter.helper';
import {MatFormField, MatLabel, MatOption, MatSelect, MatSelectChange} from '@angular/material/select';
import {Song} from '../../../modules/songs/services/song';
@@ -25,7 +25,7 @@ export class AddSongComponent {
@Input() public showSongs: ShowSong[] | null = null;
@Input() public show: Show | null = null;
@Input() public addedLive = false;
public filteredSongsControl = new UntypedFormControl();
public filteredSongsControl = new FormControl<string>('', {nonNullable: true});
public filteredSongs(): Song[] {
if (!this.songs) return [];
@@ -44,7 +44,7 @@ export class AddSongComponent {
return 0;
});
const filterValue = this.filteredSongsControl.value as string;
const filterValue = this.filteredSongsControl.value;
return filterValue ? songs.filter(_ => filterSong(_, filterValue)) : songs;
}

View File

@@ -1,4 +1,5 @@
import {Component, inject} from '@angular/core';
import {Component, DestroyRef, inject} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {ActivatedRoute, Params, Router} from '@angular/router';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
@@ -10,13 +11,14 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms';
})
export class FilterComponent {
private router = inject(Router);
private destroyRef = inject(DestroyRef);
public value = '';
public constructor() {
const activatedRoute = inject(ActivatedRoute);
activatedRoute.queryParams.subscribe((params: Params) => {
activatedRoute.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params: Params) => {
const typedParams = params as {q: string};
if (typedParams.q) this.value = typedParams.q;
});

View File

@@ -1,4 +1,4 @@
import {ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnInit, Output, QueryList, ViewChildren, inject} from '@angular/core';
import {ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, QueryList, ViewChildren, inject} from '@angular/core';
import {TextRenderingService} from '../../../modules/songs/services/text-rendering.service';
import {faGripLines} from '@fortawesome/free-solid-svg-icons';
import {songSwitch} from './animation';
@@ -27,7 +27,7 @@ interface DisplaySegment {
animations: [songSwitch],
imports: [MatIconButton, FaIconComponent],
})
export class SongTextComponent implements OnInit {
export class SongTextComponent implements OnInit, OnDestroy {
private textRenderingService = inject(TextRenderingService);
private elRef = inject<ElementRef<HTMLElement>>(ElementRef);
private cRef = inject(ChangeDetectorRef);
@@ -47,6 +47,7 @@ export class SongTextComponent implements OnInit {
private invalidChordIssuesByLine = new Map<number, ChordValidationIssue[]>();
private iText = '';
private iTranspose: TransposeMode | null = null;
private offsetIntervalId: ReturnType<typeof setInterval> | null = null;
@Input()
public set chordMode(value: ChordMode) {
@@ -73,7 +74,7 @@ export class SongTextComponent implements OnInit {
}
public ngOnInit(): void {
setInterval(() => {
this.offsetIntervalId = setInterval(() => {
if (!this.fullscreen || this.index === -1 || !this.viewSections?.toArray()[this.index]) {
this.offset = 0;
this.cRef.markForCheck();
@@ -84,6 +85,12 @@ export class SongTextComponent implements OnInit {
}, 100);
}
public ngOnDestroy(): void {
if (this.offsetIntervalId) {
clearInterval(this.offsetIntervalId);
}
}
public getLines(section: Section): Line[] {
return section.lines.filter(_ => {
if (_.type !== LineType.chord) {

View File

@@ -2,7 +2,7 @@ import {Injectable, inject} from '@angular/core';
import {ActivatedRouteSnapshot, Router, UrlTree} from '@angular/router';
import {Observable} from 'rxjs';
import {UserService} from '../../services/user/user.service';
import {map} from 'rxjs/operators';
import {map, take} from 'rxjs/operators';
@Injectable({
providedIn: 'root',
@@ -18,8 +18,11 @@ export class RoleGuard {
}
return this.userService.user$.pipe(
take(1),
map(user => {
if (!user) return false;
if (!user) {
return this.router.createUrlTree(['brand', 'new-user']);
}
const roles = user.role?.split(';') ?? [];
if (roles.indexOf('admin') !== -1) {
return true;

View File

@@ -13,6 +13,8 @@ export class SectionTypePipe implements PipeTransform {
return 'Refrain';
case SectionType.Bridge:
return 'Bridge';
case SectionType.Comment:
return 'Kommentar';
}
}
}