|
|
|
@ -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> |
|
|
|
|