diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-overview/checking-overview.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-overview/checking-overview.component.spec.ts index 37ffe4e591b..ff1d4851781 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-overview/checking-overview.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-overview/checking-overview.component.spec.ts @@ -1,6 +1,7 @@ import { Location } from '@angular/common'; import { DebugElement, NgZone } from '@angular/core'; import { ComponentFixture, discardPeriodicTasks, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; import { MatExpansionPanel } from '@angular/material/expansion'; import { By } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; @@ -26,7 +27,7 @@ import { createTestProjectUserConfig } from 'realtime-server/lib/esm/scripturefo import { TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-info'; import { VerseRefData } from 'realtime-server/lib/esm/scriptureforge/models/verse-ref-data'; import { of } from 'rxjs'; -import { anything, mock, resetCalls, verify, when } from 'ts-mockito'; +import { anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; import { DialogService } from 'xforge-common/dialog.service'; import { NoticeService } from 'xforge-common/notice.service'; import { OnlineStatusService } from 'xforge-common/online-status.service'; @@ -51,6 +52,7 @@ import { CheckingOverviewComponent } from './checking-overview.component'; const mockedActivatedRoute = mock(ActivatedRoute); const mockedDialogService = mock(DialogService); +const mockedImportQuestionsDialogRef = mock(MatDialogRef); const mockedNoticeService = mock(NoticeService); const mockedProjectService = mock(SFProjectService); const mockedQuestionsService = mock(CheckingQuestionsService); @@ -88,13 +90,13 @@ describe('CheckingOverviewComponent', () => { })); it('should not display loading if user is offline', fakeAsync(() => { - const env = new TestEnvironment(); - env.testOnlineStatusService.setIsOnline(false); - tick(); - env.fixture.detectChanges(); + const env = new TestEnvironment(false); + env.onlineStatus = false; + expect(env.component.showQuestionsLoadingMessage).toBe(false); + expect(env.component.showNoQuestionsMessage).toBe(true); + env.waitForQuestions(); expect(env.loadingQuestionsLabel).toBeNull(); expect(env.noQuestionsLabel).not.toBeNull(); - env.waitForQuestions(); })); it('should not display "Add question" button for community checker', fakeAsync(() => { @@ -425,10 +427,7 @@ describe('CheckingOverviewComponent', () => { await questionDoc.submitJson0Op(op => { op.set(d => d.isArchived, false); }); - env.testOnlineStatusService.setIsOnline(false); - env.fixture.detectChanges(); - tick(); - env.fixture.detectChanges(); + env.onlineStatus = false; expect(env.loadingArchivedQuestionsLabel).toBeNull(); expect(env.noArchivedQuestionsLabel).not.toBeNull(); @@ -985,6 +984,10 @@ class TestEnvironment { projectDoc.submitJson0Op(op => op.set(p => p.texts[textIndex].chapters[chapterIndex].hasAudio, false), false); } ); + when(mockedImportQuestionsDialogRef.afterClosed()).thenReturn(of(undefined)); + when(mockedDialogService.openMatDialog(ImportQuestionsDialogComponent, anything())).thenReturn( + instance(mockedImportQuestionsDialogRef) + ); this.setCurrentUser(this.adminUser); this.testOnlineStatusService.setIsOnline(true); @@ -1112,6 +1115,8 @@ class TestEnvironment { this.testOnlineStatusService.setIsOnline(isOnline); tick(); this.fixture.detectChanges(); + tick(); + this.fixture.detectChanges(); } get warningSomeActionsUnavailableOffline(): DebugElement { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-overview/checking-overview.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-overview/checking-overview.component.ts index 47b67641472..85eac84898a 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-overview/checking-overview.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-overview/checking-overview.component.ts @@ -1,5 +1,6 @@ import { NgClass } from '@angular/common'; -import { Component, DestroyRef, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, OnDestroy, OnInit } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButton, MatIconButton, MatMiniFabButton } from '@angular/material/button'; import { MatCard, MatCardContent } from '@angular/material/card'; import { @@ -20,8 +21,8 @@ import { Operation } from 'realtime-server/lib/esm/common/models/project-rights' import { SF_PROJECT_RIGHTS, SFProjectDomain } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-rights'; import { Chapter, TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-info'; import { toVerseRef, VerseRefData } from 'realtime-server/lib/esm/scriptureforge/models/verse-ref-data'; -import { asyncScheduler, merge, Subscription } from 'rxjs'; -import { map, tap, throttleTime } from 'rxjs/operators'; +import { asyncScheduler, combineLatest, merge, Subscription } from 'rxjs'; +import { map, startWith, tap, throttleTime } from 'rxjs/operators'; import { DataLoadingComponent } from 'xforge-common/data-loading-component'; import { DialogService } from 'xforge-common/dialog.service'; import { DonutChartComponent } from 'xforge-common/donut-chart/donut-chart.component'; @@ -73,22 +74,26 @@ import { QuestionDialogService } from '../question-dialog/question-dialog.servic MatCard, MatCardContent, L10nNumberPipe - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class CheckingOverviewComponent extends DataLoadingComponent implements OnInit, OnDestroy { texts: TextInfo[] = []; projectId?: string; + questionsLoaded: boolean = false; private questionDocs = new Map(); private textsByBookId?: TextsByBookId; private projectDoc?: SFProjectProfileDoc; private dataChangesSub?: Subscription; + private questionsLoadedSub?: Subscription; private projectUserConfigDoc?: SFProjectUserConfigDoc; private questionsQuery?: RealtimeQuery; constructor( private readonly destroyRef: DestroyRef, private readonly activatedRoute: ActivatedRoute, + private readonly changeDetector: ChangeDetectorRef, private readonly dialogService: DialogService, noticeService: NoticeService, readonly i18n: I18nService, @@ -207,11 +212,6 @@ export class CheckingOverviewComponent extends DataLoadingComponent implements O return this.questionsQuery.docs.filter(qd => qd.data != null && !qd.data.isArchived); } - private get questionsLoaded(): boolean { - // if the user is offline, 'ready' will never be true, but the query will still return the offline docs - return !this.onlineStatusService.isOnline || this.questionsQuery?.ready === true; - } - ngOnInit(): void { let projectDocPromise: Promise; const projectId$ = this.activatedRoute.params.pipe( @@ -223,6 +223,7 @@ export class CheckingOverviewComponent extends DataLoadingComponent implements O ); projectId$.pipe(quietTakeUntilDestroyed(this.destroyRef)).subscribe(async projectId => { this.loadingStarted(); + this.changeDetector.markForCheck(); this.projectId = projectId; try { this.projectDoc = await projectDocPromise; @@ -238,14 +239,13 @@ export class CheckingOverviewComponent extends DataLoadingComponent implements O this.loadingFinished(); } - if (this.dataChangesSub != null) { - this.dataChangesSub.unsubscribe(); - } + this.dataChangesSub?.unsubscribe(); this.dataChangesSub = merge( this.projectDoc.remoteChanges$, this.questionsQuery.remoteChanges$, this.questionsQuery.localChanges$ ) + .pipe(quietTakeUntilDestroyed(this.destroyRef)) // TODO Find a better solution than merely throttling remote changes .pipe(throttleTime(1000, asyncScheduler, { leading: true, trailing: true })) .subscribe(() => { @@ -255,11 +255,23 @@ export class CheckingOverviewComponent extends DataLoadingComponent implements O } } }); + + this.questionsLoadedSub?.unsubscribe(); + this.questionsLoadedSub = combineLatest([ + this.onlineStatusService.onlineStatus$.pipe(startWith(this.onlineStatusService.isOnline)), + this.questionsQuery.ready$.pipe(startWith(this.questionsQuery?.ready)) + ]) + .pipe(quietTakeUntilDestroyed(this.destroyRef)) + .pipe(map(([isOnline, ready]) => !isOnline || ready === true)) + .subscribe(loaded => { + // if the user is offline, 'ready' will never be true, but the query will still return the offline docs + this.questionsLoaded = loaded; + this.changeDetector.markForCheck(); + }); }); } ngOnDestroy(): void { - this.dataChangesSub?.unsubscribe(); this.questionsQuery?.dispose(); } @@ -356,6 +368,8 @@ export class CheckingOverviewComponent extends DataLoadingComponent implements O if (questionDoc.data!.isArchived !== archive) this.setQuestionArchiveStatus(questionDoc, archive); } } + + this.changeDetector.markForCheck(); } } @@ -364,6 +378,8 @@ export class CheckingOverviewComponent extends DataLoadingComponent implements O for (const questionDoc of this.getQuestionDocs(this.getTextDocIdType(text.bookNum, chapter.number), !archive)) { if (questionDoc.data!.isArchived !== archive) this.setQuestionArchiveStatus(questionDoc, archive); } + + this.changeDetector.markForCheck(); } } @@ -454,7 +470,15 @@ export class CheckingOverviewComponent extends DataLoadingComponent implements O userId: this.userService.currentUserId, textsByBookId: this.textsByBookId }; - this.dialogService.openMatDialog(ImportQuestionsDialogComponent, { data }); + this.changeDetector.detach(); + const dialogRef = this.dialogService.openMatDialog(ImportQuestionsDialogComponent, { data }); + dialogRef + .afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.changeDetector.reattach(); + this.changeDetector.markForCheck(); + }); } getBookName(text: TextInfo): string { @@ -511,6 +535,8 @@ export class CheckingOverviewComponent extends DataLoadingComponent implements O for (const questionDoc of this.questionsQuery.docs) { this.addQuestionDoc(questionDoc); } + + this.changeDetector.markForCheck(); } private addQuestionDoc(questionDoc: QuestionDoc): void { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts index 5785a086ba5..63cede9b2be 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts @@ -455,15 +455,14 @@ describe('CheckingComponent', () => { // Question 5 has been stored as the question to start at expect(env.component.questionsList!.activeQuestionDoc!.data!.dataId).toBe('q5Id'); expect(env.questions.length).toEqual(16); - - let question = env.selectQuestion(1); // Trigger route change that should happen when activating question from a different book/chapter env.setBookChapter('MAT', 1); + let question = env.selectQuestion(1); expect(env.getQuestionText(question)).toBe('Matthew question relating to chapter 1'); expect(await env.getCurrentBookAndChapter()).toBe('Matthew 1'); - question = env.selectQuestion(16); env.setBookChapter('JHN', 2); + question = env.selectQuestion(16); expect(env.getQuestionText(question)).toBe('John 2'); expect(await env.getCurrentBookAndChapter()).toBe('John 2'); env.waitForQuestionTimersToComplete(); @@ -495,6 +494,7 @@ describe('CheckingComponent', () => { env.setBookChapter('JHN', 2); env.fixture.detectChanges(); expect(env.component.questionsList!.activeQuestionDoc!.data!.dataId).toBe('q15Id'); + tick(); flush(); discardPeriodicTasks(); })); @@ -654,6 +654,7 @@ describe('CheckingComponent', () => { expect(env.component.answersPanel?.getFileSource(questionDoc.data?.audioUrl)).toBeDefined(); verify(mockedFileService.findOrUpdateCache(FileType.Audio, 'questions', questionId, 'anAudioFile.mp3')).once(); env.waitForAudioPlayer(); + tick(100); flush(); discardPeriodicTasks(); })); @@ -1731,6 +1732,7 @@ describe('CheckingComponent', () => { expect(env.scriptureText).toBe('John 2:2-5'); env.clickButton(env.saveAnswerButton); expect(env.getAnswerScriptureText(0)).toBe('…The selected text(John 2:2-5)'); + tick(100); flush(); discardPeriodicTasks(); })); @@ -2237,6 +2239,7 @@ describe('CheckingComponent', () => { it('update answer audio cache after save', fakeAsync(() => { const env = new TestEnvironment({ user: CHECKER_USER }); const questionDoc = spy(env.getQuestionDoc('q6Id')); + verify(questionDoc!.updateAnswerFileCache()).never(); env.selectQuestion(6); env.clickButton(env.getAnswerEditButton(0)); env.waitForSliderUpdate(); @@ -2298,6 +2301,7 @@ describe('CheckingComponent', () => { expect(env.getExportAnswerButton(buttonIndex).classes['status-exportable']).toBe(true); const questionDoc = env.component.questionsList!.activeQuestionDoc!; expect(questionDoc.data!.answers[0].status).toEqual(AnswerStatus.Exportable); + tick(100); flush(); discardPeriodicTasks(); })); @@ -2312,6 +2316,7 @@ describe('CheckingComponent', () => { expect(env.getResolveAnswerButton(buttonIndex).classes['status-resolved']).toBe(true); const questionDoc = env.component.questionsList!.activeQuestionDoc!; expect(questionDoc.data!.answers[0].status).toEqual(AnswerStatus.Resolved); + tick(100); flush(); discardPeriodicTasks(); })); @@ -2341,6 +2346,7 @@ describe('CheckingComponent', () => { expect(env.getExportAnswerButton(buttonIndex).classes['status-exportable']).toBeUndefined(); questionDoc = env.component.questionsList!.activeQuestionDoc!; expect(questionDoc.data!.answers[0].status).toEqual(AnswerStatus.None); + tick(100); flush(); discardPeriodicTasks(); })); @@ -2400,6 +2406,7 @@ describe('CheckingComponent', () => { env.waitForSliderUpdate(); tick(); env.fixture.detectChanges(); + tick(); segment = env.getVerse(1, 3); expect(segment.classList.contains('question-segment')).toBe(false); expect(segment.classList.contains('highlight-segment')).toBe(false); @@ -2608,7 +2615,7 @@ describe('CheckingComponent', () => { discardPeriodicTasks(); })); - it('notifies admin if chapter audio is absent and hide scripture text is enabled', fakeAsync(() => { + it('notifies admin if chapter audio is absent and hide scripture text is enabled', fakeAsync(async () => { const env = new TestEnvironment({ user: ADMIN_USER, projectBookRoute: 'MAT', @@ -2628,8 +2635,9 @@ describe('CheckingComponent', () => { op.set(p => p.texts[matTextIndex].chapters[0].hasAudio, true); }); }); + env.waitForQuestionTimersToComplete(); - env.component.addAudioTimingData(); + await env.component.addAudioTimingData(); env.waitForQuestionTimersToComplete(); env.fixture.detectChanges(); @@ -3454,6 +3462,9 @@ class TestEnvironment { questionDoc.submitJson0Op(op => op.set(q => q.answers[answerIndex].deleted, true)); this.fixture.detectChanges(); + tick(); + this.fixture.detectChanges(); + tick(); } setQuestionFilter(filter: QuestionFilter): void { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.ts index 529d7cd726c..434367b07ee 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.ts @@ -1,7 +1,17 @@ import { Dir } from '@angular/cdk/bidi'; import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { AsyncPipe, KeyValuePipe, NgClass } from '@angular/common'; -import { AfterViewInit, Component, DestroyRef, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef, + ElementRef, + OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; import { MatButton, MatIconButton } from '@angular/material/button'; import { MatIcon } from '@angular/material/icon'; import { MatListSubheaderCssMatStyler, MatSelectionList } from '@angular/material/list'; @@ -97,7 +107,8 @@ interface Summary { DonutChartComponent, AsyncPipe, KeyValuePipe - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class CheckingComponent extends DataLoadingComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild('answerPanelContainer') set answersPanelElement(answersPanelContainerElement: ElementRef) { @@ -192,6 +203,7 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A private _showScriptureAudioPlayer: boolean = false; constructor( + private readonly changeDetector: ChangeDetectorRef, private readonly destroyRef: DestroyRef, private readonly activatedRoute: ActivatedRoute, private readonly projectService: SFProjectService, @@ -252,6 +264,7 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A if (!this._isDrawerPermanent) { this.setQuestionsOverlayVisibility(false); } + this.changeDetector.markForCheck(); } } @@ -778,7 +791,10 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A .pipe(quietTakeUntilDestroyed(this.destroyRef)) .subscribe((state: BreakpointState) => { // setting isScreenSmall causes `ExpressionChangedAfterItHasBeenCheckedError`, so wrap in setTimeout - setTimeout(() => (this.isScreenSmall = state.matches)); + setTimeout(() => { + this.isScreenSmall = state.matches; + this.changeDetector.markForCheck(); + }); }); this.activeQuestionDoc$ @@ -800,6 +816,7 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A if (prevInScope != null) { this.prevQuestionOutOfScope = undefined; this.prevQuestion$ = of(prevInScope); + this.changeDetector.markForCheck(); } else { const prevQuestionQuery = await this.checkingQuestionsService.queryAdjacentQuestions( this.projectDoc!.id, @@ -816,6 +833,7 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A ) .subscribe(async query => { this.prevQuestion$ = of(this.filterQuestions(query.docs)[0]); + this.changeDetector.markForCheck(); }); } @@ -824,6 +842,7 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A if (nextInScope != null) { this.nextQuestionOutOfScope = undefined; this.nextQuestion$ = of(nextInScope); + this.changeDetector.markForCheck(); } else { const nextQuestionQuery = await this.checkingQuestionsService.queryAdjacentQuestions( this.projectDoc!.id, @@ -840,6 +859,7 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A ) .subscribe(async query => { this.nextQuestion$ = of(this.filterQuestions(query.docs)[0]); + this.changeDetector.markForCheck(); }); } }); @@ -1190,6 +1210,7 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A } else { this.scripturePanel.activeVerse = this.activeQuestionVerseRef; } + this.changeDetector.markForCheck(); } /** @@ -1257,6 +1278,7 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A this.totalVisibleQuestionsString = '0'; this.updateQuestionRefs(); this.refreshSummary(); + this.changeDetector.markForCheck(); return; } @@ -1279,6 +1301,7 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A this.updateQuestionRefs(); this.refreshSummary(); + this.changeDetector.markForCheck(); } private filterQuestions(unfilteredQuestions: readonly QuestionDoc[]): QuestionDoc[] { @@ -1313,6 +1336,7 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A t.chapters.find(c => c.number === q.data!.verseRef.chapterNum && c.hasAudio !== true) != null ) != null ); + this.changeDetector.markForCheck(); } private getAnswerIndex(answer: Answer): number { @@ -1512,6 +1536,7 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A this.showScriptureAudioPlayer ? this.scriptureAudioPlayerAreaHeight : answerPanelHeight ]); }, changeUpdateDelayMs); + this.changeDetector.markForCheck(); } // Unbind this component from the data when a user is removed from the project, otherwise console @@ -1526,6 +1551,7 @@ export class CheckingComponent extends DataLoadingComponent implements OnInit, A } this.questionsQuery = undefined; this.projectDoc = undefined; + this.changeDetector.markForCheck(); } private refreshSummary(): void { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/single-button-audio-player/single-button-audio-player.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/single-button-audio-player/single-button-audio-player.component.ts index 1c2dec57e92..9270db63e7d 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/single-button-audio-player/single-button-audio-player.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/single-button-audio-player/single-button-audio-player.component.ts @@ -1,5 +1,5 @@ import { NgClass } from '@angular/common'; -import { Component, Input, OnChanges, OnDestroy } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnChanges, OnDestroy } from '@angular/core'; import { MatIcon } from '@angular/material/icon'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { MatTooltip } from '@angular/material/tooltip'; @@ -31,7 +31,10 @@ export class SingleButtonAudioPlayerComponent extends AudioPlayerBaseComponent i this._source = source; } - constructor(onlineStatusService: OnlineStatusService) { + constructor( + onlineStatusService: OnlineStatusService, + private readonly changeDetector: ChangeDetectorRef + ) { super(onlineStatusService); } @@ -45,6 +48,7 @@ export class SingleButtonAudioPlayerComponent extends AudioPlayerBaseComponent i calculateProgress(): void { this._progressInDegrees = this.audio?.seek !== undefined ? `${(this.audio?.seek / 100) * 360}deg` : ''; + this.changeDetector.markForCheck(); } play(): void { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/core/models/question-doc.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/core/models/question-doc.ts index ba5acc36a53..5c48d20cf5d 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/core/models/question-doc.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/core/models/question-doc.ts @@ -15,7 +15,16 @@ import { Snapshot } from 'xforge-common/models/snapshot'; */ export class QuestionDoc extends ProjectDataDoc { static readonly COLLECTION = QUESTIONS_COLLECTION; - static readonly INDEX_PATHS = QUESTION_INDEX_PATHS; + static readonly INDEX_PATHS = [ + ...QUESTION_INDEX_PATHS, + // Index for CheckingQuestionsService.queryQuestions() and CheckingQuestionsService.queryAdjacentQuestions() + // As IndexedDB does not support boolean fields in indexes (see https://github.com/w3c/IndexedDB/issues/76) + { + [obj().pathStr(n => n.projectRef)]: 1, + [obj().pathStr(n => n.verseRef.bookNum)]: 1, + [obj().pathStr(n => n.verseRef.chapterNum)]: 1 + } + ]; alwaysKeepFileOffline(fileType: FileType, dataId: string): boolean { return this.data != null && fileType === FileType.Audio && !this.data.isArchived && this.data.dataId === dataId; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/audio/audio-player/audio-player.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/audio/audio-player/audio-player.component.ts index 2850b107ab8..c6c00ae35eb 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/audio/audio-player/audio-player.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/audio/audio-player/audio-player.component.ts @@ -1,5 +1,5 @@ import { Dir } from '@angular/cdk/bidi'; -import { Component, OnDestroy } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core'; import { MatIcon } from '@angular/material/icon'; import { MatSlider, MatSliderDragEvent, MatSliderThumb } from '@angular/material/slider'; import { TranslocoModule } from '@ngneat/transloco'; @@ -23,6 +23,7 @@ export class AudioPlayerComponent extends AudioPlayerBaseComponent implements On constructor( onlineStatusService: OnlineStatusService, + private readonly changeDetector: ChangeDetectorRef, readonly i18n: I18nService ) { super(onlineStatusService); @@ -52,6 +53,7 @@ export class AudioPlayerComponent extends AudioPlayerBaseComponent implements On this._timeUpdatedSubscription = this.audio?.timeUpdated$.subscribe(() => { this._currentTime = this.audio?.currentTime ?? 0; this._seek = this.audio?.seek ?? 0; + this.changeDetector.markForCheck(); }); }