diff --git a/lib/components/FilePicker/FileListRow.vue b/lib/components/FilePicker/FileListRow.vue
index 461b3359d..4f61016b5 100644
--- a/lib/components/FilePicker/FileListRow.vue
+++ b/lib/components/FilePicker/FileListRow.vue
@@ -21,7 +21,7 @@
@@ -35,11 +35,15 @@
+
+
diff --git a/lib/usables/preview.spec.ts b/lib/usables/preview.spec.ts
new file mode 100644
index 000000000..a8a110238
--- /dev/null
+++ b/lib/usables/preview.spec.ts
@@ -0,0 +1,103 @@
+/**
+ * @copyright Copyright (c) 2023 Ferdinand Thiessen
+ *
+ * @author Ferdinand Thiessen
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 .
+ */
+
+import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
+import { getPreviewURL, usePreviewURL } from './preview'
+import { File } from '@nextcloud/files'
+import { defineComponent, h, toRef } from 'vue'
+import { shallowMount } from '@vue/test-utils'
+
+describe('preview composable', () => {
+ const createData = (path: string, mime: string) => ({
+ owner: null,
+ source: `http://example.com/dav/${path}`,
+ mime,
+ mtime: new Date(),
+ root: '/',
+ })
+
+ describe('previewURL', () => {
+ beforeAll(() => {
+ vi.useFakeTimers()
+ })
+ afterAll(() => {
+ vi.useRealTimers()
+ })
+
+ it('is reactive', async () => {
+ const text = new File({
+ ...createData('text.txt', 'text/plain'),
+ id: 1,
+ })
+ const image = new File({
+ ...createData('image.png', 'image/png'),
+ id: 2,
+ })
+
+ const wrapper = shallowMount(defineComponent({
+ props: ['node'],
+ setup(props) {
+ const { previewURL } = usePreviewURL(toRef(props, 'node'))
+ return () => h('div', previewURL.value?.href)
+ },
+ }), {
+ propsData: { node: text },
+ })
+
+ expect(wrapper.text()).toMatch('/core/preview?fileId=1')
+ await wrapper.setProps({ node: image })
+ expect(wrapper.text()).toMatch('/core/preview?fileId=2')
+ })
+
+ it('uses Nodes previewUrl if available', () => {
+ const previewNode = new File({
+ ...createData('text.txt', 'text/plain'),
+ attributes: {
+ previewUrl: '/preview.svg',
+ },
+ })
+ const { previewURL } = usePreviewURL(previewNode)
+
+ expect(previewURL.value?.pathname).toBe('/preview.svg')
+ })
+
+ it('works with full URL previewUrl', () => {
+ const previewNode = new File({
+ ...createData('text.txt', 'text/plain'),
+ attributes: {
+ previewUrl: 'http://example.com/preview.svg',
+ },
+ })
+ const { previewURL } = usePreviewURL(previewNode)
+
+ expect(previewURL.value?.href.startsWith('http://example.com/preview.svg?')).toBe(true)
+ })
+
+ it('supports options', () => {
+ const previewNode = new File(createData('text.txt', 'text/plain'))
+
+ expect(getPreviewURL(previewNode, { size: 16 })?.searchParams.get('x')).toBe('16')
+ expect(getPreviewURL(previewNode, { size: 16 })?.searchParams.get('y')).toBe('16')
+
+ expect(getPreviewURL(previewNode, { mimeFallback: false })?.searchParams.get('mimeFallback')).toBe('false')
+ })
+ })
+})
diff --git a/lib/usables/preview.ts b/lib/usables/preview.ts
new file mode 100644
index 000000000..90c69cb61
--- /dev/null
+++ b/lib/usables/preview.ts
@@ -0,0 +1,92 @@
+/**
+ * @copyright Copyright (c) 2023 Ferdinand Thiessen
+ *
+ * @author Ferdinand Thiessen
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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 .
+ */
+
+import type { Node } from '@nextcloud/files'
+import type { Ref } from 'vue'
+
+import { generateUrl } from '@nextcloud/router'
+import { toValue } from '@vueuse/core'
+import { ref, watchEffect } from 'vue'
+
+interface PreviewOptions {
+ /**
+ * Size of the previews in px
+ * @default 32
+ */
+ size?: number
+ /**
+ * Should the preview fall back to the mime type icon
+ * @default true
+ */
+ mimeFallback?: boolean
+ /**
+ * Should the preview be cropped or fitted
+ * @default false (meaning it gets fitted)
+ */
+ cropPreview?: boolean
+}
+
+/**
+ * Generate the preview URL of a file node
+ *
+ * @param node The node to generate the preview for
+ * @param options Preview options
+ */
+export function getPreviewURL(node: Node, options: PreviewOptions = {}) {
+ options = { size: 32, cropPreview: false, mimeFallback: true, ...options }
+
+ try {
+ const previewUrl = node.attributes?.previewUrl
+ || generateUrl('/core/preview?fileId={fileid}', {
+ fileid: node.fileid,
+ })
+
+ let url
+ try {
+ url = new URL(previewUrl)
+ } catch (e) {
+ url = new URL(previewUrl, window.location.origin)
+ }
+
+ // Request preview with params
+ url.searchParams.set('x', `${options.size}`)
+ url.searchParams.set('y', `${options.size}`)
+ url.searchParams.set('mimeFallback', `${options.mimeFallback}`)
+
+ // Handle cropping
+ url.searchParams.set('a', options.cropPreview === true ? '0' : '1')
+ return url
+ } catch (e) {
+ return null
+ }
+}
+
+export const usePreviewURL = (node: Node | Ref, options?: PreviewOptions | Ref) => {
+ const previewURL = ref(null)
+
+ watchEffect(() => {
+ previewURL.value = getPreviewURL(toValue(node), toValue(options || {}))
+ })
+
+ return {
+ previewURL,
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 572f3c50f..3df5368cb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,19 +10,19 @@
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
- "@mdi/svg": "^7.2.96",
"@nextcloud/files": "^3.0.0-beta.19",
"@nextcloud/l10n": "^2.2.0",
+ "@nextcloud/router": "^2.1.2",
"@nextcloud/typings": "^1.7.0",
"@nextcloud/vue": "^8.0.0-beta.4",
"@types/toastify-js": "^1.12.0",
"@vueuse/core": "^10.4.0",
"toastify-js": "^1.12.0",
"vue-frag": "^1.4.3",
- "vue-material-design-icons": "^5.2.0",
"webdav": "^5.2.3"
},
"devDependencies": {
+ "@mdi/svg": "^7.2.96",
"@nextcloud/browserslist-config": "^3.0.0",
"@nextcloud/eslint-config": "^8.3.0-beta.2",
"@nextcloud/vite-config": "^1.0.0-beta.18",
@@ -40,7 +40,8 @@
"typedoc": "^0.25.0",
"typescript": "^5.1.6",
"vite": "^4.4.9",
- "vitest": "^0.34.3"
+ "vitest": "^0.34.3",
+ "vue-material-design-icons": "^5.2.0"
},
"engines": {
"node": "^20.0.0",
@@ -2714,7 +2715,8 @@
"node_modules/@mdi/svg": {
"version": "7.2.96",
"resolved": "https://registry.npmjs.org/@mdi/svg/-/svg-7.2.96.tgz",
- "integrity": "sha512-rxzuSL2RSt/pWWnFnUFQi5GJArm2tHMhx20Gee3Ydn+xT2bqbR4syfgdPrq2b+j+n5LjC7C8Fb1QDM6LKeF0cA=="
+ "integrity": "sha512-rxzuSL2RSt/pWWnFnUFQi5GJArm2tHMhx20Gee3Ydn+xT2bqbR4syfgdPrq2b+j+n5LjC7C8Fb1QDM6LKeF0cA==",
+ "dev": true
},
"node_modules/@microsoft/api-extractor": {
"version": "7.36.4",
diff --git a/package.json b/package.json
index c2a0d9e51..a5a71d79b 100644
--- a/package.json
+++ b/package.json
@@ -56,19 +56,19 @@
"vue": "^2.7.14"
},
"dependencies": {
- "@mdi/svg": "^7.2.96",
"@nextcloud/files": "^3.0.0-beta.19",
"@nextcloud/l10n": "^2.2.0",
+ "@nextcloud/router": "^2.1.2",
"@nextcloud/typings": "^1.7.0",
"@nextcloud/vue": "^8.0.0-beta.4",
"@types/toastify-js": "^1.12.0",
"@vueuse/core": "^10.4.0",
"toastify-js": "^1.12.0",
"vue-frag": "^1.4.3",
- "vue-material-design-icons": "^5.2.0",
"webdav": "^5.2.3"
},
"devDependencies": {
+ "@mdi/svg": "^7.2.96",
"@nextcloud/browserslist-config": "^3.0.0",
"@nextcloud/eslint-config": "^8.3.0-beta.2",
"@nextcloud/vite-config": "^1.0.0-beta.18",
@@ -86,7 +86,8 @@
"typedoc": "^0.25.0",
"typescript": "^5.1.6",
"vite": "^4.4.9",
- "vitest": "^0.34.3"
+ "vitest": "^0.34.3",
+ "vue-material-design-icons": "^5.2.0"
},
"engines": {
"node": "^20.0.0",
diff --git a/vite.config.ts b/vite.config.ts
index 63476549d..71a6cdc03 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -29,7 +29,10 @@ export default defineConfig((env) => {
},
},
nodeExternalsOptions: {
+ // for subpath imports like '@nextcloud/l10n/gettext'
include: [/^@nextcloud\//],
+ // we should externalize vue SFC dependencies
+ exclude: [/^vue-material-design-icons\//, /\.vue(\?|$)/],
},
libraryFormats: ['es', 'cjs'],
replace: {
|