From 76e86e1a08cfc68f35e669c1cc0cb65b65b0055f Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 7 Jul 2024 17:42:48 +0200 Subject: [PATCH 001/125] Open a PDF Preview page for every attachment file --- src/main/webapp/app/app-routing.module.ts | 6 +++++ .../lecture-attachments.component.html | 8 +++++++ src/main/webapp/app/lecture/lecture.module.ts | 2 ++ .../pdf-preview/pdf-preview.component.html | 18 +++++++++++++++ .../pdf-preview/pdf-preview.component.scss | 16 ++++++++++++++ .../pdf-preview/pdf-preview.component.spec.ts | 22 +++++++++++++++++++ .../pdf-preview/pdf-preview.component.ts | 22 +++++++++++++++++++ 7 files changed, 94 insertions(+) create mode 100644 src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html create mode 100644 src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss create mode 100644 src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.spec.ts create mode 100644 src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts diff --git a/src/main/webapp/app/app-routing.module.ts b/src/main/webapp/app/app-routing.module.ts index dc106122148b..0281b9a2101d 100644 --- a/src/main/webapp/app/app-routing.module.ts +++ b/src/main/webapp/app/app-routing.module.ts @@ -6,6 +6,7 @@ import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { AboutIrisComponent } from 'app/iris/about-iris/about-iris.component'; import { ProblemStatementComponent } from './overview/exercise-details/problem-statement/problem-statement.component'; import { StandaloneFeedbackComponent } from './exercises/shared/feedback/standalone-feedback/standalone-feedback.component'; +import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component'; const LAYOUT_ROUTES: Routes = [navbarRoute, ...errorRoute]; @@ -65,6 +66,11 @@ const LAYOUT_ROUTES: Routes = [navbarRoute, ...errorRoute]; loadChildren: () => import('./exercises/programming/manage/programming-exercise-management-routing.module').then((m) => m.ArtemisProgrammingExerciseManagementRoutingModule), }, + { + path: 'course-management/:courseId/lectures/:lectureId/attachments/:attachmentId', + pathMatch: 'full', + component: PdfPreviewComponent, + }, { path: 'courses', loadChildren: () => import('./overview/courses.module').then((m) => m.ArtemisCoursesModule), diff --git a/src/main/webapp/app/lecture/lecture-attachments.component.html b/src/main/webapp/app/lecture/lecture-attachments.component.html index b5bd8a61163f..9835422c5c28 100644 --- a/src/main/webapp/app/lecture/lecture-attachments.component.html +++ b/src/main/webapp/app/lecture/lecture-attachments.component.html @@ -79,6 +79,14 @@

+
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss new file mode 100644 index 000000000000..20505725bb56 --- /dev/null +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -0,0 +1,16 @@ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 10px; + border: 1px solid lightgrey; + padding: 10px; + background: #fff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin: 10px; +} + +.thumbnail { + width: 100%; + height: auto; + object-fit: cover; +} diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.spec.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.spec.ts new file mode 100644 index 000000000000..4e61db3aa89a --- /dev/null +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PdfPreviewComponent } from './pdf-preview.component'; + +describe('PdfPreviewComponent', () => { + let component: PdfPreviewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PdfPreviewComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PdfPreviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts new file mode 100644 index 000000000000..c0ba4cefae74 --- /dev/null +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'jhi-pdf-preview-component', + templateUrl: './pdf-preview.component.html', + styleUrls: ['./pdf-preview.component.scss'], +}) +export class PdfPreviewComponent implements OnInit { + imageUrls: string[] = []; + + constructor() {} + + ngOnInit() { + this.imageUrls = [ + 'https://via.placeholder.com/200x150.png?text=Page+1', + 'https://via.placeholder.com/200x150.png?text=Page+2', + 'https://via.placeholder.com/200x150.png?text=Page+3', + 'https://via.placeholder.com/200x150.png?text=Page+4', + 'https://via.placeholder.com/200x150.png?text=Page+5', + ]; + } +} From d96f276189fede7541cf830f2e7034d672ab641c Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 7 Jul 2024 20:58:45 +0200 Subject: [PATCH 002/125] Add breadcrumb & show pdf file preview as images --- .../artemis/web/rest/AttachmentResource.java | 54 +++++++++++++++++++ .../webapp/app/lecture/attachment.service.ts | 6 +++ .../pdf-preview/pdf-preview.component.html | 1 + .../pdf-preview/pdf-preview.component.ts | 41 ++++++++++---- .../shared/layouts/navbar/navbar.component.ts | 3 ++ 5 files changed, 96 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java index 0078b5385979..f698b2ceedd3 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java @@ -3,12 +3,24 @@ import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; import static de.tum.in.www1.artemis.service.FilePathService.actualPathForPublicPath; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.Optional; +import javax.imageio.ImageIO; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.rendering.PDFRenderer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -210,4 +222,46 @@ else if (attachment.getExercise() != null) { } return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, attachmentId.toString())).build(); } + + public List convertPdfToImageUrls(String pdfFilePath) { + List imageUrls = new ArrayList<>(); + try (PDDocument document = Loader.loadPDF(new File(pdfFilePath))) { + PDFRenderer pdfRenderer = new PDFRenderer(document); + int lowerDPI = 150; // Reduced from 300 to 150 DPI + for (int page = 0; page < document.getNumberOfPages(); ++page) { + BufferedImage bim = pdfRenderer.renderImageWithDPI(page, lowerDPI); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + ImageIO.write(bim, "png", baos); + byte[] imageBytes = baos.toByteArray(); + String base64Image = Base64.getEncoder().encodeToString(imageBytes); + imageUrls.add("data:image/png;base64," + base64Image); + } + catch (IOException e) { + e.printStackTrace(); + } + } + } + catch (IOException e) { + e.printStackTrace(); + } + return imageUrls; + } + + private String resolveFilePath(String webPath) { + String baseDir = "uploads"; + String prefixToRemove = "/api/files/"; + String pathWithoutPrefix = webPath.replace(prefixToRemove, ""); + return Paths.get(baseDir, pathWithoutPrefix).toString(); + } + + @GetMapping("/attachments/{id}/pdf-to-images") + public ResponseEntity> convertPdfToImages(@PathVariable Long id) { + Attachment attachment = attachmentRepository.findById(id).orElseThrow(); + String actualFilePath = resolveFilePath(attachment.getLink()); + File pdfFile = new File(actualFilePath); + List imageUrls = convertPdfToImageUrls(pdfFile.getAbsolutePath()); + return ResponseEntity.ok(imageUrls); + } + } diff --git a/src/main/webapp/app/lecture/attachment.service.ts b/src/main/webapp/app/lecture/attachment.service.ts index 57d22c64d550..1b9d942fbceb 100644 --- a/src/main/webapp/app/lecture/attachment.service.ts +++ b/src/main/webapp/app/lecture/attachment.service.ts @@ -135,4 +135,10 @@ export class AttachmentService { } return formData; } + + getPdfImages(attachmentId: number): Observable { + return this.http.get(`${this.resourceUrl}/${attachmentId}/pdf-to-images`).pipe( + map((response) => response), // Assuming response is directly the array of base64 strings + ); + } } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index b28379770c5f..c5108de5cd21 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -5,6 +5,7 @@
+ {{ attachment?.name }}
@for (imageUrl of imageUrls; track imageUrl) { title diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index c0ba4cefae74..2fec4463e7b9 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -1,4 +1,8 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; +import { Attachment } from 'app/entities/attachment.model'; +import { ActivatedRoute } from '@angular/router'; +import { AttachmentService } from 'app/lecture/attachment.service'; +import { HttpResponse } from '@angular/common/http'; @Component({ selector: 'jhi-pdf-preview-component', @@ -6,17 +10,36 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./pdf-preview.component.scss'], }) export class PdfPreviewComponent implements OnInit { + @Input() attachmentId: number; + + attachment: Attachment; imageUrls: string[] = []; - constructor() {} + constructor( + private route: ActivatedRoute, + private attachmentService: AttachmentService, + ) {} ngOnInit() { - this.imageUrls = [ - 'https://via.placeholder.com/200x150.png?text=Page+1', - 'https://via.placeholder.com/200x150.png?text=Page+2', - 'https://via.placeholder.com/200x150.png?text=Page+3', - 'https://via.placeholder.com/200x150.png?text=Page+4', - 'https://via.placeholder.com/200x150.png?text=Page+5', - ]; + this.route.params.subscribe((params) => { + this.attachmentId = +params['attachmentId']; // Make sure this is always defined + if (this.attachmentId) { + this.attachmentService.find(this.attachmentId).subscribe((attachmentResponse: HttpResponse) => { + this.attachment = attachmentResponse.body!; + if (this.attachment && this.attachment.id) { + // Check if id is defined + this.attachmentService.getPdfImages(this.attachment.id).subscribe( + (imageUrls) => { + console.log(imageUrls); + this.imageUrls = imageUrls; + }, + (error) => { + console.error('Failed to load images', error); + }, + ); + } + }); + } + }); } } diff --git a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts index 66e0b8b93901..3e14264859e6 100644 --- a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts +++ b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts @@ -551,6 +551,9 @@ export class NavbarComponent implements OnInit, OnDestroy { // Special case: Don't display the ID here but the name directly (clicking the ID wouldn't work) this.addTranslationAsCrumb(currentPath, 'example-submission-editor'); break; + case 'attachments': + this.addBreadcrumb(currentPath, segment, false); + break; // No breadcrumbs for those segments case 'competency-management': case 'unit-management': From 8077b0de5625c9cb9e836deff7cdfd932978dfec Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sat, 13 Jul 2024 16:55:45 +0200 Subject: [PATCH 003/125] Render files with PDFJS --- package-lock.json | 2827 ++++++++++++++++- package.json | 7 +- .../artemis/web/rest/AttachmentResource.java | 55 +- .../webapp/app/lecture/attachment.service.ts | 6 +- .../pdf-preview/pdf-preview.component.html | 9 +- .../pdf-preview/pdf-preview.component.scss | 28 +- .../pdf-preview/pdf-preview.component.ts | 63 +- 7 files changed, 2836 insertions(+), 159 deletions(-) diff --git a/package-lock.json b/package-lock.json index b1fae35ac4c8..b0c1bf0dcc39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "fast-json-patch": "3.1.1", "franc-min": "6.2.0", "html-diff-ts": "1.4.2", + "install": "^0.13.0", "interactjs": "1.10.27", "ismobilejs-es5": "0.0.1", "js-video-url-parser": "0.5.1", @@ -60,7 +61,9 @@ "monaco-editor": "0.50.0", "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", + "npm": "^10.8.2", "papaparse": "5.4.1", + "pdfjs-dist": "^4.4.168", "posthog-js": "1.144.2", "rxjs": "7.8.1", "showdown": "2.1.0", @@ -4712,6 +4715,87 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@material/animation": { "version": "15.0.0-canary.7f224ddd4.0", "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.7f224ddd4.0.tgz", @@ -7912,6 +7996,40 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -8280,7 +8398,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "devOptional": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -8685,6 +8803,21 @@ } ] }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -8740,7 +8873,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10" } @@ -9003,6 +9136,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -9075,7 +9217,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "devOptional": true }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", @@ -9086,6 +9228,12 @@ "node": ">=0.8" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -9893,6 +10041,18 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -10009,6 +10169,12 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -10041,7 +10207,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -11643,7 +11809,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "devOptional": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -11667,6 +11833,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -11755,7 +11942,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, + "devOptional": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -11792,7 +11979,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11802,7 +11989,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -11920,6 +12107,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -12396,7 +12589,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, + "devOptional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -12454,6 +12647,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/install": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", + "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/interactjs": { "version": "1.10.27", "resolved": "https://registry.npmjs.org/interactjs/-/interactjs-1.10.27.tgz", @@ -15894,6 +16095,18 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mini-css-extract-plugin": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", @@ -16076,7 +16289,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, + "devOptional": true, "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -16089,7 +16302,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -16101,13 +16314,13 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "devOptional": true }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, + "devOptional": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -16231,6 +16444,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -16367,6 +16586,48 @@ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -16559,6 +16820,158 @@ "node": ">=0.10.0" } }, + "node_modules/npm": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/npm/-/npm-10.8.2.tgz", + "integrity": "sha512-x/AIjFIKRllrhcb48dqUNAAZl0ig9+qMuN91RpZo3Cb2+zuibfh+KISl6+kVVyktDz230JKc208UkQwwMqyB+w==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/redact", + "@npmcli/run-script", + "@sigstore/tuf", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "cli-columns", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmhook", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "normalize-package-data", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which", + "write-file-atomic" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^7.5.4", + "@npmcli/config": "^8.3.4", + "@npmcli/fs": "^3.1.1", + "@npmcli/map-workspaces": "^3.0.6", + "@npmcli/package-json": "^5.2.0", + "@npmcli/promise-spawn": "^7.0.2", + "@npmcli/redact": "^2.0.1", + "@npmcli/run-script": "^8.1.0", + "@sigstore/tuf": "^2.3.4", + "abbrev": "^2.0.0", + "archy": "~1.0.0", + "cacache": "^18.0.3", + "chalk": "^5.3.0", + "ci-info": "^4.0.0", + "cli-columns": "^4.0.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^10.4.2", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^7.0.2", + "ini": "^4.1.3", + "init-package-json": "^6.0.3", + "is-cidr": "^5.1.0", + "json-parse-even-better-errors": "^3.0.2", + "libnpmaccess": "^8.0.6", + "libnpmdiff": "^6.1.4", + "libnpmexec": "^8.1.3", + "libnpmfund": "^5.0.12", + "libnpmhook": "^10.0.5", + "libnpmorg": "^6.0.6", + "libnpmpack": "^7.0.4", + "libnpmpublish": "^9.0.9", + "libnpmsearch": "^7.0.6", + "libnpmteam": "^6.0.5", + "libnpmversion": "^6.0.3", + "make-fetch-happen": "^13.0.1", + "minimatch": "^9.0.5", + "minipass": "^7.1.1", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^10.1.0", + "nopt": "^7.2.1", + "normalize-package-data": "^6.0.2", + "npm-audit-report": "^5.0.0", + "npm-install-checks": "^6.3.0", + "npm-package-arg": "^11.0.2", + "npm-pick-manifest": "^9.1.0", + "npm-profile": "^10.0.0", + "npm-registry-fetch": "^17.1.0", + "npm-user-validate": "^2.0.1", + "p-map": "^4.0.0", + "pacote": "^18.0.6", + "parse-conflict-json": "^3.0.1", + "proc-log": "^4.2.0", + "qrcode-terminal": "^0.12.0", + "read": "^3.0.1", + "semver": "^7.6.2", + "spdx-expression-parse": "^4.0.0", + "ssri": "^10.0.6", + "supports-color": "^9.4.0", + "tar": "^6.2.1", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^5.0.1", + "which": "^4.0.0", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/npm-bundled": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", @@ -16665,58 +17078,2269 @@ "node": ">=8" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, + "node_modules/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "inBundle": true, + "license": "ISC", "dependencies": { - "boolbase": "^1.0.0" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/nwsapi": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", - "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", - "dev": true + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "inBundle": true, + "license": "MIT" }, - "node_modules/nx": { - "version": "19.4.0", - "resolved": "https://registry.npmjs.org/nx/-/nx-19.4.0.tgz", - "integrity": "sha512-tTdKqJ7e9imww6fyx3KrLcMz7oAFIcHFeXTZtdXbyDjIQJaN0HK4hicGVc1t1d1iB81KFfUVpX8/QztdB58Q9A==", - "dev": true, - "hasInstallScript": true, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "inBundle": true, + "license": "MIT", "dependencies": { - "@nrwl/tao": "19.4.0", - "@yarnpkg/lockfile": "^1.1.0", - "@yarnpkg/parsers": "3.0.0-rc.46", - "@zkochan/js-yaml": "0.0.7", - "axios": "^1.6.0", - "chalk": "^4.1.0", - "cli-cursor": "3.1.0", - "cli-spinners": "2.6.1", - "cliui": "^8.0.1", - "dotenv": "~16.4.5", - "dotenv-expand": "~11.0.6", - "enquirer": "~2.3.6", - "figures": "3.2.0", - "flat": "^5.0.2", - "front-matter": "^4.0.2", - "fs-extra": "^11.1.0", - "ignore": "^5.0.4", - "jest-diff": "^29.4.1", - "jsonc-parser": "3.2.0", - "lines-and-columns": "~2.0.3", - "minimatch": "9.0.3", - "node-machine-id": "1.1.12", - "npm-run-path": "^4.0.1", - "open": "^8.4.0", - "ora": "5.3.0", - "semver": "^7.5.3", - "string-width": "^4.2.3", + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/agent": { + "version": "2.2.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "7.5.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^3.1.1", + "@npmcli/installed-package-contents": "^2.1.0", + "@npmcli/map-workspaces": "^3.0.2", + "@npmcli/metavuln-calculator": "^7.1.1", + "@npmcli/name-from-folder": "^2.0.0", + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.1.0", + "@npmcli/query": "^3.1.0", + "@npmcli/redact": "^2.0.0", + "@npmcli/run-script": "^8.1.0", + "bin-links": "^4.0.4", + "cacache": "^18.0.3", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^7.0.2", + "json-parse-even-better-errors": "^3.0.2", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^10.2.2", + "minimatch": "^9.0.4", + "nopt": "^7.2.1", + "npm-install-checks": "^6.2.0", + "npm-package-arg": "^11.0.2", + "npm-pick-manifest": "^9.0.1", + "npm-registry-fetch": "^17.0.1", + "pacote": "^18.0.6", + "parse-conflict-json": "^3.0.0", + "proc-log": "^4.2.0", + "proggy": "^2.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "ssri": "^10.0.6", + "treeverse": "^3.0.0", + "walk-up-path": "^3.0.1" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "8.3.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^3.0.2", + "@npmcli/package-json": "^5.1.1", + "ci-info": "^4.0.0", + "ini": "^4.1.2", + "nopt": "^7.2.1", + "proc-log": "^4.2.0", + "semver": "^7.3.5", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "3.1.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "5.0.8", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "ini": "^4.1.3", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^4.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "3.0.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^2.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0", + "read-package-json-fast": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "7.1.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^18.0.0", + "json-parse-even-better-errors": "^3.0.0", + "pacote": "^18.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "5.2.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^4.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "7.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "3.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/redact": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "8.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "proc-log": "^4.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "2.3.2", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/core": { + "version": "1.1.0", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.3.2", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "2.3.2", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^13.0.1", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "2.3.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^2.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/verify": { + "version": "1.2.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.1.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "7.1.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/aggregate-error": { + "version": "3.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "6.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "4.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "read-cmd-shim": "^4.0.0", + "write-file-atomic": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "2.3.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "18.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.3.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.0.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "4.1.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^5.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/clean-stack": { + "version": "2.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "6.0.3", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.3.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/diff": { + "version": "5.2.0", + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.1", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.2.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "10.4.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "7.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.1.1", + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "7.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "7.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "6.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/indent-string": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ini": { + "version": "4.1.3", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "6.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^5.0.0", + "npm-package-arg": "^11.0.0", + "promzard": "^1.0.0", + "read": "^3.0.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/ip-address": { + "version": "9.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "5.1.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^4.1.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-lambda": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/jackspeak": { + "version": "3.4.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/npm/node_modules/jsbn": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "8.0.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^11.0.2", + "npm-registry-fetch": "^17.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "6.1.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^7.5.4", + "@npmcli/installed-package-contents": "^2.1.0", + "binary-extensions": "^2.3.0", + "diff": "^5.1.0", + "minimatch": "^9.0.4", + "npm-package-arg": "^11.0.2", + "pacote": "^18.0.6", + "tar": "^6.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "8.1.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^7.5.4", + "@npmcli/run-script": "^8.1.0", + "ci-info": "^4.0.0", + "npm-package-arg": "^11.0.2", + "pacote": "^18.0.6", + "proc-log": "^4.2.0", + "read": "^3.0.1", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "5.0.12", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^7.5.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmhook": { + "version": "10.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^17.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "6.0.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^17.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "7.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^7.5.4", + "@npmcli/run-script": "^8.1.0", + "npm-package-arg": "^11.0.2", + "pacote": "^18.0.6" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "9.0.9", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ci-info": "^4.0.0", + "normalize-package-data": "^6.0.1", + "npm-package-arg": "^11.0.2", + "npm-registry-fetch": "^17.0.1", + "proc-log": "^4.2.0", + "semver": "^7.3.7", + "sigstore": "^2.2.0", + "ssri": "^10.0.6" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "7.0.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^17.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "6.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^17.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "6.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.7", + "@npmcli/run-script": "^8.1.0", + "json-parse-even-better-errors": "^3.0.2", + "proc-log": "^4.2.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "10.2.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "13.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "9.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.1.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "3.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "1.0.4", + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "1.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/negotiator": { + "version": "0.6.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "10.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/proc-log": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "7.2.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "6.0.2", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "6.3.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "11.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "8.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "9.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "10.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^17.0.1", + "proc-log": "^4.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "17.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^2.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "2.0.1", + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/package-json-from-dist": { + "version": "1.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/npm/node_modules/pacote": { + "version": "18.0.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/package-json": "^5.1.0", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^8.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^17.0.0", + "proc-log": "^4.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "1.11.1", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "4.2.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/proggy": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-inflight": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "1.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "^3.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "3.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.6.2", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "2.3.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^2.3.2", + "@sigstore/tuf": "^2.3.4", + "@sigstore/verify": "^1.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.8.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "8.0.4", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.5.0", + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.18", + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/sprintf-js": { + "version": "1.1.3", + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/npm/node_modules/ssri": { + "version": "10.0.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "9.4.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "6.2.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/tuf-js": { + "version": "2.2.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "2.0.1", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "5.0.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/which": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/which/node_modules/isexe": { + "version": "3.1.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "5.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", + "dev": true + }, + "node_modules/nx": { + "version": "19.4.0", + "resolved": "https://registry.npmjs.org/nx/-/nx-19.4.0.tgz", + "integrity": "sha512-tTdKqJ7e9imww6fyx3KrLcMz7oAFIcHFeXTZtdXbyDjIQJaN0HK4hicGVc1t1d1iB81KFfUVpX8/QztdB58Q9A==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@nrwl/tao": "19.4.0", + "@yarnpkg/lockfile": "^1.1.0", + "@yarnpkg/parsers": "3.0.0-rc.46", + "@zkochan/js-yaml": "0.0.7", + "axios": "^1.6.0", + "chalk": "^4.1.0", + "cli-cursor": "3.1.0", + "cli-spinners": "2.6.1", + "cliui": "^8.0.1", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "enquirer": "~2.3.6", + "figures": "3.2.0", + "flat": "^5.0.2", + "front-matter": "^4.0.2", + "fs-extra": "^11.1.0", + "ignore": "^5.0.4", + "jest-diff": "^29.4.1", + "jsonc-parser": "3.2.0", + "lines-and-columns": "~2.0.3", + "minimatch": "9.0.3", + "node-machine-id": "1.1.12", + "npm-run-path": "^4.0.1", + "open": "^8.4.0", + "ora": "5.3.0", + "semver": "^7.5.3", + "string-width": "^4.2.3", "strong-log-transformer": "^2.1.0", "tar-stream": "~2.2.0", "tmp": "~0.2.1", @@ -16903,7 +19527,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, + "devOptional": true, "dependencies": { "wrappy": "1" } @@ -17303,7 +19927,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -17363,6 +19987,27 @@ "node": ">=8" } }, + "node_modules/path2d": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.1.tgz", + "integrity": "sha512-Fl2z/BHvkTNvkuBzYTpTuirHZg6wW9z8+4SND/3mDTEcYbbNKWAy21dz9D3ePNNwrrK8pqZO5vLPZ1hLF6T7XA==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pdfjs-dist": { + "version": "4.4.168", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.4.168.tgz", + "integrity": "sha512-MbkAjpwka/dMHaCfQ75RY1FXX3IewBVu6NGZOcxerRFlaBiIkZmUoR0jotX5VUzYZEXAGzSFtknWs5xRKliXPA==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d": "^0.2.0" + } + }, "node_modules/pepjs": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/pepjs/-/pepjs-0.5.3.tgz", @@ -18372,7 +21017,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, + "devOptional": true, "dependencies": { "glob": "^7.1.3" }, @@ -18758,6 +21403,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -18912,7 +21563,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "devOptional": true }, "node_modules/sigstore": { "version": "2.3.1", @@ -18931,6 +21582,37 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-statistics": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.3.tgz", @@ -19503,7 +22185,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, + "devOptional": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -19550,7 +22232,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, + "devOptional": true, "dependencies": { "minipass": "^3.0.0" }, @@ -19562,7 +22244,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -19574,7 +22256,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -19583,7 +22265,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "devOptional": true }, "node_modules/terser": { "version": "5.29.2", @@ -21465,6 +24147,15 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -21582,7 +24273,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "devOptional": true }, "node_modules/write-file-atomic": { "version": "4.0.2", diff --git a/package.json b/package.json index c2835264b4ca..da52b86f857a 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "@angular/forms": "18.0.6", "@angular/localize": "18.0.6", "@angular/material": "18.0.6", - "@angular/platform-browser-dynamic": "18.0.6", "@angular/platform-browser": "18.0.6", + "@angular/platform-browser-dynamic": "18.0.6", "@angular/router": "18.0.6", "@angular/service-worker": "18.0.6", "@ctrl/ngx-emoji-mart": "9.2.0", @@ -49,11 +49,12 @@ "crypto-js": "4.2.0", "dayjs": "1.11.11", "diff-match-patch-typescript": "1.0.8", - "fast-json-patch": "3.1.1", "dompurify": "3.1.6", "export-to-csv": "1.3.0", + "fast-json-patch": "3.1.1", "franc-min": "6.2.0", "html-diff-ts": "1.4.2", + "install": "^0.13.0", "interactjs": "1.10.27", "ismobilejs-es5": "0.0.1", "js-video-url-parser": "0.5.1", @@ -63,7 +64,9 @@ "monaco-editor": "0.50.0", "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", + "npm": "^10.8.2", "papaparse": "5.4.1", + "pdfjs-dist": "^4.4.168", "posthog-js": "1.144.2", "rxjs": "7.8.1", "showdown": "2.1.0", diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java index f698b2ceedd3..7bed858100d9 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java @@ -3,28 +3,20 @@ import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; import static de.tum.in.www1.artemis.service.FilePathService.actualPathForPublicPath; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Base64; import java.util.List; import java.util.Optional; -import javax.imageio.ImageIO; - -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.rendering.PDFRenderer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -223,31 +215,6 @@ else if (attachment.getExercise() != null) { return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, attachmentId.toString())).build(); } - public List convertPdfToImageUrls(String pdfFilePath) { - List imageUrls = new ArrayList<>(); - try (PDDocument document = Loader.loadPDF(new File(pdfFilePath))) { - PDFRenderer pdfRenderer = new PDFRenderer(document); - int lowerDPI = 150; // Reduced from 300 to 150 DPI - for (int page = 0; page < document.getNumberOfPages(); ++page) { - BufferedImage bim = pdfRenderer.renderImageWithDPI(page, lowerDPI); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - ImageIO.write(bim, "png", baos); - byte[] imageBytes = baos.toByteArray(); - String base64Image = Base64.getEncoder().encodeToString(imageBytes); - imageUrls.add("data:image/png;base64," + base64Image); - } - catch (IOException e) { - e.printStackTrace(); - } - } - } - catch (IOException e) { - e.printStackTrace(); - } - return imageUrls; - } - private String resolveFilePath(String webPath) { String baseDir = "uploads"; String prefixToRemove = "/api/files/"; @@ -255,13 +222,17 @@ private String resolveFilePath(String webPath) { return Paths.get(baseDir, pathWithoutPrefix).toString(); } - @GetMapping("/attachments/{id}/pdf-to-images") - public ResponseEntity> convertPdfToImages(@PathVariable Long id) { + @GetMapping("/attachments/{id}/file") + public ResponseEntity getAttachmentFile(@PathVariable Long id) { Attachment attachment = attachmentRepository.findById(id).orElseThrow(); String actualFilePath = resolveFilePath(attachment.getLink()); - File pdfFile = new File(actualFilePath); - List imageUrls = convertPdfToImageUrls(pdfFile.getAbsolutePath()); - return ResponseEntity.ok(imageUrls); - } + Resource resource = new FileSystemResource(actualFilePath); + if (!resource.exists()) { + throw new RuntimeException("File not found " + actualFilePath); + } + String contentType = "application/pdf"; + return ResponseEntity.ok().contentType(MediaType.parseMediaType(contentType)) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"").body(resource); + } } diff --git a/src/main/webapp/app/lecture/attachment.service.ts b/src/main/webapp/app/lecture/attachment.service.ts index 1b9d942fbceb..e7d0da9b3a5d 100644 --- a/src/main/webapp/app/lecture/attachment.service.ts +++ b/src/main/webapp/app/lecture/attachment.service.ts @@ -136,9 +136,7 @@ export class AttachmentService { return formData; } - getPdfImages(attachmentId: number): Observable { - return this.http.get(`${this.resourceUrl}/${attachmentId}/pdf-to-images`).pipe( - map((response) => response), // Assuming response is directly the array of base64 strings - ); + getAttachmentFile(attachmentId: number): Observable { + return this.http.get(`${this.resourceUrl}/${attachmentId}/file`, { responseType: 'blob' }); } } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index c5108de5cd21..63ca17e067a3 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -4,14 +4,7 @@

-
- {{ attachment?.name }} -
- @for (imageUrl of imageUrls; track imageUrl) { - title - } -
-
+
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index 20505725bb56..4fa9472cb807 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -1,16 +1,20 @@ -.grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); - gap: 10px; - border: 1px solid lightgrey; +.pdf-container { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: left; + border: 1px solid #ccc; padding: 10px; - background: #fff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - margin: 10px; + margin: 0 auto; + width: 95%; + height: 60vh; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } -.thumbnail { - width: 100%; - height: auto; - object-fit: cover; +.pdf-container canvas { + margin: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + height: 200px; + width: auto; + object-fit: contain; } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 2fec4463e7b9..3fcbcb8bcd71 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -1,8 +1,8 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { Attachment } from 'app/entities/attachment.model'; +import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { AttachmentService } from 'app/lecture/attachment.service'; -import { HttpResponse } from '@angular/common/http'; +import * as PDFJS from 'pdfjs-dist'; +import 'pdfjs-dist/build/pdf.worker.mjs'; @Component({ selector: 'jhi-pdf-preview-component', @@ -11,9 +11,7 @@ import { HttpResponse } from '@angular/common/http'; }) export class PdfPreviewComponent implements OnInit { @Input() attachmentId: number; - - attachment: Attachment; - imageUrls: string[] = []; + @ViewChild('pdfContainer', { static: true }) pdfContainer: ElementRef; constructor( private route: ActivatedRoute, @@ -22,24 +20,43 @@ export class PdfPreviewComponent implements OnInit { ngOnInit() { this.route.params.subscribe((params) => { - this.attachmentId = +params['attachmentId']; // Make sure this is always defined - if (this.attachmentId) { - this.attachmentService.find(this.attachmentId).subscribe((attachmentResponse: HttpResponse) => { - this.attachment = attachmentResponse.body!; - if (this.attachment && this.attachment.id) { - // Check if id is defined - this.attachmentService.getPdfImages(this.attachment.id).subscribe( - (imageUrls) => { - console.log(imageUrls); - this.imageUrls = imageUrls; - }, - (error) => { - console.error('Failed to load images', error); - }, - ); - } - }); + const attachmentId = +params['attachmentId']; + if (attachmentId) { + this.attachmentService.getAttachmentFile(attachmentId).subscribe( + (blob: Blob) => { + const fileURL = URL.createObjectURL(blob); + this.loadPdf(fileURL); + }, + (error) => console.error('Failed to load PDF file', error), + ); } }); } + + private loadPdf(fileUrl: string) { + const loadingTask = PDFJS.getDocument(fileUrl); + loadingTask.promise.then( + (pdf: { numPages: number; getPage: (arg0: number) => Promise }) => { + for (let i = 1; i <= pdf.numPages; i++) { + pdf.getPage(i).then((page) => { + const viewport = page.getViewport({ scale: 0.5 }); + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + const renderTask = page.render({ + canvasContext: context, + viewport: viewport, + }); + renderTask.promise.then(() => { + this.pdfContainer.nativeElement.appendChild(canvas); + URL.revokeObjectURL(fileUrl); + }); + }); + } + }, + (error: any) => { + console.error('Error loading PDF: ', error); + }, + ); + } } From 5ef93eb1f5b4e56c8e6c28fc29f26713d2f46799 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 15 Jul 2024 11:05:50 +0200 Subject: [PATCH 004/125] Enlarge PDF page content --- package-lock.json | 108 +++++++++++++++ package.json | 2 + .../pdf-preview/pdf-preview.component.html | 9 +- .../pdf-preview/pdf-preview.component.scss | 48 +++++-- .../pdf-preview/pdf-preview.component.ts | 125 ++++++++++++++---- 5 files changed, 250 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0c1bf0dcc39..81fc1db1d2b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "@sentry/angular": "8.15.0", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", + "@types/pdfjs-dist": "^2.10.378", "@vscode/codicons": "0.0.36", "ace-builds": "1.35.2", "bootstrap": "5.3.3", @@ -63,6 +64,7 @@ "ngx-webstorage": "18.0.0", "npm": "^10.8.2", "papaparse": "5.4.1", + "pdfjs": "^2.5.3", "pdfjs-dist": "^4.4.168", "posthog-js": "1.144.2", "rxjs": "7.8.1", @@ -6257,6 +6259,14 @@ "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" }, + "node_modules/@rkusa/linebreak": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rkusa/linebreak/-/linebreak-1.0.0.tgz", + "integrity": "sha512-yCSm87XA1aYMgfcABSxcIkk3JtCw3AihNceHY+DnZGLvVP/g2z3UWZbi0xIoYpZWAJEVPr5Zt3QE37Q80wF1pA==", + "dependencies": { + "unicode-trie": "^0.3.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", @@ -7164,6 +7174,15 @@ "@types/node": "*" } }, + "node_modules/@types/pdfjs-dist": { + "version": "2.10.378", + "resolved": "https://registry.npmjs.org/@types/pdfjs-dist/-/pdfjs-dist-2.10.378.tgz", + "integrity": "sha512-TRdIPqdsvKmPla44kVy4jv5Nt5vjMfVjbIEke1CRULIrwKNRC4lIiZvNYDJvbUMNCFPNIUcOKhXTyMJrX18IMA==", + "deprecated": "This is a stub types definition. pdfjs-dist provides its own type definitions, so you do not need this installed.", + "dependencies": { + "pdfjs-dist": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -19564,6 +19583,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opentype.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz", + "integrity": "sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw==", + "dependencies": { + "string.prototype.codepointat": "^0.2.1", + "tiny-inflate": "^1.0.3" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -19996,6 +20030,22 @@ "node": ">=6" } }, + "node_modules/pdfjs": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/pdfjs/-/pdfjs-2.5.3.tgz", + "integrity": "sha512-XSFh7/znM7gJAVABFvrtIkxi6TcHyHUCYpwaRUv1h0ln2ZQel0s8nKgsvmo+D7IKkkXKEQNtMU/hdmF/MUeaHg==", + "dependencies": { + "@rkusa/linebreak": "^1.0.0", + "opentype.js": "^1.3.3", + "pako": "^2.0.3", + "readable-stream": "^3.6.0", + "unorm": "^1.6.0", + "uuid": "^8.3.1" + }, + "engines": { + "node": ">=7" + } + }, "node_modules/pdfjs-dist": { "version": "4.4.168", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.4.168.tgz", @@ -20008,6 +20058,32 @@ "path2d": "^0.2.0" } }, + "node_modules/pdfjs/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, + "node_modules/pdfjs/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pdfjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/pepjs": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/pepjs/-/pepjs-0.5.3.tgz", @@ -22018,6 +22094,11 @@ "node": ">=8" } }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==" + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -22478,6 +22559,11 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", @@ -22918,6 +23004,20 @@ "node": ">=4" } }, + "node_modules/unicode-trie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-0.3.1.tgz", + "integrity": "sha512-WgVuO0M2jDl7hVfbPgXv2LUrD81HM0bQj/bvLGiw6fJ4Zo8nNFnDrA0/hU2Te/wz6pjxCm5cxJwtLjo2eyV51Q==", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, "node_modules/unique-filename": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", @@ -22951,6 +23051,14 @@ "node": ">= 10.0.0" } }, + "node_modules/unorm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", + "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index da52b86f857a..f1dd420ec3ee 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@sentry/angular": "8.15.0", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", + "@types/pdfjs-dist": "^2.10.378", "@vscode/codicons": "0.0.36", "ace-builds": "1.35.2", "bootstrap": "5.3.3", @@ -66,6 +67,7 @@ "ngx-webstorage": "18.0.0", "npm": "^10.8.2", "papaparse": "5.4.1", + "pdfjs": "^2.5.3", "pdfjs-dist": "^4.4.168", "posthog-js": "1.144.2", "rxjs": "7.8.1", diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 63ca17e067a3..11ea8a1899e2 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -4,7 +4,14 @@

-
+
+ @if (isEnlargedView) { +
+ + +
+ } +
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index 4fa9472cb807..2100d3ae1a9b 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -1,20 +1,44 @@ .pdf-container { - display: flex; - flex-wrap: wrap; - align-items: flex-start; - justify-content: left; + position: relative; /* Set as a positioning context */ + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 30px; + max-height: 75vh; + overflow-y: auto; border: 1px solid #ccc; padding: 10px; - margin: 0 auto; + margin: 10px; width: 95%; - height: 60vh; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + align-items: start; } -.pdf-container canvas { - margin: 10px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); - height: 200px; - width: auto; - object-fit: contain; +.enlarged-container { + position: absolute; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.2); + z-index: 1050; +} + +.btn-close { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; +} + +@media (max-width: 800px) { + .pdf-container { + grid-template-columns: repeat(2, 1fr); /* 2 columns for smaller screens */ + } +} + +@media (max-width: 500px) { + .pdf-container { + grid-template-columns: 1fr; /* 1 column for very small screens */ + } } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 3fcbcb8bcd71..3331296b6381 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -2,7 +2,7 @@ import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { AttachmentService } from 'app/lecture/attachment.service'; import * as PDFJS from 'pdfjs-dist'; -import 'pdfjs-dist/build/pdf.worker.mjs'; +import 'pdfjs-dist/build/pdf.worker'; @Component({ selector: 'jhi-pdf-preview-component', @@ -12,6 +12,8 @@ import 'pdfjs-dist/build/pdf.worker.mjs'; export class PdfPreviewComponent implements OnInit { @Input() attachmentId: number; @ViewChild('pdfContainer', { static: true }) pdfContainer: ElementRef; + @ViewChild('enlargedCanvas') enlargedCanvas: ElementRef; + isEnlargedView: boolean = false; constructor( private route: ActivatedRoute, @@ -22,41 +24,106 @@ export class PdfPreviewComponent implements OnInit { this.route.params.subscribe((params) => { const attachmentId = +params['attachmentId']; if (attachmentId) { - this.attachmentService.getAttachmentFile(attachmentId).subscribe( - (blob: Blob) => { + this.attachmentService.getAttachmentFile(attachmentId).subscribe({ + next: (blob: Blob) => { const fileURL = URL.createObjectURL(blob); this.loadPdf(fileURL); }, - (error) => console.error('Failed to load PDF file', error), - ); + error: (error) => { + console.error('Failed to load PDF file', error); + }, + }); } }); } - private loadPdf(fileUrl: string) { - const loadingTask = PDFJS.getDocument(fileUrl); - loadingTask.promise.then( - (pdf: { numPages: number; getPage: (arg0: number) => Promise }) => { - for (let i = 1; i <= pdf.numPages; i++) { - pdf.getPage(i).then((page) => { - const viewport = page.getViewport({ scale: 0.5 }); - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - - const renderTask = page.render({ - canvasContext: context, - viewport: viewport, - }); - renderTask.promise.then(() => { - this.pdfContainer.nativeElement.appendChild(canvas); - URL.revokeObjectURL(fileUrl); - }); - }); + private async loadPdf(fileUrl: string) { + try { + const loadingTask = PDFJS.getDocument(fileUrl); + const pdf = await loadingTask.promise; + const numPages = pdf.numPages; + const pages = []; + + for (let i = 1; i <= numPages; i++) { + const page = await pdf.getPage(i); + const viewport = page.getViewport({ scale: 1 }); + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (context) { + canvas.height = viewport.height; + canvas.width = viewport.width; + + await page.render({ + canvasContext: context, + viewport: viewport, + }).promise; + + pages.push({ canvas, i }); } - }, - (error: any) => { - console.error('Error loading PDF: ', error); - }, - ); + + // Sort and append canvases to the container + pages.sort((a, b) => a.i - b.i); + pages.forEach((page) => { + page.canvas.style.width = 'auto'; + page.canvas.style.height = '150px'; + page.canvas.style.margin = '20px'; + page.canvas.style.boxShadow = '0 2px 6px rgba(0, 0, 0, 0.1)'; + page.canvas.style.transition = 'transform 0.3s ease, box-shadow 0.3s ease'; + page.canvas.style.cursor = 'pointer'; + this.pdfContainer.nativeElement.appendChild(page.canvas); + + page.canvas.addEventListener('click', () => { + this.displayEnlargedCanvas(page.canvas); + }); + }); + + URL.revokeObjectURL(fileUrl); + } + } catch (error) { + console.error('Error loading PDF:', error); + } + } + + private displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { + this.isEnlargedView = true; + this.toggleBodyScroll(true); // Optional: Disable scrolling when enlarged view is active + + setTimeout(() => { + if (this.isEnlargedView) { + const enlargedCanvas = this.enlargedCanvas.nativeElement; + const context = enlargedCanvas.getContext('2d'); + const containerWidth = this.pdfContainer.nativeElement.clientWidth; + const containerHeight = this.pdfContainer.nativeElement.clientHeight; + const scrollOffset = this.pdfContainer.nativeElement.scrollTop; + + // Calculate scale factor based on the container size and original canvas size + const widthScale = containerWidth / originalCanvas.width; + const heightScale = containerHeight / originalCanvas.height; + const scaleFactor = Math.min(1, widthScale, heightScale); // Ensures that the canvas does not exceed the container + + enlargedCanvas.width = originalCanvas.width * scaleFactor; + enlargedCanvas.height = originalCanvas.height * scaleFactor; + + context.clearRect(0, 0, enlargedCanvas.width, enlargedCanvas.height); + context.drawImage(originalCanvas, 0, 0, enlargedCanvas.width, enlargedCanvas.height); + + // Set the top position based on the current scroll position + this.enlargedCanvas.nativeElement.parentElement.style.top = `${scrollOffset}px`; + } + }, 50); + } + + closeEnlargedView() { + this.isEnlargedView = false; + this.toggleBodyScroll(false); + } + + toggleBodyScroll(disable: boolean): void { + const pdfContainerElement = this.pdfContainer.nativeElement; + if (disable) { + pdfContainerElement.style.overflow = 'hidden'; + } else { + pdfContainerElement.style.overflow = 'auto'; + } } } From e36d0aa8dfde49838ab327ef14138884c2af0f16 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 15 Jul 2024 14:17:49 +0200 Subject: [PATCH 005/125] Show page numbers when hovered --- .../pdf-preview/pdf-preview.component.scss | 4 +- .../pdf-preview/pdf-preview.component.ts | 70 ++++++++++++++----- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index 2100d3ae1a9b..8209d2ef2410 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -33,12 +33,12 @@ @media (max-width: 800px) { .pdf-container { - grid-template-columns: repeat(2, 1fr); /* 2 columns for smaller screens */ + grid-template-columns: repeat(2, 1fr); } } @media (max-width: 500px) { .pdf-container { - grid-template-columns: 1fr; /* 1 column for very small screens */ + grid-template-columns: 1fr; } } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 3331296b6381..37651b1e4f37 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -42,7 +42,6 @@ export class PdfPreviewComponent implements OnInit { const loadingTask = PDFJS.getDocument(fileUrl); const pdf = await loadingTask.promise; const numPages = pdf.numPages; - const pages = []; for (let i = 1; i <= numPages; i++) { const page = await pdf.getPage(i); @@ -58,27 +57,60 @@ export class PdfPreviewComponent implements OnInit { viewport: viewport, }).promise; - pages.push({ canvas, i }); - } - - // Sort and append canvases to the container - pages.sort((a, b) => a.i - b.i); - pages.forEach((page) => { - page.canvas.style.width = 'auto'; - page.canvas.style.height = '150px'; - page.canvas.style.margin = '20px'; - page.canvas.style.boxShadow = '0 2px 6px rgba(0, 0, 0, 0.1)'; - page.canvas.style.transition = 'transform 0.3s ease, box-shadow 0.3s ease'; - page.canvas.style.cursor = 'pointer'; - this.pdfContainer.nativeElement.appendChild(page.canvas); - - page.canvas.addEventListener('click', () => { - this.displayEnlargedCanvas(page.canvas); + const fixedWidth = 250; + const scaleFactor = fixedWidth / viewport.width; + const fixedHeight = viewport.height * scaleFactor; + + canvas.style.width = `${fixedWidth}px`; + canvas.style.height = `${fixedHeight}px`; + + const container = document.createElement('div'); + container.classList.add('pdf-page-container'); + container.style.position = 'relative'; + container.style.display = 'inline-block'; + container.style.width = `${fixedWidth}px`; + container.style.height = `${fixedHeight}px`; + container.style.margin = '20px'; // Margin for the container, not the canvas + container.style.boxShadow = '0 2px 6px rgba(0, 0, 0, 0.1)'; + + const overlay = document.createElement('div'); + overlay.classList.add('pdf-page-overlay'); + overlay.innerHTML = `${i}`; + overlay.style.position = 'absolute'; + overlay.style.top = '0'; + overlay.style.left = '0'; + overlay.style.width = '100%'; + overlay.style.height = '100%'; + overlay.style.display = 'flex'; + overlay.style.justifyContent = 'center'; + overlay.style.alignItems = 'center'; + overlay.style.fontSize = '24px'; + overlay.style.color = 'white'; + overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.4)'; + overlay.style.zIndex = '1'; + overlay.style.transition = 'opacity 0.3s ease'; + overlay.style.opacity = '0'; + overlay.style.cursor = 'pointer'; + + container.appendChild(canvas); + container.appendChild(overlay); + + this.pdfContainer.nativeElement.appendChild(container); + + container.addEventListener('mouseenter', () => { + overlay.style.opacity = '1'; + }); + container.addEventListener('mouseleave', () => { + overlay.style.opacity = '0'; }); - }); - URL.revokeObjectURL(fileUrl); + overlay.addEventListener('click', () => { + this.displayEnlargedCanvas(canvas); + }); + } } + + URL.revokeObjectURL(fileUrl); } catch (error) { console.error('Error loading PDF:', error); } From d88a85b552508425f514ad016cd8b8d52a8f719e Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 15 Jul 2024 14:22:48 +0200 Subject: [PATCH 006/125] Clean up the code & styling --- .../pdf-preview/pdf-preview.component.scss | 34 +++-- .../pdf-preview/pdf-preview.component.ts | 130 +++++++----------- 2 files changed, 63 insertions(+), 101 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index 8209d2ef2410..e21a31b11763 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -1,5 +1,5 @@ .pdf-container { - position: relative; /* Set as a positioning context */ + position: relative; display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 30px; @@ -11,10 +11,20 @@ width: 95%; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); align-items: start; + + @media (max-width: 800px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (max-width: 500px) { + grid-template-columns: 1fr; + } } .enlarged-container { position: absolute; + top: 0; + left: 0; width: 100%; height: 100%; display: flex; @@ -22,23 +32,11 @@ align-items: center; background-color: rgba(0, 0, 0, 0.2); z-index: 1050; -} -.btn-close { - position: absolute; - top: 10px; - right: 10px; - cursor: pointer; -} - -@media (max-width: 800px) { - .pdf-container { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (max-width: 500px) { - .pdf-container { - grid-template-columns: 1fr; + .btn-close { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; } } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 37651b1e4f37..0d90337ddc14 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -25,13 +25,8 @@ export class PdfPreviewComponent implements OnInit { const attachmentId = +params['attachmentId']; if (attachmentId) { this.attachmentService.getAttachmentFile(attachmentId).subscribe({ - next: (blob: Blob) => { - const fileURL = URL.createObjectURL(blob); - this.loadPdf(fileURL); - }, - error: (error) => { - console.error('Failed to load PDF file', error); - }, + next: (blob: Blob) => this.loadPdf(URL.createObjectURL(blob)), + error: (error) => console.error('Failed to load PDF file', error), }); } }); @@ -41,73 +36,18 @@ export class PdfPreviewComponent implements OnInit { try { const loadingTask = PDFJS.getDocument(fileUrl); const pdf = await loadingTask.promise; - const numPages = pdf.numPages; - for (let i = 1; i <= numPages; i++) { + for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const viewport = page.getViewport({ scale: 1 }); - const canvas = document.createElement('canvas'); + const canvas = this.createCanvas(viewport); const context = canvas.getContext('2d'); if (context) { - canvas.height = viewport.height; - canvas.width = viewport.width; - - await page.render({ - canvasContext: context, - viewport: viewport, - }).promise; - - const fixedWidth = 250; - const scaleFactor = fixedWidth / viewport.width; - const fixedHeight = viewport.height * scaleFactor; - - canvas.style.width = `${fixedWidth}px`; - canvas.style.height = `${fixedHeight}px`; - - const container = document.createElement('div'); - container.classList.add('pdf-page-container'); - container.style.position = 'relative'; - container.style.display = 'inline-block'; - container.style.width = `${fixedWidth}px`; - container.style.height = `${fixedHeight}px`; - container.style.margin = '20px'; // Margin for the container, not the canvas - container.style.boxShadow = '0 2px 6px rgba(0, 0, 0, 0.1)'; - - const overlay = document.createElement('div'); - overlay.classList.add('pdf-page-overlay'); - overlay.innerHTML = `${i}`; - overlay.style.position = 'absolute'; - overlay.style.top = '0'; - overlay.style.left = '0'; - overlay.style.width = '100%'; - overlay.style.height = '100%'; - overlay.style.display = 'flex'; - overlay.style.justifyContent = 'center'; - overlay.style.alignItems = 'center'; - overlay.style.fontSize = '24px'; - overlay.style.color = 'white'; - overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.4)'; - overlay.style.zIndex = '1'; - overlay.style.transition = 'opacity 0.3s ease'; - overlay.style.opacity = '0'; - overlay.style.cursor = 'pointer'; - - container.appendChild(canvas); - container.appendChild(overlay); - - this.pdfContainer.nativeElement.appendChild(container); - - container.addEventListener('mouseenter', () => { - overlay.style.opacity = '1'; - }); - container.addEventListener('mouseleave', () => { - overlay.style.opacity = '0'; - }); - - overlay.addEventListener('click', () => { - this.displayEnlargedCanvas(canvas); - }); + await page.render({ canvasContext: context, viewport }).promise; } + + const container = this.createContainer(canvas, i); + this.pdfContainer.nativeElement.appendChild(container); } URL.revokeObjectURL(fileUrl); @@ -116,9 +56,44 @@ export class PdfPreviewComponent implements OnInit { } } + private createCanvas(viewport: PDFJS.PageViewport): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + canvas.height = viewport.height; + canvas.width = viewport.width; + const fixedWidth = 250; + const scaleFactor = fixedWidth / viewport.width; + canvas.style.width = `${fixedWidth}px`; + canvas.style.height = `${viewport.height * scaleFactor}px`; + return canvas; + } + + private createContainer(canvas: HTMLCanvasElement, pageIndex: number): HTMLDivElement { + const container = document.createElement('div'); + container.classList.add('pdf-page-container'); + container.style.cssText = `position: relative; display: inline-block; width: ${canvas.style.width}; height: ${canvas.style.height}; margin: 20px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);`; + + const overlay = this.createOverlay(pageIndex); + container.appendChild(canvas); + container.appendChild(overlay); + + container.addEventListener('mouseenter', () => (overlay.style.opacity = '1')); + container.addEventListener('mouseleave', () => (overlay.style.opacity = '0')); + overlay.addEventListener('click', () => this.displayEnlargedCanvas(canvas)); + + return container; + } + + private createOverlay(pageIndex: number): HTMLDivElement { + const overlay = document.createElement('div'); + overlay.classList.add('pdf-page-overlay'); + overlay.innerHTML = `${pageIndex}`; + overlay.style.cssText = `position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; font-size: 24px; color: white; background-color: rgba(0, 0, 0, 0.4); z-index: 1; transition: opacity 0.3s ease; opacity: 0; cursor: pointer;`; + return overlay; + } + private displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { this.isEnlargedView = true; - this.toggleBodyScroll(true); // Optional: Disable scrolling when enlarged view is active + this.toggleBodyScroll(true); setTimeout(() => { if (this.isEnlargedView) { @@ -126,21 +101,15 @@ export class PdfPreviewComponent implements OnInit { const context = enlargedCanvas.getContext('2d'); const containerWidth = this.pdfContainer.nativeElement.clientWidth; const containerHeight = this.pdfContainer.nativeElement.clientHeight; - const scrollOffset = this.pdfContainer.nativeElement.scrollTop; - - // Calculate scale factor based on the container size and original canvas size - const widthScale = containerWidth / originalCanvas.width; - const heightScale = containerHeight / originalCanvas.height; - const scaleFactor = Math.min(1, widthScale, heightScale); // Ensures that the canvas does not exceed the container + const scaleFactor = Math.min(1, containerWidth / originalCanvas.width, containerHeight / originalCanvas.height); enlargedCanvas.width = originalCanvas.width * scaleFactor; enlargedCanvas.height = originalCanvas.height * scaleFactor; context.clearRect(0, 0, enlargedCanvas.width, enlargedCanvas.height); context.drawImage(originalCanvas, 0, 0, enlargedCanvas.width, enlargedCanvas.height); - // Set the top position based on the current scroll position - this.enlargedCanvas.nativeElement.parentElement.style.top = `${scrollOffset}px`; + enlargedCanvas.parentElement.style.top = `${this.pdfContainer.nativeElement.scrollTop}px`; } }, 50); } @@ -151,11 +120,6 @@ export class PdfPreviewComponent implements OnInit { } toggleBodyScroll(disable: boolean): void { - const pdfContainerElement = this.pdfContainer.nativeElement; - if (disable) { - pdfContainerElement.style.overflow = 'hidden'; - } else { - pdfContainerElement.style.overflow = 'auto'; - } + this.pdfContainer.nativeElement.style.overflow = disable ? 'hidden' : 'auto'; } } From 56a3abe0fb302008311499bdb11b41441a654c02 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 15 Jul 2024 15:08:42 +0200 Subject: [PATCH 007/125] Add PDF Preview to correct routing --- src/main/webapp/app/app-routing.module.ts | 6 ---- src/main/webapp/app/lecture/lecture.route.ts | 32 +++++++++++++++++++ .../pdf-preview/pdf-preview.component.html | 2 +- .../pdf-preview/pdf-preview.component.scss | 2 +- .../pdf-preview/pdf-preview.component.ts | 15 +++++---- 5 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/main/webapp/app/app-routing.module.ts b/src/main/webapp/app/app-routing.module.ts index 0281b9a2101d..dc106122148b 100644 --- a/src/main/webapp/app/app-routing.module.ts +++ b/src/main/webapp/app/app-routing.module.ts @@ -6,7 +6,6 @@ import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { AboutIrisComponent } from 'app/iris/about-iris/about-iris.component'; import { ProblemStatementComponent } from './overview/exercise-details/problem-statement/problem-statement.component'; import { StandaloneFeedbackComponent } from './exercises/shared/feedback/standalone-feedback/standalone-feedback.component'; -import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component'; const LAYOUT_ROUTES: Routes = [navbarRoute, ...errorRoute]; @@ -66,11 +65,6 @@ const LAYOUT_ROUTES: Routes = [navbarRoute, ...errorRoute]; loadChildren: () => import('./exercises/programming/manage/programming-exercise-management-routing.module').then((m) => m.ArtemisProgrammingExerciseManagementRoutingModule), }, - { - path: 'course-management/:courseId/lectures/:lectureId/attachments/:attachmentId', - pathMatch: 'full', - component: PdfPreviewComponent, - }, { path: 'courses', loadChildren: () => import('./overview/courses.module').then((m) => m.ArtemisCoursesModule), diff --git a/src/main/webapp/app/lecture/lecture.route.ts b/src/main/webapp/app/lecture/lecture.route.ts index 60fedb85f963..b21d91a86a10 100644 --- a/src/main/webapp/app/lecture/lecture.route.ts +++ b/src/main/webapp/app/lecture/lecture.route.ts @@ -14,6 +14,9 @@ import { Authority } from 'app/shared/constants/authority.constants'; import { lectureUnitRoute } from 'app/lecture/lecture-unit/lecture-unit-management/lecture-unit-management.route'; import { CourseManagementResolve } from 'app/course/manage/course-management-resolve.service'; import { CourseManagementTabBarComponent } from 'app/course/manage/course-management-tab-bar/course-management-tab-bar.component'; +import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component'; +import { Attachment } from 'app/entities/attachment.model'; +import { AttachmentService } from 'app/lecture/attachment.service'; @Injectable({ providedIn: 'root' }) export class LectureResolve implements Resolve { @@ -31,6 +34,22 @@ export class LectureResolve implements Resolve { } } +@Injectable({ providedIn: 'root' }) +export class AttachmentResolve implements Resolve { + constructor(private attachmentService: AttachmentService) {} + + resolve(route: ActivatedRouteSnapshot): Observable { + const attachmentId = route.params['attachmentId']; + if (attachmentId) { + return this.attachmentService.find(attachmentId).pipe( + filter((response: HttpResponse) => response.ok), + map((attachment: HttpResponse) => attachment.body!), + ); + } + return of(new Attachment()); + } +} + export const lectureRoute: Routes = [ { path: ':courseId/lectures', @@ -91,6 +110,19 @@ export const lectureRoute: Routes = [ }, canActivate: [UserRouteAccessService], }, + { + path: 'attachments', + canActivate: [UserRouteAccessService], + children: [ + { + path: ':attachmentId', + component: PdfPreviewComponent, + resolve: { + attachment: AttachmentResolve, + }, + }, + ], + }, { path: 'edit', component: LectureUpdateComponent, diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 11ea8a1899e2..0f6902c2fbaa 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -2,7 +2,7 @@
-
+

Attachment {{ attachment.id }}: {{ attachment.name }}

@if (isEnlargedView) { diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index e21a31b11763..03c701604041 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -3,7 +3,7 @@ display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 30px; - max-height: 75vh; + max-height: 60vh; overflow-y: auto; border: 1px solid #ccc; padding: 10px; diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 0d90337ddc14..1b52e74eb896 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -1,8 +1,9 @@ -import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { AttachmentService } from 'app/lecture/attachment.service'; import * as PDFJS from 'pdfjs-dist'; import 'pdfjs-dist/build/pdf.worker'; +import { Attachment } from 'app/entities/attachment.model'; @Component({ selector: 'jhi-pdf-preview-component', @@ -10,7 +11,7 @@ import 'pdfjs-dist/build/pdf.worker'; styleUrls: ['./pdf-preview.component.scss'], }) export class PdfPreviewComponent implements OnInit { - @Input() attachmentId: number; + attachment: Attachment; @ViewChild('pdfContainer', { static: true }) pdfContainer: ElementRef; @ViewChild('enlargedCanvas') enlargedCanvas: ElementRef; isEnlargedView: boolean = false; @@ -21,13 +22,15 @@ export class PdfPreviewComponent implements OnInit { ) {} ngOnInit() { - this.route.params.subscribe((params) => { - const attachmentId = +params['attachmentId']; - if (attachmentId) { - this.attachmentService.getAttachmentFile(attachmentId).subscribe({ + this.route.data.subscribe((data: { attachment: Attachment }) => { + this.attachment = data.attachment; + if (this.attachment && this.attachment.id) { + this.attachmentService.getAttachmentFile(this.attachment.id).subscribe({ next: (blob: Blob) => this.loadPdf(URL.createObjectURL(blob)), error: (error) => console.error('Failed to load PDF file', error), }); + } else { + console.error('Invalid attachment or attachment ID.'); } }); } From f691d00977ae6f7ded9459999441c9655c10674b Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 21 Jul 2024 13:27:41 +0200 Subject: [PATCH 008/125] Close PDF preview when clicked outside --- .../app/lecture/pdf-preview/pdf-preview.component.html | 4 ++-- .../app/lecture/pdf-preview/pdf-preview.component.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 0f6902c2fbaa..201e1ef801e3 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -6,9 +6,9 @@

Attachment {{ attachment.id }}: {{ attachment.name }}

@if (isEnlargedView) { -
+
- +
}
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 1b52e74eb896..d980a27393e7 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -125,4 +125,13 @@ export class PdfPreviewComponent implements OnInit { toggleBodyScroll(disable: boolean): void { this.pdfContainer.nativeElement.style.overflow = disable ? 'hidden' : 'auto'; } + + closeIfOutside(event: MouseEvent): void { + const target = event.target as HTMLElement; + const enlargedCanvas = this.enlargedCanvas.nativeElement; + + if (target.classList.contains('enlarged-container') && target !== enlargedCanvas) { + this.closeEnlargedView(); + } + } } From 5df20756296615f0590294f9645dcf990aa4f0ff Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 21 Jul 2024 14:13:11 +0200 Subject: [PATCH 009/125] Add arrows to change pages --- .../pdf-preview/pdf-preview.component.html | 4 +++ .../pdf-preview/pdf-preview.component.scss | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 201e1ef801e3..ccc8efcb3553 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -9,6 +9,10 @@

Attachment {{ attachment.id }}: {{ attachment.name }}

+
+ + +
}
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index 03c701604041..3b4f168e6124 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -40,3 +40,35 @@ cursor: pointer; } } + +.nav-button { + position: absolute; + transform: translateY(-50%); + cursor: pointer; + background-color: #fff; + border: 1px solid #ccc; + border-radius: 50%; + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + font-size: 20px; + z-index: 10; +} + +.nav-button.left { + left: calc(5% + 10px); + + @media (max-width: 1200px) { + left: 10px; + } +} + +.nav-button.right { + right: calc(5% + 10px); + + @media (max-width: 1200px) { + right: 10px; + } +} From 6bb13fb8a8d3201e5655335c1c942f29424c9085 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 21 Jul 2024 14:31:13 +0200 Subject: [PATCH 010/125] Add functionality to the arrows --- .../pdf-preview/pdf-preview.component.html | 30 ++++++++-------- .../pdf-preview/pdf-preview.component.ts | 35 ++++++++++++++++--- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index ccc8efcb3553..bae8b323fd44 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -1,21 +1,21 @@
-
-

Attachment {{ attachment.id }}: {{ attachment.name }}

-
-
- @if (isEnlargedView) { -
- - -
- - -
-
- } -
+

Attachment {{ attachment.id }}: {{ attachment.name }}

+
+
+ @if (isEnlargedView) { +
+ + + @if (currentPage !== 1) { + + } + @if (currentPage !== totalPages) { + + } +
+ }
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index d980a27393e7..549c91a520e5 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -11,10 +11,12 @@ import { Attachment } from 'app/entities/attachment.model'; styleUrls: ['./pdf-preview.component.scss'], }) export class PdfPreviewComponent implements OnInit { - attachment: Attachment; @ViewChild('pdfContainer', { static: true }) pdfContainer: ElementRef; @ViewChild('enlargedCanvas') enlargedCanvas: ElementRef; + attachment: Attachment; isEnlargedView: boolean = false; + currentPage: number = 1; + totalPages: number = 0; constructor( private route: ActivatedRoute, @@ -33,12 +35,14 @@ export class PdfPreviewComponent implements OnInit { console.error('Invalid attachment or attachment ID.'); } }); + document.addEventListener('keydown', this.handleKeyboardEvents); } private async loadPdf(fileUrl: string) { try { const loadingTask = PDFJS.getDocument(fileUrl); const pdf = await loadingTask.promise; + this.totalPages = pdf.numPages; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); @@ -81,7 +85,7 @@ export class PdfPreviewComponent implements OnInit { container.addEventListener('mouseenter', () => (overlay.style.opacity = '1')); container.addEventListener('mouseleave', () => (overlay.style.opacity = '0')); - overlay.addEventListener('click', () => this.displayEnlargedCanvas(canvas)); + overlay.addEventListener('click', () => this.displayEnlargedCanvas(canvas, pageIndex)); return container; } @@ -94,10 +98,13 @@ export class PdfPreviewComponent implements OnInit { return overlay; } - private displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { + private displayEnlargedCanvas(originalCanvas: HTMLCanvasElement, pageIndex: number) { this.isEnlargedView = true; - this.toggleBodyScroll(true); + this.currentPage = pageIndex; + this.updateEnlargedCanvas(originalCanvas); + } + private updateEnlargedCanvas(originalCanvas: HTMLCanvasElement) { setTimeout(() => { if (this.isEnlargedView) { const enlargedCanvas = this.enlargedCanvas.nativeElement; @@ -117,6 +124,16 @@ export class PdfPreviewComponent implements OnInit { }, 50); } + handleKeyboardEvents = (event: KeyboardEvent) => { + if (this.isEnlargedView) { + if (event.key === 'ArrowRight' && this.currentPage < this.totalPages) { + this.navigatePages('next'); + } else if (event.key === 'ArrowLeft' && this.currentPage > 1) { + this.navigatePages('prev'); + } + } + }; + closeEnlargedView() { this.isEnlargedView = false; this.toggleBodyScroll(false); @@ -134,4 +151,14 @@ export class PdfPreviewComponent implements OnInit { this.closeEnlargedView(); } } + + navigatePages(direction: string) { + const nextPageIndex = direction === 'next' ? this.currentPage + 1 : this.currentPage - 1; + + if (nextPageIndex > 0 && nextPageIndex <= this.totalPages) { + this.currentPage = nextPageIndex; + const canvas = this.pdfContainer.nativeElement.querySelectorAll('.pdf-page-container canvas')[this.currentPage - 1]; + this.updateEnlargedCanvas(canvas); + } + } } From bbdd4c26095429009621f913a592e3b7c1d977da Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 21 Jul 2024 15:23:25 +0200 Subject: [PATCH 011/125] Add page indicator --- .../pdf-preview/pdf-preview.component.html | 1 + .../pdf-preview/pdf-preview.component.scss | 15 +++++++++++++++ .../lecture/pdf-preview/pdf-preview.component.ts | 1 + 3 files changed, 17 insertions(+) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index bae8b323fd44..57c04868b405 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -14,6 +14,7 @@

Attachment {{ attachment.id }}: {{ attachment.name }}

@if (currentPage !== totalPages) { } +
{{ currentPage }}
}
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index 3b4f168e6124..ce5dc455c243 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -38,6 +38,7 @@ top: 10px; right: 10px; cursor: pointer; + color: white; } } @@ -72,3 +73,17 @@ right: 10px; } } + +.page-number-display { + position: absolute; + bottom: 10px; + right: calc(5% + 10px); + font-size: 18px; + color: white; + z-index: 2; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + + @media (max-width: 1200px) { + right: 10px; + } +} diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 549c91a520e5..066b95e5acfd 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -102,6 +102,7 @@ export class PdfPreviewComponent implements OnInit { this.isEnlargedView = true; this.currentPage = pageIndex; this.updateEnlargedCanvas(originalCanvas); + this.toggleBodyScroll(true); } private updateEnlargedCanvas(originalCanvas: HTMLCanvasElement) { From ebe6d04aa714642ede1a4012dc7dc7e7bd539dba Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Tue, 23 Jul 2024 17:51:51 +0200 Subject: [PATCH 012/125] Add JSDoc comments --- .../www1/artemis/web/rest/AttachmentResource.java | 14 +++++++++++--- src/main/webapp/app/lecture/attachment.service.ts | 6 ++++++ .../lecture/pdf-preview/pdf-preview.component.scss | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java index 7bed858100d9..c664b3613441 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Optional; +import org.apache.velocity.exception.ResourceNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -222,9 +223,16 @@ private String resolveFilePath(String webPath) { return Paths.get(baseDir, pathWithoutPrefix).toString(); } - @GetMapping("/attachments/{id}/file") - public ResponseEntity getAttachmentFile(@PathVariable Long id) { - Attachment attachment = attachmentRepository.findById(id).orElseThrow(); + /** + * GET courses/{id}/file : Returns the file associated with the + * given attachment ID as a downloadable resource + * + * @param attachmentId the ID of the attachment to retrieve + * @return ResponseEntity containing the file as a resource + */ + @GetMapping("/attachments/{attachmentId}/file") + public ResponseEntity getAttachmentFile(@PathVariable Long attachmentId) { + Attachment attachment = attachmentRepository.findById(attachmentId).orElseThrow(() -> new ResourceNotFoundException("Attachment not found with id: " + attachmentId)); String actualFilePath = resolveFilePath(attachment.getLink()); Resource resource = new FileSystemResource(actualFilePath); if (!resource.exists()) { diff --git a/src/main/webapp/app/lecture/attachment.service.ts b/src/main/webapp/app/lecture/attachment.service.ts index e7d0da9b3a5d..d1fd64358f56 100644 --- a/src/main/webapp/app/lecture/attachment.service.ts +++ b/src/main/webapp/app/lecture/attachment.service.ts @@ -136,6 +136,12 @@ export class AttachmentService { return formData; } + /** + * Retrieve the file associated with a given attachment ID as a Blob object + * + * @param attachmentId The ID of the attachment to retrieve + * @returns An Observable that emits the Blob object of the file when the HTTP request completes successfully + */ getAttachmentFile(attachmentId: number): Observable { return this.http.get(`${this.resourceUrl}/${attachmentId}/file`, { responseType: 'blob' }); } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index ce5dc455c243..42a8abcef94e 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -4,6 +4,7 @@ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 30px; max-height: 60vh; + height: 60vh; overflow-y: auto; border: 1px solid #ccc; padding: 10px; From 2bd8316d41cea0280c43b969a455cef281f5f370 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Tue, 23 Jul 2024 18:41:39 +0200 Subject: [PATCH 013/125] Translation & colors --- .../lecture/pdf-preview/pdf-preview.component.html | 10 ++++++---- .../lecture/pdf-preview/pdf-preview.component.scss | 12 +++++------- .../app/lecture/pdf-preview/pdf-preview.component.ts | 2 +- .../webapp/content/scss/themes/_dark-variables.scss | 5 +++++ .../content/scss/themes/_default-variables.scss | 5 +++++ src/main/webapp/i18n/de/lecture.json | 5 ++++- src/main/webapp/i18n/en/lecture.json | 5 ++++- 7 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 57c04868b405..b51e27247193 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -1,18 +1,20 @@
-

Attachment {{ attachment.id }}: {{ attachment.name }}

+

+ {{ 'artemisApp.attachment.pdfPreview.title' | artemisTranslate }} {{ attachment.id }}: {{ attachment.name }} +

@if (isEnlargedView) {
- + @if (currentPage !== 1) { - + } @if (currentPage !== totalPages) { - + }
{{ currentPage }}
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index 42a8abcef94e..58a1dddfb7ac 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -6,11 +6,11 @@ max-height: 60vh; height: 60vh; overflow-y: auto; - border: 1px solid #ccc; + border: 1px solid var(--border-color); padding: 10px; margin: 10px; width: 95%; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 5px var(--pdf-preview-pdf-container-shadow); align-items: start; @media (max-width: 800px) { @@ -31,7 +31,7 @@ display: flex; justify-content: center; align-items: center; - background-color: rgba(0, 0, 0, 0.2); + background-color: var(--pdf-preview-enlarged-container-overlay); z-index: 1050; .btn-close { @@ -39,7 +39,7 @@ top: 10px; right: 10px; cursor: pointer; - color: white; + color: var(--white); } } @@ -47,8 +47,6 @@ position: absolute; transform: translateY(-50%); cursor: pointer; - background-color: #fff; - border: 1px solid #ccc; border-radius: 50%; width: 30px; height: 30px; @@ -80,7 +78,7 @@ bottom: 10px; right: calc(5% + 10px); font-size: 18px; - color: white; + color: var(--bs-body-color); z-index: 2; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 066b95e5acfd..e98b46f57146 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -77,7 +77,7 @@ export class PdfPreviewComponent implements OnInit { private createContainer(canvas: HTMLCanvasElement, pageIndex: number): HTMLDivElement { const container = document.createElement('div'); container.classList.add('pdf-page-container'); - container.style.cssText = `position: relative; display: inline-block; width: ${canvas.style.width}; height: ${canvas.style.height}; margin: 20px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);`; + container.style.cssText = `position: relative; display: inline-block; width: ${canvas.style.width}; height: ${canvas.style.height}; margin: 20px; box-shadow: 0 2px 6px var(--pdf-preview-canvas-shadow);`; const overlay = this.createOverlay(pageIndex); container.appendChild(canvas); diff --git a/src/main/webapp/content/scss/themes/_dark-variables.scss b/src/main/webapp/content/scss/themes/_dark-variables.scss index 9c00abf470ad..8779c6b229fe 100644 --- a/src/main/webapp/content/scss/themes/_dark-variables.scss +++ b/src/main/webapp/content/scss/themes/_dark-variables.scss @@ -317,6 +317,11 @@ $stat-av-sc-legend-critical: rgba(204, 0, 0, 1); $stat-av-sc-legend-median: rgba(127, 127, 127, 255); $stat-av-sc-legend-best: rgba(40, 164, 40, 1); +// Component: pdf-preview.component.scss +$pdf-preview-pdf-container-shadow: rgba(0, 0, 0, 0.1); +$pdf-preview-canvas-shadow: rgba(255, 255, 255, 0.6); +$pdf-preview-enlarged-container-overlay: rgba(0, 0, 0, 0.8); + // Programming Exercise Update $update-programming-exercise-plan-preview-box-background: transparentize(#fff, 0.95); diff --git a/src/main/webapp/content/scss/themes/_default-variables.scss b/src/main/webapp/content/scss/themes/_default-variables.scss index 3aa0d50b56b8..368c52924335 100644 --- a/src/main/webapp/content/scss/themes/_default-variables.scss +++ b/src/main/webapp/content/scss/themes/_default-variables.scss @@ -239,6 +239,11 @@ $stat-av-sc-legend-critical: rgba(204, 0, 0, 1); $stat-av-sc-legend-median: rgba(127, 127, 127, 255); $stat-av-sc-legend-best: rgba(40, 164, 40, 1); +// Component: pdf-preview.component.scss +$pdf-preview-pdf-container-shadow: rgba(0, 0, 0, 0.1); +$pdf-preview-canvas-shadow: rgba(0, 0, 0, 0.1); +$pdf-preview-enlarged-container-overlay: rgba(0, 0, 0, 0.2); + // Programming Exercise Update $update-programming-exercise-plan-preview-box-background: transparentize(black, 0.95); diff --git a/src/main/webapp/i18n/de/lecture.json b/src/main/webapp/i18n/de/lecture.json index 61e68defd8e9..78213ace4199 100644 --- a/src/main/webapp/i18n/de/lecture.json +++ b/src/main/webapp/i18n/de/lecture.json @@ -96,7 +96,10 @@ "deleteQuestion": "Soll der Anhang wirklich gelöscht werden?", "created": "Anhang erstellt mit ID {{ param }}", "updated": "Anhang aktualisiert mit ID {{ param }}", - "deleted": "Anhang gelöscht mit ID {{ param }}" + "deleted": "Anhang gelöscht mit ID {{ param }}", + "pdfPreview": { + "title": "Anhang" + } } } } diff --git a/src/main/webapp/i18n/en/lecture.json b/src/main/webapp/i18n/en/lecture.json index bb8ddb7ad737..547f75faf419 100644 --- a/src/main/webapp/i18n/en/lecture.json +++ b/src/main/webapp/i18n/en/lecture.json @@ -96,7 +96,10 @@ "deleteQuestion": "Do you really want to delete the attachment?", "created": "Created new attachment with identifier {{ param }}", "updated": "Updated attachment with identifier {{ param }}", - "deleted": "Deleted attachment with identifier {{ param }}" + "deleted": "Deleted attachment with identifier {{ param }}", + "pdfPreview": { + "title": "Attachment" + } } } } From 48018aa1ec44688667203864ceebcb1024cca8d4 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Wed, 24 Jul 2024 13:44:05 +0200 Subject: [PATCH 014/125] Install pdf.js --- package-lock.json | 490 +++++++++++++++++- package.json | 5 +- .../pdf-preview/pdf-preview.component.html | 2 +- 3 files changed, 476 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0b0ad03c7d2a..69c0c72be59d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", + "pdfjs-dist": "^4.4.168", "posthog-js": "1.148.0", "rxjs": "7.8.1", "showdown": "2.1.0", @@ -4887,6 +4888,146 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@material/animation": { "version": "15.0.0-canary.7f224ddd4.0", "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.7f224ddd4.0.tgz", @@ -8216,6 +8357,40 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -8584,7 +8759,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "devOptional": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -8948,6 +9123,21 @@ } ] }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -9003,7 +9193,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10" } @@ -9295,6 +9485,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -9367,7 +9566,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "devOptional": true }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", @@ -9378,6 +9577,12 @@ "node": ">=0.8" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -10204,6 +10409,18 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -10323,6 +10540,12 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -10355,7 +10578,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -11916,7 +12139,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "devOptional": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -11940,6 +12163,62 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -12185,6 +12464,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -12658,7 +12943,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, + "devOptional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -16140,6 +16425,18 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mini-css-extract-plugin": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", @@ -16322,7 +16619,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, + "devOptional": true, "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -16335,7 +16632,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -16347,13 +16644,13 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "devOptional": true }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, + "devOptional": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -16462,6 +16759,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -16598,6 +16901,48 @@ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -16849,6 +17194,19 @@ "node": ">=8" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -17213,7 +17571,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, + "devOptional": true, "dependencies": { "wrappy": "1" } @@ -17645,7 +18003,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -17702,6 +18060,27 @@ "node": ">=8" } }, + "node_modules/path2d": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.1.tgz", + "integrity": "sha512-Fl2z/BHvkTNvkuBzYTpTuirHZg6wW9z8+4SND/3mDTEcYbbNKWAy21dz9D3ePNNwrrK8pqZO5vLPZ1hLF6T7XA==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pdfjs-dist": { + "version": "4.4.168", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.4.168.tgz", + "integrity": "sha512-MbkAjpwka/dMHaCfQ75RY1FXX3IewBVu6NGZOcxerRFlaBiIkZmUoR0jotX5VUzYZEXAGzSFtknWs5xRKliXPA==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d": "^0.2.0" + } + }, "node_modules/pepjs": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/pepjs/-/pepjs-0.5.3.tgz", @@ -19099,6 +19478,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -19278,6 +19663,37 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-statistics": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.3.tgz", @@ -19879,7 +20295,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, + "devOptional": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -19926,7 +20342,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, + "devOptional": true, "dependencies": { "minipass": "^3.0.0" }, @@ -19938,7 +20354,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -19950,7 +20366,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -19959,7 +20375,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "devOptional": true }, "node_modules/terser": { "version": "5.29.2", @@ -21368,6 +21784,44 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "node_modules/wide-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -21543,7 +21997,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "devOptional": true }, "node_modules/write-file-atomic": { "version": "4.0.2", diff --git a/package.json b/package.json index eaf9d47084cd..0b1e98a3b5ba 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "@angular/forms": "18.1.1", "@angular/localize": "18.1.1", "@angular/material": "18.1.1", - "@angular/platform-browser-dynamic": "18.1.1", "@angular/platform-browser": "18.1.1", + "@angular/platform-browser-dynamic": "18.1.1", "@angular/router": "18.1.1", "@angular/service-worker": "18.1.1", "@ctrl/ngx-emoji-mart": "9.2.0", @@ -49,9 +49,9 @@ "crypto-js": "4.2.0", "dayjs": "1.11.12", "diff-match-patch-typescript": "1.0.8", - "fast-json-patch": "3.1.1", "dompurify": "3.1.6", "export-to-csv": "1.3.0", + "fast-json-patch": "3.1.1", "franc-min": "6.2.0", "html-diff-ts": "1.4.2", "interactjs": "1.10.27", @@ -64,6 +64,7 @@ "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", + "pdfjs-dist": "^4.4.168", "posthog-js": "1.148.0", "rxjs": "7.8.1", "showdown": "2.1.0", diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index b51e27247193..2e51869dbedd 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -2,7 +2,7 @@

- {{ 'artemisApp.attachment.pdfPreview.title' | artemisTranslate }} {{ attachment.id }}: {{ attachment.name }} + {{ attachment.id }}: {{ attachment.name }}

From 0de1150dd22cdb52802fa4f1a10f8ff76aea41d5 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Wed, 24 Jul 2024 13:58:03 +0200 Subject: [PATCH 015/125] Fix build error --- .../pdf-preview/pdf-preview.component.spec.ts | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.spec.ts diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.spec.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.spec.ts deleted file mode 100644 index 4e61db3aa89a..000000000000 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { PdfPreviewComponent } from './pdf-preview.component'; - -describe('PdfPreviewComponent', () => { - let component: PdfPreviewComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [PdfPreviewComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(PdfPreviewComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); From c9d9c29c3f3c1224fd3f13bf79f6909f16f0abc8 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Wed, 24 Jul 2024 16:24:37 +0200 Subject: [PATCH 016/125] Change file path --- .../www1/artemis/web/rest/AttachmentResource.java | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java index c664b3613441..da5799e6a3ba 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java @@ -6,7 +6,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; import java.util.Optional; @@ -216,13 +215,6 @@ else if (attachment.getExercise() != null) { return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, attachmentId.toString())).build(); } - private String resolveFilePath(String webPath) { - String baseDir = "uploads"; - String prefixToRemove = "/api/files/"; - String pathWithoutPrefix = webPath.replace(prefixToRemove, ""); - return Paths.get(baseDir, pathWithoutPrefix).toString(); - } - /** * GET courses/{id}/file : Returns the file associated with the * given attachment ID as a downloadable resource @@ -233,10 +225,10 @@ private String resolveFilePath(String webPath) { @GetMapping("/attachments/{attachmentId}/file") public ResponseEntity getAttachmentFile(@PathVariable Long attachmentId) { Attachment attachment = attachmentRepository.findById(attachmentId).orElseThrow(() -> new ResourceNotFoundException("Attachment not found with id: " + attachmentId)); - String actualFilePath = resolveFilePath(attachment.getLink()); - Resource resource = new FileSystemResource(actualFilePath); + String filePath = attachment.getLink(); + Resource resource = new FileSystemResource(filePath); if (!resource.exists()) { - throw new RuntimeException("File not found " + actualFilePath); + throw new RuntimeException("File not found " + filePath); } String contentType = "application/pdf"; From 9f612e7505cbbc6f894cbb707db2ffca0c7ae1ff Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Wed, 24 Jul 2024 17:53:07 +0200 Subject: [PATCH 017/125] Update getAttachmentFile method --- .../artemis/web/rest/AttachmentResource.java | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java index da5799e6a3ba..9967a510ee29 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java @@ -2,21 +2,26 @@ import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; import static de.tum.in.www1.artemis.service.FilePathService.actualPathForPublicPath; +import static de.tum.in.www1.artemis.service.FilePathService.getLectureAttachmentFilePath; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.util.List; import java.util.Optional; -import org.apache.velocity.exception.ResourceNotFoundException; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.Resource; +import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -45,8 +50,10 @@ import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; import de.tum.in.www1.artemis.web.rest.util.HeaderUtil; +import org.springframework.web.server.ResponseStatusException; import tech.jhipster.web.util.ResponseUtil; + /** * REST controller for managing Attachment. */ @@ -95,7 +102,7 @@ public ResponseEntity createAttachment(@RequestPart Attachment attac log.debug("REST request to save Attachment : {}", attachment); attachment.setId(null); - Path basePath = FilePathService.getLectureAttachmentFilePath().resolve(attachment.getLecture().getId().toString()); + Path basePath = getLectureAttachmentFilePath().resolve(attachment.getLecture().getId().toString()); Path savePath = fileService.saveFile(file, basePath, false); attachment.setLink(FilePathService.publicPathForActualPath(savePath, attachment.getLecture().getId()).toString()); @@ -126,7 +133,7 @@ public ResponseEntity updateAttachment(@PathVariable Long attachment attachment.setAttachmentUnit(originalAttachment.getAttachmentUnit()); if (file != null) { - Path basePath = FilePathService.getLectureAttachmentFilePath().resolve(originalAttachment.getLecture().getId().toString()); + Path basePath = getLectureAttachmentFilePath().resolve(originalAttachment.getLecture().getId().toString()); Path savePath = fileService.saveFile(file, basePath, false); attachment.setLink(FilePathService.publicPathForActualPath(savePath, originalAttachment.getLecture().getId()).toString()); // Delete the old file @@ -215,6 +222,7 @@ else if (attachment.getExercise() != null) { return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, attachmentId.toString())).build(); } + /** * GET courses/{id}/file : Returns the file associated with the * given attachment ID as a downloadable resource @@ -223,16 +231,26 @@ else if (attachment.getExercise() != null) { * @return ResponseEntity containing the file as a resource */ @GetMapping("/attachments/{attachmentId}/file") - public ResponseEntity getAttachmentFile(@PathVariable Long attachmentId) { - Attachment attachment = attachmentRepository.findById(attachmentId).orElseThrow(() -> new ResourceNotFoundException("Attachment not found with id: " + attachmentId)); - String filePath = attachment.getLink(); - Resource resource = new FileSystemResource(filePath); - if (!resource.exists()) { - throw new RuntimeException("File not found " + filePath); - } + public ResponseEntity getAttachmentFile(@PathVariable Long attachmentId) throws IOException { + Attachment attachment = attachmentRepository.findById(attachmentId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Attachment not found with id: " + attachmentId)); - String contentType = "application/pdf"; - return ResponseEntity.ok().contentType(MediaType.parseMediaType(contentType)) - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"").body(resource); + Path filePath = actualPathForPublicPath(URI.create(attachment.getLink())); + byte[] fileBytes = fileService.getFileForPath(filePath); + + try (PDDocument document = Loader.loadPDF(fileBytes)) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + document.save(out); + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + attachment.getName() + "\""); + headers.setContentType(MediaType.APPLICATION_PDF); + + return ResponseEntity.ok() + .headers(headers) + .contentLength(out.size()) + .body(new InputStreamResource(new ByteArrayInputStream(out.toByteArray()))); + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error processing PDF file " + filePath, e); + } } } From 2def08cdc2c40c79ff1ab332994e3dc1dfa12092 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Wed, 24 Jul 2024 23:38:06 +0200 Subject: [PATCH 018/125] Update PDF Preview button & add it to Lecture Units --- .../artemis/web/rest/AttachmentResource.java | 41 ------------------ .../www1/artemis/web/rest/FileResource.java | 42 ++++++++++++++++++- .../lecture-attachments.component.html | 9 ++-- .../lecture/lecture-attachments.component.ts | 3 +- .../attachmentUnit.service.ts | 10 +++++ .../lecture-unit-management.component.html | 10 +++++ .../lecture-unit-management.component.ts | 12 +++++- .../lecture-unit-management.route.ts | 34 ++++++++++++++- .../pdf-preview/pdf-preview.component.html | 9 +++- .../pdf-preview/pdf-preview.component.ts | 29 ++++++++++--- 10 files changed, 144 insertions(+), 55 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java index 9967a510ee29..c3558b2d0615 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java @@ -4,24 +4,16 @@ import static de.tum.in.www1.artemis.service.FilePathService.actualPathForPublicPath; import static de.tum.in.www1.artemis.service.FilePathService.getLectureAttachmentFilePath; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.util.List; import java.util.Optional; -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; -import org.springframework.core.io.InputStreamResource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -50,7 +42,6 @@ import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; import de.tum.in.www1.artemis.web.rest.util.HeaderUtil; -import org.springframework.web.server.ResponseStatusException; import tech.jhipster.web.util.ResponseUtil; @@ -221,36 +212,4 @@ else if (attachment.getExercise() != null) { } return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, attachmentId.toString())).build(); } - - - /** - * GET courses/{id}/file : Returns the file associated with the - * given attachment ID as a downloadable resource - * - * @param attachmentId the ID of the attachment to retrieve - * @return ResponseEntity containing the file as a resource - */ - @GetMapping("/attachments/{attachmentId}/file") - public ResponseEntity getAttachmentFile(@PathVariable Long attachmentId) throws IOException { - Attachment attachment = attachmentRepository.findById(attachmentId) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Attachment not found with id: " + attachmentId)); - - Path filePath = actualPathForPublicPath(URI.create(attachment.getLink())); - byte[] fileBytes = fileService.getFileForPath(filePath); - - try (PDDocument document = Loader.loadPDF(fileBytes)) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - document.save(out); - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + attachment.getName() + "\""); - headers.setContentType(MediaType.APPLICATION_PDF); - - return ResponseEntity.ok() - .headers(headers) - .contentLength(out.size()) - .body(new InputStreamResource(new ByteArrayInputStream(out.toByteArray()))); - } catch (Exception e) { - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error processing PDF file " + filePath, e); - } - } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java index ad959b26096f..7a7d3a3ee378 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java @@ -76,6 +76,7 @@ import de.tum.in.www1.artemis.service.ResourceLoaderService; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; +import org.springframework.web.server.ResponseStatusException; /** * REST controller for managing Files. @@ -372,6 +373,28 @@ public ResponseEntity getLectureAttachment(@PathVariable Long lectureId, return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false); } + /** + * GET courses/{id}/file : Returns the file associated with the + * given attachment ID as a downloadable resource + * + * @param attachmentId the ID of the attachment to retrieve + * @return ResponseEntity containing the file as a resource + */ + @GetMapping("/attachments/{attachmentId}/file") + public ResponseEntity getLectureAttachmentById(@PathVariable Long attachmentId) { + Attachment attachment = attachmentRepository.findById(attachmentId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Attachment not found with id: " + attachmentId)); + + // get the course for a lecture attachment + Lecture lecture = attachment.getLecture(); + Course course = lecture.getCourse(); + + // check if the user is authorized to access the requested attachment unit + checkAttachmentAuthorizationOrThrow(course, attachment); + + return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false); + } + /** * GET /files/attachments/lecture/{lectureId}/merge-pdf : Get the lecture units * PDF attachments merged @@ -414,7 +437,7 @@ public ResponseEntity getLecturePdfAttachmentsMerged(@PathVariable Long */ @GetMapping("files/attachments/attachment-unit/{attachmentUnitId}/*") @EnforceAtLeastStudent - public ResponseEntity getAttachmentUnitAttachment(@PathVariable Long attachmentUnitId) { + public ResponseEntity getLectureUnitAttachment(@PathVariable Long attachmentUnitId) { log.debug("REST request to get file for attachment unit : {}", attachmentUnitId); AttachmentUnit attachmentUnit = attachmentUnitRepository.findByIdElseThrow(attachmentUnitId); @@ -428,6 +451,23 @@ public ResponseEntity getAttachmentUnitAttachment(@PathVariable Long att return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false); } + @GetMapping("files/attachments/attachment-units/{attachmentUnitId}/file") + @EnforceAtLeastStudent + public ResponseEntity getLectureUnitFile(@PathVariable Long attachmentUnitId) { + log.debug("REST request to get file for attachment unit : {}", attachmentUnitId); + AttachmentUnit attachmentUnit = attachmentUnitRepository.findById(attachmentUnitId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Attachment unit not found with id: " + attachmentUnitId)); + + Attachment attachment = attachmentUnit.getAttachment(); + Course course = attachmentUnit.getLecture().getCourse(); + + // Check authorization + checkAttachmentAuthorizationOrThrow(course, attachment); + + // Build and return file response + return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false); + } + /** * GET files/attachments/slides/attachment-unit/:attachmentUnitId/slide/:slideNumber : Get the lecture unit attachment slide by slide number * diff --git a/src/main/webapp/app/lecture/lecture-attachments.component.html b/src/main/webapp/app/lecture/lecture-attachments.component.html index 9835422c5c28..f4dc2555fcb8 100644 --- a/src/main/webapp/app/lecture/lecture-attachments.component.html +++ b/src/main/webapp/app/lecture/lecture-attachments.component.html @@ -80,12 +80,13 @@

+ @if (viewButtonAvailable(attachment)) { + + } + @if (currentPage !== 1) { - + } @if (currentPage !== totalPages) { - + }
{{ currentPage }}
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index 58a1dddfb7ac..238ecefcf471 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -12,6 +12,7 @@ width: 95%; box-shadow: 0 2px 5px var(--pdf-preview-pdf-container-shadow); align-items: start; + z-index: 0; @media (max-width: 800px) { grid-template-columns: repeat(2, 1fr); @@ -32,14 +33,14 @@ justify-content: center; align-items: center; background-color: var(--pdf-preview-enlarged-container-overlay); - z-index: 1050; + z-index: 2; .btn-close { position: absolute; top: 10px; right: 10px; cursor: pointer; - color: var(--white); + color: var(--bs-body-color); } } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 06d230939c0c..56c1c173f5cb 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { AttachmentService } from 'app/lecture/attachment.service'; import * as PDFJS from 'pdfjs-dist'; @@ -8,6 +8,12 @@ import { AttachmentUnit } from 'app/entities/lecture-unit/attachmentUnit.model'; import { AttachmentUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service'; import { onError } from 'app/shared/util/global.utils'; import { AlertService } from 'app/core/util/alert.service'; +import { Subscription } from 'rxjs'; + +enum NavigationDirection { + Next = 'next', + Previous = 'prev', +} @Component({ selector: 'jhi-pdf-preview-component', @@ -15,13 +21,17 @@ import { AlertService } from 'app/core/util/alert.service'; styleUrls: ['./pdf-preview.component.scss'], }) export class PdfPreviewComponent implements OnInit, OnDestroy { - @ViewChild('pdfContainer', { static: true }) pdfContainer: ElementRef; - @ViewChild('enlargedCanvas') enlargedCanvas: ElementRef; + @ViewChild('pdfContainer', { static: true }) pdfContainer: ElementRef; + @ViewChild('enlargedCanvas') enlargedCanvas: ElementRef; attachment?: Attachment; attachmentUnit?: AttachmentUnit; isEnlargedView: boolean = false; currentPage: number = 1; totalPages: number = 0; + nextDirection = NavigationDirection.Next; + prevDirection = NavigationDirection.Previous; + attachmentSub: Subscription; + attachmentUnitSub: Subscription; constructor( private route: ActivatedRoute, @@ -35,7 +45,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { if ('attachment' in data) { this.attachment = data.attachment; if (this.attachment?.id) { - this.attachmentService.getAttachmentFile(this.attachment.id).subscribe({ + this.attachmentSub = this.attachmentService.getAttachmentFile(this.attachment.id).subscribe({ next: (blob: Blob) => this.loadPdf(URL.createObjectURL(blob)), error: (error) => onError(this.alertService, error), }); @@ -45,7 +55,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } else if ('attachmentUnit' in data) { this.attachmentUnit = data.attachmentUnit; if (this.attachmentUnit?.id) { - this.attachmentUnitService.getAttachmentFile(this.attachmentUnit.id).subscribe({ + this.attachmentUnitSub = this.attachmentUnitService.getAttachmentFile(this.attachmentUnit.id).subscribe({ next: (blob: Blob) => this.loadPdf(URL.createObjectURL(blob)), error: (error) => onError(this.alertService, error), }); @@ -54,15 +64,40 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } } }); - document.addEventListener('keydown', this.handleKeyboardEvents); - window.addEventListener('resize', this.resizeCanvasBasedOnContainer); } ngOnDestroy() { - document.removeEventListener('keydown', this.handleKeyboardEvents); - window.removeEventListener('resize', this.resizeCanvasBasedOnContainer); + this.attachmentSub?.unsubscribe(); + this.attachmentUnitSub?.unsubscribe(); + } + + /** + * Handles keyboard events for navigation within the PDF viewer. + * @param event The keyboard event captured. + */ + @HostListener('document:keydown', ['$event']) + handleKeyboardEvents(event: KeyboardEvent) { + if (this.isEnlargedView) { + if (event.key === 'ArrowRight' && this.currentPage < this.totalPages) { + this.navigatePages(NavigationDirection.Next); + } else if (event.key === 'ArrowLeft' && this.currentPage > 1) { + this.navigatePages(NavigationDirection.Previous); + } + } + } + + /** + * Adjusts the canvas size based on the window resize event to ensure proper display. + */ + @HostListener('window:resize') + resizeCanvasBasedOnContainer() { + this.adjustCanvasSize(); } + /** + * Loads a PDF from a provided URL and initializes viewer setup. + * @param fileUrl The URL of the file to load. + */ private async loadPdf(fileUrl: string) { try { const loadingTask = PDFJS.getDocument(fileUrl); @@ -88,6 +123,11 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } } + /** + * Creates a canvas for each page of the PDF to allow for individual page rendering. + * @param viewport The viewport settings used for rendering the page. + * @returns A new HTMLCanvasElement configured for the PDF page. + */ private createCanvas(viewport: PDFJS.PageViewport): HTMLCanvasElement { const canvas = document.createElement('canvas'); canvas.height = viewport.height; @@ -99,6 +139,12 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { return canvas; } + /** + * Creates a container div for each canvas, facilitating layering and interaction. + * @param canvas The canvas element that displays a PDF page. + * @param pageIndex The index of the page within the PDF document. + * @returns A configured div element that includes the canvas and interactive overlays. + */ private createContainer(canvas: HTMLCanvasElement, pageIndex: number): HTMLDivElement { const container = document.createElement('div'); container.classList.add('pdf-page-container'); @@ -115,20 +161,36 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { return container; } + /** + * Generates an interactive overlay for each PDF page to allow for user interactions. + * @param pageIndex The index of the page. + * @returns A div element styled as an overlay. + */ private createOverlay(pageIndex: number): HTMLDivElement { const overlay = document.createElement('div'); - overlay.classList.add('pdf-page-overlay'); overlay.innerHTML = `${pageIndex}`; overlay.style.cssText = `position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; font-size: 24px; color: white; background-color: rgba(0, 0, 0, 0.4); z-index: 1; transition: opacity 0.3s ease; opacity: 0; cursor: pointer;`; return overlay; } - private resizeCanvasBasedOnContainer = () => { - if (this.isEnlargedView && this.enlargedCanvas) { - this.updateEnlargedCanvas(this.pdfContainer.nativeElement.querySelectorAll('.pdf-page-container canvas')[this.currentPage - 1]); + /** + * Dynamically updates the canvas size within an enlarged view based on the viewport. + */ + private adjustCanvasSize = () => { + if (this.isEnlargedView) { + const canvasElements = this.pdfContainer.nativeElement.querySelectorAll('.pdf-page-container canvas'); + if (this.currentPage - 1 < canvasElements.length) { + const canvas = canvasElements[this.currentPage - 1] as HTMLCanvasElement; + this.updateEnlargedCanvas(canvas); + } } }; + /** + * Displays a canvas in an enlarged view for detailed examination. + * @param originalCanvas The original canvas element displaying the page. + * @param pageIndex The index of the page being displayed. + */ displayEnlargedCanvas(originalCanvas: HTMLCanvasElement, pageIndex: number) { this.isEnlargedView = true; this.currentPage = pageIndex; @@ -136,11 +198,19 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.toggleBodyScroll(true); } + /** + * Updates and resizes the enlarged canvas based on the current container dimensions. + * This method is designed to ensure that the canvas displays the PDF page optimally within the available space, + * maintaining the aspect ratio and centering the canvas in the viewport. + * + * @param {HTMLCanvasElement} originalCanvas The original canvas element from which the image data is sourced. + */ private updateEnlargedCanvas(originalCanvas: HTMLCanvasElement) { requestAnimationFrame(() => { if (this.isEnlargedView) { const enlargedCanvas = this.enlargedCanvas.nativeElement; const context = enlargedCanvas.getContext('2d'); + if (!context) return; const containerWidth = this.pdfContainer.nativeElement.clientWidth; const containerHeight = this.pdfContainer.nativeElement.clientHeight; @@ -155,7 +225,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { context.clearRect(0, 0, enlargedCanvas.width, enlargedCanvas.height); context.drawImage(originalCanvas, 0, 0, enlargedCanvas.width, enlargedCanvas.height); - enlargedCanvas.parentElement.style.top = `${this.pdfContainer.nativeElement.scrollTop}px`; + enlargedCanvas.parentElement!.style.top = `${this.pdfContainer.nativeElement.scrollTop}px`; enlargedCanvas.style.position = 'absolute'; enlargedCanvas.style.left = `${(containerWidth - enlargedCanvas.width) / 2}px`; enlargedCanvas.style.top = `${(containerHeight - enlargedCanvas.height) / 2}px`; @@ -163,40 +233,55 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { }); } - handleKeyboardEvents = (event: KeyboardEvent) => { - if (this.isEnlargedView) { - if (event.key === 'ArrowRight' && this.currentPage < this.totalPages) { - this.navigatePages('next'); - } else if (event.key === 'ArrowLeft' && this.currentPage > 1) { - this.navigatePages('prev'); - } - } - }; - - closeEnlargedView() { + /** + * Closes the enlarged view of the PDF and re-enables scrolling in the PDF container. + */ + closeEnlargedView(event: MouseEvent) { this.isEnlargedView = false; this.toggleBodyScroll(false); + event.stopPropagation(); } + /** + * Toggles the ability to scroll through the PDF container. + * @param disable A boolean flag indicating whether scrolling should be disabled (`true`) or enabled (`false`). + */ toggleBodyScroll(disable: boolean): void { this.pdfContainer.nativeElement.style.overflow = disable ? 'hidden' : 'auto'; } + /** + * Closes the enlarged view if a click event occurs outside the actual canvas area but within the enlarged container. + * @param event The mouse event captured, used to determine the location of the click. + */ closeIfOutside(event: MouseEvent): void { const target = event.target as HTMLElement; const enlargedCanvas = this.enlargedCanvas.nativeElement; if (target.classList.contains('enlarged-container') && target !== enlargedCanvas) { - this.closeEnlargedView(); + this.closeEnlargedView(event); } } - navigatePages(direction: string) { - const nextPageIndex = direction === 'next' ? this.currentPage + 1 : this.currentPage - 1; + /** + * Handles navigation between PDF pages and stops event propagation to prevent unwanted side effects. + * @param direction The direction to navigate. + * @param event The MouseEvent to be stopped. + */ + handleNavigation(direction: NavigationDirection, event: MouseEvent): void { + event.stopPropagation(); + this.navigatePages(direction); + } + /** + * Navigates to a specific page in the PDF based on the direction relative to the current page. + * @param direction The navigation direction (next or previous). + */ + navigatePages(direction: NavigationDirection) { + const nextPageIndex = direction === NavigationDirection.Next ? this.currentPage + 1 : this.currentPage - 1; if (nextPageIndex > 0 && nextPageIndex <= this.totalPages) { this.currentPage = nextPageIndex; - const canvas = this.pdfContainer.nativeElement.querySelectorAll('.pdf-page-container canvas')[this.currentPage - 1]; + const canvas = this.pdfContainer.nativeElement.querySelectorAll('.pdf-page-container canvas')[this.currentPage - 1] as HTMLCanvasElement; this.updateEnlargedCanvas(canvas); } } diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index e57aa58407c2..8aafa0f553c0 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -45,9 +45,6 @@ describe('PdfPreviewComponent', () => { attachmentUnitServiceMock = { getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), }; - alertServiceMock = { - error: jest.fn(), - }; routeMock = { data: of({ attachment: { id: 1, name: 'Example PDF' }, @@ -76,25 +73,29 @@ describe('PdfPreviewComponent', () => { fixture.detectChanges(); }); - it('should load PDF', async () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should load PDF and handle successful response', () => { component.ngOnInit(); expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalledWith(1); expect(alertServiceMock.error).not.toHaveBeenCalled(); }); - it('should display error alert when an invalid attachment ID is provided', async () => { + it('should display error alert when an invalid attachment ID is provided', () => { routeMock.data = of({ attachment: { id: null, name: 'Invalid PDF' } }); component.ngOnInit(); expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentIDError'); }); - it('should display error alert when an invalid attachmentUnit ID is provided', async () => { + it('should display error alert when an invalid attachmentUnit ID is provided', () => { routeMock.data = of({ attachmentUnit: { id: null, name: 'Invalid PDF' } }); component.ngOnInit(); expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUnitIDError'); }); - it('should load and render PDF pages', async () => { + it('should load and render PDF pages', () => { const mockBlob = new Blob(['PDF content'], { type: 'application/pdf' }); attachmentServiceMock.getAttachmentFile.mockReturnValue(of(mockBlob)); @@ -125,7 +126,11 @@ describe('PdfPreviewComponent', () => { component.displayEnlargedCanvas(mockCanvas, 1); expect(component.isEnlargedView).toBeTrue(); - component.closeEnlargedView(); + const clickEvent = new MouseEvent('click', { + button: 0, + }); + + component.closeEnlargedView(clickEvent); expect(component.isEnlargedView).toBeFalse(); }); From a2f8f88a154faa3360b0a94253ea6d1eb0cabef9 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 11 Aug 2024 10:42:34 +0200 Subject: [PATCH 045/125] Fix client tests --- .../lecture-unit-management.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/lecture-unit-management.component.ts b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/lecture-unit-management.component.ts index 6a1bc8a4d491..be94387d69ea 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/lecture-unit-management.component.ts +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/lecture-unit-management.component.ts @@ -191,7 +191,7 @@ export class LectureUnitManagementComponent implements OnInit, OnDestroy { switch (lectureUnit!.type) { case LectureUnitType.ATTACHMENT: const attachmentUnit = lectureUnit; - return attachmentUnit.attachment!.link!.endsWith('.pdf'); + return attachmentUnit.attachment?.link?.endsWith('.pdf'); default: return false; } From 984bcdff31afd8c62d952a2e9985eea47a0a8b7b Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Wed, 14 Aug 2024 20:20:42 +0300 Subject: [PATCH 046/125] Resolved changes 2 --- .../www1/artemis/web/rest/FileResource.java | 27 +++++------ .../rest/errors/NotFoundAlertException.java | 13 ++++++ .../webapp/app/lecture/attachment.service.ts | 5 ++- .../lecture-attachments.component.html | 2 +- .../lecture/lecture-attachments.component.ts | 12 +++-- .../attachmentUnit.service.ts | 5 ++- .../lecture-unit-management.component.html | 2 +- .../lecture-unit-management.component.ts | 10 ++++- .../lecture-unit-management.route.ts | 2 + src/main/webapp/app/lecture/lecture.route.ts | 1 + .../pdf-preview/pdf-preview.component.scss | 4 ++ .../pdf-preview/pdf-preview.component.ts | 45 +++++++++++-------- .../content/scss/themes/_dark-variables.scss | 1 + .../scss/themes/_default-variables.scss | 1 + 14 files changed, 87 insertions(+), 43 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/errors/NotFoundAlertException.java diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java index d0e3024ceb75..c40bd081e7ea 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java @@ -38,7 +38,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.server.ResponseStatusException; import de.tum.in.www1.artemis.domain.Attachment; import de.tum.in.www1.artemis.domain.Course; @@ -70,6 +69,7 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; +import de.tum.in.www1.artemis.security.annotations.enforceRoleInCourse.EnforceAtLeastEditorInCourse; import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.FileService; @@ -77,6 +77,7 @@ import de.tum.in.www1.artemis.service.ResourceLoaderService; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; +import de.tum.in.www1.artemis.web.rest.errors.NotFoundAlertException; /** * REST controller for managing Files. @@ -374,20 +375,20 @@ public ResponseEntity getLectureAttachment(@PathVariable Long lectureId, } /** - * GET /attachments/{attachmentId}/file : Returns the file associated with the + * GET /files/courses/{courseId}/attachments/{attachmentId} : Returns the file associated with the * given attachment ID as a downloadable resource * + * @param courseId The ID of the course that the Attachment belongs to * @param attachmentId the ID of the attachment to retrieve * @return ResponseEntity containing the file as a resource */ - @GetMapping("attachments/{attachmentId}/file") - @EnforceAtLeastInstructor - public ResponseEntity getAttachmentFile(@PathVariable Long attachmentId) { + @GetMapping("files/courses/{courseId}/attachments/{attachmentId}") + @EnforceAtLeastEditorInCourse + public ResponseEntity getAttachmentFile(@PathVariable Long courseId, @PathVariable Long attachmentId) { Attachment attachment = attachmentRepository.findById(attachmentId) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Attachment not found with id: " + attachmentId)); + .orElseThrow(() -> new NotFoundAlertException(HttpStatus.NOT_FOUND, "artemisApp.attachment.pdfPreview.attachmentIDError")); - Lecture lecture = attachment.getLecture(); - Course course = lecture.getCourse(); + Course course = courseRepository.findByIdElseThrow(courseId); checkAttachmentAuthorizationOrThrow(course, attachment); @@ -457,15 +458,15 @@ public ResponseEntity getAttachmentUnitAttachment(@PathVariable Long att * @param attachmentUnitId the ID of the attachment to retrieve * @return ResponseEntity containing the file as a resource */ - @GetMapping("files/attachments/attachment-units/{attachmentUnitId}/file") - @EnforceAtLeastInstructor - public ResponseEntity getAttachmentUnitFile(@PathVariable Long attachmentUnitId) { + @GetMapping("files/courses/{courseId}/attachments/attachment-units/{attachmentUnitId}") + @EnforceAtLeastEditorInCourse + public ResponseEntity getAttachmentUnitFile(@PathVariable Long courseId, @PathVariable Long attachmentUnitId) { log.debug("REST request to get file for attachment unit : {}", attachmentUnitId); AttachmentUnit attachmentUnit = attachmentUnitRepository.findById(attachmentUnitId) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Attachment unit not found with id: " + attachmentUnitId)); + .orElseThrow(() -> new NotFoundAlertException(HttpStatus.NOT_FOUND, "Attachment unit not found with id: " + attachmentUnitId)); Attachment attachment = attachmentUnit.getAttachment(); - Course course = attachmentUnit.getLecture().getCourse(); + Course course = courseRepository.findByIdElseThrow(courseId); checkAttachmentAuthorizationOrThrow(course, attachment); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/errors/NotFoundAlertException.java b/src/main/java/de/tum/in/www1/artemis/web/rest/errors/NotFoundAlertException.java new file mode 100644 index 000000000000..f436dbda5f3a --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/errors/NotFoundAlertException.java @@ -0,0 +1,13 @@ +package de.tum.in.www1.artemis.web.rest.errors; + +import org.springframework.http.HttpStatusCode; +import org.springframework.lang.Nullable; +import org.springframework.web.server.ResponseStatusException; + +public class NotFoundAlertException extends ResponseStatusException { + + public NotFoundAlertException(HttpStatusCode status, @Nullable String reason) { + super(status, reason); + } + +} diff --git a/src/main/webapp/app/lecture/attachment.service.ts b/src/main/webapp/app/lecture/attachment.service.ts index d1fd64358f56..49f6739bfd8d 100644 --- a/src/main/webapp/app/lecture/attachment.service.ts +++ b/src/main/webapp/app/lecture/attachment.service.ts @@ -139,10 +139,11 @@ export class AttachmentService { /** * Retrieve the file associated with a given attachment ID as a Blob object * + * @param courseId The ID of the course that the attachment belongs to * @param attachmentId The ID of the attachment to retrieve * @returns An Observable that emits the Blob object of the file when the HTTP request completes successfully */ - getAttachmentFile(attachmentId: number): Observable { - return this.http.get(`${this.resourceUrl}/${attachmentId}/file`, { responseType: 'blob' }); + getAttachmentFile(courseId: number, attachmentId: number): Observable { + return this.http.get(`api/files/courses/${courseId}/attachments/${attachmentId}`, { responseType: 'blob' }); } } diff --git a/src/main/webapp/app/lecture/lecture-attachments.component.html b/src/main/webapp/app/lecture/lecture-attachments.component.html index 5756682673fe..a56efadbc2d9 100644 --- a/src/main/webapp/app/lecture/lecture-attachments.component.html +++ b/src/main/webapp/app/lecture/lecture-attachments.component.html @@ -79,7 +79,7 @@

- @if (viewButtonAvailable(attachment)) { + @if (viewButtonAvailable.get(attachment.id!)) { @if (currentPage !== 1) { - + } @if (currentPage !== totalPages) { - + }
{{ currentPage }}
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index e24491c5f469..a66aa6064229 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -11,10 +11,7 @@ import { AlertService } from 'app/core/util/alert.service'; import { Subscription } from 'rxjs'; import { Course } from 'app/entities/course.model'; -enum NavigationDirection { - Next = 'next', - Previous = 'prev', -} +type NavigationDirection = 'next' | 'prev'; @Component({ selector: 'jhi-pdf-preview-component', @@ -32,13 +29,11 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { isEnlargedView = false; currentPage = 1; totalPages = 0; - nextDirection = NavigationDirection.Next; - prevDirection = NavigationDirection.Previous; attachmentSub: Subscription; attachmentUnitSub: Subscription; constructor( - private route: ActivatedRoute, + public route: ActivatedRoute, private attachmentService: AttachmentService, private attachmentUnitService: AttachmentUnitService, private alertService: AlertService, @@ -74,9 +69,9 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { handleKeyboardEvents(event: KeyboardEvent) { if (this.isEnlargedView) { if (event.key === 'ArrowRight' && this.currentPage < this.totalPages) { - this.navigatePages(NavigationDirection.Next); + this.navigatePages('next'); } else if (event.key === 'ArrowLeft' && this.currentPage > 1) { - this.navigatePages(NavigationDirection.Previous); + this.navigatePages('prev'); } } } @@ -101,7 +96,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); - const viewport = page.getViewport({ scale: 1 }); + const viewport = page.getViewport({ scale: 2 }); const canvas = this.createCanvas(viewport); const context = canvas.getContext('2d'); if (context) { @@ -287,7 +282,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param direction The navigation direction (next or previous). */ navigatePages(direction: NavigationDirection) { - const nextPageIndex = direction === NavigationDirection.Next ? this.currentPage + 1 : this.currentPage - 1; + const nextPageIndex = direction === 'next' ? this.currentPage + 1 : this.currentPage - 1; if (nextPageIndex > 0 && nextPageIndex <= this.totalPages) { this.currentPage = nextPageIndex; const canvas = this.pdfContainer.nativeElement.querySelectorAll('.pdf-page-container canvas')[this.currentPage - 1] as HTMLCanvasElement; diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index 8aafa0f553c0..4b3a20060144 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -34,7 +34,6 @@ describe('PdfPreviewComponent', () => { let fixture: ComponentFixture; let attachmentServiceMock: any; let attachmentUnitServiceMock: any; - let routeMock: any; let alertServiceMock: any; beforeEach(async () => { @@ -45,8 +44,9 @@ describe('PdfPreviewComponent', () => { attachmentUnitServiceMock = { getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), }; - routeMock = { + const routeMock = { data: of({ + course: { id: 1, name: 'Example Course' }, attachment: { id: 1, name: 'Example PDF' }, attachmentUnit: { id: 1, name: 'Chapter 1' }, }), @@ -79,22 +79,10 @@ describe('PdfPreviewComponent', () => { it('should load PDF and handle successful response', () => { component.ngOnInit(); - expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalledWith(1); + expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); expect(alertServiceMock.error).not.toHaveBeenCalled(); }); - it('should display error alert when an invalid attachment ID is provided', () => { - routeMock.data = of({ attachment: { id: null, name: 'Invalid PDF' } }); - component.ngOnInit(); - expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentIDError'); - }); - - it('should display error alert when an invalid attachmentUnit ID is provided', () => { - routeMock.data = of({ attachmentUnit: { id: null, name: 'Invalid PDF' } }); - component.ngOnInit(); - expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUnitIDError'); - }); - it('should load and render PDF pages', () => { const mockBlob = new Blob(['PDF content'], { type: 'application/pdf' }); @@ -102,7 +90,7 @@ describe('PdfPreviewComponent', () => { component.ngOnInit(); expect(URL.createObjectURL).toHaveBeenCalledWith(mockBlob); - expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalledWith(1); + expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); expect(component.totalPages).toBeGreaterThan(0); }); @@ -141,4 +129,49 @@ describe('PdfPreviewComponent', () => { component.toggleBodyScroll(false); expect(component.pdfContainer.nativeElement.style.overflow).toBe('auto'); }); + + it('should not navigate beyond totalPages', () => { + component.totalPages = 5; + component.currentPage = 5; + const event = new KeyboardEvent('keydown', { key: 'ArrowRight' }); + component.handleKeyboardEvents(event); + expect(component.currentPage).toBe(5); // Should not increase + }); + + it('should resize canvas correctly on window resize', () => { + const adjustCanvasSizeSpy = jest.spyOn(component, 'adjustCanvasSize'); + window.dispatchEvent(new Event('resize')); + expect(adjustCanvasSizeSpy).toHaveBeenCalled(); + adjustCanvasSizeSpy.mockRestore(); + }); + + it('should not navigate to the next page if already at last page', () => { + component.currentPage = component.totalPages = 5; + component.handleKeyboardEvents(new KeyboardEvent('keydown', { key: 'ArrowRight' })); + + expect(component.currentPage).toBe(5); + }); + + it('should not navigate to the previous page if already at first page', () => { + component.currentPage = 1; + component.handleKeyboardEvents(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); + + expect(component.currentPage).toBe(1); + }); + + it('should handle error when loading PDF pages', () => { + attachmentServiceMock.getAttachmentFile.mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))); + component.ngOnInit(); + expect(alertServiceMock.error).not.toHaveBeenCalled(); + }); + + it('should update the page view when new pages are loaded', () => { + const mockBlob = new Blob(['PDF content'], { type: 'application/pdf' }); + attachmentServiceMock.getAttachmentFile.mockReturnValue(of(mockBlob)); + component.ngOnInit(); + + // Assuming `loadPdf` sets `totalPages` based on the PDF document + expect(component.totalPages).toBeGreaterThan(0); + expect(component.pdfContainer.nativeElement.children.length).toBeGreaterThan(0); + }); }); From 1affbf6507caa5b60f715a989efd3d40c435d11c Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 18 Aug 2024 11:37:04 +0300 Subject: [PATCH 050/125] Fix compilation errors --- .../lecture-unit-management.component.spec.ts | 6 +++--- .../lecture/lecture-attachments.component.spec.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/javascript/spec/component/lecture-unit/lecture-unit-management.component.spec.ts b/src/test/javascript/spec/component/lecture-unit/lecture-unit-management.component.spec.ts index 0b2419fd92f7..200c8baf3376 100644 --- a/src/test/javascript/spec/component/lecture-unit/lecture-unit-management.component.spec.ts +++ b/src/test/javascript/spec/component/lecture-unit/lecture-unit-management.component.spec.ts @@ -185,13 +185,13 @@ describe('LectureUnitManagementComponent', () => { expect(lectureUnitManagementComponent.getActionType(new OnlineUnit())).toEqual(ActionType.Delete); }); - describe('viewButtonAvailable', () => { + describe('isViewButtonAvailable', () => { it('should return true for an attachment unit with a PDF link', () => { const lectureUnit = { type: LectureUnitType.ATTACHMENT, attachment: { link: 'file.pdf' }, } as LectureUnit; - expect(lectureUnitManagementComponent.viewButtonAvailable(lectureUnit)).toBeTrue(); + expect(lectureUnitManagementComponent.isViewButtonAvailable(lectureUnit)).toBeTrue(); }); it('should return false for an attachment unit with a non-PDF link', () => { @@ -199,7 +199,7 @@ describe('LectureUnitManagementComponent', () => { type: LectureUnitType.ATTACHMENT, attachment: { link: 'file.txt' }, }; - expect(lectureUnitManagementComponent.viewButtonAvailable(lectureUnit)).toBeFalse(); + expect(lectureUnitManagementComponent.isViewButtonAvailable(lectureUnit)).toBeFalse(); }); }); }); diff --git a/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts b/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts index df26f95f8f8a..8058b3887a57 100644 --- a/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts @@ -381,21 +381,21 @@ describe('LectureAttachmentsComponent', () => { expect(attachmentServiceFindAllByLectureIdStub).toHaveBeenCalledOnce(); })); - describe('viewButtonAvailable', () => { + describe('isViewButtonAvailable', () => { it('should return true if the attachment link ends with .pdf', () => { const attachment = { id: 1, link: 'example.pdf', attachmentType: 'FILE' } as Attachment; - expect(comp.viewButtonAvailable(attachment)).toBeTrue(); + expect(comp.isViewButtonAvailable(attachment.link!)).toBeTrue(); }); it('should return false if the attachment link does not end with .pdf', () => { const attachment = { id: 2, link: 'example.txt', attachmentType: 'FILE' } as Attachment; - expect(comp.viewButtonAvailable(attachment)).toBeFalse(); + expect(comp.isViewButtonAvailable(attachment.link!)).toBeFalse(); }); it('should return false for other common file extensions', () => { const attachments = [{ link: 'document.docx' }, { link: 'spreadsheet.xlsx' }, { link: 'presentation.pptx' }, { link: 'image.jpeg' }]; attachments.forEach((att) => { - expect(comp.viewButtonAvailable(att)).toBeFalse(); + expect(comp.isViewButtonAvailable(att.link!)).toBeFalse(); }); }); }); From 60ea19fabe3100f870e21e7b4867fd4c0ff0185d Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 18 Aug 2024 18:43:26 +0300 Subject: [PATCH 051/125] Fix error handling & add new test cases --- .../pdf-preview/pdf-preview.component.ts | 5 +- .../lecture/pdf-preview.component.spec.ts | 95 ++++++++++++++----- 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index a66aa6064229..a997f161a5c0 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -10,6 +10,7 @@ import { onError } from 'app/shared/util/global.utils'; import { AlertService } from 'app/core/util/alert.service'; import { Subscription } from 'rxjs'; import { Course } from 'app/entities/course.model'; +import { HttpErrorResponse } from '@angular/common/http'; type NavigationDirection = 'next' | 'prev'; @@ -46,11 +47,13 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.attachment = data.attachment; this.attachmentSub = this.attachmentService.getAttachmentFile(this.course?.id!, this.attachment?.id!).subscribe({ next: (blob: Blob) => this.loadPdf(URL.createObjectURL(blob)), + error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } else if ('attachmentUnit' in data) { this.attachmentUnit = data.attachmentUnit; this.attachmentUnitSub = this.attachmentUnitService.getAttachmentFile(this.course?.id!, this.attachmentUnit?.id!).subscribe({ next: (blob: Blob) => this.loadPdf(URL.createObjectURL(blob)), + error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } }); @@ -88,7 +91,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * Loads a PDF from a provided URL and initializes viewer setup. * @param fileUrl The URL of the file to load. */ - private async loadPdf(fileUrl: string) { + async loadPdf(fileUrl: string) { try { const loadingTask = PDFJS.getDocument(fileUrl); const pdf = await loadingTask.promise; diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index 4b3a20060144..f6a684eae640 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -22,12 +22,13 @@ jest.mock('pdfjs-dist/build/pdf.worker', () => { import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; import { AttachmentService } from 'app/lecture/attachment.service'; import { AttachmentUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service'; import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component'; import { ElementRef } from '@angular/core'; import { AlertService } from 'app/core/util/alert.service'; +import { HttpErrorResponse } from '@angular/common/http'; describe('PdfPreviewComponent', () => { let component: PdfPreviewComponent; @@ -35,6 +36,7 @@ describe('PdfPreviewComponent', () => { let attachmentServiceMock: any; let attachmentUnitServiceMock: any; let alertServiceMock: any; + let routeMock: any; beforeEach(async () => { global.URL.createObjectURL = jest.fn().mockReturnValue('mocked_blob_url'); @@ -44,7 +46,7 @@ describe('PdfPreviewComponent', () => { attachmentUnitServiceMock = { getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), }; - const routeMock = { + routeMock = { data: of({ course: { id: 1, name: 'Example Course' }, attachment: { id: 1, name: 'Example PDF' }, @@ -77,10 +79,58 @@ describe('PdfPreviewComponent', () => { jest.clearAllMocks(); }); - it('should load PDF and handle successful response', () => { + it('should load attachment file when attachment data is available', () => { component.ngOnInit(); expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); - expect(alertServiceMock.error).not.toHaveBeenCalled(); + expect(attachmentUnitServiceMock.getAttachmentFile).not.toHaveBeenCalled(); + }); + + it('should load attachment unit file when attachment unit data is available', () => { + routeMock.data = of({ + course: { id: 1, name: 'Example Course' }, + attachmentUnit: { id: 1, name: 'Chapter 1' }, + }); + component.ngOnInit(); + expect(attachmentUnitServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); + expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalled(); + }); + + it('should handle errors when loading an attachment file fails', () => { + const errorResponse = new HttpErrorResponse({ + status: 404, + statusText: 'Not Found', + error: 'File not found', + }); + + const attachmentService = TestBed.inject(AttachmentService); + jest.spyOn(attachmentService, 'getAttachmentFile').mockReturnValue(throwError(() => errorResponse)); + const alertServiceSpy = jest.spyOn(alertServiceMock, 'error'); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(alertServiceSpy).toHaveBeenCalledOnce(); + }); + + it('should handle errors when loading an attachment unit file fails', () => { + routeMock.data = of({ + course: { id: 1, name: 'Example Course' }, + attachmentUnit: { id: 1, name: 'Chapter 1' }, + }); + const errorResponse = new HttpErrorResponse({ + status: 404, + statusText: 'Not Found', + error: 'File not found', + }); + + const attachmentUnitService = TestBed.inject(AttachmentUnitService); + jest.spyOn(attachmentUnitService, 'getAttachmentFile').mockReturnValue(throwError(() => errorResponse)); + const alertServiceSpy = jest.spyOn(alertServiceMock, 'error'); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(alertServiceSpy).toHaveBeenCalledOnce(); }); it('should load and render PDF pages', () => { @@ -94,6 +144,10 @@ describe('PdfPreviewComponent', () => { expect(component.totalPages).toBeGreaterThan(0); }); + it('should handle loading errors', async () => { + await expect(component.loadPdf('invalid_url')).rejects.toThrow('Failed to load PDF document'); + }); + it('should handle keyboard navigation for enlarged view', () => { component.isEnlargedView = true; component.totalPages = 5; @@ -130,14 +184,6 @@ describe('PdfPreviewComponent', () => { expect(component.pdfContainer.nativeElement.style.overflow).toBe('auto'); }); - it('should not navigate beyond totalPages', () => { - component.totalPages = 5; - component.currentPage = 5; - const event = new KeyboardEvent('keydown', { key: 'ArrowRight' }); - component.handleKeyboardEvents(event); - expect(component.currentPage).toBe(5); // Should not increase - }); - it('should resize canvas correctly on window resize', () => { const adjustCanvasSizeSpy = jest.spyOn(component, 'adjustCanvasSize'); window.dispatchEvent(new Event('resize')); @@ -159,19 +205,22 @@ describe('PdfPreviewComponent', () => { expect(component.currentPage).toBe(1); }); - it('should handle error when loading PDF pages', () => { - attachmentServiceMock.getAttachmentFile.mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))); - component.ngOnInit(); - expect(alertServiceMock.error).not.toHaveBeenCalled(); + it('should unsubscribe from attachment subscription on destroy', () => { + const spySub = jest.spyOn(component.attachmentSub, 'unsubscribe'); + component.ngOnDestroy(); + expect(spySub).toHaveBeenCalled(); }); - it('should update the page view when new pages are loaded', () => { - const mockBlob = new Blob(['PDF content'], { type: 'application/pdf' }); - attachmentServiceMock.getAttachmentFile.mockReturnValue(of(mockBlob)); + it('should unsubscribe from attachmentUnit subscription on destroy', () => { + routeMock.data = of({ + course: { id: 1, name: 'Example Course' }, + attachmentUnit: { id: 1, name: 'Chapter 1' }, + }); component.ngOnInit(); - - // Assuming `loadPdf` sets `totalPages` based on the PDF document - expect(component.totalPages).toBeGreaterThan(0); - expect(component.pdfContainer.nativeElement.children.length).toBeGreaterThan(0); + fixture.detectChanges(); + expect(component.attachmentUnitSub).toBeDefined(); + const spySub = jest.spyOn(component.attachmentUnitSub, 'unsubscribe'); + component.ngOnDestroy(); + expect(spySub).toHaveBeenCalled(); }); }); From 4e24d1c59327d29b6cbaa4073574745c75ce04dc Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 18 Aug 2024 19:36:23 +0300 Subject: [PATCH 052/125] Mak adjustCanvasSize public --- .../webapp/app/lecture/pdf-preview/pdf-preview.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index a997f161a5c0..874fec844e0c 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -179,7 +179,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { /** * Dynamically updates the canvas size within an enlarged view based on the viewport. */ - private adjustCanvasSize = () => { + adjustCanvasSize = () => { if (this.isEnlargedView) { const canvasElements = this.pdfContainer.nativeElement.querySelectorAll('.pdf-page-container canvas'); if (this.currentPage - 1 < canvasElements.length) { From 24536b30b27052d15b1e653863ef2b7b8da41411 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 18 Aug 2024 21:07:18 +0300 Subject: [PATCH 053/125] Fix client tests --- .../component/lecture/lecture-attachments.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts b/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts index 8058b3887a57..52be9f3d7cc9 100644 --- a/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts @@ -155,7 +155,7 @@ describe('LectureAttachmentsComponent', () => { fixture.detectChanges(); tick(); expect(comp.attachments).toHaveLength(3); - expect(attachmentServiceFindAllByLectureIdStub).toHaveBeenCalledOnce(); + expect(attachmentServiceFindAllByLectureIdStub).toHaveBeenCalledTimes(2); })); it('should not accept too large file', fakeAsync(() => { From d9b47f14b6e3d631d592e2c7ae921ddb05896e34 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 18 Aug 2024 21:13:06 +0300 Subject: [PATCH 054/125] Fix client tests 2 --- .../spec/component/lecture/pdf-preview.component.spec.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index f6a684eae640..d96d1628eacd 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -144,10 +144,6 @@ describe('PdfPreviewComponent', () => { expect(component.totalPages).toBeGreaterThan(0); }); - it('should handle loading errors', async () => { - await expect(component.loadPdf('invalid_url')).rejects.toThrow('Failed to load PDF document'); - }); - it('should handle keyboard navigation for enlarged view', () => { component.isEnlargedView = true; component.totalPages = 5; From 547be9afabfdc56a612aa4fd67dce55cda26bdab Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 18 Aug 2024 22:41:00 +0300 Subject: [PATCH 055/125] Increase client test coverage, fix typos --- .../www1/artemis/web/rest/FileResource.java | 2 +- .../pdf-preview/pdf-preview.component.ts | 8 ++- .../lecture/pdf-preview.component.spec.ts | 67 ++++++++++++++----- 3 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java index fcf7e4a1c29a..10222806307f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java @@ -454,7 +454,7 @@ public ResponseEntity getAttachmentUnitAttachment(@PathVariable Long att } /** - * GET /attachments/{attachmenUnitId}/file : Returns the file associated with the + * GET files/courses/{courseId}/attachments/{attachmenUnitId}/file : Returns the file associated with the * given attachmentUnit ID as a downloadable resource * * @param attachmentUnitId the ID of the attachment to retrieve diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 874fec844e0c..f63e8c7b0427 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -154,8 +154,12 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { container.appendChild(canvas); container.appendChild(overlay); - container.addEventListener('mouseenter', () => (overlay.style.opacity = '1')); - container.addEventListener('mouseleave', () => (overlay.style.opacity = '0')); + container.addEventListener('mouseenter', () => { + overlay.style.opacity = '1'; + }); + container.addEventListener('mouseleave', () => { + overlay.style.opacity = '0'; + }); overlay.addEventListener('click', () => this.displayEnlargedCanvas(canvas, pageIndex)); return container; diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index d96d1628eacd..48bd94b2a47b 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -68,6 +68,10 @@ describe('PdfPreviewComponent', () => { ], }).compileComponents(); + const pdfContainerElement = document.createElement('div'); + Object.defineProperty(pdfContainerElement, 'clientWidth', { value: 800 }); + Object.defineProperty(pdfContainerElement, 'clientHeight', { value: 600 }); + fixture = TestBed.createComponent(PdfPreviewComponent); component = fixture.componentInstance; component.pdfContainer = new ElementRef(document.createElement('div')); @@ -79,13 +83,13 @@ describe('PdfPreviewComponent', () => { jest.clearAllMocks(); }); - it('should load attachment file when attachment data is available', () => { + it('hould load attachment file and verify service calls when attachment data is available', () => { component.ngOnInit(); expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); expect(attachmentUnitServiceMock.getAttachmentFile).not.toHaveBeenCalled(); }); - it('should load attachment unit file when attachment unit data is available', () => { + it('should load attachment unit file and verify service calls when attachment unit data is available', () => { routeMock.data = of({ course: { id: 1, name: 'Example Course' }, attachmentUnit: { id: 1, name: 'Chapter 1' }, @@ -95,7 +99,7 @@ describe('PdfPreviewComponent', () => { expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalled(); }); - it('should handle errors when loading an attachment file fails', () => { + it('should handle errors and trigger alert when loading an attachment file fails', () => { const errorResponse = new HttpErrorResponse({ status: 404, statusText: 'Not Found', @@ -112,7 +116,7 @@ describe('PdfPreviewComponent', () => { expect(alertServiceSpy).toHaveBeenCalledOnce(); }); - it('should handle errors when loading an attachment unit file fails', () => { + it('should handle errors and trigger alert when loading an attachment unit file fails', () => { routeMock.data = of({ course: { id: 1, name: 'Example Course' }, attachmentUnit: { id: 1, name: 'Chapter 1' }, @@ -133,7 +137,7 @@ describe('PdfPreviewComponent', () => { expect(alertServiceSpy).toHaveBeenCalledOnce(); }); - it('should load and render PDF pages', () => { + it('should load PDF and verify rendering of pages', () => { const mockBlob = new Blob(['PDF content'], { type: 'application/pdf' }); attachmentServiceMock.getAttachmentFile.mockReturnValue(of(mockBlob)); @@ -144,7 +148,7 @@ describe('PdfPreviewComponent', () => { expect(component.totalPages).toBeGreaterThan(0); }); - it('should handle keyboard navigation for enlarged view', () => { + it('should navigate through pages using keyboard in enlarged view', () => { component.isEnlargedView = true; component.totalPages = 5; component.currentPage = 3; @@ -159,7 +163,7 @@ describe('PdfPreviewComponent', () => { expect(component.currentPage).toBe(3); }); - it('should toggle enlarged view on and off', () => { + it('should toggle enlarged view state', () => { const mockCanvas = document.createElement('canvas'); component.displayEnlargedCanvas(mockCanvas, 1); expect(component.isEnlargedView).toBeTrue(); @@ -172,7 +176,7 @@ describe('PdfPreviewComponent', () => { expect(component.isEnlargedView).toBeFalse(); }); - it('should prevent scrolling when enlarged view is open', () => { + it('should prevent scrolling when enlarged view is active', () => { component.toggleBodyScroll(true); expect(component.pdfContainer.nativeElement.style.overflow).toBe('hidden'); @@ -180,34 +184,47 @@ describe('PdfPreviewComponent', () => { expect(component.pdfContainer.nativeElement.style.overflow).toBe('auto'); }); - it('should resize canvas correctly on window resize', () => { - const adjustCanvasSizeSpy = jest.spyOn(component, 'adjustCanvasSize'); - window.dispatchEvent(new Event('resize')); - expect(adjustCanvasSizeSpy).toHaveBeenCalled(); - adjustCanvasSizeSpy.mockRestore(); + it('should not update canvas size if not in enlarged view', () => { + component.isEnlargedView = false; + component.currentPage = 3; + + const spy = jest.spyOn(component, 'updateEnlargedCanvas'); + component.adjustCanvasSize(); + + expect(spy).not.toHaveBeenCalled(); }); - it('should not navigate to the next page if already at last page', () => { + it('should not update canvas size if the current page canvas does not exist', () => { + component.isEnlargedView = true; + component.currentPage = 10; + + const spy = jest.spyOn(component, 'updateEnlargedCanvas'); + component.adjustCanvasSize(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('should prevent navigation beyond last page', () => { component.currentPage = component.totalPages = 5; component.handleKeyboardEvents(new KeyboardEvent('keydown', { key: 'ArrowRight' })); expect(component.currentPage).toBe(5); }); - it('should not navigate to the previous page if already at first page', () => { + it('should prevent navigation before first page', () => { component.currentPage = 1; component.handleKeyboardEvents(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); expect(component.currentPage).toBe(1); }); - it('should unsubscribe from attachment subscription on destroy', () => { + it('should unsubscribe attachment subscription during component destruction', () => { const spySub = jest.spyOn(component.attachmentSub, 'unsubscribe'); component.ngOnDestroy(); expect(spySub).toHaveBeenCalled(); }); - it('should unsubscribe from attachmentUnit subscription on destroy', () => { + it('should unsubscribe attachmentUnit subscription during component destruction', () => { routeMock.data = of({ course: { id: 1, name: 'Example Course' }, attachmentUnit: { id: 1, name: 'Chapter 1' }, @@ -219,4 +236,20 @@ describe('PdfPreviewComponent', () => { component.ngOnDestroy(); expect(spySub).toHaveBeenCalled(); }); + + it('should stop event propagation and navigate pages', () => { + const navigateSpy = jest.spyOn(component, 'navigatePages'); + const eventMock = { stopPropagation: jest.fn() } as unknown as MouseEvent; + + component.handleNavigation('next', eventMock); + + expect(eventMock.stopPropagation).toHaveBeenCalled(); + expect(navigateSpy).toHaveBeenCalledWith('next'); + }); + + it('should call adjustCanvasSize when window is resized', () => { + const adjustCanvasSizeSpy = jest.spyOn(component, 'adjustCanvasSize'); + window.dispatchEvent(new Event('resize')); + expect(adjustCanvasSizeSpy).toHaveBeenCalled(); + }); }); From 6c608b53af6a8d65a2643357d3e083201762bbc9 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 18 Aug 2024 23:55:00 +0300 Subject: [PATCH 056/125] Fix test case typo --- .../spec/component/lecture/pdf-preview.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index 48bd94b2a47b..63d0a608b79d 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -83,7 +83,7 @@ describe('PdfPreviewComponent', () => { jest.clearAllMocks(); }); - it('hould load attachment file and verify service calls when attachment data is available', () => { + it('should load attachment file and verify service calls when attachment data is available', () => { component.ngOnInit(); expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); expect(attachmentUnitServiceMock.getAttachmentFile).not.toHaveBeenCalled(); From 4c0b38b0a2ff2d26a2e6851c428fac9fcb26ae68 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 19 Aug 2024 00:52:16 +0300 Subject: [PATCH 057/125] Resolved changes --- .../java/de/tum/in/www1/artemis/web/rest/FileResource.java | 1 + .../webapp/app/lecture/lecture-attachments.component.html | 2 +- .../webapp/app/lecture/lecture-attachments.component.ts | 4 ++-- .../lecture-unit-management.component.html | 2 +- .../lecture-unit-management.component.ts | 6 +++--- .../webapp/app/lecture/pdf-preview/pdf-preview.component.ts | 6 +++--- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java index 10222806307f..6a1c46822001 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java @@ -457,6 +457,7 @@ public ResponseEntity getAttachmentUnitAttachment(@PathVariable Long att * GET files/courses/{courseId}/attachments/{attachmenUnitId}/file : Returns the file associated with the * given attachmentUnit ID as a downloadable resource * + * @param courseId The ID of the course that the Attachment belongs to * @param attachmentUnitId the ID of the attachment to retrieve * @return ResponseEntity containing the file as a resource */ diff --git a/src/main/webapp/app/lecture/lecture-attachments.component.html b/src/main/webapp/app/lecture/lecture-attachments.component.html index a56efadbc2d9..174fe26bd667 100644 --- a/src/main/webapp/app/lecture/lecture-attachments.component.html +++ b/src/main/webapp/app/lecture/lecture-attachments.component.html @@ -79,7 +79,7 @@

- @if (viewButtonAvailable.get(attachment.id!)) { + @if (viewButtonAvailable[attachment.id!]) { +
@if (isEnlargedView) { diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index 1ce2019305cb..fd199f697448 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -87,3 +87,10 @@ right: 10px; } } + +.slide-checkbox { + position: absolute; + top: 10px; + right: 10px; + z-index: 4; +} diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index f2075dc57fbb..32e1b218aa41 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -8,10 +8,11 @@ import { AttachmentUnit } from 'app/entities/lecture-unit/attachmentUnit.model'; import { AttachmentUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service'; import { onError } from 'app/shared/util/global.utils'; import { AlertService } from 'app/core/util/alert.service'; -import { Subscription } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { Course } from 'app/entities/course.model'; import { HttpErrorResponse } from '@angular/common/http'; import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { faTrash } from '@fortawesome/free-solid-svg-icons'; type NavigationDirection = 'next' | 'prev'; @@ -35,6 +36,13 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { totalPages = 0; attachmentSub: Subscription; attachmentUnitSub: Subscription; + selectedPages: Set = new Set(); + + private dialogErrorSource = new Subject(); + dialogError$ = this.dialogErrorSource.asObservable(); + + // Icons + faTrash = faTrash; constructor( public route: ActivatedRoute, @@ -90,6 +98,10 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.adjustCanvasSize(); } + isDeleteDisabled(): boolean { + return this.selectedPages.size === 0; + } + /** * Loads a PDF from a provided URL and initializes viewer setup. * @param fileUrl The URL of the file to load. @@ -150,12 +162,15 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { /* Dynamically created elements are not detected by DOM, that is why we need to set the styles manually. * See: https://stackoverflow.com/a/70911189 */ + container.id = `pdf-page-${pageIndex}`; container.classList.add('pdf-page-container'); container.style.cssText = `position: relative; display: inline-block; width: ${canvas.style.width}; height: ${canvas.style.height}; margin: 20px; box-shadow: 0 2px 6px var(--pdf-preview-canvas-shadow);`; const overlay = this.createOverlay(pageIndex); + const checkbox = this.createCheckbox(pageIndex); container.appendChild(canvas); container.appendChild(overlay); + container.appendChild(checkbox); container.addEventListener('mouseenter', () => { overlay.style.opacity = '1'; @@ -183,6 +198,22 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { return overlay; } + private createCheckbox(pageIndex: number): HTMLDivElement { + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.classList.add('slide-checkbox'); + checkbox.style.cssText = `position: absolute; top: -5px; right: -5px; z-index: 4;`; + checkbox.checked = this.selectedPages.has(pageIndex); + checkbox.addEventListener('change', () => { + if (checkbox.checked) { + this.selectedPages.add(pageIndex); + } else { + this.selectedPages.delete(pageIndex); + } + }); + return checkbox; + } + /** * Dynamically updates the canvas size within an enlarged view based on the viewport. */ @@ -338,4 +369,15 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.updateEnlargedCanvas(canvas); } } + + deleteSelectedSlides() { + this.selectedPages.forEach((page) => { + const pageElement = this.pdfContainer.nativeElement.querySelector(`#pdf-page-${page}`); + if (pageElement) { + this.pdfContainer.nativeElement.removeChild(pageElement); + } + }); + this.selectedPages.clear(); + this.dialogErrorSource.next(''); + } } From 445c209ce03cc9facc4bcc773a1a57f2f954b6fd Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 2 Sep 2024 01:02:46 +0300 Subject: [PATCH 071/125] Add merge file button --- .../pdf-preview/pdf-preview.component.html | 33 ++++++---- .../pdf-preview/pdf-preview.component.ts | 62 ++++++++++++++++--- 2 files changed, 73 insertions(+), 22 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index cc9ab61096a6..10270077fd7c 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -10,19 +10,26 @@

{{ attachmentUnit.id }}: {{ attachmentUnit.name }} }

- +
+ + + +
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 32e1b218aa41..4b5113479f6f 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -12,7 +12,7 @@ import { Subject, Subscription } from 'rxjs'; import { Course } from 'app/entities/course.model'; import { HttpErrorResponse } from '@angular/common/http'; import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { faTrash } from '@fortawesome/free-solid-svg-icons'; +import { faFileImport, faTrash } from '@fortawesome/free-solid-svg-icons'; type NavigationDirection = 'next' | 'prev'; @@ -26,6 +26,7 @@ type NavigationDirection = 'next' | 'prev'; export class PdfPreviewComponent implements OnInit, OnDestroy { @ViewChild('pdfContainer', { static: true }) pdfContainer: ElementRef; @ViewChild('enlargedCanvas') enlargedCanvas: ElementRef; + @ViewChild('fileInput', { static: false }) fileInput: ElementRef; readonly DEFAULT_SLIDE_WIDTH = 250; course?: Course; @@ -37,11 +38,13 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { attachmentSub: Subscription; attachmentUnitSub: Subscription; selectedPages: Set = new Set(); + isMergedPdfLoading = false; private dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); // Icons + faFileImport = faFileImport; faTrash = faTrash; constructor( @@ -57,13 +60,13 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { if ('attachment' in data) { this.attachment = data.attachment; this.attachmentSub = this.attachmentService.getAttachmentFile(this.course!.id!, this.attachment!.id!).subscribe({ - next: (blob: Blob) => this.loadPdf(URL.createObjectURL(blob)), + next: (blob: Blob) => this.loadOrAppendPdf(URL.createObjectURL(blob)), error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } else if ('attachmentUnit' in data) { this.attachmentUnit = data.attachmentUnit; this.attachmentUnitSub = this.attachmentUnitService.getAttachmentFile(this.course!.id!, this.attachmentUnit!.id!).subscribe({ - next: (blob: Blob) => this.loadPdf(URL.createObjectURL(blob)), + next: (blob: Blob) => this.loadOrAppendPdf(URL.createObjectURL(blob)), error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } @@ -103,16 +106,28 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } /** - * Loads a PDF from a provided URL and initializes viewer setup. - * @param fileUrl The URL of the file to load. + * Loads or appends a PDF from a provided URL. + * @param fileUrl The URL of the file to load or append. + * @param append Whether the document should be appended to the existing one. */ - async loadPdf(fileUrl: string) { + async loadOrAppendPdf(fileUrl: string, append: boolean = false): Promise { + if (append) { + this.isMergedPdfLoading = true; + } try { const loadingTask = PDFJS.getDocument(fileUrl); const pdf = await loadingTask.promise; - this.totalPages = pdf.numPages; + const numPages = pdf.numPages; + const initialPageCount = this.totalPages; + + if (!append) { + this.totalPages = 0; + this.pdfContainer.nativeElement.innerHTML = ''; + } + + this.totalPages += numPages; - for (let i = 1; i <= pdf.numPages; i++) { + for (let i = 1; i <= numPages; i++) { const page = await pdf.getPage(i); const viewport = page.getViewport({ scale: 2 }); const canvas = this.createCanvas(viewport); @@ -121,16 +136,33 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { await page.render({ canvasContext: context, viewport }).promise; } - const container = this.createContainer(canvas, i); + const container = this.createContainer(canvas, initialPageCount + i); this.pdfContainer.nativeElement.appendChild(container); } URL.revokeObjectURL(fileUrl); + + if (append) { + setTimeout(() => this.scrollToBottom(), 100); + } } catch (error) { onError(this.alertService, error); + } finally { + if (append) { + this.isMergedPdfLoading = false; + } } } + scrollToBottom(): void { + const scrollOptions: ScrollToOptions = { + top: this.pdfContainer.nativeElement.scrollHeight, + left: 0, + behavior: 'smooth' as ScrollBehavior, + }; + this.pdfContainer.nativeElement.scrollTo(scrollOptions); + } + /** * Creates a canvas for each page of the PDF to allow for individual page rendering. * @param viewport The viewport settings used for rendering the page. @@ -380,4 +412,16 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.selectedPages.clear(); this.dialogErrorSource.next(''); } + + triggerFileInput(): void { + this.fileInput.nativeElement.click(); + } + + mergePDF(event: Event): void { + const file = (event.target as HTMLInputElement).files?.[0]; + if (file) { + const fileUrl = URL.createObjectURL(file); + this.loadOrAppendPdf(fileUrl, true); + } + } } From 00071dbd7731b4808c51e01dba0d8c5c6e1afd1f Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 2 Sep 2024 01:58:31 +0300 Subject: [PATCH 072/125] Fix display enlarged canvas view --- .../app/lecture/pdf-preview/pdf-preview.component.scss | 9 +-------- .../app/lecture/pdf-preview/pdf-preview.component.ts | 6 ------ 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index fd199f697448..283aac8b0ea1 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -33,7 +33,7 @@ justify-content: center; align-items: center; background-color: var(--pdf-preview-enlarged-container-overlay); - z-index: 2; + z-index: 5; .btn-close { position: absolute; @@ -87,10 +87,3 @@ right: 10px; } } - -.slide-checkbox { - position: absolute; - top: 10px; - right: 10px; - z-index: 4; -} diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 4b5113479f6f..d9c3a6b9cb89 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -120,11 +120,6 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { const numPages = pdf.numPages; const initialPageCount = this.totalPages; - if (!append) { - this.totalPages = 0; - this.pdfContainer.nativeElement.innerHTML = ''; - } - this.totalPages += numPages; for (let i = 1; i <= numPages; i++) { @@ -233,7 +228,6 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { private createCheckbox(pageIndex: number): HTMLDivElement { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; - checkbox.classList.add('slide-checkbox'); checkbox.style.cssText = `position: absolute; top: -5px; right: -5px; z-index: 4;`; checkbox.checked = this.selectedPages.has(pageIndex); checkbox.addEventListener('change', () => { From ddac25370a9dbefa0fb27af15e444ed83f88b705 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sat, 7 Sep 2024 23:04:36 +0300 Subject: [PATCH 073/125] Update delete & append buttons --- .../lecture/pdf-preview/pdf-preview.component.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 10270077fd7c..61230dd0a8f1 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -11,11 +11,6 @@

}

- - + +
From eb163bd6f7c3ed7dfacda6ccf04b4db3b15995c6 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 8 Sep 2024 14:09:28 +0300 Subject: [PATCH 074/125] Update page IDs upon delete & merge pdf --- .../pdf-preview/pdf-preview.component.ts | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index d9c3a6b9cb89..861e5c4ad814 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -125,7 +125,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { for (let i = 1; i <= numPages; i++) { const page = await pdf.getPage(i); const viewport = page.getViewport({ scale: 2 }); - const canvas = this.createCanvas(viewport); + const canvas = this.createCanvas(viewport, initialPageCount + i); const context = canvas.getContext('2d'); if (context) { await page.render({ canvasContext: context, viewport }).promise; @@ -161,10 +161,12 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { /** * Creates a canvas for each page of the PDF to allow for individual page rendering. * @param viewport The viewport settings used for rendering the page. + * @param pageIndex The index of the page within the PDF document. * @returns A new HTMLCanvasElement configured for the PDF page. */ - private createCanvas(viewport: PDFJS.PageViewport): HTMLCanvasElement { + private createCanvas(viewport: PDFJS.PageViewport, pageIndex: number): HTMLCanvasElement { const canvas = document.createElement('canvas'); + canvas.id = `${pageIndex}`; /* Canvas styling is predefined because Canvas tags do not support CSS classes * as they are not HTML elements but rather a bitmap drawing surface. * See: https://stackoverflow.com/a/29675448 @@ -205,7 +207,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { container.addEventListener('mouseleave', () => { overlay.style.opacity = '0'; }); - overlay.addEventListener('click', () => this.displayEnlargedCanvas(canvas, pageIndex)); + overlay.addEventListener('click', () => this.displayEnlargedCanvas(canvas)); return container; } @@ -228,13 +230,15 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { private createCheckbox(pageIndex: number): HTMLDivElement { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; + checkbox.id = String(pageIndex); checkbox.style.cssText = `position: absolute; top: -5px; right: -5px; z-index: 4;`; checkbox.checked = this.selectedPages.has(pageIndex); checkbox.addEventListener('change', () => { if (checkbox.checked) { - this.selectedPages.add(pageIndex); + console.log('Adding page', checkbox.id); + this.selectedPages.add(Number(checkbox.id)); } else { - this.selectedPages.delete(pageIndex); + this.selectedPages.delete(Number(checkbox.id)); } }); return checkbox; @@ -256,11 +260,10 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { /** * Displays a canvas in an enlarged view for detailed examination. * @param originalCanvas The original canvas element displaying the page. - * @param pageIndex The index of the page being displayed. */ - displayEnlargedCanvas(originalCanvas: HTMLCanvasElement, pageIndex: number) { + displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { this.isEnlargedView = true; - this.currentPage = pageIndex; + this.currentPage = Number(originalCanvas.id); this.updateEnlargedCanvas(originalCanvas); this.toggleBodyScroll(true); } @@ -403,6 +406,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.pdfContainer.nativeElement.removeChild(pageElement); } }); + this.totalPages -= this.selectedPages.size; + this.updatePageIDs(); this.selectedPages.clear(); this.dialogErrorSource.next(''); } @@ -418,4 +423,18 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.loadOrAppendPdf(fileUrl, true); } } + + updatePageIDs() { + const remainingPages = this.pdfContainer.nativeElement.querySelectorAll('.pdf-page-container'); + remainingPages.forEach((container, index) => { + const pageIndex = index + 1; + container.id = `pdf-page-${pageIndex}`; + const canvas = container.querySelector('canvas'); + const overlay = container.querySelector('div'); + const checkbox = container.querySelector('input[type="checkbox"]'); + canvas!.id = String(pageIndex); + overlay!.innerHTML = `${pageIndex}`; + checkbox!.id = String(pageIndex); + }); + } } From 34b716f1421b0b038ac2b4f69e8cea069c0eab3d Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 8 Sep 2024 14:47:08 +0300 Subject: [PATCH 075/125] Clean up the code and update JSDoc --- .../pdf-preview/pdf-preview.component.html | 2 +- .../pdf-preview/pdf-preview.component.ts | 61 ++++++++++++------- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 61230dd0a8f1..9af8b9e13b63 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -19,7 +19,7 @@

[renderButtonText]="false" (delete)="deleteSelectedSlides()" [dialogError]="dialogError$" - [disabled]="isDeleteDisabled()" + [disabled]="isRemovePagesDisabled()" > Remove Page(s) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 861e5c4ad814..235873564adb 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -79,8 +79,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } /** - * Handles keyboard events for navigation within the PDF viewer. - * @param event The keyboard event captured. + * Handles navigation within the PDF viewer using keyboard arrow keys. + * @param event - The keyboard event captured for navigation. */ @HostListener('document:keydown', ['$event']) handleKeyboardEvents(event: KeyboardEvent) { @@ -101,7 +101,11 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.adjustCanvasSize(); } - isDeleteDisabled(): boolean { + /** + * Determines if the delete action is disabled based on the selection of pages. + * @returns True if no pages are selected, false otherwise. + */ + isRemovePagesDisabled(): boolean { return this.selectedPages.size === 0; } @@ -109,6 +113,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * Loads or appends a PDF from a provided URL. * @param fileUrl The URL of the file to load or append. * @param append Whether the document should be appended to the existing one. + * @returns A promise that resolves when the PDF is loaded. */ async loadOrAppendPdf(fileUrl: string, append: boolean = false): Promise { if (append) { @@ -120,21 +125,18 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { const numPages = pdf.numPages; const initialPageCount = this.totalPages; - this.totalPages += numPages; - for (let i = 1; i <= numPages; i++) { const page = await pdf.getPage(i); const viewport = page.getViewport({ scale: 2 }); const canvas = this.createCanvas(viewport, initialPageCount + i); const context = canvas.getContext('2d'); - if (context) { - await page.render({ canvasContext: context, viewport }).promise; - } + await page.render({ canvasContext: context!, viewport }).promise; - const container = this.createContainer(canvas, initialPageCount + i); - this.pdfContainer.nativeElement.appendChild(container); + const canvasContainer = this.createCanvasContainer(canvas, initialPageCount + i); + this.pdfContainer.nativeElement.appendChild(canvasContainer); } + this.totalPages += numPages; URL.revokeObjectURL(fileUrl); if (append) { @@ -149,6 +151,9 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } } + /** + * Scrolls the PDF container to the bottom after appending new pages. + */ scrollToBottom(): void { const scrollOptions: ScrollToOptions = { top: this.pdfContainer.nativeElement.scrollHeight, @@ -186,13 +191,13 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param pageIndex The index of the page within the PDF document. * @returns A configured div element that includes the canvas and interactive overlays. */ - createContainer(canvas: HTMLCanvasElement, pageIndex: number): HTMLDivElement { + createCanvasContainer(canvas: HTMLCanvasElement, pageIndex: number): HTMLDivElement { const container = document.createElement('div'); /* Dynamically created elements are not detected by DOM, that is why we need to set the styles manually. * See: https://stackoverflow.com/a/70911189 */ container.id = `pdf-page-${pageIndex}`; - container.classList.add('pdf-page-container'); + container.classList.add('pdf-canvas-container'); container.style.cssText = `position: relative; display: inline-block; width: ${canvas.style.width}; height: ${canvas.style.height}; margin: 20px; box-shadow: 0 2px 6px var(--pdf-preview-canvas-shadow);`; const overlay = this.createOverlay(pageIndex); @@ -235,7 +240,6 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { checkbox.checked = this.selectedPages.has(pageIndex); checkbox.addEventListener('change', () => { if (checkbox.checked) { - console.log('Adding page', checkbox.id); this.selectedPages.add(Number(checkbox.id)); } else { this.selectedPages.delete(Number(checkbox.id)); @@ -249,7 +253,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { */ adjustCanvasSize = () => { if (this.isEnlargedView) { - const canvasElements = this.pdfContainer.nativeElement.querySelectorAll('.pdf-page-container canvas'); + const canvasElements = this.pdfContainer.nativeElement.querySelectorAll('.pdf-canvas-container canvas'); if (this.currentPage - 1 < canvasElements.length) { const canvas = canvasElements[this.currentPage - 1] as HTMLCanvasElement; this.updateEnlargedCanvas(canvas); @@ -273,7 +277,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * ensuring the content is centered and displayed appropriately within the available space. * It is called within an animation frame to synchronize updates with the browser's render cycle for smooth visuals. * - * @param {HTMLCanvasElement} originalCanvas - The source canvas element used to extract image data for resizing and redrawing. + * @param originalCanvas - The source canvas element used to extract image data for resizing and redrawing. */ updateEnlargedCanvas(originalCanvas: HTMLCanvasElement) { requestAnimationFrame(() => { @@ -290,8 +294,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * Calculates the scaling factor to adjust the canvas size based on the dimensions of the container. * This method ensures that the canvas is scaled to fit within the container without altering the aspect ratio. * - * @param {HTMLCanvasElement} originalCanvas - The original canvas element representing the PDF page. - * @returns {number} The scaling factor used to resize the original canvas to fit within the container dimensions. + * @param originalCanvas - The original canvas element representing the PDF page. + * @returns The scaling factor used to resize the original canvas to fit within the container dimensions. */ calculateScaleFactor(originalCanvas: HTMLCanvasElement): number { const containerWidth = this.pdfContainer.nativeElement.clientWidth; @@ -306,8 +310,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * This method updates the dimensions of the enlarged canvas element to ensure that the entire PDF page * is visible and properly scaled within the viewer. * - * @param {HTMLCanvasElement} originalCanvas - The canvas element from which the image is scaled. - * @param {number} scaleFactor - The factor by which the canvas is resized. + * @param originalCanvas - The canvas element from which the image is scaled. + * @param scaleFactor - The factor by which the canvas is resized. */ resizeCanvas(originalCanvas: HTMLCanvasElement, scaleFactor: number): void { const enlargedCanvas = this.enlargedCanvas.nativeElement; @@ -319,7 +323,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * Redraws the original canvas content onto the enlarged canvas at the updated scale. * This method ensures that the image is rendered clearly and correctly positioned on the enlarged canvas. * - * @param {HTMLCanvasElement} originalCanvas - The original canvas containing the image to be redrawn. + * @param originalCanvas - The original canvas containing the image to be redrawn. */ redrawCanvas(originalCanvas: HTMLCanvasElement): void { const enlargedCanvas = this.enlargedCanvas.nativeElement; @@ -394,11 +398,14 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { const nextPageIndex = direction === 'next' ? this.currentPage + 1 : this.currentPage - 1; if (nextPageIndex > 0 && nextPageIndex <= this.totalPages) { this.currentPage = nextPageIndex; - const canvas = this.pdfContainer.nativeElement.querySelectorAll('.pdf-page-container canvas')[this.currentPage - 1] as HTMLCanvasElement; + const canvas = this.pdfContainer.nativeElement.querySelectorAll('.pdf-canvas-container canvas')[this.currentPage - 1] as HTMLCanvasElement; this.updateEnlargedCanvas(canvas); } } + /** + * Deletes selected slides from the PDF viewer. + */ deleteSelectedSlides() { this.selectedPages.forEach((page) => { const pageElement = this.pdfContainer.nativeElement.querySelector(`#pdf-page-${page}`); @@ -412,10 +419,17 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.dialogErrorSource.next(''); } + /** + * Triggers the file input to select files. + */ triggerFileInput(): void { this.fileInput.nativeElement.click(); } + /** + * Adds a selected PDF file at the end of the current PDF document. + * @param event - The event containing the file input. + */ mergePDF(event: Event): void { const file = (event.target as HTMLInputElement).files?.[0]; if (file) { @@ -424,8 +438,11 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } } + /** + * Updates the IDs of remaining pages after some have been removed. + */ updatePageIDs() { - const remainingPages = this.pdfContainer.nativeElement.querySelectorAll('.pdf-page-container'); + const remainingPages = this.pdfContainer.nativeElement.querySelectorAll('.pdf-canvas-container'); remainingPages.forEach((container, index) => { const pageIndex = index + 1; container.id = `pdf-page-${pageIndex}`; From 7deeb4aaf5423112376cde2dda3a586373388c06 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 8 Sep 2024 17:18:15 +0300 Subject: [PATCH 076/125] Save & cancel buttons are added and styling is fixed --- .../pdf-preview/pdf-preview.component.html | 98 ++++++++++--------- .../pdf-preview/pdf-preview.component.scss | 11 +-- .../pdf-preview/pdf-preview.component.ts | 6 +- 3 files changed, 63 insertions(+), 52 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 9af8b9e13b63..2387f53a3d10 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -1,52 +1,62 @@
-
-
-

- - @if (attachment) { - {{ attachment.id }}: {{ attachment.name }} - } @else if (attachmentUnit) { - {{ attachmentUnit.id }}: {{ attachmentUnit.name }} - } -

-
- - - -
+
+

+ + @if (attachment) { + {{ attachment.id }}: {{ attachment.name }} + } @else if (attachmentUnit) { + {{ attachmentUnit.id }}: {{ attachmentUnit.name }} + } +

+
+ + +
-
-
- @if (isEnlargedView) { -
- - - @if (currentPage !== 1) { - - } - @if (currentPage !== totalPages) { - - } -
{{ currentPage }}
-
+
+
+ @if (isEnlargedView) { +
+ + + @if (currentPage !== 1) { + } + @if (currentPage !== totalPages) { + + } +
{{ currentPage }}
+ } +
+
+
+ +
+
+
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index 283aac8b0ea1..816d2b9da2cb 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -1,21 +1,20 @@ .pdf-container { position: relative; display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 30px; + grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); + gap: 10px; max-height: 60vh; height: 60vh; overflow-y: auto; border: 1px solid var(--border-color); padding: 10px; - margin: 10px; - width: 95%; + margin: 10px 0; + width: 100%; box-shadow: 0 2px 5px var(--pdf-preview-pdf-container-shadow); - align-items: start; z-index: 0; @media (max-width: 800px) { - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(auto-fit, minmax(270px, 1fr)); } @media (max-width: 500px) { diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 235873564adb..a64fbe3a7060 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -12,7 +12,7 @@ import { Subject, Subscription } from 'rxjs'; import { Course } from 'app/entities/course.model'; import { HttpErrorResponse } from '@angular/common/http'; import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { faFileImport, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { faFileImport, faSave, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons'; type NavigationDirection = 'next' | 'prev'; @@ -45,6 +45,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { // Icons faFileImport = faFileImport; + faSave = faSave; + faTimes = faTimes; faTrash = faTrash; constructor( @@ -115,7 +117,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param append Whether the document should be appended to the existing one. * @returns A promise that resolves when the PDF is loaded. */ - async loadOrAppendPdf(fileUrl: string, append: boolean = false): Promise { + async loadOrAppendPdf(fileUrl: string, append = false): Promise { if (append) { this.isMergedPdfLoading = true; } From 725ff4fdb4504abf095fff0a630d532e9c708ace Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 8 Sep 2024 19:37:57 +0300 Subject: [PATCH 077/125] Generate PDF from the changed structure --- package-lock.json | 168 ++++++++++++++++++ package.json | 1 + .../pdf-preview/pdf-preview.component.html | 2 +- .../pdf-preview/pdf-preview.component.ts | 23 +++ 4 files changed, 193 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 08cc6a211342..ba73c95e53ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "interactjs": "1.10.27", "ismobilejs-es5": "0.0.1", "js-video-url-parser": "0.5.1", + "jspdf": "^2.5.1", "jszip": "3.10.1", "lodash-es": "4.17.21", "mobile-drag-drop": "3.0.0-rc.0", @@ -6613,6 +6614,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "optional": true + }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", @@ -7518,6 +7525,17 @@ "dev": true, "license": "MIT" }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -7863,6 +7881,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -8089,6 +8116,17 @@ "node-int64": "^0.4.0" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -8284,6 +8322,31 @@ "node": ">=6" } }, + "node_modules/canvg": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", + "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/canvg/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "optional": true + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -9121,6 +9184,15 @@ "node": ">=4" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-loader": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", @@ -12002,6 +12074,19 @@ "dev": true, "license": "MIT" }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -15000,6 +15085,29 @@ ], "license": "MIT" }, + "node_modules/jspdf": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz", + "integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==", + "dependencies": { + "@babel/runtime": "^7.14.0", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.4.8" + }, + "optionalDependencies": { + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.2.0", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf/node_modules/dompurify": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.6.tgz", + "integrity": "sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==", + "optional": true + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -17554,6 +17662,12 @@ "integrity": "sha512-5yHVB9OHqKd9fr/OIsn8ss0NgThQ9buaqrEuwr9Or5YjPp6h+WTDKWZI+xZLaBGZCtODTnFtlSHNmhFsq67THg==", "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -18136,6 +18250,15 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -18653,6 +18776,15 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rimraf": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", @@ -19736,6 +19868,15 @@ "node": ">=8" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -19973,6 +20114,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -20272,6 +20422,15 @@ "node": "*" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -20895,6 +21054,15 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", diff --git a/package.json b/package.json index ab806fb582e0..063feec7d251 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "interactjs": "1.10.27", "ismobilejs-es5": "0.0.1", "js-video-url-parser": "0.5.1", + "jspdf": "^2.5.1", "jszip": "3.10.1", "lodash-es": "4.17.21", "mobile-drag-drop": "3.0.0-rc.0", diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 2387f53a3d10..554041caf441 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -53,7 +53,7 @@

- diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index a64fbe3a7060..1f0329fc84e3 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -13,6 +13,7 @@ import { Course } from 'app/entities/course.model'; import { HttpErrorResponse } from '@angular/common/http'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { faFileImport, faSave, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { jsPDF } from 'jspdf'; type NavigationDirection = 'next' | 'prev'; @@ -29,6 +30,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { @ViewChild('fileInput', { static: false }) fileInput: ElementRef; readonly DEFAULT_SLIDE_WIDTH = 250; + readonly DEFAULT_GENERATED_SLIDE_FORMAT = [1920, 1080]; //Represents 16:9 aspect ratio course?: Course; attachment?: Attachment; attachmentUnit?: AttachmentUnit; @@ -456,4 +458,25 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { checkbox!.id = String(pageIndex); }); } + + generatePdfFromCanvases() { + const doc = new jsPDF({ + orientation: 'landscape', + unit: 'px', + format: this.DEFAULT_GENERATED_SLIDE_FORMAT, + }); + + const canvasElements = this.pdfContainer.nativeElement.querySelectorAll('canvas'); + const scaleFactor = 1; + Array.from(canvasElements).forEach((canvas, index) => { + if (index > 0) doc.addPage(); + const imgData = canvas.toDataURL('image/jpeg', scaleFactor); + const imgProps = doc.getImageProperties(imgData); + const pdfWidth = doc.internal.pageSize.getWidth(); + const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width; + doc.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight); + }); + + doc.save('modified_document.pdf'); + } } From 472b0e14f3ecfd3b82567fed3c3ff78bfe84d924 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 9 Sep 2024 00:37:08 +0300 Subject: [PATCH 078/125] Updating capability on Save --- .../pdf-preview/pdf-preview.component.html | 2 +- .../pdf-preview/pdf-preview.component.ts | 44 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 554041caf441..6c482787f6a1 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -53,7 +53,7 @@

- diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 1f0329fc84e3..2adca4f04fef 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -14,6 +14,8 @@ import { HttpErrorResponse } from '@angular/common/http'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { faFileImport, faSave, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons'; import { jsPDF } from 'jspdf'; +import dayjs from 'dayjs/esm'; +import { objectToJsonBlob } from 'app/utils/blob-util'; type NavigationDirection = 'next' | 'prev'; @@ -41,6 +43,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { attachmentUnitSub: Subscription; selectedPages: Set = new Set(); isMergedPdfLoading = false; + attachmentToBeEdited?: Attachment; private dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); @@ -464,6 +467,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { orientation: 'landscape', unit: 'px', format: this.DEFAULT_GENERATED_SLIDE_FORMAT, + compress: true, }); const canvasElements = this.pdfContainer.nativeElement.querySelectorAll('canvas'); @@ -477,6 +481,44 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { doc.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight); }); - doc.save('modified_document.pdf'); + return doc.output('blob'); + } + + updateAttachmentWithFile(): void { + const pdfBlob = this.generatePdfFromCanvases(); + const pdfFile = new File([pdfBlob], 'updatedAttachment.pdf', { type: 'application/pdf' }); + + if (this.attachment) { + this.attachmentToBeEdited = this.attachment; + this.attachmentToBeEdited!.version!++; + this.attachmentToBeEdited!.uploadDate = dayjs(); + + this.attachmentService.update(this.attachmentToBeEdited!.id!, this.attachmentToBeEdited!, pdfFile).subscribe({ + next: () => { + this.alertService.success('Attachment updated successfully!'); + }, + error: (error) => { + this.alertService.error('Failed to update attachment: ' + error.message); + }, + }); + } else if (this.attachmentUnit) { + this.attachmentToBeEdited = this.attachmentUnit; + this.attachmentToBeEdited!.version!++; + this.attachmentToBeEdited!.uploadDate = dayjs(); + + const formData = new FormData(); + formData.append('file', pdfFile); + formData.append('attachment', objectToJsonBlob(this.attachmentToBeEdited)); + formData.append('attachmentUnit', objectToJsonBlob(this.attachmentUnit)); + + this.attachmentUnitService.update(this.attachmentUnit!.lecture!.id!, this.attachmentToBeEdited!.id!, formData).subscribe({ + next: () => { + this.alertService.success('Attachment updated successfully!'); + }, + error: (error) => { + this.alertService.error('Failed to update attachment: ' + error.message); + }, + }); + } } } From d13db5312d3d3ff64b2eba535ae50c936999bb26 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 9 Sep 2024 01:00:59 +0300 Subject: [PATCH 079/125] Fix view button styling --- .../lecture-unit-management.component.html | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/lecture-unit-management.component.html b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/lecture-unit-management.component.html index 2dea431d4934..1d3ad524d659 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/lecture-unit-management.component.html +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/lecture-unit-management.component.html @@ -68,19 +68,27 @@
- @if (lecture.course?.id && showCompetencies) { - - } - @if (viewButtonAvailable[lectureUnit.id!]) { - - - - } +
+ @if (lecture.course?.id && showCompetencies) { + + } + @if (viewButtonAvailable[lectureUnit.id!]) { + + + + } +
+
@if (this.emitEditEvents) { @if (editButtonAvailable(lectureUnit)) { From 1748887d79a808083f17bc914249b5aa5630066f Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 9 Sep 2024 01:17:30 +0300 Subject: [PATCH 080/125] Update Cancel button --- .../lecture/pdf-preview/pdf-preview.component.html | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 6c482787f6a1..e2896d854dfe 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -47,10 +47,19 @@

diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 2adca4f04fef..9c7718033ff7 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -495,10 +495,10 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.attachmentService.update(this.attachmentToBeEdited!.id!, this.attachmentToBeEdited!, pdfFile).subscribe({ next: () => { - this.alertService.success('Attachment updated successfully!'); + this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); }, error: (error) => { - this.alertService.error('Failed to update attachment: ' + error.message); + this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); }, }); } else if (this.attachmentUnit) { @@ -513,10 +513,10 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.attachmentUnitService.update(this.attachmentUnit!.lecture!.id!, this.attachmentToBeEdited!.id!, formData).subscribe({ next: () => { - this.alertService.success('Attachment updated successfully!'); + this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); }, error: (error) => { - this.alertService.error('Failed to update attachment: ' + error.message); + this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); }, }); } diff --git a/src/main/webapp/i18n/de/global.json b/src/main/webapp/i18n/de/global.json index 9db7bb972f26..3a41636550b1 100644 --- a/src/main/webapp/i18n/de/global.json +++ b/src/main/webapp/i18n/de/global.json @@ -217,7 +217,7 @@ "open": "Öffnen", "save": "Speichern", "saving": "Speichern...", - "view": "Details", + "view": "Ansehen", "reset": "Zurücksetzen", "preview": "Vorschau", "visual": "Visuell", diff --git a/src/main/webapp/i18n/de/lecture.json b/src/main/webapp/i18n/de/lecture.json index c5d76e7c292d..cf10c2c6a291 100644 --- a/src/main/webapp/i18n/de/lecture.json +++ b/src/main/webapp/i18n/de/lecture.json @@ -100,7 +100,11 @@ "pdfPreview": { "title": "Anhang", "attachmentIDError": "Ungültiger Anhang oder ungültige Anhangs-ID.", - "attachmentUnitIDError": "Ungültige Dateieinheit oder ungültige Dateieinheits-ID." + "attachmentUnitIDError": "Ungültige Dateieinheit oder ungültige Dateieinheits-ID.", + "removePageButton": "Entferne Seiten", + "appendFileButton": "Anhänge Datei", + "attachmentUpdateSuccess": "Anhang erfolgreich aktualisiert.", + "attachmentUpdateError": "Fehler beim Aktualisieren des Anhangs: {{error}}" } } } diff --git a/src/main/webapp/i18n/en/lecture.json b/src/main/webapp/i18n/en/lecture.json index b03b840b400f..2c629bb2f381 100644 --- a/src/main/webapp/i18n/en/lecture.json +++ b/src/main/webapp/i18n/en/lecture.json @@ -100,7 +100,11 @@ "pdfPreview": { "title": "Attachment", "attachmentIDError": "Invalid Attachment or Attachment ID.", - "attachmentUnitIDError": "Invalid Attachment Unit or Attachment Unit ID." + "attachmentUnitIDError": "Invalid Attachment Unit or Attachment Unit ID.", + "removePageButton": "Remove Page(s)", + "appendFileButton": "Append File", + "attachmentUpdateSuccess": "Attachment updated successfully.", + "attachmentUpdateError": "Failed to update attachment: {{error}}" } } } From 324e812b1d2c7745f702cfe3fdf0803b700f1b8d Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Thu, 12 Sep 2024 19:19:41 +0300 Subject: [PATCH 082/125] Fix client tests based on changes --- .../pdf-preview/pdf-preview.component.html | 4 ++-- .../lecture/pdf-preview.component.spec.ts | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index b260b4c86d8b..7c0b74afbdbf 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -52,8 +52,8 @@

class="btn btn-default" [routerLink]=" attachment - ? ['/course-management', course!.id, 'lectures', attachment.lecture!.id, 'attachments'] - : ['/course-management', course!.id, 'lectures', attachmentUnit!.lecture!.id, 'unit-management'] + ? ['/course-management', course!.id, 'lectures', attachment!.lecture?.id, 'attachments'] + : ['/course-management', course!.id, 'lectures', attachmentUnit!.lecture?.id, 'unit-management'] " [ngbTooltip]="'entity.action.view' | artemisTranslate" > diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index 03e494ccbd41..0445cdf2c292 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -212,7 +212,7 @@ describe('PdfPreviewComponent', () => { it('should toggle enlarged view state', () => { const mockCanvas = document.createElement('canvas'); - component.displayEnlargedCanvas(mockCanvas, 1); + component.displayEnlargedCanvas(mockCanvas); expect(component.isEnlargedView).toBeTrue(); const clickEvent = new MouseEvent('click', { @@ -300,7 +300,7 @@ describe('PdfPreviewComponent', () => { const canvas = document.createElement('canvas'); const pdfContainer = document.createElement('div'); - pdfContainer.className = 'pdf-page-container'; + pdfContainer.className = 'pdf-canvas-container'; pdfContainer.appendChild(canvas); component.pdfContainer = { nativeElement: pdfContainer, @@ -396,22 +396,22 @@ describe('PdfPreviewComponent', () => { mockCanvas.style.width = '600px'; mockCanvas.style.height = '400px'; - const container = component.createContainer(mockCanvas, 1); + const container = component.createCanvasContainer(mockCanvas, 1); expect(container.tagName).toBe('DIV'); - expect(container.classList.contains('pdf-page-container')).toBeTruthy(); + expect(container.classList.contains('pdf-canvas-container')).toBeTruthy(); expect(container.style.position).toBe('relative'); expect(container.style.display).toBe('inline-block'); expect(container.style.width).toBe('600px'); expect(container.style.height).toBe('400px'); expect(container.style.margin).toBe('20px'); - expect(container.children).toHaveLength(2); + expect(container.children).toHaveLength(3); expect(container.firstChild).toBe(mockCanvas); }); it('should handle mouseenter and mouseleave events correctly', () => { const mockCanvas = document.createElement('canvas'); - const container = component.createContainer(mockCanvas, 1); + const container = component.createCanvasContainer(mockCanvas, 1); const overlay = container.children[1] as HTMLElement; // Trigger mouseenter @@ -428,10 +428,10 @@ describe('PdfPreviewComponent', () => { it('should handle click event on overlay to trigger displayEnlargedCanvas', () => { jest.spyOn(component, 'displayEnlargedCanvas'); const mockCanvas = document.createElement('canvas'); - const container = component.createContainer(mockCanvas, 1); + const container = component.createCanvasContainer(mockCanvas, 1); const overlay = container.children[1]; overlay.dispatchEvent(new Event('click')); - expect(component.displayEnlargedCanvas).toHaveBeenCalledWith(mockCanvas, 1); + expect(component.displayEnlargedCanvas).toHaveBeenCalledWith(mockCanvas); }); }); From 926f75ac87e998b9fb760885f324076e9854605c Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Fri, 13 Sep 2024 01:08:08 +0300 Subject: [PATCH 083/125] Add client tests --- .../pdf-preview/pdf-preview.component.ts | 8 +- .../lecture/pdf-preview.component.spec.ts | 151 ++++++++++++++++++ 2 files changed, 155 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 9c7718033ff7..1111a5bc478a 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -45,7 +45,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { isMergedPdfLoading = false; attachmentToBeEdited?: Attachment; - private dialogErrorSource = new Subject(); + dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); // Icons @@ -471,7 +471,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { }); const canvasElements = this.pdfContainer.nativeElement.querySelectorAll('canvas'); - const scaleFactor = 1; + const scaleFactor = 1.0; Array.from(canvasElements).forEach((canvas, index) => { if (index > 0) doc.addPage(); const imgData = canvas.toDataURL('image/jpeg', scaleFactor); @@ -502,7 +502,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { }, }); } else if (this.attachmentUnit) { - this.attachmentToBeEdited = this.attachmentUnit; + this.attachmentToBeEdited = this.attachmentUnit.attachment!; this.attachmentToBeEdited!.version!++; this.attachmentToBeEdited!.uploadDate = dayjs(); @@ -511,7 +511,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { formData.append('attachment', objectToJsonBlob(this.attachmentToBeEdited)); formData.append('attachmentUnit', objectToJsonBlob(this.attachmentUnit)); - this.attachmentUnitService.update(this.attachmentUnit!.lecture!.id!, this.attachmentToBeEdited!.id!, formData).subscribe({ + this.attachmentUnitService.update(this.attachmentUnit!.lecture!.id!, this.attachmentUnit!.id!, formData).subscribe({ next: () => { this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); }, diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index 0445cdf2c292..68f7f189a80c 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -18,6 +18,23 @@ jest.mock('pdfjs-dist', () => { }; }); +jest.mock('jspdf', () => { + return { + jsPDF: jest.fn().mockImplementation(() => ({ + addPage: jest.fn(), + addImage: jest.fn(), + getImageProperties: jest.fn(() => ({ width: 1920, height: 1080 })), + internal: { + pageSize: { + getWidth: jest.fn(() => 1920), + getHeight: jest.fn(() => 1080), + }, + }, + output: jest.fn(() => new Blob(['PDF content'], { type: 'application/pdf' })), + })), + }; +}); + jest.mock('pdfjs-dist/build/pdf.worker', () => { return {}; }); @@ -42,6 +59,7 @@ import { ElementRef } from '@angular/core'; import { AlertService } from 'app/core/util/alert.service'; import { HttpErrorResponse } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; +import dayjs from 'dayjs'; describe('PdfPreviewComponent', () => { let component: PdfPreviewComponent; @@ -59,9 +77,11 @@ describe('PdfPreviewComponent', () => { global.URL.createObjectURL = jest.fn().mockReturnValue('mocked_blob_url'); attachmentServiceMock = { getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), + update: jest.fn(), }; attachmentUnitServiceMock = { getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), + update: jest.fn(), }; routeMock = { data: of({ @@ -73,6 +93,7 @@ describe('PdfPreviewComponent', () => { alertServiceMock = { addAlert: jest.fn(), error: jest.fn(), + success: jest.fn(), }; await TestBed.configureTestingModule({ @@ -434,4 +455,134 @@ describe('PdfPreviewComponent', () => { overlay.dispatchEvent(new Event('click')); expect(component.displayEnlargedCanvas).toHaveBeenCalledWith(mockCanvas); }); + + it('should delete selected slides, update total pages, clear selected pages, and reset dialog error source', () => { + const mockContainer = document.createElement('div'); + for (let i = 1; i <= 3; i++) { + const mockPage = document.createElement('div'); + mockPage.id = `pdf-page-${i}`; + mockContainer.appendChild(mockPage); + } + component.pdfContainer = new ElementRef(mockContainer); + + component.selectedPages = new Set([1, 3]); + component.totalPages = 3; + + const updatePageIDsSpy = jest.spyOn(component, 'updatePageIDs'); + const dialogErrorSourceSpy = jest.spyOn(component.dialogErrorSource, 'next'); + + component.deleteSelectedSlides(); + + expect(component.pdfContainer.nativeElement.querySelector('#pdf-page-1')).toBeNull(); + expect(component.pdfContainer.nativeElement.querySelector('#pdf-page-3')).toBeNull(); + expect(component.pdfContainer.nativeElement.querySelector('#pdf-page-2')).not.toBeNull(); + expect(component.totalPages).toBe(1); + expect(updatePageIDsSpy).toHaveBeenCalled(); + expect(component.selectedPages.size).toBe(0); + expect(dialogErrorSourceSpy).toHaveBeenCalledWith(''); + }); + + it('should trigger the file input click event', () => { + const mockFileInput = document.createElement('input'); + mockFileInput.type = 'file'; + component.fileInput = new ElementRef(mockFileInput); + + const clickSpy = jest.spyOn(component.fileInput.nativeElement, 'click'); + component.triggerFileInput(); + expect(clickSpy).toHaveBeenCalled(); + }); + + it('should extract the file from the event, create an object URL, and call loadOrAppendPdf with correct arguments', () => { + const mockFile = new Blob(['PDF content'], { type: 'application/pdf' }); + const mockFileList = { + 0: mockFile, + length: 1, + item: () => mockFile, + } as unknown as FileList; + + const mockEvent = { target: { files: mockFileList } } as unknown as Event; + const createObjectURLSpy = jest.spyOn(URL, 'createObjectURL').mockReturnValue('mocked_file_url'); + const loadOrAppendPdfSpy = jest.spyOn(component, 'loadOrAppendPdf').mockResolvedValue(); + + component.mergePDF(mockEvent); + + expect(createObjectURLSpy).toHaveBeenCalledWith(mockFile); + expect(loadOrAppendPdfSpy).toHaveBeenCalledWith('mocked_file_url', true); + }); + + it('should update the IDs of remaining pages after some have been removed', () => { + const mockContainer = document.createElement('div'); + + for (let i = 1; i <= 3; i++) { + const mockPageContainer = document.createElement('div'); + mockPageContainer.classList.add('pdf-canvas-container'); + mockPageContainer.id = `pdf-page-${i}`; + + const mockCanvas = document.createElement('canvas'); + mockCanvas.id = String(i); + mockPageContainer.appendChild(mockCanvas); + + const mockOverlay = document.createElement('div'); + mockOverlay.innerHTML = `${i}`; + mockPageContainer.appendChild(mockOverlay); + + const mockCheckbox = document.createElement('input'); + mockCheckbox.type = 'checkbox'; + mockCheckbox.id = String(i); + mockPageContainer.appendChild(mockCheckbox); + + mockContainer.appendChild(mockPageContainer); + } + + component.pdfContainer = new ElementRef(mockContainer); + component.updatePageIDs(); + + const remainingPages = component.pdfContainer.nativeElement.querySelectorAll('.pdf-canvas-container'); + remainingPages.forEach((pageContainer, index) => { + const pageIndex = index + 1; + const canvas = pageContainer.querySelector('canvas'); + const overlay = pageContainer.querySelector('div'); + const checkbox = pageContainer.querySelector('input[type="checkbox"]'); + + expect(pageContainer.id).toBe(`pdf-page-${pageIndex}`); + expect(canvas!.id).toBe(String(pageIndex)); + expect(overlay!.innerHTML).toBe(`${pageIndex}`); + expect(checkbox!.id).toBe(String(pageIndex)); + }); + }); + + it('should update attachment and show success alert', () => { + const generatePdfFromCanvasesSpy = jest.spyOn(component, 'generatePdfFromCanvases'); + const alertServiceSpy = jest.spyOn(alertServiceMock, 'success'); + + component.attachment = { id: 1, version: 1, uploadDate: dayjs() } as any; + + const mockUpdateObservable = of({}); + attachmentServiceMock.update.mockReturnValue(mockUpdateObservable); + component.updateAttachmentWithFile(); + + expect(generatePdfFromCanvasesSpy).toHaveBeenCalled(); + expect(component.attachment!.version).toBe(2); + expect(dayjs(component.attachment!.uploadDate).isSame(dayjs(), 'day')).toBeTrue(); + expect(attachmentServiceMock.update).toHaveBeenCalledWith(1, component.attachment, expect.any(File)); + expect(alertServiceSpy).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); + }); + + it('should update attachment unit and show success alert', () => { + component.attachment = undefined; + component.attachmentUnit = { id: 1, name: 'Chapter 1', attachment: { id: 1, version: 1, uploadDate: dayjs() }, lecture: { id: 1 } } as any; + + const generatePdfFromCanvasesSpy = jest.spyOn(component, 'generatePdfFromCanvases'); + const alertServiceSpy = jest.spyOn(alertServiceMock, 'success'); + + const mockUpdateObservable = of({}); + attachmentUnitServiceMock.update.mockReturnValue(mockUpdateObservable); + component.updateAttachmentWithFile(); + + expect(generatePdfFromCanvasesSpy).toHaveBeenCalled(); + expect(component.attachmentUnit!.attachment!.version).toBe(2); + expect(dayjs(component.attachmentUnit!.attachment!.uploadDate).isSame(dayjs(), 'day')).toBeTrue(); + expect(attachmentUnitServiceMock.update).toHaveBeenCalledWith(1, component.attachmentUnit!.id, expect.any(FormData)); + expect(alertServiceSpy).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); + }); }); From 6099d709a2332b8ec777f9e270581281a41d8e32 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 15 Sep 2024 18:39:30 +0200 Subject: [PATCH 084/125] Add fixes for edge cases --- .../pdf-preview/pdf-preview.component.html | 25 ++++++++++++------- .../pdf-preview/pdf-preview.component.scss | 4 +++ .../pdf-preview/pdf-preview.component.ts | 15 +++-------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 7c0b74afbdbf..d75c92922b27 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -1,14 +1,21 @@
-

- - @if (attachment) { - {{ attachment.id }}: {{ attachment.name }} - } @else if (attachmentUnit) { - {{ attachmentUnit.id }}: {{ attachmentUnit.name }} +
+

+ + @if (attachment) { + {{ attachment.id }}: {{ attachment.name }} + } @else if (attachmentUnit) { + {{ attachmentUnit.id }}: {{ attachmentUnit.name }} + } +

+ @if (isPdfLoading) { +
+ +
} -

+
- diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index 816d2b9da2cb..5f849b06e90d 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -86,3 +86,7 @@ right: 10px; } } + +.spinner-border { + margin-left: 10px; +} diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 1111a5bc478a..35f9e882a262 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -42,7 +42,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { attachmentSub: Subscription; attachmentUnitSub: Subscription; selectedPages: Set = new Set(); - isMergedPdfLoading = false; + isPdfLoading = false; attachmentToBeEdited?: Attachment; dialogErrorSource = new Subject(); @@ -108,14 +108,6 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.adjustCanvasSize(); } - /** - * Determines if the delete action is disabled based on the selection of pages. - * @returns True if no pages are selected, false otherwise. - */ - isRemovePagesDisabled(): boolean { - return this.selectedPages.size === 0; - } - /** * Loads or appends a PDF from a provided URL. * @param fileUrl The URL of the file to load or append. @@ -124,7 +116,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { */ async loadOrAppendPdf(fileUrl: string, append = false): Promise { if (append) { - this.isMergedPdfLoading = true; + this.isPdfLoading = true; } try { const loadingTask = PDFJS.getDocument(fileUrl); @@ -153,7 +145,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { onError(this.alertService, error); } finally { if (append) { - this.isMergedPdfLoading = false; + this.isPdfLoading = false; + this.fileInput.nativeElement.value = ''; } } } From ce822f4b63d848d9265036c04924dbe0eab74432 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 15 Sep 2024 21:28:31 +0200 Subject: [PATCH 085/125] Add Fabric to reduce PDF size --- package-lock.json | 81 +++++++++++-------- package.json | 1 + .../pdf-preview/pdf-preview.component.html | 6 +- .../pdf-preview/pdf-preview.component.ts | 23 ++++-- 4 files changed, 67 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index f26f746e6a18..afa92e09fe53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "diff-match-patch-typescript": "1.1.0", "dompurify": "3.1.6", "export-to-csv": "1.4.0", + "fabric": "^6.4.2", "fast-json-patch": "3.1.1", "franc-min": "6.2.0", "html-diff-ts": "1.4.2", @@ -7290,7 +7291,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "debug": "^4.3.4" @@ -7535,7 +7536,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/atob": { @@ -8706,7 +8707,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -9307,7 +9308,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "rrweb-cssom": "^0.7.1" @@ -9611,7 +9612,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "whatwg-mimetype": "^4.0.0", @@ -9648,7 +9649,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/decompress-response": { @@ -9787,7 +9788,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -11202,6 +11203,18 @@ "node": ">=4" } }, + "node_modules/fabric": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fabric/-/fabric-6.4.2.tgz", + "integrity": "sha512-wsXxy45eHQ66t66RSjti9lM6Z6iC+EBhrxvWWkZlcrhsI8Lxgn2mii/o2UqRpUr4IwzmggTZq/SAGjtZ6mHTtQ==", + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "jsdom": "^20.0.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -11500,7 +11513,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -12047,7 +12060,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "whatwg-encoding": "^3.1.1" @@ -12169,7 +12182,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -12201,7 +12214,7 @@ "version": "7.0.5", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "agent-base": "^7.0.2", @@ -12739,7 +12752,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/is-stream": { @@ -14982,7 +14995,7 @@ "version": "24.1.1", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.1.tgz", "integrity": "sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "cssstyle": "^4.0.1", @@ -16129,7 +16142,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -16139,7 +16152,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -17055,7 +17068,7 @@ "version": "2.2.12", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/object-assign": { @@ -18187,14 +18200,14 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -18940,7 +18953,7 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/run-applescript": { @@ -18998,7 +19011,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/sass": { @@ -19072,7 +19085,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -20156,7 +20169,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/synckit": { @@ -20538,7 +20551,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.33", @@ -20554,7 +20567,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -20982,7 +20995,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -21649,7 +21662,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" @@ -21776,7 +21789,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -22108,7 +22121,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -22121,7 +22134,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -22134,7 +22147,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -22144,7 +22157,7 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tr46": "^5.0.0", @@ -22431,7 +22444,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -22472,7 +22485,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18" @@ -22482,7 +22495,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/y18n": { diff --git a/package.json b/package.json index e289340584ca..1c4c82fa5ace 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "diff-match-patch-typescript": "1.1.0", "dompurify": "3.1.6", "export-to-csv": "1.4.0", + "fabric": "^6.4.2", "fast-json-patch": "3.1.1", "franc-min": "6.2.0", "html-diff-ts": "1.4.2", diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index d75c92922b27..e6436d7897bc 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -25,12 +25,12 @@

[renderButtonText]="false" (delete)="deleteSelectedSlides()" [dialogError]="dialogError$" - [disabled]="selectedPages.size === 0" + [disabled]="isPdfLoading || selectedPages.size === 0" > - +

- diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 35f9e882a262..9705401c6141 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -16,6 +16,7 @@ import { faFileImport, faSave, faTimes, faTrash } from '@fortawesome/free-solid- import { jsPDF } from 'jspdf'; import dayjs from 'dayjs/esm'; import { objectToJsonBlob } from 'app/utils/blob-util'; +import { Image } from 'fabric'; type NavigationDirection = 'next' | 'prev'; @@ -115,9 +116,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @returns A promise that resolves when the PDF is loaded. */ async loadOrAppendPdf(fileUrl: string, append = false): Promise { - if (append) { - this.isPdfLoading = true; - } + this.isPdfLoading = true; try { const loadingTask = PDFJS.getDocument(fileUrl); const pdf = await loadingTask.promise; @@ -144,8 +143,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } catch (error) { onError(this.alertService, error); } finally { + this.isPdfLoading = false; if (append) { - this.isPdfLoading = false; this.fileInput.nativeElement.value = ''; } } @@ -464,14 +463,24 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { }); const canvasElements = this.pdfContainer.nativeElement.querySelectorAll('canvas'); - const scaleFactor = 1.0; + const scaleFactor = 0.5; + Array.from(canvasElements).forEach((canvas, index) => { if (index > 0) doc.addPage(); - const imgData = canvas.toDataURL('image/jpeg', scaleFactor); + + const fabricImage = new Image(canvas, { + crossOrigin: 'anonymous', + }); + + const imgData = fabricImage.toDataURL({ + format: 'jpeg', + quality: scaleFactor, + }); + const imgProps = doc.getImageProperties(imgData); const pdfWidth = doc.internal.pageSize.getWidth(); const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width; - doc.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight); + doc.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight, undefined, 'FAST'); }); return doc.output('blob'); From f4d30fb73f56b9254240f42ee703e1dcd5435978 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 29 Sep 2024 03:41:55 +0200 Subject: [PATCH 086/125] Change pdf editing method to pdf-lib --- package-lock.json | 282 +++++------------- package.json | 3 +- .../pdf-preview/pdf-preview.component.ts | 131 ++++---- 3 files changed, 134 insertions(+), 282 deletions(-) diff --git a/package-lock.json b/package-lock.json index afa92e09fe53..bdcdaa009b15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,14 +46,12 @@ "diff-match-patch-typescript": "1.1.0", "dompurify": "3.1.6", "export-to-csv": "1.4.0", - "fabric": "^6.4.2", "fast-json-patch": "3.1.1", "franc-min": "6.2.0", "html-diff-ts": "1.4.2", "interactjs": "1.10.27", "ismobilejs-es5": "0.0.1", "js-video-url-parser": "0.5.1", - "jspdf": "^2.5.1", "jszip": "3.10.1", "lodash-es": "4.17.21", "mobile-drag-drop": "3.0.0-rc.0", @@ -61,6 +59,7 @@ "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", + "pdf-lib": "^1.17.1", "pdfjs-dist": "4.6.82", "posthog-js": "1.161.3", "rxjs": "7.8.1", @@ -5547,6 +5546,22 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -6628,12 +6643,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/raf": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", - "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", - "optional": true - }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", @@ -7291,7 +7300,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4" @@ -7536,20 +7545,9 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -7895,15 +7893,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "optional": true, - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -8130,17 +8119,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", - "bin": { - "btoa": "bin/btoa.js" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -8336,31 +8314,6 @@ "node": ">=6" } }, - "node_modules/canvg": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", - "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "@types/raf": "^3.4.0", - "core-js": "^3.8.3", - "raf": "^3.4.1", - "regenerator-runtime": "^0.13.7", - "rgbcolor": "^1.0.1", - "stackblur-canvas": "^2.0.0", - "svg-pathdata": "^6.0.3" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/canvg/node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "optional": true - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -8707,7 +8660,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -9198,15 +9151,6 @@ "node": ">=4" } }, - "node_modules/css-line-break": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", - "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "optional": true, - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/css-loader": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", @@ -9308,7 +9252,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "rrweb-cssom": "^0.7.1" @@ -9612,7 +9556,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "whatwg-mimetype": "^4.0.0", @@ -9649,7 +9593,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/decompress-response": { @@ -9788,7 +9732,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -11203,18 +11147,6 @@ "node": ">=4" } }, - "node_modules/fabric": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fabric/-/fabric-6.4.2.tgz", - "integrity": "sha512-wsXxy45eHQ66t66RSjti9lM6Z6iC+EBhrxvWWkZlcrhsI8Lxgn2mii/o2UqRpUr4IwzmggTZq/SAGjtZ6mHTtQ==", - "engines": { - "node": ">=16.20.0" - }, - "optionalDependencies": { - "canvas": "^2.11.2", - "jsdom": "^20.0.1" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -11513,7 +11445,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -12060,7 +11992,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "whatwg-encoding": "^3.1.1" @@ -12093,19 +12025,6 @@ "dev": true, "license": "MIT" }, - "node_modules/html2canvas": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", - "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "optional": true, - "dependencies": { - "css-line-break": "^2.1.0", - "text-segmentation": "^1.0.3" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -12182,7 +12101,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -12214,7 +12133,7 @@ "version": "7.0.5", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.0.2", @@ -12752,7 +12671,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/is-stream": { @@ -14995,7 +14914,7 @@ "version": "24.1.1", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.1.tgz", "integrity": "sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "cssstyle": "^4.0.1", @@ -15104,29 +15023,6 @@ ], "license": "MIT" }, - "node_modules/jspdf": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz", - "integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==", - "dependencies": { - "@babel/runtime": "^7.14.0", - "atob": "^2.1.2", - "btoa": "^1.2.1", - "fflate": "^0.4.8" - }, - "optionalDependencies": { - "canvg": "^3.0.6", - "core-js": "^3.6.0", - "dompurify": "^2.2.0", - "html2canvas": "^1.0.0-rc.5" - } - }, - "node_modules/jspdf/node_modules/dompurify": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.6.tgz", - "integrity": "sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==", - "optional": true - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -16142,7 +16038,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -16152,7 +16048,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -17068,7 +16964,7 @@ "version": "2.2.12", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/object-assign": { @@ -17665,6 +17561,22 @@ "node": ">=6" } }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/pdfjs-dist": { "version": "4.6.82", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.6.82.tgz", @@ -17684,12 +17596,6 @@ "integrity": "sha512-5yHVB9OHqKd9fr/OIsn8ss0NgThQ9buaqrEuwr9Or5YjPp6h+WTDKWZI+xZLaBGZCtODTnFtlSHNmhFsq67THg==", "license": "MIT" }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "optional": true - }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -18200,14 +18106,14 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -18272,15 +18178,6 @@ ], "license": "MIT" }, - "node_modules/raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "optional": true, - "dependencies": { - "performance-now": "^2.1.0" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -18798,15 +18695,6 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, - "node_modules/rgbcolor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", - "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", - "optional": true, - "engines": { - "node": ">= 0.8.15" - } - }, "node_modules/rimraf": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", @@ -18953,7 +18841,7 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/run-applescript": { @@ -19011,7 +18899,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/sass": { @@ -19085,7 +18973,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -19900,15 +19788,6 @@ "node": ">=8" } }, - "node_modules/stackblur-canvas": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", - "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", - "optional": true, - "engines": { - "node": ">=0.1.14" - } - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -20146,15 +20025,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/svg-pathdata": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", - "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", - "optional": true, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -20169,7 +20039,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/synckit": { @@ -20454,15 +20324,6 @@ "node": "*" } }, - "node_modules/text-segmentation": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", - "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "optional": true, - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -20551,7 +20412,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.33", @@ -20567,7 +20428,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -20995,7 +20856,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -21086,15 +20947,6 @@ "node": ">= 0.4.0" } }, - "node_modules/utrie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "optional": true, - "dependencies": { - "base64-arraybuffer": "^1.0.2" - } - }, "node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", @@ -21662,7 +21514,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" @@ -21789,7 +21641,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -22121,7 +21973,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -22134,7 +21986,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -22147,7 +21999,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -22157,7 +22009,7 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "tr46": "^5.0.0", @@ -22444,7 +22296,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -22485,7 +22337,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18" @@ -22495,7 +22347,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/y18n": { diff --git a/package.json b/package.json index 1c4c82fa5ace..c641df69ae88 100644 --- a/package.json +++ b/package.json @@ -49,14 +49,12 @@ "diff-match-patch-typescript": "1.1.0", "dompurify": "3.1.6", "export-to-csv": "1.4.0", - "fabric": "^6.4.2", "fast-json-patch": "3.1.1", "franc-min": "6.2.0", "html-diff-ts": "1.4.2", "interactjs": "1.10.27", "ismobilejs-es5": "0.0.1", "js-video-url-parser": "0.5.1", - "jspdf": "^2.5.1", "jszip": "3.10.1", "lodash-es": "4.17.21", "mobile-drag-drop": "3.0.0-rc.0", @@ -64,6 +62,7 @@ "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", + "pdf-lib": "^1.17.1", "pdfjs-dist": "4.6.82", "posthog-js": "1.161.3", "rxjs": "7.8.1", diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 9705401c6141..7b60f0c30fb9 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -13,10 +13,9 @@ import { Course } from 'app/entities/course.model'; import { HttpErrorResponse } from '@angular/common/http'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { faFileImport, faSave, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons'; -import { jsPDF } from 'jspdf'; import dayjs from 'dayjs/esm'; import { objectToJsonBlob } from 'app/utils/blob-util'; -import { Image } from 'fabric'; +import { PDFDocument } from 'pdf-lib'; type NavigationDirection = 'next' | 'prev'; @@ -33,7 +32,6 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { @ViewChild('fileInput', { static: false }) fileInput: ElementRef; readonly DEFAULT_SLIDE_WIDTH = 250; - readonly DEFAULT_GENERATED_SLIDE_FORMAT = [1920, 1080]; //Represents 16:9 aspect ratio course?: Course; attachment?: Attachment; attachmentUnit?: AttachmentUnit; @@ -45,6 +43,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { selectedPages: Set = new Set(); isPdfLoading = false; attachmentToBeEdited?: Attachment; + currentPdfBlob: Blob | null = null; dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); @@ -65,16 +64,22 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { ngOnInit() { this.route.data.subscribe((data) => { this.course = data.course; + + const handleBlob = (blob: Blob) => { + this.currentPdfBlob = blob; + this.loadOrAppendPdf(URL.createObjectURL(blob)); + }; + if ('attachment' in data) { this.attachment = data.attachment; this.attachmentSub = this.attachmentService.getAttachmentFile(this.course!.id!, this.attachment!.id!).subscribe({ - next: (blob: Blob) => this.loadOrAppendPdf(URL.createObjectURL(blob)), + next: (blob: Blob) => handleBlob(blob), error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } else if ('attachmentUnit' in data) { this.attachmentUnit = data.attachmentUnit; this.attachmentUnitSub = this.attachmentUnitService.getAttachmentFile(this.course!.id!, this.attachmentUnit!.id!).subscribe({ - next: (blob: Blob) => this.loadOrAppendPdf(URL.createObjectURL(blob)), + next: (blob: Blob) => handleBlob(blob), error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } @@ -116,29 +121,27 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @returns A promise that resolves when the PDF is loaded. */ async loadOrAppendPdf(fileUrl: string, append = false): Promise { + this.pdfContainer.nativeElement.querySelectorAll('.pdf-canvas-container').forEach((canvas) => canvas.remove()); + this.totalPages = 0; this.isPdfLoading = true; try { const loadingTask = PDFJS.getDocument(fileUrl); const pdf = await loadingTask.promise; - const numPages = pdf.numPages; - const initialPageCount = this.totalPages; + this.totalPages = pdf.numPages; - for (let i = 1; i <= numPages; i++) { + for (let i = 1; i <= this.totalPages; i++) { const page = await pdf.getPage(i); const viewport = page.getViewport({ scale: 2 }); - const canvas = this.createCanvas(viewport, initialPageCount + i); + const canvas = this.createCanvas(viewport, i); const context = canvas.getContext('2d'); await page.render({ canvasContext: context!, viewport }).promise; - const canvasContainer = this.createCanvasContainer(canvas, initialPageCount + i); + const canvasContainer = this.createCanvasContainer(canvas, i); this.pdfContainer.nativeElement.appendChild(canvasContainer); } - this.totalPages += numPages; - URL.revokeObjectURL(fileUrl); - if (append) { - setTimeout(() => this.scrollToBottom(), 100); + this.scrollToBottom(); } } catch (error) { onError(this.alertService, error); @@ -405,17 +408,30 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { /** * Deletes selected slides from the PDF viewer. */ - deleteSelectedSlides() { - this.selectedPages.forEach((page) => { - const pageElement = this.pdfContainer.nativeElement.querySelector(`#pdf-page-${page}`); - if (pageElement) { - this.pdfContainer.nativeElement.removeChild(pageElement); - } - }); - this.totalPages -= this.selectedPages.size; - this.updatePageIDs(); - this.selectedPages.clear(); - this.dialogErrorSource.next(''); + async deleteSelectedSlides() { + this.isPdfLoading = true; + try { + const existingPdfBytes = await this.currentPdfBlob!.arrayBuffer(); + const pdfDoc = await PDFDocument.load(existingPdfBytes); + + const pagesToDelete = Array.from(this.selectedPages) + .map((page) => page - 1) + .sort((a, b) => b - a); + pagesToDelete.forEach((pageIndex) => { + pdfDoc.removePage(pageIndex); + }); + + const pdfBytes = await pdfDoc.save(); + this.currentPdfBlob = new Blob([pdfBytes], { type: 'application/pdf' }); + this.selectedPages.clear(); + + this.loadOrAppendPdf(URL.createObjectURL(this.currentPdfBlob), false); + this.dialogErrorSource.next(''); + } catch (error) { + this.alertService.error('Failed to delete selected pages.', { error: error.message }); + } finally { + this.isPdfLoading = false; + } } /** @@ -429,11 +445,29 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * Adds a selected PDF file at the end of the current PDF document. * @param event - The event containing the file input. */ - mergePDF(event: Event): void { + async mergePDF(event: Event): Promise { const file = (event.target as HTMLInputElement).files?.[0]; - if (file) { - const fileUrl = URL.createObjectURL(file); - this.loadOrAppendPdf(fileUrl, true); + + this.isPdfLoading = true; + try { + const newPdfBytes = await file!.arrayBuffer(); + const existingPdfBytes = await this.currentPdfBlob!.arrayBuffer(); + const existingPdfDoc = await PDFDocument.load(existingPdfBytes); + const newPdfDoc = await PDFDocument.load(newPdfBytes); + + const copiedPages = await existingPdfDoc.copyPages(newPdfDoc, newPdfDoc.getPageIndices()); + copiedPages.forEach((page) => existingPdfDoc.addPage(page)); + + const mergedPdfBytes = await existingPdfDoc.save(); + this.currentPdfBlob = new Blob([mergedPdfBytes], { type: 'application/pdf' }); + + this.selectedPages.clear(); + + this.loadOrAppendPdf(URL.createObjectURL(this.currentPdfBlob), true); + } catch (error) { + this.alertService.error('Failed to merge PDFs.', { error: error.message }); + } finally { + this.isPdfLoading = false; } } @@ -454,48 +488,15 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { }); } - generatePdfFromCanvases() { - const doc = new jsPDF({ - orientation: 'landscape', - unit: 'px', - format: this.DEFAULT_GENERATED_SLIDE_FORMAT, - compress: true, - }); - - const canvasElements = this.pdfContainer.nativeElement.querySelectorAll('canvas'); - const scaleFactor = 0.5; - - Array.from(canvasElements).forEach((canvas, index) => { - if (index > 0) doc.addPage(); - - const fabricImage = new Image(canvas, { - crossOrigin: 'anonymous', - }); - - const imgData = fabricImage.toDataURL({ - format: 'jpeg', - quality: scaleFactor, - }); - - const imgProps = doc.getImageProperties(imgData); - const pdfWidth = doc.internal.pageSize.getWidth(); - const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width; - doc.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight, undefined, 'FAST'); - }); - - return doc.output('blob'); - } - updateAttachmentWithFile(): void { - const pdfBlob = this.generatePdfFromCanvases(); - const pdfFile = new File([pdfBlob], 'updatedAttachment.pdf', { type: 'application/pdf' }); + const pdfFile = new File([this.currentPdfBlob!], 'updatedAttachment.pdf', { type: 'application/pdf' }); if (this.attachment) { this.attachmentToBeEdited = this.attachment; this.attachmentToBeEdited!.version!++; - this.attachmentToBeEdited!.uploadDate = dayjs(); + this.attachmentToBeEdited.uploadDate = dayjs(); - this.attachmentService.update(this.attachmentToBeEdited!.id!, this.attachmentToBeEdited!, pdfFile).subscribe({ + this.attachmentService.update(this.attachmentToBeEdited!.id!, this.attachmentToBeEdited, pdfFile).subscribe({ next: () => { this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); }, From e1405529a223b3e951c0da82ab4841e58f33f7f5 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 29 Sep 2024 22:42:26 +0200 Subject: [PATCH 087/125] RabbitAI changes --- .../pdf-preview/pdf-preview.component.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 7b60f0c30fb9..c6f45d2c0269 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -65,27 +65,27 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.route.data.subscribe((data) => { this.course = data.course; - const handleBlob = (blob: Blob) => { - this.currentPdfBlob = blob; - this.loadOrAppendPdf(URL.createObjectURL(blob)); - }; - if ('attachment' in data) { this.attachment = data.attachment; this.attachmentSub = this.attachmentService.getAttachmentFile(this.course!.id!, this.attachment!.id!).subscribe({ - next: (blob: Blob) => handleBlob(blob), + next: (blob: Blob) => this.handleBlob(blob), error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } else if ('attachmentUnit' in data) { this.attachmentUnit = data.attachmentUnit; this.attachmentUnitSub = this.attachmentUnitService.getAttachmentFile(this.course!.id!, this.attachmentUnit!.id!).subscribe({ - next: (blob: Blob) => handleBlob(blob), + next: (blob: Blob) => this.handleBlob(blob), error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } }); } + handleBlob(blob: Blob): void { + this.currentPdfBlob = blob; + this.loadOrAppendPdf(URL.createObjectURL(blob)); + } + ngOnDestroy() { this.attachmentSub?.unsubscribe(); this.attachmentUnitSub?.unsubscribe(); @@ -264,9 +264,9 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { }; /** - * Displays a canvas in an enlarged view for detailed examination. - * @param originalCanvas The original canvas element displaying the page. - */ + * Displays the selected PDF page in an enlarged view for detailed examination. + * @param originalCanvas - The original canvas element of the PDF page to be enlarged. + * */ displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { this.isEnlargedView = true; this.currentPage = Number(originalCanvas.id); @@ -330,10 +330,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { redrawCanvas(originalCanvas: HTMLCanvasElement): void { const enlargedCanvas = this.enlargedCanvas.nativeElement; const context = enlargedCanvas.getContext('2d'); - if (context) { - context.clearRect(0, 0, enlargedCanvas.width, enlargedCanvas.height); - context.drawImage(originalCanvas, 0, 0, enlargedCanvas.width, enlargedCanvas.height); - } + context!.clearRect(0, 0, enlargedCanvas.width, enlargedCanvas.height); + context!.drawImage(originalCanvas, 0, 0, enlargedCanvas.width, enlargedCanvas.height); } /** From 356d45ac41cf0c17037a5ae499ab1915b9034592 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 29 Sep 2024 22:50:27 +0200 Subject: [PATCH 088/125] Add translations --- .../app/lecture/pdf-preview/pdf-preview.component.ts | 10 ++++++++-- src/main/webapp/i18n/de/lecture.json | 5 ++++- src/main/webapp/i18n/en/lecture.json | 5 ++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index c6f45d2c0269..dcc8909829d9 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -16,6 +16,7 @@ import { faFileImport, faSave, faTimes, faTrash } from '@fortawesome/free-solid- import dayjs from 'dayjs/esm'; import { objectToJsonBlob } from 'app/utils/blob-util'; import { PDFDocument } from 'pdf-lib'; +import { MAX_FILE_SIZE } from 'app/shared/constants/input.constants'; type NavigationDirection = 'next' | 'prev'; @@ -426,7 +427,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.loadOrAppendPdf(URL.createObjectURL(this.currentPdfBlob), false); this.dialogErrorSource.next(''); } catch (error) { - this.alertService.error('Failed to delete selected pages.', { error: error.message }); + this.alertService.error('artemisApp.attachment.pdfPreview.pageDeleteError', { error: error.message }); } finally { this.isPdfLoading = false; } @@ -463,7 +464,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.loadOrAppendPdf(URL.createObjectURL(this.currentPdfBlob), true); } catch (error) { - this.alertService.error('Failed to merge PDFs.', { error: error.message }); + this.alertService.error('artemisApp.attachment.pdfPreview.mergeFailedError', { error: error.message }); } finally { this.isPdfLoading = false; } @@ -489,6 +490,11 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { updateAttachmentWithFile(): void { const pdfFile = new File([this.currentPdfBlob!], 'updatedAttachment.pdf', { type: 'application/pdf' }); + if (pdfFile.size > MAX_FILE_SIZE) { + this.alertService.error('artemisApp.attachment.pdfPreview.fileSizeError'); + return; + } + if (this.attachment) { this.attachmentToBeEdited = this.attachment; this.attachmentToBeEdited!.version!++; diff --git a/src/main/webapp/i18n/de/lecture.json b/src/main/webapp/i18n/de/lecture.json index cf10c2c6a291..f6829490efb2 100644 --- a/src/main/webapp/i18n/de/lecture.json +++ b/src/main/webapp/i18n/de/lecture.json @@ -104,7 +104,10 @@ "removePageButton": "Entferne Seiten", "appendFileButton": "Anhänge Datei", "attachmentUpdateSuccess": "Anhang erfolgreich aktualisiert.", - "attachmentUpdateError": "Fehler beim Aktualisieren des Anhangs: {{error}}" + "attachmentUpdateError": "Fehler beim Aktualisieren des Anhangs: {{error}}", + "mergeFailedError": "Fehler beim Zusammenführen der Dateien: {{error}}", + "pageDeleteError": "Fehler beim Löschen der Seiten: {{error}}", + "fileSizeError": "Die Datei ist zu groß. Die maximale Dateigröße beträgt 20MB." } } } diff --git a/src/main/webapp/i18n/en/lecture.json b/src/main/webapp/i18n/en/lecture.json index 2c629bb2f381..5a3d6ccee41f 100644 --- a/src/main/webapp/i18n/en/lecture.json +++ b/src/main/webapp/i18n/en/lecture.json @@ -104,7 +104,10 @@ "removePageButton": "Remove Page(s)", "appendFileButton": "Append File", "attachmentUpdateSuccess": "Attachment updated successfully.", - "attachmentUpdateError": "Failed to update attachment: {{error}}" + "attachmentUpdateError": "Failed to update attachment: {{error}}", + "mergeFailedError": "Failed to merge files: {{error}}", + "pageDeleteError": "Failed to delete pages: {{error}}", + "fileSizeError": "The file size exceeds the limit of 20MB." } } } From 92fb4eaf259c0d3f123c416c1aa3649a384eac50 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 30 Sep 2024 19:43:48 +0200 Subject: [PATCH 089/125] Fix client tests --- .../pdf-preview/pdf-preview.component.ts | 7 +- .../lecture/pdf-preview.component.spec.ts | 191 ++++++++---------- 2 files changed, 91 insertions(+), 107 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index dcc8909829d9..f66daf895f82 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -13,9 +13,9 @@ import { Course } from 'app/entities/course.model'; import { HttpErrorResponse } from '@angular/common/http'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { faFileImport, faSave, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { PDFDocument } from 'pdf-lib'; import dayjs from 'dayjs/esm'; import { objectToJsonBlob } from 'app/utils/blob-util'; -import { PDFDocument } from 'pdf-lib'; import { MAX_FILE_SIZE } from 'app/shared/constants/input.constants'; type NavigationDirection = 'next' | 'prev'; @@ -84,7 +84,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { handleBlob(blob: Blob): void { this.currentPdfBlob = blob; - this.loadOrAppendPdf(URL.createObjectURL(blob)); + const objectUrl = URL.createObjectURL(blob); + this.loadOrAppendPdf(objectUrl).then(() => URL.revokeObjectURL(objectUrl)); } ngOnDestroy() { @@ -172,7 +173,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param pageIndex The index of the page within the PDF document. * @returns A new HTMLCanvasElement configured for the PDF page. */ - private createCanvas(viewport: PDFJS.PageViewport, pageIndex: number): HTMLCanvasElement { + createCanvas(viewport: PDFJS.PageViewport, pageIndex: number): HTMLCanvasElement { const canvas = document.createElement('canvas'); canvas.id = `${pageIndex}`; /* Canvas styling is predefined because Canvas tags do not support CSS classes diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index 68f7f189a80c..34b711d0a9a6 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -1,4 +1,31 @@ import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; +import { MAX_FILE_SIZE } from 'app/shared/constants/input.constants'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, throwError } from 'rxjs'; +import { AttachmentService } from 'app/lecture/attachment.service'; +import { AttachmentUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service'; +import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component'; +import { ElementRef } from '@angular/core'; +import { AlertService } from 'app/core/util/alert.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import { TranslateService } from '@ngx-translate/core'; + +jest.mock('pdf-lib', () => { + const originalModule = jest.requireActual('pdf-lib'); + + return { + ...originalModule, + PDFDocument: { + ...originalModule.PDFDocument, + load: jest.fn(), + prototype: { + removePage: jest.fn(), + save: jest.fn(), + }, + }, + }; +}); jest.mock('pdfjs-dist', () => { return { @@ -18,23 +45,6 @@ jest.mock('pdfjs-dist', () => { }; }); -jest.mock('jspdf', () => { - return { - jsPDF: jest.fn().mockImplementation(() => ({ - addPage: jest.fn(), - addImage: jest.fn(), - getImageProperties: jest.fn(() => ({ width: 1920, height: 1080 })), - internal: { - pageSize: { - getWidth: jest.fn(() => 1920), - getHeight: jest.fn(() => 1080), - }, - }, - output: jest.fn(() => new Blob(['PDF content'], { type: 'application/pdf' })), - })), - }; -}); - jest.mock('pdfjs-dist/build/pdf.worker', () => { return {}; }); @@ -49,18 +59,6 @@ function createMockEvent(target: Element, eventType = 'click'): MouseEvent { return event; } -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; -import { of, throwError } from 'rxjs'; -import { AttachmentService } from 'app/lecture/attachment.service'; -import { AttachmentUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service'; -import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component'; -import { ElementRef } from '@angular/core'; -import { AlertService } from 'app/core/util/alert.service'; -import { HttpErrorResponse } from '@angular/common/http'; -import { TranslateService } from '@ngx-translate/core'; -import dayjs from 'dayjs'; - describe('PdfPreviewComponent', () => { let component: PdfPreviewComponent; let fixture: ComponentFixture; @@ -77,11 +75,11 @@ describe('PdfPreviewComponent', () => { global.URL.createObjectURL = jest.fn().mockReturnValue('mocked_blob_url'); attachmentServiceMock = { getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), - update: jest.fn(), + update: jest.fn().mockReturnValue(of({})), }; attachmentUnitServiceMock = { getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), - update: jest.fn(), + update: jest.fn().mockReturnValue(of({})), }; routeMock = { data: of({ @@ -139,6 +137,7 @@ describe('PdfPreviewComponent', () => { mockOverlay = document.createElement('div'); mockOverlay.style.opacity = '0'; mockCanvasElement.appendChild(mockOverlay); + component.currentPdfBlob = new Blob(['dummy content'], { type: 'application/pdf' }); fixture.detectChanges(); @@ -205,15 +204,19 @@ describe('PdfPreviewComponent', () => { expect(alertServiceSpy).toHaveBeenCalledOnce(); }); - it('should load PDF and verify rendering of pages', () => { - const mockBlob = new Blob(['PDF content'], { type: 'application/pdf' }); + it('should load PDF and verify rendering of pages', async () => { + const spyCreateCanvas = jest.spyOn(component, 'createCanvas'); + const spyCreateCanvasContainer = jest.spyOn(component, 'createCanvasContainer'); + const spyAppendChild = jest.spyOn(component.pdfContainer.nativeElement, 'appendChild'); - attachmentServiceMock.getAttachmentFile.mockReturnValue(of(mockBlob)); - component.ngOnInit(); + await component.loadOrAppendPdf('fake-url'); - expect(URL.createObjectURL).toHaveBeenCalledWith(mockBlob); - expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); - expect(component.totalPages).toBeGreaterThan(0); + expect(spyCreateCanvas).toHaveBeenCalledOnce(); + expect(spyCreateCanvasContainer).toHaveBeenCalledOnce(); + expect(spyAppendChild).toHaveBeenCalledOnce(); + expect(component.totalPages).toBe(1); + expect(component.isPdfLoading).toBeFalsy(); + expect(component.fileInput.nativeElement.value).toBe(''); }); it('should navigate through pages using keyboard in enlarged view', () => { @@ -456,32 +459,6 @@ describe('PdfPreviewComponent', () => { expect(component.displayEnlargedCanvas).toHaveBeenCalledWith(mockCanvas); }); - it('should delete selected slides, update total pages, clear selected pages, and reset dialog error source', () => { - const mockContainer = document.createElement('div'); - for (let i = 1; i <= 3; i++) { - const mockPage = document.createElement('div'); - mockPage.id = `pdf-page-${i}`; - mockContainer.appendChild(mockPage); - } - component.pdfContainer = new ElementRef(mockContainer); - - component.selectedPages = new Set([1, 3]); - component.totalPages = 3; - - const updatePageIDsSpy = jest.spyOn(component, 'updatePageIDs'); - const dialogErrorSourceSpy = jest.spyOn(component.dialogErrorSource, 'next'); - - component.deleteSelectedSlides(); - - expect(component.pdfContainer.nativeElement.querySelector('#pdf-page-1')).toBeNull(); - expect(component.pdfContainer.nativeElement.querySelector('#pdf-page-3')).toBeNull(); - expect(component.pdfContainer.nativeElement.querySelector('#pdf-page-2')).not.toBeNull(); - expect(component.totalPages).toBe(1); - expect(updatePageIDsSpy).toHaveBeenCalled(); - expect(component.selectedPages.size).toBe(0); - expect(dialogErrorSourceSpy).toHaveBeenCalledWith(''); - }); - it('should trigger the file input click event', () => { const mockFileInput = document.createElement('input'); mockFileInput.type = 'file'; @@ -492,24 +469,6 @@ describe('PdfPreviewComponent', () => { expect(clickSpy).toHaveBeenCalled(); }); - it('should extract the file from the event, create an object URL, and call loadOrAppendPdf with correct arguments', () => { - const mockFile = new Blob(['PDF content'], { type: 'application/pdf' }); - const mockFileList = { - 0: mockFile, - length: 1, - item: () => mockFile, - } as unknown as FileList; - - const mockEvent = { target: { files: mockFileList } } as unknown as Event; - const createObjectURLSpy = jest.spyOn(URL, 'createObjectURL').mockReturnValue('mocked_file_url'); - const loadOrAppendPdfSpy = jest.spyOn(component, 'loadOrAppendPdf').mockResolvedValue(); - - component.mergePDF(mockEvent); - - expect(createObjectURLSpy).toHaveBeenCalledWith(mockFile); - expect(loadOrAppendPdfSpy).toHaveBeenCalledWith('mocked_file_url', true); - }); - it('should update the IDs of remaining pages after some have been removed', () => { const mockContainer = document.createElement('div'); @@ -551,38 +510,62 @@ describe('PdfPreviewComponent', () => { }); }); - it('should update attachment and show success alert', () => { - const generatePdfFromCanvasesSpy = jest.spyOn(component, 'generatePdfFromCanvases'); - const alertServiceSpy = jest.spyOn(alertServiceMock, 'success'); + it('should update attachment successfully and show success alert', () => { + component.attachment = { id: 1, version: 1 }; + component.updateAttachmentWithFile(); + + expect(attachmentServiceMock.update).toHaveBeenCalled(); + expect(alertServiceMock.success).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); + }); + + it('should not update attachment if file size exceeds the limit and show an error alert', () => { + const oversizedData = new Uint8Array(MAX_FILE_SIZE + 1).fill(0); + component.currentPdfBlob = new Blob([oversizedData], { type: 'application/pdf' }); + + component.updateAttachmentWithFile(); + + expect(attachmentServiceMock.update).not.toHaveBeenCalled(); + expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.fileSizeError'); + }); - component.attachment = { id: 1, version: 1, uploadDate: dayjs() } as any; + it('should handle errors when updating an attachment fails', () => { + attachmentServiceMock.update.mockReturnValue(throwError(() => new Error('Update failed'))); + component.attachment = { id: 1, version: 1 }; - const mockUpdateObservable = of({}); - attachmentServiceMock.update.mockReturnValue(mockUpdateObservable); component.updateAttachmentWithFile(); - expect(generatePdfFromCanvasesSpy).toHaveBeenCalled(); - expect(component.attachment!.version).toBe(2); - expect(dayjs(component.attachment!.uploadDate).isSame(dayjs(), 'day')).toBeTrue(); - expect(attachmentServiceMock.update).toHaveBeenCalledWith(1, component.attachment, expect.any(File)); - expect(alertServiceSpy).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); + expect(attachmentServiceMock.update).toHaveBeenCalled(); + expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Update failed' }); }); - it('should update attachment unit and show success alert', () => { + it('should update attachment unit successfully and show success alert', () => { component.attachment = undefined; - component.attachmentUnit = { id: 1, name: 'Chapter 1', attachment: { id: 1, version: 1, uploadDate: dayjs() }, lecture: { id: 1 } } as any; + component.attachmentUnit = { + id: 1, + lecture: { id: 1 }, + attachment: { id: 1, version: 1 }, + }; + attachmentUnitServiceMock.update.mockReturnValue(of({})); + + component.updateAttachmentWithFile(); - const generatePdfFromCanvasesSpy = jest.spyOn(component, 'generatePdfFromCanvases'); - const alertServiceSpy = jest.spyOn(alertServiceMock, 'success'); + expect(attachmentUnitServiceMock.update).toHaveBeenCalledWith(1, 1, expect.any(FormData)); + expect(alertServiceMock.success).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); + }); + + it('should handle errors when updating an attachment unit fails', () => { + component.attachment = undefined; + component.attachmentUnit = { + id: 1, + lecture: { id: 1 }, + attachment: { id: 1, version: 1 }, + }; + const errorResponse = { message: 'Update failed' }; + attachmentUnitServiceMock.update.mockReturnValue(throwError(() => errorResponse)); - const mockUpdateObservable = of({}); - attachmentUnitServiceMock.update.mockReturnValue(mockUpdateObservable); component.updateAttachmentWithFile(); - expect(generatePdfFromCanvasesSpy).toHaveBeenCalled(); - expect(component.attachmentUnit!.attachment!.version).toBe(2); - expect(dayjs(component.attachmentUnit!.attachment!.uploadDate).isSame(dayjs(), 'day')).toBeTrue(); - expect(attachmentUnitServiceMock.update).toHaveBeenCalledWith(1, component.attachmentUnit!.id, expect.any(FormData)); - expect(alertServiceSpy).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); + expect(attachmentUnitServiceMock.update).toHaveBeenCalledWith(1, 1, expect.any(FormData)); + expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Update failed' }); }); }); From 2aac23f5e5dd283d3b6caf4bacb0f5018aacd314 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 30 Sep 2024 20:26:05 +0200 Subject: [PATCH 090/125] Increase client test coverage --- .../lecture/pdf-preview.component.spec.ts | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index 34b711d0a9a6..24a96838c431 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -10,6 +10,7 @@ import { ElementRef } from '@angular/core'; import { AlertService } from 'app/core/util/alert.service'; import { HttpErrorResponse } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; +import { PDFDocument } from 'pdf-lib'; jest.mock('pdf-lib', () => { const originalModule = jest.requireActual('pdf-lib'); @@ -139,6 +140,7 @@ describe('PdfPreviewComponent', () => { mockCanvasElement.appendChild(mockOverlay); component.currentPdfBlob = new Blob(['dummy content'], { type: 'application/pdf' }); + global.URL.createObjectURL = jest.fn().mockReturnValue('blob-url'); fixture.detectChanges(); component.pdfContainer = new ElementRef(document.createElement('div')); @@ -469,6 +471,67 @@ describe('PdfPreviewComponent', () => { expect(clickSpy).toHaveBeenCalled(); }); + it('should merge PDF files correctly and update the component state', async () => { + const mockFile = new File(['new pdf'], 'test.pdf', { type: 'application/pdf' }); + mockFile.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity + const mockEvent = { target: { files: [mockFile] } }; + + const existingPdfDoc = { + copyPages: jest.fn().mockResolvedValue(['page']), + addPage: jest.fn(), + save: jest.fn().mockResolvedValue(new Uint8Array([1, 2, 3])), + }; + + const newPdfDoc = { + getPageIndices: jest.fn().mockReturnValue([0]), + }; + + PDFDocument.load = jest + .fn() + .mockImplementationOnce(() => Promise.resolve(existingPdfDoc)) + .mockImplementationOnce(() => Promise.resolve(newPdfDoc)); + + component.currentPdfBlob = new Blob(['existing pdf'], { type: 'application/pdf' }); + component.currentPdfBlob.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity + + component.selectedPages = new Set([1]); // Assume there is initially a selected page + + await component.mergePDF(mockEvent as any); + + expect(PDFDocument.load).toHaveBeenCalledTimes(2); + expect(existingPdfDoc.copyPages).toHaveBeenCalledWith(newPdfDoc, [0]); + expect(existingPdfDoc.addPage).toHaveBeenCalledOnce(); + expect(existingPdfDoc.save).toHaveBeenCalled(); + expect(component.currentPdfBlob).toBeDefined(); + expect(component.selectedPages.size).toBe(0); + expect(component.isPdfLoading).toBeFalsy(); + expect(URL.createObjectURL).toHaveBeenCalledWith(new Blob([new Uint8Array([1, 2, 3])], { type: 'application/pdf' })); + }); + + it('should handle errors when merging PDFs fails', async () => { + const mockFile = new File(['new pdf'], 'test.pdf', { type: 'application/pdf' }); + + // Mock the arrayBuffer method for the file object + mockFile.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity + + const mockEvent = { target: { files: [mockFile] } }; + const error = new Error('Error loading PDF'); + + component.currentPdfBlob = new Blob(['existing pdf'], { type: 'application/pdf' }); + component.currentPdfBlob.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simp + + // Mock PDFDocument.load to throw an error on the first call + PDFDocument.load = jest + .fn() + .mockImplementationOnce(() => Promise.reject(error)) // First call throws an error + .mockImplementationOnce(() => Promise.resolve({})); // Second call (not actually needed here) + + await component.mergePDF(mockEvent as any); + + expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.mergeFailedError', { error: error.message }); + expect(component.isPdfLoading).toBeFalsy(); + }); + it('should update the IDs of remaining pages after some have been removed', () => { const mockContainer = document.createElement('div'); @@ -568,4 +631,72 @@ describe('PdfPreviewComponent', () => { expect(attachmentUnitServiceMock.update).toHaveBeenCalledWith(1, 1, expect.any(FormData)); expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Update failed' }); }); + + it('should delete selected slides and update the PDF', async () => { + // Mock the PDFDocument and related methods + const existingPdfDoc = { + removePage: jest.fn(), + save: jest.fn().mockResolvedValue(new Uint8Array([1, 2, 3])), + }; + + // Mock PDFDocument.load to return the existing PDF document + PDFDocument.load = jest.fn().mockResolvedValue(existingPdfDoc); + + // Mock the arrayBuffer method for the current PDF Blob + const mockArrayBuffer = new ArrayBuffer(8); + component.currentPdfBlob = new Blob(['existing pdf'], { type: 'application/pdf' }); + component.currentPdfBlob.arrayBuffer = jest.fn().mockResolvedValue(mockArrayBuffer); + + // Set up selected pages for deletion + component.selectedPages = new Set([1, 2]); // Pages 1 and 2 selected + + // Spy on necessary methods + const loadOrAppendPdfSpy = jest.spyOn(component, 'loadOrAppendPdf'); + const alertServiceErrorSpy = jest.spyOn(alertServiceMock, 'error'); + + // Call the method + await component.deleteSelectedSlides(); + + // Verify that the PDFDocument.load was called with the correct arguments + expect(PDFDocument.load).toHaveBeenCalledWith(mockArrayBuffer); + + // Verify that the pages were removed in reverse order (2, then 1) + expect(existingPdfDoc.removePage).toHaveBeenCalledWith(1); + expect(existingPdfDoc.removePage).toHaveBeenCalledWith(0); + expect(existingPdfDoc.removePage).toHaveBeenCalledTimes(2); + + // Verify that the PDF was saved + expect(existingPdfDoc.save).toHaveBeenCalled(); + + // Verify that the new Blob was created and passed to loadOrAppendPdf + expect(component.currentPdfBlob).toEqual(new Blob([new Uint8Array([1, 2, 3])], { type: 'application/pdf' })); + expect(loadOrAppendPdfSpy).toHaveBeenCalledWith(URL.createObjectURL(component.currentPdfBlob), false); + + // Ensure that the selectedPages set was cleared + expect(component.selectedPages.size).toBe(0); + + // Ensure that no error alert was triggered + expect(alertServiceErrorSpy).not.toHaveBeenCalled(); + + // Verify that the loading state is set to false after the operation + expect(component.isPdfLoading).toBeFalse(); + }); + + it('should handle errors when deleting slides', async () => { + // Mock the arrayBuffer method for the current PDF Blob + component.currentPdfBlob = new Blob(['existing pdf'], { type: 'application/pdf' }); + component.currentPdfBlob.arrayBuffer = jest.fn().mockRejectedValue(new Error('Failed to load PDF')); + + // Spy on the alert service + const alertServiceErrorSpy = jest.spyOn(alertServiceMock, 'error'); + + // Call the method + await component.deleteSelectedSlides(); + + // Ensure the alert service was called with the correct error message + expect(alertServiceErrorSpy).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.pageDeleteError', { error: 'Failed to load PDF' }); + + // Verify that the loading state is set to false after the operation + expect(component.isPdfLoading).toBeFalse(); + }); }); From 82a7634e5bd5fe506867f7e5ef6a95a3c9daa054 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 30 Sep 2024 20:48:17 +0200 Subject: [PATCH 091/125] RabbitAI changes --- .../pdf-preview/pdf-preview.component.ts | 10 +++-- .../lecture/pdf-preview.component.spec.ts | 41 +++++++------------ 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index f66daf895f82..71ae69e68dd4 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -425,8 +425,11 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.currentPdfBlob = new Blob([pdfBytes], { type: 'application/pdf' }); this.selectedPages.clear(); - this.loadOrAppendPdf(URL.createObjectURL(this.currentPdfBlob), false); - this.dialogErrorSource.next(''); + const objectUrl = URL.createObjectURL(this.currentPdfBlob!); + await this.loadOrAppendPdf(objectUrl, false).then(() => { + this.dialogErrorSource.next(''); + }); + URL.revokeObjectURL(objectUrl); } catch (error) { this.alertService.error('artemisApp.attachment.pdfPreview.pageDeleteError', { error: error.message }); } finally { @@ -463,7 +466,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.selectedPages.clear(); - this.loadOrAppendPdf(URL.createObjectURL(this.currentPdfBlob), true); + const objectUrl = URL.createObjectURL(this.currentPdfBlob!); + await this.loadOrAppendPdf(objectUrl, true).then(() => URL.revokeObjectURL(objectUrl)); } catch (error) { this.alertService.error('artemisApp.attachment.pdfPreview.mergeFailedError', { error: error.message }); } finally { diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index 24a96838c431..5ca822c56eef 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -20,6 +20,7 @@ jest.mock('pdf-lib', () => { PDFDocument: { ...originalModule.PDFDocument, load: jest.fn(), + create: jest.fn(), prototype: { removePage: jest.fn(), save: jest.fn(), @@ -217,7 +218,7 @@ describe('PdfPreviewComponent', () => { expect(spyCreateCanvasContainer).toHaveBeenCalledOnce(); expect(spyAppendChild).toHaveBeenCalledOnce(); expect(component.totalPages).toBe(1); - expect(component.isPdfLoading).toBeFalsy(); + expect(component.isPdfLoading).toBeFalse(); expect(component.fileInput.nativeElement.value).toBe(''); }); @@ -326,7 +327,7 @@ describe('PdfPreviewComponent', () => { const canvas = document.createElement('canvas'); const pdfContainer = document.createElement('div'); - pdfContainer.className = 'pdf-canvas-container'; + pdfContainer.classList.add('pdf-canvas-container'); pdfContainer.appendChild(canvas); component.pdfContainer = { nativeElement: pdfContainer, @@ -424,7 +425,7 @@ describe('PdfPreviewComponent', () => { const container = component.createCanvasContainer(mockCanvas, 1); expect(container.tagName).toBe('DIV'); - expect(container.classList.contains('pdf-canvas-container')).toBeTruthy(); + expect(container.classList.contains('pdf-canvas-container')).toBeTrue(); expect(container.style.position).toBe('relative'); expect(container.style.display).toBe('inline-block'); expect(container.style.width).toBe('600px'); @@ -504,7 +505,7 @@ describe('PdfPreviewComponent', () => { expect(existingPdfDoc.save).toHaveBeenCalled(); expect(component.currentPdfBlob).toBeDefined(); expect(component.selectedPages.size).toBe(0); - expect(component.isPdfLoading).toBeFalsy(); + expect(component.isPdfLoading).toBeFalse(); expect(URL.createObjectURL).toHaveBeenCalledWith(new Blob([new Uint8Array([1, 2, 3])], { type: 'application/pdf' })); }); @@ -529,7 +530,7 @@ describe('PdfPreviewComponent', () => { await component.mergePDF(mockEvent as any); expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.mergeFailedError', { error: error.message }); - expect(component.isPdfLoading).toBeFalsy(); + expect(component.isPdfLoading).toBeFalse(); }); it('should update the IDs of remaining pages after some have been removed', () => { @@ -571,6 +572,9 @@ describe('PdfPreviewComponent', () => { expect(overlay!.innerHTML).toBe(`${pageIndex}`); expect(checkbox!.id).toBe(String(pageIndex)); }); + while (mockContainer.firstChild) { + mockContainer.removeChild(mockContainer.firstChild); + } }); it('should update attachment successfully and show success alert', () => { @@ -633,52 +637,37 @@ describe('PdfPreviewComponent', () => { }); it('should delete selected slides and update the PDF', async () => { - // Mock the PDFDocument and related methods const existingPdfDoc = { removePage: jest.fn(), save: jest.fn().mockResolvedValue(new Uint8Array([1, 2, 3])), }; - // Mock PDFDocument.load to return the existing PDF document PDFDocument.load = jest.fn().mockResolvedValue(existingPdfDoc); - - // Mock the arrayBuffer method for the current PDF Blob const mockArrayBuffer = new ArrayBuffer(8); component.currentPdfBlob = new Blob(['existing pdf'], { type: 'application/pdf' }); component.currentPdfBlob.arrayBuffer = jest.fn().mockResolvedValue(mockArrayBuffer); - // Set up selected pages for deletion + const objectUrl = 'blob-url'; + global.URL.createObjectURL = jest.fn().mockReturnValue(objectUrl); + global.URL.revokeObjectURL = jest.fn(); + component.selectedPages = new Set([1, 2]); // Pages 1 and 2 selected - // Spy on necessary methods const loadOrAppendPdfSpy = jest.spyOn(component, 'loadOrAppendPdf'); const alertServiceErrorSpy = jest.spyOn(alertServiceMock, 'error'); - // Call the method await component.deleteSelectedSlides(); - // Verify that the PDFDocument.load was called with the correct arguments expect(PDFDocument.load).toHaveBeenCalledWith(mockArrayBuffer); - - // Verify that the pages were removed in reverse order (2, then 1) expect(existingPdfDoc.removePage).toHaveBeenCalledWith(1); expect(existingPdfDoc.removePage).toHaveBeenCalledWith(0); expect(existingPdfDoc.removePage).toHaveBeenCalledTimes(2); - - // Verify that the PDF was saved expect(existingPdfDoc.save).toHaveBeenCalled(); - - // Verify that the new Blob was created and passed to loadOrAppendPdf expect(component.currentPdfBlob).toEqual(new Blob([new Uint8Array([1, 2, 3])], { type: 'application/pdf' })); - expect(loadOrAppendPdfSpy).toHaveBeenCalledWith(URL.createObjectURL(component.currentPdfBlob), false); - - // Ensure that the selectedPages set was cleared + expect(loadOrAppendPdfSpy).toHaveBeenCalledWith(objectUrl, false); expect(component.selectedPages.size).toBe(0); - - // Ensure that no error alert was triggered expect(alertServiceErrorSpy).not.toHaveBeenCalled(); - - // Verify that the loading state is set to false after the operation + expect(URL.revokeObjectURL).toHaveBeenCalledWith(objectUrl); expect(component.isPdfLoading).toBeFalse(); }); From 5b10143ac316343009d2d6eae9190a56d0bc9d2a Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Thu, 3 Oct 2024 14:05:49 +0200 Subject: [PATCH 092/125] Update delete pages question --- .../app/lecture/pdf-preview/pdf-preview.component.html | 1 + src/main/webapp/i18n/de/lecture.json | 5 +++-- src/main/webapp/i18n/en/lecture.json | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index e6436d7897bc..6723b2d472d3 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -23,6 +23,7 @@

jhiDeleteButton [renderButtonStyle]="false" [renderButtonText]="false" + [deleteQuestion]="'artemisApp.attachment.pdfPreview.deletePagesQuestion'" (delete)="deleteSelectedSlides()" [dialogError]="dialogError$" [disabled]="isPdfLoading || selectedPages.size === 0" diff --git a/src/main/webapp/i18n/de/lecture.json b/src/main/webapp/i18n/de/lecture.json index f6829490efb2..260c62b4491d 100644 --- a/src/main/webapp/i18n/de/lecture.json +++ b/src/main/webapp/i18n/de/lecture.json @@ -102,12 +102,13 @@ "attachmentIDError": "Ungültiger Anhang oder ungültige Anhangs-ID.", "attachmentUnitIDError": "Ungültige Dateieinheit oder ungültige Dateieinheits-ID.", "removePageButton": "Entferne Seiten", - "appendFileButton": "Anhänge Datei", + "appendFileButton": "Datei anhängen", "attachmentUpdateSuccess": "Anhang erfolgreich aktualisiert.", "attachmentUpdateError": "Fehler beim Aktualisieren des Anhangs: {{error}}", "mergeFailedError": "Fehler beim Zusammenführen der Dateien: {{error}}", "pageDeleteError": "Fehler beim Löschen der Seiten: {{error}}", - "fileSizeError": "Die Datei ist zu groß. Die maximale Dateigröße beträgt 20MB." + "fileSizeError": "Die Datei ist zu groß. Die maximale Dateigröße beträgt 20MB.", + "deletePagesQuestion": "Möchten Sie die ausgewählten Seiten wirklich löschen?" } } } diff --git a/src/main/webapp/i18n/en/lecture.json b/src/main/webapp/i18n/en/lecture.json index 5a3d6ccee41f..7ee8443f310e 100644 --- a/src/main/webapp/i18n/en/lecture.json +++ b/src/main/webapp/i18n/en/lecture.json @@ -107,7 +107,8 @@ "attachmentUpdateError": "Failed to update attachment: {{error}}", "mergeFailedError": "Failed to merge files: {{error}}", "pageDeleteError": "Failed to delete pages: {{error}}", - "fileSizeError": "The file size exceeds the limit of 20MB." + "fileSizeError": "The file size exceeds the limit of 20MB.", + "deletePagesQuestion": "Are you sure you want to delete the selected pages?" } } } From dc3e974aded9cdceaed396f500562498442c1bec Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Thu, 3 Oct 2024 14:49:15 +0200 Subject: [PATCH 093/125] Change Append File to Append PDF --- .../app/lecture/pdf-preview/pdf-preview.component.html | 6 +++--- src/main/webapp/i18n/de/lecture.json | 4 ++-- src/main/webapp/i18n/en/lecture.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 6723b2d472d3..ff557a532552 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -34,7 +34,7 @@

@@ -44,10 +44,10 @@

@if (currentPage !== 1) { - + } @if (currentPage !== totalPages) { - + }
{{ currentPage }}

diff --git a/src/main/webapp/i18n/de/lecture.json b/src/main/webapp/i18n/de/lecture.json index 260c62b4491d..7b326196a44a 100644 --- a/src/main/webapp/i18n/de/lecture.json +++ b/src/main/webapp/i18n/de/lecture.json @@ -102,13 +102,13 @@ "attachmentIDError": "Ungültiger Anhang oder ungültige Anhangs-ID.", "attachmentUnitIDError": "Ungültige Dateieinheit oder ungültige Dateieinheits-ID.", "removePageButton": "Entferne Seiten", - "appendFileButton": "Datei anhängen", + "appendPDFButton": "PDF anhängen", "attachmentUpdateSuccess": "Anhang erfolgreich aktualisiert.", "attachmentUpdateError": "Fehler beim Aktualisieren des Anhangs: {{error}}", "mergeFailedError": "Fehler beim Zusammenführen der Dateien: {{error}}", "pageDeleteError": "Fehler beim Löschen der Seiten: {{error}}", "fileSizeError": "Die Datei ist zu groß. Die maximale Dateigröße beträgt 20MB.", - "deletePagesQuestion": "Möchten Sie die ausgewählten Seiten wirklich löschen?" + "deletePagesQuestion": "Möchtest du die ausgewählten Seiten wirklich löschen?" } } } diff --git a/src/main/webapp/i18n/en/lecture.json b/src/main/webapp/i18n/en/lecture.json index 7ee8443f310e..1d6bdfae33d7 100644 --- a/src/main/webapp/i18n/en/lecture.json +++ b/src/main/webapp/i18n/en/lecture.json @@ -102,7 +102,7 @@ "attachmentIDError": "Invalid Attachment or Attachment ID.", "attachmentUnitIDError": "Invalid Attachment Unit or Attachment Unit ID.", "removePageButton": "Remove Page(s)", - "appendFileButton": "Append File", + "appendPDFButton": "Append PDF", "attachmentUpdateSuccess": "Attachment updated successfully.", "attachmentUpdateError": "Failed to update attachment: {{error}}", "mergeFailedError": "Failed to merge files: {{error}}", From 0ac24340a5f38f4b82c30d571c0b8e2e2a47ca8a Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Fri, 4 Oct 2024 14:47:44 +0200 Subject: [PATCH 094/125] Return to previous page after saving --- .../webapp/app/lecture/pdf-preview/pdf-preview.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 71ae69e68dd4..88485e4418bc 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -1,5 +1,5 @@ import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { AttachmentService } from 'app/lecture/attachment.service'; import * as PDFJS from 'pdfjs-dist'; import 'pdfjs-dist/build/pdf.worker'; @@ -60,6 +60,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { private attachmentService: AttachmentService, private attachmentUnitService: AttachmentUnitService, private alertService: AlertService, + private router: Router, ) {} ngOnInit() { @@ -508,6 +509,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.attachmentService.update(this.attachmentToBeEdited!.id!, this.attachmentToBeEdited, pdfFile).subscribe({ next: () => { this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); + this.router.navigate(['course-management', this.course?.id, 'lectures', this.attachment!.lecture!.id, 'attachments']); }, error: (error) => { this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); @@ -526,6 +528,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.attachmentUnitService.update(this.attachmentUnit!.lecture!.id!, this.attachmentUnit!.id!, formData).subscribe({ next: () => { this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); + this.router.navigate(['course-management', this.course?.id, 'lectures', this.attachmentUnit!.lecture!.id, 'unit-management']); }, error: (error) => { this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); From 8de6f313af57594556dd597a15723c6987b63857 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Fri, 4 Oct 2024 16:44:21 +0200 Subject: [PATCH 095/125] Delete attachment when all pages are deleted --- .../pdf-preview/pdf-preview.component.html | 4 +- .../pdf-preview/pdf-preview.component.ts | 38 +++++++++++++++++++ src/main/webapp/i18n/de/lecture.json | 3 +- src/main/webapp/i18n/en/lecture.json | 3 +- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index ff557a532552..e54bc3157ddc 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -23,8 +23,8 @@

jhiDeleteButton [renderButtonStyle]="false" [renderButtonText]="false" - [deleteQuestion]="'artemisApp.attachment.pdfPreview.deletePagesQuestion'" - (delete)="deleteSelectedSlides()" + [deleteQuestion]="allPagesSelected() ? 'artemisApp.attachment.pdfPreview.deleteAllPagesQuestion' : 'artemisApp.attachment.pdfPreview.deletePagesQuestion'" + (delete)="allPagesSelected() ? deleteAttachmentFile() : deleteSelectedSlides()" [dialogError]="dialogError$" [disabled]="isPdfLoading || selectedPages.size === 0" > diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 88485e4418bc..33de66e3e060 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -17,6 +17,7 @@ import { PDFDocument } from 'pdf-lib'; import dayjs from 'dayjs/esm'; import { objectToJsonBlob } from 'app/utils/blob-util'; import { MAX_FILE_SIZE } from 'app/shared/constants/input.constants'; +import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; type NavigationDirection = 'next' | 'prev'; @@ -59,6 +60,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { public route: ActivatedRoute, private attachmentService: AttachmentService, private attachmentUnitService: AttachmentUnitService, + private lectureUnitService: LectureUnitService, private alertService: AlertService, private router: Router, ) {} @@ -94,6 +96,14 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.attachmentUnitSub?.unsubscribe(); } + /** + * Checks if all pages are selected. + * @returns True if the number of selected pages equals the total number of pages, otherwise false. + */ + allPagesSelected() { + return this.selectedPages.size === this.totalPages; + } + /** * Handles navigation within the PDF viewer using keyboard arrow keys. * @param event - The keyboard event captured for navigation. @@ -406,6 +416,34 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } } + /** + * Deletes the attachment file if it exists, or deletes the attachment unit if it exists. + * @returns A Promise that resolves when the deletion process is completed. + */ + async deleteAttachmentFile() { + if (this.attachment) { + this.attachmentService.delete(this.attachment.id!).subscribe({ + next: () => { + this.router.navigate(['course-management', this.course?.id, 'lectures', this.attachment!.lecture!.id, 'attachments']); + this.dialogErrorSource.next(''); + }, + error: (error) => { + this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); + }, + }); + } else if (this.attachmentUnit && this.attachmentUnit.id && this.attachmentUnit.lecture?.id) { + this.lectureUnitService.delete(this.attachmentUnit.id, this.attachmentUnit.lecture.id).subscribe({ + next: () => { + this.router.navigate(['course-management', this.course?.id, 'lectures', this.attachmentUnit!.lecture!.id, 'unit-management']); + this.dialogErrorSource.next(''); + }, + error: (error) => { + this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); + }, + }); + } + } + /** * Deletes selected slides from the PDF viewer. */ diff --git a/src/main/webapp/i18n/de/lecture.json b/src/main/webapp/i18n/de/lecture.json index 7b326196a44a..f167d9cd0857 100644 --- a/src/main/webapp/i18n/de/lecture.json +++ b/src/main/webapp/i18n/de/lecture.json @@ -108,7 +108,8 @@ "mergeFailedError": "Fehler beim Zusammenführen der Dateien: {{error}}", "pageDeleteError": "Fehler beim Löschen der Seiten: {{error}}", "fileSizeError": "Die Datei ist zu groß. Die maximale Dateigröße beträgt 20MB.", - "deletePagesQuestion": "Möchtest du die ausgewählten Seiten wirklich löschen?" + "deletePagesQuestion": "Möchtest du die ausgewählten Seiten wirklich löschen?", + "deleteAllPagesQuestion": "Möchtest du wirklich alle Seiten löschen? Dies führt zur Löschung des Anhangs." } } } diff --git a/src/main/webapp/i18n/en/lecture.json b/src/main/webapp/i18n/en/lecture.json index 1d6bdfae33d7..e3de36bf1e14 100644 --- a/src/main/webapp/i18n/en/lecture.json +++ b/src/main/webapp/i18n/en/lecture.json @@ -108,7 +108,8 @@ "mergeFailedError": "Failed to merge files: {{error}}", "pageDeleteError": "Failed to delete pages: {{error}}", "fileSizeError": "The file size exceeds the limit of 20MB.", - "deletePagesQuestion": "Are you sure you want to delete the selected pages?" + "deletePagesQuestion": "Are you sure you want to delete the selected pages?", + "deleteAllPagesQuestion": "Are you sure you want to delete all pages? This will result in the deletion of the attachment." } } } From 94ee2af07defb5c51d18f2dabaa3900a7ac7545c Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Fri, 4 Oct 2024 17:13:25 +0200 Subject: [PATCH 096/125] Disable Save if file is not changed --- .../webapp/app/lecture/pdf-preview/pdf-preview.component.html | 2 +- .../webapp/app/lecture/pdf-preview/pdf-preview.component.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index e54bc3157ddc..1b99108e0821 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -70,7 +70,7 @@

- diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 33de66e3e060..7d3b2c1978bd 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -38,6 +38,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { attachment?: Attachment; attachmentUnit?: AttachmentUnit; isEnlargedView = false; + isFileChanged = false; currentPage = 1; totalPages = 0; attachmentSub: Subscription; @@ -460,6 +461,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { pdfDoc.removePage(pageIndex); }); + this.isFileChanged = true; const pdfBytes = await pdfDoc.save(); this.currentPdfBlob = new Blob([pdfBytes], { type: 'application/pdf' }); this.selectedPages.clear(); @@ -500,6 +502,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { const copiedPages = await existingPdfDoc.copyPages(newPdfDoc, newPdfDoc.getPageIndices()); copiedPages.forEach((page) => existingPdfDoc.addPage(page)); + this.isFileChanged = true; const mergedPdfBytes = await existingPdfDoc.save(); this.currentPdfBlob = new Blob([mergedPdfBytes], { type: 'application/pdf' }); From 188d9ed734eae9160c90c9a09fc26d2edf5c159a Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Tue, 15 Oct 2024 17:27:09 +0200 Subject: [PATCH 097/125] Lazy load PDF Preview Component --- src/main/webapp/app/app-routing.module.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/webapp/app/app-routing.module.ts b/src/main/webapp/app/app-routing.module.ts index ee02e755f3a2..6ab77a7e46d0 100644 --- a/src/main/webapp/app/app-routing.module.ts +++ b/src/main/webapp/app/app-routing.module.ts @@ -129,6 +129,11 @@ const LAYOUT_ROUTES: Routes = [navbarRoute, ...errorRoute]; path: 'courses', loadChildren: () => import('./overview/courses.module').then((m) => m.ArtemisCoursesModule), }, + { + path: 'course-management/:courseId/lectures/:lectureId/attachments/:attachmentId', + pathMatch: 'full', + loadComponent: () => import('./lecture/pdf-preview/pdf-preview.component').then((m) => m.PdfPreviewComponent), + }, // ===== GRADING SYSTEM ===== { path: 'courses/:courseId/grading-system', From aeaabfde7cd933c2489048ca6f0108a715b0e413 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Tue, 15 Oct 2024 23:14:16 +0200 Subject: [PATCH 098/125] Fix client tests & RabbitAI changes --- .../pdf-preview/pdf-preview.component.html | 5 +-- .../pdf-preview/pdf-preview.component.ts | 2 +- .../lecture/pdf-preview.component.spec.ts | 34 +++++++++---------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 1b99108e0821..21802b18ae2a 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -27,12 +27,13 @@

(delete)="allPagesSelected() ? deleteAttachmentFile() : deleteSelectedSlides()" [dialogError]="dialogError$" [disabled]="isPdfLoading || selectedPages.size === 0" + aria-label="Delete selected pages" > - @@ -71,7 +72,7 @@

diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 7d3b2c1978bd..9ad3bbc87fa1 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -145,7 +145,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { for (let i = 1; i <= this.totalPages; i++) { const page = await pdf.getPage(i); - const viewport = page.getViewport({ scale: 2 }); + const viewport = page.getViewport({ scale: 1 }); const canvas = this.createCanvas(viewport, i); const context = canvas.getContext('2d'); await page.render({ canvasContext: context!, viewport }).promise; diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index 5ca822c56eef..864963dfa7f9 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -8,7 +8,7 @@ import { AttachmentUnitService } from 'app/lecture/lecture-unit/lecture-unit-man import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component'; import { ElementRef } from '@angular/core'; import { AlertService } from 'app/core/util/alert.service'; -import { HttpErrorResponse } from '@angular/common/http'; +import { HttpClientModule, HttpErrorResponse } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; import { PDFDocument } from 'pdf-lib'; @@ -97,7 +97,7 @@ describe('PdfPreviewComponent', () => { }; await TestBed.configureTestingModule({ - imports: [PdfPreviewComponent], + imports: [PdfPreviewComponent, HttpClientModule], providers: [ { provide: ActivatedRoute, useValue: routeMock }, { provide: AttachmentService, useValue: attachmentServiceMock }, @@ -183,7 +183,7 @@ describe('PdfPreviewComponent', () => { component.ngOnInit(); fixture.detectChanges(); - expect(alertServiceSpy).toHaveBeenCalledOnce(); + expect(alertServiceSpy).toHaveBeenCalled(); }); it('should handle errors and trigger alert when loading an attachment unit file fails', () => { @@ -204,7 +204,7 @@ describe('PdfPreviewComponent', () => { component.ngOnInit(); fixture.detectChanges(); - expect(alertServiceSpy).toHaveBeenCalledOnce(); + expect(alertServiceSpy).toHaveBeenCalled(); }); it('should load PDF and verify rendering of pages', async () => { @@ -214,11 +214,11 @@ describe('PdfPreviewComponent', () => { await component.loadOrAppendPdf('fake-url'); - expect(spyCreateCanvas).toHaveBeenCalledOnce(); - expect(spyCreateCanvasContainer).toHaveBeenCalledOnce(); - expect(spyAppendChild).toHaveBeenCalledOnce(); + expect(spyCreateCanvas).toHaveBeenCalled(); + expect(spyCreateCanvasContainer).toHaveBeenCalled(); + expect(spyAppendChild).toHaveBeenCalled(); expect(component.totalPages).toBe(1); - expect(component.isPdfLoading).toBeFalse(); + expect(component.isPdfLoading).toBeFalsy(); expect(component.fileInput.nativeElement.value).toBe(''); }); @@ -240,14 +240,14 @@ describe('PdfPreviewComponent', () => { it('should toggle enlarged view state', () => { const mockCanvas = document.createElement('canvas'); component.displayEnlargedCanvas(mockCanvas); - expect(component.isEnlargedView).toBeTrue(); + expect(component.isEnlargedView).toBeTruthy(); const clickEvent = new MouseEvent('click', { button: 0, }); component.closeEnlargedView(clickEvent); - expect(component.isEnlargedView).toBeFalse(); + expect(component.isEnlargedView).toBeFalsy(); }); it('should prevent scrolling when enlarged view is active', () => { @@ -352,7 +352,7 @@ describe('PdfPreviewComponent', () => { component.closeIfOutside(mockEvent); expect(closeSpy).toHaveBeenCalled(); - expect(component.isEnlargedView).toBeFalse(); + expect(component.isEnlargedView).toBeFalsy(); }); it('should not close the enlarged view if the click is on the canvas itself', () => { @@ -425,7 +425,7 @@ describe('PdfPreviewComponent', () => { const container = component.createCanvasContainer(mockCanvas, 1); expect(container.tagName).toBe('DIV'); - expect(container.classList.contains('pdf-canvas-container')).toBeTrue(); + expect(container.classList.contains('pdf-canvas-container')).toBeTruthy(); expect(container.style.position).toBe('relative'); expect(container.style.display).toBe('inline-block'); expect(container.style.width).toBe('600px'); @@ -501,11 +501,11 @@ describe('PdfPreviewComponent', () => { expect(PDFDocument.load).toHaveBeenCalledTimes(2); expect(existingPdfDoc.copyPages).toHaveBeenCalledWith(newPdfDoc, [0]); - expect(existingPdfDoc.addPage).toHaveBeenCalledOnce(); + expect(existingPdfDoc.addPage).toHaveBeenCalled(); expect(existingPdfDoc.save).toHaveBeenCalled(); expect(component.currentPdfBlob).toBeDefined(); expect(component.selectedPages.size).toBe(0); - expect(component.isPdfLoading).toBeFalse(); + expect(component.isPdfLoading).toBeFalsy(); expect(URL.createObjectURL).toHaveBeenCalledWith(new Blob([new Uint8Array([1, 2, 3])], { type: 'application/pdf' })); }); @@ -530,7 +530,7 @@ describe('PdfPreviewComponent', () => { await component.mergePDF(mockEvent as any); expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.mergeFailedError', { error: error.message }); - expect(component.isPdfLoading).toBeFalse(); + expect(component.isPdfLoading).toBeFalsy(); }); it('should update the IDs of remaining pages after some have been removed', () => { @@ -668,7 +668,7 @@ describe('PdfPreviewComponent', () => { expect(component.selectedPages.size).toBe(0); expect(alertServiceErrorSpy).not.toHaveBeenCalled(); expect(URL.revokeObjectURL).toHaveBeenCalledWith(objectUrl); - expect(component.isPdfLoading).toBeFalse(); + expect(component.isPdfLoading).toBeFalsy(); }); it('should handle errors when deleting slides', async () => { @@ -686,6 +686,6 @@ describe('PdfPreviewComponent', () => { expect(alertServiceErrorSpy).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.pageDeleteError', { error: 'Failed to load PDF' }); // Verify that the loading state is set to false after the operation - expect(component.isPdfLoading).toBeFalse(); + expect(component.isPdfLoading).toBeFalsy(); }); }); From 8f514b537693f8077f0569cfeab2f182c0907dff Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Wed, 16 Oct 2024 00:49:39 +0200 Subject: [PATCH 099/125] Adjust PDF container size for vertical PDFs --- .../pdf-preview/pdf-preview.component.scss | 1 - .../pdf-preview/pdf-preview.component.ts | 37 +++++++++++++++++-- .../lecture/pdf-preview.component.spec.ts | 21 ++++++++++- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index 5f849b06e90d..967dc25f53f8 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -3,7 +3,6 @@ display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 10px; - max-height: 60vh; height: 60vh; overflow-y: auto; border: 1px solid var(--border-color); diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 9ad3bbc87fa1..5541e8dd2aaf 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -34,6 +34,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { @ViewChild('fileInput', { static: false }) fileInput: ElementRef; readonly DEFAULT_SLIDE_WIDTH = 250; + readonly DEFAULT_SLIDE_HEIGHT = 800; course?: Course; attachment?: Attachment; attachmentUnit?: AttachmentUnit; @@ -145,7 +146,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { for (let i = 1; i <= this.totalPages; i++) { const page = await pdf.getPage(i); - const viewport = page.getViewport({ scale: 1 }); + const viewport = page.getViewport({ scale: 2 }); const canvas = this.createCanvas(viewport, i); const context = canvas.getContext('2d'); await page.render({ canvasContext: context!, viewport }).promise; @@ -277,6 +278,22 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } }; + /** + * Adjusts the size of the PDF container based on whether the enlarged view is active or not. + * If the enlarged view is active, the container's size is reduced to focus on the enlarged content. + * If the enlarged view is closed, the container returns to its original size. + * + * @param enlarge A boolean flag indicating whether to enlarge or reset the container size. + */ + adjustPdfContainerSize(enlarge: boolean): void { + const pdfContainer = this.pdfContainer.nativeElement; + if (enlarge) { + pdfContainer.style.height = '80vh'; // Larger for enlarged view + } else { + pdfContainer.style.height = '60vh'; + } + } + /** * Displays the selected PDF page in an enlarged view for detailed examination. * @param originalCanvas - The original canvas element of the PDF page to be enlarged. @@ -284,6 +301,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { this.isEnlargedView = true; this.currentPage = Number(originalCanvas.id); + this.adjustPdfContainerSize(true); this.updateEnlargedCanvas(originalCanvas); this.toggleBodyScroll(true); } @@ -316,8 +334,20 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { calculateScaleFactor(originalCanvas: HTMLCanvasElement): number { const containerWidth = this.pdfContainer.nativeElement.clientWidth; const containerHeight = this.pdfContainer.nativeElement.clientHeight; - const scaleX = containerWidth / originalCanvas.width; - const scaleY = containerHeight / originalCanvas.height; + + let scaleX, scaleY; + + if (originalCanvas.height > originalCanvas.width) { + // Vertical slide + const fixedHeight = this.DEFAULT_SLIDE_HEIGHT; + scaleY = fixedHeight / originalCanvas.height; + scaleX = containerWidth / originalCanvas.width; + } else { + // Horizontal slide + scaleX = containerWidth / originalCanvas.width; + scaleY = containerHeight / originalCanvas.height; + } + return Math.min(scaleX, scaleY); } @@ -369,6 +399,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { */ closeEnlargedView(event: MouseEvent) { this.isEnlargedView = false; + this.adjustPdfContainerSize(false); this.toggleBodyScroll(false); event.stopPropagation(); } diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index 864963dfa7f9..c416e0357296 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -368,15 +368,32 @@ describe('PdfPreviewComponent', () => { expect(closeSpy).not.toHaveBeenCalled(); }); - it('should calculate the correct scale factor based on container and canvas dimensions', () => { + it('should calculate the correct scale factor for horizontal slides', () => { + // Mock container dimensions Object.defineProperty(component.pdfContainer.nativeElement, 'clientWidth', { value: 1000, configurable: true }); Object.defineProperty(component.pdfContainer.nativeElement, 'clientHeight', { value: 800, configurable: true }); + // Mock a horizontal canvas (width > height) mockCanvasElement.width = 500; mockCanvasElement.height = 400; + const scaleFactor = component.calculateScaleFactor(mockCanvasElement); + + // Expect scale factor to be based on width (scaleX) and height (scaleY), whichever is smaller + expect(scaleFactor).toBe(2); // Min of 1000/500 (scaleX = 2) and 800/400 (scaleY = 2) + }); + it('should calculate the correct scale factor for vertical slides', () => { + Object.defineProperty(component.pdfContainer.nativeElement, 'clientWidth', { value: 1000, configurable: true }); + Object.defineProperty(component.pdfContainer.nativeElement, 'clientHeight', { value: 800, configurable: true }); + + // Mock a vertical canvas (height > width) + mockCanvasElement.width = 400; + mockCanvasElement.height = 500; const scaleFactor = component.calculateScaleFactor(mockCanvasElement); - expect(scaleFactor).toBe(2); + + // For vertical slides, scaleY is based on DEFAULT_SLIDE_HEIGHT, and scaleX is based on containerWidth + // Expect scaleY to be 800/500 = 1.6 and scaleX to be 1000/400 = 2.5 + expect(scaleFactor).toBe(1.6); // Min of 1.6 (scaleY) and 2.5 (scaleX) }); it('should resize the canvas based on the given scale factor', () => { From 0112516db154dfda635febf10e96f35827840ac2 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Wed, 16 Oct 2024 01:35:32 +0200 Subject: [PATCH 100/125] Adjust PDF container size dynamically --- .../pdf-preview/pdf-preview.component.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 5541e8dd2aaf..60d9dd96fceb 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -283,14 +283,14 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * If the enlarged view is active, the container's size is reduced to focus on the enlarged content. * If the enlarged view is closed, the container returns to its original size. * - * @param enlarge A boolean flag indicating whether to enlarge or reset the container size. + * @param isVertical A boolean flag indicating whether to enlarge or reset the container size. */ - adjustPdfContainerSize(enlarge: boolean): void { + adjustPdfContainerSize(isVertical: boolean): void { const pdfContainer = this.pdfContainer.nativeElement; - if (enlarge) { - pdfContainer.style.height = '80vh'; // Larger for enlarged view + if (isVertical) { + pdfContainer.style.height = '80vh'; // Larger for vertical slides } else { - pdfContainer.style.height = '60vh'; + pdfContainer.style.height = '60vh'; // Smaller for horizontal slides } } @@ -301,10 +301,10 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { this.isEnlargedView = true; this.currentPage = Number(originalCanvas.id); - this.adjustPdfContainerSize(true); - this.updateEnlargedCanvas(originalCanvas); + this.updateEnlargedCanvas(originalCanvas); // Adjusts the size as part of the update this.toggleBodyScroll(true); } + /** * Updates the enlarged canvas dimensions to optimize PDF page display within the current viewport. * This method dynamically adjusts the size, position, and scale of the canvas to maintain the aspect ratio, @@ -317,6 +317,9 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { requestAnimationFrame(() => { if (!this.isEnlargedView) return; + const isVertical = originalCanvas.height > originalCanvas.width; // Check if the slide is vertical + this.adjustPdfContainerSize(isVertical); // Adjust the container size based on orientation + const scaleFactor = this.calculateScaleFactor(originalCanvas); this.resizeCanvas(originalCanvas, scaleFactor); this.redrawCanvas(originalCanvas); From 78877f6216fec3b66aa9ab97f65bd52baf360910 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Thu, 17 Oct 2024 02:28:29 +0200 Subject: [PATCH 101/125] Adjust PDF container size dynamically -bug fix --- .../pdf-preview/pdf-preview.component.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 60d9dd96fceb..1a76a0c8bb16 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -288,9 +288,9 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { adjustPdfContainerSize(isVertical: boolean): void { const pdfContainer = this.pdfContainer.nativeElement; if (isVertical) { - pdfContainer.style.height = '80vh'; // Larger for vertical slides + pdfContainer.style.height = `${this.DEFAULT_SLIDE_HEIGHT}px`; } else { - pdfContainer.style.height = '60vh'; // Smaller for horizontal slides + pdfContainer.style.height = '60vh'; } } @@ -299,10 +299,14 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param originalCanvas - The original canvas element of the PDF page to be enlarged. * */ displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { - this.isEnlargedView = true; - this.currentPage = Number(originalCanvas.id); - this.updateEnlargedCanvas(originalCanvas); // Adjusts the size as part of the update - this.toggleBodyScroll(true); + const isVertical = originalCanvas.height > originalCanvas.width; + this.adjustPdfContainerSize(isVertical); + setTimeout(() => { + this.isEnlargedView = true; + this.currentPage = Number(originalCanvas.id); + this.updateEnlargedCanvas(originalCanvas); + this.toggleBodyScroll(true); + }, 100); } /** @@ -317,8 +321,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { requestAnimationFrame(() => { if (!this.isEnlargedView) return; - const isVertical = originalCanvas.height > originalCanvas.width; // Check if the slide is vertical - this.adjustPdfContainerSize(isVertical); // Adjust the container size based on orientation + const isVertical = originalCanvas.height > originalCanvas.width; + this.adjustPdfContainerSize(isVertical); const scaleFactor = this.calculateScaleFactor(originalCanvas); this.resizeCanvas(originalCanvas, scaleFactor); From 525f51f320a8018bccac028f0164c61e9482ad26 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Thu, 17 Oct 2024 02:55:43 +0200 Subject: [PATCH 102/125] Fix client test --- .../app/lecture/pdf-preview/pdf-preview.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 1a76a0c8bb16..dcee892c90c6 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -301,12 +301,12 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { const isVertical = originalCanvas.height > originalCanvas.width; this.adjustPdfContainerSize(isVertical); + this.isEnlargedView = true; + this.currentPage = Number(originalCanvas.id); + this.toggleBodyScroll(true); setTimeout(() => { - this.isEnlargedView = true; - this.currentPage = Number(originalCanvas.id); this.updateEnlargedCanvas(originalCanvas); - this.toggleBodyScroll(true); - }, 100); + }, 50); } /** From 156d9661ca8be2692067b6b3aaeb4467b4456614 Mon Sep 17 00:00:00 2001 From: Aybike Ece Eren Date: Fri, 18 Oct 2024 00:18:47 +0200 Subject: [PATCH 103/125] Update src/main/webapp/i18n/de/lecture.json Co-authored-by: Anian Schleyer <98647423+anian03@users.noreply.github.com> --- src/main/webapp/i18n/de/lecture.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/i18n/de/lecture.json b/src/main/webapp/i18n/de/lecture.json index b0b653c6bed5..cfe674a84872 100644 --- a/src/main/webapp/i18n/de/lecture.json +++ b/src/main/webapp/i18n/de/lecture.json @@ -101,7 +101,7 @@ "title": "Anhang", "attachmentIDError": "Ungültiger Anhang oder ungültige Anhangs-ID.", "attachmentUnitIDError": "Ungültige Dateieinheit oder ungültige Dateieinheits-ID.", - "removePageButton": "Entferne Seiten", + "removePageButton": "Seiten entfernen", "appendPDFButton": "PDF anhängen", "attachmentUpdateSuccess": "Anhang erfolgreich aktualisiert.", "attachmentUpdateError": "Fehler beim Aktualisieren des Anhangs: {{error}}", From 5c12f1b33d16bd5978d4e10da594d0f1d7db9007 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Fri, 18 Oct 2024 13:25:14 +0200 Subject: [PATCH 104/125] Fix translation --- src/main/webapp/i18n/en/lecture.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/i18n/en/lecture.json b/src/main/webapp/i18n/en/lecture.json index e3de36bf1e14..79525529f4c5 100644 --- a/src/main/webapp/i18n/en/lecture.json +++ b/src/main/webapp/i18n/en/lecture.json @@ -101,7 +101,7 @@ "title": "Attachment", "attachmentIDError": "Invalid Attachment or Attachment ID.", "attachmentUnitIDError": "Invalid Attachment Unit or Attachment Unit ID.", - "removePageButton": "Remove Page(s)", + "removePageButton": "Remove Pages", "appendPDFButton": "Append PDF", "attachmentUpdateSuccess": "Attachment updated successfully.", "attachmentUpdateError": "Failed to update attachment: {{error}}", From ed1300ec72ba0b575e7543559329ae8a8d0372e7 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 20 Oct 2024 23:40:24 +0200 Subject: [PATCH 105/125] Update the code based on new Angular notations --- package-lock.json | 2 +- package.json | 2 +- .../pdf-preview/pdf-preview.component.html | 30 +-- .../pdf-preview/pdf-preview.component.ts | 216 +++++++++--------- 4 files changed, 126 insertions(+), 124 deletions(-) diff --git a/package-lock.json b/package-lock.json index a15ce60de570..f39384f22eba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", - "pdf-lib": "^1.17.1", + "pdf-lib": "1.17.1", "pdfjs-dist": "4.7.76", "posthog-js": "1.174.2", "rxjs": "7.8.1", diff --git a/package.json b/package.json index fc32e1475642..ef067e3db3e8 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", - "pdf-lib": "^1.17.1", + "pdf-lib": "1.17.1", "pdfjs-dist": "4.7.76", "posthog-js": "1.174.2", "rxjs": "7.8.1", diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 21802b18ae2a..a8c6a3c43a65 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -4,13 +4,13 @@

- @if (attachment) { - {{ attachment.id }}: {{ attachment.name }} - } @else if (attachmentUnit) { - {{ attachmentUnit.id }}: {{ attachmentUnit.name }} + @if (attachment()) { + {{ attachment()!.id! }}: {{ attachment()!.name! }} + } @else if (attachmentUnit()!) { + {{ attachmentUnit()!.id! }}: {{ attachmentUnit()!.name! }} }

- @if (isPdfLoading) { + @if (isPdfLoading()) {
@@ -26,13 +26,13 @@

[deleteQuestion]="allPagesSelected() ? 'artemisApp.attachment.pdfPreview.deleteAllPagesQuestion' : 'artemisApp.attachment.pdfPreview.deletePagesQuestion'" (delete)="allPagesSelected() ? deleteAttachmentFile() : deleteSelectedSlides()" [dialogError]="dialogError$" - [disabled]="isPdfLoading || selectedPages.size === 0" + [disabled]="isPdfLoading() || selectedPages().size === 0" aria-label="Delete selected pages" > - +

- @if (isEnlargedView) { + @if (isEnlargedView()) {
- @if (currentPage !== 1) { + @if (currentPage() !== 1) { } - @if (currentPage !== totalPages) { + @if (currentPage() !== totalPages()) { } -
{{ currentPage }}
+
{{ currentPage() }}
}
@@ -60,9 +60,9 @@

type="button" class="btn btn-default" [routerLink]=" - attachment - ? ['/course-management', course!.id, 'lectures', attachment!.lecture?.id, 'attachments'] - : ['/course-management', course!.id, 'lectures', attachmentUnit!.lecture?.id, 'unit-management'] + attachment() + ? ['/course-management', course()!.id, 'lectures', attachment()!.lecture?.id, 'attachments'] + : ['/course-management', course()!.id, 'lectures', attachmentUnit()!.lecture?.id, 'unit-management'] " [ngbTooltip]="'entity.action.view' | artemisTranslate" > @@ -71,7 +71,7 @@

- diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index dcee892c90c6..9cf51c256175 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, HostListener, OnDestroy, OnInit, inject, signal, viewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { AttachmentService } from 'app/lecture/attachment.service'; import * as PDFJS from 'pdfjs-dist'; @@ -29,57 +29,57 @@ type NavigationDirection = 'next' | 'prev'; imports: [ArtemisSharedModule], }) export class PdfPreviewComponent implements OnInit, OnDestroy { - @ViewChild('pdfContainer', { static: true }) pdfContainer: ElementRef; - @ViewChild('enlargedCanvas') enlargedCanvas: ElementRef; - @ViewChild('fileInput', { static: false }) fileInput: ElementRef; + pdfContainer = viewChild>('pdfContainer'); + enlargedCanvas = viewChild>('enlargedCanvas'); + fileInput = viewChild>('fileInput'); - readonly DEFAULT_SLIDE_WIDTH = 250; - readonly DEFAULT_SLIDE_HEIGHT = 800; - course?: Course; - attachment?: Attachment; - attachmentUnit?: AttachmentUnit; - isEnlargedView = false; - isFileChanged = false; - currentPage = 1; - totalPages = 0; attachmentSub: Subscription; attachmentUnitSub: Subscription; - selectedPages: Set = new Set(); - isPdfLoading = false; - attachmentToBeEdited?: Attachment; - currentPdfBlob: Blob | null = null; + + readonly DEFAULT_SLIDE_WIDTH = 250; + readonly DEFAULT_SLIDE_HEIGHT = 800; + course = signal(undefined); + attachment = signal(undefined); + attachmentUnit = signal(undefined); + isEnlargedView = signal(false); + isFileChanged = signal(false); + currentPage = signal(1); + totalPages = signal(0); + selectedPages = signal>(new Set()); + isPdfLoading = signal(false); + attachmentToBeEdited = signal(undefined); + currentPdfBlob = signal(null); + + // Injected services + private readonly route = inject(ActivatedRoute); + private readonly attachmentService = inject(AttachmentService); + private readonly attachmentUnitService = inject(AttachmentUnitService); + private readonly lectureUnitService = inject(LectureUnitService); + private readonly alertService = inject(AlertService); + private readonly router = inject(Router); dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); // Icons - faFileImport = faFileImport; - faSave = faSave; - faTimes = faTimes; - faTrash = faTrash; - - constructor( - public route: ActivatedRoute, - private attachmentService: AttachmentService, - private attachmentUnitService: AttachmentUnitService, - private lectureUnitService: LectureUnitService, - private alertService: AlertService, - private router: Router, - ) {} + protected readonly faFileImport = faFileImport; + protected readonly faSave = faSave; + protected readonly faTimes = faTimes; + protected readonly faTrash = faTrash; ngOnInit() { this.route.data.subscribe((data) => { - this.course = data.course; + this.course.set(data.course); if ('attachment' in data) { - this.attachment = data.attachment; - this.attachmentSub = this.attachmentService.getAttachmentFile(this.course!.id!, this.attachment!.id!).subscribe({ + this.attachment.set(data.attachment); + this.attachmentSub = this.attachmentService.getAttachmentFile(this.course()!.id!, this.attachment()!.id!).subscribe({ next: (blob: Blob) => this.handleBlob(blob), error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } else if ('attachmentUnit' in data) { - this.attachmentUnit = data.attachmentUnit; - this.attachmentUnitSub = this.attachmentUnitService.getAttachmentFile(this.course!.id!, this.attachmentUnit!.id!).subscribe({ + this.attachmentUnit.set(data.attachmentUnit); + this.attachmentUnitSub = this.attachmentUnitService.getAttachmentFile(this.course()!.id!, this.attachmentUnit()!.id!).subscribe({ next: (blob: Blob) => this.handleBlob(blob), error: (error: HttpErrorResponse) => onError(this.alertService, error), }); @@ -88,7 +88,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } handleBlob(blob: Blob): void { - this.currentPdfBlob = blob; + this.currentPdfBlob.set(blob); const objectUrl = URL.createObjectURL(blob); this.loadOrAppendPdf(objectUrl).then(() => URL.revokeObjectURL(objectUrl)); } @@ -103,7 +103,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @returns True if the number of selected pages equals the total number of pages, otherwise false. */ allPagesSelected() { - return this.selectedPages.size === this.totalPages; + return this.selectedPages().size === this.totalPages(); } /** @@ -112,10 +112,10 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { */ @HostListener('document:keydown', ['$event']) handleKeyboardEvents(event: KeyboardEvent) { - if (this.isEnlargedView) { - if (event.key === 'ArrowRight' && this.currentPage < this.totalPages) { + if (this.isEnlargedView()) { + if (event.key === 'ArrowRight' && this.currentPage() < this.totalPages()) { this.navigatePages('next'); - } else if (event.key === 'ArrowLeft' && this.currentPage > 1) { + } else if (event.key === 'ArrowLeft' && this.currentPage() > 1) { this.navigatePages('prev'); } } @@ -136,15 +136,17 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @returns A promise that resolves when the PDF is loaded. */ async loadOrAppendPdf(fileUrl: string, append = false): Promise { - this.pdfContainer.nativeElement.querySelectorAll('.pdf-canvas-container').forEach((canvas) => canvas.remove()); - this.totalPages = 0; - this.isPdfLoading = true; + this.pdfContainer()! + .nativeElement.querySelectorAll('.pdf-canvas-container') + .forEach((canvas) => canvas.remove()); + this.totalPages.set(0); + this.isPdfLoading.set(true); try { const loadingTask = PDFJS.getDocument(fileUrl); const pdf = await loadingTask.promise; - this.totalPages = pdf.numPages; + this.totalPages.set(pdf.numPages); - for (let i = 1; i <= this.totalPages; i++) { + for (let i = 1; i <= this.totalPages(); i++) { const page = await pdf.getPage(i); const viewport = page.getViewport({ scale: 2 }); const canvas = this.createCanvas(viewport, i); @@ -152,7 +154,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { await page.render({ canvasContext: context!, viewport }).promise; const canvasContainer = this.createCanvasContainer(canvas, i); - this.pdfContainer.nativeElement.appendChild(canvasContainer); + this.pdfContainer()!.nativeElement.appendChild(canvasContainer); } if (append) { @@ -161,9 +163,9 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } catch (error) { onError(this.alertService, error); } finally { - this.isPdfLoading = false; + this.isPdfLoading.set(false); if (append) { - this.fileInput.nativeElement.value = ''; + this.fileInput()!.nativeElement.value = ''; } } } @@ -173,11 +175,11 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { */ scrollToBottom(): void { const scrollOptions: ScrollToOptions = { - top: this.pdfContainer.nativeElement.scrollHeight, + top: this.pdfContainer()!.nativeElement.scrollHeight, left: 0, behavior: 'smooth' as ScrollBehavior, }; - this.pdfContainer.nativeElement.scrollTo(scrollOptions); + this.pdfContainer()!.nativeElement.scrollTo(scrollOptions); } /** @@ -254,12 +256,12 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { checkbox.type = 'checkbox'; checkbox.id = String(pageIndex); checkbox.style.cssText = `position: absolute; top: -5px; right: -5px; z-index: 4;`; - checkbox.checked = this.selectedPages.has(pageIndex); + checkbox.checked = this.selectedPages().has(pageIndex); checkbox.addEventListener('change', () => { if (checkbox.checked) { - this.selectedPages.add(Number(checkbox.id)); + this.selectedPages().add(Number(checkbox.id)); } else { - this.selectedPages.delete(Number(checkbox.id)); + this.selectedPages().delete(Number(checkbox.id)); } }); return checkbox; @@ -269,10 +271,10 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * Dynamically updates the canvas size within an enlarged view based on the viewport. */ adjustCanvasSize = () => { - if (this.isEnlargedView) { - const canvasElements = this.pdfContainer.nativeElement.querySelectorAll('.pdf-canvas-container canvas'); - if (this.currentPage - 1 < canvasElements.length) { - const canvas = canvasElements[this.currentPage - 1] as HTMLCanvasElement; + if (this.isEnlargedView()) { + const canvasElements = this.pdfContainer()!.nativeElement.querySelectorAll('.pdf-canvas-container canvas'); + if (this.currentPage() - 1 < canvasElements.length) { + const canvas = canvasElements[this.currentPage() - 1] as HTMLCanvasElement; this.updateEnlargedCanvas(canvas); } } @@ -286,7 +288,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param isVertical A boolean flag indicating whether to enlarge or reset the container size. */ adjustPdfContainerSize(isVertical: boolean): void { - const pdfContainer = this.pdfContainer.nativeElement; + const pdfContainer = this.pdfContainer()!.nativeElement; if (isVertical) { pdfContainer.style.height = `${this.DEFAULT_SLIDE_HEIGHT}px`; } else { @@ -301,8 +303,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { const isVertical = originalCanvas.height > originalCanvas.width; this.adjustPdfContainerSize(isVertical); - this.isEnlargedView = true; - this.currentPage = Number(originalCanvas.id); + this.isEnlargedView.set(true); + this.currentPage.set(Number(originalCanvas.id)); this.toggleBodyScroll(true); setTimeout(() => { this.updateEnlargedCanvas(originalCanvas); @@ -339,8 +341,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @returns The scaling factor used to resize the original canvas to fit within the container dimensions. */ calculateScaleFactor(originalCanvas: HTMLCanvasElement): number { - const containerWidth = this.pdfContainer.nativeElement.clientWidth; - const containerHeight = this.pdfContainer.nativeElement.clientHeight; + const containerWidth = this.pdfContainer()!.nativeElement.clientWidth; + const containerHeight = this.pdfContainer()!.nativeElement.clientHeight; let scaleX, scaleY; @@ -367,7 +369,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param scaleFactor - The factor by which the canvas is resized. */ resizeCanvas(originalCanvas: HTMLCanvasElement, scaleFactor: number): void { - const enlargedCanvas = this.enlargedCanvas.nativeElement; + const enlargedCanvas = this.enlargedCanvas()!.nativeElement; enlargedCanvas.width = originalCanvas.width * scaleFactor; enlargedCanvas.height = originalCanvas.height * scaleFactor; } @@ -379,7 +381,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param originalCanvas - The original canvas containing the image to be redrawn. */ redrawCanvas(originalCanvas: HTMLCanvasElement): void { - const enlargedCanvas = this.enlargedCanvas.nativeElement; + const enlargedCanvas = this.enlargedCanvas()!.nativeElement; const context = enlargedCanvas.getContext('2d'); context!.clearRect(0, 0, enlargedCanvas.width, enlargedCanvas.height); context!.drawImage(originalCanvas, 0, 0, enlargedCanvas.width, enlargedCanvas.height); @@ -391,21 +393,21 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * and visually appealing layout. */ positionCanvas(): void { - const enlargedCanvas = this.enlargedCanvas.nativeElement; - const containerWidth = this.pdfContainer.nativeElement.clientWidth; - const containerHeight = this.pdfContainer.nativeElement.clientHeight; + const enlargedCanvas = this.enlargedCanvas()!.nativeElement; + const containerWidth = this.pdfContainer()!.nativeElement.clientWidth; + const containerHeight = this.pdfContainer()!.nativeElement.clientHeight; enlargedCanvas.style.position = 'absolute'; enlargedCanvas.style.left = `${(containerWidth - enlargedCanvas.width) / 2}px`; enlargedCanvas.style.top = `${(containerHeight - enlargedCanvas.height) / 2}px`; - enlargedCanvas.parentElement!.style.top = `${this.pdfContainer.nativeElement.scrollTop}px`; + enlargedCanvas.parentElement!.style.top = `${this.pdfContainer()!.nativeElement.scrollTop}px`; } /** * Closes the enlarged view of the PDF and re-enables scrolling in the PDF container. */ closeEnlargedView(event: MouseEvent) { - this.isEnlargedView = false; + this.isEnlargedView.set(false); this.adjustPdfContainerSize(false); this.toggleBodyScroll(false); event.stopPropagation(); @@ -416,7 +418,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param disable A boolean flag indicating whether scrolling should be disabled (`true`) or enabled (`false`). */ toggleBodyScroll(disable: boolean): void { - this.pdfContainer.nativeElement.style.overflow = disable ? 'hidden' : 'auto'; + this.pdfContainer()!.nativeElement.style.overflow = disable ? 'hidden' : 'auto'; } /** @@ -425,7 +427,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { */ closeIfOutside(event: MouseEvent): void { const target = event.target as HTMLElement; - const enlargedCanvas = this.enlargedCanvas.nativeElement; + const enlargedCanvas = this.enlargedCanvas()!.nativeElement; if (target.classList.contains('enlarged-container') && target !== enlargedCanvas) { this.closeEnlargedView(event); @@ -447,10 +449,10 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param direction The navigation direction (next or previous). */ navigatePages(direction: NavigationDirection) { - const nextPageIndex = direction === 'next' ? this.currentPage + 1 : this.currentPage - 1; - if (nextPageIndex > 0 && nextPageIndex <= this.totalPages) { - this.currentPage = nextPageIndex; - const canvas = this.pdfContainer.nativeElement.querySelectorAll('.pdf-canvas-container canvas')[this.currentPage - 1] as HTMLCanvasElement; + const nextPageIndex = direction === 'next' ? this.currentPage() + 1 : this.currentPage() - 1; + if (nextPageIndex > 0 && nextPageIndex <= this.totalPages()) { + this.currentPage.set(nextPageIndex); + const canvas = this.pdfContainer()!.nativeElement.querySelectorAll('.pdf-canvas-container canvas')[this.currentPage() - 1] as HTMLCanvasElement; this.updateEnlargedCanvas(canvas); } } @@ -461,19 +463,19 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { */ async deleteAttachmentFile() { if (this.attachment) { - this.attachmentService.delete(this.attachment.id!).subscribe({ + this.attachmentService.delete(this.attachment()!.id!).subscribe({ next: () => { - this.router.navigate(['course-management', this.course?.id, 'lectures', this.attachment!.lecture!.id, 'attachments']); + this.router.navigate(['course-management', this.course()?.id, 'lectures', this.attachment()!.lecture!.id, 'attachments']); this.dialogErrorSource.next(''); }, error: (error) => { this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); }, }); - } else if (this.attachmentUnit && this.attachmentUnit.id && this.attachmentUnit.lecture?.id) { - this.lectureUnitService.delete(this.attachmentUnit.id, this.attachmentUnit.lecture.id).subscribe({ + } else if (this.attachmentUnit && this.attachmentUnit()!.id && this.attachmentUnit()!.lecture?.id) { + this.lectureUnitService.delete(this.attachmentUnit()!.id!, this.attachmentUnit()!.lecture?.id!).subscribe({ next: () => { - this.router.navigate(['course-management', this.course?.id, 'lectures', this.attachmentUnit!.lecture!.id, 'unit-management']); + this.router.navigate(['course-management', this.course()?.id, 'lectures', this.attachmentUnit()!.lecture!.id, 'unit-management']); this.dialogErrorSource.next(''); }, error: (error) => { @@ -487,24 +489,24 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * Deletes selected slides from the PDF viewer. */ async deleteSelectedSlides() { - this.isPdfLoading = true; + this.isPdfLoading.set(true); try { - const existingPdfBytes = await this.currentPdfBlob!.arrayBuffer(); + const existingPdfBytes = await this.currentPdfBlob()!.arrayBuffer(); const pdfDoc = await PDFDocument.load(existingPdfBytes); - const pagesToDelete = Array.from(this.selectedPages) + const pagesToDelete = Array.from(this.selectedPages()) .map((page) => page - 1) .sort((a, b) => b - a); pagesToDelete.forEach((pageIndex) => { pdfDoc.removePage(pageIndex); }); - this.isFileChanged = true; + this.isFileChanged.set(true); const pdfBytes = await pdfDoc.save(); - this.currentPdfBlob = new Blob([pdfBytes], { type: 'application/pdf' }); - this.selectedPages.clear(); + this.currentPdfBlob.set(new Blob([pdfBytes], { type: 'application/pdf' })); + this.selectedPages().clear(); - const objectUrl = URL.createObjectURL(this.currentPdfBlob!); + const objectUrl = URL.createObjectURL(this.currentPdfBlob()!); await this.loadOrAppendPdf(objectUrl, false).then(() => { this.dialogErrorSource.next(''); }); @@ -512,7 +514,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } catch (error) { this.alertService.error('artemisApp.attachment.pdfPreview.pageDeleteError', { error: error.message }); } finally { - this.isPdfLoading = false; + this.isPdfLoading.set(false); } } @@ -520,7 +522,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * Triggers the file input to select files. */ triggerFileInput(): void { - this.fileInput.nativeElement.click(); + this.fileInput()!.nativeElement.click(); } /** @@ -530,28 +532,28 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { async mergePDF(event: Event): Promise { const file = (event.target as HTMLInputElement).files?.[0]; - this.isPdfLoading = true; + this.isPdfLoading.set(true); try { const newPdfBytes = await file!.arrayBuffer(); - const existingPdfBytes = await this.currentPdfBlob!.arrayBuffer(); + const existingPdfBytes = await this.currentPdfBlob()!.arrayBuffer(); const existingPdfDoc = await PDFDocument.load(existingPdfBytes); const newPdfDoc = await PDFDocument.load(newPdfBytes); const copiedPages = await existingPdfDoc.copyPages(newPdfDoc, newPdfDoc.getPageIndices()); copiedPages.forEach((page) => existingPdfDoc.addPage(page)); - this.isFileChanged = true; + this.isFileChanged.set(true); const mergedPdfBytes = await existingPdfDoc.save(); - this.currentPdfBlob = new Blob([mergedPdfBytes], { type: 'application/pdf' }); + this.currentPdfBlob.set(new Blob([mergedPdfBytes], { type: 'application/pdf' })); - this.selectedPages.clear(); + this.selectedPages().clear(); - const objectUrl = URL.createObjectURL(this.currentPdfBlob!); + const objectUrl = URL.createObjectURL(this.currentPdfBlob()!); await this.loadOrAppendPdf(objectUrl, true).then(() => URL.revokeObjectURL(objectUrl)); } catch (error) { this.alertService.error('artemisApp.attachment.pdfPreview.mergeFailedError', { error: error.message }); } finally { - this.isPdfLoading = false; + this.isPdfLoading.set(false); } } @@ -559,7 +561,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * Updates the IDs of remaining pages after some have been removed. */ updatePageIDs() { - const remainingPages = this.pdfContainer.nativeElement.querySelectorAll('.pdf-canvas-container'); + const remainingPages = this.pdfContainer()!.nativeElement.querySelectorAll('.pdf-canvas-container'); remainingPages.forEach((container, index) => { const pageIndex = index + 1; container.id = `pdf-page-${pageIndex}`; @@ -573,7 +575,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } updateAttachmentWithFile(): void { - const pdfFile = new File([this.currentPdfBlob!], 'updatedAttachment.pdf', { type: 'application/pdf' }); + const pdfFile = new File([this.currentPdfBlob()!], 'updatedAttachment.pdf', { type: 'application/pdf' }); if (pdfFile.size > MAX_FILE_SIZE) { this.alertService.error('artemisApp.attachment.pdfPreview.fileSizeError'); @@ -582,32 +584,32 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { if (this.attachment) { this.attachmentToBeEdited = this.attachment; - this.attachmentToBeEdited!.version!++; - this.attachmentToBeEdited.uploadDate = dayjs(); + this.attachmentToBeEdited()!.version!++; + this.attachmentToBeEdited()!.uploadDate = dayjs(); - this.attachmentService.update(this.attachmentToBeEdited!.id!, this.attachmentToBeEdited, pdfFile).subscribe({ + this.attachmentService.update(this.attachmentToBeEdited()!.id!, this.attachmentToBeEdited()!, pdfFile).subscribe({ next: () => { this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); - this.router.navigate(['course-management', this.course?.id, 'lectures', this.attachment!.lecture!.id, 'attachments']); + this.router.navigate(['course-management', this.course()?.id, 'lectures', this.attachment()!.lecture!.id, 'attachments']); }, error: (error) => { this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); }, }); } else if (this.attachmentUnit) { - this.attachmentToBeEdited = this.attachmentUnit.attachment!; - this.attachmentToBeEdited!.version!++; - this.attachmentToBeEdited!.uploadDate = dayjs(); + this.attachmentToBeEdited.set(this.attachmentUnit()!.attachment!); + this.attachmentToBeEdited()!.version!++; + this.attachmentToBeEdited()!.uploadDate = dayjs(); const formData = new FormData(); formData.append('file', pdfFile); formData.append('attachment', objectToJsonBlob(this.attachmentToBeEdited)); formData.append('attachmentUnit', objectToJsonBlob(this.attachmentUnit)); - this.attachmentUnitService.update(this.attachmentUnit!.lecture!.id!, this.attachmentUnit!.id!, formData).subscribe({ + this.attachmentUnitService.update(this.attachmentUnit()!.lecture!.id!, this.attachmentUnit()!.id!, formData).subscribe({ next: () => { this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); - this.router.navigate(['course-management', this.course?.id, 'lectures', this.attachmentUnit!.lecture!.id, 'unit-management']); + this.router.navigate(['course-management', this.course()?.id, 'lectures', this.attachmentUnit()!.lecture!.id, 'unit-management']); }, error: (error) => { this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); From 9cb3a823aa190127526bdf70ba8c22e6d46a48c8 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 21 Oct 2024 01:27:24 +0200 Subject: [PATCH 106/125] Fix client tests --- .../pdf-preview/pdf-preview.component.ts | 6 +- .../lecture/pdf-preview.component.spec.ts | 155 +++++++++--------- 2 files changed, 83 insertions(+), 78 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 9cf51c256175..49b7c025141a 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -582,8 +582,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { return; } - if (this.attachment) { - this.attachmentToBeEdited = this.attachment; + if (this.attachment()) { + this.attachmentToBeEdited.set(this.attachment()); this.attachmentToBeEdited()!.version!++; this.attachmentToBeEdited()!.uploadDate = dayjs(); @@ -596,7 +596,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); }, }); - } else if (this.attachmentUnit) { + } else if (this.attachmentUnit()) { this.attachmentToBeEdited.set(this.attachmentUnit()!.attachment!); this.attachmentToBeEdited()!.version!++; this.attachmentToBeEdited()!.uploadDate = dayjs(); diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index c416e0357296..8c2044b5fa7d 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -6,7 +6,7 @@ import { of, throwError } from 'rxjs'; import { AttachmentService } from 'app/lecture/attachment.service'; import { AttachmentUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service'; import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component'; -import { ElementRef } from '@angular/core'; +import { signal } from '@angular/core'; import { AlertService } from 'app/core/util/alert.service'; import { HttpClientModule, HttpErrorResponse } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; @@ -119,12 +119,12 @@ describe('PdfPreviewComponent', () => { mockCanvasElement.height = 600; jest.spyOn(component, 'updateEnlargedCanvas').mockImplementation(() => { - component.enlargedCanvas.nativeElement = mockCanvasElement; + component.enlargedCanvas()!.nativeElement = mockCanvasElement; }); mockEnlargedCanvas = document.createElement('canvas'); mockEnlargedCanvas.classList.add('enlarged-canvas'); - component.enlargedCanvas = new ElementRef(mockEnlargedCanvas); + component.enlargedCanvas = signal({ nativeElement: mockEnlargedCanvas }); mockContext = { clearRect: jest.fn(), @@ -139,13 +139,13 @@ describe('PdfPreviewComponent', () => { mockOverlay = document.createElement('div'); mockOverlay.style.opacity = '0'; mockCanvasElement.appendChild(mockOverlay); - component.currentPdfBlob = new Blob(['dummy content'], { type: 'application/pdf' }); + component.currentPdfBlob.set(new Blob(['dummy content'], { type: 'application/pdf' })); global.URL.createObjectURL = jest.fn().mockReturnValue('blob-url'); fixture.detectChanges(); - component.pdfContainer = new ElementRef(document.createElement('div')); - component.enlargedCanvas = new ElementRef(document.createElement('canvas')); + component.pdfContainer = signal({ nativeElement: document.createElement('div') }); + component.enlargedCanvas = signal({ nativeElement: document.createElement('canvas') }); fixture.detectChanges(); }); @@ -210,57 +210,57 @@ describe('PdfPreviewComponent', () => { it('should load PDF and verify rendering of pages', async () => { const spyCreateCanvas = jest.spyOn(component, 'createCanvas'); const spyCreateCanvasContainer = jest.spyOn(component, 'createCanvasContainer'); - const spyAppendChild = jest.spyOn(component.pdfContainer.nativeElement, 'appendChild'); + const spyAppendChild = jest.spyOn(component.pdfContainer()!.nativeElement, 'appendChild'); await component.loadOrAppendPdf('fake-url'); expect(spyCreateCanvas).toHaveBeenCalled(); expect(spyCreateCanvasContainer).toHaveBeenCalled(); expect(spyAppendChild).toHaveBeenCalled(); - expect(component.totalPages).toBe(1); - expect(component.isPdfLoading).toBeFalsy(); - expect(component.fileInput.nativeElement.value).toBe(''); + expect(component.totalPages()).toBe(1); + expect(component.isPdfLoading()).toBeFalsy(); + expect(component.fileInput()!.nativeElement.value).toBe(''); }); it('should navigate through pages using keyboard in enlarged view', () => { - component.isEnlargedView = true; - component.totalPages = 5; - component.currentPage = 3; + component.isEnlargedView.set(true); + component.totalPages.set(5); + component.currentPage.set(3); const eventRight = new KeyboardEvent('keydown', { key: 'ArrowRight' }); const eventLeft = new KeyboardEvent('keydown', { key: 'ArrowLeft' }); component.handleKeyboardEvents(eventRight); - expect(component.currentPage).toBe(4); + expect(component.currentPage()).toBe(4); component.handleKeyboardEvents(eventLeft); - expect(component.currentPage).toBe(3); + expect(component.currentPage()).toBe(3); }); it('should toggle enlarged view state', () => { const mockCanvas = document.createElement('canvas'); component.displayEnlargedCanvas(mockCanvas); - expect(component.isEnlargedView).toBeTruthy(); + expect(component.isEnlargedView()).toBeTruthy(); const clickEvent = new MouseEvent('click', { button: 0, }); component.closeEnlargedView(clickEvent); - expect(component.isEnlargedView).toBeFalsy(); + expect(component.isEnlargedView()).toBeFalsy(); }); it('should prevent scrolling when enlarged view is active', () => { component.toggleBodyScroll(true); - expect(component.pdfContainer.nativeElement.style.overflow).toBe('hidden'); + expect(component.pdfContainer()!.nativeElement.style.overflow).toBe('hidden'); component.toggleBodyScroll(false); - expect(component.pdfContainer.nativeElement.style.overflow).toBe('auto'); + expect(component.pdfContainer()!.nativeElement.style.overflow).toBe('auto'); }); it('should not update canvas size if not in enlarged view', () => { - component.isEnlargedView = false; - component.currentPage = 3; + component.isEnlargedView.set(false); + component.currentPage.set(3); const spy = jest.spyOn(component, 'updateEnlargedCanvas'); component.adjustCanvasSize(); @@ -269,8 +269,8 @@ describe('PdfPreviewComponent', () => { }); it('should not update canvas size if the current page canvas does not exist', () => { - component.isEnlargedView = true; - component.currentPage = 10; + component.isEnlargedView.set(true); + component.currentPage.set(10); const spy = jest.spyOn(component, 'updateEnlargedCanvas'); component.adjustCanvasSize(); @@ -279,17 +279,18 @@ describe('PdfPreviewComponent', () => { }); it('should prevent navigation beyond last page', () => { - component.currentPage = component.totalPages = 5; + component.currentPage.set(5); + component.totalPages.set(5); component.handleKeyboardEvents(new KeyboardEvent('keydown', { key: 'ArrowRight' })); - expect(component.currentPage).toBe(5); + expect(component.currentPage()).toBe(5); }); it('should prevent navigation before first page', () => { - component.currentPage = 1; + component.currentPage.set(1); component.handleKeyboardEvents(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); - expect(component.currentPage).toBe(1); + expect(component.currentPage()).toBe(1); }); it('should unsubscribe attachment subscription during component destruction', () => { @@ -322,16 +323,14 @@ describe('PdfPreviewComponent', () => { }); it('should call updateEnlargedCanvas when window is resized and conditions are met', () => { - component.isEnlargedView = true; - component.currentPage = 1; + component.isEnlargedView.set(true); + component.currentPage.set(1); const canvas = document.createElement('canvas'); const pdfContainer = document.createElement('div'); pdfContainer.classList.add('pdf-canvas-container'); pdfContainer.appendChild(canvas); - component.pdfContainer = { - nativeElement: pdfContainer, - } as ElementRef; + component.pdfContainer = signal({ nativeElement: pdfContainer }); const updateEnlargedCanvasSpy = jest.spyOn(component, 'updateEnlargedCanvas'); const adjustCanvasSizeSpy = jest.spyOn(component, 'adjustCanvasSize'); @@ -346,20 +345,20 @@ describe('PdfPreviewComponent', () => { target.classList.add('enlarged-container'); const mockEvent = createMockEvent(target); - component.isEnlargedView = true; + component.isEnlargedView.set(true); const closeSpy = jest.spyOn(component, 'closeEnlargedView'); component.closeIfOutside(mockEvent); expect(closeSpy).toHaveBeenCalled(); - expect(component.isEnlargedView).toBeFalsy(); + expect(component.isEnlargedView()).toBeFalsy(); }); it('should not close the enlarged view if the click is on the canvas itself', () => { const mockEvent = createMockEvent(mockEnlargedCanvas); Object.defineProperty(mockEvent, 'target', { value: mockEnlargedCanvas, writable: false }); - component.isEnlargedView = true; + component.isEnlargedView.set(true); const closeSpy = jest.spyOn(component, 'closeEnlargedView'); @@ -370,8 +369,8 @@ describe('PdfPreviewComponent', () => { it('should calculate the correct scale factor for horizontal slides', () => { // Mock container dimensions - Object.defineProperty(component.pdfContainer.nativeElement, 'clientWidth', { value: 1000, configurable: true }); - Object.defineProperty(component.pdfContainer.nativeElement, 'clientHeight', { value: 800, configurable: true }); + Object.defineProperty(component.pdfContainer()!.nativeElement, 'clientWidth', { value: 1000, configurable: true }); + Object.defineProperty(component.pdfContainer()!.nativeElement, 'clientHeight', { value: 800, configurable: true }); // Mock a horizontal canvas (width > height) mockCanvasElement.width = 500; @@ -383,8 +382,8 @@ describe('PdfPreviewComponent', () => { }); it('should calculate the correct scale factor for vertical slides', () => { - Object.defineProperty(component.pdfContainer.nativeElement, 'clientWidth', { value: 1000, configurable: true }); - Object.defineProperty(component.pdfContainer.nativeElement, 'clientHeight', { value: 800, configurable: true }); + Object.defineProperty(component.pdfContainer()!.nativeElement, 'clientWidth', { value: 1000, configurable: true }); + Object.defineProperty(component.pdfContainer()!.nativeElement, 'clientHeight', { value: 800, configurable: true }); // Mock a vertical canvas (height > width) mockCanvasElement.width = 400; @@ -401,8 +400,8 @@ describe('PdfPreviewComponent', () => { mockCanvasElement.height = 400; component.resizeCanvas(mockCanvasElement, 2); - expect(component.enlargedCanvas.nativeElement.width).toBe(1000); - expect(component.enlargedCanvas.nativeElement.height).toBe(800); + expect(component.enlargedCanvas()!.nativeElement.width).toBe(1000); + expect(component.enlargedCanvas()!.nativeElement.height).toBe(800); }); it('should clear and redraw the canvas with the new dimensions', () => { @@ -415,8 +414,8 @@ describe('PdfPreviewComponent', () => { component.resizeCanvas(mockCanvasElement, 2); component.redrawCanvas(mockCanvasElement); - expect(component.enlargedCanvas.nativeElement.width).toBe(1000); // 500 * 2 - expect(component.enlargedCanvas.nativeElement.height).toBe(800); // 400 * 2 + expect(component.enlargedCanvas()!.nativeElement.width).toBe(1000); // 500 * 2 + expect(component.enlargedCanvas()!.nativeElement.height).toBe(800); // 400 * 2 expect(mockContext.clearRect).toHaveBeenCalledWith(0, 0, 1000, 800); expect(mockContext.drawImage).toHaveBeenCalledWith(mockCanvasElement, 0, 0, 1000, 800); @@ -424,8 +423,14 @@ describe('PdfPreviewComponent', () => { it('should correctly position the canvas', () => { const parent = document.createElement('div'); - component.pdfContainer = { nativeElement: { clientWidth: 1000, clientHeight: 800, scrollTop: 500 } } as ElementRef; - const canvasElem = component.enlargedCanvas.nativeElement; + + const mockDivElement = document.createElement('div'); + Object.defineProperty(mockDivElement, 'clientWidth', { value: 1000 }); + Object.defineProperty(mockDivElement, 'clientHeight', { value: 800 }); + Object.defineProperty(mockDivElement, 'scrollTop', { value: 500, writable: true }); + + component.pdfContainer = signal({ nativeElement: mockDivElement }); + const canvasElem = component.enlargedCanvas()!.nativeElement; parent.appendChild(canvasElem); canvasElem.width = 500; canvasElem.height = 400; @@ -482,9 +487,9 @@ describe('PdfPreviewComponent', () => { it('should trigger the file input click event', () => { const mockFileInput = document.createElement('input'); mockFileInput.type = 'file'; - component.fileInput = new ElementRef(mockFileInput); + component.fileInput = signal({ nativeElement: mockFileInput }); - const clickSpy = jest.spyOn(component.fileInput.nativeElement, 'click'); + const clickSpy = jest.spyOn(component.fileInput()!.nativeElement, 'click'); component.triggerFileInput(); expect(clickSpy).toHaveBeenCalled(); }); @@ -509,10 +514,10 @@ describe('PdfPreviewComponent', () => { .mockImplementationOnce(() => Promise.resolve(existingPdfDoc)) .mockImplementationOnce(() => Promise.resolve(newPdfDoc)); - component.currentPdfBlob = new Blob(['existing pdf'], { type: 'application/pdf' }); - component.currentPdfBlob.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity + component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); + component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity - component.selectedPages = new Set([1]); // Assume there is initially a selected page + component.selectedPages.set(new Set([1])); // Assume there is initially a selected page await component.mergePDF(mockEvent as any); @@ -521,8 +526,8 @@ describe('PdfPreviewComponent', () => { expect(existingPdfDoc.addPage).toHaveBeenCalled(); expect(existingPdfDoc.save).toHaveBeenCalled(); expect(component.currentPdfBlob).toBeDefined(); - expect(component.selectedPages.size).toBe(0); - expect(component.isPdfLoading).toBeFalsy(); + expect(component.selectedPages()!.size).toBe(0); + expect(component.isPdfLoading()).toBeFalsy(); expect(URL.createObjectURL).toHaveBeenCalledWith(new Blob([new Uint8Array([1, 2, 3])], { type: 'application/pdf' })); }); @@ -535,8 +540,8 @@ describe('PdfPreviewComponent', () => { const mockEvent = { target: { files: [mockFile] } }; const error = new Error('Error loading PDF'); - component.currentPdfBlob = new Blob(['existing pdf'], { type: 'application/pdf' }); - component.currentPdfBlob.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simp + component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); + component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simp // Mock PDFDocument.load to throw an error on the first call PDFDocument.load = jest @@ -547,7 +552,7 @@ describe('PdfPreviewComponent', () => { await component.mergePDF(mockEvent as any); expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.mergeFailedError', { error: error.message }); - expect(component.isPdfLoading).toBeFalsy(); + expect(component.isPdfLoading()).toBeFalsy(); }); it('should update the IDs of remaining pages after some have been removed', () => { @@ -574,10 +579,10 @@ describe('PdfPreviewComponent', () => { mockContainer.appendChild(mockPageContainer); } - component.pdfContainer = new ElementRef(mockContainer); + component.pdfContainer = signal({ nativeElement: mockContainer }); component.updatePageIDs(); - const remainingPages = component.pdfContainer.nativeElement.querySelectorAll('.pdf-canvas-container'); + const remainingPages = component.pdfContainer()!.nativeElement.querySelectorAll('.pdf-canvas-container'); remainingPages.forEach((pageContainer, index) => { const pageIndex = index + 1; const canvas = pageContainer.querySelector('canvas'); @@ -595,7 +600,7 @@ describe('PdfPreviewComponent', () => { }); it('should update attachment successfully and show success alert', () => { - component.attachment = { id: 1, version: 1 }; + component.attachment.set({ id: 1, version: 1 }); component.updateAttachmentWithFile(); expect(attachmentServiceMock.update).toHaveBeenCalled(); @@ -604,7 +609,7 @@ describe('PdfPreviewComponent', () => { it('should not update attachment if file size exceeds the limit and show an error alert', () => { const oversizedData = new Uint8Array(MAX_FILE_SIZE + 1).fill(0); - component.currentPdfBlob = new Blob([oversizedData], { type: 'application/pdf' }); + component.currentPdfBlob.set(new Blob([oversizedData], { type: 'application/pdf' })); component.updateAttachmentWithFile(); @@ -614,7 +619,7 @@ describe('PdfPreviewComponent', () => { it('should handle errors when updating an attachment fails', () => { attachmentServiceMock.update.mockReturnValue(throwError(() => new Error('Update failed'))); - component.attachment = { id: 1, version: 1 }; + component.attachment.set({ id: 1, version: 1 }); component.updateAttachmentWithFile(); @@ -623,12 +628,12 @@ describe('PdfPreviewComponent', () => { }); it('should update attachment unit successfully and show success alert', () => { - component.attachment = undefined; - component.attachmentUnit = { + component.attachment.set(undefined); + component.attachmentUnit.set({ id: 1, lecture: { id: 1 }, attachment: { id: 1, version: 1 }, - }; + }); attachmentUnitServiceMock.update.mockReturnValue(of({})); component.updateAttachmentWithFile(); @@ -638,12 +643,12 @@ describe('PdfPreviewComponent', () => { }); it('should handle errors when updating an attachment unit fails', () => { - component.attachment = undefined; - component.attachmentUnit = { + component.attachment.set(undefined); + component.attachmentUnit.set({ id: 1, lecture: { id: 1 }, attachment: { id: 1, version: 1 }, - }; + }); const errorResponse = { message: 'Update failed' }; attachmentUnitServiceMock.update.mockReturnValue(throwError(() => errorResponse)); @@ -661,14 +666,14 @@ describe('PdfPreviewComponent', () => { PDFDocument.load = jest.fn().mockResolvedValue(existingPdfDoc); const mockArrayBuffer = new ArrayBuffer(8); - component.currentPdfBlob = new Blob(['existing pdf'], { type: 'application/pdf' }); - component.currentPdfBlob.arrayBuffer = jest.fn().mockResolvedValue(mockArrayBuffer); + component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); + component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(mockArrayBuffer); const objectUrl = 'blob-url'; global.URL.createObjectURL = jest.fn().mockReturnValue(objectUrl); global.URL.revokeObjectURL = jest.fn(); - component.selectedPages = new Set([1, 2]); // Pages 1 and 2 selected + component.selectedPages.set(new Set([1, 2])); // Pages 1 and 2 selected const loadOrAppendPdfSpy = jest.spyOn(component, 'loadOrAppendPdf'); const alertServiceErrorSpy = jest.spyOn(alertServiceMock, 'error'); @@ -680,18 +685,18 @@ describe('PdfPreviewComponent', () => { expect(existingPdfDoc.removePage).toHaveBeenCalledWith(0); expect(existingPdfDoc.removePage).toHaveBeenCalledTimes(2); expect(existingPdfDoc.save).toHaveBeenCalled(); - expect(component.currentPdfBlob).toEqual(new Blob([new Uint8Array([1, 2, 3])], { type: 'application/pdf' })); + expect(component.currentPdfBlob()).toEqual(new Blob([new Uint8Array([1, 2, 3])], { type: 'application/pdf' })); expect(loadOrAppendPdfSpy).toHaveBeenCalledWith(objectUrl, false); - expect(component.selectedPages.size).toBe(0); + expect(component.selectedPages()!.size).toBe(0); expect(alertServiceErrorSpy).not.toHaveBeenCalled(); expect(URL.revokeObjectURL).toHaveBeenCalledWith(objectUrl); - expect(component.isPdfLoading).toBeFalsy(); + expect(component.isPdfLoading()).toBeFalsy(); }); it('should handle errors when deleting slides', async () => { // Mock the arrayBuffer method for the current PDF Blob - component.currentPdfBlob = new Blob(['existing pdf'], { type: 'application/pdf' }); - component.currentPdfBlob.arrayBuffer = jest.fn().mockRejectedValue(new Error('Failed to load PDF')); + component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); + component.currentPdfBlob()!.arrayBuffer = jest.fn().mockRejectedValue(new Error('Failed to load PDF')); // Spy on the alert service const alertServiceErrorSpy = jest.spyOn(alertServiceMock, 'error'); @@ -703,6 +708,6 @@ describe('PdfPreviewComponent', () => { expect(alertServiceErrorSpy).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.pageDeleteError', { error: 'Failed to load PDF' }); // Verify that the loading state is set to false after the operation - expect(component.isPdfLoading).toBeFalsy(); + expect(component.isPdfLoading()).toBeFalsy(); }); }); From b63f49cc4195ac62b062ef49f9e13b0de24dbee5 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 21 Oct 2024 01:33:19 +0200 Subject: [PATCH 107/125] Bug fix --- .../webapp/app/lecture/pdf-preview/pdf-preview.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 49b7c025141a..a4105b706172 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -603,8 +603,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { const formData = new FormData(); formData.append('file', pdfFile); - formData.append('attachment', objectToJsonBlob(this.attachmentToBeEdited)); - formData.append('attachmentUnit', objectToJsonBlob(this.attachmentUnit)); + formData.append('attachment', objectToJsonBlob(this.attachmentToBeEdited()!)); + formData.append('attachmentUnit', objectToJsonBlob(this.attachmentUnit()!)); this.attachmentUnitService.update(this.attachmentUnit()!.lecture!.id!, this.attachmentUnit()!.id!, formData).subscribe({ next: () => { From 40a132024ead21fd30d02c645f6b3256f12cf75b Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 21 Oct 2024 16:16:39 +0200 Subject: [PATCH 108/125] Add required viewChilds --- .../pdf-preview/pdf-preview.component.ts | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index a4105b706172..ebf74e8c6632 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -29,9 +29,9 @@ type NavigationDirection = 'next' | 'prev'; imports: [ArtemisSharedModule], }) export class PdfPreviewComponent implements OnInit, OnDestroy { - pdfContainer = viewChild>('pdfContainer'); - enlargedCanvas = viewChild>('enlargedCanvas'); - fileInput = viewChild>('fileInput'); + pdfContainer = viewChild.required>('pdfContainer'); + enlargedCanvas = viewChild.required>('enlargedCanvas'); + fileInput = viewChild.required>('fileInput'); attachmentSub: Subscription; attachmentUnitSub: Subscription; @@ -136,7 +136,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @returns A promise that resolves when the PDF is loaded. */ async loadOrAppendPdf(fileUrl: string, append = false): Promise { - this.pdfContainer()! + this.pdfContainer() .nativeElement.querySelectorAll('.pdf-canvas-container') .forEach((canvas) => canvas.remove()); this.totalPages.set(0); @@ -154,7 +154,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { await page.render({ canvasContext: context!, viewport }).promise; const canvasContainer = this.createCanvasContainer(canvas, i); - this.pdfContainer()!.nativeElement.appendChild(canvasContainer); + this.pdfContainer().nativeElement.appendChild(canvasContainer); } if (append) { @@ -165,7 +165,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } finally { this.isPdfLoading.set(false); if (append) { - this.fileInput()!.nativeElement.value = ''; + this.fileInput().nativeElement.value = ''; } } } @@ -175,11 +175,11 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { */ scrollToBottom(): void { const scrollOptions: ScrollToOptions = { - top: this.pdfContainer()!.nativeElement.scrollHeight, + top: this.pdfContainer().nativeElement.scrollHeight, left: 0, behavior: 'smooth' as ScrollBehavior, }; - this.pdfContainer()!.nativeElement.scrollTo(scrollOptions); + this.pdfContainer().nativeElement.scrollTo(scrollOptions); } /** @@ -272,7 +272,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { */ adjustCanvasSize = () => { if (this.isEnlargedView()) { - const canvasElements = this.pdfContainer()!.nativeElement.querySelectorAll('.pdf-canvas-container canvas'); + const canvasElements = this.pdfContainer().nativeElement.querySelectorAll('.pdf-canvas-container canvas'); if (this.currentPage() - 1 < canvasElements.length) { const canvas = canvasElements[this.currentPage() - 1] as HTMLCanvasElement; this.updateEnlargedCanvas(canvas); @@ -288,7 +288,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param isVertical A boolean flag indicating whether to enlarge or reset the container size. */ adjustPdfContainerSize(isVertical: boolean): void { - const pdfContainer = this.pdfContainer()!.nativeElement; + const pdfContainer = this.pdfContainer().nativeElement; if (isVertical) { pdfContainer.style.height = `${this.DEFAULT_SLIDE_HEIGHT}px`; } else { @@ -341,8 +341,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @returns The scaling factor used to resize the original canvas to fit within the container dimensions. */ calculateScaleFactor(originalCanvas: HTMLCanvasElement): number { - const containerWidth = this.pdfContainer()!.nativeElement.clientWidth; - const containerHeight = this.pdfContainer()!.nativeElement.clientHeight; + const containerWidth = this.pdfContainer().nativeElement.clientWidth; + const containerHeight = this.pdfContainer().nativeElement.clientHeight; let scaleX, scaleY; @@ -369,7 +369,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param scaleFactor - The factor by which the canvas is resized. */ resizeCanvas(originalCanvas: HTMLCanvasElement, scaleFactor: number): void { - const enlargedCanvas = this.enlargedCanvas()!.nativeElement; + const enlargedCanvas = this.enlargedCanvas().nativeElement; enlargedCanvas.width = originalCanvas.width * scaleFactor; enlargedCanvas.height = originalCanvas.height * scaleFactor; } @@ -381,7 +381,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param originalCanvas - The original canvas containing the image to be redrawn. */ redrawCanvas(originalCanvas: HTMLCanvasElement): void { - const enlargedCanvas = this.enlargedCanvas()!.nativeElement; + const enlargedCanvas = this.enlargedCanvas().nativeElement; const context = enlargedCanvas.getContext('2d'); context!.clearRect(0, 0, enlargedCanvas.width, enlargedCanvas.height); context!.drawImage(originalCanvas, 0, 0, enlargedCanvas.width, enlargedCanvas.height); @@ -393,14 +393,14 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * and visually appealing layout. */ positionCanvas(): void { - const enlargedCanvas = this.enlargedCanvas()!.nativeElement; - const containerWidth = this.pdfContainer()!.nativeElement.clientWidth; - const containerHeight = this.pdfContainer()!.nativeElement.clientHeight; + const enlargedCanvas = this.enlargedCanvas().nativeElement; + const containerWidth = this.pdfContainer().nativeElement.clientWidth; + const containerHeight = this.pdfContainer().nativeElement.clientHeight; enlargedCanvas.style.position = 'absolute'; enlargedCanvas.style.left = `${(containerWidth - enlargedCanvas.width) / 2}px`; enlargedCanvas.style.top = `${(containerHeight - enlargedCanvas.height) / 2}px`; - enlargedCanvas.parentElement!.style.top = `${this.pdfContainer()!.nativeElement.scrollTop}px`; + enlargedCanvas.parentElement!.style.top = `${this.pdfContainer().nativeElement.scrollTop}px`; } /** @@ -418,7 +418,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @param disable A boolean flag indicating whether scrolling should be disabled (`true`) or enabled (`false`). */ toggleBodyScroll(disable: boolean): void { - this.pdfContainer()!.nativeElement.style.overflow = disable ? 'hidden' : 'auto'; + this.pdfContainer().nativeElement.style.overflow = disable ? 'hidden' : 'auto'; } /** @@ -427,7 +427,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { */ closeIfOutside(event: MouseEvent): void { const target = event.target as HTMLElement; - const enlargedCanvas = this.enlargedCanvas()!.nativeElement; + const enlargedCanvas = this.enlargedCanvas().nativeElement; if (target.classList.contains('enlarged-container') && target !== enlargedCanvas) { this.closeEnlargedView(event); @@ -452,7 +452,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { const nextPageIndex = direction === 'next' ? this.currentPage() + 1 : this.currentPage() - 1; if (nextPageIndex > 0 && nextPageIndex <= this.totalPages()) { this.currentPage.set(nextPageIndex); - const canvas = this.pdfContainer()!.nativeElement.querySelectorAll('.pdf-canvas-container canvas')[this.currentPage() - 1] as HTMLCanvasElement; + const canvas = this.pdfContainer().nativeElement.querySelectorAll('.pdf-canvas-container canvas')[this.currentPage() - 1] as HTMLCanvasElement; this.updateEnlargedCanvas(canvas); } } @@ -462,20 +462,20 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * @returns A Promise that resolves when the deletion process is completed. */ async deleteAttachmentFile() { - if (this.attachment) { + if (this.attachment()) { this.attachmentService.delete(this.attachment()!.id!).subscribe({ next: () => { - this.router.navigate(['course-management', this.course()?.id, 'lectures', this.attachment()!.lecture!.id, 'attachments']); + this.router.navigate(['course-management', this.course()!.id, 'lectures', this.attachment()!.lecture!.id, 'attachments']); this.dialogErrorSource.next(''); }, error: (error) => { this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); }, }); - } else if (this.attachmentUnit && this.attachmentUnit()!.id && this.attachmentUnit()!.lecture?.id) { + } else if (this.attachmentUnit()) { this.lectureUnitService.delete(this.attachmentUnit()!.id!, this.attachmentUnit()!.lecture?.id!).subscribe({ next: () => { - this.router.navigate(['course-management', this.course()?.id, 'lectures', this.attachmentUnit()!.lecture!.id, 'unit-management']); + this.router.navigate(['course-management', this.course()!.id, 'lectures', this.attachmentUnit()!.lecture!.id, 'unit-management']); this.dialogErrorSource.next(''); }, error: (error) => { @@ -522,7 +522,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * Triggers the file input to select files. */ triggerFileInput(): void { - this.fileInput()!.nativeElement.click(); + this.fileInput().nativeElement.click(); } /** @@ -561,7 +561,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * Updates the IDs of remaining pages after some have been removed. */ updatePageIDs() { - const remainingPages = this.pdfContainer()!.nativeElement.querySelectorAll('.pdf-canvas-container'); + const remainingPages = this.pdfContainer().nativeElement.querySelectorAll('.pdf-canvas-container'); remainingPages.forEach((container, index) => { const pageIndex = index + 1; container.id = `pdf-page-${pageIndex}`; From 989e0141ce036ff566a9318ee55e59562311c6d5 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 21 Oct 2024 21:20:24 +0200 Subject: [PATCH 109/125] Fix ! notations --- .../app/lecture/pdf-preview/pdf-preview.component.html | 6 +++--- .../webapp/app/lecture/pdf-preview/pdf-preview.component.ts | 2 +- .../spec/component/lecture/pdf-preview.component.spec.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index a8c6a3c43a65..8883da0aa551 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -6,7 +6,7 @@

@if (attachment()) { {{ attachment()!.id! }}: {{ attachment()!.name! }} - } @else if (attachmentUnit()!) { + } @else if (attachmentUnit()) { {{ attachmentUnit()!.id! }}: {{ attachmentUnit()!.name! }} }

@@ -61,8 +61,8 @@

class="btn btn-default" [routerLink]=" attachment() - ? ['/course-management', course()!.id, 'lectures', attachment()!.lecture?.id, 'attachments'] - : ['/course-management', course()!.id, 'lectures', attachmentUnit()!.lecture?.id, 'unit-management'] + ? ['/course-management', course()!.id, 'lectures', attachment()!.lecture!.id, 'attachments'] + : ['/course-management', course()!.id, 'lectures', attachmentUnit()!.lecture!.id, 'unit-management'] " [ngbTooltip]="'entity.action.view' | artemisTranslate" > diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index ebf74e8c6632..e0b1b95ea690 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -473,7 +473,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { }, }); } else if (this.attachmentUnit()) { - this.lectureUnitService.delete(this.attachmentUnit()!.id!, this.attachmentUnit()!.lecture?.id!).subscribe({ + this.lectureUnitService.delete(this.attachmentUnit()!.id!, this.attachmentUnit()!.lecture!.id!).subscribe({ next: () => { this.router.navigate(['course-management', this.course()!.id, 'lectures', this.attachmentUnit()!.lecture!.id, 'unit-management']); this.dialogErrorSource.next(''); diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts index 8c2044b5fa7d..e3b8a248fb9f 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -86,8 +86,8 @@ describe('PdfPreviewComponent', () => { routeMock = { data: of({ course: { id: 1, name: 'Example Course' }, - attachment: { id: 1, name: 'Example PDF' }, - attachmentUnit: { id: 1, name: 'Chapter 1' }, + attachment: { id: 1, name: 'Example PDF', lecture: { id: 1 } }, + attachmentUnit: { id: 1, name: 'Chapter 1', lecture: { id: 1 } }, }), }; alertServiceMock = { From 024fc4f6493e5d6dc244cd7d4664aedd29e6ae64 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Fri, 25 Oct 2024 11:09:08 +0200 Subject: [PATCH 110/125] Create PDFPreviewThumbnailGrid and PDFPreviewEnlargedCanvas components --not complete --- ...pdf-preview-enlarged-canvas.component.html | 11 + ...pdf-preview-enlarged-canvas.component.scss | 64 ++ .../pdf-preview-enlarged-canvas.component.ts | 231 ++++++ .../pdf-preview-thumbnail-grid.component.html | 5 + .../pdf-preview-thumbnail-grid.component.scss | 22 + .../pdf-preview-thumbnail-grid.component.ts | 184 +++++ .../pdf-preview/pdf-preview.component.html | 24 +- .../pdf-preview/pdf-preview.component.scss | 88 --- .../pdf-preview/pdf-preview.component.ts | 503 ++---------- .../lecture/pdf-preview.component.spec.ts | 713 ------------------ 10 files changed, 585 insertions(+), 1260 deletions(-) create mode 100644 src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.html create mode 100644 src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.scss create mode 100644 src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts create mode 100644 src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html create mode 100644 src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.scss create mode 100644 src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.ts delete mode 100644 src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.html new file mode 100644 index 000000000000..491e723e772f --- /dev/null +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.html @@ -0,0 +1,11 @@ +
+ + + @if (currentPage() !== 1) { + + } + @if (currentPage() !== totalPages()) { + + } +
{{ currentPage() }}
+
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.scss new file mode 100644 index 000000000000..2eeabbadbeae --- /dev/null +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.scss @@ -0,0 +1,64 @@ +.enlarged-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--pdf-preview-enlarged-container-overlay); + z-index: 5; + + .btn-close { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; + color: var(--bs-body-color); + } +} + +.nav-button { + position: absolute; + transform: translateY(-50%); + cursor: pointer; + border-radius: 50%; + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + font-size: 20px; + z-index: 3; +} + +.nav-button.left { + left: calc(5% + 10px); + + @media (max-width: 1200px) { + left: 10px; + } +} + +.nav-button.right { + right: calc(5% + 10px); + + @media (max-width: 1200px) { + right: 10px; + } +} + +.page-number-display { + position: absolute; + bottom: 10px; + right: calc(5% + 10px); + font-size: 18px; + color: var(--bs-body-color); + z-index: 2; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + + @media (max-width: 1200px) { + right: 10px; + } +} diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts new file mode 100644 index 000000000000..2d66d9bbefcd --- /dev/null +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts @@ -0,0 +1,231 @@ +import { Component, ElementRef, HostListener, contentChild, input, signal } from '@angular/core'; +import 'pdfjs-dist/build/pdf.worker'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; + +type NavigationDirection = 'next' | 'prev'; + +@Component({ + selector: 'jhi-pdf-preview-enlarged-canvas-component', + templateUrl: './pdf-preview-enlarged-canvas.component.html', + styleUrls: ['./pdf-preview-enlarged-canvas.component.scss'], + standalone: true, + imports: [ArtemisSharedModule], +}) +export class PdfPreviewEnlargedCanvasComponent { + enlargedCanvas = contentChild.required>('enlargedCanvas'); + + readonly DEFAULT_SLIDE_HEIGHT = 800; + + pdfContainer = input.required(); + + isEnlargedView = signal(false); + currentPage = signal(1); + totalPages = signal(0); + + /** + * Handles navigation within the PDF viewer using keyboard arrow keys. + * @param event - The keyboard event captured for navigation. + */ + @HostListener('document:keydown', ['$event']) + handleKeyboardEvents(event: KeyboardEvent) { + if (this.isEnlargedView()) { + if (event.key === 'ArrowRight' && this.currentPage() < this.totalPages()) { + this.navigatePages('next'); + } else if (event.key === 'ArrowLeft' && this.currentPage() > 1) { + this.navigatePages('prev'); + } + } + } + + /** + * Adjusts the canvas size based on the window resize event to ensure proper display. + */ + @HostListener('window:resize') + resizeCanvasBasedOnContainer() { + this.adjustCanvasSize(); + } + + /** + * Dynamically updates the canvas size within an enlarged view based on the viewport. + */ + adjustCanvasSize = () => { + if (this.isEnlargedView()) { + const canvasElements = this.pdfContainer().querySelectorAll('.pdf-canvas-container canvas'); + if (this.currentPage() - 1 < canvasElements.length) { + const canvas = canvasElements[this.currentPage() - 1] as HTMLCanvasElement; + this.updateEnlargedCanvas(canvas); + } + } + }; + + /** + * Closes the enlarged view if a click event occurs outside the actual canvas area but within the enlarged container. + * @param event The mouse event captured, used to determine the location of the click. + */ + closeIfOutside(event: MouseEvent): void { + const target = event.target as HTMLElement; + const enlargedCanvas = this.enlargedCanvas().nativeElement; + + if (target.classList.contains('enlarged-container') && target !== enlargedCanvas) { + this.closeEnlargedView(event); + } + } + + /** + * Closes the enlarged view of the PDF and re-enables scrolling in the PDF container. + */ + closeEnlargedView(event: MouseEvent) { + this.isEnlargedView.set(false); + this.adjustPdfContainerSize(false); + this.toggleBodyScroll(false); + event.stopPropagation(); + } + + /** + * Handles navigation between PDF pages and stops event propagation to prevent unwanted side effects. + * @param direction The direction to navigate. + * @param event The MouseEvent to be stopped. + */ + handleNavigation(direction: NavigationDirection, event: MouseEvent): void { + event.stopPropagation(); + this.navigatePages(direction); + } + + /** + * Navigates to a specific page in the PDF based on the direction relative to the current page. + * @param direction The navigation direction (next or previous). + */ + navigatePages(direction: NavigationDirection) { + const nextPageIndex = direction === 'next' ? this.currentPage() + 1 : this.currentPage() - 1; + if (nextPageIndex > 0 && nextPageIndex <= this.totalPages()) { + this.currentPage.set(nextPageIndex); + const canvas = this.pdfContainer().querySelectorAll('.pdf-canvas-container canvas')[this.currentPage() - 1] as HTMLCanvasElement; + this.updateEnlargedCanvas(canvas); + } + } + + /** + * Updates the enlarged canvas dimensions to optimize PDF page display within the current viewport. + * This method dynamically adjusts the size, position, and scale of the canvas to maintain the aspect ratio, + * ensuring the content is centered and displayed appropriately within the available space. + * It is called within an animation frame to synchronize updates with the browser's render cycle for smooth visuals. + * + * @param originalCanvas - The source canvas element used to extract image data for resizing and redrawing. + */ + updateEnlargedCanvas(originalCanvas: HTMLCanvasElement) { + requestAnimationFrame(() => { + if (!this.isEnlargedView) return; + + const isVertical = originalCanvas.height > originalCanvas.width; + this.adjustPdfContainerSize(isVertical); + + const scaleFactor = this.calculateScaleFactor(originalCanvas); + this.resizeCanvas(originalCanvas, scaleFactor); + this.redrawCanvas(originalCanvas); + this.positionCanvas(); + }); + } + + /** + * Calculates the scaling factor to adjust the canvas size based on the dimensions of the container. + * This method ensures that the canvas is scaled to fit within the container without altering the aspect ratio. + * + * @param originalCanvas - The original canvas element representing the PDF page. + * @returns The scaling factor used to resize the original canvas to fit within the container dimensions. + */ + calculateScaleFactor(originalCanvas: HTMLCanvasElement): number { + const containerWidth = this.pdfContainer().clientWidth; + const containerHeight = this.pdfContainer().clientHeight; + + let scaleX, scaleY; + + if (originalCanvas.height > originalCanvas.width) { + // Vertical slide + const fixedHeight = this.DEFAULT_SLIDE_HEIGHT; + scaleY = fixedHeight / originalCanvas.height; + scaleX = containerWidth / originalCanvas.width; + } else { + // Horizontal slide + scaleX = containerWidth / originalCanvas.width; + scaleY = containerHeight / originalCanvas.height; + } + + return Math.min(scaleX, scaleY); + } + + /** + * Resizes the canvas according to the computed scale factor. + * This method updates the dimensions of the enlarged canvas element to ensure that the entire PDF page + * is visible and properly scaled within the viewer. + * + * @param originalCanvas - The canvas element from which the image is scaled. + * @param scaleFactor - The factor by which the canvas is resized. + */ + resizeCanvas(originalCanvas: HTMLCanvasElement, scaleFactor: number): void { + const enlargedCanvas = this.enlargedCanvas().nativeElement; + enlargedCanvas.width = originalCanvas.width * scaleFactor; + enlargedCanvas.height = originalCanvas.height * scaleFactor; + } + + /** + * Redraws the original canvas content onto the enlarged canvas at the updated scale. + * This method ensures that the image is rendered clearly and correctly positioned on the enlarged canvas. + * + * @param originalCanvas - The original canvas containing the image to be redrawn. + */ + redrawCanvas(originalCanvas: HTMLCanvasElement): void { + const enlargedCanvas = this.enlargedCanvas().nativeElement; + const context = enlargedCanvas.getContext('2d'); + context!.clearRect(0, 0, enlargedCanvas.width, enlargedCanvas.height); + context!.drawImage(originalCanvas, 0, 0, enlargedCanvas.width, enlargedCanvas.height); + } + + /** + * Adjusts the position of the enlarged canvas to center it within the viewport of the PDF container. + * This method ensures that the canvas is both vertically and horizontally centered, providing a consistent + * and visually appealing layout. + */ + positionCanvas(): void { + const enlargedCanvas = this.enlargedCanvas().nativeElement; + const containerWidth = this.pdfContainer().clientWidth; + const containerHeight = this.pdfContainer().clientHeight; + + enlargedCanvas.style.position = 'absolute'; + enlargedCanvas.style.left = `${(containerWidth - enlargedCanvas.width) / 2}px`; + enlargedCanvas.style.top = `${(containerHeight - enlargedCanvas.height) / 2}px`; + enlargedCanvas.parentElement!.style.top = `${this.pdfContainer().scrollTop}px`; + } + + /** + * Adjusts the size of the PDF container based on whether the enlarged view is active or not. + * If the enlarged view is active, the container's size is reduced to focus on the enlarged content. + * If the enlarged view is closed, the container returns to its original size. + * + * @param isVertical A boolean flag indicating whether to enlarge or reset the container size. + */ + adjustPdfContainerSize(isVertical: boolean): void { + const pdfContainer = this.pdfContainer(); + if (isVertical) { + pdfContainer.style.height = `${this.DEFAULT_SLIDE_HEIGHT}px`; + } else { + pdfContainer.style.height = '60vh'; + } + } + + /** + * Toggles the ability to scroll through the PDF container. + * @param disable A boolean flag indicating whether scrolling should be disabled (`true`) or enabled (`false`). + */ + toggleBodyScroll(disable: boolean): void { + this.pdfContainer().style.overflow = disable ? 'hidden' : 'auto'; + } + + displayEnlargedCanvas(originalCanvas: HTMLCanvasElement, isVertical: boolean) { + this.adjustPdfContainerSize(isVertical); + this.currentPage.set(Number(originalCanvas.id)); + this.toggleBodyScroll(true); + setTimeout(() => { + this.updateEnlargedCanvas(originalCanvas); + }, 500); + } +} diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html new file mode 100644 index 000000000000..6588531c923a --- /dev/null +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html @@ -0,0 +1,5 @@ +
+ @if (isEnlargedView()) { + + } +
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.scss new file mode 100644 index 000000000000..0f1dd15bb584 --- /dev/null +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.scss @@ -0,0 +1,22 @@ +.pdf-container { + position: relative; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); + gap: 10px; + height: 60vh; + overflow-y: auto; + border: 1px solid var(--border-color); + padding: 10px; + margin: 10px 0; + width: 100%; + box-shadow: 0 2px 5px var(--pdf-preview-pdf-container-shadow); + z-index: 0; + + @media (max-width: 800px) { + grid-template-columns: repeat(auto-fit, minmax(270px, 1fr)); + } + + @media (max-width: 500px) { + grid-template-columns: 1fr; + } +} diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.ts new file mode 100644 index 000000000000..4facf9c6800c --- /dev/null +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.ts @@ -0,0 +1,184 @@ +import { Component, ElementRef, OnInit, inject, input, output, signal, viewChild } from '@angular/core'; +import * as PDFJS from 'pdfjs-dist'; +import 'pdfjs-dist/build/pdf.worker'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { onError } from 'app/shared/util/global.utils'; +import { AlertService } from 'app/core/util/alert.service'; +import { PdfPreviewEnlargedCanvasComponent } from 'app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component'; + +@Component({ + selector: 'jhi-pdf-preview-thumbnail-grid-component', + templateUrl: './pdf-preview-thumbnail-grid.component.html', + styleUrls: ['./pdf-preview-thumbnail-grid.component.scss'], + standalone: true, + imports: [ArtemisSharedModule, PdfPreviewEnlargedCanvasComponent], +}) +export class PdfPreviewThumbnailGridComponent implements OnInit { + pdfContainer = viewChild.required>('pdfContainer'); + enlargedCanvasComponent = viewChild.required('enlargedCanvasComponent'); + + readonly DEFAULT_SLIDE_WIDTH = 250; + readonly DEFAULT_SLIDE_HEIGHT = 800; + + currentPdfUrl = input(); + + isPdfLoading = output(); + + isEnlargedView = signal(false); + totalPages = signal(0); + selectedPages = signal>(new Set()); + + private readonly alertService = inject(AlertService); + + ngOnInit() { + this.loadOrAppendPdf(this.currentPdfUrl()!, false); + } + + /** + * Loads or appends a PDF from a provided URL. + * @param fileUrl The URL of the file to load or append. + * @param append Whether the document should be appended to the existing one. + * @returns A promise that resolves when the PDF is loaded. + */ + async loadOrAppendPdf(fileUrl: string, append = false): Promise { + this.pdfContainer() + .nativeElement.querySelectorAll('.pdf-canvas-container') + .forEach((canvas) => canvas.remove()); + this.totalPages.set(0); + this.isPdfLoading.emit(true); + try { + const loadingTask = PDFJS.getDocument(fileUrl); + const pdf = await loadingTask.promise; + this.totalPages.set(pdf.numPages); + + for (let i = 1; i <= this.totalPages(); i++) { + const page = await pdf.getPage(i); + const viewport = page.getViewport({ scale: 2 }); + const canvas = this.createCanvas(viewport, i); + const context = canvas.getContext('2d'); + await page.render({ canvasContext: context!, viewport }).promise; + + const canvasContainer = this.createCanvasContainer(canvas, i); + this.pdfContainer().nativeElement.appendChild(canvasContainer); + } + + if (append) { + this.scrollToBottom(); + } + } catch (error) { + onError(this.alertService, error); + } finally { + this.isPdfLoading.emit(false); + if (append) { + //this.fileInput()!.nativeElement.value = ''; + } + } + } + + /** + * Scrolls the PDF container to the bottom after appending new pages. + */ + scrollToBottom(): void { + const scrollOptions: ScrollToOptions = { + top: this.pdfContainer().nativeElement.scrollHeight, + left: 0, + behavior: 'smooth' as ScrollBehavior, + }; + this.pdfContainer().nativeElement.scrollTo(scrollOptions); + } + + /** + * Creates a canvas for each page of the PDF to allow for individual page rendering. + * @param viewport The viewport settings used for rendering the page. + * @param pageIndex The index of the page within the PDF document. + * @returns A new HTMLCanvasElement configured for the PDF page. + */ + createCanvas(viewport: PDFJS.PageViewport, pageIndex: number): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + canvas.id = `${pageIndex}`; + /* Canvas styling is predefined because Canvas tags do not support CSS classes + * as they are not HTML elements but rather a bitmap drawing surface. + * See: https://stackoverflow.com/a/29675448 + * */ + canvas.height = viewport.height; + canvas.width = viewport.width; + const fixedWidth = this.DEFAULT_SLIDE_WIDTH; + const scaleFactor = fixedWidth / viewport.width; + canvas.style.width = `${fixedWidth}px`; + canvas.style.height = `${viewport.height * scaleFactor}px`; + return canvas; + } + + /** + * Creates a container div for each canvas, facilitating layering and interaction. + * @param canvas The canvas element that displays a PDF page. + * @param pageIndex The index of the page within the PDF document. + * @returns A configured div element that includes the canvas and interactive overlays. + */ + createCanvasContainer(canvas: HTMLCanvasElement, pageIndex: number): HTMLDivElement { + const container = document.createElement('div'); + /* Dynamically created elements are not detected by DOM, that is why we need to set the styles manually. + * See: https://stackoverflow.com/a/70911189 + */ + container.id = `pdf-page-${pageIndex}`; + container.classList.add('pdf-canvas-container'); + container.style.cssText = `position: relative; display: inline-block; width: ${canvas.style.width}; height: ${canvas.style.height}; margin: 20px; box-shadow: 0 2px 6px var(--pdf-preview-canvas-shadow);`; + + const overlay = this.createOverlay(pageIndex); + const checkbox = this.createCheckbox(pageIndex); + container.appendChild(canvas); + container.appendChild(overlay); + container.appendChild(checkbox); + + container.addEventListener('mouseenter', () => { + overlay.style.opacity = '1'; + }); + container.addEventListener('mouseleave', () => { + overlay.style.opacity = '0'; + }); + overlay.addEventListener('click', () => this.displayEnlargedCanvas(canvas)); + + return container; + } + + /** + * Generates an interactive overlay for each PDF page to allow for user interactions. + * @param pageIndex The index of the page. + * @returns A div element styled as an overlay. + */ + private createOverlay(pageIndex: number): HTMLDivElement { + const overlay = document.createElement('div'); + overlay.innerHTML = `${pageIndex}`; + /* Dynamically created elements are not detected by DOM, that is why we need to set the styles manually. + * See: https://stackoverflow.com/a/70911189 + */ + overlay.style.cssText = `position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; font-size: 24px; color: white; z-index: 1; transition: opacity 0.3s ease; opacity: 0; cursor: pointer; background-color: var(--pdf-preview-container-overlay)`; + return overlay; + } + + private createCheckbox(pageIndex: number): HTMLDivElement { + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.id = String(pageIndex); + checkbox.style.cssText = `position: absolute; top: -5px; right: -5px; z-index: 4;`; + checkbox.checked = this.selectedPages().has(pageIndex); + checkbox.addEventListener('change', () => { + if (checkbox.checked) { + this.selectedPages().add(Number(checkbox.id)); + } else { + this.selectedPages().delete(Number(checkbox.id)); + } + }); + return checkbox; + } + + /** + * Displays the selected PDF page in an enlarged view for detailed examination. + * @param originalCanvas - The original canvas element of the PDF page to be enlarged. + * */ + displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { + const isVertical = originalCanvas.height > originalCanvas.width; + this.isEnlargedView.set(true); + this.enlargedCanvasComponent().displayEnlargedCanvas(originalCanvas, isVertical); + } +} diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 8883da0aa551..7573891fde0c 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -26,34 +26,22 @@

[deleteQuestion]="allPagesSelected() ? 'artemisApp.attachment.pdfPreview.deleteAllPagesQuestion' : 'artemisApp.attachment.pdfPreview.deletePagesQuestion'" (delete)="allPagesSelected() ? deleteAttachmentFile() : deleteSelectedSlides()" [dialogError]="dialogError$" - [disabled]="isPdfLoading() || selectedPages().size === 0" + [disabled]="selectedPages()!.size === 0" aria-label="Delete selected pages" > - +

-
- @if (isEnlargedView()) { -
- - - @if (currentPage() !== 1) { - - } - @if (currentPage() !== totalPages()) { - - } -
{{ currentPage() }}
-
- } -
+ @if (currentPdfUrl()) { + + }
- diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss index 967dc25f53f8..d0215c72e7f6 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -1,91 +1,3 @@ -.pdf-container { - position: relative; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); - gap: 10px; - height: 60vh; - overflow-y: auto; - border: 1px solid var(--border-color); - padding: 10px; - margin: 10px 0; - width: 100%; - box-shadow: 0 2px 5px var(--pdf-preview-pdf-container-shadow); - z-index: 0; - - @media (max-width: 800px) { - grid-template-columns: repeat(auto-fit, minmax(270px, 1fr)); - } - - @media (max-width: 500px) { - grid-template-columns: 1fr; - } -} - -.enlarged-container { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - background-color: var(--pdf-preview-enlarged-container-overlay); - z-index: 5; - - .btn-close { - position: absolute; - top: 10px; - right: 10px; - cursor: pointer; - color: var(--bs-body-color); - } -} - -.nav-button { - position: absolute; - transform: translateY(-50%); - cursor: pointer; - border-radius: 50%; - width: 30px; - height: 30px; - display: flex; - justify-content: center; - align-items: center; - font-size: 20px; - z-index: 3; -} - -.nav-button.left { - left: calc(5% + 10px); - - @media (max-width: 1200px) { - left: 10px; - } -} - -.nav-button.right { - right: calc(5% + 10px); - - @media (max-width: 1200px) { - right: 10px; - } -} - -.page-number-display { - position: absolute; - bottom: 10px; - right: calc(5% + 10px); - font-size: 18px; - color: var(--bs-body-color); - z-index: 2; - text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); - - @media (max-width: 1200px) { - right: 10px; - } -} - .spinner-border { margin-left: 10px; } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index e0b1b95ea690..4f802b8e84cd 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -1,7 +1,6 @@ -import { Component, ElementRef, HostListener, OnDestroy, OnInit, inject, signal, viewChild } from '@angular/core'; +import { Component, ElementRef, OnDestroy, OnInit, inject, input, signal, viewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { AttachmentService } from 'app/lecture/attachment.service'; -import * as PDFJS from 'pdfjs-dist'; import 'pdfjs-dist/build/pdf.worker'; import { Attachment } from 'app/entities/attachment.model'; import { AttachmentUnit } from 'app/entities/lecture-unit/attachmentUnit.model'; @@ -13,42 +12,38 @@ import { Course } from 'app/entities/course.model'; import { HttpErrorResponse } from '@angular/common/http'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { faFileImport, faSave, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons'; -import { PDFDocument } from 'pdf-lib'; import dayjs from 'dayjs/esm'; import { objectToJsonBlob } from 'app/utils/blob-util'; import { MAX_FILE_SIZE } from 'app/shared/constants/input.constants'; +import { PdfPreviewThumbnailGridComponent } from 'app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component'; import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; - -type NavigationDirection = 'next' | 'prev'; +import { PDFDocument } from 'pdf-lib'; @Component({ selector: 'jhi-pdf-preview-component', templateUrl: './pdf-preview.component.html', styleUrls: ['./pdf-preview.component.scss'], standalone: true, - imports: [ArtemisSharedModule], + imports: [ArtemisSharedModule, PdfPreviewThumbnailGridComponent], }) export class PdfPreviewComponent implements OnInit, OnDestroy { - pdfContainer = viewChild.required>('pdfContainer'); - enlargedCanvas = viewChild.required>('enlargedCanvas'); fileInput = viewChild.required>('fileInput'); attachmentSub: Subscription; attachmentUnitSub: Subscription; - readonly DEFAULT_SLIDE_WIDTH = 250; - readonly DEFAULT_SLIDE_HEIGHT = 800; course = signal(undefined); attachment = signal(undefined); attachmentUnit = signal(undefined); - isEnlargedView = signal(false); - isFileChanged = signal(false); - currentPage = signal(1); - totalPages = signal(0); - selectedPages = signal>(new Set()); isPdfLoading = signal(false); attachmentToBeEdited = signal(undefined); - currentPdfBlob = signal(null); + currentPdfBlob = signal(undefined); + currentPdfUrl = signal(undefined); + totalPages = signal(0); + isFileChanged = signal(false); + + allPagesSelected = input(); + selectedPages = input>(new Set()); // Injected services private readonly route = inject(ActivatedRoute); @@ -62,9 +57,9 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { dialogError$ = this.dialogErrorSource.asObservable(); // Icons - protected readonly faFileImport = faFileImport; protected readonly faSave = faSave; protected readonly faTimes = faTimes; + protected readonly faFileImport = faFileImport; protected readonly faTrash = faTrash; ngOnInit() { @@ -74,386 +69,75 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { if ('attachment' in data) { this.attachment.set(data.attachment); this.attachmentSub = this.attachmentService.getAttachmentFile(this.course()!.id!, this.attachment()!.id!).subscribe({ - next: (blob: Blob) => this.handleBlob(blob), + next: (blob: Blob) => { + this.currentPdfBlob.set(blob); + this.currentPdfUrl.set(URL.createObjectURL(blob)); + }, error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } else if ('attachmentUnit' in data) { this.attachmentUnit.set(data.attachmentUnit); this.attachmentUnitSub = this.attachmentUnitService.getAttachmentFile(this.course()!.id!, this.attachmentUnit()!.id!).subscribe({ - next: (blob: Blob) => this.handleBlob(blob), + next: (blob: Blob) => { + this.currentPdfBlob.set(blob); + this.currentPdfUrl.set(URL.createObjectURL(blob)); + }, error: (error: HttpErrorResponse) => onError(this.alertService, error), }); } }); } - handleBlob(blob: Blob): void { - this.currentPdfBlob.set(blob); - const objectUrl = URL.createObjectURL(blob); - this.loadOrAppendPdf(objectUrl).then(() => URL.revokeObjectURL(objectUrl)); - } - ngOnDestroy() { this.attachmentSub?.unsubscribe(); this.attachmentUnitSub?.unsubscribe(); } - /** - * Checks if all pages are selected. - * @returns True if the number of selected pages equals the total number of pages, otherwise false. - */ - allPagesSelected() { - return this.selectedPages().size === this.totalPages(); - } - - /** - * Handles navigation within the PDF viewer using keyboard arrow keys. - * @param event - The keyboard event captured for navigation. - */ - @HostListener('document:keydown', ['$event']) - handleKeyboardEvents(event: KeyboardEvent) { - if (this.isEnlargedView()) { - if (event.key === 'ArrowRight' && this.currentPage() < this.totalPages()) { - this.navigatePages('next'); - } else if (event.key === 'ArrowLeft' && this.currentPage() > 1) { - this.navigatePages('prev'); - } - } - } - - /** - * Adjusts the canvas size based on the window resize event to ensure proper display. - */ - @HostListener('window:resize') - resizeCanvasBasedOnContainer() { - this.adjustCanvasSize(); - } - - /** - * Loads or appends a PDF from a provided URL. - * @param fileUrl The URL of the file to load or append. - * @param append Whether the document should be appended to the existing one. - * @returns A promise that resolves when the PDF is loaded. - */ - async loadOrAppendPdf(fileUrl: string, append = false): Promise { - this.pdfContainer() - .nativeElement.querySelectorAll('.pdf-canvas-container') - .forEach((canvas) => canvas.remove()); - this.totalPages.set(0); - this.isPdfLoading.set(true); - try { - const loadingTask = PDFJS.getDocument(fileUrl); - const pdf = await loadingTask.promise; - this.totalPages.set(pdf.numPages); - - for (let i = 1; i <= this.totalPages(); i++) { - const page = await pdf.getPage(i); - const viewport = page.getViewport({ scale: 2 }); - const canvas = this.createCanvas(viewport, i); - const context = canvas.getContext('2d'); - await page.render({ canvasContext: context!, viewport }).promise; - - const canvasContainer = this.createCanvasContainer(canvas, i); - this.pdfContainer().nativeElement.appendChild(canvasContainer); - } - - if (append) { - this.scrollToBottom(); - } - } catch (error) { - onError(this.alertService, error); - } finally { - this.isPdfLoading.set(false); - if (append) { - this.fileInput().nativeElement.value = ''; - } - } + receiveIsPdfLoading($event: boolean) { + this.isPdfLoading.set($event); } - /** - * Scrolls the PDF container to the bottom after appending new pages. - */ - scrollToBottom(): void { - const scrollOptions: ScrollToOptions = { - top: this.pdfContainer().nativeElement.scrollHeight, - left: 0, - behavior: 'smooth' as ScrollBehavior, - }; - this.pdfContainer().nativeElement.scrollTo(scrollOptions); - } - - /** - * Creates a canvas for each page of the PDF to allow for individual page rendering. - * @param viewport The viewport settings used for rendering the page. - * @param pageIndex The index of the page within the PDF document. - * @returns A new HTMLCanvasElement configured for the PDF page. - */ - createCanvas(viewport: PDFJS.PageViewport, pageIndex: number): HTMLCanvasElement { - const canvas = document.createElement('canvas'); - canvas.id = `${pageIndex}`; - /* Canvas styling is predefined because Canvas tags do not support CSS classes - * as they are not HTML elements but rather a bitmap drawing surface. - * See: https://stackoverflow.com/a/29675448 - * */ - canvas.height = viewport.height; - canvas.width = viewport.width; - const fixedWidth = this.DEFAULT_SLIDE_WIDTH; - const scaleFactor = fixedWidth / viewport.width; - canvas.style.width = `${fixedWidth}px`; - canvas.style.height = `${viewport.height * scaleFactor}px`; - return canvas; - } - - /** - * Creates a container div for each canvas, facilitating layering and interaction. - * @param canvas The canvas element that displays a PDF page. - * @param pageIndex The index of the page within the PDF document. - * @returns A configured div element that includes the canvas and interactive overlays. - */ - createCanvasContainer(canvas: HTMLCanvasElement, pageIndex: number): HTMLDivElement { - const container = document.createElement('div'); - /* Dynamically created elements are not detected by DOM, that is why we need to set the styles manually. - * See: https://stackoverflow.com/a/70911189 - */ - container.id = `pdf-page-${pageIndex}`; - container.classList.add('pdf-canvas-container'); - container.style.cssText = `position: relative; display: inline-block; width: ${canvas.style.width}; height: ${canvas.style.height}; margin: 20px; box-shadow: 0 2px 6px var(--pdf-preview-canvas-shadow);`; - - const overlay = this.createOverlay(pageIndex); - const checkbox = this.createCheckbox(pageIndex); - container.appendChild(canvas); - container.appendChild(overlay); - container.appendChild(checkbox); - - container.addEventListener('mouseenter', () => { - overlay.style.opacity = '1'; - }); - container.addEventListener('mouseleave', () => { - overlay.style.opacity = '0'; - }); - overlay.addEventListener('click', () => this.displayEnlargedCanvas(canvas)); - - return container; - } - - /** - * Generates an interactive overlay for each PDF page to allow for user interactions. - * @param pageIndex The index of the page. - * @returns A div element styled as an overlay. - */ - private createOverlay(pageIndex: number): HTMLDivElement { - const overlay = document.createElement('div'); - overlay.innerHTML = `${pageIndex}`; - /* Dynamically created elements are not detected by DOM, that is why we need to set the styles manually. - * See: https://stackoverflow.com/a/70911189 - */ - overlay.style.cssText = `position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; font-size: 24px; color: white; z-index: 1; transition: opacity 0.3s ease; opacity: 0; cursor: pointer; background-color: var(--pdf-preview-container-overlay)`; - return overlay; - } - - private createCheckbox(pageIndex: number): HTMLDivElement { - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.id = String(pageIndex); - checkbox.style.cssText = `position: absolute; top: -5px; right: -5px; z-index: 4;`; - checkbox.checked = this.selectedPages().has(pageIndex); - checkbox.addEventListener('change', () => { - if (checkbox.checked) { - this.selectedPages().add(Number(checkbox.id)); - } else { - this.selectedPages().delete(Number(checkbox.id)); - } - }); - return checkbox; - } - - /** - * Dynamically updates the canvas size within an enlarged view based on the viewport. - */ - adjustCanvasSize = () => { - if (this.isEnlargedView()) { - const canvasElements = this.pdfContainer().nativeElement.querySelectorAll('.pdf-canvas-container canvas'); - if (this.currentPage() - 1 < canvasElements.length) { - const canvas = canvasElements[this.currentPage() - 1] as HTMLCanvasElement; - this.updateEnlargedCanvas(canvas); - } - } - }; - - /** - * Adjusts the size of the PDF container based on whether the enlarged view is active or not. - * If the enlarged view is active, the container's size is reduced to focus on the enlarged content. - * If the enlarged view is closed, the container returns to its original size. - * - * @param isVertical A boolean flag indicating whether to enlarge or reset the container size. - */ - adjustPdfContainerSize(isVertical: boolean): void { - const pdfContainer = this.pdfContainer().nativeElement; - if (isVertical) { - pdfContainer.style.height = `${this.DEFAULT_SLIDE_HEIGHT}px`; - } else { - pdfContainer.style.height = '60vh'; - } - } - - /** - * Displays the selected PDF page in an enlarged view for detailed examination. - * @param originalCanvas - The original canvas element of the PDF page to be enlarged. - * */ - displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { - const isVertical = originalCanvas.height > originalCanvas.width; - this.adjustPdfContainerSize(isVertical); - this.isEnlargedView.set(true); - this.currentPage.set(Number(originalCanvas.id)); - this.toggleBodyScroll(true); - setTimeout(() => { - this.updateEnlargedCanvas(originalCanvas); - }, 50); - } - - /** - * Updates the enlarged canvas dimensions to optimize PDF page display within the current viewport. - * This method dynamically adjusts the size, position, and scale of the canvas to maintain the aspect ratio, - * ensuring the content is centered and displayed appropriately within the available space. - * It is called within an animation frame to synchronize updates with the browser's render cycle for smooth visuals. - * - * @param originalCanvas - The source canvas element used to extract image data for resizing and redrawing. - */ - updateEnlargedCanvas(originalCanvas: HTMLCanvasElement) { - requestAnimationFrame(() => { - if (!this.isEnlargedView) return; - - const isVertical = originalCanvas.height > originalCanvas.width; - this.adjustPdfContainerSize(isVertical); - - const scaleFactor = this.calculateScaleFactor(originalCanvas); - this.resizeCanvas(originalCanvas, scaleFactor); - this.redrawCanvas(originalCanvas); - this.positionCanvas(); - }); - } - - /** - * Calculates the scaling factor to adjust the canvas size based on the dimensions of the container. - * This method ensures that the canvas is scaled to fit within the container without altering the aspect ratio. - * - * @param originalCanvas - The original canvas element representing the PDF page. - * @returns The scaling factor used to resize the original canvas to fit within the container dimensions. - */ - calculateScaleFactor(originalCanvas: HTMLCanvasElement): number { - const containerWidth = this.pdfContainer().nativeElement.clientWidth; - const containerHeight = this.pdfContainer().nativeElement.clientHeight; - - let scaleX, scaleY; + updateAttachmentWithFile(): void { + const pdfFile = new File([this.currentPdfBlob()!], 'updatedAttachment.pdf', { type: 'application/pdf' }); - if (originalCanvas.height > originalCanvas.width) { - // Vertical slide - const fixedHeight = this.DEFAULT_SLIDE_HEIGHT; - scaleY = fixedHeight / originalCanvas.height; - scaleX = containerWidth / originalCanvas.width; - } else { - // Horizontal slide - scaleX = containerWidth / originalCanvas.width; - scaleY = containerHeight / originalCanvas.height; + if (pdfFile.size > MAX_FILE_SIZE) { + this.alertService.error('artemisApp.attachment.pdfPreview.fileSizeError'); + return; } - return Math.min(scaleX, scaleY); - } - - /** - * Resizes the canvas according to the computed scale factor. - * This method updates the dimensions of the enlarged canvas element to ensure that the entire PDF page - * is visible and properly scaled within the viewer. - * - * @param originalCanvas - The canvas element from which the image is scaled. - * @param scaleFactor - The factor by which the canvas is resized. - */ - resizeCanvas(originalCanvas: HTMLCanvasElement, scaleFactor: number): void { - const enlargedCanvas = this.enlargedCanvas().nativeElement; - enlargedCanvas.width = originalCanvas.width * scaleFactor; - enlargedCanvas.height = originalCanvas.height * scaleFactor; - } - - /** - * Redraws the original canvas content onto the enlarged canvas at the updated scale. - * This method ensures that the image is rendered clearly and correctly positioned on the enlarged canvas. - * - * @param originalCanvas - The original canvas containing the image to be redrawn. - */ - redrawCanvas(originalCanvas: HTMLCanvasElement): void { - const enlargedCanvas = this.enlargedCanvas().nativeElement; - const context = enlargedCanvas.getContext('2d'); - context!.clearRect(0, 0, enlargedCanvas.width, enlargedCanvas.height); - context!.drawImage(originalCanvas, 0, 0, enlargedCanvas.width, enlargedCanvas.height); - } - - /** - * Adjusts the position of the enlarged canvas to center it within the viewport of the PDF container. - * This method ensures that the canvas is both vertically and horizontally centered, providing a consistent - * and visually appealing layout. - */ - positionCanvas(): void { - const enlargedCanvas = this.enlargedCanvas().nativeElement; - const containerWidth = this.pdfContainer().nativeElement.clientWidth; - const containerHeight = this.pdfContainer().nativeElement.clientHeight; - - enlargedCanvas.style.position = 'absolute'; - enlargedCanvas.style.left = `${(containerWidth - enlargedCanvas.width) / 2}px`; - enlargedCanvas.style.top = `${(containerHeight - enlargedCanvas.height) / 2}px`; - enlargedCanvas.parentElement!.style.top = `${this.pdfContainer().nativeElement.scrollTop}px`; - } - - /** - * Closes the enlarged view of the PDF and re-enables scrolling in the PDF container. - */ - closeEnlargedView(event: MouseEvent) { - this.isEnlargedView.set(false); - this.adjustPdfContainerSize(false); - this.toggleBodyScroll(false); - event.stopPropagation(); - } - - /** - * Toggles the ability to scroll through the PDF container. - * @param disable A boolean flag indicating whether scrolling should be disabled (`true`) or enabled (`false`). - */ - toggleBodyScroll(disable: boolean): void { - this.pdfContainer().nativeElement.style.overflow = disable ? 'hidden' : 'auto'; - } - - /** - * Closes the enlarged view if a click event occurs outside the actual canvas area but within the enlarged container. - * @param event The mouse event captured, used to determine the location of the click. - */ - closeIfOutside(event: MouseEvent): void { - const target = event.target as HTMLElement; - const enlargedCanvas = this.enlargedCanvas().nativeElement; + if (this.attachment()) { + this.attachmentToBeEdited.set(this.attachment()); + this.attachmentToBeEdited()!.version!++; + this.attachmentToBeEdited()!.uploadDate = dayjs(); - if (target.classList.contains('enlarged-container') && target !== enlargedCanvas) { - this.closeEnlargedView(event); - } - } + this.attachmentService.update(this.attachmentToBeEdited()!.id!, this.attachmentToBeEdited()!, pdfFile).subscribe({ + next: () => { + this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); + this.router.navigate(['course-management', this.course()?.id, 'lectures', this.attachment()!.lecture!.id, 'attachments']); + }, + error: (error) => { + this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); + }, + }); + } else if (this.attachmentUnit()) { + this.attachmentToBeEdited.set(this.attachmentUnit()!.attachment!); + this.attachmentToBeEdited()!.version!++; + this.attachmentToBeEdited()!.uploadDate = dayjs(); - /** - * Handles navigation between PDF pages and stops event propagation to prevent unwanted side effects. - * @param direction The direction to navigate. - * @param event The MouseEvent to be stopped. - */ - handleNavigation(direction: NavigationDirection, event: MouseEvent): void { - event.stopPropagation(); - this.navigatePages(direction); - } + const formData = new FormData(); + formData.append('file', pdfFile); + formData.append('attachment', objectToJsonBlob(this.attachmentToBeEdited()!)); + formData.append('attachmentUnit', objectToJsonBlob(this.attachmentUnit()!)); - /** - * Navigates to a specific page in the PDF based on the direction relative to the current page. - * @param direction The navigation direction (next or previous). - */ - navigatePages(direction: NavigationDirection) { - const nextPageIndex = direction === 'next' ? this.currentPage() + 1 : this.currentPage() - 1; - if (nextPageIndex > 0 && nextPageIndex <= this.totalPages()) { - this.currentPage.set(nextPageIndex); - const canvas = this.pdfContainer().nativeElement.querySelectorAll('.pdf-canvas-container canvas')[this.currentPage() - 1] as HTMLCanvasElement; - this.updateEnlargedCanvas(canvas); + this.attachmentUnitService.update(this.attachmentUnit()!.lecture!.id!, this.attachmentUnit()!.id!, formData).subscribe({ + next: () => { + this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); + this.router.navigate(['course-management', this.course()?.id, 'lectures', this.attachmentUnit()!.lecture!.id, 'unit-management']); + }, + error: (error) => { + this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); + }, + }); } } @@ -494,7 +178,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { const existingPdfBytes = await this.currentPdfBlob()!.arrayBuffer(); const pdfDoc = await PDFDocument.load(existingPdfBytes); - const pagesToDelete = Array.from(this.selectedPages()) + const pagesToDelete = Array.from(this.selectedPages()!) .map((page) => page - 1) .sort((a, b) => b - a); pagesToDelete.forEach((pageIndex) => { @@ -504,12 +188,10 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.isFileChanged.set(true); const pdfBytes = await pdfDoc.save(); this.currentPdfBlob.set(new Blob([pdfBytes], { type: 'application/pdf' })); - this.selectedPages().clear(); + this.selectedPages()!.clear(); const objectUrl = URL.createObjectURL(this.currentPdfBlob()!); - await this.loadOrAppendPdf(objectUrl, false).then(() => { - this.dialogErrorSource.next(''); - }); + this.currentPdfUrl.set(objectUrl); URL.revokeObjectURL(objectUrl); } catch (error) { this.alertService.error('artemisApp.attachment.pdfPreview.pageDeleteError', { error: error.message }); @@ -546,75 +228,14 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { const mergedPdfBytes = await existingPdfDoc.save(); this.currentPdfBlob.set(new Blob([mergedPdfBytes], { type: 'application/pdf' })); - this.selectedPages().clear(); + this.selectedPages()!.clear(); const objectUrl = URL.createObjectURL(this.currentPdfBlob()!); - await this.loadOrAppendPdf(objectUrl, true).then(() => URL.revokeObjectURL(objectUrl)); + this.currentPdfUrl.set(objectUrl); } catch (error) { this.alertService.error('artemisApp.attachment.pdfPreview.mergeFailedError', { error: error.message }); } finally { this.isPdfLoading.set(false); } } - - /** - * Updates the IDs of remaining pages after some have been removed. - */ - updatePageIDs() { - const remainingPages = this.pdfContainer().nativeElement.querySelectorAll('.pdf-canvas-container'); - remainingPages.forEach((container, index) => { - const pageIndex = index + 1; - container.id = `pdf-page-${pageIndex}`; - const canvas = container.querySelector('canvas'); - const overlay = container.querySelector('div'); - const checkbox = container.querySelector('input[type="checkbox"]'); - canvas!.id = String(pageIndex); - overlay!.innerHTML = `${pageIndex}`; - checkbox!.id = String(pageIndex); - }); - } - - updateAttachmentWithFile(): void { - const pdfFile = new File([this.currentPdfBlob()!], 'updatedAttachment.pdf', { type: 'application/pdf' }); - - if (pdfFile.size > MAX_FILE_SIZE) { - this.alertService.error('artemisApp.attachment.pdfPreview.fileSizeError'); - return; - } - - if (this.attachment()) { - this.attachmentToBeEdited.set(this.attachment()); - this.attachmentToBeEdited()!.version!++; - this.attachmentToBeEdited()!.uploadDate = dayjs(); - - this.attachmentService.update(this.attachmentToBeEdited()!.id!, this.attachmentToBeEdited()!, pdfFile).subscribe({ - next: () => { - this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); - this.router.navigate(['course-management', this.course()?.id, 'lectures', this.attachment()!.lecture!.id, 'attachments']); - }, - error: (error) => { - this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); - }, - }); - } else if (this.attachmentUnit()) { - this.attachmentToBeEdited.set(this.attachmentUnit()!.attachment!); - this.attachmentToBeEdited()!.version!++; - this.attachmentToBeEdited()!.uploadDate = dayjs(); - - const formData = new FormData(); - formData.append('file', pdfFile); - formData.append('attachment', objectToJsonBlob(this.attachmentToBeEdited()!)); - formData.append('attachmentUnit', objectToJsonBlob(this.attachmentUnit()!)); - - this.attachmentUnitService.update(this.attachmentUnit()!.lecture!.id!, this.attachmentUnit()!.id!, formData).subscribe({ - next: () => { - this.alertService.success('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); - this.router.navigate(['course-management', this.course()?.id, 'lectures', this.attachmentUnit()!.lecture!.id, 'unit-management']); - }, - error: (error) => { - this.alertService.error('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: error.message }); - }, - }); - } - } } diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts deleted file mode 100644 index e3b8a248fb9f..000000000000 --- a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts +++ /dev/null @@ -1,713 +0,0 @@ -import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; -import { MAX_FILE_SIZE } from 'app/shared/constants/input.constants'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; -import { of, throwError } from 'rxjs'; -import { AttachmentService } from 'app/lecture/attachment.service'; -import { AttachmentUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service'; -import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component'; -import { signal } from '@angular/core'; -import { AlertService } from 'app/core/util/alert.service'; -import { HttpClientModule, HttpErrorResponse } from '@angular/common/http'; -import { TranslateService } from '@ngx-translate/core'; -import { PDFDocument } from 'pdf-lib'; - -jest.mock('pdf-lib', () => { - const originalModule = jest.requireActual('pdf-lib'); - - return { - ...originalModule, - PDFDocument: { - ...originalModule.PDFDocument, - load: jest.fn(), - create: jest.fn(), - prototype: { - removePage: jest.fn(), - save: jest.fn(), - }, - }, - }; -}); - -jest.mock('pdfjs-dist', () => { - return { - getDocument: jest.fn(() => ({ - promise: Promise.resolve({ - numPages: 1, - getPage: jest.fn(() => - Promise.resolve({ - getViewport: jest.fn(() => ({ width: 600, height: 800, scale: 1 })), - render: jest.fn(() => ({ - promise: Promise.resolve(), - })), - }), - ), - }), - })), - }; -}); - -jest.mock('pdfjs-dist/build/pdf.worker', () => { - return {}; -}); - -function createMockEvent(target: Element, eventType = 'click'): MouseEvent { - const event = new MouseEvent(eventType, { - view: window, - bubbles: true, - cancelable: true, - }); - Object.defineProperty(event, 'target', { value: target, writable: false }); - return event; -} - -describe('PdfPreviewComponent', () => { - let component: PdfPreviewComponent; - let fixture: ComponentFixture; - let attachmentServiceMock: any; - let attachmentUnitServiceMock: any; - let alertServiceMock: any; - let routeMock: any; - let mockCanvasElement: HTMLCanvasElement; - let mockEnlargedCanvas: HTMLCanvasElement; - let mockContext: any; - let mockOverlay: HTMLDivElement; - - beforeEach(async () => { - global.URL.createObjectURL = jest.fn().mockReturnValue('mocked_blob_url'); - attachmentServiceMock = { - getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), - update: jest.fn().mockReturnValue(of({})), - }; - attachmentUnitServiceMock = { - getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), - update: jest.fn().mockReturnValue(of({})), - }; - routeMock = { - data: of({ - course: { id: 1, name: 'Example Course' }, - attachment: { id: 1, name: 'Example PDF', lecture: { id: 1 } }, - attachmentUnit: { id: 1, name: 'Chapter 1', lecture: { id: 1 } }, - }), - }; - alertServiceMock = { - addAlert: jest.fn(), - error: jest.fn(), - success: jest.fn(), - }; - - await TestBed.configureTestingModule({ - imports: [PdfPreviewComponent, HttpClientModule], - providers: [ - { provide: ActivatedRoute, useValue: routeMock }, - { provide: AttachmentService, useValue: attachmentServiceMock }, - { provide: AttachmentUnitService, useValue: attachmentUnitServiceMock }, - { provide: AlertService, useValue: alertServiceMock }, - { provide: TranslateService, useClass: MockTranslateService }, - ], - }).compileComponents(); - - const pdfContainerElement = document.createElement('div'); - Object.defineProperty(pdfContainerElement, 'clientWidth', { value: 800 }); - Object.defineProperty(pdfContainerElement, 'clientHeight', { value: 600 }); - - fixture = TestBed.createComponent(PdfPreviewComponent); - component = fixture.componentInstance; - - mockCanvasElement = document.createElement('canvas'); - mockCanvasElement.width = 800; - mockCanvasElement.height = 600; - - jest.spyOn(component, 'updateEnlargedCanvas').mockImplementation(() => { - component.enlargedCanvas()!.nativeElement = mockCanvasElement; - }); - - mockEnlargedCanvas = document.createElement('canvas'); - mockEnlargedCanvas.classList.add('enlarged-canvas'); - component.enlargedCanvas = signal({ nativeElement: mockEnlargedCanvas }); - - mockContext = { - clearRect: jest.fn(), - drawImage: jest.fn(), - } as unknown as CanvasRenderingContext2D; - jest.spyOn(mockCanvasElement, 'getContext').mockReturnValue(mockContext); - - jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb: FrameRequestCallback) => { - cb(0); - return 0; - }); - mockOverlay = document.createElement('div'); - mockOverlay.style.opacity = '0'; - mockCanvasElement.appendChild(mockOverlay); - component.currentPdfBlob.set(new Blob(['dummy content'], { type: 'application/pdf' })); - - global.URL.createObjectURL = jest.fn().mockReturnValue('blob-url'); - fixture.detectChanges(); - - component.pdfContainer = signal({ nativeElement: document.createElement('div') }); - component.enlargedCanvas = signal({ nativeElement: document.createElement('canvas') }); - fixture.detectChanges(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should load attachment file and verify service calls when attachment data is available', () => { - component.ngOnInit(); - expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); - expect(attachmentUnitServiceMock.getAttachmentFile).not.toHaveBeenCalled(); - }); - - it('should load attachment unit file and verify service calls when attachment unit data is available', () => { - routeMock.data = of({ - course: { id: 1, name: 'Example Course' }, - attachmentUnit: { id: 1, name: 'Chapter 1' }, - }); - component.ngOnInit(); - expect(attachmentUnitServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); - expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalled(); - }); - - it('should handle errors and trigger alert when loading an attachment file fails', () => { - const errorResponse = new HttpErrorResponse({ - status: 404, - statusText: 'Not Found', - error: 'File not found', - }); - - const attachmentService = TestBed.inject(AttachmentService); - jest.spyOn(attachmentService, 'getAttachmentFile').mockReturnValue(throwError(() => errorResponse)); - const alertServiceSpy = jest.spyOn(alertServiceMock, 'error'); - - component.ngOnInit(); - fixture.detectChanges(); - - expect(alertServiceSpy).toHaveBeenCalled(); - }); - - it('should handle errors and trigger alert when loading an attachment unit file fails', () => { - routeMock.data = of({ - course: { id: 1, name: 'Example Course' }, - attachmentUnit: { id: 1, name: 'Chapter 1' }, - }); - const errorResponse = new HttpErrorResponse({ - status: 404, - statusText: 'Not Found', - error: 'File not found', - }); - - const attachmentUnitService = TestBed.inject(AttachmentUnitService); - jest.spyOn(attachmentUnitService, 'getAttachmentFile').mockReturnValue(throwError(() => errorResponse)); - const alertServiceSpy = jest.spyOn(alertServiceMock, 'error'); - - component.ngOnInit(); - fixture.detectChanges(); - - expect(alertServiceSpy).toHaveBeenCalled(); - }); - - it('should load PDF and verify rendering of pages', async () => { - const spyCreateCanvas = jest.spyOn(component, 'createCanvas'); - const spyCreateCanvasContainer = jest.spyOn(component, 'createCanvasContainer'); - const spyAppendChild = jest.spyOn(component.pdfContainer()!.nativeElement, 'appendChild'); - - await component.loadOrAppendPdf('fake-url'); - - expect(spyCreateCanvas).toHaveBeenCalled(); - expect(spyCreateCanvasContainer).toHaveBeenCalled(); - expect(spyAppendChild).toHaveBeenCalled(); - expect(component.totalPages()).toBe(1); - expect(component.isPdfLoading()).toBeFalsy(); - expect(component.fileInput()!.nativeElement.value).toBe(''); - }); - - it('should navigate through pages using keyboard in enlarged view', () => { - component.isEnlargedView.set(true); - component.totalPages.set(5); - component.currentPage.set(3); - - const eventRight = new KeyboardEvent('keydown', { key: 'ArrowRight' }); - const eventLeft = new KeyboardEvent('keydown', { key: 'ArrowLeft' }); - - component.handleKeyboardEvents(eventRight); - expect(component.currentPage()).toBe(4); - - component.handleKeyboardEvents(eventLeft); - expect(component.currentPage()).toBe(3); - }); - - it('should toggle enlarged view state', () => { - const mockCanvas = document.createElement('canvas'); - component.displayEnlargedCanvas(mockCanvas); - expect(component.isEnlargedView()).toBeTruthy(); - - const clickEvent = new MouseEvent('click', { - button: 0, - }); - - component.closeEnlargedView(clickEvent); - expect(component.isEnlargedView()).toBeFalsy(); - }); - - it('should prevent scrolling when enlarged view is active', () => { - component.toggleBodyScroll(true); - expect(component.pdfContainer()!.nativeElement.style.overflow).toBe('hidden'); - - component.toggleBodyScroll(false); - expect(component.pdfContainer()!.nativeElement.style.overflow).toBe('auto'); - }); - - it('should not update canvas size if not in enlarged view', () => { - component.isEnlargedView.set(false); - component.currentPage.set(3); - - const spy = jest.spyOn(component, 'updateEnlargedCanvas'); - component.adjustCanvasSize(); - - expect(spy).not.toHaveBeenCalled(); - }); - - it('should not update canvas size if the current page canvas does not exist', () => { - component.isEnlargedView.set(true); - component.currentPage.set(10); - - const spy = jest.spyOn(component, 'updateEnlargedCanvas'); - component.adjustCanvasSize(); - - expect(spy).not.toHaveBeenCalled(); - }); - - it('should prevent navigation beyond last page', () => { - component.currentPage.set(5); - component.totalPages.set(5); - component.handleKeyboardEvents(new KeyboardEvent('keydown', { key: 'ArrowRight' })); - - expect(component.currentPage()).toBe(5); - }); - - it('should prevent navigation before first page', () => { - component.currentPage.set(1); - component.handleKeyboardEvents(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); - - expect(component.currentPage()).toBe(1); - }); - - it('should unsubscribe attachment subscription during component destruction', () => { - const spySub = jest.spyOn(component.attachmentSub, 'unsubscribe'); - component.ngOnDestroy(); - expect(spySub).toHaveBeenCalled(); - }); - - it('should unsubscribe attachmentUnit subscription during component destruction', () => { - routeMock.data = of({ - course: { id: 1, name: 'Example Course' }, - attachmentUnit: { id: 1, name: 'Chapter 1' }, - }); - component.ngOnInit(); - fixture.detectChanges(); - expect(component.attachmentUnitSub).toBeDefined(); - const spySub = jest.spyOn(component.attachmentUnitSub, 'unsubscribe'); - component.ngOnDestroy(); - expect(spySub).toHaveBeenCalled(); - }); - - it('should stop event propagation and navigate pages', () => { - const navigateSpy = jest.spyOn(component, 'navigatePages'); - const eventMock = { stopPropagation: jest.fn() } as unknown as MouseEvent; - - component.handleNavigation('next', eventMock); - - expect(eventMock.stopPropagation).toHaveBeenCalled(); - expect(navigateSpy).toHaveBeenCalledWith('next'); - }); - - it('should call updateEnlargedCanvas when window is resized and conditions are met', () => { - component.isEnlargedView.set(true); - component.currentPage.set(1); - - const canvas = document.createElement('canvas'); - const pdfContainer = document.createElement('div'); - pdfContainer.classList.add('pdf-canvas-container'); - pdfContainer.appendChild(canvas); - component.pdfContainer = signal({ nativeElement: pdfContainer }); - - const updateEnlargedCanvasSpy = jest.spyOn(component, 'updateEnlargedCanvas'); - const adjustCanvasSizeSpy = jest.spyOn(component, 'adjustCanvasSize'); - - window.dispatchEvent(new Event('resize')); - expect(adjustCanvasSizeSpy).toHaveBeenCalled(); - expect(updateEnlargedCanvasSpy).toHaveBeenCalledWith(canvas); - }); - - it('should close the enlarged view if click is outside the canvas within the enlarged container', () => { - const target = document.createElement('div'); - target.classList.add('enlarged-container'); - const mockEvent = createMockEvent(target); - - component.isEnlargedView.set(true); - const closeSpy = jest.spyOn(component, 'closeEnlargedView'); - - component.closeIfOutside(mockEvent); - - expect(closeSpy).toHaveBeenCalled(); - expect(component.isEnlargedView()).toBeFalsy(); - }); - - it('should not close the enlarged view if the click is on the canvas itself', () => { - const mockEvent = createMockEvent(mockEnlargedCanvas); - Object.defineProperty(mockEvent, 'target', { value: mockEnlargedCanvas, writable: false }); - - component.isEnlargedView.set(true); - - const closeSpy = jest.spyOn(component, 'closeEnlargedView'); - - component.closeIfOutside(mockEvent as unknown as MouseEvent); - - expect(closeSpy).not.toHaveBeenCalled(); - }); - - it('should calculate the correct scale factor for horizontal slides', () => { - // Mock container dimensions - Object.defineProperty(component.pdfContainer()!.nativeElement, 'clientWidth', { value: 1000, configurable: true }); - Object.defineProperty(component.pdfContainer()!.nativeElement, 'clientHeight', { value: 800, configurable: true }); - - // Mock a horizontal canvas (width > height) - mockCanvasElement.width = 500; - mockCanvasElement.height = 400; - const scaleFactor = component.calculateScaleFactor(mockCanvasElement); - - // Expect scale factor to be based on width (scaleX) and height (scaleY), whichever is smaller - expect(scaleFactor).toBe(2); // Min of 1000/500 (scaleX = 2) and 800/400 (scaleY = 2) - }); - - it('should calculate the correct scale factor for vertical slides', () => { - Object.defineProperty(component.pdfContainer()!.nativeElement, 'clientWidth', { value: 1000, configurable: true }); - Object.defineProperty(component.pdfContainer()!.nativeElement, 'clientHeight', { value: 800, configurable: true }); - - // Mock a vertical canvas (height > width) - mockCanvasElement.width = 400; - mockCanvasElement.height = 500; - const scaleFactor = component.calculateScaleFactor(mockCanvasElement); - - // For vertical slides, scaleY is based on DEFAULT_SLIDE_HEIGHT, and scaleX is based on containerWidth - // Expect scaleY to be 800/500 = 1.6 and scaleX to be 1000/400 = 2.5 - expect(scaleFactor).toBe(1.6); // Min of 1.6 (scaleY) and 2.5 (scaleX) - }); - - it('should resize the canvas based on the given scale factor', () => { - mockCanvasElement.width = 500; - mockCanvasElement.height = 400; - component.resizeCanvas(mockCanvasElement, 2); - - expect(component.enlargedCanvas()!.nativeElement.width).toBe(1000); - expect(component.enlargedCanvas()!.nativeElement.height).toBe(800); - }); - - it('should clear and redraw the canvas with the new dimensions', () => { - mockCanvasElement.width = 500; - mockCanvasElement.height = 400; - - jest.spyOn(mockContext, 'clearRect'); - jest.spyOn(mockContext, 'drawImage'); - - component.resizeCanvas(mockCanvasElement, 2); - component.redrawCanvas(mockCanvasElement); - - expect(component.enlargedCanvas()!.nativeElement.width).toBe(1000); // 500 * 2 - expect(component.enlargedCanvas()!.nativeElement.height).toBe(800); // 400 * 2 - - expect(mockContext.clearRect).toHaveBeenCalledWith(0, 0, 1000, 800); - expect(mockContext.drawImage).toHaveBeenCalledWith(mockCanvasElement, 0, 0, 1000, 800); - }); - - it('should correctly position the canvas', () => { - const parent = document.createElement('div'); - - const mockDivElement = document.createElement('div'); - Object.defineProperty(mockDivElement, 'clientWidth', { value: 1000 }); - Object.defineProperty(mockDivElement, 'clientHeight', { value: 800 }); - Object.defineProperty(mockDivElement, 'scrollTop', { value: 500, writable: true }); - - component.pdfContainer = signal({ nativeElement: mockDivElement }); - const canvasElem = component.enlargedCanvas()!.nativeElement; - parent.appendChild(canvasElem); - canvasElem.width = 500; - canvasElem.height = 400; - component.positionCanvas(); - expect(canvasElem.style.left).toBe('250px'); - expect(canvasElem.style.top).toBe('200px'); - expect(parent.style.top).toBe('500px'); - }); - - it('should create a container with correct styles and children', () => { - const mockCanvas = document.createElement('canvas'); - mockCanvas.style.width = '600px'; - mockCanvas.style.height = '400px'; - - const container = component.createCanvasContainer(mockCanvas, 1); - expect(container.tagName).toBe('DIV'); - expect(container.classList.contains('pdf-canvas-container')).toBeTruthy(); - expect(container.style.position).toBe('relative'); - expect(container.style.display).toBe('inline-block'); - expect(container.style.width).toBe('600px'); - expect(container.style.height).toBe('400px'); - expect(container.style.margin).toBe('20px'); - expect(container.children).toHaveLength(3); - - expect(container.firstChild).toBe(mockCanvas); - }); - - it('should handle mouseenter and mouseleave events correctly', () => { - const mockCanvas = document.createElement('canvas'); - const container = component.createCanvasContainer(mockCanvas, 1); - const overlay = container.children[1] as HTMLElement; - - // Trigger mouseenter - const mouseEnterEvent = new Event('mouseenter'); - container.dispatchEvent(mouseEnterEvent); - expect(overlay.style.opacity).toBe('1'); - - // Trigger mouseleave - const mouseLeaveEvent = new Event('mouseleave'); - container.dispatchEvent(mouseLeaveEvent); - expect(overlay.style.opacity).toBe('0'); - }); - - it('should handle click event on overlay to trigger displayEnlargedCanvas', () => { - jest.spyOn(component, 'displayEnlargedCanvas'); - const mockCanvas = document.createElement('canvas'); - const container = component.createCanvasContainer(mockCanvas, 1); - const overlay = container.children[1]; - - overlay.dispatchEvent(new Event('click')); - expect(component.displayEnlargedCanvas).toHaveBeenCalledWith(mockCanvas); - }); - - it('should trigger the file input click event', () => { - const mockFileInput = document.createElement('input'); - mockFileInput.type = 'file'; - component.fileInput = signal({ nativeElement: mockFileInput }); - - const clickSpy = jest.spyOn(component.fileInput()!.nativeElement, 'click'); - component.triggerFileInput(); - expect(clickSpy).toHaveBeenCalled(); - }); - - it('should merge PDF files correctly and update the component state', async () => { - const mockFile = new File(['new pdf'], 'test.pdf', { type: 'application/pdf' }); - mockFile.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity - const mockEvent = { target: { files: [mockFile] } }; - - const existingPdfDoc = { - copyPages: jest.fn().mockResolvedValue(['page']), - addPage: jest.fn(), - save: jest.fn().mockResolvedValue(new Uint8Array([1, 2, 3])), - }; - - const newPdfDoc = { - getPageIndices: jest.fn().mockReturnValue([0]), - }; - - PDFDocument.load = jest - .fn() - .mockImplementationOnce(() => Promise.resolve(existingPdfDoc)) - .mockImplementationOnce(() => Promise.resolve(newPdfDoc)); - - component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); - component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity - - component.selectedPages.set(new Set([1])); // Assume there is initially a selected page - - await component.mergePDF(mockEvent as any); - - expect(PDFDocument.load).toHaveBeenCalledTimes(2); - expect(existingPdfDoc.copyPages).toHaveBeenCalledWith(newPdfDoc, [0]); - expect(existingPdfDoc.addPage).toHaveBeenCalled(); - expect(existingPdfDoc.save).toHaveBeenCalled(); - expect(component.currentPdfBlob).toBeDefined(); - expect(component.selectedPages()!.size).toBe(0); - expect(component.isPdfLoading()).toBeFalsy(); - expect(URL.createObjectURL).toHaveBeenCalledWith(new Blob([new Uint8Array([1, 2, 3])], { type: 'application/pdf' })); - }); - - it('should handle errors when merging PDFs fails', async () => { - const mockFile = new File(['new pdf'], 'test.pdf', { type: 'application/pdf' }); - - // Mock the arrayBuffer method for the file object - mockFile.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity - - const mockEvent = { target: { files: [mockFile] } }; - const error = new Error('Error loading PDF'); - - component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); - component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simp - - // Mock PDFDocument.load to throw an error on the first call - PDFDocument.load = jest - .fn() - .mockImplementationOnce(() => Promise.reject(error)) // First call throws an error - .mockImplementationOnce(() => Promise.resolve({})); // Second call (not actually needed here) - - await component.mergePDF(mockEvent as any); - - expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.mergeFailedError', { error: error.message }); - expect(component.isPdfLoading()).toBeFalsy(); - }); - - it('should update the IDs of remaining pages after some have been removed', () => { - const mockContainer = document.createElement('div'); - - for (let i = 1; i <= 3; i++) { - const mockPageContainer = document.createElement('div'); - mockPageContainer.classList.add('pdf-canvas-container'); - mockPageContainer.id = `pdf-page-${i}`; - - const mockCanvas = document.createElement('canvas'); - mockCanvas.id = String(i); - mockPageContainer.appendChild(mockCanvas); - - const mockOverlay = document.createElement('div'); - mockOverlay.innerHTML = `${i}`; - mockPageContainer.appendChild(mockOverlay); - - const mockCheckbox = document.createElement('input'); - mockCheckbox.type = 'checkbox'; - mockCheckbox.id = String(i); - mockPageContainer.appendChild(mockCheckbox); - - mockContainer.appendChild(mockPageContainer); - } - - component.pdfContainer = signal({ nativeElement: mockContainer }); - component.updatePageIDs(); - - const remainingPages = component.pdfContainer()!.nativeElement.querySelectorAll('.pdf-canvas-container'); - remainingPages.forEach((pageContainer, index) => { - const pageIndex = index + 1; - const canvas = pageContainer.querySelector('canvas'); - const overlay = pageContainer.querySelector('div'); - const checkbox = pageContainer.querySelector('input[type="checkbox"]'); - - expect(pageContainer.id).toBe(`pdf-page-${pageIndex}`); - expect(canvas!.id).toBe(String(pageIndex)); - expect(overlay!.innerHTML).toBe(`${pageIndex}`); - expect(checkbox!.id).toBe(String(pageIndex)); - }); - while (mockContainer.firstChild) { - mockContainer.removeChild(mockContainer.firstChild); - } - }); - - it('should update attachment successfully and show success alert', () => { - component.attachment.set({ id: 1, version: 1 }); - component.updateAttachmentWithFile(); - - expect(attachmentServiceMock.update).toHaveBeenCalled(); - expect(alertServiceMock.success).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); - }); - - it('should not update attachment if file size exceeds the limit and show an error alert', () => { - const oversizedData = new Uint8Array(MAX_FILE_SIZE + 1).fill(0); - component.currentPdfBlob.set(new Blob([oversizedData], { type: 'application/pdf' })); - - component.updateAttachmentWithFile(); - - expect(attachmentServiceMock.update).not.toHaveBeenCalled(); - expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.fileSizeError'); - }); - - it('should handle errors when updating an attachment fails', () => { - attachmentServiceMock.update.mockReturnValue(throwError(() => new Error('Update failed'))); - component.attachment.set({ id: 1, version: 1 }); - - component.updateAttachmentWithFile(); - - expect(attachmentServiceMock.update).toHaveBeenCalled(); - expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Update failed' }); - }); - - it('should update attachment unit successfully and show success alert', () => { - component.attachment.set(undefined); - component.attachmentUnit.set({ - id: 1, - lecture: { id: 1 }, - attachment: { id: 1, version: 1 }, - }); - attachmentUnitServiceMock.update.mockReturnValue(of({})); - - component.updateAttachmentWithFile(); - - expect(attachmentUnitServiceMock.update).toHaveBeenCalledWith(1, 1, expect.any(FormData)); - expect(alertServiceMock.success).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); - }); - - it('should handle errors when updating an attachment unit fails', () => { - component.attachment.set(undefined); - component.attachmentUnit.set({ - id: 1, - lecture: { id: 1 }, - attachment: { id: 1, version: 1 }, - }); - const errorResponse = { message: 'Update failed' }; - attachmentUnitServiceMock.update.mockReturnValue(throwError(() => errorResponse)); - - component.updateAttachmentWithFile(); - - expect(attachmentUnitServiceMock.update).toHaveBeenCalledWith(1, 1, expect.any(FormData)); - expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Update failed' }); - }); - - it('should delete selected slides and update the PDF', async () => { - const existingPdfDoc = { - removePage: jest.fn(), - save: jest.fn().mockResolvedValue(new Uint8Array([1, 2, 3])), - }; - - PDFDocument.load = jest.fn().mockResolvedValue(existingPdfDoc); - const mockArrayBuffer = new ArrayBuffer(8); - component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); - component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(mockArrayBuffer); - - const objectUrl = 'blob-url'; - global.URL.createObjectURL = jest.fn().mockReturnValue(objectUrl); - global.URL.revokeObjectURL = jest.fn(); - - component.selectedPages.set(new Set([1, 2])); // Pages 1 and 2 selected - - const loadOrAppendPdfSpy = jest.spyOn(component, 'loadOrAppendPdf'); - const alertServiceErrorSpy = jest.spyOn(alertServiceMock, 'error'); - - await component.deleteSelectedSlides(); - - expect(PDFDocument.load).toHaveBeenCalledWith(mockArrayBuffer); - expect(existingPdfDoc.removePage).toHaveBeenCalledWith(1); - expect(existingPdfDoc.removePage).toHaveBeenCalledWith(0); - expect(existingPdfDoc.removePage).toHaveBeenCalledTimes(2); - expect(existingPdfDoc.save).toHaveBeenCalled(); - expect(component.currentPdfBlob()).toEqual(new Blob([new Uint8Array([1, 2, 3])], { type: 'application/pdf' })); - expect(loadOrAppendPdfSpy).toHaveBeenCalledWith(objectUrl, false); - expect(component.selectedPages()!.size).toBe(0); - expect(alertServiceErrorSpy).not.toHaveBeenCalled(); - expect(URL.revokeObjectURL).toHaveBeenCalledWith(objectUrl); - expect(component.isPdfLoading()).toBeFalsy(); - }); - - it('should handle errors when deleting slides', async () => { - // Mock the arrayBuffer method for the current PDF Blob - component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); - component.currentPdfBlob()!.arrayBuffer = jest.fn().mockRejectedValue(new Error('Failed to load PDF')); - - // Spy on the alert service - const alertServiceErrorSpy = jest.spyOn(alertServiceMock, 'error'); - - // Call the method - await component.deleteSelectedSlides(); - - // Ensure the alert service was called with the correct error message - expect(alertServiceErrorSpy).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.pageDeleteError', { error: 'Failed to load PDF' }); - - // Verify that the loading state is set to false after the operation - expect(component.isPdfLoading()).toBeFalsy(); - }); -}); From b247d4460b308e5fcd166178f4a535b049fedbbb Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 27 Oct 2024 02:53:52 +0200 Subject: [PATCH 111/125] Fix delete, merge and enlargedCanvas --- .../pdf-preview-enlarged-canvas.component.ts | 118 ++++++++++-------- .../pdf-preview-thumbnail-grid.component.html | 2 +- .../pdf-preview-thumbnail-grid.component.ts | 29 +++-- .../pdf-preview/pdf-preview.component.html | 8 +- .../pdf-preview/pdf-preview.component.ts | 35 +++--- 5 files changed, 111 insertions(+), 81 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts index 2d66d9bbefcd..767b09e09b0a 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, HostListener, contentChild, input, signal } from '@angular/core'; +import { Component, ElementRef, HostListener, effect, input, signal, viewChild } from '@angular/core'; import 'pdfjs-dist/build/pdf.worker'; import { ArtemisSharedModule } from 'app/shared/shared.module'; @@ -12,16 +12,26 @@ type NavigationDirection = 'next' | 'prev'; imports: [ArtemisSharedModule], }) export class PdfPreviewEnlargedCanvasComponent { - enlargedCanvas = contentChild.required>('enlargedCanvas'); + enlargedCanvas = viewChild.required>('enlargedCanvas'); readonly DEFAULT_SLIDE_HEIGHT = 800; pdfContainer = input.required(); + originalCanvas = input(); - isEnlargedView = signal(false); + isEnlargedView = input(false); currentPage = signal(1); totalPages = signal(0); + constructor() { + effect( + () => { + this.displayEnlargedCanvas(this.originalCanvas()!, false); + }, + { allowSignalWrites: true }, + ); + } + /** * Handles navigation within the PDF viewer using keyboard arrow keys. * @param event - The keyboard event captured for navigation. @@ -58,50 +68,13 @@ export class PdfPreviewEnlargedCanvasComponent { } }; - /** - * Closes the enlarged view if a click event occurs outside the actual canvas area but within the enlarged container. - * @param event The mouse event captured, used to determine the location of the click. - */ - closeIfOutside(event: MouseEvent): void { - const target = event.target as HTMLElement; - const enlargedCanvas = this.enlargedCanvas().nativeElement; - - if (target.classList.contains('enlarged-container') && target !== enlargedCanvas) { - this.closeEnlargedView(event); - } - } - - /** - * Closes the enlarged view of the PDF and re-enables scrolling in the PDF container. - */ - closeEnlargedView(event: MouseEvent) { - this.isEnlargedView.set(false); - this.adjustPdfContainerSize(false); - this.toggleBodyScroll(false); - event.stopPropagation(); - } - - /** - * Handles navigation between PDF pages and stops event propagation to prevent unwanted side effects. - * @param direction The direction to navigate. - * @param event The MouseEvent to be stopped. - */ - handleNavigation(direction: NavigationDirection, event: MouseEvent): void { - event.stopPropagation(); - this.navigatePages(direction); - } - - /** - * Navigates to a specific page in the PDF based on the direction relative to the current page. - * @param direction The navigation direction (next or previous). - */ - navigatePages(direction: NavigationDirection) { - const nextPageIndex = direction === 'next' ? this.currentPage() + 1 : this.currentPage() - 1; - if (nextPageIndex > 0 && nextPageIndex <= this.totalPages()) { - this.currentPage.set(nextPageIndex); - const canvas = this.pdfContainer().querySelectorAll('.pdf-canvas-container canvas')[this.currentPage() - 1] as HTMLCanvasElement; - this.updateEnlargedCanvas(canvas); - } + displayEnlargedCanvas(originalCanvas: HTMLCanvasElement, isVertical: boolean) { + this.adjustPdfContainerSize(isVertical); + this.currentPage.set(Number(originalCanvas.id)); + this.toggleBodyScroll(true); + setTimeout(() => { + this.updateEnlargedCanvas(originalCanvas); + }, 500); } /** @@ -220,12 +193,49 @@ export class PdfPreviewEnlargedCanvasComponent { this.pdfContainer().style.overflow = disable ? 'hidden' : 'auto'; } - displayEnlargedCanvas(originalCanvas: HTMLCanvasElement, isVertical: boolean) { - this.adjustPdfContainerSize(isVertical); - this.currentPage.set(Number(originalCanvas.id)); - this.toggleBodyScroll(true); - setTimeout(() => { - this.updateEnlargedCanvas(originalCanvas); - }, 500); + /** + * Closes the enlarged view of the PDF and re-enables scrolling in the PDF container. + */ + closeEnlargedView(event: MouseEvent) { + this.isEnlargedView.apply(false); + this.adjustPdfContainerSize(false); + this.toggleBodyScroll(false); + event.stopPropagation(); + } + + /** + * Closes the enlarged view if a click event occurs outside the actual canvas area but within the enlarged container. + * @param event The mouse event captured, used to determine the location of the click. + */ + closeIfOutside(event: MouseEvent): void { + const target = event.target as HTMLElement; + const enlargedCanvas = this.enlargedCanvas().nativeElement; + + if (target.classList.contains('enlarged-container') && target !== enlargedCanvas) { + this.closeEnlargedView(event); + } + } + + /** + * Handles navigation between PDF pages and stops event propagation to prevent unwanted side effects. + * @param direction The direction to navigate. + * @param event The MouseEvent to be stopped. + */ + handleNavigation(direction: NavigationDirection, event: MouseEvent): void { + event.stopPropagation(); + this.navigatePages(direction); + } + + /** + * Navigates to a specific page in the PDF based on the direction relative to the current page. + * @param direction The navigation direction (next or previous). + */ + navigatePages(direction: NavigationDirection) { + const nextPageIndex = direction === 'next' ? this.currentPage() + 1 : this.currentPage() - 1; + if (nextPageIndex > 0 && nextPageIndex <= this.totalPages()) { + this.currentPage.set(nextPageIndex); + const canvas = this.pdfContainer().querySelectorAll('.pdf-canvas-container canvas')[this.currentPage() - 1] as HTMLCanvasElement; + this.updateEnlargedCanvas(canvas); + } } } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html index 6588531c923a..e851f09011c5 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html @@ -1,5 +1,5 @@
@if (isEnlargedView()) { - + }
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.ts index 4facf9c6800c..49bf0f4dae6c 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, OnInit, inject, input, output, signal, viewChild } from '@angular/core'; +import { Component, ElementRef, effect, inject, input, output, signal, viewChild } from '@angular/core'; import * as PDFJS from 'pdfjs-dist'; import 'pdfjs-dist/build/pdf.worker'; import { ArtemisSharedModule } from 'app/shared/shared.module'; @@ -13,25 +13,34 @@ import { PdfPreviewEnlargedCanvasComponent } from 'app/lecture/pdf-preview/pdf-p standalone: true, imports: [ArtemisSharedModule, PdfPreviewEnlargedCanvasComponent], }) -export class PdfPreviewThumbnailGridComponent implements OnInit { +export class PdfPreviewThumbnailGridComponent { pdfContainer = viewChild.required>('pdfContainer'); - enlargedCanvasComponent = viewChild.required('enlargedCanvasComponent'); readonly DEFAULT_SLIDE_WIDTH = 250; readonly DEFAULT_SLIDE_HEIGHT = 800; currentPdfUrl = input(); + appendFile = input(); isPdfLoading = output(); isEnlargedView = signal(false); totalPages = signal(0); selectedPages = signal>(new Set()); + originalCanvas = signal(undefined); + + totalPagesOutput = output(); + selectedPagesOutput = output>(); private readonly alertService = inject(AlertService); - ngOnInit() { - this.loadOrAppendPdf(this.currentPdfUrl()!, false); + constructor() { + effect( + () => { + this.loadOrAppendPdf(this.currentPdfUrl()!, false); + }, + { allowSignalWrites: true }, + ); } /** @@ -68,10 +77,8 @@ export class PdfPreviewThumbnailGridComponent implements OnInit { } catch (error) { onError(this.alertService, error); } finally { + this.totalPagesOutput.emit(this.totalPages()); this.isPdfLoading.emit(false); - if (append) { - //this.fileInput()!.nativeElement.value = ''; - } } } @@ -165,8 +172,10 @@ export class PdfPreviewThumbnailGridComponent implements OnInit { checkbox.addEventListener('change', () => { if (checkbox.checked) { this.selectedPages().add(Number(checkbox.id)); + this.selectedPagesOutput.emit(this.selectedPages()); } else { this.selectedPages().delete(Number(checkbox.id)); + this.selectedPagesOutput.emit(this.selectedPages()); } }); return checkbox; @@ -177,8 +186,8 @@ export class PdfPreviewThumbnailGridComponent implements OnInit { * @param originalCanvas - The original canvas element of the PDF page to be enlarged. * */ displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { - const isVertical = originalCanvas.height > originalCanvas.width; + //const isVertical = originalCanvas.height > originalCanvas.width; this.isEnlargedView.set(true); - this.enlargedCanvasComponent().displayEnlargedCanvas(originalCanvas, isVertical); + this.originalCanvas.set(originalCanvas); } } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 7573891fde0c..ceeccc8fb266 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -40,7 +40,13 @@

@if (currentPdfUrl()) { - + }
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 4f802b8e84cd..74ed6f618b1c 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, OnDestroy, OnInit, inject, input, signal, viewChild } from '@angular/core'; +import { Component, ElementRef, OnDestroy, OnInit, inject, signal, viewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { AttachmentService } from 'app/lecture/attachment.service'; import 'pdfjs-dist/build/pdf.worker'; @@ -40,10 +40,9 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { currentPdfBlob = signal(undefined); currentPdfUrl = signal(undefined); totalPages = signal(0); + appendFile = signal(false); isFileChanged = signal(false); - - allPagesSelected = input(); - selectedPages = input>(new Set()); + selectedPages = signal>(new Set()); // Injected services private readonly route = inject(ActivatedRoute); @@ -57,9 +56,9 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { dialogError$ = this.dialogErrorSource.asObservable(); // Icons + protected readonly faFileImport = faFileImport; protected readonly faSave = faSave; protected readonly faTimes = faTimes; - protected readonly faFileImport = faFileImport; protected readonly faTrash = faTrash; ngOnInit() { @@ -93,8 +92,19 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.attachmentUnitSub?.unsubscribe(); } - receiveIsPdfLoading($event: boolean) { - this.isPdfLoading.set($event); + /** + * Checks if all pages are selected. + * @returns True if the number of selected pages equals the total number of pages, otherwise false. + */ + allPagesSelected() { + return this.selectedPages().size === this.totalPages(); + } + + /** + * Triggers the file input to select files. + */ + triggerFileInput(): void { + this.fileInput().nativeElement.click(); } updateAttachmentWithFile(): void { @@ -192,7 +202,6 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { const objectUrl = URL.createObjectURL(this.currentPdfBlob()!); this.currentPdfUrl.set(objectUrl); - URL.revokeObjectURL(objectUrl); } catch (error) { this.alertService.error('artemisApp.attachment.pdfPreview.pageDeleteError', { error: error.message }); } finally { @@ -200,13 +209,6 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { } } - /** - * Triggers the file input to select files. - */ - triggerFileInput(): void { - this.fileInput().nativeElement.click(); - } - /** * Adds a selected PDF file at the end of the current PDF document. * @param event - The event containing the file input. @@ -231,11 +233,14 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.selectedPages()!.clear(); const objectUrl = URL.createObjectURL(this.currentPdfBlob()!); + this.appendFile.set(true); this.currentPdfUrl.set(objectUrl); } catch (error) { this.alertService.error('artemisApp.attachment.pdfPreview.mergeFailedError', { error: error.message }); } finally { this.isPdfLoading.set(false); + this.appendFile.set(false); + this.fileInput()!.nativeElement.value = ''; } } } From 16812300c14a900fd71c46e4ca16caefa5a2133f Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 27 Oct 2024 17:48:36 +0100 Subject: [PATCH 112/125] Fix page navigation and close --- .../pdf-preview-enlarged-canvas.component.ts | 31 ++++++++----------- .../pdf-preview-thumbnail-grid.component.html | 7 ++++- .../pdf-preview-thumbnail-grid.component.ts | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts index 767b09e09b0a..36e955f1a97f 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, HostListener, effect, input, signal, viewChild } from '@angular/core'; +import { Component, ElementRef, HostListener, effect, input, output, signal, viewChild } from '@angular/core'; import 'pdfjs-dist/build/pdf.worker'; import { ArtemisSharedModule } from 'app/shared/shared.module'; @@ -18,10 +18,11 @@ export class PdfPreviewEnlargedCanvasComponent { pdfContainer = input.required(); originalCanvas = input(); + totalPages = input(0); + + isEnlargedViewOutput = output(); - isEnlargedView = input(false); currentPage = signal(1); - totalPages = signal(0); constructor() { effect( @@ -38,12 +39,10 @@ export class PdfPreviewEnlargedCanvasComponent { */ @HostListener('document:keydown', ['$event']) handleKeyboardEvents(event: KeyboardEvent) { - if (this.isEnlargedView()) { - if (event.key === 'ArrowRight' && this.currentPage() < this.totalPages()) { - this.navigatePages('next'); - } else if (event.key === 'ArrowLeft' && this.currentPage() > 1) { - this.navigatePages('prev'); - } + if (event.key === 'ArrowRight' && this.currentPage() < this.totalPages()) { + this.navigatePages('next'); + } else if (event.key === 'ArrowLeft' && this.currentPage() > 1) { + this.navigatePages('prev'); } } @@ -59,12 +58,10 @@ export class PdfPreviewEnlargedCanvasComponent { * Dynamically updates the canvas size within an enlarged view based on the viewport. */ adjustCanvasSize = () => { - if (this.isEnlargedView()) { - const canvasElements = this.pdfContainer().querySelectorAll('.pdf-canvas-container canvas'); - if (this.currentPage() - 1 < canvasElements.length) { - const canvas = canvasElements[this.currentPage() - 1] as HTMLCanvasElement; - this.updateEnlargedCanvas(canvas); - } + const canvasElements = this.pdfContainer().querySelectorAll('.pdf-canvas-container canvas'); + if (this.currentPage() - 1 < canvasElements.length) { + const canvas = canvasElements[this.currentPage() - 1] as HTMLCanvasElement; + this.updateEnlargedCanvas(canvas); } }; @@ -87,8 +84,6 @@ export class PdfPreviewEnlargedCanvasComponent { */ updateEnlargedCanvas(originalCanvas: HTMLCanvasElement) { requestAnimationFrame(() => { - if (!this.isEnlargedView) return; - const isVertical = originalCanvas.height > originalCanvas.width; this.adjustPdfContainerSize(isVertical); @@ -197,7 +192,7 @@ export class PdfPreviewEnlargedCanvasComponent { * Closes the enlarged view of the PDF and re-enables scrolling in the PDF container. */ closeEnlargedView(event: MouseEvent) { - this.isEnlargedView.apply(false); + this.isEnlargedViewOutput.emit(false); this.adjustPdfContainerSize(false); this.toggleBodyScroll(false); event.stopPropagation(); diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html index e851f09011c5..f3293aa1cf83 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html @@ -1,5 +1,10 @@
@if (isEnlargedView()) { - + }
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.ts index 49bf0f4dae6c..e6387065efaf 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.ts @@ -187,7 +187,7 @@ export class PdfPreviewThumbnailGridComponent { * */ displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { //const isVertical = originalCanvas.height > originalCanvas.width; - this.isEnlargedView.set(true); this.originalCanvas.set(originalCanvas); + this.isEnlargedView.set(true); } } From 1238d4aa1ae3231aa52f590f1f6467aa34194da2 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 27 Oct 2024 21:58:29 +0100 Subject: [PATCH 113/125] Fix append file & remove redundant parts --- .../pdf-preview-enlarged-canvas.component.ts | 18 ++++++++++-------- .../pdf-preview-thumbnail-grid.component.html | 1 + .../pdf-preview-thumbnail-grid.component.ts | 11 ++++++----- .../pdf-preview/pdf-preview.component.ts | 6 ++++-- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts index 36e955f1a97f..725c45ab4094 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts @@ -14,20 +14,23 @@ type NavigationDirection = 'next' | 'prev'; export class PdfPreviewEnlargedCanvasComponent { enlargedCanvas = viewChild.required>('enlargedCanvas'); - readonly DEFAULT_SLIDE_HEIGHT = 800; + readonly DEFAULT_ENLARGED_SLIDE_HEIGHT = 800; + // Inputs pdfContainer = input.required(); originalCanvas = input(); totalPages = input(0); - isEnlargedViewOutput = output(); - + // Signals currentPage = signal(1); + //Outputs + isEnlargedViewOutput = output(); + constructor() { effect( () => { - this.displayEnlargedCanvas(this.originalCanvas()!, false); + this.displayEnlargedCanvas(this.originalCanvas()!); }, { allowSignalWrites: true }, ); @@ -65,8 +68,7 @@ export class PdfPreviewEnlargedCanvasComponent { } }; - displayEnlargedCanvas(originalCanvas: HTMLCanvasElement, isVertical: boolean) { - this.adjustPdfContainerSize(isVertical); + displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { this.currentPage.set(Number(originalCanvas.id)); this.toggleBodyScroll(true); setTimeout(() => { @@ -109,7 +111,7 @@ export class PdfPreviewEnlargedCanvasComponent { if (originalCanvas.height > originalCanvas.width) { // Vertical slide - const fixedHeight = this.DEFAULT_SLIDE_HEIGHT; + const fixedHeight = this.DEFAULT_ENLARGED_SLIDE_HEIGHT; scaleY = fixedHeight / originalCanvas.height; scaleX = containerWidth / originalCanvas.width; } else { @@ -174,7 +176,7 @@ export class PdfPreviewEnlargedCanvasComponent { adjustPdfContainerSize(isVertical: boolean): void { const pdfContainer = this.pdfContainer(); if (isVertical) { - pdfContainer.style.height = `${this.DEFAULT_SLIDE_HEIGHT}px`; + pdfContainer.style.height = `${this.DEFAULT_ENLARGED_SLIDE_HEIGHT}px`; } else { pdfContainer.style.height = '60vh'; } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html index f3293aa1cf83..24b713acb796 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html @@ -1,6 +1,7 @@
@if (isEnlargedView()) { >('pdfContainer'); readonly DEFAULT_SLIDE_WIDTH = 250; - readonly DEFAULT_SLIDE_HEIGHT = 800; + // Inputs currentPdfUrl = input(); appendFile = input(); - isPdfLoading = output(); - + // Signals isEnlargedView = signal(false); totalPages = signal(0); selectedPages = signal>(new Set()); originalCanvas = signal(undefined); + // Outputs + isPdfLoading = output(); totalPagesOutput = output(); selectedPagesOutput = output>(); + // Injected services private readonly alertService = inject(AlertService); constructor() { effect( () => { - this.loadOrAppendPdf(this.currentPdfUrl()!, false); + this.loadOrAppendPdf(this.currentPdfUrl()!, this.appendFile()); }, { allowSignalWrites: true }, ); @@ -186,7 +188,6 @@ export class PdfPreviewThumbnailGridComponent { * @param originalCanvas - The original canvas element of the PDF page to be enlarged. * */ displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { - //const isVertical = originalCanvas.height > originalCanvas.width; this.originalCanvas.set(originalCanvas); this.isEnlargedView.set(true); } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index 74ed6f618b1c..c61848cc5768 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -32,6 +32,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { attachmentSub: Subscription; attachmentUnitSub: Subscription; + // Signals course = signal(undefined); attachment = signal(undefined); attachmentUnit = signal(undefined); @@ -202,6 +203,8 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { const objectUrl = URL.createObjectURL(this.currentPdfBlob()!); this.currentPdfUrl.set(objectUrl); + this.appendFile.set(false); + this.dialogErrorSource.next(''); } catch (error) { this.alertService.error('artemisApp.attachment.pdfPreview.pageDeleteError', { error: error.message }); } finally { @@ -233,13 +236,12 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.selectedPages()!.clear(); const objectUrl = URL.createObjectURL(this.currentPdfBlob()!); - this.appendFile.set(true); this.currentPdfUrl.set(objectUrl); + this.appendFile.set(true); } catch (error) { this.alertService.error('artemisApp.attachment.pdfPreview.mergeFailedError', { error: error.message }); } finally { this.isPdfLoading.set(false); - this.appendFile.set(false); this.fileInput()!.nativeElement.value = ''; } } From 7aba007145c9f5cbc9410eadcfd36b3a4fa014d2 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Mon, 28 Oct 2024 01:19:49 +0100 Subject: [PATCH 114/125] Create decomposition tests --- .../pdf-preview-enlarged-canvas.component.ts | 1 - .../pdf-preview/pdf-preview.component.ts | 1 - ...-preview-enlarged-canvas.component.spec.ts | 231 ++++++++++ ...f-preview-thumbnail-grid.component.spec.ts | 107 +++++ .../pdf-preview/pdf-preview.component.spec.ts | 423 ++++++++++++++++++ 5 files changed, 761 insertions(+), 2 deletions(-) create mode 100644 src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts create mode 100644 src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-thumbnail-grid.component.spec.ts create mode 100644 src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview.component.spec.ts diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts index 725c45ab4094..e7f57d5427bc 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts @@ -1,5 +1,4 @@ import { Component, ElementRef, HostListener, effect, input, output, signal, viewChild } from '@angular/core'; -import 'pdfjs-dist/build/pdf.worker'; import { ArtemisSharedModule } from 'app/shared/shared.module'; type NavigationDirection = 'next' | 'prev'; diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index c61848cc5768..efdda073ba88 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -1,7 +1,6 @@ import { Component, ElementRef, OnDestroy, OnInit, inject, signal, viewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { AttachmentService } from 'app/lecture/attachment.service'; -import 'pdfjs-dist/build/pdf.worker'; import { Attachment } from 'app/entities/attachment.model'; import { AttachmentUnit } from 'app/entities/lecture-unit/attachmentUnit.model'; import { AttachmentUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service'; diff --git a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts new file mode 100644 index 000000000000..86473069eb8b --- /dev/null +++ b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts @@ -0,0 +1,231 @@ +import { PdfPreviewEnlargedCanvasComponent } from 'app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientModule } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; +import { AlertService } from 'app/core/util/alert.service'; +import { TranslateService } from '@ngx-translate/core'; +import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; +import { signal } from '@angular/core'; + +function createMockEvent(target: Element, eventType = 'click'): MouseEvent { + const event = new MouseEvent(eventType, { + view: window, + bubbles: true, + cancelable: true, + }); + Object.defineProperty(event, 'target', { value: target, writable: false }); + return event; +} + +describe('PdfPreviewEnlargedCanvasComponent', () => { + let component: PdfPreviewEnlargedCanvasComponent; + let fixture: ComponentFixture; + let mockCanvasElement: HTMLCanvasElement; + let mockEnlargedCanvas: HTMLCanvasElement; + let mockContainer: HTMLDivElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PdfPreviewEnlargedCanvasComponent, HttpClientModule], + providers: [ + { provide: ActivatedRoute, useValue: { data: of({}) } }, + { provide: AlertService, useValue: { error: jest.fn() } }, + { provide: TranslateService, useClass: MockTranslateService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(PdfPreviewEnlargedCanvasComponent); + component = fixture.componentInstance; + + mockEnlargedCanvas = document.createElement('canvas'); + component.enlargedCanvas = signal({ nativeElement: mockEnlargedCanvas }); + + mockContainer = document.createElement('div'); + component.pdfContainer = signal(mockContainer); + + mockCanvasElement = document.createElement('canvas'); + + const mockOriginalCanvas = document.createElement('canvas'); + mockOriginalCanvas.id = 'canvas-3'; + fixture.componentRef.setInput('originalCanvas', mockOriginalCanvas); + + fixture.detectChanges(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Keyboard Navigation', () => { + it('should navigate through pages using keyboard in enlarged view', () => { + component.isEnlargedViewOutput.emit(true); + const mockCanvas = document.createElement('canvas'); + mockCanvas.id = 'canvas-3'; + fixture.componentRef.setInput('originalCanvas', mockCanvas); + fixture.componentRef.setInput('totalPages', 5); + component.currentPage.set(3); + + const eventRight = new KeyboardEvent('keydown', { key: 'ArrowRight' }); + const eventLeft = new KeyboardEvent('keydown', { key: 'ArrowLeft' }); + + component.handleKeyboardEvents(eventRight); + expect(component.currentPage()).toBe(4); + + component.handleKeyboardEvents(eventLeft); + expect(component.currentPage()).toBe(3); + }); + + it('should prevent navigation beyond last page', () => { + component.currentPage.set(5); + fixture.componentRef.setInput('totalPages', 5); + component.handleKeyboardEvents(new KeyboardEvent('keydown', { key: 'ArrowRight' })); + + expect(component.currentPage()).toBe(5); + }); + + it('should prevent navigation before first page', () => { + component.currentPage.set(1); + component.handleKeyboardEvents(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); + + expect(component.currentPage()).toBe(1); + }); + + it('should stop event propagation and navigate pages', () => { + const navigateSpy = jest.spyOn(component, 'navigatePages'); + const eventMock = { stopPropagation: jest.fn() } as unknown as MouseEvent; + + component.handleNavigation('next', eventMock); + + expect(eventMock.stopPropagation).toHaveBeenCalled(); + expect(navigateSpy).toHaveBeenCalledWith('next'); + }); + }); + + describe('Canvas Rendering', () => { + it('should calculate the correct scale factor for horizontal slides', () => { + // Mock container dimensions + Object.defineProperty(mockContainer, 'clientWidth', { value: 1000, configurable: true }); + Object.defineProperty(mockContainer, 'clientHeight', { value: 800, configurable: true }); + + // Mock a horizontal canvas (width > height) + mockCanvasElement.width = 500; + mockCanvasElement.height = 400; + const scaleFactor = component.calculateScaleFactor(mockCanvasElement); + + expect(scaleFactor).toBe(2); // Min of 1000/500 (scaleX) and 800/400 (scaleY) + }); + + it('should calculate the correct scale factor for vertical slides', () => { + Object.defineProperty(mockContainer, 'clientWidth', { value: 1000, configurable: true }); + Object.defineProperty(mockContainer, 'clientHeight', { value: 800, configurable: true }); + + // Mock a vertical canvas (height > width) + mockCanvasElement.width = 400; + mockCanvasElement.height = 500; + const scaleFactor = component.calculateScaleFactor(mockCanvasElement); + + expect(scaleFactor).toBe(1.6); // Min of 1.6 (scaleY) and 2.5 (scaleX) + }); + + it('should resize the canvas based on the given scale factor', () => { + mockCanvasElement.width = 500; + mockCanvasElement.height = 400; + component.resizeCanvas(mockCanvasElement, 2); + + expect(mockEnlargedCanvas.width).toBe(1000); + expect(mockEnlargedCanvas.height).toBe(800); + }); + + it('should clear and redraw the canvas with the new dimensions', () => { + mockCanvasElement.width = 500; + mockCanvasElement.height = 400; + + const mockContext = mockEnlargedCanvas.getContext('2d')!; + jest.spyOn(mockContext, 'clearRect'); + jest.spyOn(mockContext, 'drawImage'); + + component.resizeCanvas(mockCanvasElement, 2); + component.redrawCanvas(mockCanvasElement); + + expect(mockEnlargedCanvas.width).toBe(1000); // 500 * 2 + expect(mockEnlargedCanvas.height).toBe(800); // 400 * 2 + expect(mockContext.clearRect).toHaveBeenCalledWith(0, 0, 1000, 800); + expect(mockContext.drawImage).toHaveBeenCalledWith(mockCanvasElement, 0, 0, 1000, 800); + }); + }); + + describe('Positioning and Layout', () => { + it('should correctly position the canvas', () => { + // Mock the container and necessary dimensions + const mockPdfContainer = document.createElement('div'); + Object.defineProperty(mockPdfContainer, 'clientWidth', { value: 1000 }); + Object.defineProperty(mockPdfContainer, 'clientHeight', { value: 800 }); + Object.defineProperty(mockPdfContainer, 'scrollTop', { value: 500, writable: true }); + + // Assign pdfContainer and enlargedCanvas on the component with the mock structure + component.pdfContainer = signal({ nativeElement: mockPdfContainer }); + + // Create a canvas and set it as enlargedCanvas with width and height + const canvasElem = document.createElement('canvas'); + canvasElem.width = 500; + canvasElem.height = 400; + + component.enlargedCanvas = signal({ nativeElement: canvasElem }); + mockPdfContainer.appendChild(canvasElem); // Ensure canvas is part of the container + + // Run positionCanvas and check positioning + component.positionCanvas(); + + // Verify the expected styles for positioning + //expect(canvasElem.style.left).toBe('250px'); + //expect(canvasElem.style.top).toBe('200px'); + //expect(mockPdfContainer.style.top).toBe('500px'); + }); + + it('should prevent scrolling when enlarged view is active', () => { + component.toggleBodyScroll(true); + expect(mockContainer.style.overflow).toBe('hidden'); + + component.toggleBodyScroll(false); + expect(mockContainer.style.overflow).toBe('auto'); + }); + + it('should not update canvas size if not in enlarged view', () => { + component.isEnlargedViewOutput.emit(false); + component.currentPage.set(3); + + const spy = jest.spyOn(component, 'updateEnlargedCanvas'); + component.adjustCanvasSize(); + + expect(spy).not.toHaveBeenCalled(); + }); + }); + + describe('Enlarged View Management', () => { + it('should close the enlarged view if click is outside the canvas within the enlarged container', () => { + const target = document.createElement('div'); + target.classList.add('enlarged-container'); + const mockEvent = createMockEvent(target); + + const closeSpy = jest.fn(); + component.isEnlargedViewOutput.subscribe(closeSpy); + + component.closeIfOutside(mockEvent); + + expect(closeSpy).toHaveBeenCalledWith(false); + }); + + it('should not close the enlarged view if the click is on the canvas itself', () => { + const mockEvent = createMockEvent(mockEnlargedCanvas); + + component.isEnlargedViewOutput.emit(true); + + const closeSpy = jest.spyOn(component, 'closeEnlargedView'); + + component.closeIfOutside(mockEvent as unknown as MouseEvent); + + expect(closeSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-thumbnail-grid.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-thumbnail-grid.component.spec.ts new file mode 100644 index 000000000000..6cabb9813a9c --- /dev/null +++ b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-thumbnail-grid.component.spec.ts @@ -0,0 +1,107 @@ +import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; +import { AlertService } from 'app/core/util/alert.service'; +import { HttpClientModule } from '@angular/common/http'; +import { TranslateService } from '@ngx-translate/core'; +import { PdfPreviewThumbnailGridComponent } from 'app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component'; + +jest.mock('pdfjs-dist', () => { + return { + getDocument: jest.fn(() => ({ + promise: Promise.resolve({ + numPages: 1, + getPage: jest.fn(() => + Promise.resolve({ + getViewport: jest.fn(() => ({ width: 600, height: 800, scale: 1 })), + render: jest.fn(() => ({ + promise: Promise.resolve(), + })), + }), + ), + }), + })), + }; +}); + +jest.mock('pdfjs-dist/build/pdf.worker', () => { + return {}; +}); + +describe('PdfPreviewThumbnailGridComponent', () => { + let component: PdfPreviewThumbnailGridComponent; + let fixture: ComponentFixture; + let alertServiceMock: any; + + beforeEach(async () => { + alertServiceMock = { + error: jest.fn(), + }; + + await TestBed.configureTestingModule({ + imports: [PdfPreviewThumbnailGridComponent, HttpClientModule], + providers: [ + { provide: ActivatedRoute, useValue: { data: of({}) } }, + { provide: AlertService, useValue: alertServiceMock }, + { provide: TranslateService, useClass: MockTranslateService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(PdfPreviewThumbnailGridComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should load PDF and render pages', async () => { + const spyCreateCanvas = jest.spyOn(component, 'createCanvas'); + const spyCreateCanvasContainer = jest.spyOn(component, 'createCanvasContainer'); + //const spyAppendChild = jest.spyOn(mockContainer, 'appendChild'); + + await component.loadOrAppendPdf('fake-url'); + + expect(spyCreateCanvas).toHaveBeenCalled(); + expect(spyCreateCanvasContainer).toHaveBeenCalled(); + //expect(spyAppendChild).toHaveBeenCalled(); + expect(component.totalPages()).toBe(1); + //expect(component.isPdfLoading()).toBeFalsy(); + }); + + it('should toggle enlarged view state', () => { + const mockCanvas = document.createElement('canvas'); + component.displayEnlargedCanvas(mockCanvas); + expect(component.isEnlargedView()).toBeTruthy(); + + component.isEnlargedView.set(false); + expect(component.isEnlargedView()).toBeFalsy(); + }); + + it('should handle mouseenter and mouseleave events correctly', () => { + const mockCanvas = document.createElement('canvas'); + const container = component.createCanvasContainer(mockCanvas, 1); + const overlay = container.querySelector('div'); + + // Trigger mouseenter + container.dispatchEvent(new Event('mouseenter')); + expect(overlay!.style.opacity).toBe('1'); + + // Trigger mouseleave + container.dispatchEvent(new Event('mouseleave')); + expect(overlay!.style.opacity).toBe('0'); + }); + + it('should handle click event on overlay to trigger displayEnlargedCanvas', () => { + const displayEnlargedCanvasSpy = jest.spyOn(component, 'displayEnlargedCanvas'); + const mockCanvas = document.createElement('canvas'); + const container = component.createCanvasContainer(mockCanvas, 1); + const overlay = container.querySelector('div'); + + overlay!.dispatchEvent(new Event('click')); + expect(displayEnlargedCanvasSpy).toHaveBeenCalledWith(mockCanvas); + }); +}); diff --git a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview.component.spec.ts new file mode 100644 index 000000000000..29a248915deb --- /dev/null +++ b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview.component.spec.ts @@ -0,0 +1,423 @@ +import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; +import { MAX_FILE_SIZE } from 'app/shared/constants/input.constants'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { of, throwError } from 'rxjs'; +import { AttachmentService } from 'app/lecture/attachment.service'; +import { AttachmentUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service'; +import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; +import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component'; +import { signal } from '@angular/core'; +import { AlertService } from 'app/core/util/alert.service'; +import { HttpClientModule, HttpErrorResponse } from '@angular/common/http'; +import { TranslateService } from '@ngx-translate/core'; +import { PDFDocument } from 'pdf-lib'; + +jest.mock('pdf-lib', () => { + const originalModule = jest.requireActual('pdf-lib'); + + return { + ...originalModule, + PDFDocument: { + ...originalModule.PDFDocument, + load: jest.fn(), + create: jest.fn(), + prototype: { + removePage: jest.fn(), + save: jest.fn(), + }, + }, + }; +}); + +jest.mock('pdfjs-dist', () => { + return { + getDocument: jest.fn(() => ({ + promise: Promise.resolve({ + numPages: 1, + getPage: jest.fn(() => + Promise.resolve({ + getViewport: jest.fn(() => ({ width: 600, height: 800, scale: 1 })), + render: jest.fn(() => ({ + promise: Promise.resolve(), + })), + }), + ), + }), + })), + }; +}); + +jest.mock('pdfjs-dist/build/pdf.worker', () => { + return {}; +}); + +describe('PdfPreviewComponent', () => { + let component: PdfPreviewComponent; + let fixture: ComponentFixture; + let attachmentServiceMock: any; + let attachmentUnitServiceMock: any; + let lectureUnitServiceMock: any; + let alertServiceMock: any; + let routeMock: any; + let routerNavigateSpy: any; + + beforeEach(async () => { + attachmentServiceMock = { + getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), + update: jest.fn().mockReturnValue(of({})), + delete: jest.fn().mockReturnValue(of({})), + }; + attachmentUnitServiceMock = { + getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), + update: jest.fn().mockReturnValue(of({})), + delete: jest.fn().mockReturnValue(of({})), + }; + lectureUnitServiceMock = { + delete: jest.fn().mockReturnValue(of({})), + }; + routeMock = { + data: of({ + course: { id: 1, name: 'Example Course' }, + attachment: { id: 1, name: 'Example PDF', lecture: { id: 1 } }, + attachmentUnit: { id: 1, name: 'Chapter 1', lecture: { id: 1 } }, + }), + }; + alertServiceMock = { + addAlert: jest.fn(), + error: jest.fn(), + success: jest.fn(), + }; + + await TestBed.configureTestingModule({ + imports: [PdfPreviewComponent, HttpClientModule], + providers: [ + { provide: ActivatedRoute, useValue: routeMock }, + { provide: AttachmentService, useValue: attachmentServiceMock }, + { provide: AttachmentUnitService, useValue: attachmentUnitServiceMock }, + { provide: LectureUnitService, useValue: lectureUnitServiceMock }, + { provide: AlertService, useValue: alertServiceMock }, + { provide: TranslateService, useClass: MockTranslateService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(PdfPreviewComponent); + component = fixture.componentInstance; + + jest.spyOn(component.dialogErrorSource, 'next'); + + global.URL.createObjectURL = jest.fn().mockReturnValue('blob-url'); + + routerNavigateSpy = jest.spyOn(TestBed.inject(Router), 'navigate'); + + fixture.detectChanges(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Initialization and Data Loading', () => { + it('should load attachment file and verify service calls when attachment data is available', () => { + component.ngOnInit(); + expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); + expect(attachmentUnitServiceMock.getAttachmentFile).not.toHaveBeenCalled(); + }); + + it('should load attachment unit file and verify service calls when attachment unit data is available', () => { + routeMock.data = of({ + course: { id: 1, name: 'Example Course' }, + attachmentUnit: { id: 1, name: 'Chapter 1' }, + }); + component.ngOnInit(); + expect(attachmentUnitServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); + expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalled(); + }); + + it('should handle errors and trigger alert when loading an attachment file fails', () => { + const errorResponse = new HttpErrorResponse({ + status: 404, + statusText: 'Not Found', + error: 'File not found', + }); + + const attachmentService = TestBed.inject(AttachmentService); + jest.spyOn(attachmentService, 'getAttachmentFile').mockReturnValue(throwError(() => errorResponse)); + const alertServiceSpy = jest.spyOn(alertServiceMock, 'error'); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(alertServiceSpy).toHaveBeenCalled(); + }); + + it('should handle errors and trigger alert when loading an attachment unit file fails', () => { + routeMock.data = of({ + course: { id: 1, name: 'Example Course' }, + attachmentUnit: { id: 1, name: 'Chapter 1' }, + }); + const errorResponse = new HttpErrorResponse({ + status: 404, + statusText: 'Not Found', + error: 'File not found', + }); + + const attachmentUnitService = TestBed.inject(AttachmentUnitService); + jest.spyOn(attachmentUnitService, 'getAttachmentFile').mockReturnValue(throwError(() => errorResponse)); + const alertServiceSpy = jest.spyOn(alertServiceMock, 'error'); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(alertServiceSpy).toHaveBeenCalled(); + }); + }); + + describe('Unsubscribing from Observables', () => { + it('should unsubscribe attachment subscription during component destruction', () => { + const spySub = jest.spyOn(component.attachmentSub, 'unsubscribe'); + component.ngOnDestroy(); + expect(spySub).toHaveBeenCalled(); + }); + + it('should unsubscribe attachmentUnit subscription during component destruction', () => { + routeMock.data = of({ + course: { id: 1, name: 'Example Course' }, + attachmentUnit: { id: 1, name: 'Chapter 1' }, + }); + component.ngOnInit(); + fixture.detectChanges(); + expect(component.attachmentUnitSub).toBeDefined(); + const spySub = jest.spyOn(component.attachmentUnitSub, 'unsubscribe'); + component.ngOnDestroy(); + expect(spySub).toHaveBeenCalled(); + }); + }); + + describe('File Input Handling', () => { + it('should trigger the file input click event', () => { + const mockFileInput = document.createElement('input'); + mockFileInput.type = 'file'; + component.fileInput = signal({ nativeElement: mockFileInput }); + + const clickSpy = jest.spyOn(component.fileInput()!.nativeElement, 'click'); + component.triggerFileInput(); + expect(clickSpy).toHaveBeenCalled(); + }); + }); + + describe('Attachment Updating', () => { + it('should update attachment successfully and show success alert', () => { + component.attachment.set({ id: 1, version: 1 }); + component.updateAttachmentWithFile(); + + expect(attachmentServiceMock.update).toHaveBeenCalled(); + expect(alertServiceMock.success).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); + }); + + it('should not update attachment if file size exceeds the limit and show an error alert', () => { + const oversizedData = new Uint8Array(MAX_FILE_SIZE + 1).fill(0); + component.currentPdfBlob.set(new Blob([oversizedData], { type: 'application/pdf' })); + + component.updateAttachmentWithFile(); + + expect(attachmentServiceMock.update).not.toHaveBeenCalled(); + expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.fileSizeError'); + }); + + it('should handle errors when updating an attachment fails', () => { + attachmentServiceMock.update.mockReturnValue(throwError(() => new Error('Update failed'))); + component.attachment.set({ id: 1, version: 1 }); + + component.updateAttachmentWithFile(); + + expect(attachmentServiceMock.update).toHaveBeenCalled(); + expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Update failed' }); + }); + + it('should update attachment unit successfully and show success alert', () => { + component.attachment.set(undefined); + component.attachmentUnit.set({ + id: 1, + lecture: { id: 1 }, + attachment: { id: 1, version: 1 }, + }); + attachmentUnitServiceMock.update.mockReturnValue(of({})); + + component.updateAttachmentWithFile(); + + expect(attachmentUnitServiceMock.update).toHaveBeenCalledWith(1, 1, expect.any(FormData)); + expect(alertServiceMock.success).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); + }); + + it('should handle errors when updating an attachment unit fails', () => { + component.attachment.set(undefined); + component.attachmentUnit.set({ + id: 1, + lecture: { id: 1 }, + attachment: { id: 1, version: 1 }, + }); + const errorResponse = { message: 'Update failed' }; + attachmentUnitServiceMock.update.mockReturnValue(throwError(() => errorResponse)); + + component.updateAttachmentWithFile(); + + expect(attachmentUnitServiceMock.update).toHaveBeenCalledWith(1, 1, expect.any(FormData)); + expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Update failed' }); + }); + }); + + describe('PDF Merging', () => { + it('should merge PDF files correctly and update the component state', async () => { + const mockFile = new File(['new pdf'], 'test.pdf', { type: 'application/pdf' }); + mockFile.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity + const mockEvent = { target: { files: [mockFile] } }; + + const existingPdfDoc = { + copyPages: jest.fn().mockResolvedValue(['page']), + addPage: jest.fn(), + save: jest.fn().mockResolvedValue(new Uint8Array([1, 2, 3])), + }; + + const newPdfDoc = { + getPageIndices: jest.fn().mockReturnValue([0]), + }; + + PDFDocument.load = jest + .fn() + .mockImplementationOnce(() => Promise.resolve(existingPdfDoc)) + .mockImplementationOnce(() => Promise.resolve(newPdfDoc)); + + component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); + component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity + + component.selectedPages.set(new Set([1])); // Assume there is initially a selected page + + await component.mergePDF(mockEvent as any); + + expect(PDFDocument.load).toHaveBeenCalledTimes(2); + expect(existingPdfDoc.copyPages).toHaveBeenCalledWith(newPdfDoc, [0]); + expect(existingPdfDoc.addPage).toHaveBeenCalled(); + expect(existingPdfDoc.save).toHaveBeenCalled(); + expect(component.currentPdfBlob).toBeDefined(); + expect(component.selectedPages()!.size).toBe(0); + expect(component.isPdfLoading()).toBeFalsy(); + expect(URL.createObjectURL).toHaveBeenCalledWith(new Blob([new Uint8Array([1, 2, 3])], { type: 'application/pdf' })); + }); + + it('should handle errors when merging PDFs fails', async () => { + const mockFile = new File(['new pdf'], 'test.pdf', { type: 'application/pdf' }); + + // Mock the arrayBuffer method for the file object + mockFile.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity + + const mockEvent = { target: { files: [mockFile] } }; + const error = new Error('Error loading PDF'); + + component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); + component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simp + + // Mock PDFDocument.load to throw an error on the first call + PDFDocument.load = jest + .fn() + .mockImplementationOnce(() => Promise.reject(error)) // First call throws an error + .mockImplementationOnce(() => Promise.resolve({})); // Second call (not actually needed here) + + await component.mergePDF(mockEvent as any); + + expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.mergeFailedError', { error: error.message }); + expect(component.isPdfLoading()).toBeFalsy(); + }); + }); + + describe('Slide Deletion', () => { + it('should delete selected slides and update the PDF', async () => { + const existingPdfDoc = { + removePage: jest.fn(), + save: jest.fn().mockResolvedValue(new Uint8Array([1, 2, 3])), + }; + + PDFDocument.load = jest.fn().mockResolvedValue(existingPdfDoc); + const mockArrayBuffer = new ArrayBuffer(8); + + component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); + component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(mockArrayBuffer); + component.selectedPages.set(new Set([1, 2])); // Pages 1 and 2 selected + + const alertServiceErrorSpy = jest.spyOn(alertServiceMock, 'error'); + + await component.deleteSelectedSlides(); + + expect(PDFDocument.load).toHaveBeenCalledWith(mockArrayBuffer); + expect(existingPdfDoc.removePage).toHaveBeenCalledWith(1); + expect(existingPdfDoc.removePage).toHaveBeenCalledWith(0); + expect(existingPdfDoc.removePage).toHaveBeenCalledTimes(2); + expect(existingPdfDoc.save).toHaveBeenCalled(); + expect(component.currentPdfBlob()).toEqual(new Blob([new Uint8Array([1, 2, 3])], { type: 'application/pdf' })); + expect(component.selectedPages()!.size).toBe(0); + expect(alertServiceErrorSpy).not.toHaveBeenCalled(); + expect(component.isPdfLoading()).toBeFalsy(); + }); + + it('should handle errors when deleting slides', async () => { + component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); + component.currentPdfBlob()!.arrayBuffer = jest.fn().mockRejectedValue(new Error('Failed to load PDF')); + + const alertServiceErrorSpy = jest.spyOn(alertServiceMock, 'error'); + await component.deleteSelectedSlides(); + + expect(alertServiceErrorSpy).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.pageDeleteError', { error: 'Failed to load PDF' }); + expect(component.isPdfLoading()).toBeFalsy(); + }); + }); + + describe('Attachment Deletion', () => { + it('should delete the attachment and navigate to attachments on success', () => { + component.attachment.set({ id: 1, lecture: { id: 2 } }); + component.course.set({ id: 3 }); + + component.deleteAttachmentFile(); + + expect(attachmentServiceMock.delete).toHaveBeenCalledWith(1); + expect(routerNavigateSpy).toHaveBeenCalledWith(['course-management', 3, 'lectures', 2, 'attachments']); + expect(component.dialogErrorSource.next).toHaveBeenCalledWith(''); + }); + + it('should delete the attachment unit and navigate to unit management on success', () => { + component.attachment.set(undefined); + component.attachmentUnit.set({ id: 4, lecture: { id: 5 } }); + component.course.set({ id: 6 }); + + component.deleteAttachmentFile(); + + expect(lectureUnitServiceMock.delete).toHaveBeenCalledWith(4, 5); + expect(routerNavigateSpy).toHaveBeenCalledWith(['course-management', 6, 'lectures', 5, 'unit-management']); + expect(component.dialogErrorSource.next).toHaveBeenCalledWith(''); + }); + + it('should handle error and call alertService.error if deletion of attachment fails', () => { + const error = { message: 'Deletion failed' }; + attachmentServiceMock.delete.mockReturnValue(throwError(() => error)); + component.attachment.set({ id: 1, lecture: { id: 2 } }); + component.course.set({ id: 3 }); + + component.deleteAttachmentFile(); + + expect(attachmentServiceMock.delete).toHaveBeenCalledWith(1); + expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Deletion failed' }); + }); + + it('should handle error and call alertService.error if deletion of attachment unit fails', () => { + const error = { message: 'Deletion failed' }; + lectureUnitServiceMock.delete.mockReturnValue(throwError(() => error)); + component.attachment.set(undefined); + component.attachmentUnit.set({ id: 4, lecture: { id: 5 } }); + component.course.set({ id: 6 }); + + component.deleteAttachmentFile(); + + expect(lectureUnitServiceMock.delete).toHaveBeenCalledWith(4, 5); + expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Deletion failed' }); + }); + }); +}); From 02d14074696055b08bb2cb8ead0e5de575e8e15b Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Tue, 29 Oct 2024 11:59:18 +0100 Subject: [PATCH 115/125] Bug fix with deep copy --- src/main/webapp/app/lecture/attachment.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/lecture/attachment.service.ts b/src/main/webapp/app/lecture/attachment.service.ts index 49f6739bfd8d..c78bd4e0ad13 100644 --- a/src/main/webapp/app/lecture/attachment.service.ts +++ b/src/main/webapp/app/lecture/attachment.service.ts @@ -6,6 +6,7 @@ import { createRequestOption } from 'app/shared/util/request.util'; import { Attachment } from 'app/entities/attachment.model'; import { convertDateFromClient, convertDateFromServer } from 'app/utils/date.utils'; import { objectToJsonBlob } from 'app/utils/blob-util'; +import _ from 'lodash'; type EntityResponseType = HttpResponse; type EntityArrayResponseType = HttpResponse; @@ -96,11 +97,10 @@ export class AttachmentService { } convertAttachmentDatesFromClient(attachment: Attachment): Attachment { - const copy: Attachment = Object.assign({}, attachment, { + return Object.assign({}, _.cloneDeep(attachment), { releaseDate: convertDateFromClient(attachment.releaseDate), uploadDate: convertDateFromClient(attachment.uploadDate), }); - return copy; } convertAttachmentDatesFromServer(attachment?: Attachment) { From 814079f2978cde88a79e279afc47f999ddbcc720 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sat, 2 Nov 2024 15:55:45 +0100 Subject: [PATCH 116/125] Add loading spinner for enlarged canvas --- .../pdf-preview-enlarged-canvas.component.html | 7 +++++++ .../pdf-preview-enlarged-canvas.component.ts | 3 +++ .../pdf-preview-thumbnail-grid.component.html | 2 +- .../pdf-preview-thumbnail-grid.component.scss | 4 ++++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.html index 491e723e772f..d282a7381dc9 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.html @@ -1,4 +1,11 @@
+ @if (isEnlargedCanvasLoading()) { +
+
+ +
+
+ } @if (currentPage() !== 1) { diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts index e7f57d5427bc..ea7a9987ff04 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts @@ -22,6 +22,7 @@ export class PdfPreviewEnlargedCanvasComponent { // Signals currentPage = signal(1); + isEnlargedCanvasLoading = signal(false); //Outputs isEnlargedViewOutput = output(); @@ -68,6 +69,7 @@ export class PdfPreviewEnlargedCanvasComponent { }; displayEnlargedCanvas(originalCanvas: HTMLCanvasElement) { + this.isEnlargedCanvasLoading.set(true); this.currentPage.set(Number(originalCanvas.id)); this.toggleBodyScroll(true); setTimeout(() => { @@ -92,6 +94,7 @@ export class PdfPreviewEnlargedCanvasComponent { this.resizeCanvas(originalCanvas, scaleFactor); this.redrawCanvas(originalCanvas); this.positionCanvas(); + this.isEnlargedCanvasLoading.set(false); }); } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html index 24b713acb796..01ca1b9617a2 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.html @@ -1,7 +1,7 @@
@if (isEnlargedView()) { Date: Sat, 2 Nov 2024 17:48:28 +0100 Subject: [PATCH 117/125] Fix client test --- .../pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts index 86473069eb8b..0b293d034840 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts @@ -42,7 +42,7 @@ describe('PdfPreviewEnlargedCanvasComponent', () => { component.enlargedCanvas = signal({ nativeElement: mockEnlargedCanvas }); mockContainer = document.createElement('div'); - component.pdfContainer = signal(mockContainer); + fixture.componentRef.setInput('pdfContainer', mockContainer); mockCanvasElement = document.createElement('canvas'); @@ -164,7 +164,7 @@ describe('PdfPreviewEnlargedCanvasComponent', () => { Object.defineProperty(mockPdfContainer, 'scrollTop', { value: 500, writable: true }); // Assign pdfContainer and enlargedCanvas on the component with the mock structure - component.pdfContainer = signal({ nativeElement: mockPdfContainer }); + fixture.componentRef.setInput('pdfContainer', mockPdfContainer); // Create a canvas and set it as enlargedCanvas with width and height const canvasElem = document.createElement('canvas'); From bcb2ab6dfdd0917aaaa3f61408bbfb602f62d426 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sat, 2 Nov 2024 20:20:25 +0100 Subject: [PATCH 118/125] Remove redundant canvas positioning --- .../pdf-preview-enlarged-canvas.component.ts | 17 ----------- .../pdf-preview-thumbnail-grid.component.scss | 2 +- .../pdf-preview/pdf-preview.component.html | 1 + .../pdf-preview/pdf-preview.component.scss | 4 +++ ...-preview-enlarged-canvas.component.spec.ts | 29 +------------------ 5 files changed, 7 insertions(+), 46 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts index ea7a9987ff04..38d8da4e08b9 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts @@ -93,7 +93,6 @@ export class PdfPreviewEnlargedCanvasComponent { const scaleFactor = this.calculateScaleFactor(originalCanvas); this.resizeCanvas(originalCanvas, scaleFactor); this.redrawCanvas(originalCanvas); - this.positionCanvas(); this.isEnlargedCanvasLoading.set(false); }); } @@ -152,22 +151,6 @@ export class PdfPreviewEnlargedCanvasComponent { context!.drawImage(originalCanvas, 0, 0, enlargedCanvas.width, enlargedCanvas.height); } - /** - * Adjusts the position of the enlarged canvas to center it within the viewport of the PDF container. - * This method ensures that the canvas is both vertically and horizontally centered, providing a consistent - * and visually appealing layout. - */ - positionCanvas(): void { - const enlargedCanvas = this.enlargedCanvas().nativeElement; - const containerWidth = this.pdfContainer().clientWidth; - const containerHeight = this.pdfContainer().clientHeight; - - enlargedCanvas.style.position = 'absolute'; - enlargedCanvas.style.left = `${(containerWidth - enlargedCanvas.width) / 2}px`; - enlargedCanvas.style.top = `${(containerHeight - enlargedCanvas.height) / 2}px`; - enlargedCanvas.parentElement!.style.top = `${this.pdfContainer().scrollTop}px`; - } - /** * Adjusts the size of the PDF container based on whether the enlarged view is active or not. * If the enlarged view is active, the container's size is reduced to focus on the enlarged content. diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.scss index cbe9131ec39c..544b91ec4c55 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.scss @@ -1,5 +1,4 @@ .pdf-container { - position: relative; display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 10px; @@ -23,4 +22,5 @@ .enlarged-canvas { display: contents; + position: absolute; } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index ceeccc8fb266..846ac0cbbaf8 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -41,6 +41,7 @@

@if (currentPdfUrl()) { { }); }); - describe('Positioning and Layout', () => { - it('should correctly position the canvas', () => { - // Mock the container and necessary dimensions - const mockPdfContainer = document.createElement('div'); - Object.defineProperty(mockPdfContainer, 'clientWidth', { value: 1000 }); - Object.defineProperty(mockPdfContainer, 'clientHeight', { value: 800 }); - Object.defineProperty(mockPdfContainer, 'scrollTop', { value: 500, writable: true }); - - // Assign pdfContainer and enlargedCanvas on the component with the mock structure - fixture.componentRef.setInput('pdfContainer', mockPdfContainer); - - // Create a canvas and set it as enlargedCanvas with width and height - const canvasElem = document.createElement('canvas'); - canvasElem.width = 500; - canvasElem.height = 400; - - component.enlargedCanvas = signal({ nativeElement: canvasElem }); - mockPdfContainer.appendChild(canvasElem); // Ensure canvas is part of the container - - // Run positionCanvas and check positioning - component.positionCanvas(); - - // Verify the expected styles for positioning - //expect(canvasElem.style.left).toBe('250px'); - //expect(canvasElem.style.top).toBe('200px'); - //expect(mockPdfContainer.style.top).toBe('500px'); - }); - + describe('Layout', () => { it('should prevent scrolling when enlarged view is active', () => { component.toggleBodyScroll(true); expect(mockContainer.style.overflow).toBe('hidden'); From 0b5d7cabdbdafa4d62f38141e0ab62d705fd9191 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sat, 2 Nov 2024 20:30:33 +0100 Subject: [PATCH 119/125] Client test clean up --- ...f-preview-enlarged-canvas.component.spec.ts | 2 -- ...df-preview-thumbnail-grid.component.spec.ts | 5 ----- .../pdf-preview/pdf-preview.component.spec.ts | 18 ++++++++---------- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts index c865fba057f7..01812980adc0 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts @@ -191,13 +191,11 @@ describe('PdfPreviewEnlargedCanvasComponent', () => { it('should not close the enlarged view if the click is on the canvas itself', () => { const mockEvent = createMockEvent(mockEnlargedCanvas); - component.isEnlargedViewOutput.emit(true); const closeSpy = jest.spyOn(component, 'closeEnlargedView'); component.closeIfOutside(mockEvent as unknown as MouseEvent); - expect(closeSpy).not.toHaveBeenCalled(); }); }); diff --git a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-thumbnail-grid.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-thumbnail-grid.component.spec.ts index 6cabb9813a9c..968e6830506f 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-thumbnail-grid.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-thumbnail-grid.component.spec.ts @@ -61,15 +61,12 @@ describe('PdfPreviewThumbnailGridComponent', () => { it('should load PDF and render pages', async () => { const spyCreateCanvas = jest.spyOn(component, 'createCanvas'); const spyCreateCanvasContainer = jest.spyOn(component, 'createCanvasContainer'); - //const spyAppendChild = jest.spyOn(mockContainer, 'appendChild'); await component.loadOrAppendPdf('fake-url'); expect(spyCreateCanvas).toHaveBeenCalled(); expect(spyCreateCanvasContainer).toHaveBeenCalled(); - //expect(spyAppendChild).toHaveBeenCalled(); expect(component.totalPages()).toBe(1); - //expect(component.isPdfLoading()).toBeFalsy(); }); it('should toggle enlarged view state', () => { @@ -86,11 +83,9 @@ describe('PdfPreviewThumbnailGridComponent', () => { const container = component.createCanvasContainer(mockCanvas, 1); const overlay = container.querySelector('div'); - // Trigger mouseenter container.dispatchEvent(new Event('mouseenter')); expect(overlay!.style.opacity).toBe('1'); - // Trigger mouseleave container.dispatchEvent(new Event('mouseleave')); expect(overlay!.style.opacity).toBe('0'); }); diff --git a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview.component.spec.ts index 29a248915deb..2f0b2ed366f7 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview.component.spec.ts @@ -270,7 +270,7 @@ describe('PdfPreviewComponent', () => { describe('PDF Merging', () => { it('should merge PDF files correctly and update the component state', async () => { const mockFile = new File(['new pdf'], 'test.pdf', { type: 'application/pdf' }); - mockFile.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity + mockFile.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); const mockEvent = { target: { files: [mockFile] } }; const existingPdfDoc = { @@ -289,9 +289,9 @@ describe('PdfPreviewComponent', () => { .mockImplementationOnce(() => Promise.resolve(newPdfDoc)); component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); - component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity + component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); - component.selectedPages.set(new Set([1])); // Assume there is initially a selected page + component.selectedPages.set(new Set([1])); await component.mergePDF(mockEvent as any); @@ -308,20 +308,18 @@ describe('PdfPreviewComponent', () => { it('should handle errors when merging PDFs fails', async () => { const mockFile = new File(['new pdf'], 'test.pdf', { type: 'application/pdf' }); - // Mock the arrayBuffer method for the file object - mockFile.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simplicity + mockFile.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); const mockEvent = { target: { files: [mockFile] } }; const error = new Error('Error loading PDF'); component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); - component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); // Return an empty ArrayBuffer for simp + component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); - // Mock PDFDocument.load to throw an error on the first call PDFDocument.load = jest .fn() - .mockImplementationOnce(() => Promise.reject(error)) // First call throws an error - .mockImplementationOnce(() => Promise.resolve({})); // Second call (not actually needed here) + .mockImplementationOnce(() => Promise.reject(error)) + .mockImplementationOnce(() => Promise.resolve({})); await component.mergePDF(mockEvent as any); @@ -342,7 +340,7 @@ describe('PdfPreviewComponent', () => { component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' })); component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(mockArrayBuffer); - component.selectedPages.set(new Set([1, 2])); // Pages 1 and 2 selected + component.selectedPages.set(new Set([1, 2])); const alertServiceErrorSpy = jest.spyOn(alertServiceMock, 'error'); From 57ddcd3cb907dd8541b27c308c3ebcfa61ad287c Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sat, 2 Nov 2024 23:44:28 +0100 Subject: [PATCH 120/125] Add button checks --- .../app/lecture/pdf-preview/pdf-preview.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 846ac0cbbaf8..6b8e5c4d223e 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -26,14 +26,14 @@

[deleteQuestion]="allPagesSelected() ? 'artemisApp.attachment.pdfPreview.deleteAllPagesQuestion' : 'artemisApp.attachment.pdfPreview.deletePagesQuestion'" (delete)="allPagesSelected() ? deleteAttachmentFile() : deleteSelectedSlides()" [dialogError]="dialogError$" - [disabled]="selectedPages()!.size === 0" + [disabled]="isPdfLoading() || selectedPages().size === 0" aria-label="Delete selected pages" > - @@ -66,7 +66,7 @@

- From 4f87cb35186f8ce57372acd84ad1b49574cccb3b Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 3 Nov 2024 20:13:30 +0100 Subject: [PATCH 121/125] Revert "Remove redundant canvas positioning" --- .../pdf-preview-enlarged-canvas.component.ts | 17 +++++++++++ .../pdf-preview-thumbnail-grid.component.scss | 2 +- .../pdf-preview/pdf-preview.component.html | 1 - .../pdf-preview/pdf-preview.component.scss | 4 --- ...-preview-enlarged-canvas.component.spec.ts | 29 ++++++++++++++++++- 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts index 38d8da4e08b9..ea7a9987ff04 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts @@ -93,6 +93,7 @@ export class PdfPreviewEnlargedCanvasComponent { const scaleFactor = this.calculateScaleFactor(originalCanvas); this.resizeCanvas(originalCanvas, scaleFactor); this.redrawCanvas(originalCanvas); + this.positionCanvas(); this.isEnlargedCanvasLoading.set(false); }); } @@ -151,6 +152,22 @@ export class PdfPreviewEnlargedCanvasComponent { context!.drawImage(originalCanvas, 0, 0, enlargedCanvas.width, enlargedCanvas.height); } + /** + * Adjusts the position of the enlarged canvas to center it within the viewport of the PDF container. + * This method ensures that the canvas is both vertically and horizontally centered, providing a consistent + * and visually appealing layout. + */ + positionCanvas(): void { + const enlargedCanvas = this.enlargedCanvas().nativeElement; + const containerWidth = this.pdfContainer().clientWidth; + const containerHeight = this.pdfContainer().clientHeight; + + enlargedCanvas.style.position = 'absolute'; + enlargedCanvas.style.left = `${(containerWidth - enlargedCanvas.width) / 2}px`; + enlargedCanvas.style.top = `${(containerHeight - enlargedCanvas.height) / 2}px`; + enlargedCanvas.parentElement!.style.top = `${this.pdfContainer().scrollTop}px`; + } + /** * Adjusts the size of the PDF container based on whether the enlarged view is active or not. * If the enlarged view is active, the container's size is reduced to focus on the enlarged content. diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.scss index 544b91ec4c55..cbe9131ec39c 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component.scss @@ -1,4 +1,5 @@ .pdf-container { + position: relative; display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 10px; @@ -22,5 +23,4 @@ .enlarged-canvas { display: contents; - position: absolute; } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 6b8e5c4d223e..a4418df3fe25 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -41,7 +41,6 @@

@if (currentPdfUrl()) { { }); }); - describe('Layout', () => { + describe('Positioning and Layout', () => { + it('should correctly position the canvas', () => { + // Mock the container and necessary dimensions + const mockPdfContainer = document.createElement('div'); + Object.defineProperty(mockPdfContainer, 'clientWidth', { value: 1000 }); + Object.defineProperty(mockPdfContainer, 'clientHeight', { value: 800 }); + Object.defineProperty(mockPdfContainer, 'scrollTop', { value: 500, writable: true }); + + // Assign pdfContainer and enlargedCanvas on the component with the mock structure + fixture.componentRef.setInput('pdfContainer', mockPdfContainer); + + // Create a canvas and set it as enlargedCanvas with width and height + const canvasElem = document.createElement('canvas'); + canvasElem.width = 500; + canvasElem.height = 400; + + component.enlargedCanvas = signal({ nativeElement: canvasElem }); + mockPdfContainer.appendChild(canvasElem); // Ensure canvas is part of the container + + // Run positionCanvas and check positioning + component.positionCanvas(); + + // Verify the expected styles for positioning + //expect(canvasElem.style.left).toBe('250px'); + //expect(canvasElem.style.top).toBe('200px'); + //expect(mockPdfContainer.style.top).toBe('500px'); + }); + it('should prevent scrolling when enlarged view is active', () => { component.toggleBodyScroll(true); expect(mockContainer.style.overflow).toBe('hidden'); From a7a8566cb919c97aa701a60404dc04aed9ee94cf Mon Sep 17 00:00:00 2001 From: Aybike Ece Eren Date: Sun, 3 Nov 2024 20:32:15 +0100 Subject: [PATCH 122/125] Lectures: Fix undefined course issue in lecture attachments (#9601) --- src/main/webapp/app/lecture/attachment.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/lecture/attachment.service.ts b/src/main/webapp/app/lecture/attachment.service.ts index 0ba18b39a248..446d26960f8b 100644 --- a/src/main/webapp/app/lecture/attachment.service.ts +++ b/src/main/webapp/app/lecture/attachment.service.ts @@ -6,6 +6,7 @@ import { createRequestOption } from 'app/shared/util/request.util'; import { Attachment } from 'app/entities/attachment.model'; import { convertDateFromClient, convertDateFromServer } from 'app/utils/date.utils'; import { objectToJsonBlob } from 'app/utils/blob-util'; +import { cloneDeep } from 'lodash-es'; type EntityResponseType = HttpResponse; type EntityArrayResponseType = HttpResponse; @@ -96,11 +97,11 @@ export class AttachmentService { } convertAttachmentDatesFromClient(attachment: Attachment): Attachment { - const copy: Attachment = Object.assign({}, attachment, { + // Deep clone is applied to preserve all nested properties of the attachment + return Object.assign({}, cloneDeep(attachment), { releaseDate: convertDateFromClient(attachment.releaseDate), uploadDate: convertDateFromClient(attachment.uploadDate), }); - return copy; } convertAttachmentDatesFromServer(attachment?: Attachment) { From bcb89be2669246c681c74926f08bf4f05f22273c Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 3 Nov 2024 20:39:11 +0100 Subject: [PATCH 123/125] Remove redundant positioning & styling --- ...pdf-preview-enlarged-canvas.component.html | 2 +- ...pdf-preview-enlarged-canvas.component.scss | 2 -- .../pdf-preview-enlarged-canvas.component.ts | 19 ++---------- .../pdf-preview/pdf-preview.component.html | 2 +- .../pdf-preview/pdf-preview.component.scss | 3 -- .../pdf-preview/pdf-preview.component.ts | 1 - ...-preview-enlarged-canvas.component.spec.ts | 29 +------------------ 7 files changed, 5 insertions(+), 53 deletions(-) delete mode 100644 src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.html index d282a7381dc9..f5dd4f631994 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.html @@ -1,4 +1,4 @@ -
+
@if (isEnlargedCanvasLoading()) {
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.scss index 2eeabbadbeae..333c12462b49 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.scss +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.scss @@ -1,7 +1,5 @@ .enlarged-container { position: absolute; - top: 0; - left: 0; width: 100%; height: 100%; display: flex; diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts index ea7a9987ff04..789aba78aefb 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component.ts @@ -11,6 +11,7 @@ type NavigationDirection = 'next' | 'prev'; imports: [ArtemisSharedModule], }) export class PdfPreviewEnlargedCanvasComponent { + enlargedContainer = viewChild.required>('enlargedContainer'); enlargedCanvas = viewChild.required>('enlargedCanvas'); readonly DEFAULT_ENLARGED_SLIDE_HEIGHT = 800; @@ -30,6 +31,7 @@ export class PdfPreviewEnlargedCanvasComponent { constructor() { effect( () => { + this.enlargedContainer().nativeElement.style.top = `${this.pdfContainer().scrollTop}px`; this.displayEnlargedCanvas(this.originalCanvas()!); }, { allowSignalWrites: true }, @@ -93,7 +95,6 @@ export class PdfPreviewEnlargedCanvasComponent { const scaleFactor = this.calculateScaleFactor(originalCanvas); this.resizeCanvas(originalCanvas, scaleFactor); this.redrawCanvas(originalCanvas); - this.positionCanvas(); this.isEnlargedCanvasLoading.set(false); }); } @@ -152,22 +153,6 @@ export class PdfPreviewEnlargedCanvasComponent { context!.drawImage(originalCanvas, 0, 0, enlargedCanvas.width, enlargedCanvas.height); } - /** - * Adjusts the position of the enlarged canvas to center it within the viewport of the PDF container. - * This method ensures that the canvas is both vertically and horizontally centered, providing a consistent - * and visually appealing layout. - */ - positionCanvas(): void { - const enlargedCanvas = this.enlargedCanvas().nativeElement; - const containerWidth = this.pdfContainer().clientWidth; - const containerHeight = this.pdfContainer().clientHeight; - - enlargedCanvas.style.position = 'absolute'; - enlargedCanvas.style.left = `${(containerWidth - enlargedCanvas.width) / 2}px`; - enlargedCanvas.style.top = `${(containerHeight - enlargedCanvas.height) / 2}px`; - enlargedCanvas.parentElement!.style.top = `${this.pdfContainer().scrollTop}px`; - } - /** * Adjusts the size of the PDF container based on whether the enlarged view is active or not. * If the enlarged view is active, the container's size is reduced to focus on the enlarged content. diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index a4418df3fe25..16fc58678b07 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -11,7 +11,7 @@

}

@if (isPdfLoading()) { -
+
} diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss deleted file mode 100644 index d0215c72e7f6..000000000000 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.spinner-border { - margin-left: 10px; -} diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index efdda073ba88..c912c211d3c7 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -21,7 +21,6 @@ import { PDFDocument } from 'pdf-lib'; @Component({ selector: 'jhi-pdf-preview-component', templateUrl: './pdf-preview.component.html', - styleUrls: ['./pdf-preview.component.scss'], standalone: true, imports: [ArtemisSharedModule, PdfPreviewThumbnailGridComponent], }) diff --git a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts index 65c3fcc5e964..01812980adc0 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-enlarged-canvas.component.spec.ts @@ -155,34 +155,7 @@ describe('PdfPreviewEnlargedCanvasComponent', () => { }); }); - describe('Positioning and Layout', () => { - it('should correctly position the canvas', () => { - // Mock the container and necessary dimensions - const mockPdfContainer = document.createElement('div'); - Object.defineProperty(mockPdfContainer, 'clientWidth', { value: 1000 }); - Object.defineProperty(mockPdfContainer, 'clientHeight', { value: 800 }); - Object.defineProperty(mockPdfContainer, 'scrollTop', { value: 500, writable: true }); - - // Assign pdfContainer and enlargedCanvas on the component with the mock structure - fixture.componentRef.setInput('pdfContainer', mockPdfContainer); - - // Create a canvas and set it as enlargedCanvas with width and height - const canvasElem = document.createElement('canvas'); - canvasElem.width = 500; - canvasElem.height = 400; - - component.enlargedCanvas = signal({ nativeElement: canvasElem }); - mockPdfContainer.appendChild(canvasElem); // Ensure canvas is part of the container - - // Run positionCanvas and check positioning - component.positionCanvas(); - - // Verify the expected styles for positioning - //expect(canvasElem.style.left).toBe('250px'); - //expect(canvasElem.style.top).toBe('200px'); - //expect(mockPdfContainer.style.top).toBe('500px'); - }); - + describe('Layout', () => { it('should prevent scrolling when enlarged view is active', () => { component.toggleBodyScroll(true); expect(mockContainer.style.overflow).toBe('hidden'); From 9ad8e5427322f8899a8cbfff08b296effe0a7edf Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 10 Nov 2024 19:47:15 +0100 Subject: [PATCH 124/125] Add empty pdf container & disable merge pdf in the beginning --- .../app/lecture/pdf-preview/pdf-preview.component.html | 4 +++- .../app/lecture/pdf-preview/pdf-preview.component.scss | 7 +++++++ .../app/lecture/pdf-preview/pdf-preview.component.ts | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html index 16fc58678b07..ace60a5014e2 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.html @@ -33,7 +33,7 @@

- @@ -47,6 +47,8 @@

(selectedPagesOutput)="selectedPages.set($event)" (totalPagesOutput)="totalPages.set($event)" /> + } @else { +
}
diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss new file mode 100644 index 000000000000..aa9ef959e0bf --- /dev/null +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -0,0 +1,7 @@ +.empty-pdf-container { + height: 60vh; + border: 1px solid var(--border-color); + margin: 10px 0; + box-shadow: 0 2px 5px var(--pdf-preview-pdf-container-shadow); + z-index: 0; +} diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index c912c211d3c7..efdda073ba88 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -21,6 +21,7 @@ import { PDFDocument } from 'pdf-lib'; @Component({ selector: 'jhi-pdf-preview-component', templateUrl: './pdf-preview.component.html', + styleUrls: ['./pdf-preview.component.scss'], standalone: true, imports: [ArtemisSharedModule, PdfPreviewThumbnailGridComponent], }) From acba466ff2a9dcc7c6df604e9cf9ed9c0acee079 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Sun, 10 Nov 2024 21:45:54 +0100 Subject: [PATCH 125/125] CodeRabbitAI changes --- .../app/lecture/pdf-preview/pdf-preview.component.ts | 11 ++--------- .../component/lecture/pdf-preview.component.spec.ts | 0 2 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index efdda073ba88..d3271475e580 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, OnDestroy, OnInit, inject, signal, viewChild } from '@angular/core'; +import { Component, ElementRef, OnDestroy, OnInit, computed, inject, signal, viewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { AttachmentService } from 'app/lecture/attachment.service'; import { Attachment } from 'app/entities/attachment.model'; @@ -43,6 +43,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { appendFile = signal(false); isFileChanged = signal(false); selectedPages = signal>(new Set()); + allPagesSelected = computed(() => this.selectedPages().size === this.totalPages()); // Injected services private readonly route = inject(ActivatedRoute); @@ -92,14 +93,6 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { this.attachmentUnitSub?.unsubscribe(); } - /** - * Checks if all pages are selected. - * @returns True if the number of selected pages equals the total number of pages, otherwise false. - */ - allPagesSelected() { - return this.selectedPages().size === this.totalPages(); - } - /** * Triggers the file input to select files. */ diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts deleted file mode 100644 index e69de29bb2d1..000000000000