diff --git a/src/components/PeopleList.tsx b/src/components/PeopleList.tsx new file mode 100644 index 000000000..323177704 --- /dev/null +++ b/src/components/PeopleList.tsx @@ -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; +} + +export function PeopleList(props: PeopleListProps) { + return ( + + {props.people.map((person, index) => ( + + props.onSelect && props.onSelect(person, index) + }> + + + ))} + + ); +} + +export interface PhotoPeopleListProps extends PeopleListPropsBase { + file: File; +} + +export function PhotoPeopleList(props: PhotoPeopleListProps) { + const [people, setPeople] = useState>([]); + + const updateFaceImages = async () => { + const people = await getPeopleList(props.file); + setPeople(people); + }; + + useEffect(() => { + // TODO: handle multiple async updates + updateFaceImages(); + }, [props.file]); + + return ; +} + +export interface AllPeopleListProps extends PeopleListPropsBase { + limit?: number; +} + +export function AllPeopleList(props: AllPeopleListProps) { + const [people, setPeople] = useState>([]); + + 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 ; +} diff --git a/src/components/PhotoFrame.tsx b/src/components/PhotoFrame.tsx index ffd4bf8ad..dc6e47fd1 100644 --- a/src/components/PhotoFrame.tsx +++ b/src/components/PhotoFrame.tsx @@ -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; } diff --git a/src/components/PhotoSwipe/PhotoSwipe.tsx b/src/components/PhotoSwipe/PhotoSwipe.tsx index 1347de3ee..dc7927144 100644 --- a/src/components/PhotoSwipe/PhotoSwipe.tsx +++ b/src/components/PhotoSwipe/PhotoSwipe.tsx @@ -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) => ( @@ -249,35 +229,6 @@ function ExifData(props: { exif: any }) { ); } -function PeopleList(props: { file }) { - const [peopleList, setPeopleList] = useState>([]); - - const updateFaceImages = async () => { - const peopleList = await getPeopleList(props.file); - setPeopleList(peopleList); - }; - - useEffect(() => { - // TODO: handle multiple async updates - updateFaceImages(); - }, [props.file]); - - return ( - <> -
- {constants.PEOPLE} -
- - {peopleList.map((person, index) => ( - - - - ))} - - - ); -} - function InfoModal({ showInfo, handleCloseInfo, @@ -324,7 +275,10 @@ function InfoModal({ {constants.SHOW_MAP} )} - +
+ {constants.PEOPLE} +
+ {exif && ( <> diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index f3f42615d..322996e73 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -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(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) { {props.label} ); - const { Option, Control } = components; + const { Option, Control, Menu } = components; - const OptionWithIcon = (props) => ( - - ); + const OptionWithIcon = (props) => + !props.data.hide && ( + + ); const ControlWithIcon = (props) => ( ); + 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 ( + + {people && people.length > 0 && ( + <> + {constants.PEOPLE} + { + selectRef.current.blur(); + setValue(peopleSuggestions[index]); + }}> + + )} + {props.children} + + ); + }; + const customStyles = { control: (style, { isFocused }) => ({ ...style, @@ -333,8 +379,10 @@ export default function SearchBar(props: Props) { margin: '10px', }}> > { + 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[] diff --git a/src/utils/machineLearning/index.ts b/src/utils/machineLearning/index.ts index f8ef9b344..d804b5df3 100644 --- a/src/utils/machineLearning/index.ts +++ b/src/utils/machineLearning/index.ts @@ -200,6 +200,15 @@ export async function getPeopleList(file: File): Promise> { return peopleList; } +export async function getAllPeople() { + const people: Array = []; + await mlPeopleStore.iterate((person) => { + people.push(person); + }); + + return people.sort((p1, p2) => p2.files.length - p1.files.length); +} + export function findFirstIfSorted( elements: Array, comparator: (a: T, b: T) => number