diff options
author | Julien Veyssier <eneiluj@posteo.net> | 2022-06-08 18:44:49 +0300 |
---|---|---|
committer | Julien Veyssier <eneiluj@posteo.net> | 2022-09-06 15:44:53 +0300 |
commit | 3fec1f052dbb8cbe33172e779ac853eeb934ee62 (patch) | |
tree | 9cbdb5e28225dce8e45b14b06829668615fb6f55 | |
parent | 3af1eb84126c4bb9396b701c963ee229cdc26c66 (diff) |
allow media file upload, handle display in ImageView
Signed-off-by: Julien Veyssier <eneiluj@posteo.net>
-rw-r--r-- | appinfo/routes.php | 5 | ||||
-rw-r--r-- | lib/Controller/ImageController.php | 130 | ||||
-rw-r--r-- | lib/Service/ImageService.php | 257 | ||||
-rw-r--r-- | src/components/Editor/MediaHandler.vue | 41 | ||||
-rw-r--r-- | src/nodes/ImageView.vue | 72 | ||||
-rw-r--r-- | src/services/ImageResolver.js | 88 | ||||
-rw-r--r-- | src/services/SyncService.js | 12 |
7 files changed, 487 insertions, 118 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php index 9d1726566..99bd34973 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -29,7 +29,10 @@ return [ 'routes' => [ ['name' => 'Image#insertImageFile', 'url' => '/image/filepath', 'verb' => 'POST'], ['name' => 'Image#uploadImage', 'url' => '/image/upload', 'verb' => 'POST'], - ['name' => 'Image#getImage', 'url' => '/image', 'verb' => 'GET'], + ['name' => 'Image#getImageFile', 'url' => '/image', 'verb' => 'GET'], + ['name' => 'Image#getMediaFile', 'url' => '/media', 'verb' => 'GET'], + ['name' => 'Image#getMediaFilePreview', 'url' => '/mediaPreview', 'verb' => 'GET'], + ['name' => 'Image#getMediaFileMetadata', 'url' => '/mediaMetadata', 'verb' => 'GET'], ['name' => 'Session#create', 'url' => '/session/create', 'verb' => 'PUT'], ['name' => 'Session#fetch', 'url' => '/session/fetch', 'verb' => 'POST'], diff --git a/lib/Controller/ImageController.php b/lib/Controller/ImageController.php index 219fa7b68..36616e554 100644 --- a/lib/Controller/ImageController.php +++ b/lib/Controller/ImageController.php @@ -33,6 +33,7 @@ use OCA\Text\Service\ImageService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\DataDownloadResponse; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\RedirectResponse; use OCP\Files\IMimeTypeDetector; use OCP\IL10N; use OCP\IRequest; @@ -121,9 +122,11 @@ class ImageController extends Controller { try { $file = $this->getUploadedFile('image'); if (isset($file['tmp_name'], $file['name'], $file['type'])) { + /* if (!in_array($file['type'], self::IMAGE_MIME_TYPES, true)) { return new DataResponse(['error' => 'Image type not supported'], Http::STATUS_BAD_REQUEST); } + */ $newFileResource = fopen($file['tmp_name'], 'rb'); if ($newFileResource === false) { throw new Exception('Could not read file'); @@ -179,7 +182,7 @@ class ImageController extends Controller { * @NoCSRFRequired * @PublicPage * - * Serve the images in the editor + * Serve the image files in the editor * @param int $documentId * @param int $sessionId * @param string $sessionToken @@ -187,17 +190,17 @@ class ImageController extends Controller { * @param string|null $shareToken * @return DataDownloadResponse|DataResponse */ - public function getImage(int $documentId, int $sessionId, string $sessionToken, string $imageFileName, ?string $shareToken = null) { + public function getImageFile(int $documentId, int $sessionId, string $sessionToken, string $imageFileName, ?string $shareToken = null) { if (!$this->sessionService->isValidSession($documentId, $sessionId, $sessionToken)) { return new DataResponse('', Http::STATUS_FORBIDDEN); } try { if ($shareToken) { - $imageFile = $this->imageService->getImagePublic($documentId, $imageFileName, $shareToken); + $imageFile = $this->imageService->getImageFilePublic($documentId, $imageFileName, $shareToken); } else { $userId = $this->getUserIdFromSession($documentId, $sessionId, $sessionToken); - $imageFile = $this->imageService->getImage($documentId, $imageFileName, $userId); + $imageFile = $this->imageService->getImageFile($documentId, $imageFileName, $userId); } return $imageFile !== null ? new DataDownloadResponse( @@ -207,7 +210,124 @@ class ImageController extends Controller { ) : new DataResponse('', Http::STATUS_NOT_FOUND); } catch (Exception $e) { - $this->logger->error('getImage error', ['exception' => $e]); + $this->logger->error('getImageFile error', ['exception' => $e]); + return new DataResponse('', Http::STATUS_NOT_FOUND); + } + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * + * Serve the media files in the editor + * @param int $documentId + * @param int $sessionId + * @param string $sessionToken + * @param string $mediaFileName + * @param string|null $shareToken + * @return DataDownloadResponse|DataResponse + */ + public function getMediaFile(int $documentId, int $sessionId, string $sessionToken, string $mediaFileName, ?string $shareToken = null) { + if (!$this->sessionService->isValidSession($documentId, $sessionId, $sessionToken)) { + return new DataResponse('', Http::STATUS_FORBIDDEN); + } + + try { + if ($shareToken) { + $mediaFile = $this->imageService->getMediaFilePublic($documentId, $mediaFileName, $shareToken); + } else { + $userId = $this->getUserIdFromSession($documentId, $sessionId, $sessionToken); + $mediaFile = $this->imageService->getMediaFile($documentId, $mediaFileName, $userId); + } + return $mediaFile !== null + ? new DataDownloadResponse( + $mediaFile->getContent(), + (string) Http::STATUS_OK, + $this->getSecureMimeType($mediaFile->getMimeType()) + ) + : new DataResponse('', Http::STATUS_NOT_FOUND); + } catch (Exception $e) { + $this->logger->error('getMediaFile error', ['exception' => $e]); + return new DataResponse('', Http::STATUS_NOT_FOUND); + } + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * + * Serve the media files preview in the editor + * @param int $documentId + * @param int $sessionId + * @param string $sessionToken + * @param string $mediaFileName + * @param string|null $shareToken + * @return DataDownloadResponse|DataResponse|RedirectResponse + */ + public function getMediaFilePreview(int $documentId, int $sessionId, string $sessionToken, string $mediaFileName, ?string $shareToken = null) { + if (!$this->sessionService->isValidSession($documentId, $sessionId, $sessionToken)) { + return new DataResponse('', Http::STATUS_FORBIDDEN); + } + + try { + if ($shareToken) { + $preview = $this->imageService->getMediaFilePreviewPublic($documentId, $mediaFileName, $shareToken); + } else { + $userId = $this->getUserIdFromSession($documentId, $sessionId, $sessionToken); + $preview = $this->imageService->getMediaFilePreview($documentId, $mediaFileName, $userId); + } + if ($preview === null) { + return new DataResponse('', Http::STATUS_NOT_FOUND); + } + if ($preview['type'] === 'file') { + return new DataDownloadResponse( + $preview['file']->getContent(), + (string) Http::STATUS_OK, + $this->getSecureMimeType($preview['file']->getMimeType()) + ); + } elseif ($preview['type'] === 'icon') { + return new RedirectResponse($preview['iconUrl']); + } + } catch (Exception $e) { + $this->logger->error('getMediaFilePreview error', ['exception' => $e]); + return new DataResponse('', Http::STATUS_NOT_FOUND); + } + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * + * Serve the media files metadata in the editor + * @param int $documentId + * @param int $sessionId + * @param string $sessionToken + * @param string $mediaFileName + * @param string|null $shareToken + * @return DataResponse + */ + public function getMediaFileMetadata(int $documentId, int $sessionId, string $sessionToken, + string $mediaFileName, ?string $shareToken = null): DataResponse { + if (!$this->sessionService->isValidSession($documentId, $sessionId, $sessionToken)) { + return new DataResponse('', Http::STATUS_FORBIDDEN); + } + + try { + if ($shareToken) { + $metadata = $this->imageService->getMediaFileMetadataPublic($documentId, $mediaFileName, $shareToken); + } else { + $userId = $this->getUserIdFromSession($documentId, $sessionId, $sessionToken); + $metadata = $this->imageService->getMediaFileMetadataPrivate($documentId, $mediaFileName, $userId); + } + if ($metadata === null) { + return new DataResponse('', Http::STATUS_NOT_FOUND); + } + return new DataResponse($metadata); + } catch (Exception $e) { + $this->logger->error('getMediaFileMetadata error', ['exception' => $e]); return new DataResponse('', Http::STATUS_NOT_FOUND); } } diff --git a/lib/Service/ImageService.php b/lib/Service/ImageService.php index 901575355..f305a5e39 100644 --- a/lib/Service/ImageService.php +++ b/lib/Service/ImageService.php @@ -31,12 +31,14 @@ use OCA\Text\Controller\ImageController; use OCP\Constants; use OCP\Files\Folder; use OCP\Files\File; +use OCP\Files\IMimeTypeDetector; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; use OCP\IPreview; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IShare; +use OCP\Util; use Throwable; use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ConnectException; @@ -57,10 +59,6 @@ class ImageService { */ private $rootFolder; /** - * @var IClientService - */ - private $clientService; - /** * @var LoggerInterface */ private $logger; @@ -68,21 +66,25 @@ class ImageService { * @var IPreview */ private $previewManager; + /** + * @var IMimeTypeDetector + */ + private $mimeTypeDetector; public function __construct(IRootFolder $rootFolder, LoggerInterface $logger, ShareManager $shareManager, IPreview $previewManager, - IClientService $clientService) { + IMimeTypeDetector $mimeTypeDetector) { $this->rootFolder = $rootFolder; $this->shareManager = $shareManager; - $this->clientService = $clientService; $this->logger = $logger; $this->previewManager = $previewManager; + $this->mimeTypeDetector = $mimeTypeDetector; } /** - * Get image content or preview from file id + * Get image content or preview from file name * @param int $documentId * @param string $imageFileName * @param string $userId @@ -91,9 +93,9 @@ class ImageService { * @throws \OCP\Files\InvalidPathException * @throws \OCP\Files\NotPermittedException */ - public function getImage(int $documentId, string $imageFileName, string $userId) { + public function getImageFile(int $documentId, string $imageFileName, string $userId) { $textFile = $this->getTextFile($documentId, $userId); - return $this->getImagePreview($imageFileName, $textFile); + return $this->getImageFilePreview($imageFileName, $textFile); } /** @@ -107,9 +109,9 @@ class ImageService { * @throws \OCP\Files\InvalidPathException * @throws \OC\User\NoUserException */ - public function getImagePublic(int $documentId, string $imageFileName, string $shareToken) { + public function getImageFilePublic(int $documentId, string $imageFileName, string $shareToken) { $textFile = $this->getTextFilePublic($documentId, $shareToken); - return $this->getImagePreview($imageFileName, $textFile); + return $this->getImageFilePreview($imageFileName, $textFile); } /** @@ -121,10 +123,10 @@ class ImageService { * @throws \OCP\Files\InvalidPathException * @throws \OC\User\NoUserException */ - private function getImagePreview(string $imageFileName, File $textFile) { + private function getImageFilePreview(string $imageFileName, File $textFile) { $attachmentFolder = $this->getAttachmentDirectoryForFile($textFile, true); $imageFile = $attachmentFolder->get($imageFileName); - if ($imageFile instanceof File) { + if ($imageFile instanceof File && in_array($imageFile->getMimetype(), ImageController::IMAGE_MIME_TYPES)) { if ($this->previewManager->isMimeSupported($imageFile->getMimeType())) { return $this->previewManager->getPreview($imageFile, 1024, 1024); } @@ -134,6 +136,168 @@ class ImageService { } /** + * Get media file from file name + * @param int $documentId + * @param string $mediaFileName + * @param string $userId + * @return File|\OCP\Files\Node|ISimpleFile|null + * @throws NotFoundException + * @throws \OCP\Files\InvalidPathException + * @throws \OCP\Files\NotPermittedException + */ + public function getMediaFile(int $documentId, string $mediaFileName, string $userId) { + $textFile = $this->getTextFile($documentId, $userId); + return $this->getMediaFullFile($mediaFileName, $textFile); + } + + /** + * Get image content or preview from file id in public context + * @param int $documentId + * @param string $mediaFileName + * @param string $shareToken + * @return File|\OCP\Files\Node|ISimpleFile|null + * @throws NotFoundException + * @throws NotPermittedException + * @throws \OCP\Files\InvalidPathException + * @throws \OC\User\NoUserException + */ + public function getMediaFilePublic(int $documentId, string $mediaFileName, string $shareToken) { + $textFile = $this->getTextFilePublic($documentId, $shareToken); + return $this->getMediaFullFile($mediaFileName, $textFile); + } + + /** + * @param string $mediaFileName + * @param File $textFile + * @return File|null + * @throws NotFoundException + * @throws NotPermittedException + * @throws \OCP\Files\InvalidPathException + * @throws \OC\User\NoUserException + */ + private function getMediaFullFile(string $mediaFileName, File $textFile): ?File { + $attachmentFolder = $this->getAttachmentDirectoryForFile($textFile, true); + $mediaFile = $attachmentFolder->get($mediaFileName); + if ($mediaFile instanceof File) { + return $mediaFile; + } + return null; + } + + /** + * @param int $documentId + * @param string $mediaFileName + * @param string $userId + * @return array|null + * @throws NotFoundException + * @throws NotPermittedException + * @throws \OCP\Files\InvalidPathException + * @throws \OC\User\NoUserException + */ + public function getMediaFilePreview(int $documentId, string $mediaFileName, string $userId): ?array { + $textFile = $this->getTextFile($documentId, $userId); + return $this->getMediaFilePreviewFile($mediaFileName, $textFile); + } + /** + * @param int $documentId + * @param string $mediaFileName + * @param string $shareToken + * @return array|null + * @throws NotFoundException + * @throws NotPermittedException + * @throws \OCP\Files\InvalidPathException + * @throws \OC\User\NoUserException + */ + public function getMediaFilePreviewPublic(int $documentId, string $mediaFileName, string $shareToken): ?array { + $textFile = $this->getTextFilePublic($documentId, $shareToken); + return $this->getMediaFilePreviewFile($mediaFileName, $textFile); + } + + /** + * Get media preview or mimetype icon address + * @param string $mediaFileName + * @param File $textFile + * @return array|null + * @throws NotFoundException + * @throws NotPermittedException + * @throws \OCP\Files\InvalidPathException + * @throws \OC\User\NoUserException + */ + private function getMediaFilePreviewFile(string $mediaFileName, File $textFile): ?array { + $attachmentFolder = $this->getAttachmentDirectoryForFile($textFile, true); + $mediaFile = $attachmentFolder->get($mediaFileName); + if ($mediaFile instanceof File) { + if ($this->previewManager->isMimeSupported($mediaFile->getMimeType())) { + try { + return [ + 'type' => 'file', + 'file' => $this->previewManager->getPreview($mediaFile, 1024, 1024), + ]; + } catch (NotFoundException $e) { + // the preview might not be found even if the mimetype is supported + } + } + // fallback: mimetype icon URL + return [ + 'type' => 'icon', + 'iconUrl' => $this->mimeTypeDetector->mimeTypeIcon($mediaFile->getMimeType()), + ]; + } + return null; + } + + /** + * @param int $documentId + * @param string $mediaFileName + * @param string $userId + * @return array|null + * @throws NotFoundException + * @throws NotPermittedException + * @throws \OCP\Files\InvalidPathException + * @throws \OC\User\NoUserException + */ + public function getMediaFileMetadataPrivate(int $documentId, string $mediaFileName, string $userId): ?array { + $textFile = $this->getTextFile($documentId, $userId); + return $this->getMediaFileMetadata($mediaFileName, $textFile); + } + + /** + * @param int $documentId + * @param string $mediaFileName + * @param string $shareToken + * @return array|null + * @throws NotFoundException + * @throws NotPermittedException + * @throws \OCP\Files\InvalidPathException + * @throws \OC\User\NoUserException + */ + public function getMediaFileMetadataPublic(int $documentId, string $mediaFileName, string $shareToken): ?array { + $textFile = $this->getTextFilePublic($documentId, $shareToken); + return $this->getMediaFileMetadata($mediaFileName, $textFile); + } + + /** + * @param string $mediaFileName + * @param File $textFile + * @return array|null + * @throws NotFoundException + * @throws NotPermittedException + * @throws \OCP\Files\InvalidPathException + * @throws \OC\User\NoUserException + */ + private function getMediaFileMetadata(string $mediaFileName, File $textFile): ?array { + $attachmentFolder = $this->getAttachmentDirectoryForFile($textFile, true); + $mediaFile = $attachmentFolder->get($mediaFileName); + if ($mediaFile instanceof File) { + return [ + 'size' => Util::humanFileSize($mediaFile->getSize()), + 'mtime' => $mediaFile->getMTime(), + ]; + } + return null; + } + + /** * Save an uploaded image in the attachment folder * * @param int $documentId @@ -221,21 +385,15 @@ class ImageService { * @throws \OCP\Files\InvalidPathException */ private function copyImageFile(File $imageFile, Folder $saveDir, File $textFile): array { - $mimeType = $imageFile->getMimeType(); - if (in_array($mimeType, ImageController::IMAGE_MIME_TYPES, true)) { - $fileName = $this->getUniqueFileName($saveDir, $imageFile->getName()); - $targetPath = $saveDir->getPath() . '/' . $fileName; - $targetFile = $imageFile->copy($targetPath); - // get file type and name - return [ - 'name' => $fileName, - 'dirname' => $saveDir->getName(), - 'id' => $targetFile->getId(), - 'documentId' => $textFile->getId(), - ]; - } + $fileName = $this->getUniqueFileName($saveDir, $imageFile->getName()); + $targetPath = $saveDir->getPath() . '/' . $fileName; + $targetFile = $imageFile->copy($targetPath); return [ - 'error' => 'Unsupported file type', + 'name' => $fileName, + 'dirname' => $saveDir->getName(), + 'id' => $targetFile->getId(), + 'documentId' => $textFile->getId(), + 'mimetype' => $targetFile->getMimetype(), ]; } @@ -390,49 +548,6 @@ class ImageService { } /** - * Download a file and write it to a resource - * @param string $url - * @param $resource - * @return array - */ - private function simpleDownload(string $url, $resource): array { - $client = $this->clientService->newClient(); - try { - $options = [ - // does not work with sink if SSE is enabled - // 'sink' => $resource, - // rather use stream and write to the file ourselves - 'stream' => true, - 'timeout' => 0, - 'headers' => [ - 'User-Agent' => 'Nextcloud Text', - ], - ]; - - $response = $client->get($url, $options); - $body = $response->getBody(); - while (!feof($body)) { - // write ~5 MB chunks - $chunk = fread($body, 5000000); - fwrite($resource, $chunk); - } - - return ['Content-Type' => $response->getHeader('Content-Type')]; - } catch (ServerException | ClientException $e) { - //$response = $e->getResponse(); - //if ($response->getStatusCode() === 401) { - $this->logger->warning('Impossible to download image', ['exception' => $e]); - return ['error' => 'Impossible to download image']; - } catch (ConnectException $e) { - $this->logger->error('Connection error', ['exception' => $e]); - return ['error' => 'Connection error']; - } catch (Throwable | Exception $e) { - $this->logger->error('Unknown download error', ['exception' => $e]); - return ['error' => 'Unknown download error']; - } - } - - /** * Actually delete attachment files which are not pointed in the markdown content * * @param int $fileId @@ -498,7 +613,7 @@ class ImageService { $matches = []; // matches ![ANY_CONSIDERED_CORRECT_BY_PHP-MARKDOWN](.attachments.DOCUMENT_ID/ANY_FILE_NAME) and captures FILE_NAME preg_match_all( - '/\!\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[\])*\])*\])*\])*\])*\])*\]\(\.attachments\.'.$fileId.'\/([^)&]+)\)/', + '/\!\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[\])*\])*\])*\])*\])*\])*\]\(\.attachments\.' . $fileId . '\/([^)&]+)\)/', $content, $matches, PREG_SET_ORDER diff --git a/src/components/Editor/MediaHandler.vue b/src/components/Editor/MediaHandler.vue index 7004b5b16..1a0f939d9 100644 --- a/src/components/Editor/MediaHandler.vue +++ b/src/components/Editor/MediaHandler.vue @@ -132,16 +132,14 @@ export default { }) }, async uploadImageFile(file, position = null) { - if (!IMAGE_MIMES.includes(file.type)) { - showError(t('text', 'Image file format not supported')) - return - } - this.state.isUploadingImages = true return this.$syncService.uploadImage(file) .then((response) => { - this.insertAttachmentImage(response.data?.name, response.data?.id, position, response.data?.dirname) + this.insertAttachment( + response.data?.name, response.data?.id, file.type, + position, response.data?.dirname + ) }) .catch((error) => { console.error(error) @@ -165,7 +163,10 @@ export default { this.state.isUploadingImages = true return this.$syncService.insertImageFile(imagePath).then((response) => { - this.insertAttachmentImage(response.data?.name, response.data?.id, null, response.data?.dirname) + this.insertAttachment( + response.data?.name, response.data?.id, response.data?.mimetype, + null, response.data?.dirname + ) }).catch((error) => { console.error(error) showError(error?.response?.data?.error || error.message) @@ -173,7 +174,31 @@ export default { this.state.isUploadingImages = false }) }, - insertAttachmentImage(name, fileId, position = null, dirname = '') { + insertAttachment(name, fileId, mimeType, position = null, dirname = '') { + if (IMAGE_MIMES.includes(mimeType)) { + this.insertAttachmentImage(name, fileId, mimeType, position, dirname) + return + } + this.insertAttachmentMedia(name, fileId, mimeType, position, dirname) + }, + insertAttachmentMedia(name, fileId, mimeType, position = null, dirname = '') { + // inspired by the fixedEncodeURIComponent function suggested in + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent + const src = dirname + '/' + + encodeURIComponent(name).replace(/[!'()*]/g, (c) => { + return '%' + c.charCodeAt(0).toString(16).toUpperCase() + }) + // simply get rid of brackets to make sure link text is valid + // as it does not need to be unique and matching the real file name + const alt = name.replaceAll(/[[\]]/g, '') + + const chain = position + ? this.$editor.chain().focus(position) + : this.$editor.chain() + + chain.setImage({ src, alt }).focus().run() + }, + insertAttachmentImage(name, fileId, mimeType, position = null, dirname = '') { // inspired by the fixedEncodeURIComponent function suggested in // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent const src = dirname + '/' diff --git a/src/nodes/ImageView.vue b/src/nodes/ImageView.vue index eb812e6d0..8519ebd58 100644 --- a/src/nodes/ImageView.vue +++ b/src/nodes/ImageView.vue @@ -34,10 +34,25 @@ @mouseleave="showIcons = false"> <transition name="fade"> <template v-if="!failed"> - <img v-show="loaded" - :src="imageUrl" - class="image__main" - @load="onLoaded"> + <div v-if="isMediaAttachment" + class="media"> + <img v-show="loaded" + :src="imageUrl" + class="image__main" + @load="onLoaded"> + <span class="name"> + {{ alt }} + </span> + <span class="size"> + {{ attachmentMetadata.size }} + </span> + </div> + <div v-else> + <img v-show="loaded" + :src="imageUrl" + class="image__main" + @load="onLoaded"> + </div> </template> <template v-else> <ImageIcon class="image__main image__main--broken-icon" :size="100" /> @@ -45,7 +60,8 @@ </transition> <transition name="fade"> <div v-show="loaded" class="image__caption"> - <input ref="altInput" + <input v-show="!isMediaAttachment" + ref="altInput" type="text" class="image__caption__input" :value="alt" @@ -143,9 +159,14 @@ export default { showIcons: false, imageUrl: null, errorMessage: null, + attachmentType: null, + attachmentMetadata: {}, } }, computed: { + isMediaAttachment() { + return this.attachmentType !== 'image' + }, canDisplayImage() { if (!this.isSupportedImage) { return false @@ -212,24 +233,31 @@ export default { }, methods: { async init() { - const [url, fallback] = this.$imageResolver.resolve(this.src) - return this.loadImage(url).catch((e) => { - if (fallback) { - return this.loadImage(fallback) + const candidates = this.$imageResolver.resolve(this.src) + return this.load(candidates) + }, + async load(candidates) { + const candidate = candidates.shift() + return this.loadImage(candidate.url, candidate.type, candidate.name).catch((e) => { + if (candidates.length > 0) { + return this.load(candidates) // TODO if fallback works, rewrite the url with correct document ID } - return Promise.reject(e) }) }, - - async loadImage(imageUrl) { + async loadImage(imageUrl, attachmentType, name = null) { return new Promise((resolve, reject) => { const img = new Image() img.onload = () => { this.imageUrl = imageUrl this.imageLoaded = true this.loaded = true + this.attachmentType = attachmentType + console.debug('SUCCESS type', attachmentType) + if (attachmentType === 'media') { + this.loadMediaMetadata(name) + } resolve(imageUrl) } img.onerror = (e) => { @@ -238,6 +266,12 @@ export default { img.src = imageUrl }) }, + loadMediaMetadata(name) { + this.$imageResolver.getMediaMetadata(name).then((response) => { + console.debug('GOTCHAAAAAA', response.data) + this.attachmentMetadata = response.data + }) + }, onImageLoadFailure(err) { this.failed = true this.imageLoaded = false @@ -306,6 +340,20 @@ export default { max-height: calc(100vh - 50px - 50px); } + .media { + display: flex; + align-items: center; + img { + width: 32px; + height: 32px; + } + .name { + flex-grow: 1; + text-align: left; + margin-left: 8px; + } + } + .image__error-message { display: block; text-align: center; diff --git a/src/services/ImageResolver.js b/src/services/ImageResolver.js index fdad0b062..b3db6e3e0 100644 --- a/src/services/ImageResolver.js +++ b/src/services/ImageResolver.js @@ -21,6 +21,7 @@ */ import { generateUrl, generateRemoteUrl } from '@nextcloud/router' +import axios from '@nextcloud/axios' import pathNormalize from 'path-normalize' export default class ImageResolver { @@ -50,35 +51,70 @@ export default class ImageResolver { resolve(src) { if (this.#session && src.startsWith('text://')) { const imageFileName = getQueryVariable(src, 'imageFileName') - return [this.#getAttachmentUrl(imageFileName)] + return [{ + type: 'image', + url: this.#getImageAttachmentUrl(imageFileName), + }] } if (this.#session && src.startsWith(`.attachments.${this.#session?.documentId}/`)) { const imageFileName = decodeURIComponent(src.replace(`.attachments.${this.#session?.documentId}/`, '').split('?')[0]) - return [this.#getAttachmentUrl(imageFileName)] + return [ + { + type: 'image', + url: this.#getImageAttachmentUrl(imageFileName), + }, + { + type: 'media', + url: this.#getMediaPreviewUrl(imageFileName), + name: imageFileName, + }, + ] } if (isDirectUrl(src)) { - return [src] + return [{ + type: 'image', + url: src, + }] } if (hasPreview(src)) { // && this.#mime !== 'image/gif') { - return [this.#previewUrl(src)] + return [{ + type: 'image', + url: this.#previewUrl(src), + }] } // if it starts with '.attachments.1234/' if (src.match(/^\.attachments\.\d+\//)) { const imageFileName = this.#relativePath(src) .replace(/\.attachments\.\d+\//, '') - const attachmentUrl = this.#getAttachmentUrl(imageFileName) - // try the webdav url and attachment API if the fails - return [this.#davUrl(src), attachmentUrl] + // try the webdav url and attachment API if it fails + return [ + { + type: 'image', + url: this.#davUrl(src), + }, + { + type: 'image', + url: this.#getImageAttachmentUrl(imageFileName), + }, + { + type: 'media', + url: this.#getMediaPreviewUrl(imageFileName), + name: imageFileName, + }, + ] } - return [this.#davUrl(src)] + return [{ + type: 'image', + url: this.#davUrl(src), + }] } - #getAttachmentUrl(imageFileName) { + #getImageAttachmentUrl(imageFileName) { if (!this.#session) { return this.#davUrl( `${this.#attachmentDirectory}/${imageFileName}` @@ -99,6 +135,40 @@ export default class ImageResolver { }) } + #getMediaPreviewUrl(mediaFileName) { + if (this.#user || !this.#shareToken) { + return generateUrl('/apps/text/mediaPreview?documentId={documentId}&sessionId={sessionId}&sessionToken={sessionToken}&mediaFileName={mediaFileName}', { + ...this.#textApiParams(), + mediaFileName, + }) + } + + return generateUrl('/apps/text/mediaPreview?documentId={documentId}&sessionId={sessionId}&sessionToken={sessionToken}&mediaFileName={mediaFileName}&shareToken={shareToken}', { + ...this.#textApiParams(), + mediaFileName, + shareToken: this.#shareToken, + }) + } + + #getMediaMetadataUrl(mediaFileName) { + if (this.#user || !this.#shareToken) { + return generateUrl('/apps/text/mediaMetadata?documentId={documentId}&sessionId={sessionId}&sessionToken={sessionToken}&mediaFileName={mediaFileName}', { + ...this.#textApiParams(), + mediaFileName, + }) + } + + return generateUrl('/apps/text/mediaMetadata?documentId={documentId}&sessionId={sessionId}&sessionToken={sessionToken}&mediaFileName={mediaFileName}&shareToken={shareToken}', { + ...this.#textApiParams(), + mediaFileName, + shareToken: this.#shareToken, + }) + } + + getMediaMetadata(mediaFileName) { + return axios.get(this.#getMediaMetadataUrl(mediaFileName)) + } + #textApiParams() { if (this.#session) { return { diff --git a/src/services/SyncService.js b/src/services/SyncService.js index 24c50c844..fdd692f77 100644 --- a/src/services/SyncService.js +++ b/src/services/SyncService.js @@ -284,18 +284,6 @@ class SyncService { }) } - insertImageLink(imageLink) { - const params = { - documentId: this.document.id, - sessionId: this.session.id, - sessionToken: this.session.token, - shareToken: this.options.shareToken || '', - link: imageLink, - } - const url = endpointUrl('image/link') - return axios.post(url, params) - } - insertImageFile(imagePath) { const params = { documentId: this.document.id, |