Implement unclustered photos for recognize (fix #475)
Signed-off-by: Varun Patil <radialapps@gmail.com>pull/743/head
parent
a87c29a2fa
commit
590d8272d5
|
@ -94,7 +94,7 @@ class RecognizeBackend extends Backend
|
|||
// Join with cluster
|
||||
$clusterQuery = null;
|
||||
if ('NULL' === $faceName) {
|
||||
$clusterQuery = $query->expr()->isNull('rfd.cluster_id');
|
||||
$clusterQuery = $query->expr()->eq('rfd.cluster_id', $query->expr()->literal(-1));
|
||||
} else {
|
||||
$nameField = is_numeric($faceName) ? 'rfc.id' : 'rfc.title';
|
||||
$query->innerJoin('m', 'recognize_face_clusters', 'rfc', $query->expr()->andX(
|
||||
|
|
|
@ -201,18 +201,25 @@ export default defineComponent({
|
|||
if: (self: any) => self.config.albums_enabled && !self.routeIsAlbums,
|
||||
},
|
||||
{
|
||||
id: 'face-move',
|
||||
name: t('memories', 'Move to person'),
|
||||
icon: MoveIcon,
|
||||
callback: this.moveSelectionToPerson.bind(this),
|
||||
if: () => this.$route.name === 'recognize',
|
||||
if: () => this.routeIsRecognize,
|
||||
},
|
||||
{
|
||||
name: t('memories', 'Remove from person'),
|
||||
icon: CloseIcon,
|
||||
callback: this.removeSelectionFromPerson.bind(this),
|
||||
if: () => this.$route.name === 'recognize',
|
||||
if: () => this.routeIsRecognize && !this.routeIsRecognizeUnassigned,
|
||||
},
|
||||
];
|
||||
|
||||
// Move face-move to start if unassigned faces
|
||||
if (this.routeIsRecognizeUnassigned) {
|
||||
const i = this.defaultActions.findIndex((a) => a.id === 'face-move');
|
||||
this.defaultActions.unshift(this.defaultActions.splice(i, 1)[0]);
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
|
@ -800,7 +807,7 @@ export default defineComponent({
|
|||
* Move selected photos to another person
|
||||
*/
|
||||
async moveSelectionToPerson(selection: Selection) {
|
||||
if (!this.config.show_face_rect) {
|
||||
if (!this.config.show_face_rect && !this.routeIsRecognizeUnassigned) {
|
||||
showError(this.t('memories', 'You must enable "Mark person in preview" to use this feature'));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -593,7 +593,7 @@ export default defineComponent({
|
|||
API.DAYS_FILTER(query, filter, `${user}/${name}`);
|
||||
|
||||
// Face rect
|
||||
if (this.config.show_face_rect) {
|
||||
if (this.config.show_face_rect || this.routeIsRecognizeUnassigned) {
|
||||
API.DAYS_FILTER(query, DaysFilterType.FACE_RECT);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,16 +90,7 @@ export default defineComponent({
|
|||
const name = this.$route.params.name || '';
|
||||
const target = String(face.name || face.cluster_id);
|
||||
|
||||
if (
|
||||
!confirm(
|
||||
this.t('memories', 'Are you sure you want to move the selected photos from {name} to {target}?', {
|
||||
name,
|
||||
target,
|
||||
})
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!confirm(this.t('memories', 'Move the selected photos to {target}?', { target }))) return;
|
||||
|
||||
try {
|
||||
this.show = false;
|
||||
|
|
|
@ -1,17 +1,27 @@
|
|||
<template>
|
||||
<div v-if="name" class="face-top-matter">
|
||||
<NcActions>
|
||||
<div class="face-top-matter">
|
||||
<NcActions v-if="name">
|
||||
<NcActionButton :aria-label="t('memories', 'Back')" @click="back()">
|
||||
{{ t('memories', 'Back') }}
|
||||
<template #icon> <BackIcon :size="20" /> </template>
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
|
||||
<div class="name">{{ name }}</div>
|
||||
<div class="name" :class="{ rename: isReal }" @click="rename">{{ displayName }}</div>
|
||||
|
||||
<div class="right-actions">
|
||||
<NcActions :inline="1">
|
||||
<NcActionButton :aria-label="t('memories', 'Rename person')" @click="$refs.editModal?.open()" close-after-click>
|
||||
<NcActions :inline="0">
|
||||
<!-- root view (not cluster or unassigned) -->
|
||||
<template v-if="!name && routeIsRecognize && !routeIsRecognizeUnassigned">
|
||||
<NcActionButton :aria-label="t('memories', 'Unassigned faces')" @click="openUnassigned" close-after-click>
|
||||
{{ t('memories', 'Unassigned faces') }}
|
||||
<template #icon> <UnassignedIcon :size="20" /> </template>
|
||||
</NcActionButton>
|
||||
</template>
|
||||
|
||||
<!-- real cluster -->
|
||||
<template v-if="isReal">
|
||||
<NcActionButton :aria-label="t('memories', 'Rename person')" @click="rename" close-after-click>
|
||||
{{ t('memories', 'Rename person') }}
|
||||
<template #icon> <EditIcon :size="20" /> </template>
|
||||
</NcActionButton>
|
||||
|
@ -38,6 +48,7 @@
|
|||
{{ t('memories', 'Remove person') }}
|
||||
<template #icon> <DeleteIcon :size="20" /> </template>
|
||||
</NcActionButton>
|
||||
</template>
|
||||
</NcActions>
|
||||
</div>
|
||||
|
||||
|
@ -56,6 +67,7 @@ import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
|
|||
import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox';
|
||||
|
||||
import { emit } from '@nextcloud/event-bus';
|
||||
import { getCurrentUser } from '@nextcloud/auth';
|
||||
|
||||
import FaceEditModal from '../modal/FaceEditModal.vue';
|
||||
import FaceDeleteModal from '../modal/FaceDeleteModal.vue';
|
||||
|
@ -64,6 +76,9 @@ import BackIcon from 'vue-material-design-icons/ArrowLeft.vue';
|
|||
import EditIcon from 'vue-material-design-icons/Pencil.vue';
|
||||
import DeleteIcon from 'vue-material-design-icons/Close.vue';
|
||||
import MergeIcon from 'vue-material-design-icons/Merge.vue';
|
||||
import UnassignedIcon from 'vue-material-design-icons/AccountQuestion.vue';
|
||||
|
||||
import * as utils from '../../services/Utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FaceTopMatter',
|
||||
|
@ -78,6 +93,7 @@ export default defineComponent({
|
|||
EditIcon,
|
||||
DeleteIcon,
|
||||
MergeIcon,
|
||||
UnassignedIcon,
|
||||
},
|
||||
|
||||
mixins: [UserConfig],
|
||||
|
@ -86,6 +102,19 @@ export default defineComponent({
|
|||
name() {
|
||||
return this.$route.params.name || '';
|
||||
},
|
||||
|
||||
isReal() {
|
||||
return this.name && this.name !== utils.constants.FACE_NULL;
|
||||
},
|
||||
|
||||
displayName() {
|
||||
if (this.routeIsRecognizeUnassigned) {
|
||||
return this.t('memories', 'Unassigned faces');
|
||||
} else if (!this.name) {
|
||||
return this.t('memories', 'People');
|
||||
}
|
||||
return this.name;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -93,6 +122,20 @@ export default defineComponent({
|
|||
this.$router.go(-1);
|
||||
},
|
||||
|
||||
rename() {
|
||||
if (this.name) (<any>this.$refs.editModal)?.open();
|
||||
},
|
||||
|
||||
openUnassigned() {
|
||||
this.$router.push({
|
||||
name: this.$route.name as string,
|
||||
params: {
|
||||
user: String(getCurrentUser()?.uid),
|
||||
name: utils.constants.FACE_NULL,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
changeShowFaceRect() {
|
||||
this.updateSetting('show_face_rect');
|
||||
emit('memories:timeline:hard-refresh', {});
|
||||
|
@ -100,3 +143,14 @@ export default defineComponent({
|
|||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.face-top-matter {
|
||||
.name.rename:hover {
|
||||
cursor: text;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--color-placeholder-light);
|
||||
text-underline-offset: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -20,8 +20,6 @@ import ClusterTopMatter from './ClusterTopMatter.vue';
|
|||
import FaceTopMatter from './FaceTopMatter.vue';
|
||||
import AlbumTopMatter from './AlbumTopMatter.vue';
|
||||
|
||||
import { TopMatterType } from '../../types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TopMatter',
|
||||
components: {
|
||||
|
@ -44,32 +42,17 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
computed: {
|
||||
type() {
|
||||
currentmatter() {
|
||||
switch (this.$route.name) {
|
||||
case 'folders':
|
||||
return TopMatterType.FOLDER;
|
||||
return FolderTopMatter;
|
||||
case 'albums':
|
||||
return TopMatterType.ALBUM;
|
||||
return AlbumTopMatter;
|
||||
case 'tags':
|
||||
case 'places':
|
||||
return TopMatterType.CLUSTER;
|
||||
return ClusterTopMatter;
|
||||
case 'recognize':
|
||||
case 'facerecognition':
|
||||
return this.$route.params.name ? TopMatterType.FACE : TopMatterType.CLUSTER;
|
||||
default:
|
||||
return TopMatterType.NONE;
|
||||
}
|
||||
},
|
||||
|
||||
currentmatter() {
|
||||
switch (this.type) {
|
||||
case TopMatterType.FOLDER:
|
||||
return FolderTopMatter;
|
||||
case TopMatterType.ALBUM:
|
||||
return AlbumTopMatter;
|
||||
case TopMatterType.CLUSTER:
|
||||
return ClusterTopMatter;
|
||||
case TopMatterType.FACE:
|
||||
return FaceTopMatter;
|
||||
default:
|
||||
return null;
|
||||
|
|
|
@ -25,6 +25,12 @@ export default defineComponent({
|
|||
routeIsPeople(): boolean {
|
||||
return ['recognize', 'facerecognition'].includes(<string>this.$route.name);
|
||||
},
|
||||
routeIsRecognize(): boolean {
|
||||
return this.$route.name === 'recognize';
|
||||
},
|
||||
routeIsRecognizeUnassigned(): boolean {
|
||||
return this.routeIsRecognize && this.$route.params.name === constants.FACE_NULL;
|
||||
},
|
||||
routeIsArchive(): boolean {
|
||||
return this.$route.name === 'archive';
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@ import { generateUrl } from '@nextcloud/router';
|
|||
import { IFace, IPhoto } from '../../types';
|
||||
import { API } from '../API';
|
||||
import client from '../DavClient';
|
||||
import * as utils from '../Utils';
|
||||
import * as base from './base';
|
||||
|
||||
export async function getFaceList(app: 'recognize' | 'facerecognition') {
|
||||
|
@ -83,10 +84,16 @@ export async function* recognizeMoveFaceImages(user: string, face: string, targe
|
|||
// Remove each file
|
||||
const calls = photos.map((p) => async () => {
|
||||
try {
|
||||
await client.moveFile(
|
||||
`/recognize/${user}/faces/${face}/${p.faceid}-${p.basename}`,
|
||||
`/recognize/${user}/faces/${target}/${p.faceid}-${p.basename}`
|
||||
);
|
||||
const dest = `/recognize/${user}/faces/${target}`;
|
||||
const name = `${p.faceid}-${p.basename}`;
|
||||
|
||||
// NULL source needs special handling
|
||||
let source = `/recognize/${user}/faces/${face}`;
|
||||
if (face === utils.constants.FACE_NULL) {
|
||||
source = `/recognize/${user}/unassigned-faces`;
|
||||
}
|
||||
|
||||
await client.moveFile(`${source}/${name}`, `${dest}/${name}`);
|
||||
return p.faceid!;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
|
@ -11,6 +11,8 @@ export const constants = {
|
|||
FLAG_LEAVING: 1 << 5,
|
||||
FLAG_IS_LOCAL: 1 << 6,
|
||||
},
|
||||
|
||||
FACE_NULL: 'NULL',
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
13
src/types.ts
13
src/types.ts
|
@ -208,18 +208,9 @@ export type ITick = {
|
|||
key?: number;
|
||||
};
|
||||
|
||||
export type TopMatter = {
|
||||
type: TopMatterType;
|
||||
};
|
||||
export enum TopMatterType {
|
||||
NONE = 0,
|
||||
FOLDER = 1,
|
||||
CLUSTER = 2,
|
||||
FACE = 3,
|
||||
ALBUM = 4,
|
||||
}
|
||||
|
||||
export type ISelectionAction = {
|
||||
/** Identifier (optional) */
|
||||
id?: string;
|
||||
/** Display text */
|
||||
name: string;
|
||||
/** Icon component */
|
||||
|
|
|
@ -15,6 +15,8 @@ declare module 'vue' {
|
|||
routeIsFolders: boolean;
|
||||
routeIsAlbums: boolean;
|
||||
routeIsPeople: boolean;
|
||||
routeIsRecognize: boolean;
|
||||
routeIsRecognizeUnassigned: boolean;
|
||||
routeIsArchive: boolean;
|
||||
routeIsPlaces: boolean;
|
||||
routeIsMap: boolean;
|
||||
|
|
Loading…
Reference in New Issue