timeline: play livephoto on hover

pull/231/head
Varun Patil 2022-11-22 05:57:34 -08:00
parent 0f57602c1d
commit 8985927c5e
5 changed files with 129 additions and 56 deletions

View File

@ -356,4 +356,38 @@ aside.app-sidebar {
height: 100%;
display: block;
}
:root {
--livephoto-img-transition: opacity 0.5s linear, transform 0.4s ease-in-out;
}
// Live photo transitions
.memories-livephoto {
position: relative;
overflow: hidden;
contain: strict;
img,
video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: block;
transition: var(--livephoto-img-transition);
}
video,
&.playing.canplay img {
opacity: 0;
}
img,
&.playing.canplay video {
opacity: 1;
}
&.playing.canplay img {
transform: scale(1.07);
}
}
</style>

View File

@ -1,5 +1,5 @@
import PhotoSwipe from "photoswipe";
import { generateUrl } from "@nextcloud/router";
import * as utils from "../services/Utils";
function isLiveContent(content): boolean {
return Boolean(content?.data?.photo?.liveid);
@ -29,25 +29,16 @@ class LivePhotoContentSetup {
const video = document.createElement("video");
video.muted = true;
video.autoplay = false;
video.playsInline = true;
video.preload = "none";
video.src = generateUrl(
`/apps/memories/api/video/livephoto/${photo.fileid}?etag=${photo.etag}&liveid=${photo.liveid}`
);
video.src = utils.getLivePhotoVideoUrl(photo);
const div = document.createElement("div");
div.className = "livephoto";
div.className = "memories-livephoto";
div.appendChild(video);
content.element = div;
video.onplay = () => {
div.classList.add("playing");
};
video.oncanplay = () => {
div.classList.add("canplay");
};
video.onended = video.onpause = () => {
div.classList.remove("playing");
};
utils.setupLivePhotoHooks(video);
const img = document.createElement("img");
img.src = content.data.src;
@ -59,17 +50,17 @@ class LivePhotoContentSetup {
onContentActivate({ content }) {
if (isLiveContent(content) && content.element) {
content.element.querySelector("video")?.play();
const video = content.element.querySelector("video");
if (video) {
video.currentTime = 0;
video.play();
}
}
}
onContentDeactivate({ content }) {
if (isLiveContent(content) && content.element) {
const vid = content.element.querySelector("video");
if (vid) {
vid.pause();
vid.currentTime = 0;
}
content.element.querySelector("video")?.pause();
}
}

View File

@ -904,35 +904,6 @@ export default class Viewer extends Mixins(GlobalMixin) {
.pswp__top-bar {
background: linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.3));
}
.livephoto {
position: relative;
overflow: hidden;
contain: strict;
img,
video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: block;
transition: opacity 0.5s ease-in-out, transform 0.4s ease-in-out;
}
video,
&.playing.canplay img {
opacity: 0;
}
img,
&.playing.canplay video {
opacity: 1;
}
&.playing.canplay img {
transform: scale(1.07);
}
}
}
:deep .video-js .vjs-big-play-button {

View File

@ -22,7 +22,11 @@
<Video :size="22" />
</div>
<div class="livephoto">
<div
class="livephoto"
@mouseenter.passive="playVideo"
@mouseleave.passive="stopVideo"
>
<LivePhoto :size="22" v-if="data.liveid" />
</div>
@ -30,6 +34,9 @@
<div
class="img-outer fill-block"
:class="{
'memories-livephoto': data.liveid,
}"
@contextmenu="contextmenu"
@pointerdown.passive="$emit('pointerdown', $event)"
@touchstart.passive="$emit('touchstart', $event)"
@ -46,7 +53,16 @@
@load="load"
@error="error"
/>
<div class="overlay" />
<video
ref="video"
v-if="videoUrl"
:src="videoUrl"
preload="none"
muted
playsinline
loop
/>
<div class="overlay fill-block" />
</div>
</div>
</template>
@ -101,6 +117,12 @@ export default class Photo extends Mixins(GlobalMixin) {
mounted() {
this.hasFaceRect = false;
this.refresh();
// Setup video hooks
const video = this.$refs.video as HTMLVideoElement;
if (video) {
utils.setupLivePhotoHooks(video);
}
}
get videoDuration() {
@ -110,6 +132,12 @@ export default class Photo extends Mixins(GlobalMixin) {
return null;
}
get videoUrl() {
if (this.data.liveid) {
return utils.getLivePhotoVideoUrl(this.data);
}
}
async refresh() {
this.src = await this.getSrc();
}
@ -210,6 +238,23 @@ export default class Photo extends Mixins(GlobalMixin) {
e.stopPropagation();
}
}
/** Start preview video */
playVideo() {
if (this.$refs.video) {
const video = this.$refs.video as HTMLVideoElement;
video.currentTime = 0;
video.play();
}
}
/** Stop preview video */
stopVideo() {
if (this.$refs.video) {
const video = this.$refs.video as HTMLVideoElement;
video.pause();
}
}
}
</script>
@ -306,6 +351,9 @@ $icon-size: $icon-half-size * 2;
margin-right: 3px;
}
}
.livephoto {
pointer-events: auto;
}
.star-icon {
bottom: var(--icon-dist);
left: var(--icon-dist);
@ -316,6 +364,7 @@ $icon-size: $icon-half-size * 2;
/* Actual image */
div.img-outer {
position: relative;
box-sizing: border-box;
padding: 0;
@ -339,7 +388,7 @@ div.img-outer {
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
user-select: none;
transition: border-radius 0.1s ease-in;
transition: border-radius 0.1s ease-in, var(--livephoto-img-transition);
.p-outer.placeholder > & {
display: none;
@ -349,11 +398,12 @@ div.img-outer {
}
}
& > .overlay {
> video,
> .overlay {
pointer-events: none;
width: 100%;
height: 100%;
transform: translateY(-100%); // very weird stuff
}
> .overlay {
background: linear-gradient(180deg, rgba(0, 0, 0, 0.2) 0%, transparent 30%);
display: none;

View File

@ -1,5 +1,6 @@
import { getCanonicalLocale } from "@nextcloud/l10n";
import { getCurrentUser } from "@nextcloud/auth";
import { generateUrl } from "@nextcloud/router";
import { loadState } from "@nextcloud/initial-state";
import { IPhoto } from "../types";
import moment from "moment";
@ -236,6 +237,32 @@ export function getFolderRoutePath(basePath: string) {
return path;
}
/**
* Get URL to live photo video part
*/
export function getLivePhotoVideoUrl(p: IPhoto) {
return generateUrl(
`/apps/memories/api/video/livephoto/${p.fileid}?etag=${p.etag}&liveid=${p.liveid}`
);
}
/**
* Set up hooks to set classes on parent element for live photo
* @param video Video element
*/
export function setupLivePhotoHooks(video: HTMLVideoElement) {
const div = video.closest(".memories-livephoto") as HTMLDivElement;
video.onplay = () => {
div.classList.add("playing");
};
video.oncanplay = () => {
div.classList.add("canplay");
};
video.onended = video.onpause = () => {
div.classList.remove("playing");
};
}
/**
* Get route hash for viewer for photo
*/