RED-3988: extract some events
This commit is contained in:
parent
428975cd4e
commit
5c3c2b222c
@ -24,8 +24,7 @@ import { environment } from '@environments/environment';
|
|||||||
import { AnnotationDrawService } from '../../services/annotation-draw.service';
|
import { AnnotationDrawService } from '../../services/annotation-draw.service';
|
||||||
import { AnnotationActionsService } from '../../services/annotation-actions.service';
|
import { AnnotationActionsService } from '../../services/annotation-actions.service';
|
||||||
import { UserPreferenceService } from '@services/user-preference.service';
|
import { UserPreferenceService } from '@services/user-preference.service';
|
||||||
import { BASE_HREF } from '../../../../tokens';
|
import { BASE_HREF_FN, BaseHrefFn } from '../../../../tokens';
|
||||||
import { ConfigService } from '@services/config.service';
|
|
||||||
import { AutoUnsubscribe, ConfirmationDialogInput, ErrorService, LoadingService, log } from '@iqser/common-ui';
|
import { AutoUnsubscribe, ConfirmationDialogInput, ErrorService, LoadingService, log } from '@iqser/common-ui';
|
||||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||||
import { toPosition } from '../../utils/pdf-calculation.utils';
|
import { toPosition } from '../../utils/pdf-calculation.utils';
|
||||||
@ -33,7 +32,7 @@ import { MultiSelectService } from '../../services/multi-select.service';
|
|||||||
import { FilePreviewStateService } from '../../services/file-preview-state.service';
|
import { FilePreviewStateService } from '../../services/file-preview-state.service';
|
||||||
import { map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
import { map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||||
import { PageRotationService } from '../../services/page-rotation.service';
|
import { PageRotationService } from '../../services/page-rotation.service';
|
||||||
import { ALLOWED_KEYBOARD_SHORTCUTS, HeaderElements, TextPopups } from '../../utils/constants';
|
import { HeaderElements, TextPopups } from '../../utils/constants';
|
||||||
import { FilePreviewDialogService } from '../../services/file-preview-dialog.service';
|
import { FilePreviewDialogService } from '../../services/file-preview-dialog.service';
|
||||||
import { loadCompareDocumentWrapper } from '../../utils/compare-mode.utils';
|
import { loadCompareDocumentWrapper } from '../../utils/compare-mode.utils';
|
||||||
import { from } from 'rxjs';
|
import { from } from 'rxjs';
|
||||||
@ -55,16 +54,20 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
@Input() canPerformActions = false;
|
@Input() canPerformActions = false;
|
||||||
@Output() readonly annotationSelected = this.#annotationSelected$;
|
@Output() readonly annotationSelected = this.#annotationSelected$;
|
||||||
@Output() readonly manualAnnotationRequested = new EventEmitter<ManualRedactionEntryWrapper>();
|
@Output() readonly manualAnnotationRequested = new EventEmitter<ManualRedactionEntryWrapper>();
|
||||||
@Output() readonly pageChanged = new EventEmitter<number>();
|
@Output() readonly pageChanged = this.pdf.pageChanged$.pipe(tap(() => this._handleCustomActions()));
|
||||||
@Output() readonly keyUp = new EventEmitter<KeyboardEvent>();
|
@Output() readonly keyUp = this.pdf.keyUp$;
|
||||||
@ViewChild('compareFileInput', { static: true }) compareFileInput: ElementRef;
|
@ViewChild('compareFileInput', { static: true }) compareFileInput: ElementRef;
|
||||||
instance: WebViewerInstance;
|
instance: WebViewerInstance;
|
||||||
documentViewer: Core.DocumentViewer;
|
|
||||||
annotationManager: Core.AnnotationManager;
|
|
||||||
private _selectedText = '';
|
private _selectedText = '';
|
||||||
|
readonly #visibilityOffIcon = this._convertPath('/assets/icons/general/visibility-off.svg');
|
||||||
|
readonly #visibilityIcon = this._convertPath('/assets/icons/general/visibility.svg');
|
||||||
|
readonly #searchIcon = this._convertPath('/assets/icons/general/pdftron-action-search.svg');
|
||||||
|
readonly #falsePositiveIcon = this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg');
|
||||||
|
readonly #addRedactionIcon = this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg');
|
||||||
|
readonly #addDictIcon = this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg');
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(BASE_HREF) private readonly _baseHref: string,
|
@Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn,
|
||||||
private readonly _translateService: TranslateService,
|
private readonly _translateService: TranslateService,
|
||||||
private readonly _manualRedactionService: ManualRedactionService,
|
private readonly _manualRedactionService: ManualRedactionService,
|
||||||
private readonly _dialogService: FilePreviewDialogService,
|
private readonly _dialogService: FilePreviewDialogService,
|
||||||
@ -72,7 +75,6 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
private readonly _userPreferenceService: UserPreferenceService,
|
private readonly _userPreferenceService: UserPreferenceService,
|
||||||
private readonly _annotationDrawService: AnnotationDrawService,
|
private readonly _annotationDrawService: AnnotationDrawService,
|
||||||
private readonly _annotationActionsService: AnnotationActionsService,
|
private readonly _annotationActionsService: AnnotationActionsService,
|
||||||
private readonly _configService: ConfigService,
|
|
||||||
private readonly _loadingService: LoadingService,
|
private readonly _loadingService: LoadingService,
|
||||||
private readonly _pageRotationService: PageRotationService,
|
private readonly _pageRotationService: PageRotationService,
|
||||||
private readonly _fileDataService: FileDataService,
|
private readonly _fileDataService: FileDataService,
|
||||||
@ -80,8 +82,8 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
private readonly _errorService: ErrorService,
|
private readonly _errorService: ErrorService,
|
||||||
private readonly _annotationManager: REDAnnotationManager,
|
private readonly _annotationManager: REDAnnotationManager,
|
||||||
readonly pdf: PdfViewer,
|
readonly pdf: PdfViewer,
|
||||||
readonly stateService: FilePreviewStateService,
|
private readonly _state: FilePreviewStateService,
|
||||||
readonly multiSelectService: MultiSelectService,
|
private readonly _multiSelectService: MultiSelectService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@ -93,11 +95,11 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this._loadViewer();
|
this._loadViewer();
|
||||||
|
|
||||||
this.addActiveScreenSubscription = this.stateService.blob$
|
this.addActiveScreenSubscription = this._state.blob$
|
||||||
.pipe(
|
.pipe(
|
||||||
log('Reload blob'),
|
log('Reload blob'),
|
||||||
switchMap(blob => from(this.pdf.lockDocument()).pipe(map(() => blob))),
|
switchMap(blob => from(this.pdf.lockDocument()).pipe(map(() => blob))),
|
||||||
withLatestFrom(this.stateService.file$),
|
withLatestFrom(this._state.file$),
|
||||||
tap(() => this._errorService.clear()),
|
tap(() => this._errorService.clear()),
|
||||||
tap(([blob, file]) => this.pdf.loadDocument(blob, file)),
|
tap(([blob, file]) => this.pdf.loadDocument(blob, file)),
|
||||||
tap(() => this._pageRotationService.clearRotationsHideActions()),
|
tap(() => this._pageRotationService.clearRotationsHideActions()),
|
||||||
@ -124,16 +126,16 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
fileReader.onload = async () => {
|
fileReader.onload = async () => {
|
||||||
const pdfNet = this.instance.Core.PDFNet;
|
const pdfNet = this.instance.Core.PDFNet;
|
||||||
|
|
||||||
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
|
await pdfNet.initialize(environment.licenseKey ? window.atob(environment.licenseKey) : null);
|
||||||
|
|
||||||
const compareDocument = await pdfNet.PDFDoc.createFromBuffer(fileReader.result as ArrayBuffer);
|
const compareDocument = await pdfNet.PDFDoc.createFromBuffer(fileReader.result as ArrayBuffer);
|
||||||
const blob = await this.stateService.blob;
|
const blob = await this._state.blob;
|
||||||
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer());
|
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer());
|
||||||
|
|
||||||
const loadCompareDocument = async () => {
|
const loadCompareDocument = async () => {
|
||||||
this._loadingService.start();
|
this._loadingService.start();
|
||||||
const mergedDocument = await pdfNet.PDFDoc.create();
|
const mergedDocument = await pdfNet.PDFDoc.create();
|
||||||
const file = this.stateService.file;
|
const file = this._state.file;
|
||||||
await loadCompareDocumentWrapper(
|
await loadCompareDocumentWrapper(
|
||||||
currentDocument,
|
currentDocument,
|
||||||
compareDocument,
|
compareDocument,
|
||||||
@ -184,13 +186,13 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
|
|
||||||
if (action === 'deselected') {
|
if (action === 'deselected') {
|
||||||
// Remove deselected annotations from selected list
|
// Remove deselected annotations from selected list
|
||||||
nextAnnotations = this.annotationManager.getSelectedAnnotations().filter(ann => !annotations.some(a => a.Id === ann.Id));
|
nextAnnotations = this._annotationManager.selectedAnnotations.filter(ann => !annotations.some(a => a.Id === ann.Id));
|
||||||
} else if (!this.multiSelectService.isEnabled) {
|
} else if (!this._multiSelectService.isEnabled) {
|
||||||
// Only choose the last selected annotation, to bypass viewer multi select
|
// Only choose the last selected annotation, to bypass viewer multi select
|
||||||
nextAnnotations = annotations;
|
nextAnnotations = annotations;
|
||||||
} else {
|
} else {
|
||||||
// Get selected annotations from the manager, no intervention needed
|
// Get selected annotations from the manager, no intervention needed
|
||||||
nextAnnotations = this.annotationManager.getSelectedAnnotations();
|
nextAnnotations = this._annotationManager.selectedAnnotations;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.annotationSelected.emit(nextAnnotations.map(ann => ann.Id));
|
// this.annotationSelected.emit(nextAnnotations.map(ann => ann.Id));
|
||||||
@ -199,7 +201,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
return nextAnnotations.map(ann => ann.Id);
|
return nextAnnotations.map(ann => ann.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.multiSelectService.isEnabled) {
|
if (!this._multiSelectService.isEnabled) {
|
||||||
const notSelected = this._fileDataService.all.filter(wrapper => !nextAnnotations.some(ann => ann.Id === wrapper.id));
|
const notSelected = this._fileDataService.all.filter(wrapper => !nextAnnotations.some(ann => ann.Id === wrapper.id));
|
||||||
this._annotationManager.deselectAnnotations(notSelected);
|
this._annotationManager.deselectAnnotations(notSelected);
|
||||||
}
|
}
|
||||||
@ -211,87 +213,35 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
|
|
||||||
private _loadViewer() {
|
private _loadViewer() {
|
||||||
this.instance = this.pdf.instance;
|
this.instance = this.pdf.instance;
|
||||||
this.documentViewer = this.pdf.documentViewer;
|
|
||||||
this.annotationManager = this.pdf.annotationManager;
|
|
||||||
|
|
||||||
this._configureElements();
|
this._configureElements();
|
||||||
this._configureTextPopup();
|
this._configureTextPopup();
|
||||||
|
|
||||||
this.annotationManager.addEventListener('annotationChanged', (annotations: Annotation[]) => {
|
this.pdf.documentViewer.addEventListener('textSelected', (quads, selectedText, pageNumber: number) => {
|
||||||
// when a rectangle is drawn,
|
|
||||||
// it returns one annotation with tool name 'AnnotationCreateRectangle;
|
|
||||||
// this will auto select rectangle after drawing
|
|
||||||
if (annotations.length === 1 && annotations[0].ToolName === 'AnnotationCreateRectangle') {
|
|
||||||
this.annotationManager.selectAnnotations(annotations);
|
|
||||||
annotations[0].disableRotationControl();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.documentViewer.addEventListener('pageNumberUpdated', (pageNumber: number) => {
|
|
||||||
this._annotationManager.deselectAnnotations();
|
|
||||||
console.log(pageNumber);
|
|
||||||
this._ngZone.run(() => this.pageChanged.emit(pageNumber));
|
|
||||||
return this._handleCustomActions();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.documentViewer.addEventListener('keyUp', ($event: KeyboardEvent) => {
|
|
||||||
// arrows and full-screen
|
|
||||||
if (($event.target as HTMLElement)?.tagName?.toLowerCase() !== 'input') {
|
|
||||||
if ($event.key.startsWith('Arrow') || $event.key === 'f') {
|
|
||||||
this._ngZone.run(() => this.keyUp.emit($event));
|
|
||||||
$event.preventDefault();
|
|
||||||
$event.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ALLOWED_KEYBOARD_SHORTCUTS.includes($event.key)) {
|
|
||||||
$event.preventDefault();
|
|
||||||
$event.stopPropagation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.documentViewer.addEventListener('textSelected', (quads, selectedText, pageNumber: number) => {
|
|
||||||
this._selectedText = selectedText;
|
this._selectedText = selectedText;
|
||||||
const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE];
|
|
||||||
|
|
||||||
const file = this.stateService.file;
|
|
||||||
|
|
||||||
if (this.pdf.isCompare && pageNumber % 2 === 0) {
|
if (this.pdf.isCompare && pageNumber % 2 === 0) {
|
||||||
this.instance.UI.disableElements(['textPopup']);
|
this.pdf.disable('textPopup');
|
||||||
} else {
|
} else {
|
||||||
this.instance.UI.enableElements(['textPopup']);
|
this.pdf.enable('textPopup');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCurrentPageExcluded = file.isPageExcluded(this.pdf.currentPage);
|
const isCurrentPageExcluded = this._state.file.isPageExcluded(this.pdf.currentPage);
|
||||||
|
const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE];
|
||||||
|
|
||||||
if (selectedText.length > 2 && this.canPerformActions && !isCurrentPageExcluded) {
|
if (selectedText.length > 2 && this.canPerformActions && !isCurrentPageExcluded) {
|
||||||
this.instance.UI.enableElements(textActions);
|
this.pdf.enable(textActions);
|
||||||
} else {
|
} else {
|
||||||
this.instance.UI.disableElements(textActions);
|
this.pdf.disable(textActions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.instance.UI.iframeWindow.addEventListener('visibilityChanged', (event: any) => {
|
|
||||||
if (event.detail.element === 'searchPanel') {
|
|
||||||
const inputElement = this.instance.UI.iframeWindow.document.getElementById('SearchPanel__input') as HTMLInputElement;
|
|
||||||
setTimeout(() => {
|
|
||||||
inputElement.value = '';
|
|
||||||
}, 0);
|
|
||||||
if (!event.detail.isVisible) {
|
|
||||||
this.documentViewer.clearSearchResults();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _convertPath(path: string): string {
|
|
||||||
return this._baseHref + path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggleRectangleAnnotationAction(readonly = false) {
|
private _toggleRectangleAnnotationAction(readonly = false) {
|
||||||
if (!readonly) {
|
if (!readonly) {
|
||||||
this.instance.UI.enableElements([TextPopups.ADD_RECTANGLE]);
|
this.pdf.enable(TextPopups.ADD_RECTANGLE);
|
||||||
} else {
|
} else {
|
||||||
this.instance.UI.disableElements([TextPopups.ADD_RECTANGLE]);
|
this.pdf.disable(TextPopups.ADD_RECTANGLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,13 +249,8 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
this._headerConfigService.initialize(this.compareFileInput);
|
this._headerConfigService.initialize(this.compareFileInput);
|
||||||
|
|
||||||
const dossierTemplateId = this.dossier.dossierTemplateId;
|
const dossierTemplateId = this.dossier.dossierTemplateId;
|
||||||
|
const color = this._annotationDrawService.getAndConvertColor(dossierTemplateId, 'manual');
|
||||||
this.documentViewer.getTool('AnnotationCreateRectangle').setStyles({
|
this.pdf.setRectangleToolStyles(color);
|
||||||
StrokeThickness: 2,
|
|
||||||
StrokeColor: this._annotationDrawService.getAndConvertColor(dossierTemplateId, 'manual'),
|
|
||||||
FillColor: this._annotationDrawService.getAndConvertColor(dossierTemplateId, 'manual'),
|
|
||||||
Opacity: 0.6,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) {
|
#configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) {
|
||||||
@ -332,18 +277,16 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
this.instance.UI.annotationPopup.add([
|
this.instance.UI.annotationPopup.add([
|
||||||
{
|
{
|
||||||
type: 'actionButton',
|
type: 'actionButton',
|
||||||
img: allAreVisible
|
img: allAreVisible ? this.#visibilityOffIcon : this.#visibilityIcon,
|
||||||
? this._convertPath('/assets/icons/general/visibility-off.svg')
|
|
||||||
: this._convertPath('/assets/icons/general/visibility.svg'),
|
|
||||||
title: this._translateService.instant(`annotation-actions.${allAreVisible ? 'hide' : 'show'}`),
|
title: this._translateService.instant(`annotation-actions.${allAreVisible ? 'hide' : 'show'}`),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
this._ngZone.run(() => {
|
this._ngZone.run(() => {
|
||||||
if (allAreVisible) {
|
if (allAreVisible) {
|
||||||
this.annotationManager.hideAnnotations(viewerAnnotations);
|
this._annotationManager.hideAnnotations(viewerAnnotations);
|
||||||
} else {
|
} else {
|
||||||
this.annotationManager.showAnnotations(viewerAnnotations);
|
this._annotationManager.showAnnotations(viewerAnnotations);
|
||||||
}
|
}
|
||||||
this.annotationManager.deselectAllAnnotations();
|
this._annotationManager.deselectAnnotations();
|
||||||
this._fileDataService.updateHiddenAnnotations(viewerAnnotations, allAreVisible);
|
this._fileDataService.updateHiddenAnnotations(viewerAnnotations, allAreVisible);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -361,7 +304,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
{
|
{
|
||||||
type: 'actionButton',
|
type: 'actionButton',
|
||||||
dataElement: TextPopups.ADD_RECTANGLE,
|
dataElement: TextPopups.ADD_RECTANGLE,
|
||||||
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'),
|
img: this.#addRedactionIcon,
|
||||||
title: this.#getTitle(ManualRedactionEntryTypes.REDACTION),
|
title: this.#getTitle(ManualRedactionEntryTypes.REDACTION),
|
||||||
onClick: () => this._addRectangleManualRedaction(),
|
onClick: () => this._addRectangleManualRedaction(),
|
||||||
},
|
},
|
||||||
@ -370,7 +313,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _addRectangleManualRedaction() {
|
private _addRectangleManualRedaction() {
|
||||||
const activeAnnotation = this.annotationManager.getSelectedAnnotations()[0];
|
const activeAnnotation = this._annotationManager.selectedAnnotations[0];
|
||||||
const activePage = activeAnnotation.getPageNumber();
|
const activePage = activeAnnotation.getPageNumber();
|
||||||
const quads = [this._annotationDrawService.annotationToQuads(activeAnnotation)];
|
const quads = [this._annotationDrawService.annotationToQuads(activeAnnotation)];
|
||||||
const manualRedactionEntry = this._getManualRedaction({ [activePage]: quads });
|
const manualRedactionEntry = this._getManualRedaction({ [activePage]: quads });
|
||||||
@ -387,10 +330,10 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
private _configureTextPopup() {
|
private _configureTextPopup() {
|
||||||
const searchButton = {
|
const searchButton = {
|
||||||
type: 'actionButton',
|
type: 'actionButton',
|
||||||
img: this._convertPath('/assets/icons/general/pdftron-action-search.svg'),
|
img: this.#searchIcon,
|
||||||
title: this._translateService.instant('pdf-viewer.text-popup.actions.search'),
|
title: this._translateService.instant('pdf-viewer.text-popup.actions.search'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const text = this.documentViewer.getSelectedText();
|
const text = this.pdf.documentViewer.getSelectedText();
|
||||||
const searchOptions = {
|
const searchOptions = {
|
||||||
caseSensitive: true, // match case
|
caseSensitive: true, // match case
|
||||||
wholeWord: true, // match whole words only
|
wholeWord: true, // match whole words only
|
||||||
@ -410,7 +353,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
popups.push({
|
popups.push({
|
||||||
type: 'actionButton',
|
type: 'actionButton',
|
||||||
dataElement: TextPopups.ADD_FALSE_POSITIVE,
|
dataElement: TextPopups.ADD_FALSE_POSITIVE,
|
||||||
img: this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg'),
|
img: this.#falsePositiveIcon,
|
||||||
title: this.#getTitle(ManualRedactionEntryTypes.FALSE_POSITIVE),
|
title: this.#getTitle(ManualRedactionEntryTypes.FALSE_POSITIVE),
|
||||||
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.FALSE_POSITIVE),
|
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.FALSE_POSITIVE),
|
||||||
});
|
});
|
||||||
@ -419,14 +362,14 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
popups.push({
|
popups.push({
|
||||||
type: 'actionButton',
|
type: 'actionButton',
|
||||||
dataElement: TextPopups.ADD_REDACTION,
|
dataElement: TextPopups.ADD_REDACTION,
|
||||||
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'),
|
img: this.#addRedactionIcon,
|
||||||
title: this.#getTitle(ManualRedactionEntryTypes.REDACTION),
|
title: this.#getTitle(ManualRedactionEntryTypes.REDACTION),
|
||||||
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.REDACTION),
|
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.REDACTION),
|
||||||
});
|
});
|
||||||
popups.push({
|
popups.push({
|
||||||
type: 'actionButton',
|
type: 'actionButton',
|
||||||
dataElement: TextPopups.ADD_DICTIONARY,
|
dataElement: TextPopups.ADD_DICTIONARY,
|
||||||
img: this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'),
|
img: this.#addDictIcon,
|
||||||
title: this.#getTitle(ManualRedactionEntryTypes.DICTIONARY),
|
title: this.#getTitle(ManualRedactionEntryTypes.DICTIONARY),
|
||||||
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.DICTIONARY),
|
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.DICTIONARY),
|
||||||
});
|
});
|
||||||
@ -441,8 +384,8 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _addManualRedactionOfType(type: ManualRedactionEntryType) {
|
private _addManualRedactionOfType(type: ManualRedactionEntryType) {
|
||||||
const selectedQuads: Readonly<Record<string, Core.Math.Quad[]>> = this.documentViewer.getSelectedTextQuads();
|
const selectedQuads: Readonly<Record<string, Core.Math.Quad[]>> = this.pdf.documentViewer.getSelectedTextQuads();
|
||||||
const text = this.documentViewer.getSelectedText();
|
const text = this.pdf.documentViewer.getSelectedText();
|
||||||
const manualRedactionEntry = this._getManualRedaction(selectedQuads, text, true);
|
const manualRedactionEntry = this._getManualRedaction(selectedQuads, text, true);
|
||||||
this.manualAnnotationRequested.emit({ manualRedactionEntry, type });
|
this.manualAnnotationRequested.emit({ manualRedactionEntry, type });
|
||||||
}
|
}
|
||||||
@ -455,7 +398,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
HeaderElements.ROTATE_RIGHT_BUTTON,
|
HeaderElements.ROTATE_RIGHT_BUTTON,
|
||||||
];
|
];
|
||||||
|
|
||||||
const isCurrentPageExcluded = this.stateService.file.isPageExcluded(this.pdf.currentPage);
|
const isCurrentPageExcluded = this._state.file.isPageExcluded(this.pdf.currentPage);
|
||||||
|
|
||||||
if (this.canPerformActions && !isCurrentPageExcluded) {
|
if (this.canPerformActions && !isCurrentPageExcluded) {
|
||||||
try {
|
try {
|
||||||
@ -463,11 +406,11 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// happens
|
// happens
|
||||||
}
|
}
|
||||||
this.instance.UI.enableElements(textPopupsToToggle);
|
this.pdf.enable(textPopupsToToggle);
|
||||||
this._headerConfigService.enable(headerItemsToToggle);
|
this._headerConfigService.enable(headerItemsToToggle);
|
||||||
|
|
||||||
if (this._selectedText.length > 2) {
|
if (this._selectedText.length > 2) {
|
||||||
this.instance.UI.enableElements([TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]);
|
this.pdf.enable([TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -488,7 +431,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
this.instance.UI.disableTools(['AnnotationCreateRectangle']);
|
this.instance.UI.disableTools(['AnnotationCreateRectangle']);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.instance.UI.disableElements(textPopupElementsToDisable);
|
this.pdf.disable(textPopupElementsToDisable);
|
||||||
this._headerConfigService.disable(headerElementsToDisable);
|
this._headerConfigService.disable(headerElementsToDisable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -502,7 +445,7 @@ export class PdfPaginatorComponent extends AutoUnsubscribe implements OnInit, On
|
|||||||
for (const key of Object.keys(quads)) {
|
for (const key of Object.keys(quads)) {
|
||||||
for (const quad of quads[key]) {
|
for (const quad of quads[key]) {
|
||||||
const page = parseInt(key, 10);
|
const page = parseInt(key, 10);
|
||||||
const pageHeight = this.documentViewer.getPageHeight(page);
|
const pageHeight = this.pdf.documentViewer.getPageHeight(page);
|
||||||
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.#translateQuad(page, quad) : quad));
|
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.#translateQuad(page, quad) : quad));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,6 @@ export const ActionsHelpModeKeys = {
|
|||||||
'hint-image': 'picture',
|
'hint-image': 'picture',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ALLOWED_KEYBOARD_SHORTCUTS: List = ['+', '-', 'p', 'r', 'Escape'] as const;
|
|
||||||
|
|
||||||
export const ALL_HOTKEYS: List = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown'] as const;
|
export const ALL_HOTKEYS: List = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown'] as const;
|
||||||
|
|
||||||
export type HeaderElementType =
|
export type HeaderElementType =
|
||||||
|
|||||||
@ -1,19 +1,14 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Core } from '@pdftron/webviewer';
|
import { Core } from '@pdftron/webviewer';
|
||||||
import type { List } from '@iqser/common-ui';
|
import type { List } from '@iqser/common-ui';
|
||||||
import { DeleteAnnotationsOptions } from '@shared/components/reusable-pdf-viewer/types';
|
import { AnnotationPredicate, DeleteAnnotationsOptions } from '@shared/components/reusable-pdf-viewer/types';
|
||||||
import type { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
import type { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||||
import { fromEvent, Observable } from 'rxjs';
|
import { fromEvent, Observable } from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import { tap } from 'rxjs/operators';
|
||||||
|
import { getIds } from '@shared/components/reusable-pdf-viewer/functions';
|
||||||
import AnnotationManager = Core.AnnotationManager;
|
import AnnotationManager = Core.AnnotationManager;
|
||||||
import Annotation = Core.Annotations.Annotation;
|
import Annotation = Core.Annotations.Annotation;
|
||||||
|
|
||||||
type AnnotationPredicate = (value: Annotation) => boolean;
|
|
||||||
|
|
||||||
function getIds(items?: List<string | AnnotationWrapper>): List | undefined {
|
|
||||||
return items?.map(value => (typeof value === 'string' ? value : value.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
@ -37,6 +32,7 @@ export class REDAnnotationManager {
|
|||||||
init(annotationManager: AnnotationManager) {
|
init(annotationManager: AnnotationManager) {
|
||||||
this.#manager = annotationManager;
|
this.#manager = annotationManager;
|
||||||
this.annotationSelected$ = this.#annotationSelected$;
|
this.annotationSelected$ = this.#annotationSelected$;
|
||||||
|
this.#autoSelectRectangleAfterCreation();
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAnnotation(annotation: AnnotationWrapper | string) {
|
deleteAnnotation(annotation: AnnotationWrapper | string) {
|
||||||
@ -90,6 +86,18 @@ export class REDAnnotationManager {
|
|||||||
this.#manager.showAnnotations(annotations);
|
this.#manager.showAnnotations(annotations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#autoSelectRectangleAfterCreation() {
|
||||||
|
this.#manager.addEventListener('annotationChanged', (annotations: Annotation[]) => {
|
||||||
|
// when a rectangle is drawn,
|
||||||
|
// it returns one annotation with tool name 'AnnotationCreateRectangle;
|
||||||
|
// this will auto select rectangle after drawing
|
||||||
|
if (annotations.length === 1 && annotations[0].ToolName === 'AnnotationCreateRectangle') {
|
||||||
|
this.#manager.selectAnnotations(annotations);
|
||||||
|
annotations[0].disableRotationControl();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#getAnnotationsById(ids: List) {
|
#getAnnotationsById(ids: List) {
|
||||||
return ids.map(id => this.#manager.getAnnotationById(id)).filter(a => !!a);
|
return ids.map(id => this.#manager.getAnnotationById(id)).filter(a => !!a);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { CustomError } from '@iqser/common-ui';
|
import { CustomError, List } from '@iqser/common-ui';
|
||||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||||
|
|
||||||
|
export const ALLOWED_KEYBOARD_SHORTCUTS: List = ['+', '-', 'p', 'r', 'Escape'] as const;
|
||||||
|
|
||||||
export const DOCUMENT_LOADING_ERROR = new CustomError(_('error.file-preview.label'), _('error.file-preview.action'), 'iqser:refresh');
|
export const DOCUMENT_LOADING_ERROR = new CustomError(_('error.file-preview.label'), _('error.file-preview.action'), 'iqser:refresh');
|
||||||
|
|
||||||
export const USELESS_ELEMENTS = [
|
export const USELESS_ELEMENTS = [
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { List } from '@iqser/common-ui';
|
||||||
|
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||||
|
import { ALLOWED_KEYBOARD_SHORTCUTS } from '@shared/components/reusable-pdf-viewer/constants';
|
||||||
|
|
||||||
|
export function stopAndPrevent<T extends Event>($event: T) {
|
||||||
|
$event.preventDefault();
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopAndPreventIfNotAllowed($event: KeyboardEvent) {
|
||||||
|
if (!ALLOWED_KEYBOARD_SHORTCUTS.includes($event.key)) {
|
||||||
|
stopAndPrevent($event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIds(items?: List<string | AnnotationWrapper>): List | undefined {
|
||||||
|
return items?.map(value => (typeof value === 'string' ? value : value.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function asList(dataElements: string[] | string): string[] {
|
||||||
|
return typeof dataElements === 'string' ? [dataElements] : dataElements;
|
||||||
|
}
|
||||||
@ -3,20 +3,23 @@ import WebViewer, { Core, WebViewerInstance, WebViewerOptions } from '@pdftron/w
|
|||||||
import { environment } from '@environments/environment';
|
import { environment } from '@environments/environment';
|
||||||
import { BASE_HREF_FN } from '../../../../tokens';
|
import { BASE_HREF_FN } from '../../../../tokens';
|
||||||
import { File } from '@red/domain';
|
import { File } from '@red/domain';
|
||||||
import { ErrorService, shareDistinctLast, shareLast } from '@iqser/common-ui';
|
import { ErrorService, log, shareDistinctLast, shareLast } from '@iqser/common-ui';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators';
|
import { debounceTime, distinctUntilChanged, filter, map, startWith, tap } from 'rxjs/operators';
|
||||||
import { BehaviorSubject, combineLatest, fromEvent, merge, Observable } from 'rxjs';
|
import { BehaviorSubject, combineLatest, fromEvent, merge, Observable } from 'rxjs';
|
||||||
import { ConfigService } from '@services/config.service';
|
import { ConfigService } from '@services/config.service';
|
||||||
import { NGXLogger } from 'ngx-logger';
|
import { NGXLogger } from 'ngx-logger';
|
||||||
import { DISABLED_HOTKEYS, DOCUMENT_LOADING_ERROR, USELESS_ELEMENTS } from './constants';
|
import { DISABLED_HOTKEYS, DOCUMENT_LOADING_ERROR, USELESS_ELEMENTS } from './constants';
|
||||||
import { Rgb } from '@shared/components/reusable-pdf-viewer/types';
|
import { Rgb } from '@shared/components/reusable-pdf-viewer/types';
|
||||||
import { UserPreferenceService } from '@services/user-preference.service';
|
import { UserPreferenceService } from '@services/user-preference.service';
|
||||||
|
import { asList, stopAndPrevent, stopAndPreventIfNotAllowed } from '@shared/components/reusable-pdf-viewer/functions';
|
||||||
|
import { REDAnnotationManager } from '@shared/components/reusable-pdf-viewer/annotation-manager.service';
|
||||||
import AnnotationManager = Core.AnnotationManager;
|
import AnnotationManager = Core.AnnotationManager;
|
||||||
import TextTool = Core.Tools.TextTool;
|
import TextTool = Core.Tools.TextTool;
|
||||||
import Annotation = Core.Annotations.Annotation;
|
import Annotation = Core.Annotations.Annotation;
|
||||||
import TextHighlightAnnotation = Core.Annotations.TextHighlightAnnotation;
|
import TextHighlightAnnotation = Core.Annotations.TextHighlightAnnotation;
|
||||||
import DocumentViewer = Core.DocumentViewer;
|
import DocumentViewer = Core.DocumentViewer;
|
||||||
|
import Color = Core.Annotations.Color;
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@ -34,8 +37,10 @@ export class PdfViewer {
|
|||||||
|
|
||||||
loaded$: Observable<boolean>;
|
loaded$: Observable<boolean>;
|
||||||
pageComplete$: Observable<unknown>;
|
pageComplete$: Observable<unknown>;
|
||||||
|
pageChanged$: Observable<number>;
|
||||||
compareMode$: Observable<boolean>;
|
compareMode$: Observable<boolean>;
|
||||||
totalPages$: Observable<number>;
|
totalPages$: Observable<number>;
|
||||||
|
keyUp$: Observable<KeyboardEvent>;
|
||||||
|
|
||||||
#instance: WebViewerInstance;
|
#instance: WebViewerInstance;
|
||||||
readonly #compareMode$ = new BehaviorSubject(false);
|
readonly #compareMode$ = new BehaviorSubject(false);
|
||||||
@ -43,6 +48,7 @@ export class PdfViewer {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly _logger: NGXLogger,
|
private readonly _logger: NGXLogger,
|
||||||
private readonly _activatedRoute: ActivatedRoute,
|
private readonly _activatedRoute: ActivatedRoute,
|
||||||
|
private readonly _annotationManager: REDAnnotationManager,
|
||||||
private readonly _userPreferenceService: UserPreferenceService,
|
private readonly _userPreferenceService: UserPreferenceService,
|
||||||
private readonly _injector: Injector,
|
private readonly _injector: Injector,
|
||||||
) {}
|
) {}
|
||||||
@ -81,8 +87,18 @@ export class PdfViewer {
|
|||||||
return this.isCompare ? Math.ceil(currentInternalPage / 2) : currentInternalPage;
|
return this.isCompare ? Math.ceil(currentInternalPage / 2) : currentInternalPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get #keyUp$() {
|
||||||
|
return fromEvent<KeyboardEvent>(this.documentViewer, 'keyUp').pipe(
|
||||||
|
tap(stopAndPreventIfNotAllowed),
|
||||||
|
filter($event => ($event.target as HTMLElement)?.tagName?.toLowerCase() !== 'input'),
|
||||||
|
filter($event => $event.key.startsWith('Arrow') || $event.key === 'f'),
|
||||||
|
tap(stopAndPrevent),
|
||||||
|
log('[PDF] Keyboard shortcut'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get #totalPages$() {
|
get #totalPages$() {
|
||||||
const layoutChanged$ = fromEvent(this.documentViewer, 'layoutChanged');
|
const layoutChanged$ = fromEvent(this.documentViewer, 'layoutChanged').pipe(startWith(''));
|
||||||
const pageCount$ = layoutChanged$.pipe(
|
const pageCount$ = layoutChanged$.pipe(
|
||||||
map(() => this.pageCount),
|
map(() => this.pageCount),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
@ -123,6 +139,11 @@ export class PdfViewer {
|
|||||||
return this.documentViewer.getCurrentPage();
|
return this.documentViewer.getCurrentPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get #pageChanged$() {
|
||||||
|
const page$ = fromEvent<number>(this.documentViewer, 'pageNumberUpdated');
|
||||||
|
return page$.pipe(tap(() => this._annotationManager.deselectAnnotations()));
|
||||||
|
}
|
||||||
|
|
||||||
navigateTo(page: string | number) {
|
navigateTo(page: string | number) {
|
||||||
const parsedNumber = typeof page === 'string' ? parseInt(page, 10) : page;
|
const parsedNumber = typeof page === 'string' ? parseInt(page, 10) : page;
|
||||||
const paginationOffset = this.#paginationOffset;
|
const paginationOffset = this.#paginationOffset;
|
||||||
@ -152,14 +173,34 @@ export class PdfViewer {
|
|||||||
this.loaded$ = merge(this.#documentUnloaded$, this.#documentLoaded$).pipe(shareLast());
|
this.loaded$ = merge(this.#documentUnloaded$, this.#documentLoaded$).pipe(shareLast());
|
||||||
this.compareMode$ = this.#compareMode$.asObservable();
|
this.compareMode$ = this.#compareMode$.asObservable();
|
||||||
this.pageComplete$ = this.#pageComplete$.pipe(shareLast());
|
this.pageComplete$ = this.#pageComplete$.pipe(shareLast());
|
||||||
|
this.pageChanged$ = this.#pageChanged$.pipe(shareLast());
|
||||||
this.totalPages$ = this.#totalPages$.pipe(shareDistinctLast());
|
this.totalPages$ = this.#totalPages$.pipe(shareDistinctLast());
|
||||||
|
this.keyUp$ = this.#keyUp$;
|
||||||
this.#setSelectionMode();
|
this.#setSelectionMode();
|
||||||
this.#configureElements();
|
this.#configureElements();
|
||||||
this.#disableHotkeys();
|
this.#disableHotkeys();
|
||||||
|
this.#clearSearchResultsWhenVisibilityChanged();
|
||||||
|
|
||||||
return this.#instance;
|
return this.#instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setRectangleToolStyles(color: Color) {
|
||||||
|
this.documentViewer.getTool('AnnotationCreateRectangle').setStyles({
|
||||||
|
StrokeThickness: 2,
|
||||||
|
StrokeColor: color,
|
||||||
|
FillColor: color,
|
||||||
|
Opacity: 0.6,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enable(dataElements: string[] | string) {
|
||||||
|
this.#instance.UI.enableElements(asList(dataElements));
|
||||||
|
}
|
||||||
|
|
||||||
|
disable(dataElements: string[] | string) {
|
||||||
|
this.#instance.UI.disableElements(asList(dataElements));
|
||||||
|
}
|
||||||
|
|
||||||
getPageHeight(page: number) {
|
getPageHeight(page: number) {
|
||||||
try {
|
try {
|
||||||
return this.documentViewer.getPageHeight(page);
|
return this.documentViewer.getPageHeight(page);
|
||||||
@ -235,6 +276,23 @@ export class PdfViewer {
|
|||||||
return annotation instanceof this.#instance.Core.Annotations.TextHighlightAnnotation;
|
return annotation instanceof this.#instance.Core.Annotations.TextHighlightAnnotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#clearSearchResultsWhenVisibilityChanged() {
|
||||||
|
const iframeWindow = this.#instance.UI.iframeWindow;
|
||||||
|
iframeWindow.addEventListener('visibilityChanged', (event: any) => {
|
||||||
|
if (event.detail.element !== 'searchPanel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputElement = iframeWindow.document.getElementById('SearchPanel__input') as HTMLInputElement;
|
||||||
|
|
||||||
|
setTimeout(() => (inputElement.value = ''), 0);
|
||||||
|
|
||||||
|
if (!event.detail.isVisible) {
|
||||||
|
this.documentViewer.clearSearchResults();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#setInitialDisplayMode() {
|
#setInitialDisplayMode() {
|
||||||
this.#instance.UI.setFitMode('FitPage');
|
this.#instance.UI.setFitMode('FitPage');
|
||||||
const displayModeManager = this.documentViewer.getDisplayModeManager();
|
const displayModeManager = this.documentViewer.getDisplayModeManager();
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
import { Core } from '@pdftron/webviewer';
|
||||||
|
import Annotation = Core.Annotations.Annotation;
|
||||||
|
|
||||||
export interface Rgb {
|
export interface Rgb {
|
||||||
readonly r: number;
|
readonly r: number;
|
||||||
readonly g: number;
|
readonly g: number;
|
||||||
@ -10,3 +13,5 @@ export interface DeleteAnnotationsOptions {
|
|||||||
readonly isUndoRedo?: boolean;
|
readonly isUndoRedo?: boolean;
|
||||||
readonly source?: string;
|
readonly source?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AnnotationPredicate = (value: Annotation) => boolean;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user