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%; height: 100%;
display: block; 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> </style>

View File

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

View File

@ -904,35 +904,6 @@ export default class Viewer extends Mixins(GlobalMixin) {
.pswp__top-bar { .pswp__top-bar {
background: linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.3)); 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 { :deep .video-js .vjs-big-play-button {

View File

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

View File

@ -1,5 +1,6 @@
import { getCanonicalLocale } from "@nextcloud/l10n"; import { getCanonicalLocale } from "@nextcloud/l10n";
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from "@nextcloud/auth";
import { generateUrl } from "@nextcloud/router";
import { loadState } from "@nextcloud/initial-state"; import { loadState } from "@nextcloud/initial-state";
import { IPhoto } from "../types"; import { IPhoto } from "../types";
import moment from "moment"; import moment from "moment";
@ -236,6 +237,32 @@ export function getFolderRoutePath(basePath: string) {
return path; 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 * Get route hash for viewer for photo
*/ */