Merge branch 'master' into VM/TestsIds
This commit is contained in:
commit
7ad373d164
@ -175,6 +175,7 @@ export class EditDossierDialogComponent extends BaseDialogComponent implements A
|
||||
key: 'members',
|
||||
title: _('edit-dossier-dialog.nav-items.team-members'),
|
||||
sideNavTitle: _('edit-dossier-dialog.nav-items.members'),
|
||||
readonly: !this._permissionsService.canEditTeamMembers(),
|
||||
},
|
||||
{
|
||||
key: 'dossierAttributes',
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
<redaction-team-members
|
||||
(remove)="toggleSelected($event)"
|
||||
[canAdd]="false"
|
||||
[canRemove]="hasOwner"
|
||||
[canRemove]="hasOwner && !disabled"
|
||||
[dossierId]="dossier.dossierId"
|
||||
[largeSpacing]="true"
|
||||
[memberIds]="selectedApproversList"
|
||||
@ -29,7 +29,7 @@
|
||||
<redaction-team-members
|
||||
(remove)="toggleSelected($event)"
|
||||
[canAdd]="false"
|
||||
[canRemove]="hasOwner"
|
||||
[canRemove]="hasOwner && !disabled"
|
||||
[dossierId]="dossier.dossierId"
|
||||
[largeSpacing]="true"
|
||||
[memberIds]="selectedReviewers$ | async"
|
||||
@ -53,22 +53,27 @@
|
||||
|
||||
<div class="members-list">
|
||||
<div
|
||||
(click)="!isOwner(userId) && toggleSelected(userId)"
|
||||
(click)="!disabled && toggleSelected(userId)"
|
||||
*ngFor="let userId of membersSelectOptions; let index = index"
|
||||
[class.disabled]="disabled"
|
||||
[class.selected]="isMemberSelected(userId)"
|
||||
[id]="'member-' + index"
|
||||
>
|
||||
<redaction-initials-avatar [user]="userId" [withName]="true" size="large"></redaction-initials-avatar>
|
||||
<div class="actions">
|
||||
<div (click)="toggleApprover(userId, $event)" *ngIf="!isOwner(userId)" class="make-approver">
|
||||
<iqser-round-checkbox
|
||||
[active]="isApprover(userId)"
|
||||
class="mr-8"
|
||||
<div
|
||||
(click)="!disabled && toggleApprover(userId, $event)"
|
||||
*ngIf="!disabled || isApprover(userId)"
|
||||
class="make-approver"
|
||||
>
|
||||
<iqser-round-checkbox [active]="isApprover(userId)" [disabled]="disabled" class="mr-8">
|
||||
[id]="'round-checkbox-' + index"
|
||||
></iqser-round-checkbox>
|
||||
<span translate="assign-dossier-owner.dialog.make-approver"></span>
|
||||
</iqser-round-checkbox>
|
||||
<span
|
||||
[translate]="disabled ? 'assign-dossier-owner.dialog.approver' : 'assign-dossier-owner.dialog.make-approver'"
|
||||
></span>
|
||||
</div>
|
||||
<mat-icon *ngIf="!isOwner(userId)" svgIcon="iqser:check"></mat-icon>
|
||||
<mat-icon svgIcon="iqser:check"></mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -24,7 +24,6 @@ redaction-team-members {
|
||||
margin-bottom: 2px;
|
||||
padding: 3px 5px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: background-color ease-in-out 0.1s;
|
||||
width: 560px;
|
||||
@ -50,6 +49,10 @@ redaction-team-members {
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.selected,
|
||||
&:hover {
|
||||
background-color: variables.$grey-2;
|
||||
|
||||
@ -7,6 +7,7 @@ import { EditDossierSaveResult, EditDossierSectionInterface } from '../edit-doss
|
||||
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { DossiersService } from '@services/dossiers/dossiers.service';
|
||||
import { compareLists } from '@utils/functions';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-edit-dossier-team',
|
||||
@ -51,7 +52,7 @@ export class EditDossierTeamComponent extends AutoUnsubscribe implements EditDos
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
return !this.userService.currentUser.isManager || !this.form.get('owner').value;
|
||||
return !this._permissionsService.canEditTeamMembers() || !this.form.get('owner').value;
|
||||
}
|
||||
|
||||
get hasOwner() {
|
||||
@ -68,18 +69,14 @@ export class EditDossierTeamComponent extends AutoUnsubscribe implements EditDos
|
||||
|
||||
const initialApprovers = [...this.dossier.approverIds].sort();
|
||||
const currentApprovers = this.selectedApproversList.sort();
|
||||
return this._compareLists(initialMembers, currentMembers) || this._compareLists(initialApprovers, currentApprovers);
|
||||
}
|
||||
|
||||
isOwner(userId: string): boolean {
|
||||
return userId === this.selectedOwnerId;
|
||||
return compareLists(initialMembers, currentMembers) || compareLists(initialApprovers, currentApprovers);
|
||||
}
|
||||
|
||||
async save(): EditDossierSaveResult {
|
||||
if (!this.hasOwner) {
|
||||
const owner = this.form.get('owner').value;
|
||||
if (!this.isApprover(owner)) {
|
||||
this.toggleApprover(owner);
|
||||
const ownerId: string = this.form.get('owner').value;
|
||||
if (!this.isApprover(ownerId)) {
|
||||
this.toggleApprover(ownerId);
|
||||
}
|
||||
this._updateLists();
|
||||
}
|
||||
@ -106,10 +103,6 @@ export class EditDossierTeamComponent extends AutoUnsubscribe implements EditDos
|
||||
toggleApprover(userId: string, $event?: MouseEvent) {
|
||||
$event?.stopPropagation();
|
||||
|
||||
if (this.isOwner(userId) && this.isApprover(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isApprover(userId)) {
|
||||
this.selectedApproversList.splice(this.selectedApproversList.indexOf(userId), 1);
|
||||
} else {
|
||||
@ -159,18 +152,18 @@ export class EditDossierTeamComponent extends AutoUnsubscribe implements EditDos
|
||||
this.form = this._formBuilder.group({
|
||||
owner: [
|
||||
{
|
||||
value: this.dossier?.ownerId,
|
||||
disabled: !this.userService.currentUser.isManager,
|
||||
value: this.dossier.ownerId,
|
||||
disabled: !this._permissionsService.canEditTeamMembers(),
|
||||
},
|
||||
Validators.required,
|
||||
],
|
||||
approvers: [[...this.dossier?.approverIds]],
|
||||
members: [[...this.dossier?.memberIds]],
|
||||
approvers: [[...this.dossier.approverIds]],
|
||||
members: [[...this.dossier.memberIds]],
|
||||
});
|
||||
this.addSubscription = this.form.get('owner').valueChanges.subscribe(owner => {
|
||||
this.addSubscription = this.form.get('owner').valueChanges.subscribe((ownerId: string) => {
|
||||
if (this.hasOwner) {
|
||||
if (!this.isApprover(owner)) {
|
||||
this.toggleApprover(owner);
|
||||
if (!this.isApprover(ownerId)) {
|
||||
this.toggleApprover(ownerId);
|
||||
}
|
||||
// If it is an approver, it is already a member, no need to check
|
||||
this._updateLists();
|
||||
@ -183,18 +176,4 @@ export class EditDossierTeamComponent extends AutoUnsubscribe implements EditDos
|
||||
this._setSelectedReviewersList();
|
||||
this.setMembersSelectOptions();
|
||||
}
|
||||
|
||||
private _compareLists(l1: string[], l2: string[]) {
|
||||
if (l1.length !== l2.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let idx = 0; idx < l1.length; ++idx) {
|
||||
if (l1[idx] !== l2[idx]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,16 +4,16 @@
|
||||
|
||||
<input #compareFileInput (change)="uploadFile($event.target['files'])" accept="application/pdf" class="file-upload-input" type="file" />
|
||||
|
||||
<div *ngIf="pdf?.totalPages && pdf?.currentPage" class="pagination noselect">
|
||||
<div (click)="pdf.previousPage()">
|
||||
<div *ngIf="pdfViewer?.totalPages && pdfViewer?.currentPage" class="pagination noselect">
|
||||
<div (click)="pdfViewer.navigatePreviousPage()">
|
||||
<mat-icon class="chevron-icon" svgIcon="red:nav-prev"></mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
#pageInput
|
||||
(change)="pdf.navigateToPage(pageInput.value)"
|
||||
[max]="pdf.totalPages"
|
||||
[value]="pdf.currentPage"
|
||||
(change)="pdfViewer.navigateToPage(pageInput.value)"
|
||||
[max]="pdfViewer.totalPages"
|
||||
[value]="pdfViewer.currentPage"
|
||||
class="page-number-input"
|
||||
min="1"
|
||||
type="number"
|
||||
@ -21,9 +21,9 @@
|
||||
</div>
|
||||
<div class="separator">/</div>
|
||||
<div>
|
||||
{{ pdf.totalPages }}
|
||||
{{ pdfViewer.totalPages }}
|
||||
</div>
|
||||
<div (click)="pdf.nextPage()">
|
||||
<div (click)="pdfViewer.navigateNextPage()">
|
||||
<mat-icon class="chevron-icon" svgIcon="red:nav-next"></mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -36,7 +36,6 @@ import { ViewModeService } from '../../services/view-mode.service';
|
||||
import { MultiSelectService } from '../../services/multi-select.service';
|
||||
import { FilePreviewStateService } from '../../services/file-preview-state.service';
|
||||
import { map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { FileManagementService } from '@services/entity-services/file-management.service';
|
||||
import { PageRotationService } from '../../services/page-rotation.service';
|
||||
import { ALLOWED_KEYBOARD_SHORTCUTS, HeaderElements, TextPopups } from '../../shared/constants';
|
||||
import { FilePreviewDialogService } from '../../services/file-preview-dialog.service';
|
||||
@ -44,6 +43,7 @@ import { loadCompareDocumentWrapper } from '../../../dossier/utils/compare-mode.
|
||||
import { from, fromEvent } from 'rxjs';
|
||||
import { FileDataService } from '../../services/file-data.service';
|
||||
import { ViewerHeaderConfigService } from '../../services/viewer-header-config.service';
|
||||
import { TooltipsService } from '../../services/tooltips.service';
|
||||
import Tools = Core.Tools;
|
||||
import TextTool = Tools.TextTool;
|
||||
import Annotation = Core.Annotations.Annotation;
|
||||
@ -80,14 +80,14 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
|
||||
private readonly _annotationActionsService: AnnotationActionsService,
|
||||
private readonly _configService: ConfigService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _fileManagementService: FileManagementService,
|
||||
private readonly _pageRotationService: PageRotationService,
|
||||
private readonly _fileDataService: FileDataService,
|
||||
private readonly _headerConfigService: ViewerHeaderConfigService,
|
||||
private readonly _tooltipsService: TooltipsService,
|
||||
readonly stateService: FilePreviewStateService,
|
||||
readonly viewModeService: ViewModeService,
|
||||
readonly multiSelectService: MultiSelectService,
|
||||
readonly pdf: PdfViewer,
|
||||
readonly pdfViewer: PdfViewer,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@ -98,7 +98,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
|
||||
|
||||
this.addActiveScreenSubscription = this.stateService.blob$
|
||||
.pipe(
|
||||
switchMap(blob => from(this.pdf.lockDocument()).pipe(map(() => blob))),
|
||||
switchMap(blob => from(this.pdfViewer.lockDocument()).pipe(map(() => blob))),
|
||||
withLatestFrom(this.stateService.file$),
|
||||
tap(([blob, file]) => this._loadDocument(blob, file)),
|
||||
)
|
||||
@ -148,7 +148,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
|
||||
this.viewModeService.compareMode = true;
|
||||
},
|
||||
() => {
|
||||
this.pdf.navigateToPage(1);
|
||||
this.pdfViewer.navigateToPage(1);
|
||||
},
|
||||
this.instance.Core.PDFNet,
|
||||
);
|
||||
@ -183,43 +183,40 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
|
||||
fileReader.readAsArrayBuffer(fileToCompare);
|
||||
}
|
||||
|
||||
private async _closeCompareMode() {
|
||||
this.viewModeService.compareMode = false;
|
||||
const pdfNet = this.instance.Core.PDFNet;
|
||||
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
|
||||
const blob = await this.stateService.blob;
|
||||
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer());
|
||||
|
||||
const filename = (await this.stateService.file).filename ?? 'document.pdf';
|
||||
this.instance.UI.loadDocument(currentDocument, { filename });
|
||||
|
||||
this._headerConfigService.disable([HeaderElements.CLOSE_COMPARE_BUTTON]);
|
||||
this._headerConfigService.enable([HeaderElements.COMPARE_BUTTON]);
|
||||
|
||||
this.pdf.navigateToPage(1);
|
||||
}
|
||||
|
||||
private async _loadViewer() {
|
||||
this.instance = await this.pdf.loadViewer(this.viewer.nativeElement as HTMLElement);
|
||||
this.documentViewer = this.pdf.documentViewer;
|
||||
this.annotationManager = this.pdf.annotationManager;
|
||||
this.instance = await this.pdfViewer.loadViewer(this.viewer.nativeElement as HTMLElement);
|
||||
this.documentViewer = this.pdfViewer.documentViewer;
|
||||
this.annotationManager = this.pdfViewer.annotationManager;
|
||||
|
||||
this._setSelectionMode();
|
||||
this._configureElements();
|
||||
this.pdf.disableHotkeys();
|
||||
this.pdfViewer.disableHotkeys();
|
||||
await this._configureTextPopup();
|
||||
|
||||
this.annotationManager.addEventListener('annotationSelected', async (annotations: Annotation[], action) => {
|
||||
const nextAnnotations = this.multiSelectService.isEnabled ? this.annotationManager.getSelectedAnnotations() : annotations;
|
||||
let nextAnnotations: Annotation[];
|
||||
|
||||
if (action === 'deselected') {
|
||||
// Remove deselected annotations from selected list
|
||||
nextAnnotations = this.annotationManager.getSelectedAnnotations().filter(ann => !annotations.find(a => a.Id === ann.Id));
|
||||
} else if (!this.multiSelectService.isEnabled) {
|
||||
// Only choose the last selected annotation, to bypass viewer multi select
|
||||
nextAnnotations = annotations;
|
||||
} else {
|
||||
// Get selected annotations from the manager, no intervention needed
|
||||
nextAnnotations = this.annotationManager.getSelectedAnnotations();
|
||||
}
|
||||
|
||||
this.annotationSelected.emit(nextAnnotations.map(ann => ann.Id));
|
||||
this.annotationSelected.emit(action === 'selected' ? nextAnnotations.map(ann => ann.Id) : []);
|
||||
if (action === 'deselected') {
|
||||
return this._toggleRectangleAnnotationAction(true);
|
||||
}
|
||||
|
||||
if (!this.multiSelectService.isEnabled) {
|
||||
const visibleAnnotations = await this._fileDataService.visibleAnnotations;
|
||||
this.pdf.deselectAnnotations(visibleAnnotations.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id)));
|
||||
this.pdfViewer.deselectAnnotations(
|
||||
visibleAnnotations.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id)),
|
||||
);
|
||||
}
|
||||
await this.#configureAnnotationSpecificActions(annotations);
|
||||
this._toggleRectangleAnnotationAction(annotations.length === 1 && annotations[0].ReadOnly);
|
||||
@ -236,7 +233,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
|
||||
});
|
||||
|
||||
this.documentViewer.addEventListener('pageNumberUpdated', (pageNumber: number) => {
|
||||
this.pdf.deselectAllAnnotations();
|
||||
this.pdfViewer.deselectAllAnnotations();
|
||||
this._ngZone.run(() => this.pageChanged.emit(pageNumber));
|
||||
return this._handleCustomActions();
|
||||
});
|
||||
@ -273,7 +270,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
|
||||
this.instance.UI.enableElements(['textPopup']);
|
||||
}
|
||||
|
||||
if (selectedText.length > 2 && this.canPerformActions && !this.pdf.isCurrentPageExcluded(file)) {
|
||||
if (selectedText.length > 2 && this.canPerformActions && !this.pdfViewer.isCurrentPageExcluded(file)) {
|
||||
this.instance.UI.enableElements(textActions);
|
||||
} else {
|
||||
this.instance.UI.disableElements(textActions);
|
||||
@ -317,12 +314,6 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
|
||||
}
|
||||
}
|
||||
|
||||
private async _toggleTooltips(title: string, img: string): Promise<void> {
|
||||
await this._userPreferenceService.toggleFilePreviewTooltipsPreference();
|
||||
this._updateTooltipsVisibility();
|
||||
this.instance.UI.updateElement(HeaderElements.TOGGLE_TOOLTIPS, { title, img });
|
||||
}
|
||||
|
||||
private _configureElements() {
|
||||
this.instance.UI.disableElements([
|
||||
'pageNavOverlay',
|
||||
@ -347,14 +338,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
|
||||
'annotationGroupButton',
|
||||
]);
|
||||
|
||||
const toggleTooltipsFn = async (title: string, img: string) => {
|
||||
await this._toggleTooltips(title, img);
|
||||
};
|
||||
const closeCompareFn = async () => {
|
||||
await this._closeCompareMode();
|
||||
};
|
||||
|
||||
this._headerConfigService.initialize(toggleTooltipsFn, this.compareFileInput, closeCompareFn);
|
||||
this._headerConfigService.initialize(this.compareFileInput);
|
||||
|
||||
const dossierTemplateId = this.dossier.dossierTemplateId;
|
||||
|
||||
@ -433,13 +417,13 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
|
||||
const quads = [this._annotationDrawService.annotationToQuads(activeAnnotation)];
|
||||
const manualRedaction = this._getManualRedaction({ [activePage]: quads });
|
||||
this._cleanUpSelectionAndButtonState();
|
||||
this.pdf.deleteAnnotations([activeAnnotation.Id]);
|
||||
this.pdfViewer.deleteAnnotations([activeAnnotation.Id]);
|
||||
|
||||
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(quads, manualRedaction, 'REDACTION', activeAnnotation.Id));
|
||||
}
|
||||
|
||||
private _cleanUpSelectionAndButtonState() {
|
||||
this.pdf.deselectAllAnnotations();
|
||||
this.pdfViewer.deselectAllAnnotations();
|
||||
this._headerConfigService.disable([HeaderElements.SHAPE_TOOL_GROUP_BUTTON]);
|
||||
this._headerConfigService.enable([HeaderElements.SHAPE_TOOL_GROUP_BUTTON]);
|
||||
}
|
||||
@ -514,10 +498,9 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
|
||||
HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
|
||||
HeaderElements.ROTATE_LEFT_BUTTON,
|
||||
HeaderElements.ROTATE_RIGHT_BUTTON,
|
||||
HeaderElements.ANNOTATION_POPUP,
|
||||
];
|
||||
|
||||
const isCurrentPageExcluded = this.pdf.isCurrentPageExcluded(await this.stateService.file);
|
||||
const isCurrentPageExcluded = this.pdfViewer.isCurrentPageExcluded(await this.stateService.file);
|
||||
|
||||
if (this.canPerformActions && !isCurrentPageExcluded) {
|
||||
try {
|
||||
@ -540,7 +523,6 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
|
||||
|
||||
if (isCurrentPageExcluded) {
|
||||
const allowedActionsWhenPageExcluded: string[] = [
|
||||
HeaderElements.ANNOTATION_POPUP,
|
||||
TextPopups.ADD_RECTANGLE,
|
||||
TextPopups.ADD_REDACTION,
|
||||
HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
|
||||
@ -566,7 +548,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
|
||||
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.pdf.translateQuad(page, quad) : quad));
|
||||
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.pdfViewer.translateQuad(page, quad) : quad));
|
||||
}
|
||||
}
|
||||
|
||||
@ -582,16 +564,11 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
|
||||
|
||||
private _setReadyAndInitialState() {
|
||||
this._ngZone.run(() => {
|
||||
this.pdf.emitDocumentLoaded();
|
||||
this.pdfViewer.emitDocumentLoaded();
|
||||
const routePageNumber: number = this._activatedRoute.snapshot.queryParams.page;
|
||||
this.pageChanged.emit(routePageNumber || 1);
|
||||
this._setInitialDisplayMode();
|
||||
this._updateTooltipsVisibility();
|
||||
this._tooltipsService.updateTooltipsVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
private _updateTooltipsVisibility(): void {
|
||||
const current = this._userPreferenceService.getFilePreviewTooltipsPreference();
|
||||
this.instance.UI.setAnnotationContentOverlayHandler(() => (current ? undefined : false));
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import { PageRotationService } from './services/page-rotation.service';
|
||||
import { PdfViewer } from './services/pdf-viewer.service';
|
||||
import { FileDataService } from './services/file-data.service';
|
||||
import { ViewerHeaderConfigService } from './services/viewer-header-config.service';
|
||||
import { TooltipsService } from './services/tooltips.service';
|
||||
|
||||
export const filePreviewScreenProviders = [
|
||||
FilterService,
|
||||
@ -34,4 +35,5 @@ export const filePreviewScreenProviders = [
|
||||
FileDataService,
|
||||
dossiersServiceProvider,
|
||||
ViewerHeaderConfigService,
|
||||
TooltipsService,
|
||||
];
|
||||
|
||||
@ -337,23 +337,14 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
}
|
||||
}
|
||||
|
||||
async viewerPageChanged($event: any) {
|
||||
if (typeof $event !== 'number') {
|
||||
async viewerPageChanged(page: any) {
|
||||
if (typeof page !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
this._scrollViews();
|
||||
this.multiSelectService.deactivate();
|
||||
|
||||
// Add current page in URL query params
|
||||
const extras: NavigationExtras = {
|
||||
queryParams: { page: $event },
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
};
|
||||
await this._router.navigate([], extras);
|
||||
|
||||
this._changeDetectorRef.markForCheck();
|
||||
await this.#updateQueryParamsPage(page);
|
||||
}
|
||||
|
||||
viewerReady() {
|
||||
@ -367,8 +358,16 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
// Go to initial page from query params
|
||||
const pageNumber: string = this._lastPage || this._activatedRoute.snapshot.queryParams.page;
|
||||
if (pageNumber) {
|
||||
setTimeout(() => {
|
||||
this.selectPage(parseInt(pageNumber, 10));
|
||||
setTimeout(async () => {
|
||||
const file = await this.stateService.file;
|
||||
let page = parseInt(pageNumber, 10);
|
||||
if (page < file.numberOfPages || isNaN(page)) {
|
||||
page = 1;
|
||||
await this.#updateQueryParamsPage(page);
|
||||
} else if (page > file.numberOfPages) {
|
||||
page = file.numberOfPages;
|
||||
}
|
||||
this.selectPage(page);
|
||||
this._scrollViews();
|
||||
this._changeDetectorRef.markForCheck();
|
||||
this._loadingService.stop();
|
||||
@ -455,6 +454,18 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
return this._cleanupAndRedrawAnnotations(annotationsToDraw);
|
||||
}
|
||||
|
||||
async #updateQueryParamsPage(page: number): Promise<void> {
|
||||
// Add current page in URL query params
|
||||
const extras: NavigationExtras = {
|
||||
queryParams: { page },
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
};
|
||||
await this._router.navigate([], extras);
|
||||
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
#getAnnotationsToDraw(newAnnotations: Record<string, AnnotationWrapper>, oldAnnotations: Record<string, AnnotationWrapper>) {
|
||||
return Object.values(newAnnotations).filter(newAnnotation => {
|
||||
const oldAnnotation = oldAnnotations[newAnnotation.id];
|
||||
|
||||
@ -16,9 +16,10 @@ import {
|
||||
} from '@iqser/common-ui';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ViewerHeaderConfigService } from './viewer-header-config.service';
|
||||
|
||||
const actionButtons = [HeaderElements.APPLY_ROTATION, HeaderElements.DISCARD_ROTATION];
|
||||
const oneRotationDegree = 90;
|
||||
const ACTION_BUTTONS = [HeaderElements.APPLY_ROTATION, HeaderElements.DISCARD_ROTATION];
|
||||
const ONE_ROTATION_DEGREE = 90;
|
||||
|
||||
@Injectable()
|
||||
export class PageRotationService {
|
||||
@ -31,6 +32,7 @@ export class PageRotationService {
|
||||
private readonly _screenState: FilePreviewStateService,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _fileManagementService: FileManagementService,
|
||||
private readonly _headerConfigService: ViewerHeaderConfigService,
|
||||
) {}
|
||||
|
||||
isRotated(page: number) {
|
||||
@ -58,7 +60,7 @@ export class PageRotationService {
|
||||
const rotations = this.#rotations$.value;
|
||||
|
||||
for (const page of Object.keys(rotations)) {
|
||||
const times = rotations[page] / oneRotationDegree;
|
||||
const times = rotations[page] / ONE_ROTATION_DEGREE;
|
||||
for (let i = 1; i <= times; i++) {
|
||||
this._pdf?.documentViewer?.rotateCounterClockwise(Number(page));
|
||||
}
|
||||
@ -116,14 +118,11 @@ export class PageRotationService {
|
||||
return this.hasRotations() ? this.showConfirmationDialog() : of(ConfirmOptions.DISCARD_CHANGES);
|
||||
}
|
||||
|
||||
// TODO: Ideally, enabling/disabling buttons should be done through ViewerHeaderConfigService, not through _pdf.UI,
|
||||
// but circular dependencies =D
|
||||
|
||||
#showActionButtons() {
|
||||
this._pdf.UI.enableElements(actionButtons);
|
||||
this._headerConfigService.enable(ACTION_BUTTONS);
|
||||
}
|
||||
|
||||
#hideActionButtons() {
|
||||
this._pdf.UI.disableElements(actionButtons);
|
||||
this._headerConfigService.disable(ACTION_BUTTONS);
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,13 +153,13 @@ export class PdfViewer {
|
||||
this._navigateToPage(this.paginationOffset === 2 ? parsedNumber * this.paginationOffset - 1 : parsedNumber);
|
||||
}
|
||||
|
||||
previousPage() {
|
||||
navigatePreviousPage() {
|
||||
if (this._currentInternalPage > 1) {
|
||||
this._navigateToPage(Math.max(this._currentInternalPage - this.paginationOffset, 1));
|
||||
}
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
navigateNextPage() {
|
||||
if (this._currentInternalPage < this._totalInternalPages) {
|
||||
this._navigateToPage(Math.min(this._currentInternalPage + this.paginationOffset, this._totalInternalPages));
|
||||
}
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { PdfViewer } from './pdf-viewer.service';
|
||||
import { UserPreferenceService } from '@services/user-preference.service';
|
||||
import { HeaderElements } from '../shared/constants';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BASE_HREF } from '../../../tokens';
|
||||
|
||||
@Injectable()
|
||||
export class TooltipsService {
|
||||
constructor(
|
||||
@Inject(BASE_HREF) private readonly _baseHref: string,
|
||||
private readonly _pdfViewer: PdfViewer,
|
||||
private readonly _userPreferenceService: UserPreferenceService,
|
||||
private readonly _translateService: TranslateService,
|
||||
) {}
|
||||
|
||||
get toggleTooltipsBtnTitle(): string {
|
||||
return this._translateService.instant(_('pdf-viewer.toggle-tooltips'), {
|
||||
active: this._userPreferenceService.getFilePreviewTooltipsPreference(),
|
||||
});
|
||||
}
|
||||
|
||||
get toggleTooltipsBtnIcon(): string {
|
||||
return this._convertPath(
|
||||
this._userPreferenceService.getFilePreviewTooltipsPreference()
|
||||
? '/assets/icons/general/pdftron-action-enable-tooltips.svg'
|
||||
: '/assets/icons/general/pdftron-action-disable-tooltips.svg',
|
||||
);
|
||||
}
|
||||
|
||||
updateTooltipsVisibility(): void {
|
||||
const current = this._userPreferenceService.getFilePreviewTooltipsPreference();
|
||||
this._pdfViewer.instance.UI.setAnnotationContentOverlayHandler(() => (current ? undefined : false));
|
||||
}
|
||||
|
||||
async toggleTooltips(): Promise<void> {
|
||||
await this._userPreferenceService.toggleFilePreviewTooltipsPreference();
|
||||
this.updateTooltipsVisibility();
|
||||
this._pdfViewer.instance.UI.updateElement(HeaderElements.TOGGLE_TOOLTIPS, {
|
||||
title: this.toggleTooltipsBtnTitle,
|
||||
img: this.toggleTooltipsBtnIcon,
|
||||
});
|
||||
}
|
||||
|
||||
private _convertPath(path: string): string {
|
||||
return this._baseHref + path;
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,14 @@
|
||||
import { ElementRef, Inject, Injectable } from '@angular/core';
|
||||
import { ElementRef, Inject, Injectable, Injector } from '@angular/core';
|
||||
import { IHeaderElement, RotationTypes } from '@red/domain';
|
||||
import { HeaderElements, HeaderElementType } from '../shared/constants';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { PageRotationService } from './page-rotation.service';
|
||||
import { BASE_HREF } from '../../../tokens';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { UserPreferenceService } from '@services/user-preference.service';
|
||||
import { PdfViewer } from './pdf-viewer.service';
|
||||
import { TooltipsService } from './tooltips.service';
|
||||
import { environment } from '@environments/environment';
|
||||
import { ViewModeService } from './view-mode.service';
|
||||
import { FilePreviewStateService } from './file-preview-state.service';
|
||||
import { PageRotationService } from './page-rotation.service';
|
||||
|
||||
@Injectable()
|
||||
export class ViewerHeaderConfigService {
|
||||
@ -16,24 +18,72 @@ export class ViewerHeaderConfigService {
|
||||
private _buttons: Map<HeaderElementType, IHeaderElement>;
|
||||
private _config: Map<HeaderElementType, boolean> = new Map<HeaderElementType, boolean>([
|
||||
[HeaderElements.SHAPE_TOOL_GROUP_BUTTON, true],
|
||||
[HeaderElements.ROTATE_LEFT_BUTTON, true],
|
||||
[HeaderElements.ROTATE_RIGHT_BUTTON, true],
|
||||
[HeaderElements.APPLY_ROTATION, true],
|
||||
[HeaderElements.DISCARD_ROTATION, true],
|
||||
[HeaderElements.TOGGLE_TOOLTIPS, true],
|
||||
[HeaderElements.ANNOTATION_POPUP, true],
|
||||
[HeaderElements.COMPARE_BUTTON, true],
|
||||
[HeaderElements.CLOSE_COMPARE_BUTTON, false],
|
||||
[HeaderElements.ROTATE_LEFT_BUTTON, true],
|
||||
[HeaderElements.ROTATE_RIGHT_BUTTON, true],
|
||||
[HeaderElements.APPLY_ROTATION, false],
|
||||
[HeaderElements.DISCARD_ROTATION, false],
|
||||
]);
|
||||
private _rotationService: PageRotationService;
|
||||
|
||||
constructor(
|
||||
@Inject(BASE_HREF) private readonly _baseHref: string,
|
||||
private readonly _injector: Injector,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _pageRotationService: PageRotationService,
|
||||
private readonly _userPreferenceService: UserPreferenceService,
|
||||
private readonly _pdfViewer: PdfViewer,
|
||||
private readonly _tooltipsService: TooltipsService,
|
||||
private readonly _viewModeService: ViewModeService,
|
||||
private readonly _stateService: FilePreviewStateService,
|
||||
) {}
|
||||
|
||||
private get _rectangle(): IHeaderElement {
|
||||
return {
|
||||
type: 'toolGroupButton',
|
||||
toolGroup: 'rectangleTools',
|
||||
dataElement: HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
|
||||
img: this._convertPath('/assets/icons/general/rectangle.svg'),
|
||||
title: 'annotation.rectangle',
|
||||
};
|
||||
}
|
||||
|
||||
private get _toggleTooltips(): IHeaderElement {
|
||||
return {
|
||||
type: 'actionButton',
|
||||
element: HeaderElements.TOGGLE_TOOLTIPS,
|
||||
dataElement: HeaderElements.TOGGLE_TOOLTIPS,
|
||||
title: this._tooltipsService.toggleTooltipsBtnTitle,
|
||||
img: this._tooltipsService.toggleTooltipsBtnIcon,
|
||||
onClick: async () => {
|
||||
await this._tooltipsService.toggleTooltips();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private get _closeCompare(): IHeaderElement {
|
||||
return {
|
||||
type: 'actionButton',
|
||||
element: HeaderElements.CLOSE_COMPARE_BUTTON,
|
||||
dataElement: HeaderElements.CLOSE_COMPARE_BUTTON,
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-close-compare.svg'),
|
||||
title: 'Leave Compare Mode',
|
||||
onClick: async () => {
|
||||
await this._closeCompareMode();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private get _rotateLeft(): IHeaderElement {
|
||||
return {
|
||||
type: 'actionButton',
|
||||
element: HeaderElements.ROTATE_LEFT_BUTTON,
|
||||
dataElement: HeaderElements.ROTATE_LEFT_BUTTON,
|
||||
img: this._convertPath('/assets/icons/general/rotate-left.svg'),
|
||||
onClick: () => this._rotationService.addRotation(RotationTypes.LEFT),
|
||||
};
|
||||
}
|
||||
|
||||
private get _applyRotation(): IHeaderElement {
|
||||
return {
|
||||
type: 'customElement',
|
||||
@ -49,7 +99,7 @@ export class ViewerHeaderConfigService {
|
||||
margin: 0 12px;
|
||||
`;
|
||||
paragraph.addEventListener('click', async () => {
|
||||
await this._pageRotationService.applyRotation();
|
||||
await this._rotationService.applyRotation();
|
||||
});
|
||||
return paragraph;
|
||||
},
|
||||
@ -70,67 +120,24 @@ export class ViewerHeaderConfigService {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
`;
|
||||
paragraph.addEventListener('click', () => {
|
||||
this._pageRotationService.discardRotation();
|
||||
});
|
||||
paragraph.addEventListener('click', () => this._rotationService.discardRotation());
|
||||
return paragraph;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private get _rectangle(): IHeaderElement {
|
||||
return {
|
||||
type: 'toolGroupButton',
|
||||
toolGroup: 'rectangleTools',
|
||||
dataElement: HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
|
||||
img: this._convertPath('/assets/icons/general/rectangle.svg'),
|
||||
title: 'annotation.rectangle',
|
||||
};
|
||||
}
|
||||
|
||||
private get _rotateLeft(): IHeaderElement {
|
||||
return {
|
||||
type: 'actionButton',
|
||||
element: 'tooltips',
|
||||
dataElement: HeaderElements.ROTATE_LEFT_BUTTON,
|
||||
img: this._convertPath('/assets/icons/general/rotate-left.svg'),
|
||||
onClick: () => this._pageRotationService.addRotation(RotationTypes.LEFT),
|
||||
};
|
||||
}
|
||||
|
||||
private get _rotateRight(): IHeaderElement {
|
||||
return {
|
||||
type: 'actionButton',
|
||||
element: 'tooltips',
|
||||
element: HeaderElements.ROTATE_RIGHT_BUTTON,
|
||||
dataElement: HeaderElements.ROTATE_RIGHT_BUTTON,
|
||||
img: this._convertPath('/assets/icons/general/rotate-right.svg'),
|
||||
onClick: () => this._pageRotationService.addRotation(RotationTypes.RIGHT),
|
||||
onClick: () => this._rotationService.addRotation(RotationTypes.RIGHT),
|
||||
};
|
||||
}
|
||||
|
||||
private get _toggleTooltipsBtnTitle(): string {
|
||||
return this._translateService.instant(_('pdf-viewer.toggle-tooltips'), {
|
||||
active: this._userPreferenceService.getFilePreviewTooltipsPreference(),
|
||||
});
|
||||
}
|
||||
|
||||
private get _toggleTooltipsBtnIcon(): string {
|
||||
return this._convertPath(
|
||||
this._userPreferenceService.getFilePreviewTooltipsPreference()
|
||||
? '/assets/icons/general/pdftron-action-enable-tooltips.svg'
|
||||
: '/assets/icons/general/pdftron-action-disable-tooltips.svg',
|
||||
);
|
||||
}
|
||||
|
||||
initialize(
|
||||
toggleTooltips: (title: string, img: string) => Promise<void>,
|
||||
compareFileInput: ElementRef,
|
||||
closeCompareMode: () => Promise<void>,
|
||||
): void {
|
||||
if (this._buttons) {
|
||||
console.error('ERROR: ViewerHeaderConfigService can only be initialized once!');
|
||||
return;
|
||||
}
|
||||
initialize(compareFileInput: ElementRef): void {
|
||||
this._rotationService = this._injector.get<PageRotationService>(PageRotationService);
|
||||
|
||||
this._buttons = new Map([
|
||||
[HeaderElements.SHAPE_TOOL_GROUP_BUTTON, this._rectangle],
|
||||
@ -138,9 +145,9 @@ export class ViewerHeaderConfigService {
|
||||
[HeaderElements.ROTATE_RIGHT_BUTTON, this._rotateRight],
|
||||
[HeaderElements.APPLY_ROTATION, this._applyRotation],
|
||||
[HeaderElements.DISCARD_ROTATION, this._discardRotation],
|
||||
[HeaderElements.TOGGLE_TOOLTIPS, this._toggleTooltips(toggleTooltips)],
|
||||
[HeaderElements.TOGGLE_TOOLTIPS, this._toggleTooltips],
|
||||
[HeaderElements.COMPARE_BUTTON, this._compare(compareFileInput)],
|
||||
[HeaderElements.CLOSE_COMPARE_BUTTON, this._closeCompare(closeCompareMode)],
|
||||
[HeaderElements.CLOSE_COMPARE_BUTTON, this._closeCompare],
|
||||
]);
|
||||
|
||||
this._updateElements();
|
||||
@ -154,36 +161,26 @@ export class ViewerHeaderConfigService {
|
||||
this._updateState(elements, false);
|
||||
}
|
||||
|
||||
private _toggleTooltips(toggleTooltips: (title: string, img: string) => Promise<void>): IHeaderElement {
|
||||
return {
|
||||
type: 'actionButton',
|
||||
element: 'tooltips',
|
||||
dataElement: HeaderElements.TOGGLE_TOOLTIPS,
|
||||
img: this._toggleTooltipsBtnIcon,
|
||||
title: this._toggleTooltipsBtnTitle,
|
||||
onClick: async () => {
|
||||
await toggleTooltips(this._toggleTooltipsBtnTitle, this._toggleTooltipsBtnIcon);
|
||||
},
|
||||
};
|
||||
}
|
||||
private async _closeCompareMode() {
|
||||
this._viewModeService.compareMode = false;
|
||||
const pdfNet = this._pdfViewer.instance.Core.PDFNet;
|
||||
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
|
||||
const blob = await this._stateService.blob;
|
||||
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer());
|
||||
|
||||
private _closeCompare(closeCompareMode: () => Promise<void>): IHeaderElement {
|
||||
return {
|
||||
type: 'actionButton',
|
||||
element: 'closeCompare',
|
||||
dataElement: HeaderElements.CLOSE_COMPARE_BUTTON,
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-close-compare.svg'),
|
||||
title: 'Leave Compare Mode',
|
||||
onClick: async () => {
|
||||
await closeCompareMode();
|
||||
},
|
||||
};
|
||||
const filename = (await this._stateService.file).filename ?? 'document.pdf';
|
||||
this._pdfViewer.instance.UI.loadDocument(currentDocument, { filename });
|
||||
|
||||
this.disable([HeaderElements.CLOSE_COMPARE_BUTTON]);
|
||||
this.enable([HeaderElements.COMPARE_BUTTON]);
|
||||
|
||||
this._pdfViewer.navigateToPage(1);
|
||||
}
|
||||
|
||||
private _compare(compareFileInput: ElementRef): IHeaderElement {
|
||||
return {
|
||||
type: 'actionButton',
|
||||
element: 'compare',
|
||||
element: HeaderElements.COMPARE_BUTTON,
|
||||
dataElement: HeaderElements.COMPARE_BUTTON,
|
||||
img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'),
|
||||
title: 'Compare',
|
||||
|
||||
@ -9,7 +9,6 @@ export type HeaderElementType =
|
||||
| 'APPLY_ROTATION'
|
||||
| 'DISCARD_ROTATION'
|
||||
| 'TOGGLE_TOOLTIPS'
|
||||
| 'ANNOTATION_POPUP'
|
||||
| 'COMPARE_BUTTON'
|
||||
| 'CLOSE_COMPARE_BUTTON';
|
||||
|
||||
@ -20,7 +19,6 @@ export const HeaderElements: Record<HeaderElementType, HeaderElementType> = {
|
||||
APPLY_ROTATION: 'APPLY_ROTATION',
|
||||
DISCARD_ROTATION: 'DISCARD_ROTATION',
|
||||
TOGGLE_TOOLTIPS: 'TOGGLE_TOOLTIPS',
|
||||
ANNOTATION_POPUP: 'ANNOTATION_POPUP',
|
||||
COMPARE_BUTTON: 'COMPARE_BUTTON',
|
||||
CLOSE_COMPARE_BUTTON: 'CLOSE_COMPARE_BUTTON',
|
||||
} as const;
|
||||
|
||||
@ -203,10 +203,18 @@ export class PermissionsService {
|
||||
return dossier.isActive && this.isOwner(dossier, user);
|
||||
}
|
||||
|
||||
canEditTeamMembers(): boolean {
|
||||
return this.isManager();
|
||||
}
|
||||
|
||||
isAdmin(user = this._userService.currentUser): boolean {
|
||||
return user.isAdmin;
|
||||
}
|
||||
|
||||
isManager(user = this._userService.currentUser): boolean {
|
||||
return user.isManager;
|
||||
}
|
||||
|
||||
canAddComment(file: File, dossier: Dossier): boolean {
|
||||
return (this.isFileAssignee(file) || this.isApprover(dossier)) && !file.isApproved;
|
||||
}
|
||||
|
||||
@ -78,3 +78,17 @@ export function toSnakeCase(str: string): string {
|
||||
.replace(/[\s_]+/g, '_')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export function compareLists(l1: string[], l2: string[]) {
|
||||
if (l1.length !== l2.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let idx = 0; idx < l1.length; ++idx) {
|
||||
if (l1[idx] !== l2[idx]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -369,6 +369,7 @@
|
||||
},
|
||||
"assign-dossier-owner": {
|
||||
"dialog": {
|
||||
"approver": "",
|
||||
"approvers": "Genehmiger",
|
||||
"make-approver": "Zum Genehmiger ernennen",
|
||||
"no-reviewers": "Es gibt noch keine Reviewer.\nBitte aus der Liste unten auswählen.",
|
||||
|
||||
@ -369,6 +369,7 @@
|
||||
},
|
||||
"assign-dossier-owner": {
|
||||
"dialog": {
|
||||
"approver": "Approver",
|
||||
"approvers": "Approvers",
|
||||
"make-approver": "Make Approver",
|
||||
"no-reviewers": "No members with \"review only\" permission.",
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit f3c088af0368ae55195541e3793c44836bb0f481
|
||||
Subproject commit dfa5be23247ae142b4ce516eb023f5f8b55331d2
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "redaction",
|
||||
"version": "3.370.0",
|
||||
"version": "3.376.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user