recognize: allow creating new cluster (fix #117)

Signed-off-by: Varun Patil <radialapps@gmail.com>
pull/653/head
Varun Patil 2023-04-29 00:12:34 -07:00
parent a9f2f0f5ad
commit df0c8d590f
8 changed files with 162 additions and 12 deletions

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="svg2"
viewBox="0 0 300 300"
height="300"
width="300"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(-399.13437,-122.79051)"
id="layer1">
<path
style="fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="m 549.13437,232.79051 v 80"
id="path976" />
<path
style="fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="m 508.33033,270.67589 80,4.3e-4"
id="path1648" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,15 +1,16 @@
<template> <template>
<RecycleScroller <RecycleScroller
ref="recycler" ref="recycler"
type-field="cluster_type"
key-field="cluster_id"
class="grid-recycler hide-scrollbar-mobile" class="grid-recycler hide-scrollbar-mobile"
:class="{ empty: !items.length }" :class="{ empty: !items.length }"
:items="items" :items="clusters"
:skipHover="true" :skipHover="true"
:buffer="400" :buffer="400"
:itemSize="itemSize" :itemSize="itemSize"
:gridItems="gridItems" :gridItems="gridItems"
:updateInterval="100" :updateInterval="100"
key-field="cluster_id"
@resize="resize" @resize="resize"
> >
<template v-slot="{ item }"> <template v-slot="{ item }">
@ -45,6 +46,10 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
plus: {
type: Boolean,
default: false,
},
}, },
data: () => ({ data: () => ({
@ -56,9 +61,33 @@ export default defineComponent({
this.resize(); this.resize();
}, },
computed: {
clusters() {
const items = [...this.items];
// Add plus button if required
if (this.plus) {
items.unshift({
cluster_type: 'plus',
cluster_id: -1,
name: '',
count: 0,
});
}
return items;
},
},
methods: { methods: {
click(item: ICluster) { click(item: ICluster) {
this.$emit('click', item); switch (item.cluster_type) {
case 'plus':
this.$emit('plus');
break;
default:
this.$emit('click', item);
}
}, },
resize() { resize() {

View File

@ -1,6 +1,6 @@
<template> <template>
<router-link draggable="false" class="cluster fill-block" :class="{ error }" :to="target" @click.native="click"> <router-link draggable="false" class="cluster fill-block" :class="{ error }" :to="target" @click.native="click">
<div class="bbl"> <div class="bbl" v-if="data.count">
<NcCounterBubble> {{ data.count }} </NcCounterBubble> <NcCounterBubble> {{ data.count }} </NcCounterBubble>
</div> </div>
<div class="name"> <div class="name">
@ -9,7 +9,7 @@
</div> </div>
<div class="previews fill-block" ref="previews"> <div class="previews fill-block" ref="previews">
<div class="img-outer"> <div class="img-outer" :class="{ plus }">
<XImg <XImg
draggable="false" draggable="false"
class="fill-block" class="fill-block"
@ -33,6 +33,7 @@ import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble';
import type { IAlbum, ICluster, IFace, IPhoto } from '../../types'; import type { IAlbum, ICluster, IFace, IPhoto } from '../../types';
import { getPreviewUrl } from '../../services/utils/helpers'; import { getPreviewUrl } from '../../services/utils/helpers';
import errorsvg from '../../assets/error.svg'; import errorsvg from '../../assets/error.svg';
import plussvg from '../../assets/plus.svg';
import { API } from '../../services/API'; import { API } from '../../services/API';
@ -58,6 +59,7 @@ export default defineComponent({
computed: { computed: {
previewUrl() { previewUrl() {
if (this.error) return errorsvg; if (this.error) return errorsvg;
if (this.plus) return plussvg;
if (this.album) { if (this.album) {
const mock = { const mock = {
@ -87,6 +89,10 @@ export default defineComponent({
return ''; return '';
}, },
plus() {
return this.data.cluster_type === 'plus';
},
tag() { tag() {
return this.data.cluster_type === 'tags' && this.data; return this.data.cluster_type === 'tags' && this.data;
}, },
@ -213,6 +219,11 @@ img {
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
&.plus {
background-color: var(--color-primary-element-light);
color: var(--color-primary);
}
> img { > img {
object-fit: cover; object-fit: cover;
padding: 0; padding: 0;

View File

@ -1,5 +1,9 @@
<template> <template>
<img :alt="alt" :src="dataSrc" @load="load" decoding="async" /> <!-- Directly use SVG element if possible -->
<div class="svg" v-if="svg" v-html="svg" />
<!-- Otherwise use img element -->
<img v-else :alt="alt" :src="dataSrc" @load="load" decoding="async" />
</template> </template>
<script lang="ts"> <script lang="ts">
@ -46,6 +50,15 @@ export default defineComponent({
this.freeBlob(); this.freeBlob();
}, },
computed: {
svg() {
if (this.dataSrc.startsWith('data:image/svg+xml')) {
return window.atob(this.dataSrc.split(',')[1]);
}
return null;
},
},
methods: { methods: {
async loadImage() { async loadImage() {
if (!this.src) return; if (!this.src) return;
@ -94,3 +107,10 @@ export default defineComponent({
}, },
}); });
</script> </script>
<style lang="scss" scoped>
div.svg > :deep svg {
width: 100%;
height: 100%;
}
</style>

View File

@ -11,7 +11,15 @@
</NcTextField> </NcTextField>
</div> </div>
<ClusterGrid v-if="list" :items="filteredList" :link="false" :maxSize="120" @click="click" /> <ClusterGrid
v-if="list"
:items="filteredList"
:maxSize="120"
:link="false"
:plus="plus"
@click="click"
@plus="addFace"
/>
<div v-else> <div v-else>
{{ t('memories', 'Loading …') }} {{ t('memories', 'Loading …') }}
</div> </div>
@ -23,6 +31,7 @@ import { defineComponent } from 'vue';
import { ICluster, IFace } from '../../types'; import { ICluster, IFace } from '../../types';
import ClusterGrid from '../ClusterGrid.vue'; import ClusterGrid from '../ClusterGrid.vue';
import { showError } from '@nextcloud/dialogs';
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField'; import NcTextField from '@nextcloud/vue/dist/Components/NcTextField';
import * as dav from '../../services/DavRequests'; import * as dav from '../../services/DavRequests';
@ -38,6 +47,13 @@ export default defineComponent({
Magnify, Magnify,
}, },
props: {
plus: {
type: Boolean,
default: false,
},
},
data: () => ({ data: () => ({
user: '', user: '',
name: '', name: '',
@ -82,7 +98,37 @@ export default defineComponent({
this.fuse = new Fuse(this.list, { keys: ['name'] }); this.fuse = new Fuse(this.list, { keys: ['name'] });
}, },
async click(face: IFace) { async addFace() {
let name: string = '';
try {
// TODO: use a proper dialog
name = window.prompt(this.t('memories', 'Enter name of the new face'), '') ?? '';
if (!name) return;
// Create new directory in WebDAV
await dav.recognizeCreateFace(this.user, name);
return this.selectNew(name);
} catch (e) {
// Directory already exists
if (e.status === 405) return this.selectNew(name);
showError(this.t('memories', 'Failed to create face'));
}
},
selectNew(name: string) {
this.$emit('select', {
cluster_id: name,
cluster_type: 'recognize',
count: 0,
name: name,
user_id: this.user,
});
},
click(face: IFace) {
this.$emit('select', face); this.$emit('select', face);
}, },
}, },

View File

@ -5,7 +5,7 @@
</template> </template>
<div class="outer"> <div class="outer">
<FaceList @select="clickFace" /> <FaceList :plus="true" @select="clickFace" />
</div> </div>
<template #buttons> <template #buttons>
@ -29,7 +29,6 @@ import Cluster from '../frame/Cluster.vue';
import FaceList from './FaceList.vue'; import FaceList from './FaceList.vue';
import Modal from './Modal.vue'; import Modal from './Modal.vue';
import client from '../../services/DavClient';
import * as dav from '../../services/DavRequests'; import * as dav from '../../services/DavRequests';
export default defineComponent({ export default defineComponent({

View File

@ -120,5 +120,12 @@ export async function recognizeDeleteFace(user: string, name: string) {
* @param target Target name of face * @param target Target name of face
*/ */
export async function recognizeRenameFace(user: string, name: string, target: string) { export async function recognizeRenameFace(user: string, name: string, target: string) {
await client.moveFile(`/recognize/${user}/faces/${name}`, `/recognize/${user}/faces/${target}`); return await client.moveFile(`/recognize/${user}/faces/${name}`, `/recognize/${user}/faces/${target}`);
}
/**
* Create a new face in recognize.
*/
export async function recognizeCreateFace(user: string, name: string) {
return await client.createDirectory(`/recognize/${user}/faces/${name}`);
} }

View File

@ -109,7 +109,7 @@ export interface IFolder extends IPhoto {
name: string; name: string;
} }
export type ClusterTypes = 'tags' | 'albums' | 'places' | 'recognize' | 'facerecognition'; export type ClusterTypes = 'tags' | 'albums' | 'places' | 'recognize' | 'facerecognition' | 'plus';
export interface ICluster { export interface ICluster {
/** A unique identifier for the cluster */ /** A unique identifier for the cluster */