From 8768f66892f18eb8243b2cddb25b0b093ad25c0a Mon Sep 17 00:00:00 2001 From: Thomas Hackl Date: Wed, 18 Jun 2025 09:38:03 +0200 Subject: Resolve "PDF-Dateien annotieren" Closes #5397 Merge request studip/studip!4201 --- .gitignore | 1 + app/controllers/file.php | 7 + app/controllers/files_dashboard/helpers.php | 8 + app/views/file/annotate_pdf.php | 25 ++ lib/classes/JsonApi/RouteMap.php | 3 + lib/classes/JsonApi/Routes/Files/Authority.php | 8 + .../Routes/Files/FileRefsAnnotationCreate.php | 44 ++++ .../JsonApi/Routes/Files/FilesAnnotationsIndex.php | 35 +++ .../JsonApi/Routes/Files/RoutesHelperTrait.php | 1 + lib/filesystem/StandardFile.php | 15 ++ lib/models/FileRef.php | 101 ++++++++ package-lock.json | 267 ++++++++++++++++++++- package.json | 1 + resources/assets/stylesheets/scss/buttons.scss | 4 + resources/studip.d.ts | 7 + resources/vue/apps/PdfAnnotator.vue | 78 ++++++ resources/vue/components/PdfJsViewer.vue | 107 +++++++++ webpack.common.js | 4 + 18 files changed, 712 insertions(+), 4 deletions(-) create mode 100644 app/views/file/annotate_pdf.php create mode 100644 lib/classes/JsonApi/Routes/Files/FileRefsAnnotationCreate.php create mode 100644 lib/classes/JsonApi/Routes/Files/FilesAnnotationsIndex.php create mode 100644 resources/vue/apps/PdfAnnotator.vue create mode 100644 resources/vue/components/PdfJsViewer.vue diff --git a/.gitignore b/.gitignore index db28479..07da848 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ public/.htaccess public/.rnd public/assets/javascripts/*.js public/assets/javascripts/*.js.map +public/assets/javascripts/pdfjs public/assets/stylesheets/*.css public/assets/stylesheets/*.css.map public/pictures/banner/*.gif diff --git a/app/controllers/file.php b/app/controllers/file.php index e0cfa44..1dbbf96 100644 --- a/app/controllers/file.php +++ b/app/controllers/file.php @@ -1578,6 +1578,13 @@ class FileController extends AuthenticatedController } } + /** + * Display the data-dialog UI to annotate the given fileref in PDF.js (for grading homework). + */ + public function annotate_pdf_action($file_ref_id) { + $this->file_ref = FileRef::find($file_ref_id)->toRawArray(); + $this->userFullname = User::findCurrent()->getFullName(); + } protected function loadFiles($param = 'files', $plugin = null, $with_blob = false) { diff --git a/app/controllers/files_dashboard/helpers.php b/app/controllers/files_dashboard/helpers.php index d86fc9f..1ec673d 100644 --- a/app/controllers/files_dashboard/helpers.php +++ b/app/controllers/files_dashboard/helpers.php @@ -44,6 +44,14 @@ trait Helpers Icon::create('edit'), ['data-dialog' => ''] ); + if ('application/pdf' === $fileRef->file->mime_type) { + $actionMenu->addLink( + URLHelper::getURL('dispatch.php/file/annotate_pdf/' . $fileRef->id), + _('PDF-Werkzeuge'), + Icon::create('comment'), + ['data-dialog' => 'size=big'] + ); + } $actionMenu->addLink( URLHelper::getURL('dispatch.php/file/update/'.$fileRef->id), _('Datei aktualisieren'), diff --git a/app/views/file/annotate_pdf.php b/app/views/file/annotate_pdf.php new file mode 100644 index 0000000..a732a8f --- /dev/null +++ b/app/views/file/annotate_pdf.php @@ -0,0 +1,25 @@ + +withProps([ + 'file-ref' => $file_ref, + 'user-fullname' => $userFullname, + 'class' => 'annotate-pdf-root' +]) ?> + diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index 6b2f429..86c6d92 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -636,6 +636,8 @@ class RouteMap $group->get('/file-refs/{id}/content', Routes\Files\FileRefsContentShow::class); $group->post('/file-refs/{id}/content', Routes\Files\FileRefsContentUpdate::class); + $group->post('/file-refs/{id}/annotations', Routes\Files\FileRefsAnnotationCreate::class); + $group->get('/folders/{id}', Routes\Files\FoldersShow::class); $group->patch('/folders/{id}', Routes\Files\FoldersUpdate::class); $group->delete('/folders/{id}', Routes\Files\FoldersDelete::class); @@ -651,6 +653,7 @@ class RouteMap $group->get('/files/{id}', Routes\Files\FilesShow::class); $group->get('/files/{id}/file-refs', Routes\Files\FileRefsOfFilesShow::class); + $group->get('/files/{id}/annotations', Routes\Files\FilesAnnotationsIndex::class); $this->addRelationship($group, '/files/{id}/relationships/file-refs', Routes\Files\Rel\FileRefsOfFile::class); } diff --git a/lib/classes/JsonApi/Routes/Files/Authority.php b/lib/classes/JsonApi/Routes/Files/Authority.php index 8f25bb6..f96475d 100644 --- a/lib/classes/JsonApi/Routes/Files/Authority.php +++ b/lib/classes/JsonApi/Routes/Files/Authority.php @@ -139,4 +139,12 @@ class Authority { return self::canCreateFileRefsInFolder($user, $destinationFolder) && self::canShowFolder($user, $sourceFolder); } + + public static function canAnnotateFileRef(User $user, \FileRef $fileRef) + { + $range = $fileRef->getRangeCourseId(); + return static::canCreateFileRefsInFolder($user, $fileRef->folder->getTypedFolder()) + && $GLOBALS['perm']->have_studip_perm('tutor', $fileRef->getRangeCourseId(), $user->id); + } + } diff --git a/lib/classes/JsonApi/Routes/Files/FileRefsAnnotationCreate.php b/lib/classes/JsonApi/Routes/Files/FileRefsAnnotationCreate.php new file mode 100644 index 0000000..78c0f3e --- /dev/null +++ b/lib/classes/JsonApi/Routes/Files/FileRefsAnnotationCreate.php @@ -0,0 +1,44 @@ +getUser($request); + + $originalFileRef = \FileRef::find($args['id']); + if (!$originalFileRef) { + throw new RecordNotFoundException(); + } + + if ('application/pdf' !== $originalFileRef->file->mime_type) { + throw new UnprocessableEntityException(); + } + + if (!Authority::canAnnotateFileRef($user, $originalFileRef)) { + throw new AuthorizationFailedException(); + } + + $folder = $originalFileRef->folder->getTypedFolder(); + $fileRef = $this->handleUpload($request, $folder); + $fileRef->content_terms_of_use_id = $originalFileRef->content_terms_of_use_id; + $fileRef->folder_id = $originalFileRef->folder_id; + + // Store annotated file, updating its metadata. + $fileRef->setAnnotatedFileVersion($originalFileRef, $user); + $fileRef->store(); + + return $this->redirectToFileRef($response, $fileRef); + } +} diff --git a/lib/classes/JsonApi/Routes/Files/FilesAnnotationsIndex.php b/lib/classes/JsonApi/Routes/Files/FilesAnnotationsIndex.php new file mode 100644 index 0000000..9ecc356 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Files/FilesAnnotationsIndex.php @@ -0,0 +1,35 @@ +getUser($request), $file)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse( + \File::findBySQL( + "`metadata` LIKE :annotationref ORDER BY `name`", + ['annotationref' => '%"annotations:original_file_id":"' .$file->id . '"%'] + ) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/Files/RoutesHelperTrait.php b/lib/classes/JsonApi/Routes/Files/RoutesHelperTrait.php index fea4f16..65d3b30 100644 --- a/lib/classes/JsonApi/Routes/Files/RoutesHelperTrait.php +++ b/lib/classes/JsonApi/Routes/Files/RoutesHelperTrait.php @@ -185,6 +185,7 @@ trait RoutesHelperTrait /** * @SuppressWarnings(PHPMD.Superglobals) + * @return \FileRef|null */ protected function handleUpload(Request $request, \FolderType $folder) { diff --git a/lib/filesystem/StandardFile.php b/lib/filesystem/StandardFile.php index b11e2f5..2aaf3d4 100644 --- a/lib/filesystem/StandardFile.php +++ b/lib/filesystem/StandardFile.php @@ -248,6 +248,13 @@ class StandardFile implements FileType, ArrayAccess, StandardFileInterface ['data-dialog' => ''], 'file-edit' ); + if ('application/pdf' === $this->fileref->file->mime_type) { + $actionMenu->addLink( + URLHelper::getURL('dispatch.php/file/annotate_pdf/' . $this->fileref->id), + _('PDF-Werkzeuge'), + Icon::create('comment'), + ['data-dialog' => 'size=big'] ); + } $actionMenu->addLink( URLHelper::getURL('dispatch.php/file/update/' . $this->fileref->id), _('Datei aktualisieren'), @@ -344,6 +351,14 @@ class StandardFile implements FileType, ArrayAccess, StandardFileInterface URLHelper::getURL("dispatch.php/file/edit/{$this->getId()}", $extra_link_params), ['data-dialog' => ''] ); + + if ('application/pdf' === $this->getMimeType()) { + $buttons[] = Studip\LinkButton::createComment( + _('PDF-Werkzeuge'), + URLHelper::getURL("dispatch.php/file/annotate_pdf/{$this->getId()}", $extra_link_params), + ['data-dialog' => 'size=big'] + ); + } } if ($this->isDownloadable($GLOBALS['user']->id)) { $buttons[] = Studip\LinkButton::createDownload( diff --git a/lib/models/FileRef.php b/lib/models/FileRef.php index 34b3841..070ca0a 100644 --- a/lib/models/FileRef.php +++ b/lib/models/FileRef.php @@ -683,4 +683,105 @@ class FileRef extends SimpleORMap implements PrivacyObject, FeedbackRange $sql_data = self::getUploadedFilesSql($user_id, $begin, $end, $course_id, $unknown_license_only); return self::countBySql($sql_data['sql'], $sql_data['params']); } + + /** + * Creates appropriate metadata for an annotated (PDF) file. + * + * @param File $originalFile the original file that was annotated + * @param User $annotatorId the annotating person + * @param User $owner + * @return FileRef + * @throws Exception + */ + public function setAnnotatedFileVersion(FileRef $originalFileRef, User $annotator) + { + $metadata = $originalFileRef->file->metadata; + + $owner = User::find($metadata['homework:visible_for_user_id'] + ?? $originalFileRef->user_id); + + // Set metadata indicating who has annotated this file. + $annotators = $metadata['annotations:annotated_by']?->getArrayCopy() ?? []; + if (!in_array($annotator->id, $annotators)) { + $annotators[] = $annotator->id; + } + + // Increment the version number by one each time. + $version = ($metadata['annotations:version'] ?? 0) + 1; + + // Generate new file name with version number and the names of the users who annotated it + $originalFilename = $metadata['annotations:original_filename'] + ?? $originalFileRef->file->name; + $annotators_names = \User::findAndMapMany(function ($user) { + return $user->getFullName(); + }, $annotators); + + $pathinfo = pathinfo($originalFilename); + + $annotatedFilename = $pathinfo['filename'] + . ' Version ' . $version . ' mit Anmerkungen von ' . join(', ', $annotators_names) + . '.' . $pathinfo['extension']; + + $this->file->metadata = [ + 'homework:visible_for_user_id' => $owner->id, + 'annotations:annotated_by' => $annotators, + 'annotations:original_filename' => $originalFilename, + 'annotations:original_file_id' => $originalFileRef->file->id, + 'annotations:version' => $version, + ]; + // Set the filename on the file and store it. + $this->file->name = $annotatedFilename; + $this->file->store(); + + $this->name = $this->file->name; + + // TODO Is this the correct behavior? Maybe everyone who annotated the file should be notified? + $this->notifyFileOwner($owner, $annotator, $originalFileRef->folder->getTypedFolder()); + return $this; + } + + /** + * Notify file owner that their file was edited/annotated by another user. + * @param User $owner + * @param User $editor + * @param FolderType $folder + * @return void + */ + public function notifyFileOwner(User $owner, User $editor, FolderType $folder) + { + $tutorName = $editor->getFullName(); + $profileLink = \URLHelper::getLink('dispatch.php/profile?username=' . $editor->username); + $folderName = $folder->name; + $folderLink = \URLHelper::getLink( + 'dispatch.php/course/files/index/' . $folder->getId(), + ['cid' => $folder->range_id] + ); + $rangeName = $folder->getRangeObject()->getFullName(); + $rangeUrl = URLHelper::getLink('dispatch.php/' . $folder->getRangeObject()->getRangeUrl(), ['cid' => $folder->range_id]); + $fileLink = \URLHelper::getLink('dispatch.php/file/details/' . $this->id, ['cid' => $folder->range_id]); + + setTempLanguage($owner->id); + $subject = _('Benachrichtigung Ă¼ber PDF-Annotationen'); + $message = '

