Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Load preview URL for download-disabled shares #2484

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions cypress/e2e/download-forbidden.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ describe('Disable download button if forbidden', { testIsolation: true }, () =>
.should('contain', 'image1 .jpg')
})

// TODO: Fix no-download files on server
it.skip('See the image can be shown', () => {
it('See the image can be shown', () => {
cy.getFile('image1.jpg').should('be.visible')
cy.openFile('image1.jpg')
cy.get('body > .viewer').should('be.visible')
Expand Down
6 changes: 2 additions & 4 deletions cypress/e2e/sharing/download-share-disabled.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,14 @@ describe(`Download ${fileName} in viewer`, function() {
cy.get('body > .viewer').should('be.visible')
})

// TODO: FIX DOWNLOAD DISABLED SHARES
it.skip('Does not see a loading animation', function() {
it('Does not see a loading animation', function() {
cy.get('body > .viewer', { timeout: 10000 })
.should('be.visible')
.and('have.class', 'modal-mask')
.and('not.have.class', 'icon-loading')
})

// TODO: FIX DOWNLOAD DISABLED SHARES
it.skip('See the title on the viewer header but not the Download nor the menu button', function() {
it('See the title on the viewer header but not the Download nor the menu button', function() {
cy.get('body > .viewer .modal-header__name').should('contain', 'image1.jpg')
cy.get('body a[download="image1.jpg"]').should('not.exist')
cy.get('body > .viewer .modal-header button.action-item__menutoggle').should('not.exist')
Expand Down
125 changes: 70 additions & 55 deletions src/components/Images.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,63 +11,63 @@
:fileid="fileid"
@close="onClose" />

<template v-else-if="data !== null">
<img v-if="!livePhotoCanBePlayed"
ref="image"
:alt="alt"
<IconImageBroken v-if="!data" :size="64" />

<img v-else-if="!livePhotoCanBePlayed"
ref="image"
:alt="alt"
:class="{
dragging,
loaded,
zoomed: zoomRatio > 1
}"
:src="data"
:style="imgStyle"
@error.capture.prevent.stop.once="onFail"
@load="updateImgSize"
@wheel.stop.prevent="updateZoom"
@dblclick.prevent="onDblclick"
@pointerdown.prevent="pointerDown"
@pointerup.prevent="pointerUp"
@pointermove.prevent="pointerMove">

<template v-else-if="livePhoto">
<video v-show="livePhotoCanBePlayed"
ref="video"
:class="{
dragging,
loaded,
zoomed: zoomRatio > 1
}"
:src="data"
:style="imgStyle"
@error.capture.prevent.stop.once="onFail"
@load="updateImgSize"
:playsinline="true"
:poster="data"
:src="livePhotoSrc"
preload="metadata"
@canplaythrough="doneLoadingLivePhoto"
@loadedmetadata="updateImgSize"
@wheel.stop.prevent="updateZoom"
@error.capture.prevent.stop.once="onFail"
@dblclick.prevent="onDblclick"
@pointerdown.prevent="pointerDown"
@pointerup.prevent="pointerUp"
@pointermove.prevent="pointerMove">

<template v-if="livePhoto">
<video v-show="livePhotoCanBePlayed"
ref="video"
:class="{
dragging,
loaded,
zoomed: zoomRatio > 1
}"
:style="imgStyle"
:playsinline="true"
:poster="data"
:src="livePhotoSrc"
preload="metadata"
@canplaythrough="doneLoadingLivePhoto"
@loadedmetadata="updateImgSize"
@wheel.stop.prevent="updateZoom"
@error.capture.prevent.stop.once="onFail"
@dblclick.prevent="onDblclick"
@pointerdown.prevent="pointerDown"
@pointerup.prevent="pointerUp"
@pointermove.prevent="pointerMove"
@ended="stopLivePhoto" />
<button v-if="width !== 0"
class="live-photo_play_button"
:style="{left: `calc(50% - ${width/2}px)`}"
:disabled="!livePhotoCanBePlayed"
:aria-description="t('viewer', 'Play the live photo')"
@click="playLivePhoto"
@pointerenter="playLivePhoto"
@focus="playLivePhoto"
@pointerleave="stopLivePhoto"
@blur="stopLivePhoto">
<PlayCircleOutline v-if="livePhotoCanBePlayed" />
<NcLoadingIcon v-else />
<!-- TRANSLATORS Label of the button used at the top left corner of live photos to play them -->
{{ t('viewer', 'LIVE') }}
</button>
</template>
@pointermove.prevent="pointerMove"
@ended="stopLivePhoto" />
<button v-if="width !== 0"
class="live-photo_play_button"
:style="{left: `calc(50% - ${width/2}px)`}"
:disabled="!livePhotoCanBePlayed"
:aria-description="t('viewer', 'Play the live photo')"
@click="playLivePhoto"
@pointerenter="playLivePhoto"
@focus="playLivePhoto"
@pointerleave="stopLivePhoto"
@blur="stopLivePhoto">
<PlayCircleOutline v-if="livePhotoCanBePlayed" />
<NcLoadingIcon v-else />
<!-- TRANSLATORS Label of the button used at the top left corner of live photos to play them -->
{{ t('viewer', 'LIVE') }}
</button>
</template>
</div>
</template>
Expand All @@ -76,6 +76,7 @@
import Vue from 'vue'
import AsyncComputed from 'vue-async-computed'
import PlayCircleOutline from 'vue-material-design-icons/PlayCircleOutline.vue'
import IconImageBroken from 'vue-material-design-icons/ImageBroken.vue'

import axios from '@nextcloud/axios'
import { basename } from '@nextcloud/paths'
Expand All @@ -85,13 +86,15 @@ import { NcLoadingIcon } from '@nextcloud/vue'
import ImageEditor from './ImageEditor.vue'
import { findLivePhotoPeerFromFileId } from '../utils/livePhotoUtils'
import { getDavPath } from '../utils/fileUtils'
import { getPreviewIfAny } from '../utils/previewUtils'

Vue.use(AsyncComputed)

export default {
name: 'Images',

components: {
IconImageBroken,
ImageEditor,
PlayCircleOutline,
NcLoadingIcon,
Expand Down Expand Up @@ -175,23 +178,35 @@ export default {

// Load the raw gif instead of the static preview
if (this.mime === 'image/gif') {
// if the source failed fallback to the preview
if (this.fallback) {
return this.previewPath
}
return this.src
}

// If there is no preview and we have a direct source
// load it instead
if (this.source && !this.hasPreview && !this.previewUrl) {
return this.source
// First try the preview if any
if (!this.fallback && this.previewPath) {
return this.previewPath
}

// If loading the preview failed once, let's load the original file
if (this.fallback) {
return this.src
}
return this.src
},

return this.previewPath
async previewPath() {
return await getPreviewIfAny({
...this.$attrs,
fileid: this.fileid,
filename: this.filename,
previewUrl: this.previewUrl,
hasPreview: this.hasPreview,
davPath: this.davPath,
etag: this.$attrs.etag,
})
},
},

watch: {
active(val, old) {
// the item was hidden before and is now the current view
Expand Down
15 changes: 13 additions & 2 deletions src/mixins/Mime.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import debounce from 'debounce'
import PreviewUrl from '../mixins/PreviewUrl.js'
import parsePath from 'path-parse'
import { getDavPath } from '../utils/fileUtils.ts'

export default {
inheritAttrs: false,
mixins: [PreviewUrl],
props: {
// Is the current component shown
active: {
Expand Down Expand Up @@ -98,6 +97,18 @@ export default {
},

computed: {
/**
* Absolute dav remote path of the file
*
* @return {string}
*/
davPath() {
return getDavPath({
filename: this.filename,
basename: this.basename,
})
},

name() {
return parsePath(this.basename).name
},
Expand Down
56 changes: 0 additions & 56 deletions src/mixins/PreviewUrl.js

This file was deleted.

4 changes: 3 additions & 1 deletion src/utils/canDownload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import type { FileInfo } from './fileUtils'
export function canDownload(fileInfo: FileInfo) {
// TODO: This should probably be part of `@nextcloud/sharing`
// check share attributes
const shareAttributes = typeof fileInfo?.shareAttributes === 'string' ? JSON.parse(fileInfo.shareAttributes || '[]') : fileInfo?.shareAttributes
const shareAttributes = typeof fileInfo?.shareAttributes === 'string'
? JSON.parse(fileInfo.shareAttributes || '[]')
: fileInfo?.shareAttributes

if (shareAttributes && shareAttributes.length > 0) {
const downloadAttribute = shareAttributes.find(({ scope, key }) => scope === 'permissions' && key === 'download')
Expand Down
51 changes: 17 additions & 34 deletions src/utils/fileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import { getLanguage } from '@nextcloud/l10n'
import { encodePath } from '@nextcloud/paths'
import camelcase from 'camelcase'

import { isNumber } from './numberUtil'

export interface FileInfo {
/** ID of the file (not unique if shared, use source instead) */
fileid?: number
Expand All @@ -33,7 +31,7 @@ export interface FileInfo {
/** File type */
type: 'directory'|'file'
/** Attributes for file shares */
shareAttributes?: string|Array<{value:boolean|string|number|null|object|Array<unknown>, key: string, scope: string}>
shareAttributes?: string|Array<{ value: unknown, key: string, scope: string }>

// custom attributes not fetch from API

Expand Down Expand Up @@ -78,13 +76,7 @@ export function sortCompare(fileInfo1: FileInfo, fileInfo2: FileInfo, key: strin
return 1
}

// if this is a number, let's sort by integer
if (isNumber(fileInfo1[key]) && isNumber(fileInfo2[key])) {
const result = Number(fileInfo1[key]) - Number(fileInfo2[key])
return asc ? result : -result
}

// else we sort by string, so let's sort directories first
// let's sort directories first
if (fileInfo1.type === 'directory' && fileInfo2.type !== 'directory') {
return -1
} else if (fileInfo1.type !== 'directory' && fileInfo2.type === 'directory') {
Expand All @@ -97,8 +89,8 @@ export function sortCompare(fileInfo1: FileInfo, fileInfo2: FileInfo, key: strin
}
// finally sort by name
return asc
? fileInfo1[key].localeCompare(fileInfo2[key], getLanguage(), { numeric: true })
: -fileInfo1[key].localeCompare(fileInfo2[key], getLanguage(), { numeric: true })
? String(fileInfo1[key]).localeCompare(fileInfo2[key], getLanguage(), { numeric: true })
: -String(fileInfo1[key]).localeCompare(fileInfo2[key], getLanguage(), { numeric: true })
}

/**
Expand All @@ -107,29 +99,20 @@ export function sortCompare(fileInfo1: FileInfo, fileInfo2: FileInfo, key: strin
* @param obj The stat response to convert
*/
export function genFileInfo(obj: FileStat): FileInfo {
const fileInfo = {}

Object.keys(obj).forEach(key => {
const data = obj[key]

// flatten object if any
if (!!data && typeof data === 'object' && !Array.isArray(data)) {
Object.assign(fileInfo, genFileInfo(data))
} else {
// format key and add it to the fileInfo
if (data === 'false') {
fileInfo[camelcase(key)] = false
} else if (data === 'true') {
fileInfo[camelcase(key)] = true
} else {
fileInfo[camelcase(key)] = isNumber(data)
? Number(data)
: data
}
}
})
const fileStat = {
...(obj.props ?? {}),
...obj,
props: undefined,
}

return fileInfo as FileInfo
const fileInfo = Object.entries(fileStat)
// Make property names camel case
.map(([key, value]) => [camelcase(key), value])
// Convert boolean - Numbers are already parsed by the WebDAV client
.map(([key, value]) => [key, ['true', 'false'].includes(value as never) ? value === 'true' : value])
// remove undefined properties
.filter(([, value]) => value !== undefined)
return Object.fromEntries(fileInfo)
}

/**
Expand Down
Loading
Loading