Merge pull request #47605 from nextcloud/artonge/feat/files_blurhash

feat: Use the blurhash in Files
pull/47627/head
Louis 2024-08-29 23:31:13 +07:00 committed by GitHub
commit 1cc7851209
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 90 additions and 23 deletions

@ -14,16 +14,22 @@
</template>
</template>
<!-- Decorative image, should not be aria documented -->
<img v-else-if="previewUrl && backgroundFailed !== true"
ref="previewImg"
alt=""
class="files-list__row-icon-preview"
:class="{'files-list__row-icon-preview--loaded': backgroundFailed === false}"
loading="lazy"
:src="previewUrl"
@error="onBackgroundError"
@load="backgroundFailed = false">
<!-- Decorative images, should not be aria documented -->
<span v-else-if="previewUrl" class="files-list__row-icon-preview-container">
<canvas v-if="hasBlurhash && (backgroundFailed === true || !backgroundLoaded)"
ref="canvas"
class="files-list__row-icon-blurhash"
aria-hidden="true" />
<img v-if="backgroundFailed !== true"
ref="previewImg"
alt=""
class="files-list__row-icon-preview"
:class="{'files-list__row-icon-preview--loaded': backgroundFailed === false}"
loading="lazy"
:src="previewUrl"
@error="onBackgroundError"
@load="onBackgroundLoad">
</span>
<FileIcon v-else v-once />
@ -45,9 +51,10 @@ import type { UserConfig } from '../../types.ts'
import { Node, FileType } from '@nextcloud/files'
import { generateUrl } from '@nextcloud/router'
import { translate as t } from '@nextcloud/l10n'
import { Type as ShareType } from '@nextcloud/sharing'
import { ShareType } from '@nextcloud/sharing'
import { decode } from 'blurhash'
import { defineComponent } from 'vue'
import Vue from 'vue'
import AccountGroupIcon from 'vue-material-design-icons/AccountGroup.vue'
import AccountPlusIcon from 'vue-material-design-icons/AccountPlus.vue'
import FileIcon from 'vue-material-design-icons/File.vue'
@ -64,8 +71,9 @@ import FavoriteIcon from './FavoriteIcon.vue'
import { isLivePhoto } from '../../services/LivePhotos'
import { useUserConfigStore } from '../../store/userconfig.ts'
import logger from '../../logger.ts'
export default Vue.extend({
export default defineComponent({
name: 'FileEntryPreview',
components: {
@ -107,6 +115,7 @@ export default Vue.extend({
data() {
return {
backgroundFailed: undefined as boolean | undefined,
backgroundLoaded: false,
}
},
@ -183,7 +192,7 @@ export default Vue.extend({
// Link and mail shared folders
const shareTypes = Object.values(this.source?.attributes?.['share-types'] || {}).flat() as number[]
if (shareTypes.some(type => type === ShareType.SHARE_TYPE_LINK || type === ShareType.SHARE_TYPE_EMAIL)) {
if (shareTypes.some(type => type === ShareType.Link || type === ShareType.Email)) {
return LinkIcon
}
@ -206,6 +215,16 @@ export default Vue.extend({
return null
},
hasBlurhash() {
return this.source.attributes['metadata-blurhash'] !== undefined
},
},
mounted() {
if (this.hasBlurhash && this.$refs.canvas) {
this.drawBlurhash()
}
},
methods: {
@ -213,17 +232,44 @@ export default Vue.extend({
reset() {
// Reset background state to cancel any ongoing requests
this.backgroundFailed = undefined
if (this.$refs.previewImg) {
this.$refs.previewImg.src = ''
this.backgroundLoaded = false
const previewImg = this.$refs.previewImg as HTMLImageElement | undefined
if (previewImg) {
previewImg.src = ''
}
},
onBackgroundLoad() {
this.backgroundFailed = false
this.backgroundLoaded = true
},
onBackgroundError(event) {
// Do not fail if we just reset the background
if (event.target?.src === '') {
return
}
this.backgroundFailed = true
this.backgroundLoaded = false
},
drawBlurhash() {
const canvas = this.$refs.canvas as HTMLCanvasElement
const width = canvas.width
const height = canvas.height
const pixels = decode(this.source.attributes['metadata-blurhash'], width, height)
const ctx = canvas.getContext('2d')
if (ctx === null) {
logger.error('Cannot create context for blurhash canvas')
return
}
const imageData = ctx.createImageData(width, height)
imageData.data.set(pixels)
ctx.putImageData(imageData, 0, 0)
},
t,

@ -556,11 +556,24 @@ export default defineComponent({
}
}
&-preview {
&-preview-container {
position: relative; // Needed for the blurshash to be positioned correctly
overflow: hidden;
width: var(--icon-preview-size);
height: var(--icon-preview-size);
border-radius: var(--border-radius);
}
&-blurhash {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
object-fit: cover;
}
&-preview {
// Center and contain the preview
object-fit: contain;
object-position: center;

@ -66,5 +66,6 @@ registerPreviewServiceWorker()
registerDavProperty('nc:hidden', { nc: 'http://nextcloud.org/ns' })
registerDavProperty('nc:is-mount-root', { nc: 'http://nextcloud.org/ns' })
registerDavProperty('nc:metadata-blurhash', { nc: 'http://nextcloud.org/ns' })
initLivePhotos()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
package-lock.json generated

@ -38,6 +38,7 @@
"@vueuse/integrations": "^11.0.1",
"backbone": "^1.4.1",
"blueimp-md5": "^2.19.0",
"blurhash": "^2.0.5",
"browserslist-useragent-regexp": "^4.1.1",
"camelcase": "^8.0.0",
"cancelable-promise": "^4.3.1",
@ -8082,6 +8083,11 @@
"integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==",
"license": "MIT"
},
"node_modules/blurhash": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
"integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w=="
},
"node_modules/bmp-js": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",

@ -68,6 +68,7 @@
"@vueuse/integrations": "^11.0.1",
"backbone": "^1.4.1",
"blueimp-md5": "^2.19.0",
"blurhash": "^2.0.5",
"browserslist-useragent-regexp": "^4.1.1",
"camelcase": "^8.0.0",
"cancelable-promise": "^4.3.1",