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%;
|
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>
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue