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
|
// Join with cluster
|
||||||
$clusterQuery = null;
|
$clusterQuery = null;
|
||||||
if ('NULL' === $faceName) {
|
if ('NULL' === $faceName) {
|
||||||
$clusterQuery = $query->expr()->isNull('rfd.cluster_id');
|
$clusterQuery = $query->expr()->eq('rfd.cluster_id', $query->expr()->literal(-1));
|
||||||
} else {
|
} else {
|
||||||
$nameField = is_numeric($faceName) ? 'rfc.id' : 'rfc.title';
|
$nameField = is_numeric($faceName) ? 'rfc.id' : 'rfc.title';
|
||||||
$query->innerJoin('m', 'recognize_face_clusters', 'rfc', $query->expr()->andX(
|
$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,
|
if: (self: any) => self.config.albums_enabled && !self.routeIsAlbums,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'face-move',
|
||||||
name: t('memories', 'Move to person'),
|
name: t('memories', 'Move to person'),
|
||||||
icon: MoveIcon,
|
icon: MoveIcon,
|
||||||
callback: this.moveSelectionToPerson.bind(this),
|
callback: this.moveSelectionToPerson.bind(this),
|
||||||
if: () => this.$route.name === 'recognize',
|
if: () => this.routeIsRecognize,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('memories', 'Remove from person'),
|
name: t('memories', 'Remove from person'),
|
||||||
icon: CloseIcon,
|
icon: CloseIcon,
|
||||||
callback: this.removeSelectionFromPerson.bind(this),
|
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() {
|
beforeDestroy() {
|
||||||
|
@ -800,7 +807,7 @@ export default defineComponent({
|
||||||
* Move selected photos to another person
|
* Move selected photos to another person
|
||||||
*/
|
*/
|
||||||
async moveSelectionToPerson(selection: Selection) {
|
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'));
|
showError(this.t('memories', 'You must enable "Mark person in preview" to use this feature'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -593,7 +593,7 @@ export default defineComponent({
|
||||||
API.DAYS_FILTER(query, filter, `${user}/${name}`);
|
API.DAYS_FILTER(query, filter, `${user}/${name}`);
|
||||||
|
|
||||||
// Face rect
|
// Face rect
|
||||||
if (this.config.show_face_rect) {
|
if (this.config.show_face_rect || this.routeIsRecognizeUnassigned) {
|
||||||
API.DAYS_FILTER(query, DaysFilterType.FACE_RECT);
|
API.DAYS_FILTER(query, DaysFilterType.FACE_RECT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,16 +90,7 @@ export default defineComponent({
|
||||||
const name = this.$route.params.name || '';
|
const name = this.$route.params.name || '';
|
||||||
const target = String(face.name || face.cluster_id);
|
const target = String(face.name || face.cluster_id);
|
||||||
|
|
||||||
if (
|
if (!confirm(this.t('memories', 'Move the selected photos to {target}?', { target }))) return;
|
||||||
!confirm(
|
|
||||||
this.t('memories', 'Are you sure you want to move the selected photos from {name} to {target}?', {
|
|
||||||
name,
|
|
||||||
target,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.show = false;
|
this.show = false;
|
||||||
|
|
|
@ -1,43 +1,54 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="name" class="face-top-matter">
|
<div class="face-top-matter">
|
||||||
<NcActions>
|
<NcActions v-if="name">
|
||||||
<NcActionButton :aria-label="t('memories', 'Back')" @click="back()">
|
<NcActionButton :aria-label="t('memories', 'Back')" @click="back()">
|
||||||
{{ t('memories', 'Back') }}
|
{{ t('memories', 'Back') }}
|
||||||
<template #icon> <BackIcon :size="20" /> </template>
|
<template #icon> <BackIcon :size="20" /> </template>
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
</NcActions>
|
</NcActions>
|
||||||
|
|
||||||
<div class="name">{{ name }}</div>
|
<div class="name" :class="{ rename: isReal }" @click="rename">{{ displayName }}</div>
|
||||||
|
|
||||||
<div class="right-actions">
|
<div class="right-actions">
|
||||||
<NcActions :inline="1">
|
<NcActions :inline="0">
|
||||||
<NcActionButton :aria-label="t('memories', 'Rename person')" @click="$refs.editModal?.open()" close-after-click>
|
<!-- root view (not cluster or unassigned) -->
|
||||||
{{ t('memories', 'Rename person') }}
|
<template v-if="!name && routeIsRecognize && !routeIsRecognizeUnassigned">
|
||||||
<template #icon> <EditIcon :size="20" /> </template>
|
<NcActionButton :aria-label="t('memories', 'Unassigned faces')" @click="openUnassigned" close-after-click>
|
||||||
</NcActionButton>
|
{{ t('memories', 'Unassigned faces') }}
|
||||||
<NcActionButton
|
<template #icon> <UnassignedIcon :size="20" /> </template>
|
||||||
:aria-label="t('memories', 'Merge with different person')"
|
</NcActionButton>
|
||||||
@click="$refs.mergeModal?.open()"
|
</template>
|
||||||
close-after-click
|
|
||||||
>
|
<!-- real cluster -->
|
||||||
{{ t('memories', 'Merge with different person') }}
|
<template v-if="isReal">
|
||||||
<template #icon> <MergeIcon :size="20" /> </template>
|
<NcActionButton :aria-label="t('memories', 'Rename person')" @click="rename" close-after-click>
|
||||||
</NcActionButton>
|
{{ t('memories', 'Rename person') }}
|
||||||
<NcActionCheckbox
|
<template #icon> <EditIcon :size="20" /> </template>
|
||||||
:aria-label="t('memories', 'Mark person in preview')"
|
</NcActionButton>
|
||||||
:checked.sync="config.show_face_rect"
|
<NcActionButton
|
||||||
@change="changeShowFaceRect"
|
:aria-label="t('memories', 'Merge with different person')"
|
||||||
>
|
@click="$refs.mergeModal?.open()"
|
||||||
{{ t('memories', 'Mark person in preview') }}
|
close-after-click
|
||||||
</NcActionCheckbox>
|
>
|
||||||
<NcActionButton
|
{{ t('memories', 'Merge with different person') }}
|
||||||
:aria-label="t('memories', 'Remove person')"
|
<template #icon> <MergeIcon :size="20" /> </template>
|
||||||
@click="$refs.deleteModal?.open()"
|
</NcActionButton>
|
||||||
close-after-click
|
<NcActionCheckbox
|
||||||
>
|
:aria-label="t('memories', 'Mark person in preview')"
|
||||||
{{ t('memories', 'Remove person') }}
|
:checked.sync="config.show_face_rect"
|
||||||
<template #icon> <DeleteIcon :size="20" /> </template>
|
@change="changeShowFaceRect"
|
||||||
</NcActionButton>
|
>
|
||||||
|
{{ t('memories', 'Mark person in preview') }}
|
||||||
|
</NcActionCheckbox>
|
||||||
|
<NcActionButton
|
||||||
|
:aria-label="t('memories', 'Remove person')"
|
||||||
|
@click="$refs.deleteModal?.open()"
|
||||||
|
close-after-click
|
||||||
|
>
|
||||||
|
{{ t('memories', 'Remove person') }}
|
||||||
|
<template #icon> <DeleteIcon :size="20" /> </template>
|
||||||
|
</NcActionButton>
|
||||||
|
</template>
|
||||||
</NcActions>
|
</NcActions>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -56,6 +67,7 @@ import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
|
||||||
import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox';
|
import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox';
|
||||||
|
|
||||||
import { emit } from '@nextcloud/event-bus';
|
import { emit } from '@nextcloud/event-bus';
|
||||||
|
import { getCurrentUser } from '@nextcloud/auth';
|
||||||
|
|
||||||
import FaceEditModal from '../modal/FaceEditModal.vue';
|
import FaceEditModal from '../modal/FaceEditModal.vue';
|
||||||
import FaceDeleteModal from '../modal/FaceDeleteModal.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 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';
|
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({
|
export default defineComponent({
|
||||||
name: 'FaceTopMatter',
|
name: 'FaceTopMatter',
|
||||||
|
@ -78,6 +93,7 @@ export default defineComponent({
|
||||||
EditIcon,
|
EditIcon,
|
||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
MergeIcon,
|
MergeIcon,
|
||||||
|
UnassignedIcon,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [UserConfig],
|
mixins: [UserConfig],
|
||||||
|
@ -86,6 +102,19 @@ export default defineComponent({
|
||||||
name() {
|
name() {
|
||||||
return this.$route.params.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: {
|
methods: {
|
||||||
|
@ -93,6 +122,20 @@ export default defineComponent({
|
||||||
this.$router.go(-1);
|
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() {
|
changeShowFaceRect() {
|
||||||
this.updateSetting('show_face_rect');
|
this.updateSetting('show_face_rect');
|
||||||
emit('memories:timeline:hard-refresh', {});
|
emit('memories:timeline:hard-refresh', {});
|
||||||
|
@ -100,3 +143,14 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</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 FaceTopMatter from './FaceTopMatter.vue';
|
||||||
import AlbumTopMatter from './AlbumTopMatter.vue';
|
import AlbumTopMatter from './AlbumTopMatter.vue';
|
||||||
|
|
||||||
import { TopMatterType } from '../../types';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'TopMatter',
|
name: 'TopMatter',
|
||||||
components: {
|
components: {
|
||||||
|
@ -44,32 +42,17 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
type() {
|
currentmatter() {
|
||||||
switch (this.$route.name) {
|
switch (this.$route.name) {
|
||||||
case 'folders':
|
case 'folders':
|
||||||
return TopMatterType.FOLDER;
|
return FolderTopMatter;
|
||||||
case 'albums':
|
case 'albums':
|
||||||
return TopMatterType.ALBUM;
|
return AlbumTopMatter;
|
||||||
case 'tags':
|
case 'tags':
|
||||||
case 'places':
|
case 'places':
|
||||||
return TopMatterType.CLUSTER;
|
return ClusterTopMatter;
|
||||||
case 'recognize':
|
case 'recognize':
|
||||||
case 'facerecognition':
|
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;
|
return FaceTopMatter;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -25,6 +25,12 @@ export default defineComponent({
|
||||||
routeIsPeople(): boolean {
|
routeIsPeople(): boolean {
|
||||||
return ['recognize', 'facerecognition'].includes(<string>this.$route.name);
|
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 {
|
routeIsArchive(): boolean {
|
||||||
return this.$route.name === 'archive';
|
return this.$route.name === 'archive';
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { generateUrl } from '@nextcloud/router';
|
||||||
import { IFace, IPhoto } from '../../types';
|
import { IFace, IPhoto } from '../../types';
|
||||||
import { API } from '../API';
|
import { API } from '../API';
|
||||||
import client from '../DavClient';
|
import client from '../DavClient';
|
||||||
|
import * as utils from '../Utils';
|
||||||
import * as base from './base';
|
import * as base from './base';
|
||||||
|
|
||||||
export async function getFaceList(app: 'recognize' | 'facerecognition') {
|
export async function getFaceList(app: 'recognize' | 'facerecognition') {
|
||||||
|
@ -83,10 +84,16 @@ export async function* recognizeMoveFaceImages(user: string, face: string, targe
|
||||||
// Remove each file
|
// Remove each file
|
||||||
const calls = photos.map((p) => async () => {
|
const calls = photos.map((p) => async () => {
|
||||||
try {
|
try {
|
||||||
await client.moveFile(
|
const dest = `/recognize/${user}/faces/${target}`;
|
||||||
`/recognize/${user}/faces/${face}/${p.faceid}-${p.basename}`,
|
const name = `${p.faceid}-${p.basename}`;
|
||||||
`/recognize/${user}/faces/${target}/${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!;
|
return p.faceid!;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
@ -11,6 +11,8 @@ export const constants = {
|
||||||
FLAG_LEAVING: 1 << 5,
|
FLAG_LEAVING: 1 << 5,
|
||||||
FLAG_IS_LOCAL: 1 << 6,
|
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;
|
key?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TopMatter = {
|
|
||||||
type: TopMatterType;
|
|
||||||
};
|
|
||||||
export enum TopMatterType {
|
|
||||||
NONE = 0,
|
|
||||||
FOLDER = 1,
|
|
||||||
CLUSTER = 2,
|
|
||||||
FACE = 3,
|
|
||||||
ALBUM = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ISelectionAction = {
|
export type ISelectionAction = {
|
||||||
|
/** Identifier (optional) */
|
||||||
|
id?: string;
|
||||||
/** Display text */
|
/** Display text */
|
||||||
name: string;
|
name: string;
|
||||||
/** Icon component */
|
/** Icon component */
|
||||||
|
|
|
@ -15,6 +15,8 @@ declare module 'vue' {
|
||||||
routeIsFolders: boolean;
|
routeIsFolders: boolean;
|
||||||
routeIsAlbums: boolean;
|
routeIsAlbums: boolean;
|
||||||
routeIsPeople: boolean;
|
routeIsPeople: boolean;
|
||||||
|
routeIsRecognize: boolean;
|
||||||
|
routeIsRecognizeUnassigned: boolean;
|
||||||
routeIsArchive: boolean;
|
routeIsArchive: boolean;
|
||||||
routeIsPlaces: boolean;
|
routeIsPlaces: boolean;
|
||||||
routeIsMap: boolean;
|
routeIsMap: boolean;
|
||||||
|
|
Loading…
Reference in New Issue