'; + $message .= sprintf(_('%1$s hat im Verzeichnis "%2$s" in "%3$s" eine kommentierte Version Ihrer PDF-Datei hochgeladen: %4$s'), + static::formatLink($tutorName, $profileLink), + static::formatLink($folderName, $folderLink), + static::formatLink($rangeName, $rangeUrl), + static::formatLink($this->name, $fileLink), + ); + $message .= '

'; + restoreLanguage(); + + messaging::sendSystemMessage($owner->id, $subject, $message); + } + + /** + * Helper function for formatting a link. + * @param string $text + * @param string $url + * @return string + */ + private static function formatLink(string $text, string $url): string { + return sprintf('%s', htmlReady($url), htmlReady($text)); + } } diff --git a/package-lock.json b/package-lock.json index 8ce8a07..c552be9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@playwright/test": "^1.33.0", "@popperjs/core": "^2.11.2", "@rsdoctor/webpack-plugin": "^1.0.1", + "@studip/pdfjs-studip": "^5.3.54", "@types/jquery": "^3.5.16", "@types/jqueryui": "^1.12.16", "@types/lodash": "^4.14.191", @@ -3618,6 +3619,199 @@ "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", "dev": true }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.70.tgz", + "integrity": "sha512-nD6NGa4JbNYSZYsTnLGrqe9Kn/lCkA4ybXt8sx5ojDqZjr2i0TWAHxx/vhgfjX+i3hCdKWufxYwi7CfXqtITSA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.70", + "@napi-rs/canvas-darwin-arm64": "0.1.70", + "@napi-rs/canvas-darwin-x64": "0.1.70", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.70", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.70", + "@napi-rs/canvas-linux-arm64-musl": "0.1.70", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.70", + "@napi-rs/canvas-linux-x64-gnu": "0.1.70", + "@napi-rs/canvas-linux-x64-musl": "0.1.70", + "@napi-rs/canvas-win32-x64-msvc": "0.1.70" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.70.tgz", + "integrity": "sha512-I/YOuQ0wbkVYxVaYtCgN42WKTYxNqFA0gTcTrHIGG1jfpDSyZWII/uHcjOo4nzd19io6Y4+/BqP8E5hJgf9OmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.70.tgz", + "integrity": "sha512-4pPGyXetHIHkw2TOJHujt3mkCP8LdDu8+CT15ld9Id39c752RcI0amDHSuMLMQfAjvusA9B5kKxazwjMGjEJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.70.tgz", + "integrity": "sha512-+2N6Os9LbkmDMHL+raknrUcLQhsXzc5CSXRbXws9C3pv/mjHRVszQ9dhFUUe9FjfPhCJznO6USVdwOtu7pOrzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.70.tgz", + "integrity": "sha512-QjscX9OaKq/990sVhSMj581xuqLgiaPVMjjYvWaCmAJRkNQ004QfoSMEm3FoTqM4DRoquP8jvuEXScVJsc1rqQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.70.tgz", + "integrity": "sha512-LNakMOwwqwiHIwMpnMAbFRczQMQ7TkkMyATqFCOtUJNlE6LPP/QiUj/mlFrNbUn/hctqShJ60gWEb52ZTALbVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.70.tgz", + "integrity": "sha512-wBTOllEYNfJCHOdZj9v8gLzZ4oY3oyPX8MSRvaxPm/s7RfEXxCyZ8OhJ5xAyicsDdbE5YBZqdmaaeP5+xKxvtg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.70.tgz", + "integrity": "sha512-GVUUPC8TuuFqHip0rxHkUqArQnlzmlXmTEBuXAWdgCv85zTCFH8nOHk/YCF5yo0Z2eOm8nOi90aWs0leJ4OE5Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.70.tgz", + "integrity": "sha512-/kvUa2lZRwGNyfznSn5t1ShWJnr/m5acSlhTV3eXECafObjl0VBuA1HJw0QrilLpb4Fe0VLywkpD1NsMoVDROQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.70.tgz", + "integrity": "sha512-aqlv8MLpycoMKRmds7JWCfVwNf1fiZxaU7JwJs9/ExjTD8lX2KjsO7CTeAj5Cl4aEuzxUWbJPUUE2Qu9cZ1vfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.70.tgz", + "integrity": "sha512-Q9QU3WIpwBTVHk4cPfBjGHGU4U0llQYRXgJtFtYqqGNEOKVN4OT6PQ+ve63xwIPODMpZ0HHyj/KLGc9CWc3EtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -4233,6 +4427,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@studip/pdfjs-studip": { + "version": "5.3.54", + "resolved": "https://registry.npmjs.org/@studip/pdfjs-studip/-/pdfjs-studip-5.3.54.tgz", + "integrity": "sha512-soIy9BcPjLrXWt67JUyO/PHbEFRcfp8+6xntI7OODyr8geNVU3MF2HsepIeTL+OmYU36Ay87XRLm/mE+KyBWNw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.67" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -6160,6 +6367,7 @@ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", "dev": true, + "optional": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -6173,6 +6381,7 @@ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", "dev": true, + "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" @@ -6799,6 +7008,43 @@ "node": ">=10.13.0" } }, + "node_modules/copy-webpack-plugin": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.0.tgz", + "integrity": "sha512-FgR/h5a6hzJqATDGd9YG41SeDViH+0bkHn6WNXCi5zKAZkeESeSxLySSsFLHqLEVCh0E+rITmCf0dusXWYukeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-parent": "^6.0.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2", + "tinyglobby": "^0.2.12" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/core-js": { "version": "3.31.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.0.tgz", @@ -7927,7 +8173,8 @@ "resolved": "https://registry.npmjs.org/dommatrix/-/dommatrix-1.0.3.tgz", "integrity": "sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==", "deprecated": "dommatrix is no longer maintained. Please use @thednp/dommatrix.", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/dompurify": { "version": "2.5.8", @@ -7969,6 +8216,7 @@ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, + "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -8187,6 +8435,7 @@ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, + "optional": true, "engines": { "node": ">= 0.4" } @@ -8196,6 +8445,7 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, + "optional": true, "engines": { "node": ">= 0.4" } @@ -8211,6 +8461,7 @@ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, + "optional": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -9288,6 +9539,7 @@ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "dev": true, + "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", @@ -9334,6 +9586,7 @@ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, + "optional": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -9511,6 +9764,7 @@ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, + "optional": true, "engines": { "node": ">= 0.4" }, @@ -9590,6 +9844,7 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "optional": true, "engines": { "node": ">= 0.4" }, @@ -9625,6 +9880,7 @@ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "optional": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -11430,6 +11686,7 @@ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, + "optional": true, "engines": { "node": ">= 0.4" } @@ -12164,6 +12421,7 @@ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.16.105.tgz", "integrity": "sha512-J4dn41spsAwUxCpEoVf6GVoz908IAA3mYiLmNxg8J9kfRXc2jxpbUepcP0ocp0alVNLFthTAM8DZ1RaHh8sU0A==", "dev": true, + "license": "Apache-2.0", "dependencies": { "dommatrix": "^1.0.3", "web-streams-polyfill": "^3.2.1" @@ -15943,10 +16201,11 @@ } }, "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } diff --git a/package.json b/package.json index 31cc444..cce9fac 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@playwright/test": "^1.33.0", "@popperjs/core": "^2.11.2", "@rsdoctor/webpack-plugin": "^1.0.1", + "@studip/pdfjs-studip": "^5.3.54", "@types/jquery": "^3.5.16", "@types/jqueryui": "^1.12.16", "@types/lodash": "^4.14.191", diff --git a/resources/assets/stylesheets/scss/buttons.scss b/resources/assets/stylesheets/scss/buttons.scss index 2b39a21..e251512 100644 --- a/resources/assets/stylesheets/scss/buttons.scss +++ b/resources/assets/stylesheets/scss/buttons.scss @@ -95,6 +95,10 @@ button.button { } } +.ui-button.comment { + @include button-with-icon(comment, clickable, info_alt); +} + /* Grouped Buttons */ .button-group { display: inline-flex; diff --git a/resources/studip.d.ts b/resources/studip.d.ts index c8797cc..ab9b78f 100644 --- a/resources/studip.d.ts +++ b/resources/studip.d.ts @@ -2,9 +2,16 @@ import { Emitter } from 'mitt'; import { URLHelper } from './assets/javascripts/lib/url_helper.d.ts'; export {}; +type SupportedMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'OPTIONS' | 'DELETE'; + declare global { interface Window { STUDIP: { + jsonapi: { + withPromises(): { + [key in SupportedMethod]: (url: string, options?: object) => Promise; + } + } INSTALLED_LANGUAGES: { [name: string]: InstalledLanguage }; ABSOLUTE_URI_STUDIP: string; ASSETS_URL: string; diff --git a/resources/vue/apps/PdfAnnotator.vue b/resources/vue/apps/PdfAnnotator.vue new file mode 100644 index 0000000..06d425c --- /dev/null +++ b/resources/vue/apps/PdfAnnotator.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/resources/vue/components/PdfJsViewer.vue b/resources/vue/components/PdfJsViewer.vue new file mode 100644 index 0000000..8396fda --- /dev/null +++ b/resources/vue/components/PdfJsViewer.vue @@ -0,0 +1,107 @@ +