Add face merging (fix #65)
parent
dec1f489da
commit
7f8b818056
|
@ -0,0 +1,162 @@
|
||||||
|
<template>
|
||||||
|
<Modal @close="close" size="large">
|
||||||
|
<template #title>
|
||||||
|
{{ t('memories', 'Merge {name} with person', { name }) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="outer">
|
||||||
|
<div class="photo" v-for="photo of detail" :key="photo.fileid" >
|
||||||
|
<Tag :data="photo" :rowHeight="115" :noNavigate="true" @open="clickFace" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="procesingTotal > 0" class="info-pad">
|
||||||
|
{{ t('memories', 'Processing … {n}/{m}', {
|
||||||
|
n: processing,
|
||||||
|
m: procesingTotal,
|
||||||
|
}) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #buttons>
|
||||||
|
<NcButton @click="close" class="button" type="error">
|
||||||
|
{{ t('memories', 'Cancel') }}
|
||||||
|
</NcButton>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Mixins, Watch } from 'vue-property-decorator';
|
||||||
|
import { NcButton, NcTextField } from '@nextcloud/vue';
|
||||||
|
import { showError } from '@nextcloud/dialogs'
|
||||||
|
import { IFileInfo, IPhoto, ITag } from '../types';
|
||||||
|
import Tag from './Tag.vue';
|
||||||
|
|
||||||
|
import Modal from './Modal.vue';
|
||||||
|
import GlobalMixin from '../mixins/GlobalMixin';
|
||||||
|
import * as dav from '../services/DavRequests';
|
||||||
|
import client from '../services/DavClient';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
NcButton,
|
||||||
|
NcTextField,
|
||||||
|
Modal,
|
||||||
|
Tag,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class FaceMergeModal extends Mixins(GlobalMixin) {
|
||||||
|
private user: string = "";
|
||||||
|
private name: string = "";
|
||||||
|
private detail: IPhoto[] = [];
|
||||||
|
private processing = 0;
|
||||||
|
private procesingTotal = 0;
|
||||||
|
|
||||||
|
@Watch('$route')
|
||||||
|
async routeChange(from: any, to: any) {
|
||||||
|
this.refreshParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.refreshParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async refreshParams() {
|
||||||
|
this.user = this.$route.params.user || '';
|
||||||
|
this.name = this.$route.params.name || '';
|
||||||
|
this.detail = [];
|
||||||
|
this.processing = 0;
|
||||||
|
this.procesingTotal = 0;
|
||||||
|
|
||||||
|
const data = await dav.getPeopleData();
|
||||||
|
let detail = data[0].detail;
|
||||||
|
detail.forEach((photo: IPhoto) => {
|
||||||
|
photo.flag = this.c.FLAG_IS_FACE | this.c.FLAG_IS_TAG;
|
||||||
|
});
|
||||||
|
detail = detail.filter((photo: ITag) => {
|
||||||
|
const pname = photo.name || photo.fileid.toString();
|
||||||
|
return photo.user_id !== this.user || pname !== this.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.detail = detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async clickFace(face: ITag) {
|
||||||
|
const newName = face.name || face.fileid.toString();
|
||||||
|
if (!confirm(this.t('memories', 'Are you sure you want to merge {name} with {newName}?', { name: this.name, newName}))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all files for current face
|
||||||
|
let res = await client.getDirectoryContents(
|
||||||
|
`/recognize/${this.user}/faces/${this.name}`, { details: true }
|
||||||
|
) as any;
|
||||||
|
let data: IFileInfo[] = res.data;
|
||||||
|
this.procesingTotal = data.length;
|
||||||
|
|
||||||
|
// Don't try too much
|
||||||
|
let failures = 0;
|
||||||
|
|
||||||
|
// Create move calls
|
||||||
|
const calls = data.map((p) => async () => {
|
||||||
|
// Short circuit if we have too many failures
|
||||||
|
if (failures === 10) {
|
||||||
|
showError(this.t('memories', 'Too many failures, aborting'));
|
||||||
|
failures++;
|
||||||
|
}
|
||||||
|
if (failures >= 10) return;
|
||||||
|
|
||||||
|
// Move to new face with webdav
|
||||||
|
try {
|
||||||
|
await client.moveFile(
|
||||||
|
`/recognize/${this.user}/faces/${this.name}/${p.basename}`,
|
||||||
|
`/recognize/${face.user_id}/faces/${newName}/${p.basename}`
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showError(this.t('memories', 'Error while moving {basename}', p));
|
||||||
|
failures++;
|
||||||
|
} finally {
|
||||||
|
this.processing++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for await (const _ of dav.runInParallel(calls, 10)) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go to new face
|
||||||
|
if (failures === 0) {
|
||||||
|
this.$router.push({ name: 'people', params: { user: face.user_id, name: newName } });
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
showError(this.t('photos', 'Failed to move {name}.', {
|
||||||
|
name: this.name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public close() {
|
||||||
|
this.$emit('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.outer {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.photo {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.info-pad {
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -11,11 +11,15 @@
|
||||||
|
|
||||||
<div class="right-actions">
|
<div class="right-actions">
|
||||||
<NcActions :inline="1">
|
<NcActions :inline="1">
|
||||||
<NcActionButton :aria-label="t('memories', 'Rename person')" @click="showEditModal=true">
|
<NcActionButton :aria-label="t('memories', 'Rename person')" @click="showEditModal=true" close-after-click>
|
||||||
{{ t('memories', 'Rename person') }}
|
{{ t('memories', 'Rename person') }}
|
||||||
<template #icon> <EditIcon :size="20" /> </template>
|
<template #icon> <EditIcon :size="20" /> </template>
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
<NcActionButton :aria-label="t('memories', 'Remove person')" @click="showDeleteModal=true">
|
<NcActionButton :aria-label="t('memories', 'Merge with different person')" @click="showMergeModal=true" close-after-click>
|
||||||
|
{{ t('memories', 'Merge with different person') }}
|
||||||
|
<template #icon> <MergeIcon :size="20" /> </template>
|
||||||
|
</NcActionButton>
|
||||||
|
<NcActionButton :aria-label="t('memories', 'Remove person')" @click="showDeleteModal=true" close-after-click>
|
||||||
{{ t('memories', 'Remove person') }}
|
{{ t('memories', 'Remove person') }}
|
||||||
<template #icon> <DeleteIcon :size="20" /> </template>
|
<template #icon> <DeleteIcon :size="20" /> </template>
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
|
@ -24,6 +28,7 @@
|
||||||
|
|
||||||
<FaceEditModal v-if="showEditModal" @close="showEditModal=false" />
|
<FaceEditModal v-if="showEditModal" @close="showEditModal=false" />
|
||||||
<FaceDeleteModal v-if="showDeleteModal" @close="showDeleteModal=false" />
|
<FaceDeleteModal v-if="showDeleteModal" @close="showDeleteModal=false" />
|
||||||
|
<FaceMergeModal v-if="showMergeModal" @close="showMergeModal=false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -34,9 +39,11 @@ import GlobalMixin from '../mixins/GlobalMixin';
|
||||||
import { NcActions, NcActionButton } from '@nextcloud/vue';
|
import { NcActions, NcActionButton } from '@nextcloud/vue';
|
||||||
import FaceEditModal from './FaceEditModal.vue';
|
import FaceEditModal from './FaceEditModal.vue';
|
||||||
import FaceDeleteModal from './FaceDeleteModal.vue';
|
import FaceDeleteModal from './FaceDeleteModal.vue';
|
||||||
|
import FaceMergeModal from './FaceMergeModal.vue';
|
||||||
import BackIcon from 'vue-material-design-icons/ArrowLeft.vue';
|
import BackIcon from 'vue-material-design-icons/ArrowLeft.vue';
|
||||||
import EditIcon from 'vue-material-design-icons/Pencil.vue';
|
import EditIcon from 'vue-material-design-icons/Pencil.vue';
|
||||||
import DeleteIcon from 'vue-material-design-icons/Close.vue';
|
import DeleteIcon from 'vue-material-design-icons/Close.vue';
|
||||||
|
import MergeIcon from 'vue-material-design-icons/Merge.vue';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
@ -44,15 +51,18 @@ import DeleteIcon from 'vue-material-design-icons/Close.vue';
|
||||||
NcActionButton,
|
NcActionButton,
|
||||||
FaceEditModal,
|
FaceEditModal,
|
||||||
FaceDeleteModal,
|
FaceDeleteModal,
|
||||||
|
FaceMergeModal,
|
||||||
BackIcon,
|
BackIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
|
MergeIcon,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class FaceTopMatter extends Mixins(GlobalMixin) {
|
export default class FaceTopMatter extends Mixins(GlobalMixin) {
|
||||||
private name: string = '';
|
private name: string = '';
|
||||||
private showEditModal: boolean = false;
|
private showEditModal: boolean = false;
|
||||||
private showDeleteModal: boolean = false;
|
private showDeleteModal: boolean = false;
|
||||||
|
private showMergeModal: boolean = false;
|
||||||
|
|
||||||
@Watch('$route')
|
@Watch('$route')
|
||||||
async routeChange(from: any, to: any) {
|
async routeChange(from: any, to: any) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<NcModal
|
<NcModal
|
||||||
size="small"
|
:size="size"
|
||||||
@close="$emit('close')"
|
@close="$emit('close')"
|
||||||
:outTransition="true">
|
:outTransition="true">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
import { NcModal } from '@nextcloud/vue';
|
import { NcModal } from '@nextcloud/vue';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -27,6 +27,7 @@ import { NcModal } from '@nextcloud/vue';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export default class Modal extends Vue {
|
export default class Modal extends Vue {
|
||||||
|
@Prop({default: 'small'}) private size?: string;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ interface IFaceDetection extends IPhoto {
|
||||||
export default class Tag extends Mixins(GlobalMixin) {
|
export default class Tag extends Mixins(GlobalMixin) {
|
||||||
@Prop() data: ITag;
|
@Prop() data: ITag;
|
||||||
@Prop() rowHeight: number;
|
@Prop() rowHeight: number;
|
||||||
|
@Prop() noNavigate: boolean;
|
||||||
|
|
||||||
// Separate property because the one on data isn't reactive
|
// Separate property because the one on data isn't reactive
|
||||||
private previews: IPhoto[] = [];
|
private previews: IPhoto[] = [];
|
||||||
|
@ -112,6 +113,11 @@ export default class Tag extends Mixins(GlobalMixin) {
|
||||||
|
|
||||||
/** Open tag */
|
/** Open tag */
|
||||||
openTag() {
|
openTag() {
|
||||||
|
this.$emit('open', this.data);
|
||||||
|
if (this.noNavigate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isFace) {
|
if (this.isFace) {
|
||||||
const name = this.data.name || this.data.fileid.toString();
|
const name = this.data.name || this.data.fileid.toString();
|
||||||
const user = this.data.user_id;
|
const user = this.data.user_id;
|
||||||
|
@ -178,7 +184,7 @@ export default class Tag extends Mixins(GlobalMixin) {
|
||||||
top: 50%; width: 100%;
|
top: 50%; width: 100%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
color: white;
|
color: white;
|
||||||
width: 100%;
|
width: 90%;
|
||||||
padding: 0 5%;
|
padding: 0 5%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
|
|
Loading…
Reference in New Issue