diff --git a/apps/red-ui/src/app/app-routing.module.ts b/apps/red-ui/src/app/app-routing.module.ts index 0033f3247..e118d9941 100644 --- a/apps/red-ui/src/app/app-routing.module.ts +++ b/apps/red-ui/src/app/app-routing.module.ts @@ -146,6 +146,7 @@ const routes: IqserRoutes = [ }, { path: 'downloads', + // TODO: transform into a lazy loaded module component: DownloadsListScreenComponent, canActivate: [CompositeRouteGuard, IqserPermissionsGuard], data: { @@ -224,7 +225,7 @@ const routes: IqserRoutes = [ @NgModule({ imports: [RouterModule.forRoot(routes, { scrollPositionRestoration: 'enabled' })], - providers: [{ provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }], + providers: [{ provide: RouteReuseStrategy, useExisting: CustomRouteReuseStrategy }], exports: [RouterModule], }) export class AppRoutingModule {} diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts index 74f342a1c..176786262 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts @@ -40,7 +40,7 @@ import { AnnotationDrawService } from '../pdf-viewer/services/annotation-draw.se import { AnnotationProcessingService } from './services/annotation-processing.service'; import { Dictionary, File, ViewModes } from '@red/domain'; import { PermissionsService } from '@services/permissions.service'; -import { combineLatest, firstValueFrom, from, Observable, of, pairwise } from 'rxjs'; +import { combineLatest, firstValueFrom, Observable, of, pairwise } from 'rxjs'; import { PreferencesKeys, UserPreferenceService } from '@users/user-preference.service'; import { byId, byPage, download, handleFilterDelta, hasChanges } from '../../utils'; import { FilesService } from '@services/files/files.service'; @@ -617,9 +617,8 @@ export class FilePreviewScreenComponent this.addActiveScreenSubscription = this.state.blob$ .pipe( - switchMap(blob => from(this._documentViewer.lock()).pipe(map(() => blob))), tap(() => this._errorService.clear()), - tap(blob => this.pdf.loadDocument(blob, this.state.file, () => this.state.reloadBlob())), + switchMap(blob => this.pdf.loadDocument(blob, this.state.file, () => this.state.reloadBlob())), ) .subscribe(); diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-draw.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-draw.service.ts index 7f6f45016..d7d9ab624 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-draw.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-draw.service.ts @@ -36,7 +36,7 @@ export class AnnotationDrawService { async draw(annotations: List, hideSkipped: boolean, dossierTemplateId: string) { try { - await this._draw(annotations, hideSkipped, dossierTemplateId); + await this._pdf.runWithCleanup(async () => await this._draw(annotations, hideSkipped, dossierTemplateId)); } catch (e) { console.error(e); } diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/document-viewer.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/document-viewer.service.ts index 6bdb2a5d1..3d50465dc 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/document-viewer.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/document-viewer.service.ts @@ -1,12 +1,12 @@ import { inject, Injectable } from '@angular/core'; import { Core } from '@pdftron/webviewer'; import { NGXLogger } from 'ngx-logger'; -import { fromEvent, merge, Observable } from 'rxjs'; +import { fromEvent, merge, Observable, Subject } from 'rxjs'; import { debounceTime, filter, map, tap } from 'rxjs/operators'; import { ActivatedRoute } from '@angular/router'; import { PdfViewer } from './pdf-viewer.service'; import { UserPreferenceService } from '@users/user-preference.service'; -import { log, shareLast } from '@iqser/common-ui'; +import { log, shareDistinctLast, shareLast } from '@iqser/common-ui'; import { stopAndPrevent, stopAndPreventIfNotAllowed } from '../utils/functions'; import { RotationType, RotationTypes } from '@red/domain'; import { AnnotationToolNames } from '../utils/constants'; @@ -23,6 +23,8 @@ export class REDDocumentViewer { selectedText = ''; #document: DocumentViewer; + readonly #documentClosed$ = new Subject(); + readonly #logger = inject(NGXLogger); readonly #userPreferenceService = inject(UserPreferenceService); readonly #pdf = inject(PdfViewer); @@ -37,7 +39,7 @@ export class REDDocumentViewer { } get #documentUnloaded$() { - const event$ = fromEvent(this.#document, 'documentUnloaded'); + const event$ = merge(fromEvent(this.#document, 'documentUnloaded'), this.#documentClosed$); const toBool$ = event$.pipe(map(() => false)); return toBool$.pipe(tap(() => this.#logger.info('[PDF] Document unloaded'))); @@ -48,10 +50,14 @@ export class REDDocumentViewer { const toBool$ = event$.pipe(map(() => true)); return toBool$.pipe( - tap(() => this.#flattenAnnotations()), - tap(() => this.#setCurrentPage()), - tap(() => this.#setInitialDisplayMode()), - tap(() => this.updateTooltipsVisibility()), + tap(() => + this.#pdf.runWithCleanup(async () => { + await this.#flattenAnnotations(); + this.#setCurrentPage(); + this.#setInitialDisplayMode(); + this.updateTooltipsVisibility(); + }), + ), tap(() => this.#logger.info('[PDF] Document loaded')), ); } @@ -86,7 +92,7 @@ export class REDDocumentViewer { } get #loaded$() { - return merge(this.#documentUnloaded$, this.#documentLoaded$).pipe(shareLast()); + return merge(this.#documentUnloaded$, this.#documentLoaded$).pipe(shareDistinctLast()); } clearSelection() { @@ -95,9 +101,16 @@ export class REDDocumentViewer { } close() { - this.#logger.info('[PDF] Closing document'); - this.#document.closeDocument(); - this.#pdf.closeCompareMode(); + this.#documentClosed$.next(undefined); + + const closeAction = async () => { + this.#logger.info('[PDF] Closing document'); + this.#document.closeDocument(); + this.#pdf.closeCompareMode(); + await this.#pdf.instance.UI.closeDocument(); + }; + + this.#pdf.runWithCleanup(closeAction).then(); } updateTooltipsVisibility(): void { @@ -113,17 +126,6 @@ export class REDDocumentViewer { this.textSelected$ = this.#textSelected$; } - async lock() { - const document = await this.PDFDoc; - if (!document) { - return false; - } - - await document.lock(); - this.#logger.info('[PDF] Locked'); - return true; - } - async blob() { const data = await this.document.getFileData(); return new Blob([new Uint8Array(data)], { type: 'application/pdf' }); diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/pdf-viewer.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/pdf-viewer.service.ts index 2ac4a9e15..aaf4f88d2 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/pdf-viewer.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/pdf-viewer.service.ts @@ -1,11 +1,10 @@ -import { Inject, Injectable, Injector } from '@angular/core'; +import { inject, Inject, Injectable, Injector } from '@angular/core'; import WebViewer, { Core, WebViewerInstance, WebViewerOptions } from '@pdftron/webviewer'; -import { BASE_HREF_FN, BaseHrefFn, ErrorService, shareDistinctLast } from '@iqser/common-ui'; -import { File, IHeaderElement } from '@red/domain'; +import { BASE_HREF_FN, BaseHrefFn, ErrorService, getConfig, shareDistinctLast } from '@iqser/common-ui'; +import { AppConfig, File, IHeaderElement } from '@red/domain'; import { ActivatedRoute } from '@angular/router'; import { map, startWith } from 'rxjs/operators'; import { BehaviorSubject, combineLatest, fromEvent, Observable, switchMap } from 'rxjs'; -import { ConfigService } from '@services/config.service'; import { NGXLogger } from 'ngx-logger'; import { DISABLED_HOTKEYS, DOCUMENT_LOADING_ERROR, SEARCH_OPTIONS, USELESS_ELEMENTS } from '../utils/constants'; import { Rgb } from '../utils/types'; @@ -22,7 +21,7 @@ import Quad = Core.Math.Quad; @Injectable() export class PdfViewer { - readonly currentPage$ = this._activatedRoute.queryParamMap.pipe( + readonly currentPage$ = inject(ActivatedRoute).queryParamMap.pipe( map(params => Number(params.get('page') ?? '1')), shareDistinctLast(), ); @@ -37,11 +36,13 @@ export class PdfViewer { totalPages$: Observable; #instance: WebViewerInstance; + readonly #licenseKey = inject(LicenseService).activeLicenseKey; + readonly #config = getConfig(); readonly #compareMode$ = new BehaviorSubject(false); readonly #searchButton: IHeaderElement = { type: 'actionButton', img: this._convertPath('/assets/icons/general/pdftron-action-search.svg'), - title: this._translateService.instant('pdf-viewer.text-popup.actions.search'), + title: inject(TranslateService).instant('pdf-viewer.text-popup.actions.search'), onClick: () => { this.#instance.UI.openElements(['searchPanel']); setTimeout(() => this.#searchForSelectedText(), 250); @@ -51,10 +52,7 @@ export class PdfViewer { constructor( private readonly _logger: NGXLogger, private readonly _injector: Injector, - private readonly _activatedRoute: ActivatedRoute, - private readonly _licenseService: LicenseService, private readonly _errorService: ErrorService, - private readonly _translateService: TranslateService, private readonly _userPreferenceService: UserPreferenceService, @Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn, ) {} @@ -154,26 +152,28 @@ export class PdfViewer { this.#instance.Core.setCustomFontURL('https://' + window.location.host + this._convertPath('/assets/pdftron')); } - try { - await this.PDFNet.initialize(this._licenseService.activeLicenseKey); - } catch (e) { - this._errorService.set(e); - throw e; - } + await this.runWithCleanup(async () => { + try { + await this.PDFNet.initialize(this.#licenseKey); + } catch (e) { + this._errorService.set(e); + throw e; + } - this.#instance.UI.setTheme(this._userPreferenceService.getTheme()); - this._logger.info('[PDF] Initialized'); + this.#instance.UI.setTheme(this._userPreferenceService.getTheme()); + this._logger.info('[PDF] Initialized'); - this.documentViewer = this.#instance.Core.documentViewer; + this.documentViewer = this.#instance.Core.documentViewer; - this.compareMode$ = this.#compareMode$.asObservable(); - this.pageChanged$ = this.#pageChanged$.pipe(shareDistinctLast()); - this.totalPages$ = this.#totalPages$.pipe(shareDistinctLast()); - this.#setSelectionMode(); - this.#configureElements(); - this.#disableHotkeys(); - this.#clearSearchResultsWhenVisibilityChanged(); - this.#listenForCommandF(); + this.compareMode$ = this.#compareMode$.asObservable(); + this.pageChanged$ = this.#pageChanged$.pipe(shareDistinctLast()); + this.totalPages$ = this.#totalPages$.pipe(shareDistinctLast()); + this.#setSelectionMode(); + this.#configureElements(); + this.#disableHotkeys(); + this.#clearSearchResultsWhenVisibilityChanged(); + this.#listenForCommandF(); + }); return this.#instance; } @@ -196,6 +196,10 @@ export class PdfViewer { this.#compareMode$.next(false); } + runWithCleanup(action: () => Promise | void) { + return this.PDFNet.runWithCleanup(action, this.#licenseKey); + } + async loadDocument(blob: Blob, file: File, actionOnError?: () => void) { const onError = () => { this._injector.get(ErrorService).set(DOCUMENT_LOADING_ERROR); @@ -208,7 +212,11 @@ export class PdfViewer { this._logger.info('[PDF] Loading document...'); - this.#instance.UI.loadDocument(blob, { documentId: file.fileId, filename: file?.filename ?? 'document.pdf', onError }); + await this.runWithCleanup(async () => { + const document = await this.documentViewer.getDocument()?.getPDFDoc(); + await document?.lock(); + this.#instance.UI.loadDocument(blob, { documentId: file.fileId, filename: file?.filename ?? 'document.pdf', onError }); + }); } quad(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number) { @@ -307,13 +315,12 @@ export class PdfViewer { #setSelectionMode(): void { const textTool = this.#instance.Core.Tools.TextTool as unknown as TextTool; - const configService = this._injector.get(ConfigService); - textTool.SELECTION_MODE = configService.values.SELECTION_MODE; + textTool.SELECTION_MODE = this.#config.SELECTION_MODE; } #getInstance(htmlElement: HTMLElement) { const options: WebViewerOptions = { - licenseKey: this._licenseService.activeLicenseKey, + licenseKey: this.#licenseKey, fullAPI: true, path: this._convertPath('/assets/wv-resources'), css: this._convertPath('/assets/pdftron/stylesheet.css'),