-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Use core preview controller for loading file previews and fallba…
…ck to MDI icons Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
- Loading branch information
Showing
7 changed files
with
272 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<template> | ||
<div :style="canLoadPreview ? { backgroundImage: `url(${previewURL})`} : undefined" | ||
:aria-label="t('Mime type {mime}', { mime: node.mime || t('unknown') })" | ||
class="file-picker__file-icon"> | ||
<template v-if="!canLoadPreview"> | ||
<IconFile v-if="isFile" :size="20" /> | ||
<IconFolder v-else :size="20" /> | ||
</template> | ||
</div> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { FileType, type Node } from '@nextcloud/files' | ||
import { usePreviewURL } from '../../usables/preview' | ||
import { computed, ref, toRef, watch } from 'vue' | ||
import { t } from '../../utils/l10n' | ||
import IconFile from 'vue-material-design-icons/File.vue' | ||
import IconFolder from 'vue-material-design-icons/Folder.vue' | ||
const props = defineProps<{ | ||
node: Node | ||
}>() | ||
const { previewURL } = usePreviewURL(toRef(props, 'node')) | ||
const isFile = computed(() => props.node.type === FileType.File) | ||
const canLoadPreview = ref(false) | ||
watch(previewURL, () => { | ||
canLoadPreview.value = false | ||
if (previewURL.value) { | ||
const loader = document.createElement('img') | ||
loader.src = previewURL.value.href | ||
loader.onload = () => { canLoadPreview.value = true } | ||
loader.onerror = () => document.body.removeChild(loader) | ||
document.body.appendChild(loader) | ||
} | ||
}, { immediate: true }) | ||
</script> | ||
|
||
<style scoped lang="scss"> | ||
.file-picker__file-icon { | ||
width: 32px; | ||
height: 32px; | ||
min-width: 32px; | ||
min-height: 32px; | ||
background-repeat: no-repeat; | ||
background-size: contain; | ||
// for the fallback | ||
display: flex; | ||
justify-content: center; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
/** | ||
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de> | ||
* | ||
* @author Ferdinand Thiessen <opensource@fthiessen.de> | ||
* | ||
* @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 <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
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') | ||
}) | ||
|
||
it('returns null on invalid URLs', () => { | ||
const previewNode = new File({ | ||
...createData('text.txt', 'text/plain'), | ||
attributes: { | ||
previewUrl: 'ht//tp://////invalid\\.com', | ||
} | ||
}) | ||
|
||
expect(getPreviewURL(previewNode)).toBe(null) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/** | ||
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de> | ||
* | ||
* @author Ferdinand Thiessen <opensource@fthiessen.de> | ||
* | ||
* @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 <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
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<Node>, options?: PreviewOptions | Ref<PreviewOptions>) => { | ||
const previewURL = ref<URL|null>(null) | ||
|
||
watchEffect(() => { | ||
previewURL.value = getPreviewURL(toValue(node), toValue(options || {})) | ||
}) | ||
|
||
return { | ||
previewURL, | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters