Add people to search, filter photos by person

This commit is contained in:
Shailesh Pandit 2021-11-28 21:45:44 +05:30
parent af130f803b
commit 1d41644ac8
7 changed files with 189 additions and 63 deletions

View file

@ -0,0 +1,95 @@
import React, { useState, useEffect } from 'react';
import TFJSImage from 'components/TFJSImage';
import { Person } from 'utils/machineLearning/types';
import { getAllPeople, getPeopleList } from 'utils/machineLearning';
import styled from 'styled-components';
import { File } from 'services/fileService';
const FaceChipContainer = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
margin-top: 10px;
margin-bottom: 10px;
`;
const FaceChip = styled.div`
width: 112px;
height: 112px;
margin-right: 10px;
border-radius: 50%;
overflow: hidden;
position: relative;
cursor: pointer;
`;
interface PeopleListPropsBase {
onSelect?: (person: Person, index: number) => void;
}
export interface PeopleListProps extends PeopleListPropsBase {
people: Array<Person>;
}
export function PeopleList(props: PeopleListProps) {
return (
<FaceChipContainer>
{props.people.map((person, index) => (
<FaceChip
key={index}
onClick={() =>
props.onSelect && props.onSelect(person, index)
}>
<TFJSImage faceImage={person.faceImage}></TFJSImage>
</FaceChip>
))}
</FaceChipContainer>
);
}
export interface PhotoPeopleListProps extends PeopleListPropsBase {
file: File;
}
export function PhotoPeopleList(props: PhotoPeopleListProps) {
const [people, setPeople] = useState<Array<Person>>([]);
const updateFaceImages = async () => {
const people = await getPeopleList(props.file);
setPeople(people);
};
useEffect(() => {
// TODO: handle multiple async updates
updateFaceImages();
}, [props.file]);
return <PeopleList people={people} onSelect={props.onSelect}></PeopleList>;
}
export interface AllPeopleListProps extends PeopleListPropsBase {
limit?: number;
}
export function AllPeopleList(props: AllPeopleListProps) {
const [people, setPeople] = useState<Array<Person>>([]);
useEffect(() => {
// TODO: handle multiple async updates
async function updateFaceImages() {
let people = await getAllPeople();
if (props.limit) {
people = people.slice(0, props.limit);
}
setPeople(people);
}
updateFaceImages();
return () => {
setPeople([]);
};
}, [props]);
return <PeopleList people={people} onSelect={props.onSelect}></PeopleList>;
}

View file

@ -186,6 +186,13 @@ const PhotoFrame = ({
return false;
}
if (
search.person &&
search.person.files.indexOf(item.id) === -1
) {
return false;
}
if (isSharedFile(item) && !isSharedCollection) {
return false;
}

View file

@ -36,9 +36,7 @@ import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import CloseIcon from 'components/icons/CloseIcon';
import TickIcon from 'components/icons/TickIcon';
import TFJSImage from 'components/TFJSImage';
import { Person } from 'utils/machineLearning/types';
import { getPeopleList } from 'utils/machineLearning';
import { PhotoPeopleList } from 'components/PeopleList';
interface Iprops {
isOpen: boolean;
@ -70,24 +68,6 @@ const Pre = styled.pre`
padding: 7px 15px;
`;
const FaceChipContainer = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
margin-top: 10px;
margin-bottom: 10px;
`;
const FaceChip = styled.div`
width: 112px;
height: 112px;
margin-right: 10px;
border-radius: 50%;
overflow: hidden;
position: relative;
`;
const renderInfoItem = (label: string, value: string | JSX.Element) => (
<Row>
<Label width="30%">{label}</Label>
@ -249,35 +229,6 @@ function ExifData(props: { exif: any }) {
);
}
function PeopleList(props: { file }) {
const [peopleList, setPeopleList] = useState<Array<Person>>([]);
const updateFaceImages = async () => {
const peopleList = await getPeopleList(props.file);
setPeopleList(peopleList);
};
useEffect(() => {
// TODO: handle multiple async updates
updateFaceImages();
}, [props.file]);
return (
<>
<div>
<Legend>{constants.PEOPLE}</Legend>
</div>
<FaceChipContainer>
{peopleList.map((person, index) => (
<FaceChip key={index}>
<TFJSImage faceImage={person.faceImage}></TFJSImage>
</FaceChip>
))}
</FaceChipContainer>
</>
);
}
function InfoModal({
showInfo,
handleCloseInfo,
@ -324,7 +275,10 @@ function InfoModal({
{constants.SHOW_MAP}
</a>
)}
<PeopleList file={items[photoSwipe?.getCurrentIndex()]} />
<div>
<Legend>{constants.PEOPLE}</Legend>
</div>
<PhotoPeopleList file={items[photoSwipe?.getCurrentIndex()]} />
{exif && (
<>
<ExifData exif={exif} />

View file

@ -1,11 +1,12 @@
import { Search, SearchStats } from 'pages/gallery';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import AsyncSelect from 'react-select/async';
import { components } from 'react-select';
import debounce from 'debounce-promise';
import {
Bbox,
getAllPeopleSuggestion,
getHolidaySuggestion,
getYearSuggestion,
parseHumanDate,
@ -25,6 +26,8 @@ import { File, FILE_TYPE } from 'services/fileService';
import ImageIcon from './icons/ImageIcon';
import VideoIcon from './icons/VideoIcon';
import { IconButton } from './Container';
import { Person } from 'utils/machineLearning/types';
import { PeopleList } from './PeopleList';
const Wrapper = styled.div<{ isDisabled: boolean; isOpen: boolean }>`
position: fixed;
@ -75,12 +78,20 @@ const SearchInput = styled.div`
margin: auto;
`;
const Legend = styled.span`
font-size: 20px;
color: #ddd;
display: inline;
padding: 20px 10px;
`;
export enum SuggestionType {
DATE,
LOCATION,
COLLECTION,
IMAGE,
VIDEO,
PERSON,
}
export interface DateValue {
date?: number;
@ -90,7 +101,8 @@ export interface DateValue {
export interface Suggestion {
type: SuggestionType;
label: string;
value: Bbox | DateValue | number;
value: Bbox | DateValue | number | Person;
hide?: boolean;
}
interface Props {
isOpen: boolean;
@ -104,6 +116,7 @@ interface Props {
files: File[];
}
export default function SearchBar(props: Props) {
const selectRef = useRef(null);
const [value, setValue] = useState<Suggestion>(null);
const handleChange = (value) => {
@ -116,14 +129,14 @@ export default function SearchBar(props: Props) {
// Functionality
// = =========================
const getAutoCompleteSuggestions = async (searchPhrase: string) => {
const options = await getAllPeopleSuggestion();
// const options = [];
searchPhrase = searchPhrase.trim().toLowerCase();
if (!searchPhrase?.length) {
return [];
}
const options = [
...getHolidaySuggestion(searchPhrase),
...getYearSuggestion(searchPhrase),
];
options.push(...getHolidaySuggestion(searchPhrase));
options.push(...getYearSuggestion(searchPhrase));
const searchedDates = parseHumanDate(searchPhrase);
@ -178,6 +191,7 @@ export default function SearchBar(props: Props) {
const getOptions = debounce(getAutoCompleteSuggestions, 250);
const search = (selectedOption: Suggestion) => {
// console.log('search...');
if (!selectedOption) {
return;
}
@ -203,6 +217,10 @@ export default function SearchBar(props: Props) {
props.setSearch({ fileIndex: selectedOption.value as number });
setValue(null);
break;
case SuggestionType.PERSON:
props.setSearch({ person: selectedOption.value as Person });
props.setOpen(true);
break;
}
};
const resetSearch = () => {
@ -246,13 +264,17 @@ export default function SearchBar(props: Props) {
<span>{props.label}</span>
</div>
);
const { Option, Control } = components;
const { Option, Control, Menu } = components;
const OptionWithIcon = (props) => (
<Option {...props}>
<LabelWithIcon type={props.data.type} label={props.data.label} />
</Option>
);
const OptionWithIcon = (props) =>
!props.data.hide && (
<Option {...props}>
<LabelWithIcon
type={props.data.type}
label={props.data.label}
/>
</Option>
);
const ControlWithIcon = (props) => (
<Control {...props}>
<span
@ -267,6 +289,30 @@ export default function SearchBar(props: Props) {
</Control>
);
const CustomMenu = (props) => {
// console.log("props.selectProps.options: ", selectRef);
const peopleSuggestions = props.selectProps.options.filter(
(o) => o.type === SuggestionType.PERSON
);
const people = peopleSuggestions.map((o) => o.value);
return (
<Menu {...props}>
{people && people.length > 0 && (
<>
<Legend>{constants.PEOPLE}</Legend>
<PeopleList
people={people}
onSelect={(person, index) => {
selectRef.current.blur();
setValue(peopleSuggestions[index]);
}}></PeopleList>
</>
)}
{props.children}
</Menu>
);
};
const customStyles = {
control: (style, { isFocused }) => ({
...style,
@ -333,8 +379,10 @@ export default function SearchBar(props: Props) {
margin: '10px',
}}>
<AsyncSelect
ref={selectRef}
value={value}
components={{
Menu: CustomMenu,
Option: OptionWithIcon,
Control: ControlWithIcon,
}}

View file

@ -93,6 +93,7 @@ import {
Trash,
} from 'services/trashService';
import DeleteBtn from 'components/DeleteBtn';
import { Person } from 'utils/machineLearning/types';
export const DeadCenter = styled.div`
flex: 1;
@ -123,6 +124,7 @@ export type Search = {
date?: DateValue;
location?: Bbox;
fileIndex?: number;
person?: Person;
};
export interface SearchStats {
resultCount: number;

View file

@ -7,6 +7,7 @@ import { Collection } from './collectionService';
import { File } from './fileService';
import { User } from './userService';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { getAllPeople } from 'utils/machineLearning';
const ENDPOINT = getEndpoint();
const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
@ -105,6 +106,16 @@ export function getYearSuggestion(searchPhrase: string): Suggestion[] {
return [];
}
export async function getAllPeopleSuggestion(): Promise<Array<Suggestion>> {
const people = await getAllPeople();
return people.slice(0, 6).map((person) => ({
label: person.name,
type: SuggestionType.PERSON,
value: person,
hide: true,
}));
}
export function searchCollection(
searchPhrase: string,
collections: Collection[]

View file

@ -200,6 +200,15 @@ export async function getPeopleList(file: File): Promise<Array<Person>> {
return peopleList;
}
export async function getAllPeople() {
const people: Array<Person> = [];
await mlPeopleStore.iterate<Person, void>((person) => {
people.push(person);
});
return people.sort((p1, p2) => p2.files.length - p1.files.length);
}
export function findFirstIfSorted<T>(
elements: Array<T>,
comparator: (a: T, b: T) => number