Skip to content

Commit

Permalink
Add live photo support base on files metadata
Browse files Browse the repository at this point in the history
Signed-off-by: Louis Chemineau <[email protected]>
  • Loading branch information
artonge committed Jan 3, 2024
1 parent 464ca47 commit 6e6ad64
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 115 deletions.
189 changes: 154 additions & 35 deletions src/components/Images.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,82 @@
-->

<template>
<ImageEditor v-if="editing"
:mime="mime"
:src="src"
:fileid="fileid"
@close="onClose" />

<img v-else-if="data !== null"
:alt="alt"
:class="{
dragging,
loaded,
zoomed: zoomRatio !== 1
}"
:src="data"
:style="imgStyle"
@error.capture.prevent.stop.once="onFail"
@load="updateImgSize"
@wheel="updateZoom"
@dblclick.prevent="onDblclick"
@mousedown.prevent="dragStart">
<div class="image_container">
<ImageEditor v-if="editing"
:mime="mime"
:src="src"
:fileid="fileid"
@close="onClose" />

<template v-else-if="data !== null">
<img v-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="updateZoom"
@dblclick.prevent="onDblclick"
@mousedown.prevent="dragStart">

<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="updateZoom"
@error.capture.prevent.stop.once="onFail"
@dblclick.prevent="onDblclick"
@mousedown.prevent="dragStart"
@ended="stopLivePhoto" />
<button v-if="livePhoto && 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 />
{{ t('viewer', 'LIVE') }}
</button>
</template>
</template>
</div>
</template>

<script>
import axios from '@nextcloud/axios'
import Vue from 'vue'
import AsyncComputed from 'vue-async-computed'
import ImageEditor from './ImageEditor.vue'
import PlayCircleOutline from 'vue-material-design-icons/PlayCircleOutline.vue'

import axios from '@nextcloud/axios'
import { basename } from '@nextcloud/paths'
import { translate } from '@nextcloud/l10n'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'

import ImageEditor from './ImageEditor.vue'
import { livePhoto } from '../utils/livePhotoUtils'
import { getDavPath } from '../utils/fileUtils'

Vue.use(AsyncComputed)

Expand All @@ -57,6 +105,8 @@ export default {

components: {
ImageEditor,
PlayCircleOutline,
NcLoadingIcon,
},

props: {
Expand All @@ -76,6 +126,7 @@ export default {
shiftY: 0,
zoomRatio: 1,
fallback: false,
livePhotoCanBePlayed: false,
}
},

Expand Down Expand Up @@ -103,6 +154,19 @@ export default {
width: this.zoomWidth + 'px',
}
},
livePhoto() {
return livePhoto(this, this.fileList)
},
livePhotoSrc() {
return this.livePhoto.source ?? this.livePhotoDavPath
},
/** @return {string|null} */
livePhotoDavPath() {
return getDavPath({
filename: this.livePhoto.filename,
basename: this.livePhoto.basename,
})
},
},

