diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx index bfd86e0db3..ead1211fdc 100644 --- a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx @@ -12,18 +12,25 @@ import { type ParsedMetadataDate, } from "@/media/file-metadata"; import { FileType } from "@/media/file-type"; -import { UnidentifiedFaces } from "@/new/photos/components/PeopleList"; +import { + AnnotatedFacePeopleList, + UnclusteredFaceList, +} from "@/new/photos/components/PeopleList"; import { PhotoDateTimePicker } from "@/new/photos/components/PhotoDateTimePicker"; import { photoSwipeZIndex } from "@/new/photos/components/PhotoViewer"; import { tagNumericValue, type RawExifTags } from "@/new/photos/services/exif"; -import { annotatedFaceIDsForFile, AnnotatedFacesForFile, getAnnotatedFacesForFile, getFacesForFile, isMLEnabled } from "@/new/photos/services/ml"; +import { + AnnotatedFacesForFile, + getAnnotatedFacesForFile, + isMLEnabled, + type AnnotatedFaceID, +} from "@/new/photos/services/ml"; import { EnteFile } from "@/new/photos/types/file"; import { formattedByteSize } from "@/new/photos/utils/units"; import CopyButton from "@ente/shared/components/CodeBlock/CopyButton"; import { FlexWrapper } from "@ente/shared/components/Container"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import { getPublicMagicMetadataSync } from "@ente/shared/file-metadata"; -import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded"; import { formatDate, formatTime } from "@ente/shared/time/format"; import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; import CameraOutlined from "@mui/icons-material/CameraOutlined"; @@ -98,7 +105,9 @@ export const FileInfo: React.FC = ({ const [exifInfo, setExifInfo] = useState(); const [openRawExif, setOpenRawExif] = useState(false); - const [annotatedFaces, setAnnotatedFaces] = useState(); + const [annotatedFaces, setAnnotatedFaces] = useState< + AnnotatedFacesForFile | undefined + >(); const location = useMemo(() => { if (file) { @@ -108,18 +117,20 @@ export const FileInfo: React.FC = ({ return exif?.parsed?.location; }, [file, exif]); + useEffect(() => { + if (!file) return; - useEffect(() => { - let didCancel = false; + let didCancel = false; - void (async () => { - const result = await getAnnotatedFacesForFile(file); - !didCancel && setAnnotatedFaces(result); - })(); - - return () => { didCancel = true;} - }, [file]); + void (async () => { + const result = await getAnnotatedFacesForFile(file); + !didCancel && setAnnotatedFaces(result); + })(); + return () => { + didCancel = true; + }; + }, [file]); useEffect(() => { setExifInfo(parseExifInfo(exif)); @@ -144,6 +155,10 @@ export const FileInfo: React.FC = ({ getMapDisableConfirmationDialog(() => updateMapEnabled(false)), ); + const handleSelectFace = (annotatedFaceID: AnnotatedFaceID) => { + console.log(annotatedFaceID); + }; + return ( @@ -282,11 +297,17 @@ export const FileInfo: React.FC = ({ )} - {isMLEnabled() && ( + {isMLEnabled() && annotatedFaces && ( <> - {annotatedFaces?.annotatedFaceIDs.length && - // {/* TODO-Cluster */} - + + )} diff --git a/web/packages/new/photos/components/PeopleList.tsx b/web/packages/new/photos/components/PeopleList.tsx index 3553ed78aa..8ced5627e2 100644 --- a/web/packages/new/photos/components/PeopleList.tsx +++ b/web/packages/new/photos/components/PeopleList.tsx @@ -1,4 +1,5 @@ import { useIsMobileWidth } from "@/base/hooks"; +import { pt } from "@/base/i18n"; import { faceCrop, type AnnotatedFaceID } from "@/new/photos/services/ml"; import type { Person } from "@/new/photos/services/ml/people"; import type { EnteFile } from "@/new/photos/types/file"; @@ -25,7 +26,7 @@ export const SearchPeopleList: React.FC = ({ sx={{ justifyContent: people.length > 3 ? "center" : "start" }} > {people.slice(0, isMobileWidth ? 6 : 7).map((person) => ( - onSelectPerson(person)} > @@ -34,7 +35,7 @@ export const SearchPeopleList: React.FC = ({ enteFile={person.displayFaceFile} placeholderDimension={87} /> - + ))} ); @@ -49,7 +50,7 @@ const SearchPeopleContainer = styled("div")` margin-block-end: 15px; `; -const SearchPeopleButton = styled(UnstyledButton)( +const SearchPersonButton = styled(UnstyledButton)( ({ theme }) => ` width: 87px; height: 87px; @@ -67,6 +68,13 @@ const SearchPeopleButton = styled(UnstyledButton)( ); export interface AnnotatedFacePeopleListProps { + /** + * The {@link EnteFile} whose information we are showing. + */ + enteFile: EnteFile; + /** + * The list of faces in the file that are associated with a person. + */ annotatedFaceIDs: AnnotatedFaceID[]; /** * Called when the user selects a face in the list. @@ -80,41 +88,43 @@ export interface AnnotatedFacePeopleListProps { */ export const AnnotatedFacePeopleList: React.FC< AnnotatedFacePeopleListProps -> = ({ annotatedFaceIDs, onSelectFace }) => { - const isMobileWidth = useIsMobileWidth(); +> = ({ enteFile, annotatedFaceIDs, onSelectFace }) => { + if (annotatedFaceIDs.length == 0) return <>; + return ( - 3 ? "center" : "start" }} - > - {people.slice(0, isMobileWidth ? 6 : 7).map((person) => ( - onSelectPerson(person)} - > - - - ))} - + <> + + {t("people")} + + + {annotatedFaceIDs.map((annotatedFaceID) => ( + onSelectFace(annotatedFaceID)} + > + + + ))} + + ); }; -const SearchPeopleContainer = styled("div")` +const AnnotatedFacePeopleContainer = styled("div")` display: flex; flex-wrap: wrap; align-items: center; gap: 5px; - margin-block-start: 12px; - margin-block-end: 15px; `; -const SearchPeopleButton = styled(UnstyledButton)( +const AnnotatedFaceButton = styled(UnstyledButton)( ({ theme }) => ` - width: 87px; - height: 87px; + width: 112px; + height: 112px; border-radius: 50%; overflow: hidden; & > img { @@ -128,7 +138,48 @@ const SearchPeopleButton = styled(UnstyledButton)( `, ); -const FaceChipContainer = styled("div")` +export interface UnclusteredFaceListProps { + /** + * The {@link EnteFile} whose information we are showing. + */ + enteFile: EnteFile; + /** + * The list of faces in the file that are not associated with a person. + */ + faceIDs: string[]; +} + +/** + * Show the list of faces in the given file that are not associated with a + * specific person. + */ +export const UnclusteredFaceList: React.FC = ({ + enteFile, + faceIDs, +}) => { + if (faceIDs.length == 0) return <>; + + return ( + <> + + {pt("Other faces")} + {/*t("UNIDENTIFIED_FACES") TODO-Cluster */} + + + {faceIDs.map((faceID) => ( + + + + ))} + + + ); +}; + +const UnclusteredFacesContainer = styled("div")` display: flex; flex-wrap: wrap; justify-content: center; @@ -138,63 +189,19 @@ const FaceChipContainer = styled("div")` overflow: auto; `; -const FaceChip = styled("div")<{ clickable?: boolean }>` +const UnclusteredFace = styled("div")` width: 112px; height: 112px; margin: 5px; border-radius: 50%; overflow: hidden; position: relative; - cursor: ${({ clickable }) => (clickable ? "pointer" : "normal")}; & > img { width: 100%; height: 100%; } `; -export interface PhotoPeopleListProps { - file: EnteFile; - onSelect?: (person: Person, index: number) => void; -} - -export function PhotoPeopleList() { - return <>; -} - -interface UnidentifiedFacesProps { - enteFile: EnteFile; -} - -/** - * Show the list of faces in the given file that are not linked to a specific - * person ("face cluster"). - */ -export const UnidentifiedFaces: React.FC = ({ - enteFile, -}) => { - const [faceIDs, setFaceIDs] = useState([]); - - if (faceIDs.length == 0) return <>; - - return ( - <> - - {t("UNIDENTIFIED_FACES")} - - - {faceIDs.map((faceID) => ( - - - - ))} - - - ); -}; - interface FaceCropImageViewProps { /** The ID of the face to display. */ faceID: string;