migrate angular 21 finalize

This commit is contained in:
2026-03-09 22:56:31 +01:00
parent 26c99a0dae
commit bb08e46b0c
63 changed files with 738 additions and 783 deletions

View File

@@ -1,12 +1,7 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
# Angular 21-supported baseline to avoid unsupported-browser build warnings.
last 2 Chrome major versions
last 2 Edge major versions
last 2 Firefox major versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.
last 2 Safari major versions
last 2 iOS major versions

804
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@
"@angular/common": "^21.2.2",
"@angular/compiler": "^21.2.2",
"@angular/core": "^21.2.2",
"@angular/fire": "^21.0.0-rc.0",
"@angular/fire": "21.0.0-rc.0",
"@angular/forms": "^21.2.2",
"@angular/material": "^21.2.1",
"@angular/platform-browser": "^21.2.2",
@@ -28,7 +28,6 @@
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"docx": "^9.6.0",
"firebase": "^12.10.0",
"lodash": "^4.17.21",
"ngx-mat-select-search": "^8.0.4",
"qrcode": "^1.5.4",
"rxjs": "~7.8.1",
@@ -49,13 +48,12 @@
"@angular/language-service": "^21.2.2",
"@types/jasmine": "~6.0.0",
"@types/jasminewd2": "~2.0.13",
"@types/lodash": "^4.17.24",
"@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/parser": "^8.57.0",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"firebase-tools": "^14.27.0",
"firebase-tools": "^15.9.1",
"jasmine-core": "~6.1.0",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.4.4",

View File

