summaryrefslogtreecommitdiffstats
path: root/apps/files/src/components/FileEntry.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src/components/FileEntry.vue')
-rw-r--r--apps/files/src/components/FileEntry.vue130
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>