recognize: allow creating new cluster (fix #117)
Signed-off-by: Varun Patil <radialapps@gmail.com>pull/653/head
parent
a9f2f0f5ad
commit
df0c8d590f
|
@ -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 |
|
@ -1,15 +1,16 @@
|
|||
<template>
|
||||
<RecycleScroller
|
||||
ref="recycler"
|
||||
type-field="cluster_type"
|
||||
key-field="cluster_id"
|
||||
class="grid-recycler hide-scrollbar-mobile"
|
||||
:class="{ empty: !items.length }"
|
||||
:items="items"
|
||||
:items="clusters"
|
||||
:skipHover="true"
|
||||
:buffer="400"
|
||||
:itemSize="itemSize"
|
||||
:gridItems="gridItems"
|
||||
:updateInterval="100"
|
||||
key-field="cluster_id"
|
||||
@resize="resize"
|
||||
>
|
||||
<template v-slot="{ item }">
|
||||
|
@ -45,6 +46,10 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
plus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
|
@ -56,9 +61,33 @@ export default defineComponent({
|
|||
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: {
|
||||
click(item: ICluster) {
|
||||
this.$emit('click', item);
|
||||
switch (item.cluster_type) {
|
||||
case 'plus':
|
||||
this.$emit('plus');
|
||||
break;
|
||||
default:
|
||||
this.$emit('click', item);
|
||||
}
|
||||
},
|
||||
|
||||
resize() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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>
|
||||
</div>
|
||||
<div class="name">
|
||||
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
|
||||
<div class="previews fill-block" ref="previews">
|
||||
<div class="img-outer">
|
||||
<div class="img-outer" :class="{ plus }">
|
||||
<XImg
|
||||
draggable="false"
|
||||
class="fill-block"
|
||||
|
@ -33,6 +33,7 @@ import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble';
|
|||
import type { IAlbum, ICluster, IFace, IPhoto } from '../../types';
|
||||
import { getPreviewUrl } from '../../services/utils/helpers';
|
||||
import errorsvg from '../../assets/error.svg';
|
||||
import plussvg from '../../assets/plus.svg';
|
||||
|
||||
import { API } from '../../services/API';
|
||||
|
||||
|
@ -58,6 +59,7 @@ export default defineComponent({
|
|||
computed: {
|
||||
previewUrl() {
|
||||
if (this.error) return errorsvg;
|
||||
if (this.plus) return plussvg;
|
||||
|
||||
if (this.album) {
|
||||
const mock = {
|
||||
|
@ -87,6 +89,10 @@ export default defineComponent({
|
|||
return '';
|
||||
},
|
||||
|
||||
plus() {
|
||||
return this.data.cluster_type === 'plus';
|
||||
},
|
||||
|
||||
tag() {
|
||||
return this.data.cluster_type === 'tags' && this.data;
|
||||
},
|
||||
|
@ -213,6 +219,11 @@ img {
|
|||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
&.plus {
|
||||
background-color: var(--color-primary-element-light);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
> img {
|
||||
object-fit: cover;
|
||||
padding: 0;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<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>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -46,6 +50,15 @@ export default defineComponent({
|
|||
this.freeBlob();
|
||||
},
|
||||
|
||||
computed: {
|
||||
svg() {
|
||||
if (this.dataSrc.startsWith('data:image/svg+xml')) {
|
||||
return window.atob(this.dataSrc.split(',')[1]);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadImage() {
|
||||
if (!this.src) return;
|
||||
|
@ -94,3 +107,10 @@ export default defineComponent({
|
|||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
div.svg > :deep svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,7 +11,15 @@
|
|||
</NcTextField>
|
||||
</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>
|
||||
{{ t('memories', 'Loading …') }}
|
||||
</div>
|
||||
|
@ -23,6 +31,7 @@ import { defineComponent } from 'vue';
|
|||
import { ICluster, IFace } from '../../types';
|
||||
import ClusterGrid from '../ClusterGrid.vue';
|
||||
|
||||
import { showError } from '@nextcloud/dialogs';
|
||||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField';
|
||||
|
||||
import * as dav from '../../services/DavRequests';
|
||||
|
@ -38,6 +47,13 @@ export default defineComponent({
|
|||
Magnify,
|
||||
},
|
||||
|
||||
props: {
|
||||
plus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
user: '',
|
||||
name: '',
|
||||
|
@ -82,7 +98,37 @@ export default defineComponent({
|
|||
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);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</template>
|
||||
|
||||
<div class="outer">
|
||||
<FaceList @select="clickFace" />
|
||||
<FaceList :plus="true" @select="clickFace" />
|
||||
</div>
|
||||
|
||||
<template #buttons>
|
||||
|
@ -29,7 +29,6 @@ import Cluster from '../frame/Cluster.vue';
|
|||
import FaceList from './FaceList.vue';
|
||||
|
||||
import Modal from './Modal.vue';
|
||||
import client from '../../services/DavClient';
|
||||
import * as dav from '../../services/DavRequests';
|
||||
|
||||
export default defineComponent({
|
||||
|
|
|
@ -120,5 +120,12 @@ export async function recognizeDeleteFace(user: string, name: string) {
|
|||
* @param target Target name of face
|
||||
*/
|
||||
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}`);
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@ export interface IFolder extends IPhoto {
|
|||
name: string;
|
||||
}
|
||||
|
||||
export type ClusterTypes = 'tags' | 'albums' | 'places' | 'recognize' | 'facerecognition';
|
||||
export type ClusterTypes = 'tags' | 'albums' | 'places' | 'recognize' | 'facerecognition' | 'plus';
|
||||
|
||||
export interface ICluster {
|
||||
/** A unique identifier for the cluster */
|
||||
|
|
Loading…
Reference in New Issue