Pull request #333: File preview state service

Merge in RED/ui from refactor-fp/state to master

* commit '527d4933a07ba99565e5a6c4e7d6223f2aa3b6e4':
  Fixes
  Fixed errors on document reload
  WIP
  Fixed reload document on file data change
  Fixed canSwitchToDeltaView
  State service
This commit is contained in:
Adina Teudan 2022-01-22 12:32:27 +01:00
commit cfafc88121
14 changed files with 176 additions and 146 deletions

View File

@ -2,33 +2,45 @@ import { Dictionary, File, IRedactionLog, IRedactionLogEntry, IViewedPage, ViewM
import { AnnotationWrapper } from './annotation.wrapper';
import { RedactionLogEntryWrapper } from './redaction-log-entry.wrapper';
import * as moment from 'moment';
import { BehaviorSubject } from 'rxjs';
export class FileDataModel {
static readonly DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes;
hasChangeLog: boolean;
allAnnotations: AnnotationWrapper[];
readonly hasChangeLog$ = new BehaviorSubject<boolean>(false);
readonly blob$ = new BehaviorSubject<Blob>(undefined);
readonly file$ = new BehaviorSubject<File>(undefined);
constructor(
public file: File,
public fileData: Blob,
private readonly _file: File,
private readonly _blob: Blob,
private _redactionLog: IRedactionLog,
public viewedPages?: IViewedPage[],
private _dictionaryData?: { [p: string]: Dictionary },
private _areDevFeaturesEnabled?: boolean,
) {
this.file$.next(_file);
this.blob$.next(_blob);
this._buildAllAnnotations();
}
get file(): File {
return this.file$.value;
}
set file(file: File) {
this.file$.next(file);
}
get redactionLog(): IRedactionLog {
return this._redactionLog;
}
set redactionLog(redactionLog: IRedactionLog) {
this._redactionLog = redactionLog;
this._buildAllAnnotations();
}
get redactionLog() {
return this._redactionLog;
}
getVisibleAnnotations(viewMode: ViewMode) {
return this.allAnnotations.filter(annotation => {
if (viewMode === 'STANDARD') {
@ -47,7 +59,7 @@ export class FileDataModel {
const previousAnnotations = this.allAnnotations || [];
this.allAnnotations = entries
.map(entry => AnnotationWrapper.fromData(entry))
.filter(ann => ann.manual || !this.file.excludedPages.includes(ann.pageNumber));
.filter(ann => ann.manual || !this._file.excludedPages.includes(ann.pageNumber));
if (!this._areDevFeaturesEnabled) {
this.allAnnotations = this.allAnnotations.filter(annotation => !annotation.isFalsePositive);
@ -121,8 +133,8 @@ export class FileDataModel {
}
private _isChangeLogEntry(redactionLogEntry: IRedactionLogEntry, wrapper: RedactionLogEntryWrapper) {
if (this.file.numberOfAnalyses > 1) {
var viableChanges = redactionLogEntry.changes.filter(c => c.analysisNumber > 1);
if (this._file.numberOfAnalyses > 1) {
const viableChanges = redactionLogEntry.changes.filter(c => c.analysisNumber > 1);
viableChanges.sort((a, b) => moment(a.dateTime).valueOf() - moment(b.dateTime).valueOf());
const lastChange = viableChanges.length >= 1 ? viableChanges[viableChanges.length - 1] : undefined;
@ -141,7 +153,7 @@ export class FileDataModel {
wrapper.changeLogType = relevantChanges[relevantChanges.length - 1].type;
wrapper.isChangeLogEntry = true;
viewedPage.showAsUnseen = moment(viewedPage.viewedTime).valueOf() < moment(lastChange.dateTime).valueOf();
this.hasChangeLog = true;
this.hasChangeLog$.next(true);
} else {
// no relevant changes - hide removed anyway
wrapper.isChangeLogEntry = false;

View File

@ -10,7 +10,6 @@ import { SharedModule } from '@shared/shared.module';
import { DossiersRoutingModule } from './dossiers-routing.module';
import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module';
import { DossiersDialogService } from './services/dossiers-dialog.service';
import { PdfViewerDataService } from './services/pdf-viewer-data.service';
import { ManualAnnotationService } from './services/manual-annotation.service';
import { AnnotationProcessingService } from './services/annotation-processing.service';
import { EditDossierDialogComponent } from './dialogs/edit-dossier-dialog/edit-dossier-dialog.component';
@ -54,7 +53,7 @@ const components = [
...dialogs,
];
const services = [DossiersDialogService, ManualAnnotationService, PdfViewerDataService, AnnotationProcessingService];
const services = [DossiersDialogService, ManualAnnotationService, AnnotationProcessingService];
@NgModule({
declarations: [...components],

View File

@ -96,7 +96,6 @@
[file]="file"
[number]="pageNumber"
[showDottedIcon]="hasOnlyManualRedactionsAndIsExcluded(pageNumber)"
[viewedPages]="viewedPages"
></redaction-page-indicator>
</div>

View File

@ -28,7 +28,7 @@ import { PermissionsService } from '@services/permissions.service';
import { WebViewerInstance } from '@pdftron/webviewer';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { File, IViewedPage } from '@red/domain';
import { File } from '@red/domain';
import { ExcludedPagesService } from '../../services/excluded-pages.service';
import { MultiSelectService } from '../../services/multi-select.service';
import { DocumentInfoService } from '../../services/document-info.service';
@ -52,7 +52,6 @@ export class FileWorkloadComponent {
@Input() activeViewerPage: number;
@Input() shouldDeselectAnnotationsOnPageChange: boolean;
@Input() dialogRef: MatDialogRef<unknown>;
@Input() viewedPages: IViewedPage[];
@Input() file!: File;
@Input() annotationActionsTemplate: TemplateRef<unknown>;
@Input() viewer: WebViewerInstance;
@ -225,7 +224,7 @@ export class FileWorkloadComponent {
scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed'): void {
if (this._annotationsElement) {
const elements = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`);
const elements: HTMLElement[] = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`);
FileWorkloadComponent._scrollToFirstElement(elements, mode);
}
}
@ -235,7 +234,7 @@ export class FileWorkloadComponent {
if (!this.selectedAnnotations || this.selectedAnnotations.length === 0 || !this._annotationsElement) {
return;
}
const elements = this._annotationsElement.nativeElement.querySelectorAll(
const elements: HTMLElement[] = this._annotationsElement.nativeElement.querySelectorAll(
`div[annotation-id="${this._firstSelectedAnnotation?.id}"].active`,
);
FileWorkloadComponent._scrollToFirstElement(elements);
@ -412,7 +411,7 @@ export class FileWorkloadComponent {
private _scrollQuickNavigationToPage(page: number) {
if (this._quickNavigationElement) {
const elements = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
const elements: HTMLElement[] = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
FileWorkloadComponent._scrollToFirstElement(elements);
}
}

View File

@ -6,6 +6,7 @@ import { ViewedPagesService } from '@services/entity-services/viewed-pages.servi
import { File, IViewedPage } from '@red/domain';
import { AutoUnsubscribe } from '@iqser/common-ui';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
@Component({
selector: 'redaction-page-indicator',
@ -18,7 +19,6 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
@Input() active = false;
@Input() showDottedIcon = false;
@Input() number: number;
@Input() viewedPages: IViewedPage[];
@Input() activeSelection = false;
@Output() readonly pageSelected = new EventEmitter<number>();
@ -33,25 +33,17 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
private readonly _configService: ConfigService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _permissionService: PermissionsService,
private readonly _stateService: FilePreviewStateService,
) {
super();
}
get activePage() {
return this.viewedPages?.find(p => p.page === this.number);
return this._viewedPages.find(p => p.page === this.number);
}
private _setReadState() {
const readBefore = this.read;
const activePage = this.activePage;
if (!activePage) {
this.read = false;
} else {
// console.log('setting read to',activePage.showAsUnseen, !activePage.showAsUnseen);
this.read = !activePage.showAsUnseen;
}
// console.log(this.number, readBefore, activePage, this.read);
this._changeDetectorRef.detectChanges();
private get _viewedPages(): IViewedPage[] {
return this._stateService.fileData?.viewedPages || [];
}
ngOnChanges() {
@ -87,20 +79,33 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
}
}
private _setReadState() {
const readBefore = this.read;
const activePage = this.activePage;
if (!activePage) {
this.read = false;
} else {
// console.log('setting read to',activePage.showAsUnseen, !activePage.showAsUnseen);
this.read = !activePage.showAsUnseen;
}
// console.log(this.number, readBefore, activePage, this.read);
this._changeDetectorRef.detectChanges();
}
private async _markPageRead() {
await this._viewedPagesService.addPage({ page: this.number }, this.file.dossierId, this.file.fileId).toPromise();
if (this.activePage) {
this.activePage.showAsUnseen = false;
} else {
this.viewedPages?.push({ page: this.number, fileId: this.file.fileId });
this._viewedPages.push({ page: this.number, fileId: this.file.fileId });
}
this._setReadState();
}
private async _markPageUnread() {
await this._viewedPagesService.removePage(this.file.dossierId, this.file.fileId, this.number).toPromise();
this.viewedPages?.splice(
this.viewedPages?.findIndex(p => p.page === this.number),
this._viewedPages.splice(
this._viewedPages.findIndex(p => p.page === this.number),
1,
);
this._setReadState();

View File

@ -27,7 +27,7 @@ import { AnnotationActionsService } from '../../services/annotation-actions.serv
import { UserPreferenceService } from '@services/user-preference.service';
import { BASE_HREF } from '../../../../../../tokens';
import { ConfigService } from '@services/config.service';
import { ConfirmationDialogInput, LoadingService } from '@iqser/common-ui';
import { AutoUnsubscribe, ConfirmationDialogInput, LoadingService, shareDistinctLast } from '@iqser/common-ui';
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
import { loadCompareDocumentWrapper } from '../../../../utils/compare-mode.utils';
import { PdfViewerUtils } from '../../../../utils/pdf-viewer.utils';
@ -36,11 +36,13 @@ import { ActivatedRoute } from '@angular/router';
import { toPosition } from '../../../../utils/pdf-calculation.utils';
import { ViewModeService } from '../../services/view-mode.service';
import { MultiSelectService } from '../../services/multi-select.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { filter, switchMap, tap } from 'rxjs/operators';
import Tools = Core.Tools;
import TextTool = Tools.TextTool;
import Annotation = Core.Annotations.Annotation;
const ALLOWED_KEYBOARD_SHORTCUTS = ['+', '-', 'p', 'r', 'Escape'] as const;
const ALLOWED_KEYBOARD_SHORTCUTS: readonly string[] = ['+', '-', 'p', 'r', 'Escape'] as const;
const dataElements = {
ADD_REDACTION: 'add-redaction',
ADD_DICTIONARY: 'add-dictionary',
@ -61,8 +63,7 @@ const dataElements = {
templateUrl: './pdf-viewer.component.html',
styleUrls: ['./pdf-viewer.component.scss'],
})
export class PdfViewerComponent implements OnInit, OnChanges {
@Input() fileData: Blob;
export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnChanges {
@Input() file: File;
@Input() dossier: Dossier;
@Input() canPerformActions = false;
@ -95,9 +96,12 @@ export class PdfViewerComponent implements OnInit, OnChanges {
private readonly _annotationActionsService: AnnotationActionsService,
private readonly _configService: ConfigService,
private readonly _loadingService: LoadingService,
private readonly _stateService: FilePreviewStateService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
) {}
) {
super();
}
private get _toggleTooltipsBtnTitle(): string {
return this._translateService.instant(_('pdf-viewer.toggle-tooltips'), {
@ -115,7 +119,17 @@ export class PdfViewerComponent implements OnInit, OnChanges {
async ngOnInit() {
this._setReadyAndInitialState = this._setReadyAndInitialState.bind(this);
await this.loadViewer();
await this._loadViewer();
this.addActiveScreenSubscription = this._stateService.fileData$
.pipe(
filter(fileData => !!fileData),
switchMap(fileData => fileData.blob$),
// Skip document reload if file content hasn't changed
shareDistinctLast(),
tap(() => this._loadDocument()),
)
.subscribe();
}
ngOnChanges(changes: SimpleChanges): void {
@ -123,16 +137,12 @@ export class PdfViewerComponent implements OnInit, OnChanges {
return;
}
if (changes.fileData) {
this._loadDocument();
}
if (changes.canPerformActions) {
this._handleCustomActions();
}
}
uploadFile(files: any) {
uploadFile(files: FileList) {
const fileToCompare = files[0];
this.compareFileInput.nativeElement.value = null;
@ -148,7 +158,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
const compareDocument = await pdfNet.PDFDoc.createFromBuffer(fileReader.result as ArrayBuffer);
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.fileData.arrayBuffer());
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this._stateService.fileData.blob$.value.arrayBuffer());
const loadCompareDocument = async () => {
this._loadingService.start();
@ -201,7 +211,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this.viewModeService.compareMode = false;
const pdfNet = this.instance.Core.PDFNet;
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.fileData.arrayBuffer());
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this._stateService.fileData.blob$.value.arrayBuffer());
this.instance.UI.loadDocument(currentDocument, {
filename: this.file ? this.file.filename : 'document.pdf',
});
@ -210,7 +220,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this.utils.navigateToPage(1);
}
async loadViewer() {
private async _loadViewer() {
this.instance = await WebViewer(
{
licenseKey: environment.licenseKey ? atob(environment.licenseKey) : null,
@ -219,7 +229,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
css: this._convertPath('/assets/pdftron/stylesheet.css'),
backendType: 'ems',
},
this.viewer.nativeElement,
this.viewer.nativeElement as HTMLElement,
);
this.documentViewer = this.instance.Core.documentViewer;
@ -231,7 +241,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this.utils.disableHotkeys();
this._configureTextPopup();
this.annotationManager.addEventListener('annotationSelected', (annotations, action) => {
this.annotationManager.addEventListener('annotationSelected', (annotations: Annotation[], action) => {
this.annotationSelected.emit(this.annotationManager.getSelectedAnnotations().map(ann => ann.Id));
if (action === 'deselected') {
this._toggleRectangleAnnotationAction(true);
@ -241,7 +251,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
}
});
this.annotationManager.addEventListener('annotationChanged', annotations => {
this.annotationManager.addEventListener('annotationChanged', (annotations: Annotation[]) => {
// when a rectangle is drawn,
// it returns one annotation with tool name 'AnnotationCreateRectangle;
// this will auto select rectangle after drawing
@ -251,7 +261,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
}
});
this.documentViewer.addEventListener('pageNumberUpdated', pageNumber => {
this.documentViewer.addEventListener('pageNumberUpdated', (pageNumber: number) => {
if (this.shouldDeselectAnnotationsOnPageChange) {
this.utils.deselectAllAnnotations();
}
@ -261,9 +271,9 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this.documentViewer.addEventListener('documentLoaded', this._setReadyAndInitialState);
this.documentViewer.addEventListener('keyUp', $event => {
this.documentViewer.addEventListener('keyUp', ($event: KeyboardEvent) => {
// arrows and full-screen
if ($event.target?.tagName?.toLowerCase() !== 'input') {
if (($event.target as HTMLElement)?.tagName?.toLowerCase() !== 'input') {
if ($event.key.startsWith('Arrow') || $event.key === 'f') {
this._ngZone.run(() => {
this.keyUp.emit($event);
@ -273,7 +283,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
}
}
if (ALLOWED_KEYBOARD_SHORTCUTS.indexOf($event.key) < 0) {
if (!ALLOWED_KEYBOARD_SHORTCUTS.includes($event.key)) {
$event.preventDefault();
$event.stopPropagation();
}
@ -301,8 +311,6 @@ export class PdfViewerComponent implements OnInit, OnChanges {
}
}
});
this._loadDocument();
}
private _setInitialDisplayMode() {
@ -568,7 +576,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
}
private _addManualRedactionOfType(type: ManualRedactionEntryType) {
const selectedQuads = this.documentViewer.getSelectedTextQuads();
const selectedQuads: Readonly<Record<string, Core.Math.Quad[]>> = this.documentViewer.getSelectedTextQuads();
const text = this.documentViewer.getSelectedText();
const manualRedaction = this._getManualRedaction(selectedQuads, text, true);
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(selectedQuads, manualRedaction, type));
@ -624,7 +632,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
for (const quad of quads[key]) {
const page = parseInt(key, 10);
const pageHeight = this.documentViewer.getPageHeight(page);
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.utils.translateQuads(page, quad) : quad));
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.utils.translateQuad(page, quad) : quad));
}
}
@ -634,11 +642,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
}
private _loadDocument() {
if (!this.fileData) {
return;
}
this.instance.UI.loadDocument(this.fileData, {
this.instance.UI.loadDocument(this._stateService.fileData.blob$.value, {
filename: this.file ? this.file.filename : 'document.pdf',
});
}
@ -647,7 +651,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this._ngZone.run(() => {
this.utils.ready = true;
this.viewerReady.emit(this.instance);
const routePageNumber = this._activatedRoute.snapshot.queryParams.page;
const routePageNumber: number = this._activatedRoute.snapshot.queryParams.page;
this.pageChanged.emit(routePageNumber || 1);
this._setInitialDisplayMode();
this._updateTooltipsVisibility();

View File

@ -1,5 +1,5 @@
<ng-container *ngIf="viewModeService.viewMode$ | async as viewMode">
<div
<button
(click)="switchView.emit('STANDARD')"
[class.active]="viewModeService.isStandard"
[matTooltip]="'file-preview.standard-tooltip' | translate"
@ -7,27 +7,27 @@
iqserHelpMode="standard-view"
>
{{ 'file-preview.standard' | translate }}
</div>
</button>
<div
(click)="canSwitchToDeltaView && switchView.emit('DELTA')"
<button
(click)="switchView.emit('DELTA')"
[class.active]="viewModeService.isDelta"
[class.disabled]="!canSwitchToDeltaView"
[disabled]="(canSwitchToDeltaView$ | async) === false"
[matTooltip]="'file-preview.delta-tooltip' | translate"
class="red-tab"
iqserHelpMode="delta-view"
>
{{ 'file-preview.delta' | translate }}
</div>
</button>
<div
<button
(click)="canSwitchToRedactedView && switchView.emit('REDACTED')"
[class.active]="viewModeService.isRedacted"
[class.disabled]="!canSwitchToRedactedView"
[disabled]="!canSwitchToRedactedView"
[matTooltip]="'file-preview.redacted-tooltip' | translate"
class="red-tab"
iqserHelpMode="preview-view"
>
{{ 'file-preview.redacted' | translate }}
</div>
</button>
</ng-container>

View File

@ -1,10 +1,12 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { File, ViewMode } from '@red/domain';
import { ViewModeService } from '../../services/view-mode.service';
import { FileDataModel } from '@models/file/file-data.model';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { Observable } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
@Component({
selector: 'redaction-view-switch [file] [fileData]',
selector: 'redaction-view-switch [file]',
templateUrl: './view-switch.component.html',
styleUrls: ['./view-switch.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
@ -12,19 +14,18 @@ import { FileDataModel } from '@models/file/file-data.model';
export class ViewSwitchComponent implements OnChanges {
@Output() readonly switchView = new EventEmitter<ViewMode>();
@Input() file: File;
@Input() fileData: FileDataModel;
canSwitchToDeltaView = false;
readonly canSwitchToDeltaView$: Observable<boolean>;
canSwitchToRedactedView = false;
constructor(readonly viewModeService: ViewModeService) {}
constructor(readonly viewModeService: ViewModeService, private readonly _stateService: FilePreviewStateService) {
this.canSwitchToDeltaView$ = this._stateService.fileData$.pipe(
filter(fileData => !!fileData),
switchMap(fileData => fileData?.hasChangeLog$),
);
}
ngOnChanges(changes: SimpleChanges) {
if (changes.fileData) {
const fileData = changes.fileData.currentValue as FileDataModel;
this.canSwitchToDeltaView = fileData?.hasChangeLog;
}
if (changes.file) {
const file = changes?.file.currentValue as File;
this.canSwitchToRedactedView = !file.analysisRequired && !file.excluded;

View File

@ -1,9 +1,9 @@
<ng-container *ngIf="dossier$ | async as dossier">
<ng-container *ngIf="file$ | async as file">
<ng-container *ngIf="fileData?.file$ | async as file">
<section [class.fullscreen]="fullScreen">
<div class="page-header">
<div class="flex flex-1">
<redaction-view-switch (switchView)="switchView($event)" [fileData]="fileData" [file]="file"></redaction-view-switch>
<redaction-view-switch (switchView)="switchView($event)" [file]="file"></redaction-view-switch>
</div>
<div class="flex-1 actions-container">
@ -76,7 +76,6 @@
[canPerformActions]="canPerformAnnotationActions$ | async"
[class.hidden]="!ready"
[dossier]="dossier"
[fileData]="fileData?.fileData"
[file]="file"
[shouldDeselectAnnotationsOnPageChange]="shouldDeselectAnnotationsOnPageChange"
></redaction-pdf-viewer>
@ -110,7 +109,6 @@
[dialogRef]="dialogRef"
[file]="file"
[selectedAnnotations]="selectedAnnotations"
[viewedPages]="fileData?.viewedPages"
[viewer]="activeViewer"
></redaction-file-workload>
</div>

View File

@ -14,13 +14,11 @@ import {
OnDetach,
processFilters,
shareDistinctLast,
shareLast,
} from '@iqser/common-ui';
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ManualAnnotationResponse } from '@models/file/manual-annotation-response';
import { FileDataModel } from '@models/file/file-data.model';
import { AnnotationDrawService } from './services/annotation-draw.service';
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
import { Dossier, File, ViewMode } from '@red/domain';
@ -37,7 +35,7 @@ import { handleFilterDelta } from '@utils/filter-utils';
import { FilesService } from '@services/entity-services/files.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { FileManagementService } from '@services/entity-services/file-management.service';
import { filter, map, switchMap, tap } from 'rxjs/operators';
import { map, switchMap, tap } from 'rxjs/operators';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { WatermarkService } from '@shared/services/watermark.service';
import { ExcludedPagesService } from './services/excluded-pages.service';
@ -49,6 +47,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { CommentingService } from './services/commenting.service';
import { SkippedService } from './services/skipped.service';
import { AnnotationActionsService } from './services/annotation-actions.service';
import { FilePreviewStateService } from './services/file-preview-state.service';
import { FileDataModel } from '../../../../models/file/file-data.model';
import Annotation = Core.Annotations.Annotation;
import PDFNet = Core.PDFNet;
@ -67,6 +67,8 @@ const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown'];
SkippedService,
AnnotationDrawService,
AnnotationActionsService,
FilePreviewStateService,
PdfViewerDataService,
],
})
export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach {
@ -75,7 +77,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
dialogRef: MatDialogRef<unknown>;
fullScreen = false;
shouldDeselectAnnotationsOnPageChange = true;
fileData: FileDataModel;
selectedAnnotations: AnnotationWrapper[] = [];
displayPdfViewer = false;
activeViewerPage: number = null;
@ -83,7 +84,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
readonly dossierId: string;
readonly canPerformAnnotationActions$: Observable<boolean>;
readonly dossier$: Observable<Dossier>;
readonly file$: Observable<File>;
readonly fileId: string;
ready = false;
private _instance: WebViewerInstance;
@ -98,6 +98,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
constructor(
readonly permissionsService: PermissionsService,
readonly userPreferenceService: UserPreferenceService,
private readonly _stateService: FilePreviewStateService,
private readonly _watermarkService: WatermarkService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _activatedRoute: ActivatedRoute,
@ -105,7 +106,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _router: Router,
private readonly _annotationProcessingService: AnnotationProcessingService,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _fileDownloadService: PdfViewerDataService,
private readonly _pdfViewerDataService: PdfViewerDataService,
private readonly _filesService: FilesService,
private readonly _ngZone: NgZone,
private readonly _fileManagementService: FileManagementService,
@ -126,12 +127,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this.dossierId = _activatedRoute.snapshot.paramMap.get('dossierId');
this.dossier$ = _dossiersService.getEntityChanged$(this.dossierId);
this.fileId = _activatedRoute.snapshot.paramMap.get('fileId');
this.file$ = _filesMapService.watch$(this.dossierId, this.fileId).pipe(
tap(async file => {
await this._reloadFile(file);
}),
shareLast(),
);
this.canPerformAnnotationActions$ = this._canPerformAnnotationActions$;
document.documentElement.addEventListener('fullscreenchange', () => {
@ -153,8 +148,16 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return this._instance;
}
get fileData(): FileDataModel {
return this._stateService.fileData;
}
private get _canPerformAnnotationActions$() {
return combineLatest([this.file$, this.viewModeService.viewMode$, this.viewModeService.compareMode$]).pipe(
return combineLatest([
this._stateService.fileData$.pipe(switchMap(fileData => fileData.file$)),
this.viewModeService.viewMode$,
this.viewModeService.compareMode$,
]).pipe(
map(([file, viewMode]) => this.permissionsService.canPerformAnnotationActions(file) && viewMode === 'STANDARD'),
shareDistinctLast(),
);
@ -260,11 +263,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
});
console.log(`[REDACTION] Process time: ${new Date().getTime() - processStartTime} ms`);
console.log(`[REDACTION] Filter rebuild time: ${new Date().getTime() - startTime}`);
console.log();
}
handleAnnotationSelected(annotationIds: string[]) {
// TODO: use includes() here
this.selectedAnnotations = annotationIds
.map(id => this.visibleAnnotations.find(annotationWrapper => annotationWrapper.id === id))
.filter(ann => ann !== undefined);
@ -475,17 +476,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
if (previousFile.lastOCRTime !== this.fileData?.file?.lastOCRTime) {
return;
}
// excluded pages or document exclusion has changed
const fileHasBeenExcludedOrIncluded = previousFile.excluded !== this.fileData.file.excluded;
const excludedPagesHaveChanged =
JSON.stringify(previousFile.excludedPages) !== JSON.stringify(this.fileData.file.excludedPages);
if (fileHasBeenExcludedOrIncluded || excludedPagesHaveChanged) {
await this._deleteAnnotations();
await this._cleanupAndRedrawAnnotations();
}
}
await this._stampPDF();
}
private async _stampPDF() {
@ -543,20 +534,28 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
}
private async _fileUpdated(file: File): Promise<File> {
if (!this.fileData || file.lastProcessed === this.fileData.file.lastProcessed) {
await this._reloadFile(file);
} else {
// File reanalysed
const previousAnnotations = this.visibleAnnotations;
await this._loadFileData(file);
await this._reloadAnnotations(previousAnnotations);
}
return file;
}
private _subscribeToFileUpdates(): void {
this.addActiveScreenSubscription = this._filesMapService
.watch$(this.dossierId, this.fileId)
.pipe(switchMap(file => this._fileUpdated(file)))
.subscribe();
this.addActiveScreenSubscription = timer(0, 5000)
.pipe(switchMap(() => this._filesService.reload(this.dossierId, this.fileId)))
.subscribe();
this.addActiveScreenSubscription = this._filesMapService.fileReanalysed$
.pipe(filter(file => file.fileId === this.fileId))
.subscribe(async file => {
if (file.lastProcessed !== this.fileData?.file.lastProcessed) {
const previousAnnotations = this.visibleAnnotations;
await this._loadFileData(file);
await this._reloadAnnotations(previousAnnotations);
}
this._loadingService.stop();
});
this.addActiveScreenSubscription = this._dossiersService
.getEntityDeleted$(this.dossierId)
@ -590,13 +589,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return this._router.navigate([this._dossiersService.find(this.dossierId).routerLink]);
}
const fileData = await this._fileDownloadService.loadDataFor(file, this.fileData).toPromise();
const fileData = await this._pdfViewerDataService.loadDataFor(file).toPromise();
if (file.isPending) {
return;
}
this.fileData = fileData;
this._stateService.fileData = fileData;
}
@Debounce(0)
@ -620,7 +619,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
const currentPageAnnotations = this.visibleAnnotations.filter(a => a.pageNumber === page);
this.fileData.redactionLog = await this._fileDownloadService.loadRedactionLogFor(this.dossierId, this.fileId).toPromise();
this.fileData.redactionLog = await this._pdfViewerDataService.loadRedactionLogFor(this.dossierId, this.fileId).toPromise();
this._deleteAnnotations(currentPageAnnotations);
await this._cleanupAndRedrawAnnotations(currentPageAnnotations, annotation => annotation.pageNumber === page);

View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { FileDataModel } from '@models/file/file-data.model';
@Injectable()
export class FilePreviewStateService {
readonly fileData$: Observable<FileDataModel>;
private readonly _fileData$ = new BehaviorSubject<FileDataModel>(undefined);
constructor() {
this.fileData$ = this._fileData$.asObservable();
}
get fileData(): FileDataModel {
return this._fileData$.value;
}
set fileData(fileDataModel: FileDataModel) {
this._fileData$.next(fileDataModel);
}
}

View File

@ -3,13 +3,14 @@ import { forkJoin, Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { FileDataModel } from '@models/file/file-data.model';
import { PermissionsService } from '@services/permissions.service';
import { File } from '@red/domain';
import { File, IRedactionLog, IViewedPage } from '@red/domain';
import { FileManagementService } from '@services/entity-services/file-management.service';
import { RedactionLogService } from './redaction-log.service';
import { ViewedPagesService } from '@services/entity-services/viewed-pages.service';
import { AppStateService } from '../../../state/app-state.service';
import { DossiersService } from '../../../services/entity-services/dossiers.service';
import { UserPreferenceService } from '../../../services/user-preference.service';
import { FilePreviewStateService } from '../screens/file-preview-screen/services/file-preview-state.service';
@Injectable()
export class PdfViewerDataService {
@ -21,6 +22,7 @@ export class PdfViewerDataService {
private readonly _viewedPagesService: ViewedPagesService,
private readonly _appStateService: AppStateService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _stateService: FilePreviewStateService,
) {}
loadRedactionLogFor(dossierId: string, fileId: string) {
@ -30,16 +32,17 @@ export class PdfViewerDataService {
);
}
loadDataFor(file: File, fileData?: FileDataModel): Observable<FileDataModel> {
const file$ = fileData?.file.cacheIdentifier === file.cacheIdentifier ? of(fileData.fileData) : this.downloadOriginalFile(file);
const reactionLog$ = this.loadRedactionLogFor(file.dossierId, file.fileId);
loadDataFor(file: File): Observable<FileDataModel> {
const fileData = this._stateService.fileData;
const blob$ = fileData?.file.cacheIdentifier === file.cacheIdentifier ? of(fileData.blob$.value) : this.downloadOriginalFile(file);
const redactionLog$ = this.loadRedactionLogFor(file.dossierId, file.fileId);
const viewedPages$ = this.getViewedPagesFor(file);
const dossier = this._dossiersService.find(file.dossierId);
return forkJoin([file$, reactionLog$, viewedPages$]).pipe(
return forkJoin([blob$, redactionLog$, viewedPages$]).pipe(
map(
data =>
(data: [blob: Blob, redactionLog: IRedactionLog, viewedPages: IViewedPage[]]) =>
new FileDataModel(
file,
...data,

View File

@ -111,9 +111,9 @@ export class PdfViewerUtils {
}
}
translateQuads(page: number, quads: any) {
translateQuad(page: number, quad: Core.Math.Quad) {
const rotation = this._documentViewer.getCompleteRotation(page);
return translateQuads(page, rotation, quads);
return translateQuads(page, rotation, quad);
}
deselectAllAnnotations() {

View File

@ -5,7 +5,6 @@ import { filter, startWith } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class FilesMapService {
readonly fileReanalysed$ = new Subject<File>();
private readonly _entityChanged$ = new Subject<File>();
private readonly _entityDeleted$ = new Subject<File>();
private readonly _map = new Map<string, BehaviorSubject<File[]>>();
@ -38,7 +37,6 @@ export class FilesMapService {
return entities.forEach(entity => this._entityChanged$.next(entity));
}
const reanalysedEntities = [];
const changedEntities = [];
const deletedEntities = this.get(key).filter(oldEntity => !entities.find(newEntity => newEntity.id === oldEntity.id));
@ -46,10 +44,6 @@ export class FilesMapService {
const newEntities = entities.map(newEntity => {
const oldEntity = this.get(key, newEntity.id);
if (oldEntity?.lastProcessed !== newEntity.lastProcessed) {
reanalysedEntities.push(newEntity);
}
if (newEntity.isEqual(oldEntity)) {
return oldEntity;
}
@ -62,10 +56,6 @@ export class FilesMapService {
// Emit observables only after entities have been updated
for (const file of reanalysedEntities) {
this.fileReanalysed$.next(file);
}
for (const file of changedEntities) {
this._entityChanged$.next(file);
}