memories/src/components/frame/Photo.vue

235 lines
6.1 KiB
Vue
Raw Normal View History

2022-08-20 07:11:51 +00:00
<template>
2022-10-15 18:48:17 +00:00
<div class="p-outer fill-block"
2022-09-08 17:41:16 +00:00
:class="{
'selected': (data.flag & c.FLAG_SELECTED),
2022-10-16 19:01:49 +00:00
'placeholder': (data.flag & c.FLAG_PLACEHOLDER),
2022-09-08 17:41:16 +00:00
'leaving': (data.flag & c.FLAG_LEAVING),
2022-10-16 02:55:53 +00:00
'error': (data.flag & c.FLAG_LOAD_FAIL),
2022-09-08 17:41:16 +00:00
}">
2022-09-08 17:12:16 +00:00
2022-09-16 03:17:40 +00:00
<Check :size="15" class="select"
2022-09-08 17:12:16 +00:00
v-if="!(data.flag & c.FLAG_PLACEHOLDER)"
2022-09-16 03:17:40 +00:00
@click="toggleSelect" />
2022-09-07 22:14:12 +00:00
2022-09-16 03:17:40 +00:00
<Video :size="20" v-if="data.flag & c.FLAG_IS_VIDEO" />
<Star :size="20" v-if="data.flag & c.FLAG_IS_FAVORITE" />
2022-09-12 03:46:31 +00:00
2022-10-15 18:48:17 +00:00
<div class="img-outer fill-block"
2022-10-17 03:53:38 +00:00
@click="emitClick"
2022-09-15 03:52:58 +00:00
@contextmenu="contextmenu"
@touchstart="touchstart"
@touchmove="touchend"
@touchend="touchend"
@touchcancel="touchend" >
2022-09-07 23:12:06 +00:00
<img
2022-10-18 21:08:27 +00:00
ref="img"
2022-10-15 18:48:17 +00:00
class="fill-block"
2022-10-18 21:08:27 +00:00
:src="src"
:key="data.fileid"
2022-10-18 21:08:27 +00:00
@load="load"
2022-10-16 19:01:49 +00:00
@error="error" />
2022-09-07 23:12:06 +00:00
</div>
2022-08-20 07:11:51 +00:00
</div>
</template>
2022-09-13 01:33:24 +00:00
<script lang="ts">
2022-10-18 21:08:27 +00:00
import { Component, Prop, Emit, Mixins, Watch } from 'vue-property-decorator';
2022-10-14 21:45:23 +00:00
import { IDay, IPhoto } from "../../types";
2022-09-13 01:33:24 +00:00
2022-10-14 21:45:23 +00:00
import { getPreviewUrl } from "../../services/FileUtils";
import errorsvg from "../../assets/error.svg";
import GlobalMixin from '../../mixins/GlobalMixin';
2022-08-20 07:11:51 +00:00
2022-09-16 03:17:40 +00:00
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,
},
})
2022-09-13 02:36:27 +00:00
export default class Photo extends Mixins(GlobalMixin) {
2022-09-13 01:33:24 +00:00
private touchTimer = 0;
2022-10-18 21:08:27 +00:00
private src = null;
private hasFaceRect = false;
2022-09-13 01:33:24 +00:00
@Prop() data: IPhoto;
@Prop() day: IDay;
2022-09-13 01:57:51 +00:00
@Emit('select') emitSelect(data: IPhoto) {}
2022-10-17 03:53:38 +00:00
@Emit('click') emitClick() {}
2022-09-13 01:57:51 +00:00
2022-10-18 21:08:27 +00:00
@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();
}
2022-09-13 05:33:20 +00:00
/** Get src for image to show */
2022-10-18 21:08:27 +00:00
async getSrc() {
2022-09-13 02:36:27 +00:00
if (this.data.flag & this.c.FLAG_PLACEHOLDER) {
2022-10-18 21:08:27 +00:00
return null;
2022-09-13 02:36:27 +00:00
} else if (this.data.flag & this.c.FLAG_LOAD_FAIL) {
2022-09-13 01:33:24 +00:00
return errorsvg;
} else {
2022-09-15 03:52:58 +00:00
return this.url;
2022-09-13 05:33:20 +00:00
}
}
/** Get url of the photo */
2022-09-15 03:52:58 +00:00
get url() {
2022-10-16 04:26:01 +00:00
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)
2022-09-13 05:33:20 +00:00
}
2022-10-18 21:08:27 +00:00
/** 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();
}
2022-09-13 01:33:24 +00:00
/** Error in loading image */
error(e: any) {
2022-10-16 19:01:49 +00:00
this.data.flag |= this.c.FLAG_LOAD_FAIL;
2022-10-18 21:08:27 +00:00
this.refresh();
2022-09-13 01:33:24 +00:00
}
2022-09-13 05:25:30 +00:00
/** Clear timers */
beforeUnmount() {
clearTimeout(this.touchTimer);
}
2022-09-13 01:33:24 +00:00
toggleSelect() {
2022-10-17 03:53:38 +00:00
if (this.data.flag & this.c.FLAG_PLACEHOLDER) return;
2022-09-13 01:57:51 +00:00
this.emitSelect(this.data);
2022-09-13 01:33:24 +00:00
}
2022-09-07 20:04:03 +00:00
2022-09-13 01:33:24 +00:00
touchstart() {
this.touchTimer = window.setTimeout(() => {
this.toggleSelect();
this.touchTimer = 0;
}, 600);
}
2022-09-07 22:14:12 +00:00
2022-09-13 01:33:24 +00:00
contextmenu(e: Event) {
e.preventDefault();
e.stopPropagation();
}
touchend() {
if (this.touchTimer) {
clearTimeout(this.touchTimer);
this.touchTimer = 0;
}
2022-08-20 07:11:51 +00:00
}
}
</script>
2022-09-10 02:10:51 +00:00
<style lang="scss" scoped>
2022-09-08 17:41:16 +00:00
/* Container and selection */
.p-outer {
2022-09-10 02:10:51 +00:00
&.leaving {
transition: all 0.2s ease-in;
transform: scale(0.9);
opacity: 0;
}
2022-09-08 17:41:16 +00:00
}
2022-09-10 02:10:51 +00:00
2022-10-16 04:06:26 +00:00
// Distance of icon from border
$icon-dist: min(10px, 6%);
2022-09-12 03:57:47 +00:00
/* Extra icons */
2022-09-16 03:17:40 +00:00
.check-icon.select {
2022-09-07 22:14:12 +00:00
position: absolute;
2022-10-16 04:06:26 +00:00
top: $icon-dist; left: $icon-dist;
2022-09-12 03:46:31 +00:00
z-index: 100;
2022-09-07 22:14:12 +00:00
background-color: var(--color-main-background);
border-radius: 50%;
cursor: pointer;
2022-10-10 23:13:52 +00:00
display: none;
2022-09-10 02:10:51 +00:00
2022-10-10 23:13:52 +00:00
.p-outer:hover > & { display: flex; }
.selected > & { display: flex; filter: invert(1); }
2022-09-07 22:14:12 +00:00
}
2022-09-16 03:17:40 +00:00
.video-icon, .star-icon {
2022-08-20 07:11:51 +00:00
position: absolute;
2022-09-12 03:46:31 +00:00
z-index: 100;
pointer-events: none;
2022-09-16 03:17:40 +00:00
filter: invert(1) brightness(100);
}
.video-icon {
2022-10-16 04:06:26 +00:00
top: $icon-dist; right: $icon-dist;
2022-09-16 03:17:40 +00:00
}
.star-icon {
2022-10-16 04:06:26 +00:00
bottom: $icon-dist; left: $icon-dist;
2022-08-20 07:11:51 +00:00
}
2022-09-08 17:41:16 +00:00
/* Actual image */
2022-09-10 02:10:51 +00:00
div.img-outer {
2022-09-07 23:12:06 +00:00
padding: 2px;
@media (max-width: 768px) { padding: 1px; }
2022-10-16 04:06:26 +00:00
transition: padding 0.1s ease;
background-clip: content-box, padding-box;
background-color: var(--color-background-dark);
2022-09-10 02:10:51 +00:00
2022-10-16 04:06:26 +00:00
.selected > & { padding: calc($icon-dist - 2px); }
2022-09-08 01:27:51 +00:00
2022-09-15 17:20:02 +00:00
> img {
background-clip: content-box;
object-fit: cover;
cursor: pointer;
2022-09-10 02:10:51 +00:00
2022-09-15 17:20:02 +00:00
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
user-select: none;
2022-10-16 04:06:26 +00:00
transition: box-shadow 0.1s ease;
2022-09-15 17:20:02 +00:00
2022-10-16 04:06:26 +00:00
.selected > & { box-shadow: 0 0 4px 2px var(--color-primary); }
2022-10-16 19:01:49 +00:00
.p-outer.placeholder > & { display: none; }
2022-10-16 02:55:53 +00:00
.p-outer.error & { object-fit: contain; }
2022-09-15 17:20:02 +00:00
}
}
2022-08-20 07:11:51 +00:00
</style>