From b6c2fe1645ac83b083554bc07a7ef6b1e4988395 Mon Sep 17 00:00:00 2001 From: benjamin Date: Mon, 9 Mar 2026 21:50:49 +0100 Subject: [PATCH] migrate firebase db --- src/app/modules/brand/brand.component.html | 2 +- src/app/modules/guest/guest-show.ts | 3 +- .../monitor/monitor.component.html | 6 +- .../presentation/monitor/monitor.component.ts | 12 ++- .../presentation/remote/remote.component.less | 3 +- src/app/modules/shows/edit/edit.component.ts | 3 +- .../shows/services/show-data.service.ts | 9 +- .../shows/services/show-song-data.service.ts | 8 +- src/app/modules/shows/services/show.ts | 3 +- .../songs/services/song-data.service.spec.ts | 12 +-- .../modules/songs/services/song.service.ts | 3 +- src/app/modules/songs/services/song.ts | 3 +- src/app/services/db.service.spec.ts | 8 +- src/app/services/db.service.ts | 92 ++++++++++++++++--- src/app/services/fullscreen.ts | 22 ++++- src/app/services/user/user.service.ts | 4 +- .../components/card/card.component.less | 4 +- src/main.ts | 8 +- 18 files changed, 143 insertions(+), 62 deletions(-) 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 @@
- +
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 @@
-
+
@@ -29,7 +29,7 @@
!!_), - 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(), ],