diff --git a/src/app/modules/brand/brand.component.html b/src/app/modules/brand/brand.component.html
index 9c1d6ef..f77f2d7 100644
--- a/src/app/modules/brand/brand.component.html
+++ b/src/app/modules/brand/brand.component.html
@@ -1,4 +1,4 @@
-
© 2019 - 2024 - Benjamin Ifland
+
© 2019 - 2026 - Benjamin Ifland
diff --git a/src/app/modules/guest/guest-show.ts b/src/app/modules/guest/guest-show.ts
index 35b9dd7..726869f 100644
--- a/src/app/modules/guest/guest-show.ts
+++ b/src/app/modules/guest/guest-show.ts
@@ -1,6 +1,5 @@
-import firebase from 'firebase/compat/app';
import {Song} from '../songs/services/song';
-import Timestamp = firebase.firestore.Timestamp;
+import {Timestamp} from '@angular/fire/firestore';
export interface GuestShow {
id: string;
diff --git a/src/app/modules/presentation/monitor/monitor.component.html b/src/app/modules/presentation/monitor/monitor.component.html
index 4ed07f8..cdb2ba4 100644
--- a/src/app/modules/presentation/monitor/monitor.component.html
+++ b/src/app/modules/presentation/monitor/monitor.component.html
@@ -1,5 +1,5 @@
-
+
!!_),
- map(_ => _),
- map(_ => _.currentShow),
+ filter((settings): settings is NonNullable => !!settings),
+ map(settings => settings.currentShow),
+ filter((showId): showId is string => !!showId),
distinctUntilChanged(),
tap(_ => (this.currentShowId = _)),
takeUntil(this.destroy$)
@@ -92,6 +91,9 @@ export class MonitorComponent implements OnInit, OnDestroy {
map(show => ({showId: show.id, presentationSongId: show.presentationSongId})),
distinctUntilChanged((a, b) => a.showId === b.showId && a.presentationSongId === b.presentationSongId),
tap(({presentationSongId}) => {
+ if (presentationSongId === 'title' || presentationSongId === 'dynamicText' || !presentationSongId) {
+ this.song = null;
+ }
if (this.songId !== presentationSongId) {
this.songId = 'empty';
}
diff --git a/src/app/modules/presentation/remote/remote.component.less b/src/app/modules/presentation/remote/remote.component.less
index 2221556..2df8b0b 100644
--- a/src/app/modules/presentation/remote/remote.component.less
+++ b/src/app/modules/presentation/remote/remote.component.less
@@ -8,7 +8,6 @@
margin-bottom: 10px;
box-sizing: border-box;
color: var(--text);
- border: 1px solid var(--surface-border);
@media screen and (max-width: 860px) {
width: 100vw;
@@ -46,7 +45,7 @@
overflow: hidden;
transition: var(--transition);
cursor: pointer;
- outline: 1px solid var(--divider);
+ outline: 1px solid var(--surface-muted);
&:hover {
outline: 1px solid var(--primary-hover);
diff --git a/src/app/modules/shows/edit/edit.component.ts b/src/app/modules/shows/edit/edit.component.ts
index 77961b6..85b905c 100644
--- a/src/app/modules/shows/edit/edit.component.ts
+++ b/src/app/modules/shows/edit/edit.component.ts
@@ -7,7 +7,7 @@ import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/
import {ActivatedRoute, Router} from '@angular/router';
import {faSave} from '@fortawesome/free-solid-svg-icons';
import {map, switchMap} from 'rxjs/operators';
-import firebase from 'firebase/compat/app';
+import {Timestamp} from '@angular/fire/firestore';
import {CardComponent} from '../../../widget-modules/components/card/card.component';
import {MatFormField, MatLabel, MatSuffix} from '@angular/material/form-field';
import {MatSelect} from '@angular/material/select';
@@ -18,7 +18,6 @@ import {MatDatepicker, MatDatepickerInput, MatDatepickerToggle} from '@angular/m
import {ButtonRowComponent} from '../../../widget-modules/components/button-row/button-row.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/show-type.pipe';
-import Timestamp = firebase.firestore.Timestamp;
@Component({
selector: 'app-edit',
diff --git a/src/app/modules/shows/services/show-data.service.ts b/src/app/modules/shows/services/show-data.service.ts
index 1f16cad..6f8afcd 100644
--- a/src/app/modules/shows/services/show-data.service.ts
+++ b/src/app/modules/shows/services/show-data.service.ts
@@ -3,8 +3,7 @@ import {Observable} from 'rxjs';
import {DbService} from '../../../services/db.service';
import {Show} from './show';
import {map, shareReplay} from 'rxjs/operators';
-import {QueryFn} from '@angular/fire/compat/firestore/interfaces';
-import firebase from 'firebase/compat/app';
+import {orderBy, QueryConstraint, Timestamp, where} from '@angular/fire/firestore';
@Injectable({
providedIn: 'root',
@@ -28,11 +27,11 @@ export class ShowDataService {
const startDate = new Date();
startDate.setHours(0, 0, 0, 0);
startDate.setDate(startDate.getDate() - lastMonths * 30);
- const startTimestamp = firebase.firestore.Timestamp.fromDate(startDate);
+ const startTimestamp = Timestamp.fromDate(startDate);
- const queryFn: QueryFn = ref => ref.where('published', '==', true).where('date', '>=', startTimestamp).orderBy('date', 'desc');
+ const queryConstraints: QueryConstraint[] = [where('published', '==', true), where('date', '>=', startTimestamp), orderBy('date', 'desc')];
- return this.dbService.col$(this.collection, queryFn).pipe(
+ return this.dbService.col$(this.collection, queryConstraints).pipe(
map(shows => shows.filter(show => !show.archived)),
shareReplay({
bufferSize: 1,
diff --git a/src/app/modules/shows/services/show-song-data.service.ts b/src/app/modules/shows/services/show-song-data.service.ts
index d7e29e0..dc8a415 100644
--- a/src/app/modules/shows/services/show-song-data.service.ts
+++ b/src/app/modules/shows/services/show-song-data.service.ts
@@ -2,7 +2,7 @@ import {Injectable} from '@angular/core';
import {DbService} from '../../../services/db.service';
import {Observable} from 'rxjs';
import {ShowSong} from './show-song';
-import {QueryFn} from '@angular/fire/compat/firestore/interfaces';
+import {QueryConstraint} from '@angular/fire/firestore';
import {shareReplay} from 'rxjs/operators';
@Injectable({
@@ -15,9 +15,9 @@ export class ShowSongDataService {
public constructor(private dbService: DbService) {}
- public list$ = (showId: string, queryFn?: QueryFn): Observable => {
- if (queryFn) {
- return this.dbService.col$(`${this.collection}/${showId}/${this.subCollection}`, queryFn);
+ public list$ = (showId: string, queryConstraints?: QueryConstraint[]): Observable => {
+ if (queryConstraints && queryConstraints.length > 0) {
+ return this.dbService.col$(`${this.collection}/${showId}/${this.subCollection}`, queryConstraints);
}
const cached = this.listCache.get(showId);
diff --git a/src/app/modules/shows/services/show.ts b/src/app/modules/shows/services/show.ts
index 452a124..9425eb3 100644
--- a/src/app/modules/shows/services/show.ts
+++ b/src/app/modules/shows/services/show.ts
@@ -1,5 +1,4 @@
-import firebase from 'firebase/compat/app';
-import Timestamp = firebase.firestore.Timestamp;
+import {Timestamp} from '@angular/fire/firestore';
export type PresentationBackground = 'none' | 'blue' | 'green' | 'leder' | 'praise' | 'bible';
diff --git a/src/app/modules/songs/services/song-data.service.spec.ts b/src/app/modules/songs/services/song-data.service.spec.ts
index 7664796..a1ef7a8 100644
--- a/src/app/modules/songs/services/song-data.service.spec.ts
+++ b/src/app/modules/songs/services/song-data.service.spec.ts
@@ -1,24 +1,20 @@
import {TestBed} from '@angular/core/testing';
import {SongDataService} from './song-data.service';
-import {AngularFirestore} from '@angular/fire/compat/firestore';
import {firstValueFrom, of} from 'rxjs';
+import {DbService} from '../../../services/db.service';
describe('SongDataService', () => {
const songs = [{title: 'title1'}];
- const angularFirestoreCollection = {
- valueChanges: () => of(songs),
- };
-
- const mockAngularFirestore = {
- collection: () => angularFirestoreCollection,
+ const mockDbService = {
+ col$: () => of(songs),
};
beforeEach(
() =>
void TestBed.configureTestingModule({
- providers: [{provide: AngularFirestore, useValue: mockAngularFirestore}],
+ providers: [{provide: DbService, useValue: mockDbService}],
})
);
diff --git a/src/app/modules/songs/services/song.service.ts b/src/app/modules/songs/services/song.service.ts
index f139de9..7eb6821 100644
--- a/src/app/modules/songs/services/song.service.ts
+++ b/src/app/modules/songs/services/song.service.ts
@@ -3,8 +3,7 @@ import {firstValueFrom, Observable} from 'rxjs';
import {Song} from './song';
import {SongDataService} from './song-data.service';
import {UserService} from '../../../services/user/user.service';
-import firebase from 'firebase/compat/app';
-import Timestamp = firebase.firestore.Timestamp;
+import {Timestamp} from '@angular/fire/firestore';
// declare let importCCLI: any;
diff --git a/src/app/modules/songs/services/song.ts b/src/app/modules/songs/services/song.ts
index 6b30ae6..4412290 100644
--- a/src/app/modules/songs/services/song.ts
+++ b/src/app/modules/songs/services/song.ts
@@ -1,6 +1,5 @@
-import firebase from 'firebase/compat/app';
import {SongLegalOwner, SongLegalType, SongStatus, SongType} from './song.service';
-import Timestamp = firebase.firestore.Timestamp;
+import {Timestamp} from '@angular/fire/firestore';
export interface Song {
id: string;
diff --git a/src/app/services/db.service.spec.ts b/src/app/services/db.service.spec.ts
index 741beb2..634de52 100644
--- a/src/app/services/db.service.spec.ts
+++ b/src/app/services/db.service.spec.ts
@@ -1,9 +1,15 @@
import {TestBed} from '@angular/core/testing';
+import {Firestore} from '@angular/fire/firestore';
import {DbService} from './db.service';
describe('DbService', () => {
- beforeEach(() => void TestBed.configureTestingModule({}));
+ beforeEach(
+ () =>
+ void TestBed.configureTestingModule({
+ providers: [{provide: Firestore, useValue: {}}],
+ })
+ );
it('should be created', () => {
const service: DbService = TestBed.inject(DbService);
diff --git a/src/app/services/db.service.ts b/src/app/services/db.service.ts
index cf70945..39db2c7 100644
--- a/src/app/services/db.service.ts
+++ b/src/app/services/db.service.ts
@@ -1,24 +1,89 @@
import {Injectable} from '@angular/core';
-import {AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument} from '@angular/fire/compat/firestore';
+import {
+ addDoc,
+ collection,
+ collectionData,
+ CollectionReference,
+ deleteDoc,
+ doc,
+ docData,
+ DocumentData,
+ DocumentReference,
+ Firestore,
+ query,
+ QueryConstraint,
+ setDoc,
+ updateDoc,
+ WithFieldValue,
+} from '@angular/fire/firestore';
import {Observable} from 'rxjs';
-import {QueryFn} from '@angular/fire/compat/firestore/interfaces';
import {map} from 'rxjs/operators';
-type CollectionPredicate = string | AngularFirestoreCollection;
-type DocumentPredicate = string | AngularFirestoreDocument;
+type CollectionPredicate = string | DbCollection;
+type DocumentPredicate = string | DbDocument;
+
+export class DbCollection {
+ public constructor(
+ private readonly fs: Firestore,
+ private readonly path: string
+ ) {}
+
+ public add(data: Partial): Promise> {
+ return addDoc(this.ref as CollectionReference, data as WithFieldValue);
+ }
+
+ public valueChanges(options?: {idField?: string}): Observable {
+ return collectionData(this.ref, options as {idField?: never}) as Observable;
+ }
+
+ private get ref(): CollectionReference {
+ return collection(this.fs, this.path);
+ }
+}
+
+export class DbDocument {
+ public constructor(
+ private readonly fs: Firestore,
+ private readonly path: string
+ ) {}
+
+ public set(data: Partial): Promise {
+ return setDoc(this.ref as DocumentReference, data as WithFieldValue);
+ }
+
+ public update(data: Partial): Promise {
+ return updateDoc(this.ref, data as Partial);
+ }
+
+ public delete(): Promise {
+ return deleteDoc(this.ref);
+ }
+
+ public collection(subPath: string): DbCollection {
+ return new DbCollection(this.fs, `${this.path}/${subPath}`);
+ }
+
+ public valueChanges(options?: {idField?: string}): Observable<(NonNullable & {id?: string}) | undefined> {
+ return docData(this.ref as DocumentReference, options as {idField?: never}) as Observable<(NonNullable & {id?: string}) | undefined>;
+ }
+
+ private get ref(): DocumentReference {
+ return doc(this.fs, this.path);
+ }
+}
@Injectable({
providedIn: 'root',
})
export class DbService {
- public constructor(private afs: AngularFirestore) {}
+ public constructor(private fs: Firestore) {}
- public col(ref: CollectionPredicate, queryFn?: QueryFn): AngularFirestoreCollection {
- return typeof ref === 'string' ? this.afs.collection(ref, queryFn) : ref;
+ public col(ref: CollectionPredicate): DbCollection {
+ return typeof ref === 'string' ? new DbCollection(this.fs, ref) : ref;
}
- public doc(ref: DocumentPredicate): AngularFirestoreDocument {
- return typeof ref === 'string' ? this.afs.doc(ref) : ref;
+ public doc(ref: DocumentPredicate): DbDocument {
+ return typeof ref === 'string' ? new DbDocument(this.fs, ref) : ref;
}
public doc$(ref: DocumentPredicate): Observable<(NonNullable & {id?: string}) | null> {
@@ -27,7 +92,12 @@ export class DbService {
.pipe(map(_ => (_ ? _ : null)));
}
- public col$(ref: CollectionPredicate, queryFn?: QueryFn): Observable {
- return this.col(ref, queryFn).valueChanges({idField: 'id'});
+ public col$(ref: CollectionPredicate, queryConstraints: QueryConstraint[] = []): Observable {
+ if (typeof ref !== 'string' || queryConstraints.length === 0) {
+ return this.col(ref).valueChanges({idField: 'id'});
+ }
+
+ const q = query(collection(this.fs, ref), ...queryConstraints);
+ return collectionData(q, {idField: 'id'}) as Observable;
}
}
diff --git a/src/app/services/fullscreen.ts b/src/app/services/fullscreen.ts
index aa675d5..3620834 100644
--- a/src/app/services/fullscreen.ts
+++ b/src/app/services/fullscreen.ts
@@ -1,13 +1,29 @@
const elem = document.documentElement;
export const openFullscreen = () => {
- if (elem.requestFullscreen) {
- void elem.requestFullscreen();
+ if (!elem.requestFullscreen || !document.fullscreenEnabled) {
+ return;
+ }
+
+ try {
+ const promise = elem.requestFullscreen();
+ if (promise && typeof promise.catch === 'function') {
+ void promise.catch(() => {
+ // Browser may reject when no user gesture is present. Keep app usable.
+ });
+ }
+ } catch {
+ // Some browsers may throw synchronously if fullscreen is not allowed.
}
};
export const closeFullscreen = () => {
if (document.exitFullscreen) {
- void document.exitFullscreen();
+ const promise = document.exitFullscreen();
+ if (promise && typeof promise.catch === 'function') {
+ void promise.catch(() => {
+ // Ignore; leaving fullscreen is a best-effort action.
+ });
+ }
}
};
diff --git a/src/app/services/user/user.service.ts b/src/app/services/user/user.service.ts
index 7e04b66..b4c3632 100644
--- a/src/app/services/user/user.service.ts
+++ b/src/app/services/user/user.service.ts
@@ -8,7 +8,7 @@ import {environment} from '../../../environments/environment';
import {Router} from '@angular/router';
import {ShowDataService} from '../../modules/shows/services/show-data.service';
import {ShowSongDataService} from '../../modules/shows/services/show-song-data.service';
-import firebase from 'firebase/compat/app';
+import {increment} from '@angular/fire/firestore';
export interface SongUsageMigrationResult {
usersProcessed: number;
@@ -159,7 +159,7 @@ export class UserService {
if (!user) return null;
await this.db.doc('users/' + user.id).update({
- [`songUsage.${songId}`]: firebase.firestore.FieldValue.increment(direction),
+ [`songUsage.${songId}`]: increment(direction),
});
}
diff --git a/src/app/widget-modules/components/card/card.component.less b/src/app/widget-modules/components/card/card.component.less
index b041ddb..e23f2c9 100644
--- a/src/app/widget-modules/components/card/card.component.less
+++ b/src/app/widget-modules/components/card/card.component.less
@@ -5,12 +5,12 @@
border-radius: 8px;
background: var(--surface);
backdrop-filter: blur(15px);
- border: 1px solid var(--surface-border);
+ border: none;
overflow: hidden;
width: 800px;
position: relative;
color: var(--text);
- padding-bottom: 5px;
+ padding: 5px 0;
@media screen and (max-width: 860px) {
width: 100vw;
diff --git a/src/main.ts b/src/main.ts
index 885bc1b..296a640 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -7,12 +7,11 @@ import {provideAnimations} from '@angular/platform-browser/animations';
import {AppRoutingModule} from './app/app-routing.module';
import {ServiceWorkerModule} from '@angular/service-worker';
import {AngularFireModule} from '@angular/fire/compat';
-import {AngularFirestoreModule} from '@angular/fire/compat/firestore';
import {AngularFireStorageModule} from '@angular/fire/compat/storage';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {AppComponent} from './app/app.component';
-import {provideFirebaseApp, initializeApp} from '@angular/fire/app';
-import {provideFirestore, getFirestore} from '@angular/fire/firestore';
+import {getApp, initializeApp, provideFirebaseApp} from '@angular/fire/app';
+import {initializeFirestore, persistentLocalCache, persistentMultipleTabManager, provideFirestore} from '@angular/fire/firestore';
import {getAuth, provideAuth} from '@angular/fire/auth';
import {UserService} from './app/services/user/user.service';
@@ -37,13 +36,12 @@ bootstrapApplication(AppComponent, {
enabled: environment.production,
}),
AngularFireModule.initializeApp(environment.firebase),
- AngularFirestoreModule.enablePersistence({synchronizeTabs: true}),
AngularFireStorageModule,
FontAwesomeModule
),
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideAuth(() => getAuth()),
- provideFirestore(() => getFirestore()),
+ provideFirestore(() => initializeFirestore(getApp(), {localCache: persistentLocalCache({tabManager: persistentMultipleTabManager()})})),
{provide: MAT_DATE_LOCALE, useValue: 'de-DE'},
provideAnimations(),
],