Just save this

[ci skip]
This commit is contained in:
shamoon
2025-07-01 11:35:08 -07:00
parent 1cd21d0f38
commit d3644463cc
11 changed files with 404 additions and 12 deletions

View File

@@ -0,0 +1,29 @@
<pdf-viewer [src]="pdfSrc" [render-text]="false" zoom="0.4" (after-load-complete)="pdfLoaded($event)"></pdf-viewer>
<div class="modal-header">
<h4 class="modal-title">{{ title }}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()"></button>
</div>
<div class="modal-body">
<div cdkDropList (cdkDropListDropped)="drop($event)" cdkDropListOrientation="mixed" class="d-flex flex-wrap row-cols-5">
@for (p of pages; track p.page; let i = $index) {
<div class="page-item p-2" cdkDrag>
<div class="btn-group mb-1">
<button class="btn btn-sm btn-secondary" (click)="rotate(i)"><i-bs name="arrow-clockwise"></i-bs></button>
<button class="btn btn-sm btn-outline-secondary" (click)="toggleSplit(i)"><i-bs name="scissors"></i-bs></button>
<button class="btn btn-sm btn-danger" (click)="remove(i)"><i-bs name="trash"></i-bs></button>
</div>
<div class="pdf-viewer-container w-100 mt-3">
<pdf-viewer [src]="pdfSrc" [page]="p.page" [rotation]="p.rotate" [original-size]="false" [show-all]="false" [render-text]="false"></pdf-viewer>
</div>
</div>
}
</div>
</div>
<div class="modal-footer">
<div class="form-check form-switch me-auto">
<input class="form-check-input" type="checkbox" id="deleteSwitch" [(ngModel)]="deleteOriginal">
<label class="form-check-label" for="deleteSwitch" i18n>Delete original after edit</label>
</div>
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">{{ cancelBtnCaption }}</button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="pages.length === 0">{{ btnCaption }}</button>
</div>

View File

@@ -0,0 +1,9 @@
.pdf-viewer-container {
background-color: gray;
height: 120px;
pdf-viewer {
width: 100%;
height: 100%;
}
}

View File

@@ -0,0 +1,36 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { PDFEditorComponent } from './pdf-editor.component'
describe('PDFEditorComponent', () => {
let component: PDFEditorComponent
let fixture: ComponentFixture<PDFEditorComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PDFEditorComponent, NgxBootstrapIconsModule.pick(allIcons)],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
{ provide: NgbActiveModal, useValue: {} },
],
}).compileComponents()
fixture = TestBed.createComponent(PDFEditorComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should rotate and reorder pages', () => {
component.pages = [
{ page: 1, rotate: 0, splitAfter: false },
{ page: 2, rotate: 0, splitAfter: false },
]
component.rotate(0)
expect(component.pages[0].rotate).toBe(90)
component.drop({ previousIndex: 0, currentIndex: 1 } as any)
expect(component.pages[0].page).toBe(2)
})
})

View File

