This commit is contained in:
Manav Rathi
2024-10-18 11:01:59 +05:30
parent 2f7ea0f232
commit faa2e1edfb
2 changed files with 153 additions and 166 deletions

View File

@@ -1,161 +0,0 @@
import type { ModalVisibilityProps } from "@/base/components/utils/modal";
import { pt } from "@/base/i18n";
import { addCGroup, addClusterToCGroup } from "@/new/photos/services/ml";
import { ensure } from "@/utils/ensure";
import {
Dialog,
DialogContent,
DialogTitle,
styled,
Typography,
useMediaQuery,
} from "@mui/material";
import { t } from "i18next";
import React, { useState } from "react";
import type { FaceCluster } from "../services/ml/cluster";
import type { CGroupPerson, Person } from "../services/ml/people";
import { SpaceBetweenFlex, type ButtonishProps } from "./mui";
import { DialogCloseIconButton } from "./mui/Dialog";
import { SingleInputDialog } from "./SingleInputForm";
import {
ItemCard,
LargeTileButton,
LargeTilePlusOverlay,
LargeTileTextOverlay,
} from "./Tiles";
import { useWrapAsyncOperation } from "./use-wrap-async";
type AddPersonDialogProps = ModalVisibilityProps & {
/**
* The list of people from show the existing named people.
*/
people: Person[];
/**
* The cluster to add to the selected person (existing or new).
*/
cluster: FaceCluster;
};
/**
* A dialog allowing the user to select one of the existing named persons they
* have, or create a new one, and then associate the provided cluster to it,
* creating or updating a remote "person".
*/
export const AddPersonDialog: React.FC<AddPersonDialogProps> = ({
open,
onClose,
people,
cluster,
}) => {
const isFullScreen = useMediaQuery("(max-width: 490px)");
const [openNameInput, setOpenNameInput] = useState(false);
const cgroupPeople: CGroupPerson[] = people.filter(
(p) => p.type != "cluster",
);
const handleAddPerson = () => setOpenNameInput(true);
const handleSelectPerson = useWrapAsyncOperation((id: string) =>
addClusterToCGroup(
ensure(cgroupPeople.find((p) => p.id == id)).cgroup,
cluster,
),
);
const handleAddPersonWithName = (name: string) => addCGroup(name, cluster);
// [Note: Calling setState during rendering]
//
// Calling setState during rendering should be avoided when there are
// cleaner alternatives, but it is not completely verboten, and it has
// documented semantics:
//
// > React will discard the currently rendering component's output and
// > immediately attempt to render it again with the new state.
// >
// > https://react.dev/reference/react/useState
// If we're opened without any existing people that can be selected, jump
// directly to the add person dialog.
if (open && !openNameInput && !cgroupPeople.length) {
onClose();
setOpenNameInput(true);
return <></>;
}
return (
<>
<Dialog
{...{ open, onClose }}
fullWidth
fullScreen={isFullScreen}
PaperProps={{ sx: { maxWidth: "490px" } }}
>
<SpaceBetweenFlex sx={{ padding: "10px 8px 6px 0" }}>
<DialogTitle variant="h3" fontWeight={"bold"}>
{pt("Add name")}
</DialogTitle>
<DialogCloseIconButton {...{ onClose }} />
</SpaceBetweenFlex>
<DialogContent_>
<AddPerson onClick={handleAddPerson} />
{cgroupPeople.map((person) => (
<PersonButton
key={person.id}
person={person}
onPersonClick={handleSelectPerson}
/>
))}
</DialogContent_>
</Dialog>
<SingleInputDialog
open={openNameInput}
onClose={() => setOpenNameInput(false)}
title={pt("New person") /* TODO-Cluster */}
label={pt("Add name")}
placeholder={t("enter_name")}
autoComplete="name"
autoFocus
submitButtonTitle={t("add")}
onSubmit={handleAddPersonWithName}
/>
</>
);
};
const DialogContent_ = styled(DialogContent)`
display: flex;
flex-wrap: wrap;
gap: 4px;
`;
interface PersonButtonProps {
person: Person;
onPersonClick: (personID: string) => void;
}
const PersonButton: React.FC<PersonButtonProps> = ({
person,
onPersonClick,
}) => (
<ItemCard
TileComponent={LargeTileButton}
coverFile={person.displayFaceFile}
coverFaceID={person.displayFaceID}
onClick={() => onPersonClick(person.id)}
>
<LargeTileTextOverlay>
<Typography>{person.name ?? ""}</Typography>
</LargeTileTextOverlay>
</ItemCard>
);
const AddPerson: React.FC<ButtonishProps> = ({ onClick }) => (
<ItemCard TileComponent={LargeTileButton} onClick={onClick}>
<LargeTileTextOverlay>{pt("New person")}</LargeTileTextOverlay>
<LargeTilePlusOverlay>+</LargeTilePlusOverlay>
</ItemCard>
);

View File