asyncComputed: {
Expand Down Expand Up @@ -147,8 +211,13 @@ export default {
methods: {
// Updates the dimensions of the modal
updateImgSize() {
this.naturalHeight = this.$el.naturalHeight
this.naturalWidth = this.$el.naturalWidth
if (this.$refs.image) {
this.naturalHeight = this.$refs.image.naturalHeight
this.naturalWidth = this.$refs.image.naturalWidth
} else if (this.$refs.video) {
this.naturalHeight = this.$refs.video.getBoundingClientRect().height
this.naturalWidth = this.$refs.video.getBoundingClientRect().width
}

this.updateHeightWidth()
this.doneLoading()
Expand All @@ -157,7 +226,7 @@ export default {
/**
* Manually retrieve the path and return its base64
*
* @return {string}
* @return {Promise<string>}
*/
async getBase64FromImage() {
const file = await axios.get(this.src)
Expand All @@ -167,8 +236,8 @@ export default {
/**
* Handle zooming
*
* @param {Event} event the scroll event
* @return {null}
* @param {WheelEvent} event the scroll event
* @return {void}
*/
updateZoom(event) {
if (!this.canZoom) {
Expand All @@ -179,8 +248,9 @@ export default {
event.preventDefault()

// scrolling position relative to the image
const scrollX = event.clientX - this.$el.x - (this.width * this.zoomRatio / 2)
const scrollY = event.clientY - this.$el.y - (this.height * this.zoomRatio / 2)
const element = this.$refs.image ?? this.$refs.video
const scrollX = event.clientX - element.x - (this.width * this.zoomRatio / 2)
const scrollY = event.clientY - element.y - (this.height * this.zoomRatio / 2)
const scrollPercX = scrollX / (this.width * this.zoomRatio)
const scrollPercY = scrollY / (this.height * this.zoomRatio)
const isZoomIn = event.deltaY < 0
Expand Down Expand Up @@ -216,24 +286,32 @@ export default {
/**
* Dragging handlers
*
* @param {Event} event the event
* @param {DragEvent} event the event
*/
dragStart(event) {
const { pageX, pageY } = event

this.dragX = pageX
this.dragY = pageY
this.dragging = true
this.$el.onmouseup = this.dragEnd
this.$el.onmousemove = this.dragHandler
const element = this.$refs.image ?? this.$refs.video
element.onmouseup = this.dragEnd
element.onmousemove = this.dragHandler
},
/**
* @param {DragEvent} event the event
*/
dragEnd(event) {
event.preventDefault()

this.dragging = false
this.$el.onmouseup = null
this.$el.onmousemove = null
const element = this.$refs.image ?? this.$refs.video
element.onmouseup = null
element.onmousemove = null
},
/**
* @param {DragEvent} event the event
*/
dragHandler(event) {
event.preventDefault()
const { pageX, pageY } = event
Expand Down Expand Up @@ -269,6 +347,26 @@ export default {
this.fallback = true
}
},
doneLoadingLivePhoto() {
this.livePhotoCanBePlayed = true
this.doneLoading()
},
playLivePhoto() {
if (!this.livePhotoCanBePlayed) {
return
}

/** @type {HTMLVideoElement} */
const video = this.$refs.video
video.play()
},
stopLivePhoto() {
/** @type {HTMLVideoElement} */
const video = this.$refs.video
video.load()
},

t: translate,
},
}
</script>
Expand All @@ -277,7 +375,14 @@ export default {
$checkered-size: 8px;
$checkered-color: #efefef;

img {
.image_container {
display: flex;
align-items: center;
height: 100%;
justify-content: center;
}

img, video {
max-width: 100%;
max-height: 100%;
align-self: center;
Expand Down Expand Up @@ -312,4 +417,18 @@ img {
cursor: move;
}
}

.live-photo_play_button {
position: absolute;
top: 0;
// left: is set dynamically on the element itself
margin: 16px !important;
display: flex;
align-items: center;
border: none;
gap: 4px;
border-radius: var(--border-radius);
padding: 4px 8px;
background-color: var(--color-main-background-blur);
}
</style>
24 changes: 11 additions & 13 deletions src/components/Videos.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@
<script>
// eslint-disable-next-line n/no-missing-import
import '@skjnldsv/vue-plyr/dist/vue-plyr.css'
import logger from '../services/logger.js'
import { imagePath } from '@nextcloud/router'
import logger from '../services/logger.js'
import { livePhoto } from '../utils/livePhotoUtils'
import { getPreviewIfAny } from '../utils/previewUtils'

const VuePlyr = () => import(/* webpackChunkName: 'plyr' */'@skjnldsv/vue-plyr')

const liveExt = ['jpg', 'jpeg', 'png']
const liveExtRegex = new RegExp(`\\.(${liveExt.join('|')})$`, 'i')
const blankVideo = imagePath('viewer', 'blank.mp4')

export default {
Expand All @@ -78,16 +78,14 @@ export default {
},

computed: {
livePhoto() {
return this.fileList.find(file => {
// if same filename and extension is allowed
return file.filename !== this.filename
&& file.basename.startsWith(this.name)
&& liveExtRegex.test(file.basename)
})
},
livePhotoPath() {
return this.livePhoto && this.getPreviewIfAny(this.livePhoto)
const peerFile = livePhoto(this, this.fileList)

if (peerFile === undefined) {
return undefined
}

return getPreviewIfAny(peerFile)
},
player() {
return this.$refs.plyr.player
Expand All @@ -101,7 +99,7 @@ export default {
loadSprite: false,
fullscreen: {
iosNative: true,
}
},
}
},
},
Expand Down
5 changes: 5 additions & 0 deletions src/mixins/Mime.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ export default {
type: Boolean,
default: false,
},
// The file id of the peer live photo file
metadataFilesLivePhoto: {
type: Number,
default: undefined,
},
},

data() {
Expand Down
25 changes: 3 additions & 22 deletions src/mixins/PreviewUrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { encodePath } from '@nextcloud/paths'
import { generateUrl } from '@nextcloud/router'
import { getToken, isPublic } from '../utils/davUtils.ts'
import { getPreviewIfAny } from '../utils/previewUtils.ts'
import { getDavPath } from '../utils/fileUtils.ts'

export default {
Expand Down Expand Up @@ -68,25 +66,8 @@ export default {
* @param {string|null} data.etag the etag of the file
* @return {string} the absolute url
*/
getPreviewIfAny({ fileid, filename, previewUrl, hasPreview, davPath, etag }) {
if (previewUrl) {
return previewUrl
}

const searchParams = `fileId=${fileid}`
+ `&x=${Math.floor(screen.width * devicePixelRatio)}`
+ `&y=${Math.floor(screen.height * devicePixelRatio)}`
+ '&a=true'
+ (etag !== null ? `&etag=${etag.replace(/&quot;/g, '')}` : '')

if (hasPreview) {
// TODO: find a nicer standard way of doing this?
if (isPublic()) {
return generateUrl(`/apps/files_sharing/publicpreview/${getToken()}?file=${encodePath(filename)}&${searchParams}`)
}
return generateUrl(`/core/preview?${searchParams}`)
}
return davPath
getPreviewIfAny(data) {
return getPreviewIfAny(data)
},
},
}
Loading

0 comments on commit 6e6ad64

Please sign in to comment.