@@ -78,7 +78,6 @@ type CollectionsProps = Omit<
|
||||
*/
|
||||
export const GalleryBarAndListHeader: React.FC<CollectionsProps> = ({
|
||||
shouldHide,
|
||||
showPeopleSectionButton,
|
||||
mode,
|
||||
onChangeMode,
|
||||
collectionSummaries,
|
||||
@@ -193,7 +192,6 @@ export const GalleryBarAndListHeader: React.FC<CollectionsProps> = ({
|
||||
<>
|
||||
<GalleryBarImpl
|
||||
{...{
|
||||
showPeopleSectionButton,
|
||||
mode,
|
||||
onChangeMode,
|
||||
activeCollectionID,
|
||||
|
||||
@@ -14,6 +14,7 @@ const DropDiv = styled("div")`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`;
|
||||
const Overlay = styled("div")`
|
||||
border-width: 8px;
|
||||
|
||||
@@ -26,8 +26,7 @@ import {
|
||||
import { useAppContext } from "@/new/photos/types/context";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||
import ScienceIcon from "@mui/icons-material/Science";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import { Stack } from "@mui/material";
|
||||
import DropdownInput from "components/DropdownInput";
|
||||
import { t } from "i18next";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
@@ -65,6 +64,15 @@ export const Preferences: React.FC<NestedSidebarDrawerVisibilityProps> = ({
|
||||
/>
|
||||
<Stack sx={{ px: "16px", py: "20px", gap: "24px" }}>
|
||||
<LanguageSelector />
|
||||
{isMLSupported && (
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
endIcon={<ChevronRight />}
|
||||
onClick={showMLSettings}
|
||||
label={t("ml_search")}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
)}
|
||||
<EnteMenuItem
|
||||
onClick={showMapSettings}
|
||||
endIcon={<ChevronRight />}
|
||||
@@ -75,21 +83,6 @@ export const Preferences: React.FC<NestedSidebarDrawerVisibilityProps> = ({
|
||||
endIcon={<ChevronRight />}
|
||||
label={t("advanced")}
|
||||
/>
|
||||
{isMLSupported && (
|
||||
<Box>
|
||||
<MenuSectionTitle
|
||||
title={t("labs")}
|
||||
icon={<ScienceIcon />}
|
||||
/>
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
endIcon={<ChevronRight />}
|
||||
onClick={showMLSettings}
|
||||
label={t("ml_search")}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<MapSettings
|
||||
|
||||
@@ -844,10 +844,6 @@ export default function Gallery() {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
// `peopleState` will be undefined only when ML is disabled, otherwise it'll
|
||||
// be present, with empty arrays, even if people data is still syncing.
|
||||
const showPeopleSectionButton = peopleState !== undefined;
|
||||
|
||||
return (
|
||||
<GalleryContext.Provider
|
||||
value={{
|
||||
@@ -965,7 +961,6 @@ export default function Gallery() {
|
||||
setActiveCollectionID: handleSetActiveCollectionID,
|
||||
hiddenCollectionSummaries:
|
||||
state.hiddenCollectionSummaries,
|
||||
showPeopleSectionButton,
|
||||
people:
|
||||
(state.view.type == "people"
|
||||
? state.view.visiblePeople
|
||||
|
||||
@@ -49,6 +49,7 @@ body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pswp__button--custom {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const SpaceBetweenFlex = styled("div")`
|
||||
* A flex child that fills the entire flex direction, and shows its children
|
||||
* after centering them both vertically and horizontally.
|
||||
*/
|
||||
export const CenteredBox = styled("div")`
|
||||
export const CenteredFill = styled("div")`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -41,13 +41,10 @@ import {
|
||||
type ListChildComponentProps,
|
||||
areEqual,
|
||||
} from "react-window";
|
||||
import { isMLSupported } from "../../services/ml";
|
||||
import type { GalleryBarMode } from "./reducer";
|
||||
|
||||
export interface GalleryBarImplProps {
|
||||
/**
|
||||
* When `true`, the bar shows a button to switch to the people section.
|
||||
*/
|
||||
showPeopleSectionButton: boolean;
|
||||
/**
|
||||
* What are we displaying currently.
|
||||
*/
|
||||
@@ -100,7 +97,6 @@ export interface GalleryBarImplProps {
|
||||
}
|
||||
|
||||
export const GalleryBarImpl: React.FC<GalleryBarImplProps> = ({
|
||||
showPeopleSectionButton,
|
||||
mode,
|
||||
onChangeMode,
|
||||
collectionSummaries,
|
||||
@@ -255,9 +251,7 @@ export const GalleryBarImpl: React.FC<GalleryBarImplProps> = ({
|
||||
sx={people.length ? {} : { borderBlockEndColor: "transparent" }}
|
||||
>
|
||||
<Row1>
|
||||
<ModeIndicator
|
||||
{...{ showPeopleSectionButton, mode, onChangeMode }}
|
||||
/>
|
||||
<ModeIndicator {...{ mode, onChangeMode }} />
|
||||
{controls1}
|
||||
</Row1>
|
||||
<Row2>
|
||||
@@ -314,20 +308,17 @@ export const Row2 = styled(Box)`
|
||||
`;
|
||||
|
||||
const ModeIndicator: React.FC<
|
||||
Pick<
|
||||
GalleryBarImplProps,
|
||||
"showPeopleSectionButton" | "mode" | "onChangeMode"
|
||||
>
|
||||
> = ({ showPeopleSectionButton, mode, onChangeMode }) => {
|
||||
Pick<GalleryBarImplProps, "mode" | "onChangeMode">
|
||||
> = ({ mode, onChangeMode }) => {
|
||||
// Mode switcher is not shown in the hidden albums section.
|
||||
if (mode == "hidden-albums") {
|
||||
return <Typography>{t("hidden_albums")}</Typography>;
|
||||
}
|
||||
|
||||
// Show the static mode indicator with only the "Albums" title if we have
|
||||
// not been asked to show the people button (there are no other sections to
|
||||
// switch to in such a case).
|
||||
if (!showPeopleSectionButton) {
|
||||
// Show the static mode indicator with only the "Albums" title if ML is not
|
||||
// supported on this client (web), since there are no other sections to
|
||||
// switch to in such a case.
|
||||
if (!isMLSupported) {
|
||||
return <Typography>{t("albums")}</Typography>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ActivityErrorIndicator } from "@/base/components/ErrorIndicator";
|
||||
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
|
||||
import { CenteredBox, SpaceBetweenFlex } from "@/base/components/mui/Container";
|
||||
import {
|
||||
CenteredFill,
|
||||
SpaceBetweenFlex,
|
||||
} from "@/base/components/mui/Container";
|
||||
import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton";
|
||||
import { LoadingButton } from "@/base/components/mui/LoadingButton";
|
||||
import {
|
||||
@@ -691,15 +694,15 @@ const SuggestionsDialog: React.FC<SuggestionsDialogProps> = ({
|
||||
sx={{ display: "flex", "&&&": { pt: 0 } }}
|
||||
>
|
||||
{state.activity == "fetching" ? (
|
||||
<CenteredBox>
|
||||
<CenteredFill>
|
||||
<ActivityIndicator>
|
||||
{t("people_suggestions_finding")}
|
||||
</ActivityIndicator>
|
||||
</CenteredBox>
|
||||
</CenteredFill>
|
||||
) : state.fetchFailed ? (
|
||||
<CenteredBox>
|
||||
<CenteredFill>
|
||||
<ActivityErrorIndicator />
|
||||
</CenteredBox>
|
||||
</CenteredFill>
|
||||
) : state.showChoices ? (
|
||||
<SuggestionOrChoiceList
|
||||
items={state.choices}
|
||||
@@ -707,14 +710,14 @@ const SuggestionsDialog: React.FC<SuggestionsDialogProps> = ({
|
||||
onUpdateItem={handleUpdateItem}
|
||||
/>
|
||||
) : state.suggestions.length == 0 ? (
|
||||
<CenteredBox>
|
||||
<CenteredFill>
|
||||
<Typography
|
||||
color="text.muted"
|
||||
sx={{ textAlign: "center" }}
|
||||
>
|
||||
t{"people_suggestions_empty"}
|
||||
</Typography>
|
||||
</CenteredBox>
|
||||
</CenteredFill>
|
||||
) : (
|
||||
<SuggestionOrChoiceList
|
||||
items={state.suggestions}
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
* there.
|
||||
*/
|
||||
|
||||
import { CenteredFill } from "@/base/components/mui/Container";
|
||||
import type { SearchOption } from "@/new/photos/services/search/types";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import { Typography } from "@mui/material";
|
||||
import { Paper, Stack, Typography } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { enableML } from "../../services/ml";
|
||||
import { EnableML, FaceConsent } from "../sidebar/MLSettings";
|
||||
import { useMLStatusSnapshot } from "../utils/use-snapshot";
|
||||
import { useWrapAsyncOperation } from "../utils/use-wrap-async";
|
||||
import { GalleryItemsHeaderAdapter, GalleryItemsSummary } from "./ListHeader";
|
||||
|
||||
/**
|
||||
@@ -46,25 +49,62 @@ export const SearchResultsHeader: React.FC<SearchResultsHeaderProps> = ({
|
||||
export const PeopleEmptyState: React.FC = () => {
|
||||
const mlStatus = useMLStatusSnapshot();
|
||||
|
||||
const message =
|
||||
mlStatus?.phase == "done"
|
||||
? t("people_empty_too_few")
|
||||
: t("syncing_wait");
|
||||
switch (mlStatus?.phase) {
|
||||
case "disabled":
|
||||
return <PeopleEmptyStateDisabled />;
|
||||
case "done":
|
||||
return (
|
||||
<PeopleEmptyStateMessage>
|
||||
{t("people_empty_too_few")}
|
||||
</PeopleEmptyStateMessage>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<PeopleEmptyStateMessage>
|
||||
{t("syncing_wait")}
|
||||
</PeopleEmptyStateMessage>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const PeopleEmptyStateMessage: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => (
|
||||
<CenteredFill>
|
||||
<Typography
|
||||
color="text.muted"
|
||||
sx={{
|
||||
mx: 1,
|
||||
// Approximately compensate for the hidden section bar (86px),
|
||||
// and then add a bit extra padding so that the message appears
|
||||
// visually off the center, towards the top.
|
||||
paddingBlockEnd: "126px",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
</CenteredFill>
|
||||
);
|
||||
|
||||
export const PeopleEmptyStateDisabled: React.FC = () => {
|
||||
const [showConsent, setShowConsent] = useState(false);
|
||||
|
||||
const handleConsent = useWrapAsyncOperation(async () => {
|
||||
await enableML();
|
||||
});
|
||||
|
||||
return (
|
||||
<VerticallyCentered>
|
||||
<Typography
|
||||
color="text.muted"
|
||||
sx={{
|
||||
mx: 1,
|
||||
// Approximately compensate for the hidden section bar (86px),
|
||||
// and then add a bit extra padding so that the message appears
|
||||
// visually off the center, towards the top.
|
||||
paddingBlockEnd: "126px",
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</Typography>
|
||||
</VerticallyCentered>
|
||||
<Stack sx={{ alignItems: "center", flex: 1, overflow: "auto" }}>
|
||||
<Paper sx={{ maxWidth: "390px", padding: "4px", mb: "2rem" }}>
|
||||
{!showConsent ? (
|
||||
<EnableML onEnable={() => setShowConsent(true)} />
|
||||
) : (
|
||||
<FaceConsent
|
||||
onConsent={handleConsent}
|
||||
onCancel={() => setShowConsent(false)}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -54,7 +54,7 @@ export const MLSettings: React.FC<NestedSidebarDrawerVisibilityProps> = ({
|
||||
if (!mlStatus) {
|
||||
component = <Loading />;
|
||||
} else if (mlStatus.phase == "disabled") {
|
||||
component = <EnableML onEnable={handleEnableML} />;
|
||||
component = <EnableML onEnable={handleEnableML} showMagicSearchHint />;
|
||||
} else {
|
||||
component = (
|
||||
<ManageML {...{ mlStatus }} onDisableML={handleDisableML} />
|
||||
@@ -77,7 +77,7 @@ export const MLSettings: React.FC<NestedSidebarDrawerVisibilityProps> = ({
|
||||
</Stack>
|
||||
</NestedSidebarDrawer>
|
||||
|
||||
<FaceConsent
|
||||
<FaceConsentDrawer
|
||||
open={openFaceConsent}
|
||||
onClose={() => setOpenFaceConsent(false)}
|
||||
onRootClose={handleRootClose}
|
||||
@@ -98,9 +98,16 @@ const Loading: React.FC = () => {
|
||||
interface EnableMLProps {
|
||||
/** Called when the user enables ML. */
|
||||
onEnable: () => void;
|
||||
/**
|
||||
* If true, a footnote describing the magic search feature will be shown.
|
||||
*/
|
||||
showMagicSearchHint?: boolean;
|
||||
}
|
||||
|
||||
const EnableML: React.FC<EnableMLProps> = ({ onEnable }) => {
|
||||
export const EnableML: React.FC<EnableMLProps> = ({
|
||||
onEnable,
|
||||
showMagicSearchHint,
|
||||
}) => {
|
||||
const moreDetails = () =>
|
||||
openURL("https://help.ente.io/photos/features/machine-learning");
|
||||
|
||||
@@ -118,35 +125,63 @@ const EnableML: React.FC<EnableMLProps> = ({ onEnable }) => {
|
||||
{t("more_details")}
|
||||
</Button>
|
||||
</Stack>
|
||||
<Typography color="text.faint" variant="small">
|
||||
{t("ml_search_footnote")}
|
||||
</Typography>
|
||||
{showMagicSearchHint && (
|
||||
<Typography color="text.faint" variant="small">
|
||||
{t("ml_search_footnote")}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
type FaceConsentProps = NestedSidebarDrawerVisibilityProps & {
|
||||
/** Called when the user provides their consent. */
|
||||
onConsent: () => void;
|
||||
};
|
||||
type FaceConsentDrawerProps = NestedSidebarDrawerVisibilityProps &
|
||||
Pick<FaceConsentProps, "onConsent">;
|
||||
|
||||
const FaceConsent: React.FC<FaceConsentProps> = ({
|
||||
const FaceConsentDrawer: React.FC<FaceConsentDrawerProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onRootClose,
|
||||
onConsent,
|
||||
}) => {
|
||||
const [acceptTerms, setAcceptTerms] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setAcceptTerms(false);
|
||||
}, [open]);
|
||||
|
||||
const handleRootClose = () => {
|
||||
onClose();
|
||||
onRootClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<NestedSidebarDrawer
|
||||
{...{ open, onClose }}
|
||||
onRootClose={handleRootClose}
|
||||
>
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<SidebarDrawerTitlebar
|
||||
onClose={onClose}
|
||||
onRootClose={handleRootClose}
|
||||
title={t("ml_consent_title")}
|
||||
/>
|
||||
<FaceConsent onConsent={onConsent} onCancel={onClose} />
|
||||
</Stack>
|
||||
</NestedSidebarDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
interface FaceConsentProps {
|
||||
/** Called when the user provides their consent. */
|
||||
onConsent: () => void;
|
||||
/** Called when the user cancels out. */
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const FaceConsent: React.FC<FaceConsentProps> = ({
|
||||
onConsent,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [acceptTerms, setAcceptTerms] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setAcceptTerms(false);
|
||||
}, []);
|
||||
|
||||
const privacyPolicyLink = (
|
||||
<Link
|
||||
target="_blank"
|
||||
@@ -160,62 +195,44 @@ const FaceConsent: React.FC<FaceConsentProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<NestedSidebarDrawer
|
||||
{...{ open, onClose }}
|
||||
onRootClose={handleRootClose}
|
||||
>
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<SidebarDrawerTitlebar
|
||||
onClose={onClose}
|
||||
onRootClose={handleRootClose}
|
||||
title={t("ml_consent_title")}
|
||||
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
|
||||
<Typography component="div" color="text.muted" px={"8px"}>
|
||||
<Trans
|
||||
i18nKey={"ml_consent_description"}
|
||||
components={{ a: privacyPolicyLink }}
|
||||
/>
|
||||
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
|
||||
<Typography component="div" color="text.muted" px={"8px"}>
|
||||
<Trans
|
||||
i18nKey={"ml_consent_description"}
|
||||
components={{ a: privacyPolicyLink }}
|
||||
</Typography>
|
||||
<FormGroup sx={{ width: "100%" }}>
|
||||
<FormControlLabel
|
||||
sx={{
|
||||
color: "text.muted",
|
||||
ml: 0,
|
||||
mt: 2,
|
||||
}}
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={acceptTerms}
|
||||
onChange={(e) => setAcceptTerms(e.target.checked)}
|
||||
/>
|
||||
</Typography>
|
||||
<FormGroup sx={{ width: "100%" }}>
|
||||
<FormControlLabel
|
||||
sx={{
|
||||
color: "text.muted",
|
||||
ml: 0,
|
||||
mt: 2,
|
||||
}}
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={acceptTerms}
|
||||
onChange={(e) =>
|
||||
setAcceptTerms(e.target.checked)
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={t("ml_consent_confirmation")}
|
||||
/>
|
||||
</FormGroup>
|
||||
<Stack px={"8px"} spacing={"8px"}>
|
||||
<Button
|
||||
color={"accent"}
|
||||
size="large"
|
||||
disabled={!acceptTerms}
|
||||
onClick={onConsent}
|
||||
>
|
||||
{t("ml_consent")}
|
||||
</Button>
|
||||
<Button
|
||||
color={"secondary"}
|
||||
size="large"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
label={t("ml_consent_confirmation")}
|
||||
/>
|
||||
</FormGroup>
|
||||
<Stack px={"8px"} spacing={"8px"}>
|
||||
<Button
|
||||
color={"accent"}
|
||||
size="large"
|
||||
disabled={!acceptTerms}
|
||||
onClick={onConsent}
|
||||
>
|
||||
{t("ml_consent")}
|
||||
</Button>
|
||||
<Button color={"secondary"} size="large" onClick={onCancel}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</NestedSidebarDrawer>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user