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>
|
<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) {
|
||||||
|
switch (item.cluster_type) {
|
||||||
|
case 'plus':
|
||||||
|
this.$emit('plus');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
this.$emit('click', item);
|
this.$emit('click', item);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
resize() {
|
resize() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
Loading…
Reference in New Issue