parent
c56911709c
commit
af226344b8
|
@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
- **Feature**: Allow adding photos to multiple albums together ([#752](https://github.com/pulsejet/memories/pull/752))
|
- **Feature**: Allow adding photos to multiple albums together ([#752](https://github.com/pulsejet/memories/pull/752))
|
||||||
- **Feature**: Show albums of photo in metadata ([#752](https://github.com/pulsejet/memories/pull/752))
|
- **Feature**: Show albums of photo in metadata ([#752](https://github.com/pulsejet/memories/pull/752))
|
||||||
|
- **Feature**: Show faces in photo in sidebar metadata
|
||||||
|
|
||||||
## [v5.2.1] - 2023-07-03
|
## [v5.2.1] - 2023-07-03
|
||||||
|
|
||||||
|
|
|
@ -131,6 +131,10 @@ class RecognizeBackend extends Backend
|
||||||
|
|
||||||
public function getClusters(): array
|
public function getClusters(): array
|
||||||
{
|
{
|
||||||
|
/** @var \OCP\IRequest $request */
|
||||||
|
$request = \OC::$server->get(\OCP\IRequest::class);
|
||||||
|
$fileid = (int) $request->getParam('fileid', -1);
|
||||||
|
|
||||||
$query = $this->tq->getBuilder();
|
$query = $this->tq->getBuilder();
|
||||||
|
|
||||||
// SELECT all face clusters
|
// SELECT all face clusters
|
||||||
|
@ -149,6 +153,20 @@ class RecognizeBackend extends Backend
|
||||||
// WHERE this cluster belongs to the user
|
// WHERE this cluster belongs to the user
|
||||||
$query->where($query->expr()->eq('rfc.user_id', $query->createNamedParameter(Util::getUID())));
|
$query->where($query->expr()->eq('rfc.user_id', $query->createNamedParameter(Util::getUID())));
|
||||||
|
|
||||||
|
// WHERE these clusters contain fileid if specified
|
||||||
|
if ($fileid > 0) {
|
||||||
|
$fSq = $this->tq->getBuilder()
|
||||||
|
->select('rfd.file_id')
|
||||||
|
->from('recognize_face_detections', 'rfd')
|
||||||
|
->where($query->expr()->andX(
|
||||||
|
$query->expr()->eq('rfd.cluster_id', 'rfc.id'),
|
||||||
|
$query->expr()->eq('rfd.file_id', $query->createNamedParameter($fileid, \PDO::PARAM_INT)),
|
||||||
|
))
|
||||||
|
->getSQL()
|
||||||
|
;
|
||||||
|
$query->andWhere($query->createFunction("EXISTS ({$fSq})"));
|
||||||
|
}
|
||||||
|
|
||||||
// GROUP by ID of face cluster
|
// GROUP by ID of face cluster
|
||||||
$query->groupBy('rfc.id');
|
$query->groupBy('rfc.id');
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="outer" v-if="fileid">
|
<div class="outer" v-if="fileid">
|
||||||
|
<div v-if="people.length" class="people">
|
||||||
|
<div class="section-title">{{ t('memories', 'People') }}</div>
|
||||||
|
<div class="container" v-for="face of people" :key="face.cluster_id">
|
||||||
|
<Cluster :data="face" :counters="false"> </Cluster>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="albums.length">
|
<div v-if="albums.length">
|
||||||
<div class="section-title">{{ t('memories', 'Albums') }}</div>
|
<div class="section-title">{{ t('memories', 'Albums') }}</div>
|
||||||
<AlbumsList class="albums" :albums="albums" />
|
<AlbumsList class="albums" :albums="albums" />
|
||||||
|
@ -64,6 +71,7 @@ import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import UserConfig from '../mixins/UserConfig';
|
import UserConfig from '../mixins/UserConfig';
|
||||||
import AlbumsList from './modal/AlbumsList.vue';
|
import AlbumsList from './modal/AlbumsList.vue';
|
||||||
|
import Cluster from './frame/Cluster.vue';
|
||||||
|
|
||||||
import EditIcon from 'vue-material-design-icons/Pencil.vue';
|
import EditIcon from 'vue-material-design-icons/Pencil.vue';
|
||||||
import CalendarIcon from 'vue-material-design-icons/Calendar.vue';
|
import CalendarIcon from 'vue-material-design-icons/Calendar.vue';
|
||||||
|
@ -77,7 +85,7 @@ import * as utils from '../services/Utils';
|
||||||
import * as dav from '../services/DavRequests';
|
import * as dav from '../services/DavRequests';
|
||||||
import { API } from '../services/API';
|
import { API } from '../services/API';
|
||||||
|
|
||||||
import type { IAlbum, IImageInfo, IPhoto } from '../types';
|
import type { IAlbum, IFace, IImageInfo, IPhoto } from '../types';
|
||||||
|
|
||||||
interface TopField {
|
interface TopField {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -92,18 +100,24 @@ export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
NcActions,
|
NcActions,
|
||||||
NcActionButton,
|
NcActionButton,
|
||||||
EditIcon,
|
|
||||||
AlbumsList,
|
AlbumsList,
|
||||||
|
Cluster,
|
||||||
|
EditIcon,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [UserConfig],
|
mixins: [UserConfig],
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
// Basic info and metadata
|
||||||
fileid: null as number | null,
|
fileid: null as number | null,
|
||||||
filename: '',
|
filename: '',
|
||||||
exif: {} as { [prop: string]: any },
|
exif: {} as { [prop: string]: any },
|
||||||
baseInfo: {} as IImageInfo,
|
baseInfo: {} as IImageInfo,
|
||||||
|
|
||||||
|
// Cluster lists
|
||||||
albums: [] as IAlbum[],
|
albums: [] as IAlbum[],
|
||||||
|
people: [] as IFace[],
|
||||||
|
|
||||||
state: 0,
|
state: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -342,6 +356,7 @@ export default defineComponent({
|
||||||
this.fileid = null;
|
this.fileid = null;
|
||||||
this.exif = {};
|
this.exif = {};
|
||||||
this.albums = [];
|
this.albums = [];
|
||||||
|
this.people = [];
|
||||||
|
|
||||||
const state = this.state;
|
const state = this.state;
|
||||||
const url = API.Q(utils.getImageInfoUrl(photo), { tags: 1 });
|
const url = API.Q(utils.getImageInfoUrl(photo), { tags: 1 });
|
||||||
|
@ -355,24 +370,26 @@ export default defineComponent({
|
||||||
|
|
||||||
// trigger other refreshes
|
// trigger other refreshes
|
||||||
this.refreshAlbums();
|
this.refreshAlbums();
|
||||||
|
this.refreshPeople();
|
||||||
|
|
||||||
return this.baseInfo;
|
return this.baseInfo;
|
||||||
},
|
},
|
||||||
|
|
||||||
async refreshAlbums(): Promise<IAlbum[]> {
|
async refreshAlbums(): Promise<void> {
|
||||||
if (!this.config.albums_enabled) return [];
|
if (!this.config.albums_enabled) return;
|
||||||
|
this.albums = await this.guardState(dav.getAlbums(1, this.fileid!));
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshPeople(): Promise<void> {
|
||||||
|
if (!this.config.recognize_enabled) return;
|
||||||
|
this.people = await this.guardState(dav.getFaceList('recognize', this.fileid!));
|
||||||
|
},
|
||||||
|
|
||||||
|
async guardState<T>(promise: Promise<T>): Promise<T> {
|
||||||
const state = this.state;
|
const state = this.state;
|
||||||
|
const res = await promise;
|
||||||
let list: IAlbum[] = [];
|
if (state === this.state) return res;
|
||||||
try {
|
throw new Error('state changed');
|
||||||
list = await dav.getAlbums(1, this.fileid!);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('metadata: failed to load albums', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state !== this.state) return list;
|
|
||||||
return (this.albums = list);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleFileUpdated({ fileid }: { fileid: number }) {
|
handleFileUpdated({ fileid }: { fileid: number }) {
|
||||||
|
@ -397,6 +414,19 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.people {
|
||||||
|
> .section-title {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
> .container {
|
||||||
|
width: calc(100% / 3);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.top-field {
|
.top-field {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
|
|
|
@ -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="count-bubble" v-if="data.count">
|
<div class="count-bubble" v-if="counters && data.count">
|
||||||
<NcCounterBubble> {{ data.count }} </NcCounterBubble>
|
<NcCounterBubble> {{ data.count }} </NcCounterBubble>
|
||||||
</div>
|
</div>
|
||||||
<div class="name">
|
<div class="name">
|
||||||
|
@ -55,6 +55,10 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
counters: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -106,8 +106,8 @@ export class API {
|
||||||
return gen(`${BASE}/tags/set/{fileid}`, { fileid });
|
return gen(`${BASE}/tags/set/{fileid}`, { fileid });
|
||||||
}
|
}
|
||||||
|
|
||||||
static FACE_LIST(app: 'recognize' | 'facerecognition') {
|
static FACE_LIST(app: 'recognize' | 'facerecognition', fileid?: number) {
|
||||||
return gen(`${BASE}/clusters/${app}`);
|
return API.Q(gen(`${BASE}/clusters/${app}`), { fileid });
|
||||||
}
|
}
|
||||||
|
|
||||||
static CLUSTER_PREVIEW(backend: ClusterTypes, name: string | number) {
|
static CLUSTER_PREVIEW(backend: ClusterTypes, name: string | number) {
|
||||||
|
|
|
@ -8,8 +8,13 @@ import client from '../DavClient';
|
||||||
import * as utils from '../Utils';
|
import * as utils from '../Utils';
|
||||||
import * as base from './base';
|
import * as base from './base';
|
||||||
|
|
||||||
export async function getFaceList(app: 'recognize' | 'facerecognition') {
|
/**
|
||||||
return (await axios.get<IFace[]>(API.FACE_LIST(app))).data;
|
* Get list of faces
|
||||||
|
* @param app Backend app to use
|
||||||
|
* @param fileid File to filter by (optional)
|
||||||
|
*/
|
||||||
|
export async function getFaceList(app: 'recognize' | 'facerecognition', fileid?: number) {
|
||||||
|
return (await axios.get<IFace[]>(API.FACE_LIST(app, fileid))).data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue