people list checkpoint

This commit is contained in:
Manav Rathi
2024-09-27 13:23:17 +05:30
parent 4e04739d54
commit 2827a166dc
2 changed files with 118 additions and 90 deletions

View File

@@ -12,18 +12,25 @@ import {
type ParsedMetadataDate, type ParsedMetadataDate,
} from "@/media/file-metadata"; } from "@/media/file-metadata";
import { FileType } from "@/media/file-type"; 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 { PhotoDateTimePicker } from "@/new/photos/components/PhotoDateTimePicker";
import { photoSwipeZIndex } from "@/new/photos/components/PhotoViewer"; import { photoSwipeZIndex } from "@/new/photos/components/PhotoViewer";
import { tagNumericValue, type RawExifTags } from "@/new/photos/services/exif"; 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 { EnteFile } from "@/new/photos/types/file";
import { formattedByteSize } from "@/new/photos/utils/units"; import { formattedByteSize } from "@/new/photos/utils/units";
import CopyButton from "@ente/shared/components/CodeBlock/CopyButton"; import CopyButton from "@ente/shared/components/CodeBlock/CopyButton";
import { FlexWrapper } from "@ente/shared/components/Container"; import { FlexWrapper } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner"; import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { getPublicMagicMetadataSync } from "@ente/shared/file-metadata"; import { getPublicMagicMetadataSync } from "@ente/shared/file-metadata";
import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded";
import { formatDate, formatTime } from "@ente/shared/time/format"; import { formatDate, formatTime } from "@ente/shared/time/format";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
import CameraOutlined from "@mui/icons-material/CameraOutlined"; import CameraOutlined from "@mui/icons-material/CameraOutlined";
@@ -98,7 +105,9 @@ export const FileInfo: React.FC<FileInfoProps> = ({
const [exifInfo, setExifInfo] = useState<ExifInfo | undefined>(); const [exifInfo, setExifInfo] = useState<ExifInfo | undefined>();
const [openRawExif, setOpenRawExif] = useState(false); const [openRawExif, setOpenRawExif] = useState(false);
const [annotatedFaces, setAnnotatedFaces] = useState<AnnotatedFacesForFile | undefined>(); const [annotatedFaces, setAnnotatedFaces] = useState<
AnnotatedFacesForFile | undefined
>();
const location = useMemo(() => { const location = useMemo(() => {
if (file) { if (file) {
@@ -108,18 +117,20 @@ export const FileInfo: React.FC<FileInfoProps> = ({
return exif?.parsed?.location; return exif?.parsed?.location;
}, [file, exif]); }, [file, exif]);
useEffect(() => {
if (!file) return;
useEffect(() => { let didCancel = false;
let didCancel = false;
void (async () => { void (async () => {
const result = await getAnnotatedFacesForFile(file); const result = await getAnnotatedFacesForFile(file);
!didCancel && setAnnotatedFaces(result); !didCancel && setAnnotatedFaces(result);
})(); })();
return () => { didCancel = true;}
}, [file]);
return () => {
didCancel = true;
};
}, [file]);
useEffect(() => { useEffect(() => {
setExifInfo(parseExifInfo(exif)); setExifInfo(parseExifInfo(exif));
@@ -144,6 +155,10 @@ export const FileInfo: React.FC<FileInfoProps> = ({
getMapDisableConfirmationDialog(() => updateMapEnabled(false)), getMapDisableConfirmationDialog(() => updateMapEnabled(false)),
); );
const handleSelectFace = (annotatedFaceID: AnnotatedFaceID) => {
console.log(annotatedFaceID);
};
return ( return (
<FileInfoSidebar open={showInfo} onClose={handleCloseInfo}> <FileInfoSidebar open={showInfo} onClose={handleCloseInfo}>
<Titlebar onClose={handleCloseInfo} title={t("INFO")} backIsClose /> <Titlebar onClose={handleCloseInfo} title={t("INFO")} backIsClose />
@@ -282,11 +297,17 @@ export const FileInfo: React.FC<FileInfoProps> = ({
</InfoItem> </InfoItem>
)} )}
{isMLEnabled() && ( {isMLEnabled() && annotatedFaces && (
<> <>
{annotatedFaces?.annotatedFaceIDs.length && <AnnotatedFacePeopleList
// {/* TODO-Cluster <PhotoPeopleList file={file} /> */} enteFile={file}
<UnidentifiedFaces enteFile={file} /> annotatedFaceIDs={annotatedFaces.annotatedFaceIDs}
onSelectFace={handleSelectFace}
/>
<UnclusteredFaceList
enteFile={file}
faceIDs={annotatedFaces.otherFaceIDs}
/>
</> </>
)} )}
</Stack> </Stack>

View File

@@ -1,4 +1,5 @@
import { useIsMobileWidth } from "@/base/hooks"; import { useIsMobileWidth } from "@/base/hooks";
import { pt } from "@/base/i18n";
import { faceCrop, type AnnotatedFaceID } from "@/new/photos/services/ml"; import { faceCrop, type AnnotatedFaceID } from "@/new/photos/services/ml";
import type { Person } from "@/new/photos/services/ml/people"; import type { Person } from "@/new/photos/services/ml/people";
import type { EnteFile } from "@/new/photos/types/file"; import type { EnteFile } from "@/new/photos/types/file";
@@ -25,7 +26,7 @@ export const SearchPeopleList: React.FC<SearchPeopleListProps> = ({
sx={{ justifyContent: people.length > 3 ? "center" : "start" }} sx={{ justifyContent: people.length > 3 ? "center" : "start" }}
> >
{people.slice(0, isMobileWidth ? 6 : 7).map((person) => ( {people.slice(0, isMobileWidth ? 6 : 7).map((person) => (
<SearchPeopleButton <SearchPersonButton
key={person.id} key={person.id}
onClick={() => onSelectPerson(person)} onClick={() => onSelectPerson(person)}
> >
@@ -34,7 +35,7 @@ export const SearchPeopleList: React.FC<SearchPeopleListProps> = ({
enteFile={person.displayFaceFile} enteFile={person.displayFaceFile}
placeholderDimension={87} placeholderDimension={87}
/> />
</SearchPeopleButton> </SearchPersonButton>
))} ))}
</SearchPeopleContainer> </SearchPeopleContainer>
); );
@@ -49,7 +50,7 @@ const SearchPeopleContainer = styled("div")`
margin-block-end: 15px; margin-block-end: 15px;
`; `;
const SearchPeopleButton = styled(UnstyledButton)( const SearchPersonButton = styled(UnstyledButton)(
({ theme }) => ` ({ theme }) => `
width: 87px; width: 87px;
height: 87px; height: 87px;
@@ -67,6 +68,13 @@ const SearchPeopleButton = styled(UnstyledButton)(
); );
export interface AnnotatedFacePeopleListProps { 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[]; annotatedFaceIDs: AnnotatedFaceID[];
/** /**
* Called when the user selects a face in the list. * Called when the user selects a face in the list.
@@ -80,41 +88,43 @@ export interface AnnotatedFacePeopleListProps {
*/ */
export const AnnotatedFacePeopleList: React.FC< export const AnnotatedFacePeopleList: React.FC<
AnnotatedFacePeopleListProps AnnotatedFacePeopleListProps
> = ({ annotatedFaceIDs, onSelectFace }) => { > = ({ enteFile, annotatedFaceIDs, onSelectFace }) => {
const isMobileWidth = useIsMobileWidth(); if (annotatedFaceIDs.length == 0) return <></>;
return ( return (
<SearchPeopleContainer <>
sx={{ justifyContent: people.length > 3 ? "center" : "start" }} <Typography variant="large" p={1}>
> {t("people")}
{people.slice(0, isMobileWidth ? 6 : 7).map((person) => ( </Typography>
<SearchPeopleButton <AnnotatedFacePeopleContainer>
key={person.id} {annotatedFaceIDs.map((annotatedFaceID) => (
onClick={() => onSelectPerson(person)} <AnnotatedFaceButton
> key={annotatedFaceID.faceID}
<FaceCropImageView onClick={() => onSelectFace(annotatedFaceID)}
faceID={person.displayFaceID} >
enteFile={person.displayFaceFile} <FaceCropImageView
placeholderDimension={87} faceID={annotatedFaceID.faceID}
/> enteFile={enteFile}
</SearchPeopleButton> placeholderDimension={112}
))} />
</SearchPeopleContainer> </AnnotatedFaceButton>
))}
</AnnotatedFacePeopleContainer>
</>
); );
}; };
const SearchPeopleContainer = styled("div")` const AnnotatedFacePeopleContainer = styled("div")`
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
margin-block-start: 12px;
margin-block-end: 15px;
`; `;
const SearchPeopleButton = styled(UnstyledButton)( const AnnotatedFaceButton = styled(UnstyledButton)(
({ theme }) => ` ({ theme }) => `
width: 87px; width: 112px;
height: 87px; height: 112px;
border-radius: 50%; border-radius: 50%;
overflow: hidden; overflow: hidden;
& > img { & > 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<UnclusteredFaceListProps> = ({
enteFile,
faceIDs,
}) => {
if (faceIDs.length == 0) return <></>;
return (
<>
<Typography variant="large" p={1}>
{pt("Other faces")}
{/*t("UNIDENTIFIED_FACES") TODO-Cluster */}
</Typography>
<UnclusteredFacesContainer>
{faceIDs.map((faceID) => (
<UnclusteredFace key={faceID}>
<FaceCropImageView
placeholderDimension={112}
{...{ enteFile, faceID }}
/>
</UnclusteredFace>
))}
</UnclusteredFacesContainer>
</>
);
};
const UnclusteredFacesContainer = styled("div")`
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
@@ -138,63 +189,19 @@ const FaceChipContainer = styled("div")`
overflow: auto; overflow: auto;
`; `;
const FaceChip = styled("div")<{ clickable?: boolean }>` const UnclusteredFace = styled("div")`
width: 112px; width: 112px;
height: 112px; height: 112px;
margin: 5px; margin: 5px;
border-radius: 50%; border-radius: 50%;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
cursor: ${({ clickable }) => (clickable ? "pointer" : "normal")};
& > img { & > img {
width: 100%; width: 100%;
height: 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<UnidentifiedFacesProps> = ({
enteFile,
}) => {
const [faceIDs, setFaceIDs] = useState<string[]>([]);
if (faceIDs.length == 0) return <></>;
return (
<>
<Typography variant="large" p={1}>
{t("UNIDENTIFIED_FACES")}
</Typography>
<FaceChipContainer>
{faceIDs.map((faceID) => (
<FaceChip key={faceID}>
<FaceCropImageView
placeholderDimension={112}
{...{ enteFile, faceID }}
/>
</FaceChip>
))}
</FaceChipContainer>
</>
);
};
interface FaceCropImageViewProps { interface FaceCropImageViewProps {
/** The ID of the face to display. */ /** The ID of the face to display. */
faceID: string; faceID: string;