memories/src/components/frame/Photo.vue

236 lines
6.1 KiB
Vue

<template>
<div class="p-outer fill-block"
:class="{
'selected': (data.flag & c.FLAG_SELECTED),
'placeholder': (data.flag & c.FLAG_PLACEHOLDER),
'leaving': (data.flag & c.FLAG_LEAVING),
'error': (data.flag & c.FLAG_LOAD_FAIL),
}">
<Check :size="15" class="select"
v-if="!(data.flag & c.FLAG_PLACEHOLDER)"
@click="toggleSelect" />
<Video :size="20" v-if="data.flag & c.FLAG_IS_VIDEO" />
<Star :size="20" v-if="data.flag & c.FLAG_IS_FAVORITE" />
<div class="img-outer fill-block"
@click="emitClick"
@contextmenu="contextmenu"
@touchstart="touchstart"
@touchmove="touchend"
@touchend="touchend"
@touchcancel="touchend" >
<img
ref="img"
class="fill-block"
:src="src"
:key="data.fileid"
@load="load"
@error="error" />
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Emit, Mixins, Watch } from 'vue-property-decorator';
import { IDay, IPhoto } from "../../types";
import { getPreviewUrl } from "../../services/FileUtils";
import errorsvg from "../../assets/error.svg";
import GlobalMixin from '../../mixins/GlobalMixin';
import Check from 'vue-material-design-icons/Check.vue';
import Video from 'vue-material-design-icons/Video.vue';
import Star from 'vue-material-design-icons/Star.vue';
@Component({
components: {
Check,
Video,
Star,
},
})
export default class Photo extends Mixins(GlobalMixin) {
private touchTimer = 0;
private src = null;
private hasFaceRect = false;
@Prop() data: IPhoto;
@Prop() day: IDay;
@Emit('select') emitSelect(data: IPhoto) {}
@Emit('click') emitClick() {}
@Watch('data')
onDataChange(newData: IPhoto, oldData: IPhoto) {
// Copy flags relevant to this component
if (oldData && newData) {
newData.flag |= oldData.flag & (this.c.FLAG_SELECTED | this.c.FLAG_LOAD_FAIL);
}
}
mounted() {
this.hasFaceRect = false;
this.refresh();
}
async refresh() {
this.src = await this.getSrc();
}
/** Get src for image to show */
async getSrc() {
if (this.data.flag & this.c.FLAG_PLACEHOLDER) {
return null;
} else if (this.data.flag & this.c.FLAG_LOAD_FAIL) {
return errorsvg;
} else {
return this.url;
}
}
/** Get url of the photo */
get url() {
let size = 256;
if (this.data.w && this.data.h) {
size = Math.floor(size * Math.max(this.data.w, this.data.h) / Math.min(this.data.w, this.data.h));
}
return getPreviewUrl(this.data.fileid, this.data.etag, false, size)
}
/** Set src with overlay face rect */
async addFaceRect() {
if (!this.data.facerect || this.hasFaceRect) return;
this.hasFaceRect = true;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const img = this.$refs.img as HTMLImageElement;
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
context.drawImage(img, 0, 0);
context.strokeStyle = '#00ff00';
context.lineWidth = 2;
context.strokeRect(
this.data.facerect.x * img.naturalWidth,
this.data.facerect.y * img.naturalHeight,
this.data.facerect.w * img.naturalWidth,
this.data.facerect.h * img.naturalHeight,
);
canvas.toBlob((blob) => {
this.src = URL.createObjectURL(blob);
}, 'image/jpeg', 0.95)
}
/** Post load tasks */
load() {
this.addFaceRect();
}
/** Error in loading image */
error(e: any) {
this.data.flag |= this.c.FLAG_LOAD_FAIL;
this.refresh();
}
/** Clear timers */
beforeUnmount() {
clearTimeout(this.touchTimer);
}
toggleSelect() {
if (this.data.flag & this.c.FLAG_PLACEHOLDER) return;
this.emitSelect(this.data);
}
touchstart() {
this.touchTimer = window.setTimeout(() => {
this.toggleSelect();
this.touchTimer = 0;
}, 600);
}
contextmenu(e: Event) {
e.preventDefault();
e.stopPropagation();
}
touchend() {
if (this.touchTimer) {
clearTimeout(this.touchTimer);
this.touchTimer = 0;
}
}
}
</script>
<style lang="scss" scoped>
/* Container and selection */
.p-outer {
&.leaving {
transition: all 0.2s ease-in;
transform: scale(0.9);
opacity: 0;
}
}
// Distance of icon from border
$icon-dist: min(10px, 6%);
/* Extra icons */
.check-icon.select {
position: absolute;
top: $icon-dist; left: $icon-dist;
z-index: 100;
background-color: var(--color-main-background);
border-radius: 50%;
cursor: pointer;
display: none;
.p-outer:hover > & { display: flex; }
.selected > & { display: flex; filter: invert(1); }
}
.video-icon, .star-icon {
position: absolute;
z-index: 100;
pointer-events: none;
filter: invert(1) brightness(100);
}
.video-icon {
top: $icon-dist; right: $icon-dist;
}
.star-icon {
bottom: $icon-dist; left: $icon-dist;
}
/* Actual image */
div.img-outer {
padding: 2px;
box-sizing: border-box;
@media (max-width: 768px) { padding: 1px; }
transition: padding 0.1s ease;
background-clip: content-box, padding-box;
background-color: var(--color-background-dark);
.selected > & { padding: calc($icon-dist - 2px); }
> img {
background-clip: content-box;
object-fit: cover;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
user-select: none;
transition: box-shadow 0.1s ease;
.selected > & { box-shadow: 0 0 4px 2px var(--color-primary); }
.p-outer.placeholder > & { display: none; }
.p-outer.error & { object-fit: contain; }
}
}
</style>