Skip to content

Commit

Permalink
Merge pull request #1015 from nextcloud-libraries/fix/stable4-previews
Browse files Browse the repository at this point in the history
[stable4] Backport preview generation for FilePicker
  • Loading branch information
susnux authored Sep 23, 2023
2 parents c23dbc5 + f547d54 commit 783eef3
Show file tree
Hide file tree
Showing 9 changed files with 594 additions and 20 deletions.
6 changes: 6 additions & 0 deletions l10n/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ msgstr ""
msgid "Home"
msgstr ""

msgid "Mime type {mime}"
msgstr ""

msgid "Modified"
msgstr ""

Expand Down Expand Up @@ -115,6 +118,9 @@ msgstr ""
msgid "Undo"
msgstr ""

msgid "unknown"
msgstr ""

msgid "Unset"
msgstr ""

Expand Down
13 changes: 6 additions & 7 deletions lib/components/FilePicker/FileList.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<template>
<div class="file-picker__files" ref="fileContainer">
<div ref="fileContainer" class="file-picker__files">
<table>
<thead>
<tr>
<th class="row-checkbox" v-if="multiselect">
<th v-if="multiselect" class="row-checkbox">
<span class="hidden-visually">
{{ t('Select entry') }}
</span>
Expand All @@ -16,8 +16,7 @@
<th :aria-sort="sortByName" class="row-name">
<div class="header-wrapper">
<span class="file-picker__header-preview" />
<NcButton
:wide="true"
<NcButton :wide="true"
type="tertiary"
data-test="file-picker_sort-name"
@click="toggleSortByName">
Expand Down Expand Up @@ -54,7 +53,7 @@
</thead>
<tbody>
<template v-if="loading">
<LoadingTableRow v-for="index in skeletonNumber" :key="index" :show-checkbox="multiselect"/>
<LoadingTableRow v-for="index in skeletonNumber" :key="index" :show-checkbox="multiselect" />
</template>
<template v-else>
<FileListRow v-for="file in sortedFiles"
Expand Down Expand Up @@ -197,7 +196,7 @@ const fileContainer = ref<HTMLDivElement>()
const resize = () => nextTick(() => {
const nodes = fileContainer.value?.parentElement?.children || []
let height = fileContainer.value?.parentElement?.clientHeight || 450
for(let index = 0; index < nodes.length; index++) {
for (let index = 0; index < nodes.length; index++) {
if (!fileContainer.value?.isSameNode(nodes[index])) {
height -= nodes[index].clientHeight
}
Expand Down Expand Up @@ -276,7 +275,7 @@ const fileContainer = ref<HTMLDivElement>()
}
th :deep(.button-vue__wrapper) {
color: var(--color-text-maxcontrast);
.button-vue__text {
font-weight: normal;
}
Expand Down
13 changes: 5 additions & 8 deletions lib/components/FilePicker/FileListRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</td>
<td class="row-name">
<div class="file-picker__name-container" data-testid="row-name">
<div :class="fileListIconStyles['file-picker__file-icon']" :style="{ backgroundImage }" />
<FilePreview :node="node" />
<div class="file-picker__file-name" :title="displayName" v-text="displayName" />
<div class="file-picker__file-extension" v-text="fileExtension" />
</div>
Expand All @@ -36,13 +36,15 @@
</tr>
</template>
<script setup lang="ts">
import { type Node, formatFileSize, FileType } from '@nextcloud/files'
import type { Node } from '@nextcloud/files'
import { formatFileSize, FileType } from '@nextcloud/files'
import { NcCheckboxRadioSwitch } from '@nextcloud/vue'
import { computed } from 'vue'
import { t } from '../../utils/l10n'
import FilePreview from './FilePreview.vue'
import NcDatetime from './NcDatetime.vue'
import fileListIconStyles from './FileListIcon.module.scss'
const props = defineProps<{
/** Can directories be picked */
Expand Down Expand Up @@ -84,11 +86,6 @@ const isDirectory = computed(() => props.node.type === FileType.Folder)
*/
const isPickable = computed(() => props.canPick && (props.allowPickDirectory || !isDirectory.value))
/**
* Background image url for the given nodes mime type
*/
const backgroundImage = computed(() => `url(${window.OC.MimeType.getIconUrl(props.node.mime)})`)
/**
* Toggle the selection state
*/
Expand Down
55 changes: 55 additions & 0 deletions lib/components/FilePicker/FilePreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<template>
<div :style="canLoadPreview ? { backgroundImage: `url(${previewURL})`} : undefined"
:aria-label="t('Mime type {mime}', { mime: node.mime || t('unknown') })"
class="file-picker__file-icon">
<template v-if="!canLoadPreview">
<IconFile v-if="isFile" :size="20" />
<IconFolder v-else :size="20" />
</template>
</div>
</template>

<script setup lang="ts">
import { FileType, type Node } from '@nextcloud/files'
import { usePreviewURL } from '../../usables/preview'
import { computed, ref, toRef, watch } from 'vue'
import { t } from '../../utils/l10n'
import IconFile from 'vue-material-design-icons/File.vue'
import IconFolder from 'vue-material-design-icons/Folder.vue'
const props = defineProps<{
node: Node
}>()
const { previewURL } = usePreviewURL(toRef(props, 'node'))
const isFile = computed(() => props.node.type === FileType.File)
const canLoadPreview = ref(false)
watch(previewURL, () => {
canLoadPreview.value = false
if (previewURL.value) {
const loader = document.createElement('img')
loader.src = previewURL.value.href
loader.onerror = () => loader.remove()
loader.onload = () => { canLoadPreview.value = true; loader.remove() }
document.body.appendChild(loader)
}
}, { immediate: true })
</script>

<style scoped lang="scss">
.file-picker__file-icon {
width: 32px;
height: 32px;
min-width: 32px;
min-height: 32px;
background-repeat: no-repeat;
background-size: contain;
// for the fallback
display: flex;
justify-content: center;
}
</style>
1 change: 0 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@ export { spawnDialog } from './utils/dialogs.js'

export { FilePickerVue } from './components/FilePicker/index.js'
export type { IFilePickerButton } from './components/types.js'

93 changes: 93 additions & 0 deletions lib/usables/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
*
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

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

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

interface PreviewOptions {
/**
* Size of the previews in px
* @default 32
*/
size?: number
/**
* Should the preview fall back to the mime type icon
* @default true
*/
mimeFallback?: boolean
/**
* Should the preview be cropped or fitted
* @default false (meaning it gets fitted)
*/
cropPreview?: boolean
}

/**
* Generate the preview URL of a file node
*
* @param node The node to generate the preview for
* @param options Preview options
*/
export function getPreviewURL(node: Node, options: PreviewOptions = {}) {
options = { size: 32, cropPreview: false, mimeFallback: true, ...options }

try {
const previewUrl = node.attributes?.previewUrl
|| generateUrl('/core/preview?fileId={fileid}', {
fileid: node.fileid,
})

let url
try {
url = new URL(previewUrl)
} catch (e) {
url = new URL(previewUrl, window.location.origin)
}

// Request preview with params
url.searchParams.set('x', `${options.size}`)
url.searchParams.set('y', `${options.size}`)
url.searchParams.set('mimeFallback', `${options.mimeFallback}`)
url.searchParams.set('c', `${node.attributes.etag}`) // cache busting

// Handle cropping
url.searchParams.set('a', options.cropPreview === true ? '0' : '1')
return url
} catch (e) {
return null
}
}

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

watchEffect(() => {
previewURL.value = getPreviewURL(toValue(node), toValue(options || {}))
})

return {
previewURL,
}
}
Loading

0 comments on commit 783eef3

Please sign in to comment.