@@ -0,0 +1,90 @@
import {
CdkDragDrop,
DragDropModule,
moveItemInArray,
} from '@angular/cdk/drag-drop'
import { Component, OnInit, inject } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DocumentService } from 'src/app/services/rest/document.service'
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'
interface PageOperation {
page: number
rotate: number
splitAfter: boolean
}
@Component({
selector: 'pngx-pdf-editor',
templateUrl: './pdf-editor.component.html',
styleUrl: './pdf-editor.component.scss',
imports: [
DragDropModule,
FormsModule,
PdfViewerModule,
NgxBootstrapIconsModule,
],
})
export class PDFEditorComponent
extends ConfirmDialogComponent
implements OnInit
{
private documentService = inject(DocumentService)
activeModal = inject(NgbActiveModal)
documentID: number
pages: PageOperation[] = []
totalPages = 0
deleteOriginal = false
get pdfSrc(): string {
return this.documentService.getPreviewUrl(this.documentID)
}
ngOnInit() {}
pdfLoaded(pdf: PDFDocumentProxy) {
this.totalPages = pdf.numPages
this.pages = Array.from({ length: this.totalPages }, (_, i) => ({
page: i + 1,
rotate: 0,
splitAfter: false,
}))
}
rotate(i: number) {
this.pages[i].rotate = (this.pages[i].rotate + 90) % 360
}
remove(i: number) {
this.pages.splice(i, 1)
}
toggleSplit(i: number) {
this.pages[i].splitAfter = !this.pages[i].splitAfter
}
drop(event: CdkDragDrop<PageOperation[]>) {
moveItemInArray(this.pages, event.previousIndex, event.currentIndex)
}
getOperations() {
const operations = this.pages.map((p, idx) => ({
page: p.page,
rotate: p.rotate,
doc: this.computeDocIndex(idx),
}))
return operations
}
private computeDocIndex(index: number): number {
let docIndex = 0
for (let i = 0; i <= index; i++) {
if (this.pages[i].splitAfter && i < index) docIndex++
}
return docIndex
}
}

View File

@@ -66,6 +66,10 @@
<i-bs name="arrow-clockwise"></i-bs>&nbsp;<ng-container i18n>Rotate</ng-container>
</button>
<button ngbDropdownItem (click)="editPdf()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF">
<i-bs name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit PDF</ng-container>
</button>
<button ngbDropdownItem (click)="deletePages()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF || previewNumPages === 1">
<i-bs name="file-earmark-minus"></i-bs>&nbsp;<ng-container i18n>Delete page(s)</ng-container>
</button>

View File

@@ -1219,6 +1219,29 @@ describe('DocumentDetailComponent', () => {
req.flush(true)
})
it('should support pdf editor', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[0]))
initNormally()
component.editPdf()
expect(modal).not.toBeUndefined()
modal.componentInstance.documentID = doc.id
modal.componentInstance.pages = [{ page: 1, rotate: 0, splitAfter: false }]
modal.componentInstance.confirm()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/bulk_edit/`
)
expect(req.request.body).toEqual({
documents: [doc.id],
method: 'edit_pdf',
parameters: {
operations: [{ page: 1, rotate: 0, doc: 0 }],
delete_original: false,
},
})
req.flush(true)
})
it('should support keyboard shortcuts', () => {
initNormally()

View File

@@ -101,6 +101,7 @@ import { TagsComponent } from '../common/input/tags/tags.component'
import { TextComponent } from '../common/input/text/text.component'
import { UrlComponent } from '../common/input/url/url.component'
import { PageHeaderComponent } from '../common/page-header/page-header.component'
import { PDFEditorComponent } from '../common/pdf-editor/pdf-editor.component'
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
import { DocumentHistoryComponent } from '../document-history/document-history.component'
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
@@ -1417,6 +1418,45 @@ export class DocumentDetailComponent
})
}
editPdf() {
let modal = this.modalService.open(PDFEditorComponent, {
backdrop: 'static',
size: 'xl',
scrollable: true,
})
modal.componentInstance.title = $localize`Edit PDF`
modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.documentID = this.document.id
modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
modal.componentInstance.buttonsEnabled = false
this.documentsService
.bulkEdit([this.document.id], 'edit_pdf', {
operations: modal.componentInstance.getOperations(),
delete_original: modal.componentInstance.deleteOriginal,
})
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
this.toastService.showInfo(
$localize`PDF edit operation for "${this.document.title}" will begin in the background.`
)
modal.close()
},
error: (error) => {
if (modal) {
modal.componentInstance.buttonsEnabled = true
}
this.toastService.showError(
$localize`Error executing PDF edit operation`,
error
)
},
})
})
}
deletePages() {
let modal = this.modalService.open(DeletePagesConfirmDialogComponent, {
backdrop: 'static',