memories/src/components/frame/Photo.vue

291 lines
8.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),
'p-loading': !(data.flag & c.FLAG_LOADED),
2022-09-08 17:41:16 +00:00
'leaving': (data.flag & c.FLAG_LEAVING),
'exit-left': (data.flag & c.FLAG_EXIT_LEFT),
'enter-right': (data.flag & c.FLAG_ENTER_RIGHT),
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-09-15 03:52:58 +00:00
@click="click"
@contextmenu="contextmenu"
@touchstart="touchstart"
@touchmove="touchend"
@touchend="touchend"
@touchcancel="touchend" >
2022-09-07 23:12:06 +00:00
<img
2022-10-15 18:48:17 +00:00
class="fill-block"
2022-09-15 03:52:58 +00:00
:src="src()"
:key="data.fileid"
@error="error"
2022-09-15 03:52:58 +00:00
@load="load" />
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">
import { Component, Prop, Emit, Mixins } from 'vue-property-decorator';
2022-10-14 21:45:23 +00:00
import { IDay, IPhoto } from "../../types";
2022-10-11 20:38:09 +00:00
import { subscribe, unsubscribe } from '@nextcloud/event-bus';
2022-09-13 01:33:24 +00:00
2022-09-16 00:50:44 +00:00
import { showError } from '@nextcloud/dialogs'
2022-10-14 21:45:23 +00:00
import { getPreviewUrl } from "../../services/FileUtils";
import * as dav from "../../services/DavRequests";
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;
@Prop() data: IPhoto;
@Prop() day: IDay;
2022-10-11 20:38:09 +00:00
@Emit('delete') emitDelete(remPhotos: IPhoto[]) {}
2022-09-13 01:57:51 +00:00
@Emit('select') emitSelect(data: IPhoto) {}
@Emit('clickImg') emitClickImg(component: any) {}
2022-09-13 05:33:20 +00:00
/** Get src for image to show */
2022-09-15 03:52:58 +00:00
src() {
2022-09-13 02:36:27 +00:00
if (this.data.flag & this.c.FLAG_PLACEHOLDER) {
2022-09-13 01:33:24 +00:00
return undefined;
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;
2022-09-13 02:36:27 +00:00
} else if (this.data.flag & this.c.FLAG_FORCE_RELOAD) {
this.data.flag &= ~this.c.FLAG_FORCE_RELOAD;
2022-09-13 01:33:24 +00:00
return undefined;
} 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-09-13 05:25:30 +00:00
/** Image loaded successfully */
load() {
this.data.flag |= this.c.FLAG_LOADED;
}
2022-09-13 01:33:24 +00:00
/** Error in loading image */
error(e: any) {
2022-09-13 02:36:27 +00:00
this.data.flag |= (this.c.FLAG_LOADED | this.c.FLAG_LOAD_FAIL);
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
/** Pass to parent */
click() {
2022-09-13 01:57:51 +00:00
this.emitClickImg(this);
2022-09-13 01:33:24 +00:00
}
/** Open viewer */
async openFile() {
// Check if this is a placeholder
2022-09-13 02:36:27 +00:00
if (this.data.flag & this.c.FLAG_PLACEHOLDER) {
2022-09-13 01:33:24 +00:00
return;
2022-09-08 01:07:53 +00:00
}
2022-09-13 01:33:24 +00:00
// Check if already loaded fileInfos or load
let fileInfos = this.day.fileInfos;
if (!fileInfos) {
const ids = this.day.detail.map(p => p.fileid);
try {
fileInfos = await dav.getFiles(ids);
} catch (e) {
console.error('Failed to load fileInfos', e);
}
2022-09-13 01:33:24 +00:00
if (fileInfos.length === 0) {
2022-08-20 21:12:29 +00:00
return;
}
2022-08-20 07:11:51 +00:00
2022-09-13 01:33:24 +00:00
// Fix sorting of the fileInfos
const itemPositions = {};
for (const [index, id] of ids.entries()) {
itemPositions[id] = index;
}
fileInfos.sort(function (a, b) {
return itemPositions[a.fileid] - itemPositions[b.fileid];
});
// Store in day with a original copy
this.day.fileInfos = fileInfos;
this.day.origFileIds = new Set(fileInfos.map(f => f.fileid));
}
2022-08-20 07:11:51 +00:00
2022-09-13 01:33:24 +00:00
// Get this photo in the fileInfos
const photo = fileInfos.find(d => Number(d.fileid) === Number(this.data.fileid));
if (!photo) {
2022-09-16 23:26:29 +00:00
showError(this.t('memories', 'Cannot find this photo anymore!'));
2022-09-13 01:33:24 +00:00
return;
}
// Key to store sidebar state
const SIDEBAR_KEY = 'memories:sidebar-open';
2022-10-11 20:38:09 +00:00
// Subscribe to delete events
const deleteHandler = ({ fileid }) => {
const photo = this.day.detail.find(p => p.fileid === fileid);
this.emitDelete([photo]);
};
subscribe('files:file:deleted', deleteHandler);
2022-09-13 01:33:24 +00:00
// Open viewer
globalThis.OCA.Viewer.open({
path: photo.filename, // path
list: fileInfos, // file list
canLoop: false, // don't loop
onClose: () => { // on viewer close
if (globalThis.OCA.Files.Sidebar.file) {
localStorage.setItem(SIDEBAR_KEY, '1');
} else {
localStorage.removeItem(SIDEBAR_KEY);
2022-08-20 07:11:51 +00:00
}
2022-09-13 01:33:24 +00:00
globalThis.OCA.Files.Sidebar.close();
2022-10-11 20:38:09 +00:00
unsubscribe('files:file:deleted', deleteHandler);
2022-09-13 01:33:24 +00:00
},
});
2022-08-20 07:11:51 +00:00
2022-09-13 01:33:24 +00:00
// Restore sidebar state
if (localStorage.getItem(SIDEBAR_KEY) === '1') {
globalThis.OCA.Files.Sidebar.open(photo.filename);
}
}
2022-08-20 07:11:51 +00:00
2022-09-13 01:33:24 +00:00
toggleSelect() {
2022-09-13 02:36:27 +00:00
if (this.data.flag & this.c.FLAG_PLACEHOLDER) {
2022-09-13 01:33:24 +00:00
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;
}
&.exit-left {
transition: all 0.2s ease-in;
transform: translateX(-20%);
opacity: 0.8;
2022-09-08 17:41:16 +00:00
}
2022-09-10 02:10:51 +00:00
&.enter-right {
animation: enter-right 0.2s ease-out forwards;
2022-09-08 17:41:16 +00:00
}
}
2022-09-10 02:10:51 +00:00
@keyframes enter-right {
from { transform: translateX(20%); opacity: 0.8; }
2022-09-10 02:10:51 +00:00
to { transform: translateX(0); opacity: 1; }
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 02:55:53 +00:00
.p-outer.p-loading > & { display: none; }
.p-outer.error & { object-fit: contain; }
2022-09-15 17:20:02 +00:00
}
}
2022-08-20 07:11:51 +00:00
</style>