Browse Source

Implement file reference wiget

Signed-off-by: Julius Härtl <jus@bitgrid.net>
Julius Härtl 1 year ago
parent
commit
6130f1a78e

+ 1 - 0
apps/files/composer/composer/autoload_classmap.php

@@ -50,6 +50,7 @@ return array(
     'OCA\\Files\\Helper' => $baseDir . '/../lib/Helper.php',
     'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => $baseDir . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php',
     'OCA\\Files\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
+    'OCA\\Files\\Listener\\RenderReferenceEventListener' => $baseDir . '/../lib/Listener/RenderReferenceEventListener.php',
     'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php',
     'OCA\\Files\\Migration\\Version12101Date20221011153334' => $baseDir . '/../lib/Migration/Version12101Date20221011153334.php',
     'OCA\\Files\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',

+ 1 - 0
apps/files/composer/composer/autoload_static.php

@@ -65,6 +65,7 @@ class ComposerStaticInitFiles
         'OCA\\Files\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php',
         'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => __DIR__ . '/..' . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php',
         'OCA\\Files\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
+        'OCA\\Files\\Listener\\RenderReferenceEventListener' => __DIR__ . '/..' . '/../lib/Listener/RenderReferenceEventListener.php',
         'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php',
         'OCA\\Files\\Migration\\Version12101Date20221011153334' => __DIR__ . '/..' . '/../lib/Migration/Version12101Date20221011153334.php',
         'OCA\\Files\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',

+ 3 - 0
apps/files/lib/AppInfo/Application.php

@@ -44,6 +44,7 @@ use OCA\Files\Event\LoadAdditionalScriptsEvent;
 use OCA\Files\Event\LoadSidebar;
 use OCA\Files\Listener\LegacyLoadAdditionalScriptsAdapter;
 use OCA\Files\Listener\LoadSidebarListener;
+use OCA\Files\Listener\RenderReferenceEventListener;
 use OCA\Files\Notification\Notifier;
 use OCA\Files\Search\FilesSearchProvider;
 use OCA\Files\Service\TagService;
@@ -53,6 +54,7 @@ use OCP\AppFramework\App;
 use OCP\AppFramework\Bootstrap\IBootContext;
 use OCP\AppFramework\Bootstrap\IBootstrap;
 use OCP\AppFramework\Bootstrap\IRegistrationContext;
+use OCP\Collaboration\Reference\RenderReferenceEvent;
 use OCP\Collaboration\Resources\IProviderManager;
 use OCP\IConfig;
 use OCP\IL10N;
@@ -118,6 +120,7 @@ class Application extends App implements IBootstrap {
 
 		$context->registerEventListener(LoadAdditionalScriptsEvent::class, LegacyLoadAdditionalScriptsAdapter::class);
 		$context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class);
+		$context->registerEventListener(RenderReferenceEvent::class, RenderReferenceEventListener::class);
 
 		$context->registerSearchProvider(FilesSearchProvider::class);
 

+ 39 - 0
apps/files/lib/Listener/RenderReferenceEventListener.php

@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Files\Listener;
+
+use OCP\Collaboration\Reference\RenderReferenceEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+
+class RenderReferenceEventListener implements IEventListener {
+	public function handle(Event $event): void {
+		if (!$event instanceof RenderReferenceEvent) {
+			return;
+		}
+
+		\OCP\Util::addScript('files', 'reference-files');
+	}
+}

+ 58 - 0
apps/files/src/reference-files.js

@@ -0,0 +1,58 @@
+/**
+ * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import Vue from 'vue'
+import { translate as t } from '@nextcloud/l10n'
+
+import { registerWidget, registerCustomPickerElement, NcCustomPickerRenderResult } from '@nextcloud/vue/dist/Components/NcRichText.js'
+
+import FileWidget from './views/ReferenceFileWidget.vue'
+import FileReferencePickerElement from './views/FileReferencePickerElement.vue'
+
+Vue.mixin({
+	methods: {
+		t,
+	},
+})
+
+registerWidget('file', (el, { richObjectType, richObject, accessible }) => {
+	const Widget = Vue.extend(FileWidget)
+	new Widget({
+		propsData: {
+			richObjectType,
+			richObject,
+			accessible,
+		},
+	}).$mount(el)
+})
+
+registerCustomPickerElement('files', (el, { providerId, accessible }) => {
+	const Element = Vue.extend(FileReferencePickerElement)
+	const vueElement = new Element({
+		propsData: {
+			providerId,
+			accessible,
+		},
+	}).$mount(el)
+	return new NcCustomPickerRenderResult(vueElement.$el, vueElement)
+}, (el, renderResult) => {
+	renderResult.object.$destroy()
+})

+ 113 - 0
apps/files/src/views/FileReferencePickerElement.vue

@@ -0,0 +1,113 @@
+<!--
+  - @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
+  -
+  - @author Julius Härtl <jus@bitgrid.net>
+  -
+  - @license GNU AGPL version 3 or any later version
+  -
+  - This program is free software: you can redistribute it and/or modify
+  - it under the terms of the GNU Affero General Public License as
+  - published by the Free Software Foundation, either version 3 of the
+  - License, or (at your option) any later version.
+  -
+  - This program is distributed in the hope that it will be useful,
+  - but WITHOUT ANY WARRANTY; without even the implied warranty of
+  - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  - GNU Affero General Public License for more details.
+  -
+  - You should have received a copy of the GNU Affero General Public License
+  - along with this program. If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<template>
+	<div ref="picker" class="reference-file-picker" />
+</template>
+
+<script>
+import { FilePickerType } from '@nextcloud/dialogs'
+import { generateUrl } from '@nextcloud/router'
+export default {
+	name: 'FileReferencePickerElement',
+	components: {
+	},
+	props: {
+		providerId: {
+			type: String,
+			required: true,
+		},
+		accessible: {
+			type: Boolean,
+			default: false,
+		},
+	},
+	mounted() {
+		this.openFilePicker()
+		window.addEventListener('click', this.onWindowClick)
+	},
+	beforeDestroy() {
+		window.removeEventListener('click', this.onWindowClick)
+	},
+	methods: {
+		onWindowClick(e) {
+			if (e.target.tagName === 'A' && e.target.classList.contains('oc-dialog-close')) {
+				this.$emit('cancel')
+			}
+		},
+		async openFilePicker() {
+			OC.dialogs.filepicker(
+				t('files', 'Select file or folder to link to'),
+				(file) => {
+					const client = OC.Files.getClient()
+					client.getFileInfo(file).then((_status, fileInfo) => {
+						this.submit(fileInfo.id)
+					})
+				},
+				false, // multiselect
+				[], // mime filter
+				false, // modal
+				FilePickerType.Choose, // type
+				'',
+				{
+					target: this.$refs.picker,
+				},
+			)
+		},
+		submit(fileId) {
+			const fileLink = window.location.protocol + '//' + window.location.host
+				+ generateUrl('/f/{fileId}', { fileId })
+			this.$emit('submit', fileLink)
+		},
+	},
+}
+</script>
+
+<style scoped lang="scss">
+.reference-file-picker {
+	flex-grow: 1;
+	margin-top: 44px;
+
+	&:deep(.oc-dialog) {
+		transform: none !important;
+		box-shadow: none !important;
+		flex-grow: 1 !important;
+		position: static !important;
+		width: 100% !important;
+		height: auto !important;
+		padding: 0 !important;
+		max-width: initial;
+
+		.oc-dialog-close {
+			display: none;
+		}
+
+		.oc-dialog-buttonrow.onebutton.aside {
+			position: absolute;
+			padding: 12px 32px;
+		}
+
+		.oc-dialog-content {
+			max-width: 100% !important;
+		}
+	}
+}
+</style>

+ 182 - 0
apps/files/src/views/ReferenceFileWidget.vue

@@ -0,0 +1,182 @@
+<!--
+  - @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
+  -
+  - @author Julius Härtl <jus@bitgrid.net>
+  -
+  - @license GNU AGPL version 3 or any later version
+  -
+  - This program is free software: you can redistribute it and/or modify
+  - it under the terms of the GNU Affero General Public License as
+  - published by the Free Software Foundation, either version 3 of the
+  - License, or (at your option) any later version.
+  -
+  - This program is distributed in the hope that it will be useful,
+  - but WITHOUT ANY WARRANTY; without even the implied warranty of
+  - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  - GNU Affero General Public License for more details.
+  -
+  - You should have received a copy of the GNU Affero General Public License
+  - along with this program. If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<template>
+	<div v-if="!accessible" class="widget-file widget-file--no-access">
+		<div class="widget-file--image widget-file--image--icon icon-folder" />
+		<div class="widget-file--details">
+			<p class="widget-file--title">
+				{{ t('files', 'File cannot be accessed') }}
+			</p>
+			<p class="widget-file--description">
+				{{ t('files', 'You might not have have permissions to view it, ask the sender to share it') }}
+			</p>
+		</div>
+	</div>
+	<a v-else
+		class="widget-file"
+		:href="richObject.link"
+		@click.prevent="navigate">
+		<div class="widget-file--image" :class="filePreviewClass" :style="filePreview" />
+		<div class="widget-file--details">
+			<p class="widget-file--title">{{ richObject.name }}</p>
+			<p class="widget-file--description">{{ fileSize }}<br>{{ fileMtime }}</p>
+			<p class="widget-file--link">{{ filePath }}</p>
+		</div>
+	</a>
+</template>
+<script>
+import { generateUrl } from '@nextcloud/router'
+import path from 'path'
+
+export default {
+	name: 'ReferenceFileWidget',
+	props: {
+		richObject: {
+			type: Object,
+			required: true,
+		},
+		accessible: {
+			type: Boolean,
+			default: true,
+		},
+	},
+	data() {
+		return {
+			previewUrl: window.OC.MimeType.getIconUrl(this.richObject.mimetype),
+		}
+	},
+	computed: {
+		fileSize() {
+			return window.OC.Util.humanFileSize(this.richObject.size)
+		},
+		fileMtime() {
+			return window.OC.Util.relativeModifiedDate(this.richObject.mtime * 1000)
+		},
+		filePath() {
+			return path.dirname(this.richObject.path)
+		},
+		filePreview() {
+			if (this.previewUrl) {
+				return {
+					backgroundImage: 'url(' + this.previewUrl + ')',
+				}
+			}
+
+			return {
+				backgroundImage: 'url(' + window.OC.MimeType.getIconUrl(this.richObject.mimetype) + ')',
+			}
+
+		},
+		filePreviewClass() {
+			if (this.previewUrl) {
+				return 'widget-file--image--preview'
+			}
+			return 'widget-file--image--icon'
+
+		},
+	},
+	mounted() {
+		if (this.richObject['preview-available']) {
+			const previewUrl = generateUrl('/core/preview?fileId={fileId}&x=250&y=250', {
+				fileId: this.richObject.id,
+			})
+			const img = new Image()
+			img.onload = () => {
+				this.previewUrl = previewUrl
+			}
+			img.onerror = err => {
+				console.error('could not load recommendation preview', err)
+			}
+			img.src = previewUrl
+		}
+	},
+	methods: {
+		navigate() {
+			if (OCA.Viewer && OCA.Viewer.mimetypes.indexOf(this.richObject.mimetype) !== -1) {
+				OCA.Viewer.open({ path: this.richObject.path })
+				return
+			}
+			window.location = generateUrl('/f/' + this.id)
+		},
+	},
+}
+</script>
+<style lang="scss" scoped>
+.widget-file {
+	display: flex;
+	flex-grow: 1;
+	color: var(--color-main-text) !important;
+	text-decoration: none !important;
+
+	&--image {
+		min-width: 40%;
+		background-position: center;
+		background-size: cover;
+		background-repeat: no-repeat;
+
+		&.widget-file--image--icon {
+			min-width: 88px;
+			background-size: 44px;
+		}
+	}
+
+	&--title {
+		overflow: hidden;
+		text-overflow: ellipsis;
+		white-space: nowrap;
+		font-weight: bold;
+	}
+
+	&--details {
+		padding: 12px;
+		flex-grow: 1;
+		display: flex;
+		flex-direction: column;
+
+		p {
+			margin: 0;
+			padding: 0;
+		}
+	}
+
+	&--description {
+		overflow: hidden;
+		text-overflow: ellipsis;
+		display: -webkit-box;
+		-webkit-line-clamp: 3;
+		line-clamp: 3;
+		-webkit-box-orient: vertical;
+	}
+
+	&--link {
+		color: var(--color-text-maxcontrast);
+	}
+
+	&.widget-file--no-access {
+		padding: 12px;
+
+		.widget-file--details {
+			padding: 0;
+		}
+	}
+}
+</style>

+ 33 - 9
lib/private/Collaboration/Reference/File/FileReferenceProvider.php

@@ -25,8 +25,8 @@ declare(strict_types=1);
 namespace OC\Collaboration\Reference\File;
 
 use OC\User\NoUserException;
+use OCP\Collaboration\Reference\ADiscoverableReferenceProvider;
 use OCP\Collaboration\Reference\IReference;
-use OCP\Collaboration\Reference\IReferenceProvider;
 use OCP\Collaboration\Reference\Reference;
 use OCP\Files\IMimeTypeDetector;
 use OCP\Files\InvalidPathException;
@@ -34,27 +34,34 @@ use OCP\Files\IRootFolder;
 use OCP\Files\Node;
 use OCP\Files\NotFoundException;
 use OCP\Files\NotPermittedException;
+use OCP\IL10N;
 use OCP\IPreview;
 use OCP\IURLGenerator;
 use OCP\IUserSession;
+use OCP\L10N\IFactory;
 
-class FileReferenceProvider implements IReferenceProvider {
+class FileReferenceProvider extends ADiscoverableReferenceProvider {
 	private IURLGenerator $urlGenerator;
 	private IRootFolder $rootFolder;
 	private ?string $userId;
 	private IPreview $previewManager;
 	private IMimeTypeDetector $mimeTypeDetector;
-
-	public function __construct(IURLGenerator $urlGenerator,
-								IRootFolder $rootFolder,
-								IUserSession $userSession,
-								IMimeTypeDetector $mimeTypeDetector,
-								IPreview $previewManager) {
+	private IL10N $l10n;
+
+	public function __construct(
+		IURLGenerator $urlGenerator,
+		IRootFolder $rootFolder,
+		IUserSession $userSession,
+		IMimeTypeDetector $mimeTypeDetector,
+		IPreview $previewManager,
+		IFactory $l10n
+	) {
 		$this->urlGenerator = $urlGenerator;
 		$this->rootFolder = $rootFolder;
 		$this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null;
 		$this->previewManager = $previewManager;
 		$this->mimeTypeDetector = $mimeTypeDetector;
+		$this->l10n = $l10n->get('files');
 	}
 
 	public function matchReference(string $referenceText): bool {
@@ -145,9 +152,10 @@ class FileReferenceProvider implements IReferenceProvider {
 				'id' => $file->getId(),
 				'name' => $file->getName(),
 				'size' => $file->getSize(),
-				'path' => $file->getPath(),
+				'path' => $userFolder->getRelativePath($file->getPath()),
 				'link' => $reference->getUrl(),
 				'mimetype' => $file->getMimetype(),
+				'mtime' => $file->getMTime(),
 				'preview-available' => $this->previewManager->isAvailable($file)
 			]);
 		} catch (InvalidPathException|NotFoundException|NotPermittedException|NoUserException $e) {
@@ -162,4 +170,20 @@ class FileReferenceProvider implements IReferenceProvider {
 	public function getCacheKey(string $referenceId): ?string {
 		return $this->userId ?? '';
 	}
+
+	public function getId(): string {
+		return 'files';
+	}
+
+	public function getTitle(): string {
+		return $this->l10n->t('Files');
+	}
+
+	public function getOrder(): int {
+		return 0;
+	}
+
+	public function getIconUrl(): string {
+		return $this->urlGenerator->imagePath('files', 'folder.svg');
+	}
 }

+ 6 - 0
lib/public/RichObjectStrings/Definitions.php

@@ -347,6 +347,12 @@ class Definitions {
 					'description' => 'Whether or not a preview is available. If `no` the mimetype icon should be used',
 					'example' => 'yes',
 				],
+				'mtime' => [
+					'since' => '25.0.0',
+					'required' => false,
+					'description' => 'The mtime of the file/folder as unix timestamp',
+					'example' => '1661854213',
+				],
 			],
 		],
 		'forms-form' => [

+ 1 - 0
webpack.modules.js

@@ -52,6 +52,7 @@ module.exports = {
 		sidebar: path.join(__dirname, 'apps/files/src', 'sidebar.js'),
 		main: path.join(__dirname, 'apps/files/src', 'main.js'),
 		'personal-settings': path.join(__dirname, 'apps/files/src', 'main-personal-settings.js'),
+		'reference-files': path.join(__dirname, 'apps/files/src', 'reference-files.js'),
 	},
 	files_sharing: {
 		additionalScripts: path.join(__dirname, 'apps/files_sharing/src', 'additionalScripts.js'),