Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 9 additions & 17 deletions lib/components/FilePicker/FilePreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div :style="canLoadPreview ? { backgroundImage: `url(${previewURL})`} : undefined"
<div :style="previewLoaded ? { backgroundImage: `url(${previewURL})`} : undefined"
:class="fileListIconStyles['file-picker__file-icon']">
<template v-if="!canLoadPreview">
<template v-if="!previewLoaded">
<IconFile v-if="isFile" :size="20" />
<IconFolder v-else :size="20" />
</template>
Expand All @@ -14,14 +14,15 @@

<script setup lang="ts">
import { FileType, type Node } from '@nextcloud/files'
import { computed, ref, watchEffect } from 'vue'
import { getPreviewURL } from '../../composables/preview'
import { computed, ref, toRef } from 'vue'
import { usePreviewURL } from '../../composables/preview'

import IconFile from 'vue-material-design-icons/File.vue'
import IconFolder from 'vue-material-design-icons/Folder.vue'

// CSS modules
import fileListIconStylesModule from './FileListIcon.module.scss'

// workaround for vue2.7 bug, can be removed with vue3
const fileListIconStyles = ref(fileListIconStylesModule)

Expand All @@ -30,21 +31,12 @@ const props = defineProps<{
cropImagePreviews: boolean
}>()

const previewURL = computed(() => getPreviewURL(props.node, { cropPreview: props.cropImagePreviews }))
const {
previewURL,
previewLoaded,
} = usePreviewURL(toRef(props, 'node'), computed(() => ({ cropPreview: props.cropImagePreviews })))

const isFile = computed(() => props.node.type === FileType.File)
const canLoadPreview = ref(false)

watchEffect(() => {
canLoadPreview.value = false

if (previewURL.value) {
const loader = new Image()
loader.src = previewURL.value.href
loader.onerror = () => loader.remove()
loader.onload = () => { canLoadPreview.value = true; loader.remove() }
}
})
</script>

<script lang="ts">
Expand Down
12 changes: 11 additions & 1 deletion lib/composables/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
*/

import type { Node } from '@nextcloud/files'
import type { MaybeRef } from '@vueuse/core'
import type { Ref } from 'vue'

import { generateUrl } from '@nextcloud/router'
import { toValue } from '@vueuse/core'
import { ref, watchEffect } from 'vue'
import { preloadImage } from '../utils/imagePreload'

interface PreviewOptions {
/**
Expand Down Expand Up @@ -66,14 +68,22 @@ export function getPreviewURL(node: Node, options: PreviewOptions = {}) {
}
}

export const usePreviewURL = (node: Node | Ref<Node>, options?: PreviewOptions | Ref<PreviewOptions>) => {
export const usePreviewURL = (node: Node | Ref<Node>, options?: MaybeRef<PreviewOptions>) => {
const previewURL = ref<URL|null>(null)
const previewLoaded = ref(false)

watchEffect(() => {
previewLoaded.value = false
previewURL.value = getPreviewURL(toValue(node), toValue(options || {}))
if (previewURL.value) {
preloadImage(previewURL.value.href).then((success: boolean) => {
previewLoaded.value = success
})
}
})

return {
previewURL,
previewLoaded,
}
}
25 changes: 25 additions & 0 deletions lib/utils/imagePreload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import PQueue from 'p-queue'

const queue = new PQueue({ concurrency: 5 })

/**
* Preload an image URL
* @param url URL of the image
*/
export function preloadImage(url: string): Promise<boolean> {
const { resolve, promise } = Promise.withResolvers<boolean>()
queue.add(() => {
const image = new Image()
image.onerror = () => resolve(false)
image.onload = () => resolve(true)
image.src = url
return promise
})

return promise
}
51 changes: 47 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"@types/toastify-js": "^1.12.3",
"@vueuse/core": "^10.11.1",
"cancelable-promise": "^4.3.1",
"p-queue": "^8.0.1",
"toastify-js": "^1.12.0",
"vue-frag": "^1.4.3",
"webdav": "^5.7.1"
Expand All @@ -82,6 +83,7 @@
"@vue/test-utils": "^1.3.6",
"@vue/tsconfig": "^0.5.1",
"@zamiell/typedoc-plugin-not-exported": "^0.3.0",
"core-js": "^3.39.0",
"gettext-extractor": "^3.8.0",
"gettext-parser": "^8.0.0",
"happy-dom": "^14.12.3",
Expand Down
7 changes: 7 additions & 0 deletions test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*/

// Polyfill like the server does
import 'core-js/stable/index.js'
23 changes: 0 additions & 23 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,6 @@ export default defineConfig((env) => {
// Fix for vite config, TODO: remove with next release
cssCodeSplit: false,
},
// vitest configuration
test: {
environment: 'happy-dom',
coverage: {
all: true,
provider: 'v8',
include: ['lib/**/*.ts', 'lib/*.ts'],
exclude: ['lib/**/*.spec.ts'],
},
css: {
modules: {
classNameStrategy: 'non-scoped',
},
},
server: {
deps: {
inline: [
/@nextcloud\/vue/, // Fix unresolvable .css extension for ssr
/@nextcloud\/files/, // Fix CommonJS cancelable-promise not supporting named exports
],
},
},
},
},
// We build for ESM and legacy common js
libraryFormats: ['es', 'cjs'],
Expand Down
40 changes: 35 additions & 5 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,41 @@
* SPDX-License-Identifier: CC0-1.0
*/
import type { ConfigEnv } from 'vite'
import { defineConfig, type ViteUserConfig } from 'vitest/config'
import config from './vite.config'

export default async (env: ConfigEnv) => {
export default defineConfig(async (env: ConfigEnv): Promise<ViteUserConfig> => {
const cfg = await config(env)
// filter node-externals which will interfere with vitest
cfg.plugins = cfg.plugins!.filter((plugin) => plugin && (!('name' in plugin) || plugin.name !== 'node-externals'))
return cfg
}

return {
...cfg,

// filter node-externals which will interfere with vitest
plugins: cfg.plugins!.filter((plugin) => plugin && (!('name' in plugin) || plugin.name !== 'node-externals')),

// vitest configuration
test: {
environment: 'happy-dom',
coverage: {
all: true,
provider: 'v8',
include: ['lib/**/*.ts', 'lib/*.ts'],
exclude: ['lib/**/*.spec.ts'],
},
css: {
modules: {
classNameStrategy: 'non-scoped',
},
},
setupFiles: 'test/setup.ts',
server: {
deps: {
inline: [
/@nextcloud\/vue/, // Fix unresolvable .css extension for ssr
/@nextcloud\/files/, // Fix CommonJS cancelable-promise not supporting named exports
],
},
},
},
}
})