frame: animate live photo icon (close #898)

Signed-off-by: Varun Patil <radialapps@gmail.com>
pull/900/head
Varun Patil 2023-10-30 02:12:56 -07:00
parent 543646624a
commit c6f5ed5b05
4 changed files with 129 additions and 16 deletions

View File

@ -2,6 +2,10 @@
All notable changes to this project will be documented in this file.
## [Unreleased]
- Icon animation when playing live photos ([#898](https://github.com/pulsejet/memories/issues/898))
## [v6.0.1] - 2023-10-27
- Bug fixes in video streaming.

View File

@ -24,7 +24,7 @@
<VideoIcon :size="22" />
</div>
<div class="livephoto" v-if="data.liveid" @mouseenter.passive="playVideo" @mouseleave.passive="stopVideo">
<LivePhotoIcon :size="22" />
<LivePhotoIcon :size="22" :spin="liveWaiting" :playing="livePlaying" />
</div>
<RawIcon class="raw" v-if="isRaw" :size="28" />
</div>
@ -75,10 +75,10 @@ import { defineComponent, type PropType } from 'vue';
import * as utils from '@services/utils';
import LivePhotoIcon from '@components/icons/LivePhoto.vue';
import CheckCircleIcon from 'vue-material-design-icons/CheckCircle.vue';
import StarIcon from 'vue-material-design-icons/Star.vue';
import VideoIcon from 'vue-material-design-icons/PlayCircleOutline.vue';
import LivePhotoIcon from 'vue-material-design-icons/MotionPlayOutline.vue';
import LocalIcon from 'vue-material-design-icons/CloudOff.vue';
import RawIcon from 'vue-material-design-icons/Raw.vue';
@ -90,10 +90,10 @@ import errorsvg from '@assets/error.svg';
export default defineComponent({
name: 'Photo',
components: {
LivePhotoIcon,
CheckCircleIcon,
VideoIcon,
StarIcon,
LivePhotoIcon,
LocalIcon,
RawIcon,
},
@ -119,7 +119,9 @@ export default defineComponent({
data: () => ({
touchTimer: 0,
playLiveTimer: 0,
livePlayTimer: 0,
liveWaiting: false,
livePlaying: false,
faceSrc: null as string | null,
}),
@ -139,13 +141,16 @@ export default defineComponent({
const video = this.refs.video;
if (video) {
utils.setupLivePhotoHooks(video);
video.addEventListener('playing', () => (this.livePlaying = true));
video.addEventListener('pause', () => (this.livePlaying = false));
video.addEventListener('ended', () => (this.livePlaying = false));
}
},
/** Clear timers */
beforeDestroy() {
clearTimeout(this.touchTimer);
clearTimeout(this.playLiveTimer);
clearTimeout(this.livePlayTimer);
// Clean up blob url if face rect was created
if (this.faceSrc) {
@ -272,9 +277,11 @@ export default defineComponent({
/** Start preview video */
playVideo() {
this.liveWaiting = true;
utils.setRenewingTimeout(
this,
'playLiveTimer',
'livePlayTimer',
async () => {
const video = this.refs.video;
if (!video || this.data.flag & this.c.FLAG_SELECTED) return;
@ -284,6 +291,8 @@ export default defineComponent({
await video.play();
} catch (e) {
// ignore, pause was probably called too soon
} finally {
this.liveWaiting = false;
}
},
400,
@ -292,8 +301,11 @@ export default defineComponent({
/** Stop preview video */
stopVideo() {
window.clearTimeout(this.playLiveTimer);
this.refs.video?.pause();
window.clearTimeout(this.livePlayTimer);
this.livePlayTimer = 0;
this.liveWaiting = false;
this.livePlaying = false;
},
},
});

View File

@ -0,0 +1,102 @@
<template>
<span
v-bind="$attrs"
:aria-hidden="!title"
:aria-label="title"
:style="{ width: `${size}px`, height: `${size}px` }"
class="material-design-icon live-photo-icon"
:class="{ spin }"
role="img"
@click="$emit('click', $event)"
>
<svg :fill="fillColor" class="material-design-icon__svg ring" :width="size" :height="size" viewBox="0 0 24 24">
<path
d="M 22,12 C 22,6.46 17.54,2 12,2 10.83,2 9.7,2.19 8.62,2.56 L 9.32,4.5 C 10.17,4.16 11.06,3.97 12,3.97 c 4.41,0 8.03,3.62 8.03,8.03 0,4.41 -3.62,8.03 -8.03,8.03 -4.41,0 -8.03,-3.62 -8.03,-8.03 0,-0.94 0.19,-1.88 0.53,-2.72 L 2.56,8.62 C 2.19,9.7 2,10.83 2,12 2,17.54 6.46,22 12,22 17.54,22 22,17.54 22,12 M 5.47,3.97 C 6.32,3.97 7,4.68 7,5.47 7,6.32 6.32,7 5.47,7 4.68,7 3.97,6.32 3.97,5.47 c 0,-0.79 0.71,-1.5 1.5,-1.5 z"
></path>
</svg>
<svg
:fill="fillColor"
class="material-design-icon__svg play"
:class="{ visible: !playing }"
:width="size"
:height="size"
viewBox="0 0 24 24"
>
<path d="M 10,16.5 16,12 10,7.5"></path>
</svg>
<svg
v-if="playing"
:fill="fillColor"
class="material-design-icon__svg pause"
:class="{ visible: playing }"
:width="size"
:height="size"
viewBox="0 0 24 24"
>
<path d="m 9,9 h 2 v 6 H 9 m 4,-6 h 2 v 6 h -2" />
</svg>
</span>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'LivePhoto',
emits: ['click'],
props: {
title: {
type: String,
},
fillColor: {
type: String,
default: 'currentColor',
},
size: {
type: Number,
default: 24,
},
spin: {
type: Boolean,
default: false,
},
playing: {
type: Boolean,
default: false,
},
},
});
</script>
<style scoped lang="scss">
.live-photo-icon {
position: relative;
pointer-events: none;
> svg {
position: absolute;
}
&.spin > .ring {
animation: spin 1s linear infinite;
}
> .play,
> .pause {
opacity: 0;
transition: opacity 0.2s ease-in-out;
&.visible {
opacity: 1;
}
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
}
</style>

View File

@ -165,15 +165,10 @@ export function getLivePhotoVideoUrl(p: IPhoto, transcode: boolean) {
*/
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');
};
video.addEventListener('play', () => div.classList.add('playing'));
video.addEventListener('canplay', () => div.classList.add('canplay'));
video.addEventListener('ended', () => div.classList.remove('playing'));
video.addEventListener('pause', () => div.classList.remove('playing'));
}
/**