@@ -1,4 +1,4 @@
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
import {ChangeDetectionStrategy, Component, OnInit, inject} from '@angular/core';
import {fader} from './animations';
import {ScrollService} from './services/scroll.service';
import {register} from 'swiper/element/bundle';
@@ -14,7 +14,9 @@ import {NavigationComponent} from './widget-modules/components/application-frame
imports: [RouterOutlet, NavigationComponent],
})
export class AppComponent implements OnInit {
public constructor(private scrollService: ScrollService) {
private scrollService = inject(ScrollService);
public constructor() {
register();
}

View File

@@ -1,4 +1,4 @@
import {Component} from '@angular/core';
import {Component, inject} from '@angular/core';
import {UserService} from '../../../services/user/user.service';
import {Observable} from 'rxjs';
import {User} from '../../../services/user/user';
@@ -12,9 +12,13 @@ import {AsyncPipe} from '@angular/common';
imports: [BrandComponent, AsyncPipe],
})
export class NewUserComponent {
private userService = inject(UserService);
public user$: Observable<User | null> | null = null;
public constructor(private userService: UserService) {
public constructor() {
const userService = this.userService;
this.user$ = userService.user$;
}
}

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {Observable} from 'rxjs';
import {shareReplay} from 'rxjs/operators';
import {DbService} from 'src/app/services/db.service';
@@ -8,6 +8,8 @@ import {GuestShow} from './guest-show';
providedIn: 'root',
})
export class GuestShowDataService {
private dbService = inject(DbService);
private collection = 'guest';
public list$: Observable<GuestShow[]> = this.dbService.col$<GuestShow>(this.collection).pipe(
shareReplay({
@@ -16,8 +18,6 @@ export class GuestShowDataService {
})
);
public constructor(private dbService: DbService) {}
public read$: (id: string) => Observable<GuestShow | null> = (id: string): Observable<GuestShow | null> => this.dbService.doc$(`${this.collection}/${id}`);
public update$: (id: string, data: Partial<GuestShow>) => Promise<void> = async (id: string, data: Partial<GuestShow>): Promise<void> =>
await this.dbService.doc(this.collection + '/' + id).update(data);

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {Show} from '../shows/services/show';
import {Song} from '../songs/services/song';
import {GuestShowDataService} from './guest-show-data.service';
@@ -8,10 +8,8 @@ import {ShowService} from '../shows/services/show.service';
providedIn: 'root',
})
export class GuestShowService {
public constructor(
private showService: ShowService,
private guestShowDataService: GuestShowDataService
) {}
private showService = inject(ShowService);
private guestShowDataService = inject(GuestShowDataService);
public async share(show: Show, songs: Song[]): Promise<string> {
const data = {

View File

@@ -1,4 +1,4 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {Component, CUSTOM_ELEMENTS_SCHEMA, inject} from '@angular/core';
import {GuestShowDataService} from './guest-show-data.service';
import {ActivatedRoute} from '@angular/router';
import {map, switchMap} from 'rxjs/operators';
@@ -16,17 +16,15 @@ import {ShowTypePipe} from '../../widget-modules/pipes/show-type-translater/show
imports: [SongTextComponent, AsyncPipe, DatePipe, ShowTypePipe],
})
export class GuestComponent {
private currentRoute = inject(ActivatedRoute);
private service = inject(GuestShowDataService);
private configService = inject(ConfigService);
public show$ = this.currentRoute.params.pipe(
map(param => param.id as string),
switchMap(id => this.service.read$(id))
);
public config$ = this.configService.get$();
public constructor(
private currentRoute: ActivatedRoute,
private service: GuestShowDataService,
private configService: ConfigService
) {}
public trackBy = (index: number, show: Song) => show.id;
}

View File

@@ -1,4 +1,4 @@
import {ChangeDetectorRef, Component, OnDestroy, OnInit} from '@angular/core';
import {ChangeDetectorRef, Component, OnDestroy, OnInit, inject} from '@angular/core';
import {distinctUntilChanged, filter, map, shareReplay, switchMap, takeUntil, tap} from 'rxjs/operators';
import {ShowService} from '../../shows/services/show.service';
import {Song} from '../../songs/services/song';
@@ -25,6 +25,12 @@ import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/s
imports: [LogoComponent, SongTextComponent, LegalComponent, AsyncPipe, DatePipe, ShowTypePipe],
})
export class MonitorComponent implements OnInit, OnDestroy {
private showService = inject(ShowService);
private showSongService = inject(ShowSongService);
private globalSettingsService = inject(GlobalSettingsService);
private configService = inject(ConfigService);
private cRef = inject(ChangeDetectorRef);
public song: Song | null = null;
public zoom = 10;
public currentShowId: string | null = null;
@@ -38,13 +44,9 @@ export class MonitorComponent implements OnInit, OnDestroy {
public presentationBackground: PresentationBackground = 'none';
private destroy$ = new Subject<void>();
public constructor(
private showService: ShowService,
private showSongService: ShowSongService,
private globalSettingsService: GlobalSettingsService,
private configService: ConfigService,
private cRef: ChangeDetectorRef
) {
public constructor() {
const configService = this.configService;
this.config$ = configService.get$();
}

View File

@@ -1,4 +1,4 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, inject} from '@angular/core';
import {combineLatest, Subject} from 'rxjs';
import {PresentationBackground, Show} from '../../shows/services/show';
import {ShowSongService} from '../../shows/services/show-song.service';
@@ -61,6 +61,12 @@ export interface PresentationSong {
],
})
export class RemoteComponent implements OnDestroy {
private showService = inject(ShowService);
private showSongService = inject(ShowSongService);
private songService = inject(SongService);
private textRenderingService = inject(TextRenderingService);
private cRef = inject(ChangeDetectorRef);
public show: Show | null = null;
public showSongs: ShowSong[] = [];
public songs$ = this.songService.list$();
@@ -73,14 +79,9 @@ export class RemoteComponent implements OnDestroy {
public presentationDynamicTextChanged$ = new Subject<{presentationDynamicText: string; showId: string}>();
private destroy$ = new Subject<void>();
public constructor(
private showService: ShowService,
private showSongService: ShowSongService,
private songService: SongService,
private textRenderingService: TextRenderingService,
globalSettingsService: GlobalSettingsService,
private cRef: ChangeDetectorRef
) {
public constructor() {
const globalSettingsService = inject(GlobalSettingsService);
const currentShowId$ = globalSettingsService.get$.pipe(
filter((settings): settings is NonNullable<typeof settings> => !!settings),
map(_ => _.currentShow),

View File

@@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, inject} from '@angular/core';
import {map} from 'rxjs/operators';
import {ShowService} from '../../shows/services/show.service';
import {Show} from '../../shows/services/show';
@@ -19,17 +19,15 @@ import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/s
imports: [CardComponent, MatButton, UserNameComponent, AsyncPipe, DatePipe, ShowTypePipe],
})
export class SelectComponent implements OnInit {
private showService = inject(ShowService);
private globalSettingsService = inject(GlobalSettingsService);
private router = inject(Router);
public visible = false;
public shows$ = this.showService
.list$(true)
.pipe(map(_ => _.filter(_ => _.date.toDate() > new Date(new Date().setMonth(new Date().getMonth() - 1))).sort((a, b) => (b.date < a.date ? -1 : b.date > a.date ? 1 : 0))));
public constructor(
private showService: ShowService,
private globalSettingsService: GlobalSettingsService,
private router: Router
) {}
public async selectShow(show: Show) {
this.visible = false;

View File

@@ -1,4 +1,4 @@
import {Component, Inject} from '@angular/core';
import {Component, inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, MatDialogContent} from '@angular/material/dialog';
import {MatButton} from '@angular/material/button';
import QRCode from 'qrcode';
@@ -17,9 +17,13 @@ export interface ShareDialogData {
styleUrl: './share-dialog.component.less',
})
export class ShareDialogComponent {
public data = inject<ShareDialogData>(MAT_DIALOG_DATA);
public qrCode: string;
public constructor(@Inject(MAT_DIALOG_DATA) public data: ShareDialogData) {
public constructor() {
const data = this.data;
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
QRCode.toDataURL(data.url, {
type: 'image/jpeg',

View File

@@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, inject} from '@angular/core';
import {ShowDataService} from '../services/show-data.service';
import {Observable, take} from 'rxjs';
import {Show} from '../services/show';
@@ -42,6 +42,10 @@ import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/s
],
})
export class EditComponent implements OnInit {
private showService = inject(ShowService);
private router = inject(Router);
private activatedRoute = inject(ActivatedRoute);
public shows$: Observable<Show[]>;
public showTypePublic = ShowService.SHOW_TYPE_PUBLIC;
public showTypePrivate = ShowService.SHOW_TYPE_PRIVATE;
@@ -52,12 +56,9 @@ export class EditComponent implements OnInit {
});
public faSave = faSave;
public constructor(
private showService: ShowService,
showDataService: ShowDataService,
private router: Router,
private activatedRoute: ActivatedRoute
) {
public constructor() {
const showDataService = inject(ShowDataService);
this.shows$ = showDataService.list$;
}

View File

@@ -1,4 +1,4 @@
import {Component, Input} from '@angular/core';
import {Component, Input, inject} from '@angular/core';
import {KeyValue} from '@angular/common';
import {ActivatedRoute, Router} from '@angular/router';
import {ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup} from '@angular/forms';
@@ -9,7 +9,6 @@ import {distinctUntilChanged, map, switchMap} from 'rxjs/operators';
import {combineLatest, Observable, of} from 'rxjs';
import {dynamicSort, onlyUnique} from '../../../../services/filter.helper';
import {UserService} from '../../../../services/user/user.service';
import {isEqual} from 'lodash';
import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatSelect} from '@angular/material/select';
import {MatOptgroup, MatOption} from '@angular/material/core';
@@ -22,6 +21,10 @@ import {ShowTypePipe} from '../../../../widget-modules/pipes/show-type-translate
imports: [ReactiveFormsModule, MatFormField, MatLabel, MatSelect, MatOption, MatOptgroup, ShowTypePipe],
})
export class FilterComponent {
private router = inject(Router);
private showService = inject(ShowService);
private userService = inject(UserService);
@Input() public route = '/shows/';
@Input() public shows: Show[] = [];
@@ -38,13 +41,10 @@ export class FilterComponent {
public owners: {key: string; value: string}[] = [];
public constructor(
private router: Router,
private showService: ShowService,
private userService: UserService,
activatedRoute: ActivatedRoute,
fb: UntypedFormBuilder
) {
public constructor() {
const activatedRoute = inject(ActivatedRoute);
const fb = inject(UntypedFormBuilder);
this.filterFormGroup = fb.group({
time: 1,
owner: null,
@@ -92,7 +92,7 @@ export class FilterComponent {
map(owners => {
return owners.sort(dynamicSort('value'));
}),
distinctUntilChanged(isEqual),
distinctUntilChanged((left, right) => this.sameOwners(left, right)),
map(_ => _ as {key: string; value: string}[])
);
};
@@ -104,4 +104,12 @@ export class FilterComponent {
});
await this.router.navigateByUrl(route);
}
private sameOwners(left: {key: string; value: string}[], right: {key: string; value: string}[]): boolean {
if (left.length !== right.length) {
return false;
}
return left.every((owner, index) => owner.key === right[index]?.key && owner.value === right[index]?.value);
}
}

View File

@@ -1,4 +1,4 @@
import {Component} from '@angular/core';
import {Component, inject} from '@angular/core';
import {combineLatest} from 'rxjs';
import {Show} from '../services/show';
import {fade} from '../../../animations';
@@ -22,8 +22,9 @@ import {SortByPipe} from '../../../widget-modules/pipes/sort-by/sort-by.pipe';
imports: [RoleDirective, ListHeaderComponent, FilterComponent, CardComponent, ListItemComponent, RouterLink, AsyncPipe, SortByPipe],
})
export class ListComponent {
public shows$ = this.showService.list$();
public privateShows$ = this.showService.list$().pipe(map(show => show.filter(_ => !_.published)));
private showService = inject(ShowService);
private activatedRoute = inject(ActivatedRoute);
public lastMonths$ = this.activatedRoute.queryParams.pipe(
map(params => {
const filterValues = params as FilterValues;
@@ -43,8 +44,9 @@ export class ListComponent {
return filterValues?.showType;
})
);
public shows$ = this.showService.list$();
public privateShows$ = this.showService.list$().pipe(map(show => show.filter(_ => !_.published)));
public queriedPublicShows$ = this.lastMonths$.pipe(switchMap(lastMonths => this.showService.listPublicSince$(lastMonths)));
public fallbackPublicShows$ = combineLatest([this.shows$, this.lastMonths$]).pipe(
map(([shows, lastMonths]) => {
const startDate = new Date();
@@ -54,7 +56,6 @@ export class ListComponent {
return shows.filter(show => show.published && !show.archived && show.date.toDate() >= startDate);
})
);
public publicShows$ = combineLatest([this.queriedPublicShows$, this.fallbackPublicShows$, this.owner$, this.showType$]).pipe(
map(([queriedShows, fallbackShows, owner, showType]) => {
const shows = queriedShows.length > 0 || fallbackShows.length === 0 ? queriedShows : fallbackShows;
@@ -63,10 +64,5 @@ export class ListComponent {
})
);
public constructor(
private showService: ShowService,
private activatedRoute: ActivatedRoute
) {}
public trackBy = (index: number, show: unknown) => (show as Show).id;
}

View File

@@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, inject} from '@angular/core';
import {ShowDataService} from '../services/show-data.service';
import {Observable} from 'rxjs';
import {Show} from '../services/show';
@@ -40,6 +40,9 @@ import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/s
],
})
export class NewComponent implements OnInit {
private showService = inject(ShowService);
private router = inject(Router);
public shows$: Observable<Show[]>;
public showTypePublic = ShowService.SHOW_TYPE_PUBLIC;
public showTypePrivate = ShowService.SHOW_TYPE_PRIVATE;
@@ -49,11 +52,9 @@ export class NewComponent implements OnInit {
});
public faSave = faSave;
public constructor(
private showService: ShowService,
showDataService: ShowDataService,
private router: Router
) {
public constructor() {
const showDataService = inject(ShowDataService);
this.shows$ = showDataService.list$;
}

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {Document, HeadingLevel, ISectionOptions, Packer, Paragraph} from 'docx';
import {ShowService} from './show.service';
import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/show-type.pipe';
@@ -26,13 +26,11 @@ export interface DownloadOptions {
providedIn: 'root',
})
export class DocxService {
public constructor(
private showService: ShowService,
private showSongService: ShowSongService,
private textRenderingService: TextRenderingService,
private userService: UserService,
private configService: ConfigService
) {}
private showService = inject(ShowService);
private showSongService = inject(ShowSongService);
private textRenderingService = inject(TextRenderingService);
private userService = inject(UserService);
private configService = inject(ConfigService);
public async create(showId: string, options: DownloadOptions = {}): Promise<void> {
const data = await this.prepareData(showId);
@@ -194,7 +192,8 @@ export class DocxService {
if (!config) return null;
const showSongs = await this.showSongService.list(showId);
const songsAsync = showSongs.map(showSong => {
const songsLoaded = showSongs
.map(showSong => {
const sections = this.textRenderingService.parse(showSong.text, {
baseKey: showSong.keyOriginal,
targetKey: showSong.key,
@@ -203,8 +202,7 @@ export class DocxService {
showSong,
sections,
};
});
const songsLoaded = (await Promise.all(songsAsync))
})
.filter(_ => !!_)
.map(
_ =>

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {Observable} from 'rxjs';
import {DbService} from '../../../services/db.service';
import {Show} from './show';
@@ -9,6 +9,8 @@ import {orderBy, QueryConstraint, Timestamp, where} from '@angular/fire/firestor
providedIn: 'root',
})
export class ShowDataService {
private dbService = inject(DbService);
private collection = 'shows';
public list$: Observable<Show[]> = this.dbService.col$<Show>(this.collection).pipe(
// server-side ordering cuts client work and keeps stable order across subscribers
@@ -19,8 +21,6 @@ export class ShowDataService {
})
);
public constructor(private dbService: DbService) {}
public listRaw$ = () => this.dbService.col$<Show>(this.collection);
public listPublicSince$(lastMonths: number): Observable<Show[]> {

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {DbService} from '../../../services/db.service';
import {Observable} from 'rxjs';
import {ShowSong} from './show-song';
@@ -9,12 +9,12 @@ import {shareReplay} from 'rxjs/operators';
providedIn: 'root',
})
export class ShowSongDataService {
private dbService = inject(DbService);
private collection = 'shows';
private subCollection = 'songs';
private listCache = new Map<string, Observable<ShowSong[]>>();
public constructor(private dbService: DbService) {}
public list$ = (showId: string, queryConstraints?: QueryConstraint[]): Observable<ShowSong[]> => {
if (queryConstraints && queryConstraints.length > 0) {
return this.dbService.col$(`${this.collection}/${showId}/${this.subCollection}`, queryConstraints);

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {ShowSongDataService} from './show-song-data.service';
import {firstValueFrom, Observable} from 'rxjs';
import {ShowSong} from './show-song';
@@ -10,12 +10,10 @@ import {ShowService} from './show.service';
providedIn: 'root',
})
export class ShowSongService {
public constructor(
private showSongDataService: ShowSongDataService,
private songDataService: SongDataService,
private userService: UserService,
private showService: ShowService
) {}
private showSongDataService = inject(ShowSongDataService);
private songDataService = inject(SongDataService);
private userService = inject(UserService);
private showService = inject(ShowService);
public async new$(showId: string, songId: string, addedLive = false): Promise<string | null> {
const song = await firstValueFrom(this.songDataService.read$(songId));

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {ShowDataService} from './show-data.service';
import {Show} from './show';
import {Observable} from 'rxjs';
@@ -10,15 +10,17 @@ import {User} from '../../../services/user/user';
providedIn: 'root',
})
export class ShowService {
private showDataService = inject(ShowDataService);
private userService = inject(UserService);
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'];
private user: User | null = null;
public constructor(
private showDataService: ShowDataService,
private userService: UserService
) {
public constructor() {
const userService = this.userService;
userService.user$.subscribe(_ => (this.user = _));
}

View File

@@ -1,4 +1,4 @@
import {ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, HostListener, OnDestroy, OnInit} from '@angular/core';
import {ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, HostListener, OnDestroy, OnInit, inject} from '@angular/core';
import {filter, map, shareReplay, switchMap, tap} from 'rxjs/operators';
import {ActivatedRoute, Router} from '@angular/router';
import {ShowService} from '../services/show.service';
@@ -80,6 +80,16 @@ import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/s
],
})
export class ShowComponent implements OnInit, OnDestroy {
private activatedRoute = inject(ActivatedRoute);
private showService = inject(ShowService);
private songService = inject(SongService);
private showSongService = inject(ShowSongService);
private docxService = inject(DocxService);
private router = inject(Router);
private cRef = inject(ChangeDetectorRef);
public dialog = inject(MatDialog);
private guestShowService = inject(GuestShowService);
public show$: Observable<Show | null> | null = null;
public songs$: Observable<Song[] | null> | null = null;
public showSongs: ShowSong[] | null = null;
@@ -105,18 +115,6 @@ export class ShowComponent implements OnInit, OnDestroy {
public currentTime: Date;
private subs: Subscription[] = [];
public constructor(
private activatedRoute: ActivatedRoute,
private showService: ShowService,
private songService: SongService,
private showSongService: ShowSongService,
private docxService: DocxService,
private router: Router,
private cRef: ChangeDetectorRef,
public dialog: MatDialog,
private guestShowService: GuestShowService
) {}
public ngOnInit(): void {
this.currentTime = new Date();
setInterval(() => {

View File

@@ -1,4 +1,4 @@
import {Component, Input, OnInit, ViewChild} from '@angular/core';
import {Component, Input, OnInit, ViewChild, inject} from '@angular/core';
import {ShowSongService} from '../../services/show-song.service';
import {ShowSong} from '../../services/show-song';
import {getScale} from '../../../songs/services/key.helper';
@@ -39,6 +39,8 @@ import {ButtonComponent} from '../../../../widget-modules/components/button/butt
],
})
export class SongComponent implements OnInit {
private showSongService = inject(ShowSongService);
@Input() public show: Show | null = null;
@Input() public showId: string | null = null;
@Input() public showText: boolean | null = null;
@@ -55,8 +57,6 @@ export class SongComponent implements OnInit {
public editSongControl = new UntypedFormControl();
@ViewChild('option') private keyOptions: MatSelect;
public constructor(private showSongService: ShowSongService) {}
@Input()
public set showSong(song: ShowSong) {
this.iSong = song;

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {File} from './file';
import {Observable} from 'rxjs';
import {FileServer} from './fileServer';
@@ -8,7 +8,7 @@ import {DbService} from '../../../services/db.service';
providedIn: 'root',
})
export class FileDataService {
public constructor(private db: DbService) {}
private db = inject(DbService);
public async set(songId: string, file: FileServer): Promise<string> {
const songRef = this.db.doc('songs/' + songId);

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {deleteObject, getDownloadURL, ref, Storage} from '@angular/fire/storage';
import {from, Observable} from 'rxjs';
import {FileDataService} from './file-data.service';
@@ -7,10 +7,8 @@ import {FileDataService} from './file-data.service';
providedIn: 'root',
})
export class FileService {
public constructor(
private storage: Storage,
private fileDataService: FileDataService
) {}
private storage = inject(Storage);
private fileDataService = inject(FileDataService);
public getDownloadUrl(path: string): Observable<string> {
return from(getDownloadURL(ref(this.storage, path)));

View File

@@ -2,14 +2,14 @@ import {getScale, scaleMapping} from './key.helper';
describe('key.helper', () => {
it('should render Gb correctly', () => {
expect(scaleMapping['Gb']).toBe('G♭');
void expect(scaleMapping['Gb']).toBe('G♭');
});
it('should expose a sharp-based scale for D', () => {
expect(getScale('D')).toEqual(['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H']);
void expect(getScale('D')).toEqual(['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H']);
});
it('should keep flat-based spelling for Db', () => {
expect(getScale('Db')).toEqual(['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H']);
void expect(getScale('Db')).toEqual(['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H']);
});
});

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {Song} from './song';
import {Observable} from 'rxjs';
import {DbService} from '../../../services/db.service';
@@ -8,6 +8,8 @@ import {shareReplay, startWith} from 'rxjs/operators';
providedIn: 'root',
})
export class SongDataService {
private dbService = inject(DbService);
private collection = 'songs';
public list$: Observable<Song[]> = this.dbService.col$<Song>(this.collection).pipe(
startWith([] as Song[]), // immediate empty emit keeps UI responsive while first snapshot arrives
@@ -17,11 +19,6 @@ export class SongDataService {
})
);
public constructor(private dbService: DbService) {
// Warm the shared stream once at startup to avoid first-navigation delay.
// this.list$.subscribe();
}
public read$ = (songId: string): Observable<Song | null> => 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;

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {Observable} from 'rxjs';
import {SongService} from './song.service';
@@ -9,7 +9,7 @@ import {take} from 'rxjs/operators';
providedIn: 'root',
})
export class SongListResolver {
public constructor(private songService: SongService) {}
private songService = inject(SongService);
public resolve(): Observable<Song[]> {
return this.songService.list$().pipe(take(1));

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {firstValueFrom, Observable} from 'rxjs';
import {Song} from './song';
import {SongDataService} from './song-data.service';
@@ -16,21 +16,15 @@ export type SongLegalType = 'open' | 'allowed';
providedIn: 'root',
})
export class SongService {
private songDataService = inject(SongDataService);
private userService = inject(UserService);
public static TYPES: SongType[] = ['Praise', 'Worship', 'Misc'];
public static STATUS: SongStatus[] = ['draft', 'set', 'final'];
public static LEGAL_OWNER: SongLegalOwner[] = ['CCLI', 'other'];
public static LEGAL_TYPE: SongLegalType[] = ['open', 'allowed'];
// private list: Song[];
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 read$ = (songId: string): Observable<Song | null> => this.songDataService.read$(songId);
public read = (songId: string): Promise<Song | null> => firstValueFrom(this.read$(songId));

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {TransposeService} from './transpose.service';
import {TransposeMode} from './transpose-mode';
import {SectionType} from './section-type';
@@ -11,9 +11,9 @@ import {Line} from './line';
providedIn: 'root',
})
export class TextRenderingService {
private regexSection = /(Strophe|Refrain|Bridge)/;
private transposeService = inject(TransposeService);
public constructor(private transposeService: TransposeService) {}
private regexSection = /(Strophe|Refrain|Bridge)/;
public parse(text: string, transpose: TransposeMode | null, withComments = true): Section[] {
if (!text) {

View File

@@ -34,18 +34,18 @@ describe('TransposeService', () => {
const distance = service.getDistance('C', 'Db');
const map = service.getMap('C', 'Db', distance);
expect(distance).toBe(1);
expect(map?.['C']).toBe('Db');
expect(map?.['G']).toBe('Ab');
void expect(distance).toBe(1);
void expect(map?.['C']).toBe('Db');
void expect(map?.['G']).toBe('Ab');
});
it('should keep german B/H notation consistent', () => {
const distance = service.getDistance('H', 'C');
const map = service.getMap('H', 'C', distance);
expect(distance).toBe(1);
expect(map?.['H']).toBe('C');
expect(map?.['B']).toBe('C#');
void expect(distance).toBe(1);
void expect(map?.['H']).toBe('C');
void expect(map?.['B']).toBe('C#');
});
it('should render unknown chords as X', () => {
@@ -57,7 +57,7 @@ describe('TransposeService', () => {
const rendered = service.renderChords(line);
expect(rendered.text).toBe('Xsus4');
void expect(rendered.text).toBe('Xsus4');
});
it('should render unknown slash chords as X', () => {
@@ -69,7 +69,7 @@ describe('TransposeService', () => {
const rendered = service.renderChords(line);
expect(rendered.text).toBe('C/X');
void expect(rendered.text).toBe('C/X');
});
it('should transpose lines with long chord positions without truncating', () => {
@@ -81,7 +81,7 @@ describe('TransposeService', () => {
const rendered = service.renderChords(line);
expect(rendered.text.length).toBe(121);
expect(rendered.text.endsWith('C')).toBeTrue();
void expect(rendered.text.length).toBe(121);
void expect(rendered.text.endsWith('C')).toBeTrue();
});
});

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {Upload} from './upload';
import {FileDataService} from './file-data.service';
import {ref, Storage, uploadBytesResumable} from '@angular/fire/storage';
@@ -9,12 +9,8 @@ import {FileServer} from './fileServer';
providedIn: 'root',
})
export class UploadService extends FileBase {
public constructor(
private fileDataService: FileDataService,
private storage: Storage
) {
super();
}
private fileDataService = inject(FileDataService);
private storage = inject(Storage);
public pushUpload(songId: string, upload: Upload): void {
const directory = this.directory(songId);

View File

@@ -1,4 +1,4 @@
import {Component, Input} from '@angular/core';
import {Component, Input, inject} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup} from '@angular/forms';
import {SongService} from '../../services/song.service';
@@ -21,6 +21,8 @@ import {SongTypePipe} from '../../../../widget-modules/pipes/song-type-translate
imports: [ReactiveFormsModule, MatFormField, MatLabel, MatInput, MatSelect, MatOption, LegalTypePipe, KeyPipe, SongTypePipe],
})
export class FilterComponent {
private router = inject(Router);
public filterFormGroup: UntypedFormGroup;
@Input() public route = '/';
@Input() public songs: Song[] = [];
@@ -28,11 +30,10 @@ export class FilterComponent {
public legalType = SongService.LEGAL_TYPE;
public keys = KEYS;
public constructor(
private router: Router,
activatedRoute: ActivatedRoute,
fb: UntypedFormBuilder
) {
public constructor() {
const activatedRoute = inject(ActivatedRoute);
const fb = inject(UntypedFormBuilder);
this.filterFormGroup = fb.group({
q: '',
type: '',

View File

@@ -1,4 +1,4 @@
import {ChangeDetectionStrategy, Component, OnDestroy, OnInit} from '@angular/core';
import {ChangeDetectionStrategy, Component, OnDestroy, OnInit, inject} from '@angular/core';
import {SongService} from '../services/song.service';
import {Song} from '../services/song';
import {map} from 'rxjs/operators';
@@ -25,6 +25,10 @@ import {FaIconComponent} from '@fortawesome/angular-fontawesome';
imports: [ListHeaderComponent, FilterComponent, CardComponent, RouterLink, RoleDirective, FaIconComponent, AsyncPipe],
})
export class SongListComponent implements OnInit, OnDestroy {
private songService = inject(SongService);
private activatedRoute = inject(ActivatedRoute);
private scrollService = inject(ScrollService);
public anyFilterActive = false;
public songs$: Observable<Song[]> = combineLatest([
this.activatedRoute.queryParams.pipe(map(_ => _ as FilterValues)),
@@ -39,12 +43,6 @@ export class SongListComponent implements OnInit, OnDestroy {
public faDraft = faPencilRuler;
public faFinal = faCheck;
public constructor(
private songService: SongService,
private activatedRoute: ActivatedRoute,
private scrollService: ScrollService
) {}
public ngOnInit(): void {
setTimeout(() => this.scrollService.restoreScrollPositionFor('songlist'), 100);
setTimeout(() => this.scrollService.restoreScrollPositionFor('songlist'), 300);

View File

@@ -1,4 +1,4 @@
import {Component} from '@angular/core';
import {Component, inject} from '@angular/core';
import {Upload} from '../../../services/upload';
import {UploadService} from '../../../services/upload.service';
import {ActivatedRoute} from '@angular/router';
@@ -19,16 +19,16 @@ import {FileComponent} from './file/file.component';
imports: [CardComponent, NgStyle, MatIconButton, MatIcon, FileComponent, AsyncPipe],
})
export class EditFileComponent {
private activatedRoute = inject(ActivatedRoute);
private uploadService = inject(UploadService);
private fileService = inject(FileDataService);
public selectedFiles: FileList | null = null;
public currentUpload: Upload | null = null;
public songId: string | null = null;
public files$: Observable<File[]>;
public constructor(
private activatedRoute: ActivatedRoute,
private uploadService: UploadService,
private fileService: FileDataService
) {
public constructor() {
this.activatedRoute.params
.pipe(
map(param => param as {songId: string}),

View File

@@ -1,4 +1,4 @@
import {Component, Input} from '@angular/core';
import {Component, Input, inject} from '@angular/core';
import {Observable} from 'rxjs';
import {File} from '../../../../services/file';
import {faTrashAlt} from '@fortawesome/free-solid-svg-icons';
@@ -14,6 +14,8 @@ import {AsyncPipe} from '@angular/common';
imports: [MatIconButton, FaIconComponent, AsyncPipe],
})
export class FileComponent {
private fileService = inject(FileService);
public url$: Observable<string> | null = null;
public name = '';
public faTrash = faTrashAlt;
@@ -21,8 +23,6 @@ export class FileComponent {
private fileId: string | null = null;
private path: string | null = null;
public constructor(private fileService: FileService) {}
@Input()
public set file(file: File) {
this.url$ = this.fileService.getDownloadUrl(file.path + '/' + file.name);

View File

@@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, inject} from '@angular/core';
import {Song} from '../../../services/song';
import {ReactiveFormsModule, UntypedFormGroup} from '@angular/forms';
import {ActivatedRoute, Router, RouterStateSnapshot} from '@angular/router';
@@ -59,6 +59,12 @@ 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);
public dialog = inject(MatDialog);
public song: Song | null = null;
public form: UntypedFormGroup = new UntypedFormGroup({});
public keys = KEYS;
@@ -73,14 +79,6 @@ export class EditSongComponent implements OnInit {
public faLink = faExternalLinkAlt;
public songtextFocus = false;
public constructor(
private activatedRoute: ActivatedRoute,
private songService: SongService,
private editService: EditService,
private router: Router,
public dialog: MatDialog
) {}
public ngOnInit(): void {
this.activatedRoute.params
.pipe(

View File

@@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, inject} from '@angular/core';
import {first, map, switchMap} from 'rxjs/operators';
import {ActivatedRoute} from '@angular/router';
import {SongService} from '../../../services/song.service';
@@ -13,12 +13,10 @@ import {CardComponent} from '../../../../../widget-modules/components/card/card.
imports: [CardComponent, DatePipe],
})
export class HistoryComponent implements OnInit {
public song: Song | null = null;
private activatedRoute = inject(ActivatedRoute);
private songService = inject(SongService);
public constructor(
private activatedRoute: ActivatedRoute,
private songService: SongService
) {}
public song: Song | null = null;
public ngOnInit(): void {
this.activatedRoute.params

View File

@@ -1,4 +1,4 @@
import {Component, Input} from '@angular/core';
import {Component, Input, inject} from '@angular/core';
import {File} from '../../services/file';
import {getDownloadURL, ref, Storage} from '@angular/fire/storage';
import {from, Observable} from 'rxjs';
@@ -11,11 +11,11 @@ import {AsyncPipe} from '@angular/common';
imports: [AsyncPipe],
})
export class FileComponent {
private storage = inject(Storage);
public url$: Observable<string> | null = null;
public name = '';
public constructor(private storage: Storage) {}
@Input()
public set file(file: File) {
this.url$ = from(getDownloadURL(ref(this.storage, file.path + '/' + file.name)));

View File

@@ -1,4 +1,4 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Component, OnDestroy, OnInit, inject} from '@angular/core';
import {faSave} from '@fortawesome/free-solid-svg-icons';
import {ReactiveFormsModule, UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms';
import {SongService} from '../../services/song.service';
@@ -19,6 +19,9 @@ import {ButtonComponent} from '../../../../widget-modules/components/button/butt
imports: [CardComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, ButtonRowComponent, ButtonComponent],
})
export class NewComponent implements OnInit, OnDestroy {
private songService = inject(SongService);
private router = inject(Router);
public faSave = faSave;
public form: UntypedFormGroup = new UntypedFormGroup({
number: new UntypedFormControl(null, Validators.required),
@@ -26,11 +29,6 @@ export class NewComponent implements OnInit, OnDestroy {
});
private subs: Subscription[] = [];
public constructor(
private songService: SongService,
private router: Router
) {}
public ngOnInit(): void {
this.form.reset();

View File

@@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, inject} from '@angular/core';
import {ActivatedRoute, Router, RouterLink} from '@angular/router';
import {SongService} from '../services/song.service';
import {distinctUntilChanged, map, switchMap} from 'rxjs/operators';
@@ -51,6 +51,14 @@ import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/s
],
})
export class SongComponent implements OnInit {
private activatedRoute = inject(ActivatedRoute);
private songService = inject(SongService);
private fileService = inject(FileDataService);
private userService = inject(UserService);
private router = inject(Router);
private showService = inject(ShowService);
private showSongService = inject(ShowSongService);
public song$: Observable<Song | null> | null = null;
public files$: Observable<File[] | null> | null = null;
public user$: Observable<User | null> | null = null;
@@ -60,15 +68,9 @@ export class SongComponent implements OnInit {
public faFileCirclePlus = faFileCirclePlus;
public privateShows$ = this.showService.list$().pipe(map(show => show.filter(_ => !_.published).sort((a, b) => b.date.toMillis() - a.date.toMillis())));
public constructor(
private activatedRoute: ActivatedRoute,
private songService: SongService,
private fileService: FileDataService,
private userService: UserService,
private router: Router,
private showService: ShowService,
private showSongService: ShowSongService
) {
public constructor() {
const userService = this.userService;
this.user$ = userService.user$;
}

View File

@@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, inject} from '@angular/core';
import {UserService} from '../../../services/user/user.service';
import {Observable} from 'rxjs';
import {User} from '../../../services/user/user';
@@ -40,11 +40,11 @@ import {UsersComponent} from './users/users.component';
],
})
export class InfoComponent implements OnInit {
private userService = inject(UserService);
public user$: Observable<User | null> | null = null;
public faSignOut = faSignOutAlt;
public constructor(private userService: UserService) {}
public ngOnInit(): void {
this.user$ = this.userService.user$;
}

View File

@@ -1,4 +1,4 @@
import {Component, Input} from '@angular/core';
import {Component, Input, inject} from '@angular/core';
import {User} from '../../../../../services/user/user';
import {UserService} from '../../../../../services/user/user.service';
import {ROLE_TYPES} from '../../../../../services/user/roles';
@@ -20,6 +20,8 @@ import {RolePipe} from '../../role.pipe';
imports: [MatFormField, MatLabel, MatInput, ReactiveFormsModule, FormsModule, MatSelect, MatOption, MatIconButton, FaIconComponent, RolePipe],
})
export class UserComponent {
private userService = inject(UserService);
public id = '';
public name = '';
public roles: string[] = [];
@@ -27,8 +29,6 @@ export class UserComponent {
public edit = false;
public faClose = faTimes;
public constructor(private userService: UserService) {}
@Input()
public set user(value: User) {
this.id = value.id;

View File

@@ -1,4 +1,4 @@
import {Component} from '@angular/core';
import {Component, inject} from '@angular/core';
import {UserService} from '../../../../services/user/user.service';
import {Observable} from 'rxjs';
import {User} from '../../../../services/user/user';
@@ -14,9 +14,13 @@ import {SortByPipe} from '../../../../widget-modules/pipes/sort-by/sort-by.pipe'
imports: [CardComponent, UserComponent, AsyncPipe, SortByPipe],
})
export class UsersComponent {
private userService = inject(UserService);
public users$: Observable<User[]>;
public constructor(private userService: UserService) {
public constructor() {
const userService = this.userService;
this.users$ = userService.list$();
}
}

View File

@@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, inject} from '@angular/core';
import {ReactiveFormsModule, UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms';
import {Router, RouterLink} from '@angular/router';
import {UserService} from '../../../services/user/user.service';
@@ -17,6 +17,9 @@ import {AuthMessagePipe} from './auth-message.pipe';
imports: [LogoComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, MatButton, RouterLink, AuthMessagePipe],
})
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]),
@@ -25,13 +28,8 @@ export class LoginComponent implements OnInit {
public faSignIn = faSignInAlt;
public faNewUser = faUserPlus;
public constructor(
private userService: UserService,
private router: Router
) {}
public ngOnInit(): void {
this.form.reset;
this.form.reset();
}
public async onLogin(): Promise<void> {

View File

@@ -1,4 +1,4 @@
import {AfterViewInit, Component} from '@angular/core';
import {AfterViewInit, Component, inject} from '@angular/core';
import {Router} from '@angular/router';
import {UserService} from '../../../services/user/user.service';
@@ -8,13 +8,13 @@ import {UserService} from '../../../services/user/user.service';
styleUrls: ['./logout.component.less'],
})
export class LogoutComponent implements AfterViewInit {
public constructor(
private userService: UserService,
private router: Router
) {}
private userService = inject(UserService);
private router = inject(Router);
public async ngAfterViewInit(): Promise<void> {
public ngAfterViewInit(): void {
void (async () => {
await this.userService.logout();
await this.router.navigateByUrl('/');
})();
}
}

View File

@@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, inject} from '@angular/core';
import {ReactiveFormsModule, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms';
import {UserService} from '../../../services/user/user.service';
import {faUserPlus} from '@fortawesome/free-solid-svg-icons';
@@ -15,6 +15,9 @@ import {ButtonComponent} from '../../../widget-modules/components/button/button.
imports: [CardComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, ButtonRowComponent, ButtonComponent],
})
export class NewComponent implements OnInit {
private fb = inject(UntypedFormBuilder);
private userService = inject(UserService);
public form: UntypedFormGroup = this.fb.group({
email: new UntypedFormControl(null, [Validators.required, Validators.email]),
name: new UntypedFormControl(null, [Validators.required]),
@@ -22,11 +25,6 @@ export class NewComponent implements OnInit {
});
public faNewUser = faUserPlus;
public constructor(
private fb: UntypedFormBuilder,
private userService: UserService
) {}
public ngOnInit(): void {
this.form.reset();
}

View File

@@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, inject} from '@angular/core';
import {ReactiveFormsModule, UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms';
import {Router} from '@angular/router';
import {UserService} from '../../../services/user/user.service';
@@ -18,6 +18,9 @@ import {AuthMessagePipe} from '../login/auth-message.pipe';
imports: [CardComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, ButtonRowComponent, ButtonComponent, AuthMessagePipe],
})
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]),
});
@@ -25,11 +28,6 @@ export class PasswordComponent implements OnInit {
public errorMessage = '';
public faNewPassword = faWindowRestore;
public constructor(
public userService: UserService,
private router: Router
) {}
public ngOnInit(): void {
this.form.reset();
}

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {DbService} from './db.service';
import {firstValueFrom, Observable} from 'rxjs';
import {Config} from './config';
@@ -8,6 +8,8 @@ import {shareReplay} from 'rxjs/operators';
providedIn: 'root',
})
export class ConfigService {
private db = inject(DbService);
private readonly config$ = this.db.doc$<Config>('global/config').pipe(
shareReplay({
bufferSize: 1,
@@ -15,8 +17,6 @@ export class ConfigService {
})
);
public constructor(private db: DbService) {}
public get$ = (): Observable<Config | null> => this.config$;
public get = (): Promise<Config | null> => firstValueFrom(this.get$());
}

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {
addDoc,
collection,
@@ -76,7 +76,7 @@ export class DbDocument<T> {
providedIn: 'root',
})
export class DbService {
public constructor(private fs: Firestore) {}
private fs = inject(Firestore);
public col<T>(ref: CollectionPredicate<T>): DbCollection<T> {
return typeof ref === 'string' ? new DbCollection<T>(this.fs, ref) : ref;

View File

@@ -6,12 +6,9 @@ export const openFullscreen = () => {
}
try {
const promise = elem.requestFullscreen();
if (promise && typeof promise.catch === 'function') {
void promise.catch(() => {
void elem.requestFullscreen().catch(() => {
// Browser may reject when no user gesture is present. Keep app usable.
});
}
} catch {
// Some browsers may throw synchronously if fullscreen is not allowed.
}
@@ -19,11 +16,8 @@ export const openFullscreen = () => {
export const closeFullscreen = () => {
if (document.exitFullscreen) {
const promise = document.exitFullscreen();
if (promise && typeof promise.catch === 'function') {
void promise.catch(() => {
void document.exitFullscreen().catch(() => {
// Ignore; leaving fullscreen is a best-effort action.
});
}
}
};

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {DbService} from './db.service';
import {GlobalSettings} from './global-settings';
import {Observable} from 'rxjs';
@@ -8,6 +8,8 @@ import {shareReplay} from 'rxjs/operators';
providedIn: 'root',
})
export class GlobalSettingsService {
private db = inject(DbService);
private readonly settings$ = this.db.doc$<GlobalSettings>('global/static').pipe(
shareReplay({
bufferSize: 1,
@@ -15,8 +17,6 @@ export class GlobalSettingsService {
})
);
public constructor(private db: DbService) {}
public get get$(): Observable<GlobalSettings | null> {
return this.settings$;
}

View File

@@ -1,18 +1,16 @@
import {Directive, ElementRef, Input, OnInit, TemplateRef, ViewContainerRef} from '@angular/core';
import {Directive, ElementRef, Input, OnInit, TemplateRef, ViewContainerRef, inject} from '@angular/core';
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 currentUserId: string | null = null;
private iAppOwner: string | null = null;
public constructor(
private element: ElementRef,
private templateRef: TemplateRef<unknown>,
private viewContainer: ViewContainerRef,
private userService: UserService
) {}
@Input()
public set appOwner(value: string) {
this.iAppOwner = value;

View File

@@ -1,4 +1,4 @@
import {ChangeDetectorRef, Directive, ElementRef, Input, OnInit, TemplateRef, ViewContainerRef} from '@angular/core';
import {ChangeDetectorRef, Directive, ElementRef, Input, OnInit, TemplateRef, ViewContainerRef, inject} from '@angular/core';
import {roles} from './roles';
import {UserService} from './user.service';
import {User} from './user';
@@ -6,19 +6,17 @@ 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);
@Input() public appRole: roles[] = [];
private currentUser: User | null = null;
private loggedIn = false;
private currentViewState = false;
public constructor(
private element: ElementRef,
private templateRef: TemplateRef<unknown>,
private viewContainer: ViewContainerRef,
private userService: UserService,
private changeDetection: ChangeDetectorRef
) {}
public ngOnInit(): void {
combineLatest([this.userService.user$, this.userService.loggedIn$()]).subscribe(_ => {
this.currentUser = _[0];

View File

@@ -1,4 +1,4 @@
import {Component, Input} from '@angular/core';
import {Component, Input, inject} from '@angular/core';
import {UserService} from '../user.service';
import {map} from 'rxjs/operators';
import {Observable} from 'rxjs';
@@ -11,9 +11,9 @@ import {AsyncPipe} from '@angular/common';
imports: [AsyncPipe],
})
export class UserNameComponent {
public name$: Observable<string | null> | null = null;
private userService = inject(UserService);
public constructor(private userService: UserService) {}
public name$: Observable<string | null> | null = null;
@Input()
public set userId(id: string) {

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {Auth, authState, createUserWithEmailAndPassword, sendPasswordResetEmail, signInWithEmailAndPassword, signOut} from '@angular/fire/auth';
import {BehaviorSubject, firstValueFrom, Observable} from 'rxjs';
import {filter, map, shareReplay, switchMap, take, tap} from 'rxjs/operators';
@@ -20,18 +20,18 @@ export interface SongUsageMigrationResult {
providedIn: 'root',
})
export class UserService {
private auth = inject(Auth);
private db = inject(DbService);
private router = inject(Router);
private showDataService = inject(ShowDataService);
private showSongDataService = inject(ShowSongDataService);
public users$ = this.db.col$<User>('users').pipe(shareReplay({bufferSize: 1, refCount: true}));
private iUserId$ = new BehaviorSubject<string | null>(null);
private iUser$ = new BehaviorSubject<User | null>(null);
private userByIdCache = new Map<string, Observable<User | null>>();
public constructor(
private auth: Auth,
private db: DbService,
private router: Router,
private showDataService: ShowDataService,
private showSongDataService: ShowSongDataService
) {
public constructor() {
authState(this.auth)
.pipe(
filter(auth => !!auth),

View File

@@ -1,4 +1,4 @@
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
import {ChangeDetectionStrategy, Component, Input, inject} from '@angular/core';
import {ReactiveFormsModule, UntypedFormControl} from '@angular/forms';
import {filterSong} from '../../../services/filter.helper';
import {MatFormField, MatLabel, MatOption, MatSelect, MatSelectChange} from '@angular/material/select';
@@ -18,17 +18,15 @@ import {NgxMatSelectSearchModule} from 'ngx-mat-select-search';
imports: [MatFormField, MatSelect, MatOption, MatLabel, NgxMatSelectSearchModule, ReactiveFormsModule],
})
export class AddSongComponent {
private showSongService = inject(ShowSongService);
private showService = inject(ShowService);
@Input() public songs: Song[] | null = null;
@Input() public showSongs: ShowSong[] | null = null;
@Input() public show: Show | null = null;
@Input() public addedLive = false;
public filteredSongsControl = new UntypedFormControl();
public constructor(
private showSongService: ShowSongService,
private showService: ShowService
) {}
public filteredSongs(): Song[] {
if (!this.songs) return [];
const songs = this.songs

View File

@@ -1,4 +1,4 @@
import {Component} from '@angular/core';
import {Component, inject} from '@angular/core';
import {ActivatedRoute, Params, Router} from '@angular/router';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
@@ -9,12 +9,13 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms';
imports: [ReactiveFormsModule, FormsModule],
})
export class FilterComponent {
private router = inject(Router);
public value = '';
public constructor(
private router: Router,
activatedRoute: ActivatedRoute
) {
public constructor() {
const activatedRoute = inject(ActivatedRoute);
activatedRoute.queryParams.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} from '@angular/core';
import {ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, 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';
@@ -21,6 +21,10 @@ export type ChordMode = 'show' | 'hide' | 'onlyFirst';
imports: [MatIconButton, FaIconComponent],
})
export class SongTextComponent implements OnInit {
private textRenderingService = inject(TextRenderingService);
private elRef = inject<ElementRef<HTMLElement>>(ElementRef);
private cRef = inject(ChangeDetectorRef);
public sections: Section[] = [];
@Input() public header: string | null = null;
@Input() public index = -1;
@@ -35,12 +39,6 @@ export class SongTextComponent implements OnInit {
private iText = '';
private iTranspose: TransposeMode | null = null;
public constructor(
private textRenderingService: TextRenderingService,
private elRef: ElementRef<HTMLElement>,
private cRef: ChangeDetectorRef
) {}
@Input()
public set chordMode(value: ChordMode) {
this.iChordMode = value ?? 'hide';

View File

@@ -1,10 +1,9 @@
import {Directive, ElementRef, Input, OnInit} from '@angular/core';
import {Directive, ElementRef, Input, OnInit, inject} from '@angular/core';
@Directive({selector: '[appAutofocus]'})
export class AutofocusDirective implements OnInit {
private focus = true;
public constructor(private el: ElementRef<HTMLElement>) {}
private el = inject<ElementRef<HTMLElement>>(ElementRef);
@Input()
public set autofocus(condition: boolean) {

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {CanActivate, Router, UrlTree} from '@angular/router';
import {Auth, authState} from '@angular/fire/auth';
import {Observable} from 'rxjs';
@@ -8,10 +8,8 @@ import {map, take} from 'rxjs/operators';
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
public constructor(
private auth: Auth,
private router: Router
) {}
private auth = inject(Auth);
private router = inject(Router);
public canActivate(): Observable<boolean | UrlTree> {
return authState(this.auth).pipe(

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {Injectable, inject} from '@angular/core';
import {ActivatedRouteSnapshot, Router, UrlTree} from '@angular/router';
import {Observable} from 'rxjs';
import {UserService} from '../../services/user/user.service';
@@ -8,10 +8,8 @@ import {map} from 'rxjs/operators';
providedIn: 'root',
})
export class RoleGuard {
public constructor(
private userService: UserService,
private router: Router
) {}
private userService = inject(UserService);
private router = inject(Router);
public canActivate(next: ActivatedRouteSnapshot): Observable<boolean | UrlTree> {
const requiredRoles = next.data.requiredRoles as string[];

View File

@@ -1,5 +1,4 @@
import {Pipe, PipeTransform} from '@angular/core';
import {orderBy} from 'lodash';
@Pipe({
name: 'sortBy',
@@ -19,7 +18,29 @@ export class SortByPipe implements PipeTransform {
if (value.length <= 1) {
return value;
} // array with only one item
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
return orderBy(value, [column], [order]);
return [...value].sort((left, right) => {
const leftValue = this.getComparableValue(left, column);
const rightValue = this.getComparableValue(right, column);
const result = leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
return order === 'asc' ? result : -result;
});
}
private getComparableValue(item: unknown, column: string): string | number {
const value = (item as Record<string, unknown>)[column];
if (value instanceof Date) {
return value.getTime();
}
switch (typeof value) {
case 'number':
return value;
case 'string':
return value;
case 'boolean':
return value ? 1 : 0;
default:
return '';
}
}
}