timeline: play livephoto on hover
parent
0f57602c1d
commit
8985927c5e
34
src/App.vue
34
src/App.vue
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue