Allow moving faces to other cluster (fix #78)

old-stable24
Varun Patil 2022-10-18 18:07:00 -07:00
parent 479ed696cf
commit 452e9d0d69
2 changed files with 138 additions and 4 deletions

View File

@ -24,7 +24,9 @@
</NcActions>
</div>
<!-- Selection Modals -->
<EditDate ref="editDate" @refresh="refresh" />
<FaceMoveModal ref="faceMoveModal" @moved="deletePhotos" :updateLoading="updateLoading" />
</div>
</template>
@ -39,6 +41,7 @@ import { IHeadRow, IPhoto, ISelectionAction } from '../types';
import * as dav from "../services/DavRequests";
import EditDate from "./modal/EditDate.vue"
import FaceMoveModal from "./modal/FaceMoveModal.vue"
import StarIcon from 'vue-material-design-icons/Star.vue';
import DownloadIcon from 'vue-material-design-icons/Download.vue';
@ -48,6 +51,7 @@ import ArchiveIcon from 'vue-material-design-icons/PackageDown.vue';
import UnarchiveIcon from 'vue-material-design-icons/PackageUp.vue';
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue';
import CloseIcon from 'vue-material-design-icons/Close.vue';
import MoveIcon from 'vue-material-design-icons/ImageMove.vue';
type Selection = Map<number, IPhoto>;
@ -56,6 +60,7 @@ type Selection = Map<number, IPhoto>;
NcActions,
NcActionButton,
EditDate,
FaceMoveModal,
CloseIcon,
},
@ -70,7 +75,7 @@ export default class SelectionHandler extends Mixins(GlobalMixin) {
refresh() {}
@Emit('delete')
delete(photos: IPhoto[]) {}
deletePhotos(photos: IPhoto[]) {}
@Emit('updateLoading')
updateLoading(delta: number) {}
@ -118,6 +123,12 @@ export default class SelectionHandler extends Mixins(GlobalMixin) {
callback: this.viewInFolder.bind(this),
if: () => this.selection.size === 1,
},
{
name: t('memories', 'Move to another person'),
icon: MoveIcon,
callback: this.moveSelectionToPerson.bind(this),
if: () => this.$route.name === 'people',
},
{
name: t('memories', 'Remove from person'),
icon: CloseIcon,
@ -263,7 +274,7 @@ export default class SelectionHandler extends Mixins(GlobalMixin) {
for await (const delIds of dav.deleteFilesByIds(Array.from(selection.keys()))) {
const delPhotos = delIds.map(id => selection.get(id));
this.delete(delPhotos);
this.deletePhotos(delPhotos);
}
}
@ -307,7 +318,7 @@ export default class SelectionHandler extends Mixins(GlobalMixin) {
continue
}
const delPhotos = delIds.map(id => selection.get(id));
this.delete(delPhotos);
this.deletePhotos(delPhotos);
}
}
@ -321,6 +332,13 @@ export default class SelectionHandler extends Mixins(GlobalMixin) {
return this.$route.name === 'archive';
}
/**
* Move selected photos to another person
*/
private async moveSelectionToPerson(selection: Selection) {
(<any>this.$refs.faceMoveModal).open(Array.from(selection.values()));
}
/**
* Remove currently selected photos from person
*/
@ -334,7 +352,7 @@ export default class SelectionHandler extends Mixins(GlobalMixin) {
// Run query
for await (let delIds of dav.removeFaceImages(user, name, Array.from(selection.keys()))) {
const delPhotos = delIds.filter(x => x).map(id => selection.get(id));
this.delete(delPhotos);
this.deletePhotos(delPhotos);
}
}
}

View File

@ -0,0 +1,116 @@
<template>
<Modal @close="close" size="large" v-if="isOpen">
<template #title>
{{ t('memories', 'Move selected photos to person') }}
</template>
<div class="outer">
<FaceList @select="clickFace" />
</div>
<template #buttons>
<NcButton @click="close" class="button" type="error">
{{ t('memories', 'Cancel') }}
</NcButton>
</template>
</Modal>
</template>
<script lang="ts">
import { Component, Emit, Mixins, Prop } from 'vue-property-decorator';
import { NcButton, NcTextField } from '@nextcloud/vue';
import { showError } from '@nextcloud/dialogs'
import { IPhoto, ITag } from '../../types';
import Tag from '../frame/Tag.vue';
import FaceList from './FaceList.vue';
import Modal from './Modal.vue';
import GlobalMixin from '../../mixins/GlobalMixin';
import client from '../../services/DavClient';
import * as dav from '../../services/DavRequests';
@Component({
components: {
NcButton,
NcTextField,
Modal,
Tag,
FaceList,
}
})
export default class FaceMoveModal extends Mixins(GlobalMixin) {
private isOpen = false;
private photos: IPhoto[] = [];
@Prop()
private updateLoading: (delta: number) => void;
public open(photos: IPhoto[]) {
if (this.photos.length) {
// is processing
return;
}
this.isOpen = true;
this.photos = photos;
}
@Emit('close')
public close() {
this.photos = [];
this.isOpen = false;
}
@Emit('moved')
public moved(list: IPhoto[]) {}
public async clickFace(face: ITag) {
const user = this.$route.params.user || '';
const name = this.$route.params.name || '';
const newName = face.name || face.fileid.toString();
try {
this.isOpen = false;
this.updateLoading(1);
// Create map to return IPhoto later
let photoMap = new Map<number, IPhoto>();
for (const photo of this.photos) {
photoMap.set(photo.fileid, photo);
}
let data = await dav.getFiles(this.photos.map(p => p.fileid));
// Create move calls
const calls = data.map((p) => async () => {
try {
await client.moveFile(
`/recognize/${user}/faces/${name}/${p.fileid}-${p.basename}`,
`/recognize/${face.user_id}/faces/${newName}/${p.fileid}-${p.basename}`
)
return photoMap.get(p.fileid);
} catch (e) {
console.error(e);
showError(this.t('memories', 'Error while moving {basename}', p));
}
});
for await (const resp of dav.runInParallel(calls, 10)) {
this.moved(resp);
}
} catch (error) {
console.error(error);
showError(this.t('photos', 'Failed to move {name}.', { name }));
} finally {
this.updateLoading(-1);
this.close();
}
}
}
</script>
<style lang="scss" scoped>
.outer {
margin-top: 15px;
}
</style>