diff options
Diffstat (limited to 'apps/files/src/components/FileEntry.vue')
-rw-r--r-- | apps/files/src/components/FileEntry.vue | 130 |
1 files changed, 120 insertions, 10 deletions
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index 9f1df025e1f..84990a5ba39 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -31,10 +31,18 @@ <!-- Icon or preview --> <td class="files-list__row-icon"> <FolderIcon v-if="source.type === 'folder'" /> + <!-- Decorative image, should not be aria documented --> - <span v-else-if="previewUrl" - :style="{ backgroundImage: `url('${previewUrl}')` }" - class="files-list__row-icon-preview" /> + <span v-else-if="previewUrl && !backgroundFailed" + ref="previewImg" + class="files-list__row-icon-preview" + :style="{ backgroundImage }" /> + + <span v-else-if="mimeUrl" + class="files-list__row-icon-preview files-list__row-icon-preview--mime" + :style="{ backgroundImage: mimeUrl }" /> + + <FileIcon v-else /> </td> <!-- Link to file and --> @@ -65,6 +73,7 @@ import { Folder, File } from '@nextcloud/files' import { Fragment } from 'vue-fragment' import { join } from 'path' import { translate } from '@nextcloud/l10n' +import FileIcon from 'vue-material-design-icons/File.vue' import FolderIcon from 'vue-material-design-icons/Folder.vue' import TrashCan from 'vue-material-design-icons/TrashCan.vue' import Pencil from 'vue-material-design-icons/Pencil.vue' @@ -73,19 +82,24 @@ import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' import Vue from 'vue' -import logger from '../logger' +import logger from '../logger.js' import { useSelectionStore } from '../store/selection' import { useFilesStore } from '../store/files' import { loadState } from '@nextcloud/initial-state' +import { debounce } from 'debounce' // TODO: move to store // TODO: watch 'files:config:updated' event const userConfig = loadState('files', 'config', {}) +// The preview service worker cache name (see webpack config) +const SWCacheName = 'previews' + export default Vue.extend({ name: 'FileEntry', components: { + FileIcon, FolderIcon, Fragment, NcActionButton, @@ -96,10 +110,6 @@ export default Vue.extend({ }, props: { - index: { - type: Number, - required: true, - }, source: { type: [File, Folder], required: true, @@ -118,6 +128,8 @@ export default Vue.extend({ data() { return { userConfig, + backgroundImage: '', + backgroundFailed: false, } }, @@ -171,6 +183,32 @@ export default Vue.extend({ return null } }, + + mimeUrl() { + const mimeType = this.source.mime || 'application/octet-stream' + const mimeUrl = window.OC?.MimeType?.getIconUrl?.(mimeType) + if (mimeUrl) { + return `url(${mimeUrl})` + } + return '' + }, + }, + + watch: { + source() { + this.resetPreview() + this.debounceIfNotCached() + }, + }, + + mounted() { + // Init the debounce function on mount and + // not when the module is imported ⚠ + this.debounceGetPreview = debounce(function() { + this.fetchAndApplyPreview() + }, 150, false) + + this.debounceIfNotCached() }, methods: { @@ -180,15 +218,87 @@ export default Vue.extend({ * @param {number} fileId the file id to get * @return {Folder|File} */ - getNode(fileId) { + getNode(fileId) { return this.filesStore.getNode(fileId) }, + async debounceIfNotCached() { + if (!this.previewUrl) { + return + } + + // Check if we already have this preview cached + const isCached = await this.isCachedPreview(this.previewUrl) + if (isCached) { + logger.debug('Preview already cached', { fileId: this.source.attributes.fileid, backgroundFailed: this.backgroundFailed }) + this.backgroundImage = `url(${this.previewUrl})` + this.backgroundFailed = false + return + } + + // We don't have this preview cached or it expired, requesting it + this.debounceGetPreview() + }, + + fetchAndApplyPreview() { + logger.debug('Fetching preview', { fileId: this.source.attributes.fileid }) + this.img = new Image() + this.img.onload = () => { + this.backgroundImage = `url(${this.previewUrl})` + } + this.img.onerror = (a, b, c) => { + this.backgroundFailed = true + logger.error('Failed to fetch preview', { fileId: this.source.attributes.fileid, a, b, c }) + } + this.img.src = this.previewUrl + }, + + resetPreview() { + // Reset the preview + this.backgroundImage = '' + this.backgroundFailed = false + + // If we're already fetching a preview, cancel it + if (this.img) { + // Do not fail on cancel + this.img.onerror = null + this.img.src = '' + delete this.img + } + }, + + isCachedPreview(previewUrl) { + return caches.open(SWCacheName) + .then(function(cache) { + return cache.match(previewUrl) + .then(function(response) { + return !!response // or `return response ? true : false`, or similar. + }) + }) + }, + t: translate, }, }) </script> <style scoped lang="scss"> -@import '../mixins/fileslist-row.scss' +@import '../mixins/fileslist-row.scss'; + +.files-list__row-icon-preview:not([style*="background"]) { + background: linear-gradient(110deg, var(--color-loading-dark) 0%, var(--color-loading-dark) 25%, var(--color-loading-light) 50%, var(--color-loading-dark) 75%, var(--color-loading-dark) 100%); + background-size: 400%; + animation: preview-gradient-slide 1s ease infinite; +} +</style> + +<style> +@keyframes preview-gradient-slide { + from { + background-position: 100% 0%; + } + to { + background-position: 0% 0%; + } +} </style> |