@@ -13,6 +13,8 @@ import { useIsSmallWidth } from "@/base/hooks";
import { pt } from "@/base/i18n";
import log from "@/base/log";
import {
addCGroup,
addClusterToCGroup,
applyPersonSuggestionUpdates,
deleteCGroup,
ignoreCluster,
@@ -27,6 +29,7 @@ import {
type PersonSuggestionUpdates,
type PreviewableCluster,
} from "@/new/photos/services/ml/people";
import { ensure } from "@/utils/ensure";
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
import AddIcon from "@mui/icons-material/Add";
@@ -46,18 +49,28 @@ import {
List,
ListItem,
Stack,
styled,
ToggleButton,
ToggleButtonGroup,
Tooltip,
Typography,
useMediaQuery,
} from "@mui/material";
import { t } from "i18next";
import React, { useEffect, useReducer } from "react";
import React, { useEffect, useReducer, useState } from "react";
import type { FaceCluster } from "../../services/ml/cluster";
import { useAppContext } from "../../types/context";
import { AddPersonDialog } from "../AddPersonDialog";
import { SpaceBetweenFlex } from "../mui";
import { SpaceBetweenFlex, type ButtonishProps } from "../mui";
import { DialogCloseIconButton } from "../mui/Dialog";
import { SuggestionFaceList } from "../PeopleList";
import { SingleInputDialog } from "../SingleInputForm";
import {
ItemCard,
LargeTileButton,
LargeTilePlusOverlay,
LargeTileTextOverlay,
} from "../Tiles";
import { useWrapAsyncOperation } from "../use-wrap-async";
import type { GalleryBarImplProps } from "./BarImpl";
import { GalleryItemsHeaderAdapter, GalleryItemsSummary } from "./ListHeader";
@@ -147,7 +160,8 @@ const CGroupPersonHeader: React.FC<CGroupPersonHeaderProps> = ({
});
// While technically it is possible for the cgroup not to have a name, logic
// wise we shouldn't be ending up here without a name.
// wise we shouldn't be ending up here without a name (this state is
// expected to be reached only for named persons).
const name = cgroup.data.name ?? "";
return (
@@ -197,7 +211,6 @@ const CGroupPersonHeader: React.FC<CGroupPersonHeaderProps> = ({
submitButtonTitle={t("rename")}
onSubmit={handleRename}
/>
<SuggestionsDialog
{...suggestionsVisibilityProps}
{...{ person }}
@@ -278,6 +291,141 @@ const ClusterPersonHeader: React.FC<ClusterPersonHeaderProps> = ({
);
};
type AddPersonDialogProps = ModalVisibilityProps & {
/**
* The list of people from show the existing named people.
*/
people: Person[];
/**
* The cluster to add to the selected person (existing or new).
*/
cluster: FaceCluster;
};
/**
* A dialog allowing the user to select one of the existing named persons they
* have, or create a new one, and then associate the provided cluster to it,
* creating or updating a remote "person".
*/
const AddPersonDialog: React.FC<AddPersonDialogProps> = ({
open,
onClose,
people,
cluster,
}) => {
const isFullScreen = useMediaQuery("(max-width: 490px)");
const [openNameInput, setOpenNameInput] = useState(false);
const cgroupPeople: CGroupPerson[] = people.filter(
(p) => p.type != "cluster",
);
const handleAddPerson = () => setOpenNameInput(true);
const handleSelectPerson = useWrapAsyncOperation((id: string) =>
addClusterToCGroup(
ensure(cgroupPeople.find((p) => p.id == id)).cgroup,
cluster,
),
);
const handleAddPersonWithName = (name: string) => addCGroup(name, cluster);
// [Note: Calling setState during rendering]
//
// Calling setState during rendering should be avoided when there are
// cleaner alternatives, but it is not completely verboten, and it has
// documented semantics:
//
// > React will discard the currently rendering component's output and
// > immediately attempt to render it again with the new state.
// >
// > https://react.dev/reference/react/useState
// If we're opened without any existing people that can be selected, jump
// directly to the add person dialog.
if (open && !openNameInput && !cgroupPeople.length) {
onClose();
setOpenNameInput(true);
return <></>;
}
return (
<>
<Dialog
{...{ open, onClose }}
fullWidth
fullScreen={isFullScreen}
PaperProps={{ sx: { maxWidth: "490px" } }}
>
<SpaceBetweenFlex sx={{ padding: "10px 8px 6px 0" }}>
<DialogTitle variant="h3" fontWeight={"bold"}>
{pt("Add name")}
</DialogTitle>
<DialogCloseIconButton {...{ onClose }} />
</SpaceBetweenFlex>
<DialogContent_>
<AddPerson onClick={handleAddPerson} />
{cgroupPeople.map((person) => (
<PersonButton
key={person.id}
person={person}
onPersonClick={handleSelectPerson}
/>
))}
</DialogContent_>
</Dialog>
<SingleInputDialog
open={openNameInput}
onClose={() => setOpenNameInput(false)}
title={pt("New person") /* TODO-Cluster */}
label={pt("Add name")}
placeholder={t("enter_name")}
autoComplete="name"
autoFocus
submitButtonTitle={t("add")}
onSubmit={handleAddPersonWithName}
/>
</>
);
};
const DialogContent_ = styled(DialogContent)`
display: flex;
flex-wrap: wrap;
gap: 4px;
`;
interface PersonButtonProps {
person: Person;
onPersonClick: (personID: string) => void;
}
const PersonButton: React.FC<PersonButtonProps> = ({
person,
onPersonClick,
}) => (
<ItemCard
TileComponent={LargeTileButton}
coverFile={person.displayFaceFile}
coverFaceID={person.displayFaceID}
onClick={() => onPersonClick(person.id)}
>
<LargeTileTextOverlay>
<Typography>{person.name ?? ""}</Typography>
</LargeTileTextOverlay>
</ItemCard>
);
const AddPerson: React.FC<ButtonishProps> = ({ onClick }) => (
<ItemCard TileComponent={LargeTileButton} onClick={onClick}>
<LargeTileTextOverlay>{pt("New person")}</LargeTileTextOverlay>
<LargeTilePlusOverlay>+</LargeTilePlusOverlay>
</ItemCard>
);
type SuggestionsDialogProps = ModalVisibilityProps & {
person: CGroupPerson;
};