RED-3988: make pdf viewer reusable

This commit is contained in:
Dan Percic 2022-05-18 20:58:21 +03:00
parent 6cdf4d7634
commit 7ad5fe09ac
7 changed files with 188 additions and 4 deletions

View File

@ -1,4 +1,9 @@
<router-outlet></router-outlet>
<redaction-reusable-pdf-viewer
[style.visibility]="(reusablePdfViewer.show$ | async) ? 'visible' : 'hidden'"
></redaction-reusable-pdf-viewer>
<iqser-full-page-loading-indicator></iqser-full-page-loading-indicator>
<iqser-connection-status></iqser-connection-status>
<iqser-full-page-error></iqser-full-page-error>

View File

@ -1,6 +1,7 @@
import { Component, ViewContainerRef } from '@angular/core';
import { RouterHistoryService } from '@services/router-history.service';
import { UserService } from '@services/user.service';
import { ReusablePdfViewer } from './modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service';
@Component({
selector: 'redaction-root',
@ -14,5 +15,6 @@ export class AppComponent {
public viewContainerRef: ViewContainerRef,
private readonly _routerHistoryService: RouterHistoryService,
private readonly _userService: UserService,
readonly reusablePdfViewer: ReusablePdfViewer,
) {}
}

View File

@ -20,7 +20,7 @@ import { AppRoutingModule } from './app-routing.module';
import { SharedModule } from '@shared/shared.module';
import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module';
import { DatePipe as BaseDatePipe, PlatformLocation } from '@angular/common';
import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE, BASE_HREF } from './tokens';
import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE, BASE_HREF, BASE_HREF_FN } from './tokens';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { GlobalErrorHandler } from '@utils/global-error-handler.service';
import { REDMissingTranslationHandler } from '@utils/missing-translations-handler';
@ -52,6 +52,7 @@ import { LoggerModule, NgxLoggerLevel, TOKEN_LOGGER_CONFIG, TOKEN_LOGGER_RULES_S
import { LoggerRulesService } from '@services/logger-rules.service';
import { ILoggerConfig } from '@red/domain';
import { SystemPreferencesService } from '@services/system-preferences.service';
import { ReusablePdfViewerComponent } from './modules/shared/components/reusable-pdf-viewer/reusable-pdf-viewer.component';
export function httpLoaderFactory(httpClient: HttpClient, configService: ConfigService): PruningTranslationLoader {
return new PruningTranslationLoader(httpClient, '/assets/i18n/', `.json?version=${configService.values.FRONTEND_APP_VERSION}`);
@ -69,7 +70,15 @@ function cleanupBaseUrl(baseUrl: string) {
const screens = [BaseScreenComponent, DownloadsListScreenComponent];
const components = [AppComponent, AuthErrorComponent, NotificationsComponent, SpotlightSearchComponent, BreadcrumbsComponent, ...screens];
const components = [
AppComponent,
ReusablePdfViewerComponent,
AuthErrorComponent,
NotificationsComponent,
SpotlightSearchComponent,
BreadcrumbsComponent,
...screens,
];
@NgModule({
declarations: [...components],
@ -149,6 +158,11 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
useFactory: (s: PlatformLocation) => cleanupBaseUrl(s.getBaseHrefFromDOM()),
deps: [PlatformLocation],
},
{
provide: BASE_HREF_FN,
useFactory: (baseHref: string) => (path: string) => baseHref + path,
deps: [BASE_HREF],
},
{
provide: HTTP_INTERCEPTORS,
multi: true,

View File

@ -43,6 +43,7 @@ import { FileDataService } from '../../services/file-data.service';
import { ViewerHeaderConfigService } from '../../services/viewer-header-config.service';
import { TooltipsService } from '../../services/tooltips.service';
import { ManualRedactionService } from '../../services/manual-redaction.service';
import { ReusablePdfViewer } from '../../../shared/components/reusable-pdf-viewer/reusable-pdf-viewer.service';
import Tools = Core.Tools;
import TextTool = Tools.TextTool;
import Annotation = Core.Annotations.Annotation;
@ -84,6 +85,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
private readonly _headerConfigService: ViewerHeaderConfigService,
private readonly _tooltipsService: TooltipsService,
private readonly _errorService: ErrorService,
private readonly _reusablePdfViewer: ReusablePdfViewer,
readonly stateService: FilePreviewStateService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
@ -102,6 +104,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
withLatestFrom(this.stateService.file$),
tap(() => this._errorService.clear()),
tap(([blob, file]) => this._loadDocument(blob, file)),
tap(([blob, file]) => this._reusablePdfViewer.loadDocument(blob, file)),
)
.subscribe();
}

View File

@ -0,0 +1,27 @@
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { ReusablePdfViewer } from './reusable-pdf-viewer.service';
@Component({
selector: 'redaction-reusable-pdf-viewer',
template: ' <div #viewer></div>',
styles: [
`
div {
width: calc(100% - 350px);
height: calc(100% - 111px);
bottom: 0;
left: 0;
position: absolute;
}
`,
],
})
export class ReusablePdfViewerComponent implements OnInit {
@ViewChild('viewer', { static: true }) readonly viewer: ElementRef;
constructor(readonly reusablePdfViewer: ReusablePdfViewer) {}
ngOnInit() {
return this.reusablePdfViewer.init(this.viewer.nativeElement);
}
}

View File

@ -0,0 +1,131 @@
import { Inject, Injectable, Injector } from '@angular/core';
import WebViewer, { Core, WebViewerInstance, WebViewerOptions } from '@pdftron/webviewer';
import { environment } from '../../../../../environments/environment';
import { BASE_HREF_FN, BaseHrefFn } from '../../../../tokens';
import { File } from '@red/domain';
import { CustomError, ErrorService, LoadingService, log, shareDistinctLast, shareLast } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker/';
import { ActivatedRoute, ActivationStart, Router } from '@angular/router';
import { filter, map } from 'rxjs/operators';
import { fromEvent, merge, Observable } from 'rxjs';
import { ConfigService } from '../../../../services/config.service';
import DocumentViewer = Core.DocumentViewer;
import AnnotationManager = Core.AnnotationManager;
import TextTool = Core.Tools.TextTool;
const DocLoadingError = new CustomError(_('error.file-preview.label'), _('error.file-preview.action'), 'iqser:refresh');
const uselessElements = [
'pageNavOverlay',
'menuButton',
'selectToolButton',
'textHighlightToolButton',
'textUnderlineToolButton',
'textSquigglyToolButton',
'textStrikeoutToolButton',
'viewControlsButton',
'contextMenuPopup',
'linkButton',
'toggleNotesButton',
'notesPanel',
'thumbnailControl',
'documentControl',
'ribbons',
'toolsHeader',
'rotateClockwiseButton',
'rotateCounterClockwiseButton',
'annotationStyleEditButton',
'annotationGroupButton',
];
@Injectable({
providedIn: 'root',
})
export class ReusablePdfViewer {
readonly currentPage$ = this._injector.get(ActivatedRoute).queryParamMap.pipe(
map(params => Number(params.get('page') ?? '1')),
shareDistinctLast(),
);
documentViewer: DocumentViewer;
annotationManager: AnnotationManager;
show$: Observable<boolean>;
documentLoaded$: Observable<boolean>;
#instance: WebViewerInstance;
constructor(
@Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn,
private readonly _loadingService: LoadingService,
private readonly _errorService: ErrorService,
private readonly _router: Router,
private readonly _configService: ConfigService,
private readonly _injector: Injector,
) {}
get #documentLoaded$() {
const docLoadedEvent = this.#instance.UI.Events.DOCUMENT_LOADED;
const event$ = fromEvent(this.documentViewer, docLoadedEvent).pipe(map(() => true));
return event$.pipe(log('[PDF] Document loaded'), shareLast());
}
get #show$() {
const routeChanged = this._router.events.pipe(
filter(event => event instanceof ActivationStart),
map(() => false),
);
return merge(routeChanged, this.documentLoaded$);
}
async init(htmlElement: HTMLElement) {
this.#instance = await this.#getInstance(htmlElement);
console.log('[PDF] Initialized');
this.documentViewer = this.#instance.Core.documentViewer;
this.annotationManager = this.#instance.Core.annotationManager;
this.documentLoaded$ = this.#documentLoaded$;
this.show$ = this.#show$;
this.#setSelectionMode();
this.#configureElements();
}
loadDocument(blob: Blob, file: File) {
console.log('[PDF] Loading document', blob, file);
const onError = () => {
this._loadingService.stop();
this._errorService.set(DocLoadingError);
// this.stateService.reloadBlob();
};
// const pdfNet = this._instance.Core.PDFNet;
// console.log('pdfnet', pdfNet);
// await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
// console.log('pdfnet initialized');
// const document = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer());
// console.log('document initialized');
// await document.flattenAnnotations(false);
// console.log(document);
this.#instance.UI.loadDocument(blob, { filename: file?.filename + '.pdf' ?? 'document.pdf', onError });
}
#configureElements() {
this.#instance.UI.disableElements(uselessElements);
}
#setSelectionMode(): void {
const textTool = this.#instance.Core.Tools.TextTool as unknown as TextTool;
textTool.SELECTION_MODE = this._configService.values.SELECTION_MODE;
}
#getInstance(htmlElement: HTMLElement) {
const options: WebViewerOptions = {
licenseKey: environment.licenseKey ? atob(environment.licenseKey) : null,
fullAPI: true,
path: this._convertPath('/assets/wv-resources'),
css: this._convertPath('/assets/pdftron/stylesheet.css'),
backendType: 'ems',
};
return WebViewer(options, htmlElement);
}
}

View File

@ -1,7 +1,9 @@
import { InjectionToken } from '@angular/core';
export const BASE_HREF: InjectionToken<string> = new InjectionToken<string>('BASE_HREF');
export const DOSSIER_ID: InjectionToken<string> = new InjectionToken<string>('DOSSIER_ID');
export const BASE_HREF = new InjectionToken<string>('BASE_HREF');
export type BaseHrefFn = (path: string) => string;
export const BASE_HREF_FN = new InjectionToken<BaseHrefFn>('Convert path function');
export const DOSSIER_ID = new InjectionToken<string>('DOSSIER_ID');
export const ACTIVE_DOSSIERS_SERVICE = new InjectionToken<string>('Active dossiers service');
export const ARCHIVED_DOSSIERS_SERVICE = new InjectionToken<string>('Archived dossiers service');