mirror of
https://github.com/ente-io/ente.git
synced 2025-08-12 17:20:37 +00:00
people list checkpoint
This commit is contained in:
parent
4e04739d54
commit
2827a166dc
@ -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,8 +117,9 @@ export const FileInfo: React.FC<FileInfoProps> = ({
|
|||||||
return exif?.parsed?.location;
|
return exif?.parsed?.location;
|
||||||
}, [file, exif]);
|
}, [file, exif]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
let didCancel = false;
|
let didCancel = false;
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
@ -117,10 +127,11 @@ export const FileInfo: React.FC<FileInfoProps> = ({
|
|||||||
!didCancel && setAnnotatedFaces(result);
|
!didCancel && setAnnotatedFaces(result);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => { didCancel = true;}
|
return () => {
|
||||||
|
didCancel = true;
|
||||||
|
};
|
||||||
}, [file]);
|
}, [file]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExifInfo(parseExifInfo(exif));
|
setExifInfo(parseExifInfo(exif));
|
||||||
}, [exif]);
|
}, [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>
|
||||||
|
@ -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}
|
||||||
|
onClick={() => onSelectFace(annotatedFaceID)}
|
||||||
>
|
>
|
||||||
<FaceCropImageView
|
<FaceCropImageView
|
||||||
faceID={person.displayFaceID}
|
faceID={annotatedFaceID.faceID}
|
||||||
enteFile={person.displayFaceFile}
|
enteFile={enteFile}
|
||||||
placeholderDimension={87}
|
placeholderDimension={112}
|
||||||
/>
|
/>
|
||||||
</SearchPeopleButton>
|
</AnnotatedFaceButton>
|
||||||
))}
|
))}
|
||||||
</SearchPeopleContainer>
|
</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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user