memories/src/components/frame/Photo.vue

284 lines
7.8 KiB
Vue

<template>
<div class="p-outer fill-block"
:class="{
'selected': (data.flag & c.FLAG_SELECTED),
'p-loading': !(data.flag & c.FLAG_LOADED),
'leaving': (data.flag & c.FLAG_LEAVING),
'exit-left': (data.flag & c.FLAG_EXIT_LEFT),
'enter-right': (data.flag & c.FLAG_ENTER_RIGHT),
'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="click"
@contextmenu="contextmenu"
@touchstart="touchstart"
@touchmove="touchend"
@touchend="touchend"
@touchcancel="touchend" >
<img
class="fill-block"
:src="src()"
:key="data.fileid"
@error="error"
@load="load" />
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Emit, Mixins } from 'vue-property-decorator';
import { IDay, IPhoto } from "../../types";
import { subscribe, unsubscribe } from '@nextcloud/event-bus';
import { showError } from '@nextcloud/dialogs'
import { getPreviewUrl } from "../../services/FileUtils";
import * as dav from "../../services/DavRequests";
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;
@Prop() data: IPhoto;
@Prop() day: IDay;
@Emit('delete') emitDelete(remPhotos: IPhoto[]) {}
@Emit('select') emitSelect(data: IPhoto) {}
@Emit('clickImg') emitClickImg(component: any) {}
/** Get src for image to show */
src() {
if (this.data.flag & this.c.FLAG_PLACEHOLDER) {
return undefined;
} else if (this.data.flag & this.c.FLAG_LOAD_FAIL) {
return errorsvg;
} else if (this.data.flag & this.c.FLAG_FORCE_RELOAD) {
this.data.flag &= ~this.c.FLAG_FORCE_RELOAD;
return undefined;
} else {
return this.url;
}
}
/** Get url of the photo */
get url() {
return getPreviewUrl(this.data.fileid, this.data.etag, false, 512)
}
/** Image loaded successfully */
load() {
this.data.flag |= this.c.FLAG_LOADED;
}
/** Error in loading image */
error(e: any) {
this.data.flag |= (this.c.FLAG_LOADED | this.c.FLAG_LOAD_FAIL);
}
/** Clear timers */
beforeUnmount() {
clearTimeout(this.touchTimer);
}
/** Pass to parent */
click() {
this.emitClickImg(this);
}
/** Open viewer */
async openFile() {
// Check if this is a placeholder
if (this.data.flag & this.c.FLAG_PLACEHOLDER) {
return;
}
// 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);
}
if (fileInfos.length === 0) {
return;
}
// 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));
}
// Get this photo in the fileInfos
const photo = fileInfos.find(d => Number(d.fileid) === Number(this.data.fileid));
if (!photo) {
showError(this.t('memories', 'Cannot find this photo anymore!'));
return;
}
// Key to store sidebar state
const SIDEBAR_KEY = 'memories:sidebar-open';
// 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);
// 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);
}
globalThis.OCA.Files.Sidebar.close();
unsubscribe('files:file:deleted', deleteHandler);
},
});
// Restore sidebar state
if (localStorage.getItem(SIDEBAR_KEY) === '1') {
globalThis.OCA.Files.Sidebar.open(photo.filename);
}
}
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;
}
&.exit-left {
transition: all 0.2s ease-in;
transform: translateX(-20%);
opacity: 0.8;
}
&.enter-right {
animation: enter-right 0.2s ease-out forwards;
}
}
@keyframes enter-right {
from { transform: translateX(20%); opacity: 0.8; }
to { transform: translateX(0); opacity: 1; }
}
/* Extra icons */
.check-icon.select {
position: absolute;
top: 6%; left: 6%;
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: 6%; right: 6%;
}
.star-icon {
bottom: 6%; left: 6%;
}
/* Actual image */
div.img-outer {
padding: 2px;
@media (max-width: 768px) { padding: 1px; }
transition: transform 0.1s ease;
background-clip: content-box, padding-box;
background-color: var(--color-background-dark);
.selected > & { transform: scale(0.9); }
> img {
background-clip: content-box;
object-fit: cover;
cursor: pointer;
transform: translateZ(0);
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
user-select: none;
.selected > & { box-shadow: 0 0 3px 2px var(--color-primary); }
.p-outer.p-loading > & { display: none; }
.p-outer.error & { object-fit: contain; }
}
}
</style>