Merge remote-tracking branch 'origin/main' into mobile-ffprobe

This commit is contained in:
Neeraj Gupta
2024-07-12 14:30:54 +05:30
84 changed files with 4250 additions and 944 deletions

View File

@@ -1,4 +1,4 @@
FROM golang:1.20-alpine3.17 as builder
FROM golang:1.20-alpine3.17 AS builder
RUN apk add --no-cache gcc musl-dev git build-base pkgconfig libsodium-dev
ENV GOOS=linux

View File

@@ -1,4 +1,4 @@
FROM golang:1.20-alpine3.17@sha256:9c2f89db6fda13c3c480749787f62fed5831699bb2c32881b8f327f1cf7bae42 as builder386
FROM golang:1.20-alpine3.17@sha256:9c2f89db6fda13c3c480749787f62fed5831699bb2c32881b8f327f1cf7bae42 AS builder386
RUN apt-get update
RUN apt-get install -y gcc
RUN apt-get install -y git

View File

@@ -537,7 +537,7 @@ const setupTrayItem = (mainWindow: BrowserWindow) => {
* old cache dir if it exists.
*
* Added May 2024, v1.7.0. This migration code can be removed after some time
* once most people have upgraded to newer versions.
* once most people have upgraded to newer versions (tag: Migration).
*/
const deleteLegacyDiskCacheDirIfExists = async () => {
const removeIfExists = async (dirPath: string) => {

View File

@@ -3,6 +3,7 @@
*
* The embeddings are computed using ONNX runtime, with CLIP as the model.
*/
import Tokenizer from "clip-bpe-js";
import * as ort from "onnxruntime-node";
import log from "../log";

View File

@@ -6,6 +6,7 @@
*
* The runtime used is ONNX.
*/
import * as ort from "onnxruntime-node";
import log from "../log";
import { ensure } from "../utils/common";

View File

@@ -1,5 +1,5 @@
/**
* @file AI/ML related functionality, generic layer.
* @file ML related functionality, generic layer.
*
* @see also `ml-clip.ts`, `ml-face.ts`.
*
@@ -10,6 +10,7 @@
* can use the binary ONNX runtime which is 10-20x faster than the WASM based
* web one.
*/
import { app, net } from "electron/main";
import { existsSync } from "fs";
import fs from "node:fs/promises";

View File

@@ -58,7 +58,7 @@ export const pendingUploads = async (): Promise<PendingUploads | undefined> => {
const allZipItems = uploadStatusStore.get("zipItems");
let zipItems: typeof allZipItems;
// Migration code - May 2024. Remove after a bit.
// Migration code - May 2024. Remove after a bit (tag: Migration).
//
// The older store formats will not have zipItems and instead will have
// zipPaths. If we find such a case, read the zipPaths and enqueue all of

View File

@@ -28,6 +28,7 @@ import 'messages_no.dart' as messages_no;
import 'messages_pl.dart' as messages_pl;
import 'messages_pt.dart' as messages_pt;
import 'messages_ru.dart' as messages_ru;
import 'messages_tr.dart' as messages_tr;
import 'messages_zh.dart' as messages_zh;
typedef Future<dynamic> LibraryLoader();
@@ -44,6 +45,7 @@ Map<String, LibraryLoader> _deferredLibraries = {
'pl': () => new SynchronousFuture(null),
'pt': () => new SynchronousFuture(null),
'ru': () => new SynchronousFuture(null),
'tr': () => new SynchronousFuture(null),
'zh': () => new SynchronousFuture(null),
};
@@ -73,6 +75,8 @@ MessageLookupByLibrary? _findExact(String localeName) {
return messages_pt.messages;
case 'ru':
return messages_ru.messages;
case 'tr':
return messages_tr.messages;
case 'zh':
return messages_zh.messages;
default:

1710
mobile/lib/generated/intl/messages_tr.dart generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9123,6 +9123,7 @@ class AppLocalizationDelegate extends LocalizationsDelegate<S> {
Locale.fromSubtags(languageCode: 'pl'),
Locale.fromSubtags(languageCode: 'pt'),
Locale.fromSubtags(languageCode: 'ru'),
Locale.fromSubtags(languageCode: 'tr'),
Locale.fromSubtags(languageCode: 'zh'),
];
}

View File

@@ -1,4 +1,5 @@
{
"@@locale ": "en",
"enterYourEmailAddress": "Enter your email address",
"accountWelcomeBack": "Welcome back!",
"email": "Email",

1279
mobile/lib/l10n/intl_tr.arb Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
url: "https://pub.dev"
source: hosted
version: "61.0.0"
version: "67.0.0"
_flutterfire_internals:
dependency: transitive
description:
@@ -29,10 +29,10 @@ packages:
dependency: transitive
description:
name: analyzer
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
url: "https://pub.dev"
source: hosted
version: "5.13.0"
version: "6.4.1"
animate_do:
dependency: "direct main"
description:
@@ -1236,6 +1236,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.18.1"
intl_utils:
dependency: "direct dev"
description:
name: intl_utils
sha256: c2b1f5c72c25512cbeef5ab015c008fc50fe7e04813ba5541c25272300484bf4
url: "https://pub.dev"
source: hosted
version: "2.8.7"
io:
dependency: transitive
description:

View File

@@ -204,6 +204,7 @@ dev_dependencies:
freezed: ^2.5.2
integration_test:
sdk: flutter
intl_utils: ^2.8.7
json_serializable: ^6.6.1
test: ^1.22.0

View File

@@ -1,4 +1,4 @@
FROM golang:1.21-alpine3.17 as builder
FROM golang:1.21-alpine3.17 AS builder
RUN apk add --no-cache gcc musl-dev git build-base pkgconfig libsodium-dev
ENV GOOS=linux

View File

@@ -47,7 +47,9 @@ func (u *PasskeyUser) WebAuthnName() string {
}
func (u *PasskeyUser) WebAuthnDisplayName() string {
return u.Name
// Safari requires a display name to be set, otherwise it does not recognize
// security keys.
return u.Email
}
func (u *PasskeyUser) WebAuthnCredentials() []webauthn.Credential {

View File

@@ -33,8 +33,10 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
useEffect(() => {
disableDiskLogs();
// The accounts app has no local state, but some older builds might've
// leftover some scraps. Clear it out. This code added 1 July 2024, can
// be removed after a while (tag: Migration).
// leftover some scraps. Clear it out.
//
// This code added on 1 July 2024, can be removed soon since this data
// was never saved before this was released (tag: Migration).
clearData();
void setupI18n().finally(() => setIsI18nReady(true));
logUnhandledErrorsAndRejections(true);

View File

@@ -1,16 +1,15 @@
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import { MenuItemDivider, MenuItemGroup } from "@/new/shared/components/Menu";
import { Titlebar } from "@/new/shared/components/Titlebar";
import log from "@/next/log";
import { ensure } from "@/utils/ensure";
import { CenteredFlex } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import EnteButton from "@ente/shared/components/EnteButton";
import { EnteDrawer } from "@ente/shared/components/EnteDrawer";
import FormPaper from "@ente/shared/components/Form/FormPaper";
import InfoItem from "@ente/shared/components/Info/InfoItem";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import MenuItemDivider from "@ente/shared/components/Menu/MenuItemDivider";
import { MenuItemGroup } from "@ente/shared/components/Menu/MenuItemGroup";
import SingleInputForm from "@ente/shared/components/SingleInputForm";
import Titlebar from "@ente/shared/components/Titlebar";
import { formatDateTimeFull } from "@ente/shared/time/format";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";

View File

@@ -454,7 +454,61 @@ export const beginPasskeyAuthentication = async (
*/
export const signChallenge = async (
publicKey: PublicKeyCredentialRequestOptions,
) => nullToUndefined(await navigator.credentials.get({ publicKey }));
) => {
// Hint all transports to make security keys like Yubikey work across
// varying registration/verification scenarios.
//
// During verification, we need to pass a `transport` property.
//
// > The `transports` property is hint of the methods that the client could
// > use to communicate with the relevant authenticator of the public key
// > credential to retrieve. Possible values are ["ble", "hybrid",
// > "internal", "nfc", "usb"].
// >
// > MDN
//
// When we register a passkey, we save the transport alongwith the
// credential. During authentication, we pass that transport back to the
// browser. This is the approach recommended by the spec:
//
// > When registering a new credential, the Relying Party SHOULD store the
// > value returned from getTransports(). When creating a
// > PublicKeyCredentialDescriptor for that credential, the Relying Party
// > SHOULD retrieve that stored value and set it as the value of the
// > transports member.
// >
// > https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-transports
//
// However, following this recommendation break things currently (2024) in
// various ways. For example, if a user registers a Yubikey NFC security key
// on Firefox on their laptop, then Firefox returns ["usb"]. This is
// incorrect, it should be ["usb", "nfc"] (which is what Chrome does, since
// the hardware itself supports both USB and NFC transports).
//
// Later, if the user tries to verifying with their security key on their
// iPhone Safari via NFC, the browser doesn't recognize it (which seems
// incorrect too, the transport is meant to be a "hint" not a binding).
//
// > Note that these hints represent the WebAuthn Relying Party's best
// > belief as to how an authenticator may be reached.
// >
// > https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-transports
//
// As a workaround, we override transports with known possible values.
for (const cred of publicKey.allowCredentials ?? []) {
cred.transports = [
...(cred.transports ?? []),
"usb",
"nfc",
"ble",
"hybrid",
"internal",
];
}
return nullToUndefined(await navigator.credentials.get({ publicKey }));
};
interface FinishPasskeyAuthenticationOptions {
passkeySessionID: string;

View File

@@ -1,6 +1,6 @@
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import { Titlebar } from "@/new/shared/components/Titlebar";
import { DialogProps, Stack } from "@mui/material";
import { EnteDrawer } from "components/EnteDrawer";
import Titlebar from "components/Titlebar";
import { t } from "i18next";
import { COLLECTION_ROLE, Collection } from "types/collection";

View File

@@ -1,12 +1,14 @@
import {
MenuItemDivider,
MenuItemGroup,
MenuSectionTitle,
} from "@/new/shared/components/Menu";
import { FlexWrapper } from "@ente/shared/components/Container";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import SubmitButton from "@ente/shared/components/SubmitButton";
import DoneIcon from "@mui/icons-material/Done";
import { Button, FormHelperText, Stack } from "@mui/material";
import TextField from "@mui/material/TextField";
import MenuItemDivider from "components/Menu/MenuItemDivider";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
import Avatar from "components/pages/gallery/Avatar";
import { Formik, type FormikHelpers } from "formik";
import { t } from "i18next";

View File

@@ -1,3 +1,10 @@
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import {
MenuItemDivider,
MenuItemGroup,
MenuSectionTitle,
} from "@/new/shared/components/Menu";
import { Titlebar } from "@/new/shared/components/Titlebar";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import Add from "@mui/icons-material/Add";
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
@@ -5,11 +12,6 @@ import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import ModeEditIcon from "@mui/icons-material/ModeEdit";
import Photo from "@mui/icons-material/Photo";
import { DialogProps, Stack } from "@mui/material";
import { EnteDrawer } from "components/EnteDrawer";
import MenuItemDivider from "components/Menu/MenuItemDivider";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
import Titlebar from "components/Titlebar";
import Avatar from "components/pages/gallery/Avatar";
import { t } from "i18next";
import { AppContext } from "pages/_app";

View File

@@ -1,3 +1,6 @@
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import { MenuItemDivider, MenuItemGroup } from "@/new/shared/components/Menu";
import { Titlebar } from "@/new/shared/components/Titlebar";
import log from "@/next/log";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import BlockIcon from "@mui/icons-material/Block";
@@ -5,10 +8,6 @@ import DoneIcon from "@mui/icons-material/Done";
import ModeEditIcon from "@mui/icons-material/ModeEdit";
import PhotoIcon from "@mui/icons-material/Photo";
import { DialogProps, Stack, Typography } from "@mui/material";
import { EnteDrawer } from "components/EnteDrawer";
import MenuItemDivider from "components/Menu/MenuItemDivider";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import Titlebar from "components/Titlebar";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { GalleryContext } from "pages/gallery";

View File

@@ -1,14 +1,16 @@
import { useRef, useState } from "react";
import { COLLECTION_ROLE, Collection } from "types/collection";
import {
MenuItemDivider,
MenuItemGroup,
MenuSectionTitle,
} from "@/new/shared/components/Menu";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import AddIcon from "@mui/icons-material/Add";
import ChevronRight from "@mui/icons-material/ChevronRight";
import Workspaces from "@mui/icons-material/Workspaces";
import { Stack } from "@mui/material";
import MenuItemDivider from "components/Menu/MenuItemDivider";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
import AvatarGroup from "components/pages/gallery/AvatarGroup";
import { t } from "i18next";
import AddParticipant from "./AddParticipant";

View File

@@ -1,6 +1,6 @@
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import { Titlebar } from "@/new/shared/components/Titlebar";
import { DialogProps, Stack } from "@mui/material";
import { EnteDrawer } from "components/EnteDrawer";
import Titlebar from "components/Titlebar";
import { CollectionSummaryType } from "constants/collection";
import { t } from "i18next";
import { Collection, CollectionSummary } from "types/collection";

View File

@@ -1,11 +1,13 @@
import {
MenuItemDivider,
MenuItemGroup,
MenuSectionTitle,
} from "@/new/shared/components/Menu";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import DownloadSharp from "@mui/icons-material/DownloadSharp";
import LinkIcon from "@mui/icons-material/Link";
import PublicIcon from "@mui/icons-material/Public";
import { Stack, Typography } from "@mui/material";
import MenuItemDivider from "components/Menu/MenuItemDivider";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
import { t } from "i18next";
import { GalleryContext } from "pages/gallery";
import { useContext, useState } from "react";

View File

@@ -1,10 +1,9 @@
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import { MenuItemDivider, MenuItemGroup } from "@/new/shared/components/Menu";
import { Titlebar } from "@/new/shared/components/Titlebar";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import ChevronRight from "@mui/icons-material/ChevronRight";
import { DialogProps, Stack } from "@mui/material";
import { EnteDrawer } from "components/EnteDrawer";
import MenuItemDivider from "components/Menu/MenuItemDivider";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import Titlebar from "components/Titlebar";
import { t } from "i18next";
import { useMemo, useState } from "react";
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";

View File

@@ -1,11 +1,10 @@
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import { MenuItemDivider, MenuItemGroup } from "@/new/shared/components/Menu";
import { Titlebar } from "@/new/shared/components/Titlebar";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import RemoveCircleOutline from "@mui/icons-material/RemoveCircleOutline";
import { DialogProps, Stack, Typography } from "@mui/material";
import { EnteDrawer } from "components/EnteDrawer";
import MenuItemDivider from "components/Menu/MenuItemDivider";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import Titlebar from "components/Titlebar";
import { t } from "i18next";
import { GalleryContext } from "pages/gallery";
import { useContext, useState } from "react";

View File

@@ -1,11 +1,10 @@
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import { MenuItemDivider, MenuItemGroup } from "@/new/shared/components/Menu";
import { Titlebar } from "@/new/shared/components/Titlebar";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import { formatDateTime } from "@ente/shared/time/format";
import ChevronRight from "@mui/icons-material/ChevronRight";
import { DialogProps, Stack } from "@mui/material";
import { EnteDrawer } from "components/EnteDrawer";
import MenuItemDivider from "components/Menu/MenuItemDivider";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import Titlebar from "components/Titlebar";
import { t } from "i18next";
import { useMemo, useState } from "react";
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";

View File

@@ -1,7 +1,6 @@
import { MenuItemGroup, MenuSectionTitle } from "@/new/shared/components/Menu";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import { Stack } from "@mui/material";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
import { t } from "i18next";
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";

View File

@@ -1,3 +1,4 @@
import { MenuItemDivider, MenuItemGroup } from "@/new/shared/components/Menu";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import ContentCopyIcon from "@mui/icons-material/ContentCopyOutlined";
@@ -5,8 +6,6 @@ import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import LinkIcon from "@mui/icons-material/Link";
import PublicIcon from "@mui/icons-material/Public";
import { Stack, Typography } from "@mui/material";
import MenuItemDivider from "components/Menu/MenuItemDivider";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import { t } from "i18next";
import { useState } from "react";
import { Collection, PublicURL } from "types/collection";

View File

@@ -1,11 +1,13 @@
import {
MenuItemDivider,
MenuItemGroup,
MenuSectionTitle,
} from "@/new/shared/components/Menu";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
import ModeEditIcon from "@mui/icons-material/ModeEdit";
import Photo from "@mui/icons-material/Photo";
import { Stack } from "@mui/material";
import MenuItemDivider from "components/Menu/MenuItemDivider";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
import Avatar from "components/pages/gallery/Avatar";
import { CollectionSummaryType } from "constants/collection";
import { t } from "i18next";

View File

@@ -1,16 +0,0 @@
import { Divider } from "@mui/material";
interface Iprops {
hasIcon?: boolean;
}
export default function MenuItemDivider({ hasIcon = false }: Iprops) {
return (
<Divider
sx={{
"&&&": {
my: 0,
ml: hasIcon ? "48px" : "16px",
},
}}
/>
);
}

View File

@@ -1,20 +0,0 @@
import { styled } from "@mui/material";
export const MenuItemGroup = styled("div")(
({ theme }) => `
& > .MuiMenuItem-root{
border-radius: 8px;
background-color: transparent;
}
& > .MuiMenuItem-root:not(:last-of-type) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
& > .MuiMenuItem-root:not(:first-of-type) {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
background-color: ${theme.colors.fill.faint};
border-radius: 8px;
`,
);

View File

@@ -1,28 +0,0 @@
import { VerticallyCenteredFlex } from "@ente/shared/components/Container";
import { Typography } from "@mui/material";
interface Iprops {
title: string;
icon?: JSX.Element;
}
export default function MenuSectionTitle({ title, icon }: Iprops) {
return (
<VerticallyCenteredFlex
px="8px"
py={"6px"}
gap={"8px"}
sx={{
"& > svg": {
fontSize: "17px",
color: (theme) => theme.colors.stroke.muted,
},
}}
>
{icon && icon}
<Typography variant="small" color="text.muted">
{title}
</Typography>
</VerticallyCenteredFlex>
);
}

View File

@@ -1,7 +1,7 @@
import { Titlebar } from "@/new/shared/components/Titlebar";
import CopyButton from "@ente/shared/components/CodeBlock/CopyButton";
import { formatDateTimeFull } from "@ente/shared/time/format";
import { Box, Stack, styled, Typography } from "@mui/material";
import Titlebar from "components/Titlebar";
import { t } from "i18next";
import React from "react";
import { FileInfoSidebar } from ".";

View File

@@ -1,5 +1,8 @@
import { UnidentifiedFaces } from "@/new/photos/components/PeopleList";
import { isMLEnabled } from "@/new/photos/services/ml";
import { EnteFile } from "@/new/photos/types/file";
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import { Titlebar } from "@/new/shared/components/Titlebar";
import CopyButton from "@ente/shared/components/CodeBlock/CopyButton";
import { FlexWrapper } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
@@ -11,9 +14,6 @@ import LocationOnOutlined from "@mui/icons-material/LocationOnOutlined";
import TextSnippetOutlined from "@mui/icons-material/TextSnippetOutlined";
import { Box, DialogProps, Link, Stack, styled } from "@mui/material";
import { Chip } from "components/Chip";
import { EnteDrawer } from "components/EnteDrawer";
import Titlebar from "components/Titlebar";
import { UnidentifiedFaces } from "components/ml/PeopleList";
import LinkButton from "components/pages/gallery/LinkButton";
import { t } from "i18next";
import { AppContext } from "pages/_app";

View File

@@ -1,7 +1,6 @@
import { MenuItemGroup, MenuSectionTitle } from "@/new/shared/components/Menu";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import { Box, Slider } from "@mui/material";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
import { t } from "i18next";
import type { Dispatch, SetStateAction } from "react";

View File

@@ -1,7 +1,6 @@
import { MenuItemGroup, MenuSectionTitle } from "@/new/shared/components/Menu";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import CropIcon from "@mui/icons-material/Crop";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
import { t } from "i18next";
import type { MutableRefObject } from "react";
import { useContext } from "react";

View File

@@ -1,3 +1,8 @@
import {
MenuItemDivider,
MenuItemGroup,
MenuSectionTitle,
} from "@/new/shared/components/Menu";
import log from "@/next/log";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import Crop169Icon from "@mui/icons-material/Crop169";
@@ -6,9 +11,6 @@ import CropSquareIcon from "@mui/icons-material/CropSquare";
import FlipIcon from "@mui/icons-material/Flip";
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
import RotateRightIcon from "@mui/icons-material/RotateRight";
import MenuItemDivider from "components/Menu/MenuItemDivider";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
import { t } from "i18next";
import { Fragment, useContext } from "react";
import { ImageEditorOverlayContext } from ".";

View File

@@ -1,5 +1,11 @@
import downloadManager from "@/new/photos/services/download";
import { EnteFile } from "@/new/photos/types/file";
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import {
MenuItemDivider,
MenuItemGroup,
MenuSectionTitle,
} from "@/new/shared/components/Menu";
import { nameAndExtension } from "@/next/file";
import log from "@/next/log";
import { ensure } from "@/utils/ensure";
@@ -26,10 +32,6 @@ import {
Tabs,
Typography,
} from "@mui/material";
import { EnteDrawer } from "components/EnteDrawer";
import MenuItemDivider from "components/Menu/MenuItemDivider";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
import { CORNER_THRESHOLD, FILTER_DEFAULT_VALUES } from "constants/photoEditor";
import { t } from "i18next";
import { AppContext } from "pages/_app";

View File

@@ -1,7 +1,7 @@
import { PeopleList } from "@/new/photos/components/PeopleList";
import { isMLEnabled } from "@/new/photos/services/ml";
import { Row } from "@ente/shared/components/Container";
import { Box, styled } from "@mui/material";
import { PeopleList } from "components/ml/PeopleList";
import { t } from "i18next";
import { components } from "react-select";
import { Suggestion, SuggestionType } from "types/search";

View File

@@ -1,24 +1,27 @@
import { MLSettingsBeta } from "@/new/photos/components/MLSettingsBeta";
import { canEnableML } from "@/new/photos/services/ml";
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import { MenuItemGroup, MenuSectionTitle } from "@/new/shared/components/Menu";
import { Titlebar } from "@/new/shared/components/Titlebar";
import { isDesktop } from "@/next/app";
import { pt } from "@/next/i18n";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import ChevronRight from "@mui/icons-material/ChevronRight";
import ScienceIcon from "@mui/icons-material/Science";
import { Box, DialogProps, Stack } from "@mui/material";
import { EnteDrawer } from "components/EnteDrawer";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
import Titlebar from "components/Titlebar";
import { MLSearchSettings } from "components/ml/MLSearchSettings";
import { t } from "i18next";
import isElectron from "is-electron";
import { AppContext } from "pages/_app";
import { useContext, useState } from "react";
import { useContext, useEffect, useState } from "react";
export default function AdvancedSettings({ open, onClose, onRootClose }) {
const appContext = useContext(AppContext);
const [mlSearchSettingsView, setMlSearchSettingsView] = useState(false);
const openMlSearchSettings = () => setMlSearchSettingsView(true);
const closeMlSearchSettings = () => setMlSearchSettingsView(false);
const [showMLSettings, setShowMLSettings] = useState(false);
const [openMLSettings, setOpenMLSettings] = useState(false);
useEffect(() => {
if (isDesktop) void canEnableML().then(setShowMLSettings);
}, []);
const handleRootClose = () => {
onClose();
onRootClose();
@@ -36,18 +39,6 @@ export default function AdvancedSettings({ open, onClose, onRootClose }) {
appContext.setIsCFProxyDisabled(!appContext.isCFProxyDisabled);
};
// TODO-ML:
// const [indexingStatus, setIndexingStatus] = useState<CLIPIndexingStatus>({
// indexed: 0,
// pending: 0,
// });
// useEffect(() => {
// clipService.setOnUpdateHandler(setIndexingStatus);
// clipService.getIndexingStatus().then((st) => setIndexingStatus(st));
// return () => clipService.setOnUpdateHandler(undefined);
// }, []);
return (
<EnteDrawer
transitionDuration={0}
@@ -66,21 +57,6 @@ export default function AdvancedSettings({ open, onClose, onRootClose }) {
<Box px={"8px"}>
<Stack py="20px" spacing="24px">
{isElectron() && (
<Box>
<MenuSectionTitle
title={t("LABS")}
icon={<ScienceIcon />}
/>
<MenuItemGroup>
<EnteMenuItem
endIcon={<ChevronRight />}
onClick={openMlSearchSettings}
label={t("ML_SEARCH")}
/>
</MenuItemGroup>
</Box>
)}
<Box>
<MenuItemGroup>
<EnteMenuItem
@@ -94,48 +70,29 @@ export default function AdvancedSettings({ open, onClose, onRootClose }) {
title={t("FASTER_UPLOAD_DESCRIPTION")}
/>
</Box>
{/* TODO-ML: isElectron() && (
<Box>
<MenuSectionTitle
title={t("MAGIC_SEARCH_STATUS")}
/>
<Stack py={"12px"} px={"12px"} spacing={"24px"}>
<VerticallyCenteredFlex
justifyContent="space-between"
alignItems={"center"}
>
<Typography>
{t("INDEXED_ITEMS")}
</Typography>
<Typography>
{formatNumber(
indexingStatus.indexed,
)}
</Typography>
</VerticallyCenteredFlex>
<VerticallyCenteredFlex
justifyContent="space-between"
alignItems={"center"}
>
<Typography>
{t("PENDING_ITEMS")}
</Typography>
<Typography>
{formatNumber(
indexingStatus.pending,
)}
</Typography>
</VerticallyCenteredFlex>
</Stack>
</Box>
)*/}
</Stack>
{showMLSettings && (
<Box>
<MenuSectionTitle
title={t("LABS")}
icon={<ScienceIcon />}
/>
<MenuItemGroup>
<EnteMenuItem
endIcon={<ChevronRight />}
onClick={() => setOpenMLSettings(true)}
label={pt("ML search")}
/>
</MenuItemGroup>
</Box>
)}
</Box>
</Stack>
<MLSearchSettings
open={mlSearchSettingsView}
onClose={closeMlSearchSettings}
<MLSettingsBeta
open={openMLSettings}
onClose={() => setOpenMLSettings(false)}
onRootClose={handleRootClose}
/>
</EnteDrawer>

View File

@@ -1,3 +1,6 @@
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import { MenuItemGroup } from "@/new/shared/components/Menu";
import { Titlebar } from "@/new/shared/components/Titlebar";
import log from "@/next/log";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import {
@@ -8,9 +11,6 @@ import {
Stack,
Typography,
} from "@mui/material";
import { EnteDrawer } from "components/EnteDrawer";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import Titlebar from "components/Titlebar";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { useContext, useEffect, useState } from "react";

View File

@@ -1,23 +1,32 @@
import { MLSettings } from "@/new/photos/components/MLSettings";
import { isMLSupported } from "@/new/photos/services/ml";
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import { MenuItemGroup, MenuSectionTitle } from "@/new/shared/components/Menu";
import { Titlebar } from "@/new/shared/components/Titlebar";
import {
getLocaleInUse,
pt,
setLocaleInUse,
supportedLocales,
type SupportedLocale,
} from "@/next/i18n";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import ChevronRight from "@mui/icons-material/ChevronRight";
import ScienceIcon from "@mui/icons-material/Science";
import { Box, DialogProps, Stack } from "@mui/material";
import DropdownInput from "components/DropdownInput";
import { EnteDrawer } from "components/EnteDrawer";
import Titlebar from "components/Titlebar";
import { t } from "i18next";
import { useState } from "react";
import { AppContext } from "pages/_app";
import { useContext, useState } from "react";
import AdvancedSettings from "./AdvancedSettings";
import MapSettings from "./MapSetting";
export default function Preferences({ open, onClose, onRootClose }) {
const appContext = useContext(AppContext);
const [advancedSettingsView, setAdvancedSettingsView] = useState(false);
const [mapSettingsView, setMapSettingsView] = useState(false);
const [openMLSettings, setOpenMLSettings] = useState(false);
const openAdvancedSettings = () => setAdvancedSettingsView(true);
const closeAdvancedSettings = () => setAdvancedSettingsView(false);
@@ -66,19 +75,45 @@ export default function Preferences({ open, onClose, onRootClose }) {
endIcon={<ChevronRight />}
label={t("ADVANCED")}
/>
{isMLSupported && (
<Box>
<MenuSectionTitle
title={t("LABS")}
icon={<ScienceIcon />}
/>
<MenuItemGroup>
<EnteMenuItem
endIcon={<ChevronRight />}
onClick={() => setOpenMLSettings(true)}
label={pt("ML search")}
/>
</MenuItemGroup>
<MenuSectionTitle
title={pt(
"Face recognition, magic search and more",
)}
/>
</Box>
)}
</Stack>
</Box>
</Stack>
<AdvancedSettings
open={advancedSettingsView}
onClose={closeAdvancedSettings}
onRootClose={onRootClose}
<MLSettings
open={openMLSettings}
onClose={() => setOpenMLSettings(false)}
onRootClose={handleRootClose}
appContext={appContext}
/>
<MapSettings
open={mapSettingsView}
onClose={closeMapSettings}
onRootClose={onRootClose}
/>
<AdvancedSettings
open={advancedSettingsView}
onClose={closeAdvancedSettings}
onRootClose={onRootClose}
/>
</EnteDrawer>
);
}

View File

@@ -1,4 +1,6 @@
import { openAccountsManagePasskeysPage } from "@/accounts/services/passkey";
import { initiateEmail, openURL } from "@/new/photos/utils/web";
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import log from "@/next/log";
import { savedLogs } from "@/next/log-web";
import { customAPIHost } from "@/next/origins";
@@ -34,7 +36,6 @@ import {
} from "@mui/material";
import Typography from "@mui/material/Typography";
import DeleteAccountModal from "components/DeleteAccountModal";
import { EnteDrawer } from "components/EnteDrawer";
import TwoFactorModal from "components/TwoFactor/Modal";
import { WatchFolder } from "components/WatchFolder";
import LinkButton from "components/pages/gallery/LinkButton";
@@ -73,7 +74,6 @@ import {
isSubscriptionCancelled,
isSubscriptionPastDue,
} from "utils/billing";
import { openLink } from "utils/common";
import { getDownloadAppMessage } from "utils/ui";
import { isFamilyAdmin, isPartOfFamily } from "utils/user/family";
import { testUpload } from "../../../tests/upload.test";
@@ -594,10 +594,10 @@ const HelpSection: React.FC = () => {
const { setDialogMessage } = useContext(AppContext);
const { openExportModal } = useContext(GalleryContext);
const openRoadmap = () =>
openLink("https://github.com/ente-io/ente/discussions", true);
const requestFeature = () =>
openURL("https://github.com/ente-io/ente/discussions");
const contactSupport = () => openLink("mailto:support@ente.io", true);
const contactSupport = () => initiateEmail("support@ente.io");
function openExport() {
if (isElectron()) {
@@ -610,7 +610,7 @@ const HelpSection: React.FC = () => {
return (
<>
<EnteMenuItem
onClick={openRoadmap}
onClick={requestFeature}
label={t("REQUEST_FEATURE")}
variant="secondary"
/>

View File

@@ -1,59 +0,0 @@
import { FlexWrapper } from "@ente/shared/components/Container";
import ArrowBack from "@mui/icons-material/ArrowBack";
import Close from "@mui/icons-material/Close";
import { Box, IconButton, Typography } from "@mui/material";
interface Iprops {
title: string;
caption?: string;
onClose: () => void;
backIsClose?: boolean;
onRootClose?: () => void;
actionButton?: JSX.Element;
}
export default function Titlebar({
title,
caption,
onClose,
backIsClose,
actionButton,
onRootClose,
}: Iprops): JSX.Element {
return (
<>
<FlexWrapper
height={48}
alignItems={"center"}
justifyContent="space-between"
>
<IconButton
onClick={onClose}
color={backIsClose ? "secondary" : "primary"}
>
{backIsClose ? <Close /> : <ArrowBack />}
</IconButton>
<Box display={"flex"} gap="4px">
{actionButton && actionButton}
{!backIsClose && (
<IconButton onClick={onRootClose} color={"secondary"}>
<Close />
</IconButton>
)}
</Box>
</FlexWrapper>
<Box py={0.5} px={2}>
<Typography variant="h3" fontWeight={"bold"}>
{title}
</Typography>
<Typography
variant="small"
color="text.muted"
sx={{ wordBreak: "break-all", minHeight: "17px" }}
>
{caption}
</Typography>
</Box>
</>
);
}

View File

@@ -1,342 +0,0 @@
import {
canEnableFaceIndexing,
disableML,
enableML,
isMLEnabled,
} from "@/new/photos/services/ml";
import log from "@/next/log";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import {
Box,
Button,
Checkbox,
DialogProps,
FormControlLabel,
FormGroup,
Link,
Stack,
Typography,
} from "@mui/material";
import { EnteDrawer } from "components/EnteDrawer";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import Titlebar from "components/Titlebar";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { useContext, useEffect, useState } from "react";
import { Trans } from "react-i18next";
import {
getFaceSearchEnabledStatus,
updateFaceSearchEnabledStatus,
} from "services/userService";
export const MLSearchSettings = ({ open, onClose, onRootClose }) => {
const {
setDialogMessage,
somethingWentWrong,
startLoading,
finishLoading,
} = useContext(AppContext);
const [enableFaceSearchView, setEnableFaceSearchView] = useState(false);
const openEnableFaceSearch = () => {
setEnableFaceSearchView(true);
};
const closeEnableFaceSearch = () => {
setEnableFaceSearchView(false);
};
const enableMlSearch = async () => {
try {
const hasEnabledFaceSearch = await getFaceSearchEnabledStatus();
if (!hasEnabledFaceSearch) {
openEnableFaceSearch();
} else {
enableML();
}
} catch (e) {
log.error("Enable ML search failed", e);
somethingWentWrong();
}
};
const enableFaceSearch = async () => {
try {
startLoading();
// Update the consent flag.
await updateFaceSearchEnabledStatus(true);
enableML();
closeEnableFaceSearch();
finishLoading();
} catch (e) {
log.error("Enable face search failed", e);
somethingWentWrong();
}
};
const disableMlSearch = async () => {
try {
disableML();
onClose();
} catch (e) {
log.error("Disable ML search failed", e);
somethingWentWrong();
}
};
const disableFaceSearch = async () => {
try {
startLoading();
await disableMlSearch();
finishLoading();
} catch (e) {
log.error("Disable face search failed", e);
somethingWentWrong();
}
};
const confirmDisableFaceSearch = () => {
setDialogMessage({
title: t("DISABLE_FACE_SEARCH_TITLE"),
content: (
<Typography>
<Trans i18nKey={"DISABLE_FACE_SEARCH_DESCRIPTION"} />
</Typography>
),
close: { text: t("CANCEL") },
proceed: {
variant: "primary",
text: t("DISABLE_FACE_SEARCH"),
action: disableFaceSearch,
},
});
};
const handleRootClose = () => {
onClose();
onRootClose();
};
const handleDrawerClose: DialogProps["onClose"] = (_, reason) => {
if (reason === "backdropClick") {
handleRootClose();
} else {
onClose();
}
};
return (
<Box>
<EnteDrawer
anchor="left"
transitionDuration={0}
open={open}
onClose={handleDrawerClose}
BackdropProps={{
sx: { "&&&": { backgroundColor: "transparent" } },
}}
>
{isMLEnabled() ? (
<ManageMLSearch
onClose={onClose}
disableMlSearch={disableMlSearch}
handleDisableFaceSearch={confirmDisableFaceSearch}
onRootClose={handleRootClose}
/>
) : (
<EnableMLSearch
onClose={onClose}
enableMlSearch={enableMlSearch}
onRootClose={handleRootClose}
/>
)}
</EnteDrawer>
<EnableFaceSearch
open={enableFaceSearchView}
onClose={closeEnableFaceSearch}
enableFaceSearch={enableFaceSearch}
onRootClose={handleRootClose}
/>
</Box>
);
};
function EnableFaceSearch({ open, onClose, enableFaceSearch, onRootClose }) {
const [acceptTerms, setAcceptTerms] = useState(false);
useEffect(() => {
setAcceptTerms(false);
}, [open]);
const handleRootClose = () => {
onClose();
onRootClose();
};
const handleDrawerClose: DialogProps["onClose"] = (_, reason) => {
if (reason === "backdropClick") {
handleRootClose();
} else {
onClose();
}
};
return (
<EnteDrawer
transitionDuration={0}
open={open}
onClose={handleDrawerClose}
BackdropProps={{
sx: { "&&&": { backgroundColor: "transparent" } },
}}
>
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
title={t("ENABLE_FACE_SEARCH_TITLE")}
onRootClose={handleRootClose}
/>
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
<Typography color="text.muted" px={"8px"}>
<Trans
i18nKey={"ENABLE_FACE_SEARCH_DESCRIPTION"}
components={{
a: (
<Link
target="_blank"
href="https://ente.io/privacy#8-biometric-information-privacy-policy"
underline="always"
sx={{
color: "inherit",
textDecorationColor: "inherit",
}}
/>
),
}}
/>
</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("FACE_SEARCH_CONFIRMATION")}
/>
</FormGroup>
<Stack px={"8px"} spacing={"8px"}>
<Button
color={"accent"}
size="large"
disabled={!acceptTerms}
onClick={enableFaceSearch}
>
{t("ENABLE_FACE_SEARCH")}
</Button>
<Button
color={"secondary"}
size="large"
onClick={onClose}
>
{t("CANCEL")}
</Button>
</Stack>
</Stack>
</Stack>
</EnteDrawer>
);
}
function EnableMLSearch({ onClose, enableMlSearch, onRootClose }) {
// const showDetails = () =>
// openLink("https://ente.io/blog/desktop-ml-beta", true);
const [canEnable, setCanEnable] = useState(false);
useEffect(() => {
canEnableFaceIndexing().then((v) => setCanEnable(v));
}, []);
return (
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
title={t("ML_SEARCH")}
onRootClose={onRootClose}
/>
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
{canEnable ? (
<Stack px={"8px"} spacing={"8px"}>
<Button
color={"accent"}
size="large"
onClick={enableMlSearch}
>
{t("ENABLE")}
</Button>
{/*
<Button
color="secondary"
size="large"
onClick={showDetails}
>
{t("ML_MORE_DETAILS")}
</Button>
*/}
</Stack>
) : (
<Box px={"8px"}>
{" "}
<Typography color="text.muted">
{/* <Trans i18nKey={"ENABLE_ML_SEARCH_DESCRIPTION"} /> */}
We're putting finishing touches, coming back soon!
</Typography>
</Box>
)}
</Stack>
</Stack>
);
}
function ManageMLSearch({
onClose,
disableMlSearch,
handleDisableFaceSearch,
onRootClose,
}) {
return (
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
title={t("ML_SEARCH")}
onRootClose={onRootClose}
/>
<Box px={"16px"}>
<Stack py={"20px"} spacing={"24px"}>
<MenuItemGroup>
<EnteMenuItem
onClick={disableMlSearch}
label={t("DISABLE_BETA")}
/>
</MenuItemGroup>
<MenuItemGroup>
<EnteMenuItem
onClick={handleDisableFaceSearch}
label={t("DISABLE_FACE_SEARCH")}
/>
</MenuItemGroup>
</Stack>
</Box>
</Stack>
);
}

View File

@@ -1,6 +1,6 @@
import type { AccountsContextT } from "@/accounts/types/context";
import DownloadManager from "@/new/photos/services/download";
import { initML } from "@/new/photos/services/ml";
import { initML, isMLSupported } from "@/new/photos/services/ml";
import { clientPackageName, staticAppTitle } from "@/next/app";
import { CustomHead } from "@/next/components/Head";
import { setupI18n } from "@/next/i18n";
@@ -174,7 +174,7 @@ export default function App({ Component, pageProps }: AppProps) {
}
};
initML();
if (isMLSupported) initML();
electron.onOpenURL(handleOpenURL);
electron.onAppUpdateAvailable(showUpdateDialog);

View File

@@ -1,5 +1,5 @@
import { FILE_TYPE } from "@/media/file-type";
import { faceIndexingStatus, isMLEnabled } from "@/new/photos/services/ml";
import { isMLSupported, mlStatusSnapshot } from "@/new/photos/services/ml";
import type { Person } from "@/new/photos/services/ml/people";
import { EnteFile } from "@/new/photos/types/file";
import { isDesktop } from "@/next/app";
@@ -26,9 +26,7 @@ const DIGITS = new Set(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]);
export const getDefaultOptions = async () => {
return [
// TODO-ML(MR): Skip this for now if indexing is disabled (eventually
// the indexing status should not be tied to results).
...(isMLEnabled() ? [await getIndexStatusSuggestion()] : []),
await getMLStatusSuggestion(),
...(await convertSuggestionsToOptions(await getAllPeopleSuggestion())),
].filter((t) => !!t);
};
@@ -171,37 +169,36 @@ export async function getAllPeopleSuggestion(): Promise<Array<Suggestion>> {
}
}
export async function getIndexStatusSuggestion(): Promise<Suggestion> {
try {
const indexStatus = await faceIndexingStatus();
export async function getMLStatusSuggestion(): Promise<Suggestion> {
if (!isMLSupported) return undefined;
let label: string;
switch (indexStatus.phase) {
case "scheduled":
label = t("INDEXING_SCHEDULED");
break;
case "indexing":
label = t("ANALYZING_PHOTOS", {
indexStatus,
});
break;
case "clustering":
label = t("INDEXING_PEOPLE", { indexStatus });
break;
case "done":
label = t("INDEXING_DONE", { indexStatus });
break;
}
const status = mlStatusSnapshot();
return {
label,
type: SuggestionType.INDEX_STATUS,
value: indexStatus,
hide: true,
};
} catch (e) {
log.error("getIndexStatusSuggestion failed", e);
if (!status || status.phase == "disabled" || status.phase == "paused")
return undefined;
let label: string;
switch (status.phase) {
case "scheduled":
label = t("INDEXING_SCHEDULED");
break;
case "indexing":
label = t("ANALYZING_PHOTOS", { indexStatus: status });
break;
case "clustering":
label = t("INDEXING_PEOPLE", { indexStatus: status });
break;
case "done":
label = t("INDEXING_DONE", { indexStatus: status });
break;
}
return {
label,
type: SuggestionType.INDEX_STATUS,
value: status,
hide: true,
};
}
function getDateSuggestion(searchPhrase: string): Suggestion[] {

View File

@@ -1,6 +1,5 @@
import { fetchAndSaveFeatureFlagsIfNeeded } from "@/new/photos/services/feature-flags";
import { triggerMLSync } from "@/new/photos/services/ml";
import { isDesktop } from "@/next/app";
import { isMLSupported, triggerMLSync } from "@/new/photos/services/ml";
import { syncEntities } from "services/entityService";
import { syncMapEnabled } from "services/userService";
@@ -17,7 +16,5 @@ export const sync = async () => {
await syncEntities();
await syncMapEnabled();
fetchAndSaveFeatureFlagsIfNeeded();
if (isDesktop) {
triggerMLSync();
}
if (isMLSupported) triggerMLSync();
};

View File

@@ -206,47 +206,6 @@ export const deleteAccount = async (
}
};
export const getFaceSearchEnabledStatus = async () => {
try {
const token = getToken();
const resp: AxiosResponse<GetRemoteStoreValueResponse> =
await HTTPService.get(
await apiURL("/remote-store"),
{
key: "faceSearchEnabled",
defaultValue: false,
},
{
"X-Auth-Token": token,
},
);
return resp.data.value === "true";
} catch (e) {
log.error("failed to get face search enabled status", e);
throw e;
}
};
export const updateFaceSearchEnabledStatus = async (newStatus: boolean) => {
try {
const token = getToken();
await HTTPService.post(
await apiURL("/remote-store/update"),
{
key: "faceSearchEnabled",
value: newStatus.toString(),
},
null,
{
"X-Auth-Token": token,
},
);
} catch (e) {
log.error("failed to update face search enabled status", e);
throw e;
}
};
export const syncMapEnabled = async () => {
try {
const status = await getMapEnabledStatus();

View File

@@ -1,5 +1,5 @@
import { FILE_TYPE } from "@/media/file-type";
import type { FaceIndexingStatus } from "@/new/photos/services/ml";
import type { MLStatus } from "@/new/photos/services/ml";
import type { Person } from "@/new/photos/services/ml/people";
import { EnteFile } from "@/new/photos/types/file";
import { City } from "services/locationSearchService";
@@ -31,7 +31,7 @@ export interface Suggestion {
| DateValue
| number[]
| Person
| FaceIndexingStatus
| MLStatus
| LocationTagData
| City
| FILE_TYPE

View File

@@ -1,3 +1,4 @@
import { openURL } from "@/new/photos/utils/web";
import log from "@/next/log";
import { SetDialogBoxAttributes } from "@ente/shared/components/DialogBox/types";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
@@ -8,7 +9,6 @@ import billingService from "services/billingService";
import { Plan, Subscription } from "types/billing";
import { SetLoading } from "types/gallery";
import { BonusData, UserDetails } from "types/user";
import { openLink } from "utils/common";
import { getSubscriptionPurchaseSuccessMessage } from "utils/ui";
import { getTotalFamilyUsage, isPartOfFamily } from "utils/user/family";
@@ -220,7 +220,7 @@ export async function manageFamilyMethod(
try {
setLoading(true);
const familyPortalRedirectURL = getRedirectURL(REDIRECTS.FAMILIES);
openLink(familyPortalRedirectURL, true);
openURL(familyPortalRedirectURL);
} catch (e) {
log.error("failed to redirect to family portal", e);
setDialogMessage({

View File

@@ -6,16 +6,6 @@ export const preloadImage = (imgBasePath: string) => {
new Image().srcset = srcSet.join(",");
};
export function openLink(href: string, newTab?: boolean) {
const a = document.createElement("a");
a.href = href;
if (newTab) {
a.target = "_blank";
}
a.rel = "noreferrer noopener";
a.click();
}
export function isClipboardItemPresent() {
return typeof ClipboardItem !== "undefined";
}

View File

@@ -1,3 +1,4 @@
import { openURL } from "@/new/photos/utils/web";
import { ensureElectron } from "@/next/electron";
import { AppUpdate } from "@/next/types/ipc";
import { DialogBoxAttributes } from "@ente/shared/components/DialogBox/types";
@@ -7,7 +8,6 @@ import { Link } from "@mui/material";
import { t } from "i18next";
import { Trans } from "react-i18next";
import { Subscription } from "types/billing";
import { openLink } from "utils/common";
export const getDownloadAppMessage = (): DialogBoxAttributes => {
return {
@@ -25,7 +25,7 @@ export const getDownloadAppMessage = (): DialogBoxAttributes => {
};
};
const downloadApp = () => openLink("https://ente.io/download/desktop", true);
const downloadApp = () => openURL("https://ente.io/download/desktop");
export const getTrashFilesMessage = (
deleteFileHelper,

View File

@@ -71,8 +71,11 @@ const Contents: React.FC<ContentsProps> = (props) => {
() =>
void getKV("apiOrigin").then((o) =>
setInitialAPIOrigin(
// TODO: Migration of apiOrigin from local storage to indexed DB
// Remove me after a bit (27 June 2024).
// Migrate apiOrigin from local storage to indexed DB.
//
// This code was added 27 June 2024. Note that the legacy
// value was never in production builds, only nightlies, so
// this code can be removed soon (tag: Migration).
o ?? localStorage.getItem("apiOrigin") ?? "",
),
),
@@ -215,8 +218,11 @@ const Form: React.FC<FormProps> = ({ initialAPIOrigin, onClose }) => {
const updateAPIOrigin = async (origin: string) => {
if (!origin) {
await removeKV("apiOrigin");
// TODO: Migration of apiOrigin from local storage to indexed DB
// Remove me after a bit (27 June 2024).
// Migrate apiOrigin from local storage to indexed DB.
//
// This code was added 27 June 2024. Note that the legacy value was
// never in production builds, only nightlies, so this code can be
// removed at some point soon (tag: Migration).
localStorage.removeItem("apiOrigin");
return;
}

View File

@@ -0,0 +1,416 @@
import {
disableML,
enableML,
getIsMLEnabledRemote,
isMLEnabled,
mlStatusSnapshot,
mlStatusSubscribe,
pauseML,
resumeML,
type MLStatus,
} from "@/new/photos/services/ml";
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import { MenuItemGroup } from "@/new/shared/components/Menu";
import { Titlebar } from "@/new/shared/components/Titlebar";
import { pt } from "@/next/i18n";
import log from "@/next/log";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import {
Box,
Button,
Checkbox,
Divider,
FormControlLabel,
FormGroup,
Link,
Paper,
Stack,
Typography,
type DialogProps,
} from "@mui/material";
import { t } from "i18next";
import React, { useEffect, useState, useSyncExternalStore } from "react";
import { Trans } from "react-i18next";
import type { NewAppContextPhotos } from "../types/context";
import { openURL } from "../utils/web";
interface MLSettingsProps {
/** If `true`, then this drawer page is shown. */
open: boolean;
/** Called when the user wants to go back from this drawer page. */
onClose: () => void;
/** Called when the user wants to close the entire stack of drawers. */
onRootClose: () => void;
/** See: [Note: Migrating components that need the app context]. */
appContext: NewAppContextPhotos;
}
export const MLSettings: React.FC<MLSettingsProps> = ({
open,
onClose,
onRootClose,
appContext,
}) => {
const {
startLoading,
finishLoading,
setDialogBoxAttributesV2,
somethingWentWrong,
} = appContext;
const mlStatus = useSyncExternalStore(mlStatusSubscribe, mlStatusSnapshot);
const [openFaceConsent, setOpenFaceConsent] = useState(false);
const handleRootClose = () => {
onClose();
onRootClose();
};
const handleDrawerClose: DialogProps["onClose"] = (_, reason) => {
if (reason == "backdropClick") handleRootClose();
else onClose();
};
const handleEnableML = async () => {
startLoading();
try {
if (!(await getIsMLEnabledRemote())) {
setOpenFaceConsent(true);
} else {
await enableML();
}
} catch (e) {
log.error("Failed to enable or resume ML", e);
somethingWentWrong();
} finally {
finishLoading();
}
};
const handleConsent = async () => {
startLoading();
try {
await enableML();
// Close the FaceConsent drawer, come back to ourselves.
setOpenFaceConsent(false);
} catch (e) {
log.error("Failed to enable ML", e);
somethingWentWrong();
} finally {
finishLoading();
}
};
const handleDisableML = async () => {
startLoading();
try {
await disableML();
} catch (e) {
log.error("Failed to disable ML", e);
somethingWentWrong();
} finally {
finishLoading();
}
};
let component: React.ReactNode;
if (!mlStatus) {
component = <Loading />;
} else if (mlStatus.phase == "disabled") {
component = <EnableML onEnable={handleEnableML} />;
} else {
component = (
<ManageML
{...{ mlStatus, setDialogBoxAttributesV2 }}
onDisableML={handleDisableML}
/>
);
}
return (
<Box>
<EnteDrawer
anchor="left"
transitionDuration={0}
open={open}
onClose={handleDrawerClose}
BackdropProps={{
sx: { "&&&": { backgroundColor: "transparent" } },
}}
>
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
title={pt("ML search")}
onRootClose={onRootClose}
/>
{component}
</Stack>
</EnteDrawer>
<FaceConsent
open={openFaceConsent}
onClose={() => setOpenFaceConsent(false)}
onRootClose={handleRootClose}
onConsent={handleConsent}
/>
</Box>
);
};
const Loading: React.FC = () => {
return (
<Box textAlign="center" pt={4}>
<EnteSpinner />
</Box>
);
};
interface EnableMLProps {
/** Called when the user enables ML. */
onEnable: () => void;
}
const EnableML: React.FC<EnableMLProps> = ({ onEnable }) => {
// TODO-ML: Update link.
const moreDetails = () => openURL("https://ente.io/blog/desktop-ml-beta");
return (
<Stack py={"20px"} px={"16px"} spacing={"32px"}>
<Typography color="text.muted">
{pt(
"Enable ML (Machine Learning) for face recognition, magic search and other advanced search features",
)}
</Typography>
<Stack spacing={"8px"}>
<Button color={"accent"} size="large" onClick={onEnable}>
{t("ENABLE")}
</Button>
<Button color="secondary" size="large" onClick={moreDetails}>
{t("ML_MORE_DETAILS")}
</Button>
</Stack>
<Typography color="text.faint" variant="small">
{pt(
'Magic search allows to search photos by their contents (e.g. "car", "red car" or even "ferrari")',
)}
</Typography>
</Stack>
);
};
type FaceConsentProps = Omit<MLSettingsProps, "appContext"> & {
/** Called when the user provides their consent. */
onConsent: () => void;
};
const FaceConsent: React.FC<FaceConsentProps> = ({
open,
onClose,
onRootClose,
onConsent,
}) => {
const [acceptTerms, setAcceptTerms] = useState(false);
useEffect(() => {
setAcceptTerms(false);
}, [open]);
const handleRootClose = () => {
onClose();
onRootClose();
};
const handleDrawerClose: DialogProps["onClose"] = (_, reason) => {
if (reason == "backdropClick") handleRootClose();
else onClose();
};
return (
<EnteDrawer
transitionDuration={0}
open={open}
onClose={handleDrawerClose}
BackdropProps={{
sx: { "&&&": { backgroundColor: "transparent" } },
}}
>
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
title={t("ENABLE_FACE_SEARCH_TITLE")}
onRootClose={handleRootClose}
/>
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
<Typography component="div" color="text.muted" px={"8px"}>
<Trans
i18nKey={"ENABLE_FACE_SEARCH_DESCRIPTION"}
components={{
a: (
<Link
target="_blank"
href="https://ente.io/privacy#8-biometric-information-privacy-policy"
underline="always"
sx={{
color: "inherit",
textDecorationColor: "inherit",
}}
/>
),
}}
/>
</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("FACE_SEARCH_CONFIRMATION")}
/>
</FormGroup>
<Stack px={"8px"} spacing={"8px"}>
<Button
color={"accent"}
size="large"
disabled={!acceptTerms}
onClick={onConsent}
>
{t("ENABLE_FACE_SEARCH")}
</Button>
<Button
color={"secondary"}
size="large"
onClick={onClose}
>
{t("CANCEL")}
</Button>
</Stack>
</Stack>
</Stack>
</EnteDrawer>
);
};
interface ManageMLProps {
/** The {@link MLStatus}; a non-disabled one. */
mlStatus: Exclude<MLStatus, { phase: "disabled" }>;
/** Called when the user wants to disable ML. */
onDisableML: () => void;
/** Subset of appContext. */
setDialogBoxAttributesV2: NewAppContextPhotos["setDialogBoxAttributesV2"];
}
const ManageML: React.FC<ManageMLProps> = ({
mlStatus,
onDisableML,
setDialogBoxAttributesV2,
}) => {
const { phase, nSyncedFiles, nTotalFiles } = mlStatus;
let status: string;
switch (phase) {
case "paused":
status = pt("Paused");
break;
case "indexing":
status = pt("Indexing");
break;
case "scheduled":
status = pt("Scheduled");
break;
// TODO: Clustering
default:
status = pt("Done");
break;
}
const processed = `${nSyncedFiles} / ${nTotalFiles}`;
const handleToggleLocal = () => (isMLEnabled() ? pauseML() : resumeML());
const confirmDisableML = () => {
setDialogBoxAttributesV2({
title: pt("Disable ML search"),
content: (
<Typography>
{pt(
"Do you want to disable ML search on all your devices?",
)}
</Typography>
),
close: { text: t("CANCEL") },
proceed: {
variant: "critical",
text: pt("Disable"),
action: onDisableML,
},
buttonDirection: "row",
});
};
return (
<Stack px={"16px"} py={"20px"} gap={4}>
<Stack gap={3}>
<MenuItemGroup>
<EnteMenuItem
label={pt("Enabled")}
variant="toggle"
checked={true}
onClick={confirmDisableML}
/>
</MenuItemGroup>
<MenuItemGroup>
<EnteMenuItem
label={pt("On this device")}
variant="toggle"
checked={phase != "paused"}
onClick={handleToggleLocal}
/>
</MenuItemGroup>
</Stack>
<Paper variant="outlined">
<Stack>
<Stack
direction="row"
gap={2}
px={2}
pt={1}
pb={2}
justifyContent={"space-between"}
>
<Typography color="text.faint">
{pt("Status")}
</Typography>
<Typography>{status}</Typography>
</Stack>
<Divider sx={{ marginInlineStart: 2 }} />
<Stack
direction="row"
gap={2}
px={2}
pt={2}
pb={1}
justifyContent={"space-between"}
>
<Typography color="text.faint">
{pt("Processed")}
</Typography>
<Typography textAlign="right">{processed}</Typography>
</Stack>
</Stack>
</Paper>
</Stack>
);
};

View File

@@ -0,0 +1,60 @@
import { EnteDrawer } from "@/new/shared/components/EnteDrawer";
import { Titlebar } from "@/new/shared/components/Titlebar";
import { pt, ut } from "@/next/i18n";
import { Box, Stack, Typography, type DialogProps } from "@mui/material";
import React from "react";
interface MLSettingsBetaProps {
/** If `true`, then this drawer page is shown. */
open: boolean;
/** Called when the user wants to go back from this drawer page. */
onClose: () => void;
/** Called when the user wants to close the entire stack of drawers. */
onRootClose: () => void;
}
export const MLSettingsBeta: React.FC<MLSettingsBetaProps> = ({
open,
onClose,
onRootClose,
}) => {
const handleRootClose = () => {
onClose();
onRootClose();
};
const handleDrawerClose: DialogProps["onClose"] = (_, reason) => {
if (reason == "backdropClick") handleRootClose();
else onClose();
};
return (
<Box>
<EnteDrawer
anchor="left"
transitionDuration={0}
open={open}
onClose={handleDrawerClose}
BackdropProps={{
sx: { "&&&": { backgroundColor: "transparent" } },
}}
>
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
title={pt("ML search")}
onRootClose={onRootClose}
/>
<Box px="8px">
<Typography color="text.muted">
{ut(
"We're putting finishing touches, coming back soon!",
)}
</Typography>
</Box>
</Stack>
</EnteDrawer>
</Box>
);
};

View File

@@ -3,41 +3,37 @@ import {
unidentifiedFaceIDs,
} from "@/new/photos/services/ml";
import type { Person } from "@/new/photos/services/ml/people";
import { EnteFile } from "@/new/photos/types/file";
import type { EnteFile } from "@/new/photos/types/file";
import { blobCache } from "@/next/blob-cache";
import { Skeleton, Typography, styled } from "@mui/material";
import { t } from "i18next";
import React, { useEffect, useState } from "react";
export interface PeopleListProps {
people: Array<Person>;
maxRows?: number;
people: Person[];
maxRows: number;
onSelect?: (person: Person, index: number) => void;
}
export const PeopleList = React.memo((props: PeopleListProps) => {
export const PeopleList: React.FC<PeopleListProps> = ({
people,
maxRows,
onSelect,
}) => {
return (
<FaceChipContainer
style={
props.maxRows && {
maxHeight: props.maxRows * 122 + 28,
}
}
>
{props.people.map((person, index) => (
<FaceChipContainer style={{ maxHeight: maxRows * 122 + 28 }}>
{people.map((person, index) => (
<FaceChip
key={person.id}
clickable={!!props.onSelect}
onClick={() =>
props.onSelect && props.onSelect(person, index)
}
clickable={!!onSelect}
onClick={() => onSelect && onSelect(person, index)}
>
<FaceCropImageView faceID={person.displayFaceId} />
</FaceChip>
))}
</FaceChipContainer>
);
});
};
const FaceChipContainer = styled("div")`
display: flex;
@@ -89,7 +85,7 @@ export const UnidentifiedFaces: React.FC<UnidentifiedFacesProps> = ({
useEffect(() => {
let didCancel = false;
(async () => {
const go = async () => {
const faceIDs = await unidentifiedFaceIDs(enteFile);
!didCancel && setFaceIDs(faceIDs);
// Don't block for the regeneration to happen. If anything got
@@ -99,7 +95,9 @@ export const UnidentifiedFaces: React.FC<UnidentifiedFacesProps> = ({
void regenerateFaceCropsIfNeeded(enteFile).then((r) =>
setDidRegen(r),
);
})();
};
void go();
return () => {
didCancel = true;
@@ -140,7 +138,7 @@ const FaceCropImageView: React.FC<FaceCropImageViewProps> = ({ faceID }) => {
useEffect(() => {
let didCancel = false;
if (faceID) {
blobCache("face-crops")
void blobCache("face-crops")
.then((cache) => cache.get(faceID))
.then((data) => {
if (data) {
@@ -154,6 +152,9 @@ const FaceCropImageView: React.FC<FaceCropImageViewProps> = ({ faceID }) => {
didCancel = true;
if (objectURL) URL.revokeObjectURL(objectURL);
};
// TODO: The linter warning is actually correct, objectURL should be a
// dependency, but adding that require reworking this code first.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [faceID]);
return objectURL ? (

View File

@@ -7,7 +7,7 @@ import { renderableImageBlob } from "../../utils/file";
import { readStream } from "../../utils/native-stream";
import DownloadManager from "../download";
import type { UploadItem } from "../upload/types";
import type { MLWorkerElectron } from "./worker-electron";
import type { MLWorkerElectron } from "./worker-types";
export interface ImageBitmapAndData {
bitmap: ImageBitmap;
@@ -35,22 +35,14 @@ export const imageBitmapAndData = async (
? await renderableUploadItemImageBitmap(enteFile, uploadItem, electron)
: await renderableImageBitmap(enteFile);
// Use an OffscreenCanvas to get the bitmap's data.
const { width, height } = imageBitmap;
// Use an OffscreenCanvas to get the bitmap's data.
const offscreenCanvas = new OffscreenCanvas(width, height);
const ctx = ensure(offscreenCanvas.getContext("2d"));
ctx.drawImage(imageBitmap, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
// TODO-ML: This check isn't needed, keeping it around during scaffolding.
if (
imageBitmap.width != imageData.width ||
imageBitmap.height != imageData.height
)
throw new Error("Dimension mismatch");
return { bitmap: imageBitmap, data: imageData };
};

View File

@@ -3,8 +3,8 @@ import type { Electron } from "@/next/types/ipc";
import type { ImageBitmapAndData } from "./bitmap";
import { clipIndexes } from "./db";
import { pixelRGBBicubic } from "./image";
import { cosineSimilarity, norm } from "./math";
import type { MLWorkerElectron } from "./worker-electron";
import { dotProduct, norm } from "./math";
import type { MLWorkerElectron } from "./worker-types";
/**
* The version of the CLIP indexing pipeline implemented by the current client.
@@ -26,7 +26,7 @@ export const clipIndexingVersion = 1;
* trained) encoders - one for images, and one for text - that both map to the
* same embedding space.
*
* We use this for natural language search within the app:
* We use this for natural language search (aka "magic search") within the app:
*
* 1. Pre-compute an embedding for each image.
*
@@ -202,7 +202,12 @@ export const clipMatches = async (
const textEmbedding = normalized(t);
const items = (await clipIndexes()).map(
({ fileID, embedding }) =>
[fileID, cosineSimilarity(embedding, textEmbedding)] as const,
// What we want to do is `cosineSimilarity`, but since both the
// embeddings involved are already normalized, we can save the norm
// calculations and directly do their `dotProduct`.
//
// This code is on the hot path, so these optimizations help.
[fileID, dotProduct(embedding, textEmbedding)] as const,
);
return new Map(items.filter(([, score]) => score >= 0.23));
};

View File

@@ -135,14 +135,14 @@ const openMLDB = async () => {
const deleteLegacyDB = () => {
// Delete the legacy face DB v1.
//
// This code was added June 2024 (v1.7.1-rc) and can be removed once clients
// have migrated over.
// This code was added June 2024 (v1.7.1-rc) and can be removed at some
// point when most clients have migrated (tag: Migration).
void deleteDB("mldata");
// Delete the legacy CLIP (mostly) related keys from LocalForage.
//
// This code was added July 2024 (v1.7.2-rc) and can be removed once
// sufficient clients have migrated over (tag: Migration).
// This code was added July 2024 (v1.7.2-rc) and can be removed at some
// point when most clients have migrated (tag: Migration).
void Promise.all([
localForage.removeItem("embeddings"),
localForage.removeItem("embedding_sync_time"),

View File

@@ -1,12 +1,12 @@
import {
decryptFileMetadata,
encryptFileMetadata,
} from "@/new/common/crypto/ente";
import {
getAllLocalFiles,
getLocalTrashedFiles,
} from "@/new/photos/services/files";
import type { EnteFile } from "@/new/photos/types/file";
import {
decryptFileMetadata,
encryptFileMetadata,
} from "@/new/shared/crypto/ente";
import { authenticatedRequestHeaders, ensureOk } from "@/next/http";
import { getKV, setKV } from "@/next/kv";
import log from "@/next/log";

View File

@@ -26,7 +26,7 @@ import {
warpAffineFloat32List,
} from "./image";
import { clamp } from "./math";
import type { MLWorkerElectron } from "./worker-electron";
import type { MLWorkerElectron } from "./worker-types";
/**
* The version of the face indexing pipeline implemented by the current client.

View File

@@ -3,10 +3,6 @@
*/
import { FILE_TYPE } from "@/media/file-type";
import {
isBetaUser,
isInternalUser,
} from "@/new/photos/services/feature-flags";
import type { EnteFile } from "@/new/photos/types/file";
import { isDesktop } from "@/next/app";
import { blobCache } from "@/next/blob-cache";
@@ -14,27 +10,51 @@ import { ensureElectron } from "@/next/electron";
import log from "@/next/log";
import { ComlinkWorker } from "@/next/worker/comlink-worker";
import { proxy } from "comlink";
import { isBetaUser, isInternalUser } from "../feature-flags";
import { getRemoteFlag, updateRemoteFlag } from "../remote-store";
import type { UploadItem } from "../upload/types";
import { regenerateFaceCrops } from "./crop";
import { clearMLDB, faceIndex, indexableAndIndexedCounts } from "./db";
import { MLWorker } from "./worker";
/**
* In-memory flag that tracks if ML is enabled.
* In-memory flag that tracks if ML is enabled locally.
*
* - On app start, this is read from local storage in the `initML` function.
* - On app start, this is read from local storage during {@link initML}.
*
* - If the user updates their preference, then `setMLEnabled` will get called
* with the updated preference where this value will be updated (in addition
* to updating local storage).
* - It gets updated if the user enables/disables ML (remote) or if they
* pause/resume ML (local).
*
* - It is cleared in `logoutML`.
* - It is cleared in {@link logoutML}.
*/
let _isMLEnabled = false;
let _isMLEnabledLocal = false;
/**
* In-memory flag that tracks if the remote flag for ML is set.
*
* - It is updated each time we sync the status with remote.
*
* - It is cleared in {@link logoutML}.
*/
let _isMLEnabledRemote: boolean | undefined;
/** Cached instance of the {@link ComlinkWorker} that wraps our web worker. */
let _comlinkWorker: ComlinkWorker<typeof MLWorker> | undefined;
/**
* Subscriptions to {@link MLStatus}.
*
* See {@link mlStatusSubscribe}.
*/
let _mlStatusListeners: (() => void)[] = [];
/**
* Snapshot of {@link MLStatus}.
*
* See {@link mlStatusSnapshot}.
*/
let _mlStatusSnapshot: MLStatus | undefined;
/** Lazily created, cached, instance of {@link MLWorker}. */
const worker = async () => {
if (!_comlinkWorker) _comlinkWorker = await createComlinkWorker();
@@ -49,12 +69,17 @@ const createComlinkWorker = async () => {
computeFaceEmbeddings: electron.computeFaceEmbeddings,
computeCLIPImageEmbedding: electron.computeCLIPImageEmbedding,
};
const delegate = {
workerDidProcessFile,
};
const cw = new ComlinkWorker<typeof MLWorker>(
"ML",
new Worker(new URL("worker.ts", import.meta.url)),
);
await cw.remote.then((w) => w.init(proxy(mlWorkerElectron)));
await cw.remote.then((w) =>
w.init(proxy(mlWorkerElectron), proxy(delegate)),
);
return cw;
};
@@ -64,6 +89,8 @@ const createComlinkWorker = async () => {
* This is useful during logout to immediately stop any background ML operations
* that are in-flight for the current user. After the user logs in again, a new
* {@link worker} will be created on demand for subsequent usage.
*
* It is also called when the user pauses or disables ML.
*/
export const terminateMLWorker = () => {
if (_comlinkWorker) {
@@ -72,14 +99,28 @@ export const terminateMLWorker = () => {
}
};
/**
* Return true if the current client supports ML.
*
* ML currently only works when we're running in our desktop app.
*/
// TODO-ML:
export const isMLSupported =
isDesktop && process.env.NEXT_PUBLIC_ENTE_ENABLE_WIP_ML;
/**
* Was this someone who might've enabled the beta ML? If so, show them the
* coming back soon banner while we finalize it.
* TODO-ML:
*/
export const canEnableML = async () =>
(await isInternalUser()) || (await isBetaUser());
/**
* Initialize the ML subsystem if the user has enabled it in preferences.
*/
export const initML = () => {
// ML currently only works when we're running in our desktop app.
if (!isDesktop) return;
// TODO-ML: Rename the isFace* flag since it now drives ML as a whole.
_isMLEnabled = isFaceIndexingEnabled();
_isMLEnabledLocal = isMLEnabledLocally();
};
export const logoutML = async () => {
@@ -87,83 +128,149 @@ export const logoutML = async () => {
// reasons mentioned in [Note: Caching IDB instances in separate execution
// contexts], it gets called first in the logout sequence, and then this
// function (`logoutML`) gets called at a later point in time.
_isMLEnabled = false;
_isMLEnabledLocal = false;
_isMLEnabledRemote = undefined;
_mlStatusListeners = [];
_mlStatusSnapshot = undefined;
await clearMLDB();
};
/**
* Return true if we should show an option to the user to allow them to enable
* face search in the UI.
*/
export const canEnableFaceIndexing = async () =>
(await isInternalUser()) || (await isBetaUser());
/**
* Return true if the user has enabled machine learning in their preferences.
*
* TODO-ML: The UI for this needs rework. We might retain the older remote (and
* local) storage key, but otherwise this setting now reflects the state of ML
* overall and not just face search.
* [Note: ML preferences]
*
* The user may enable ML. This enables in both locally by persisting a local
* storage flag, and sets a flag on remote so that the user's other devices can
* also enable it if they wish.
*
* The user may pause ML locally. This does not modify the remote flag, but it
* unsets the local flag. Subsequently resuming ML (locally) will set the local
* flag again.
*
* ML related operations are driven by the {@link isMLEnabled} property. This is
* true if ML is enabled locally (which implies it is also enabled on remote).
*/
export const isMLEnabled = () =>
// Impl note: Keep it fast, the UI directly calls this multiple times.
_isMLEnabled;
// Implementation note: Keep it fast, it might be called frequently.
_isMLEnabledLocal;
/**
* Enable ML.
*
* Persist the user's preference and trigger a sync.
* Persist the user's preference both locally and on remote, and trigger a sync.
*/
export const enableML = () => {
setIsFaceIndexingEnabled(true);
_isMLEnabled = true;
export const enableML = async () => {
await updateIsMLEnabledRemote(true);
setIsMLEnabledLocally(true);
_isMLEnabledRemote = true;
_isMLEnabledLocal = true;
setInterimScheduledStatus();
triggerStatusUpdate();
triggerMLSync();
};
/**
* Disable ML.
*
* Stop any in-progress ML tasks and persist the user's preference.
* Stop any in-progress ML tasks, and persist the user's preference both locally
* and on remote.
*/
export const disableML = () => {
export const disableML = async () => {
await updateIsMLEnabledRemote(false);
terminateMLWorker();
setIsFaceIndexingEnabled(false);
_isMLEnabled = false;
setIsMLEnabledLocally(false);
_isMLEnabledRemote = false;
_isMLEnabledLocal = false;
triggerStatusUpdate();
};
/**
* Return true if the user has enabled face indexing in the app's settings.
* Pause ML on this device.
*
* This setting is persisted locally (in local storage) and is not synced with
* remote. There is a separate setting, "faceSearchEnabled" that is synced with
* remote, but that tracks whether or not the user has enabled face search once
* on any client. This {@link isFaceIndexingEnabled} property, on the other
* hand, denotes whether or not indexing is enabled on the current client.
* Stop any in-progress ML tasks, and persist the user's local preference.
*/
const isFaceIndexingEnabled = () =>
export const pauseML = () => {
terminateMLWorker();
setIsMLEnabledLocally(false);
_isMLEnabledLocal = false;
triggerStatusUpdate();
};
/**
* Resume ML on this device.
*
* Persist the user's preference locally, and trigger a sync.
*/
export const resumeML = () => {
setIsMLEnabledLocally(true);
_isMLEnabledLocal = true;
setInterimScheduledStatus();
triggerStatusUpdate();
triggerMLSync();
};
/**
* Return true if ML is enabled locally.
*
* This setting is persisted locally (in local storage). It is not synced with
* remote and only tracks if ML is enabled locally.
*
* The remote status is tracked with a separate {@link isMLEnabledRemote} flag
* that is synced with remote.
*/
const isMLEnabledLocally = () =>
localStorage.getItem("faceIndexingEnabled") == "1";
/**
* Update the (locally stored) value of {@link isFaceIndexingEnabled}.
* Update the (locally stored) value of {@link isMLEnabledLocally}.
*/
const setIsFaceIndexingEnabled = (enabled: boolean) =>
const setIsMLEnabledLocally = (enabled: boolean) =>
enabled
? localStorage.setItem("faceIndexingEnabled", "1")
: localStorage.removeItem("faceIndexingEnabled");
/**
* For historical reasons, this is called "faceSearchEnabled" (it started off as
* a flag to ensure we have taken the face recognition consent from the user).
*
* Now it tracks the status of ML in general (which includes faces + consent).
*/
const mlRemoteKey = "faceSearchEnabled";
/**
* Return `true` if the flag to enable ML is set on remote.
*/
export const getIsMLEnabledRemote = () => getRemoteFlag(mlRemoteKey);
/**
* Update the remote flag that tracks ML status across the user's devices.
*/
const updateIsMLEnabledRemote = (enabled: boolean) =>
updateRemoteFlag(mlRemoteKey, enabled);
/**
* Trigger a "sync", whatever that means for the ML subsystem.
*
* This is called during the global sync sequence. If ML is enabled, then we use
* this as a signal to pull embeddings from remote, and start backfilling if
* needed.
* This is called during the global sync sequence.
*
* First we check again with remote ML flag is set. If it is not set, then we
* disable ML locally too.
*
* Otherwise, and if ML is enabled locally also, then we use this as a signal to
* pull embeddings from remote, and start backfilling if needed.
*
* This function does not wait for these processes to run to completion, and
* returns immediately.
*/
export const triggerMLSync = () => {
if (!_isMLEnabled) return;
void worker().then((w) => w.sync());
export const triggerMLSync = () => void mlSync();
const mlSync = async () => {
_isMLEnabledRemote = await getIsMLEnabledRemote();
if (!_isMLEnabledRemote) _isMLEnabledLocal = false;
triggerStatusUpdate();
if (_isMLEnabledLocal) void worker().then((w) => w.sync());
};
/**
@@ -182,51 +289,109 @@ export const triggerMLSync = () => {
* image part of the live photo that was uploaded.
*/
export const indexNewUpload = (enteFile: EnteFile, uploadItem: UploadItem) => {
if (!_isMLEnabled) return;
if (!_isMLEnabledLocal) return;
if (enteFile.metadata.fileType !== FILE_TYPE.IMAGE) return;
log.debug(() => ["ml/liveq", { enteFile, uploadItem }]);
void worker().then((w) => w.onUpload(enteFile, uploadItem));
};
export interface FaceIndexingStatus {
/**
* Which phase we are in within the indexing pipeline when viewed across the
* user's entire library:
*
* - "scheduled": There are files we know of that have not been indexed.
*
* - "indexing": The face indexer is currently running.
*
* - "clustering": All files we know of have been indexed, and we are now
* clustering the faces that were found.
*
* - "done": Face indexing and clustering is complete for the user's
* library.
*/
phase: "scheduled" | "indexing" | "clustering" | "done";
/** The number of files that have already been indexed. */
nSyncedFiles: number;
/** The total number of files that are eligible for indexing. */
nTotalFiles: number;
}
export type MLStatus =
| { phase: "disabled" /* The ML remote flag is off */ }
| {
/**
* Which phase we are in within the indexing pipeline when viewed across the
* user's entire library:
*
* - "paused": ML is currently paused on this device.
*
* - "scheduled": There are files we know of that have not been indexed.
*
* - "indexing": The indexer is currently running.
*
* - "clustering": All file we know of have been indexed, and we are now
* clustering the faces that were found.
*
* - "done": ML indexing and face clustering is complete for the user's
* library.
*/
phase: "paused" | "scheduled" | "indexing" | "clustering" | "done";
/** The number of files that have already been indexed. */
nSyncedFiles: number;
/** The total number of files that are eligible for indexing. */
nTotalFiles: number;
};
/**
* Return the current state of the face indexing pipeline.
* A function that can be used to subscribe to updates in the ML status.
*
* Precondition: ML must be enabled.
* This, along with {@link mlStatusSnapshot}, is meant to be used as arguments
* to React's {@link useSyncExternalStore}.
*
* @param callback A function that will be invoked whenever the result of
* {@link mlStatusSnapshot} changes.
*
* @returns A function that can be used to clear the subscription.
*/
export const faceIndexingStatus = async (): Promise<FaceIndexingStatus> => {
if (!isMLEnabled())
throw new Error("Cannot get indexing status when ML is not enabled");
export const mlStatusSubscribe = (onChange: () => void): (() => void) => {
_mlStatusListeners.push(onChange);
return () => {
_mlStatusListeners = _mlStatusListeners.filter((l) => l != onChange);
};
};
/**
* Return the last known, cached {@link MLStatus}.
*
* This, along with {@link mlStatusSnapshot}, is meant to be used as arguments
* to React's {@link useSyncExternalStore}.
*
* A return value of `undefined` indicates that we're still performing the
* asynchronous tasks that are needed to get the status.
*/
export const mlStatusSnapshot = (): MLStatus | undefined => {
const result = _mlStatusSnapshot;
// We don't have it yet, trigger an update.
if (!result) triggerStatusUpdate();
return result;
};
/**
* Trigger an asynchronous and unconditional update of the {@link MLStatus}
* snapshot.
*/
const triggerStatusUpdate = () => void updateMLStatusSnapshot();
/** Unconditionally update of the {@link MLStatus} snapshot. */
const updateMLStatusSnapshot = async () =>
setMLStatusSnapshot(await getMLStatus());
const setMLStatusSnapshot = (snapshot: MLStatus) => {
_mlStatusSnapshot = snapshot;
_mlStatusListeners.forEach((l) => l());
};
/**
* Return the current state of the ML subsystem.
*
* Precondition: ML must be enabled on remote, though it is fine if it is paused
* locally.
*/
const getMLStatus = async (): Promise<MLStatus> => {
if (!_isMLEnabledRemote) return { phase: "disabled" };
const { indexedCount, indexableCount } = await indexableAndIndexedCounts();
const isIndexing = await (await worker()).isIndexing();
let phase: FaceIndexingStatus["phase"];
if (indexableCount > 0) {
phase = !isIndexing ? "scheduled" : "indexing";
let phase: MLStatus["phase"];
if (!_isMLEnabledLocal) {
phase = "paused";
} else {
phase = "done";
const isIndexing = await (await worker()).isIndexing();
if (indexableCount > 0) {
phase = !isIndexing ? "scheduled" : "indexing";
} else {
phase = "done";
}
}
return {
@@ -236,6 +401,28 @@ export const faceIndexingStatus = async (): Promise<FaceIndexingStatus> => {
};
};
/**
* When the user enables or resumes ML, we wish to give immediate feedback.
*
* So this is an intermediate state with possibly incorrect counts (but correct
* phase) that is set immediately to trigger a UI update. It uses the counts
* from the last known status, just updates the phase.
*
* Once the worker is initialized and the correct counts fetched, this will
* update to the correct state (should take less than one second).
*/
const setInterimScheduledStatus = () => {
let nSyncedFiles = 0,
nTotalFiles = 0;
if (_mlStatusSnapshot && _mlStatusSnapshot.phase != "disabled") {
nSyncedFiles = _mlStatusSnapshot.nSyncedFiles;
nTotalFiles = _mlStatusSnapshot.nTotalFiles;
}
setMLStatusSnapshot({ phase: "scheduled", nSyncedFiles, nTotalFiles });
};
const workerDidProcessFile = triggerStatusUpdate;
/**
* Return the IDs of all the faces in the given {@link enteFile} that are not
* associated with a person cluster.

View File

@@ -2,7 +2,7 @@ export interface Person {
id: number;
name?: string;
files: number[];
displayFaceId?: string;
displayFaceId: string;
}
// TODO-ML(MR): Forced disable clustering. It doesn't currently work,

View File

@@ -1,3 +1,8 @@
/**
* @file Type for the objects shared (as a Comlink proxy) by the main thread and
* the ML worker.
*/
/**
* A subset of {@link Electron} provided to the {@link MLWorker}.
*
@@ -12,3 +17,16 @@ export interface MLWorkerElectron {
computeFaceEmbeddings: (input: Float32Array) => Promise<Float32Array>;
computeCLIPImageEmbedding: (input: Float32Array) => Promise<Float32Array>;
}
/**
* Callbacks invoked by the worker at various points in the indexing pipeline to
* notify the main thread of events it might be interested in.
*/
export interface MLWorkerDelegate {
/**
* Called whenever a file is processed during indexing.
*
* It is called both when the indexing was successful or failed.
*/
workerDidProcessFile: () => void;
}

View File

@@ -22,7 +22,7 @@ import {
} from "./db";
import { pullFaceEmbeddings, putCLIPIndex, putFaceIndex } from "./embedding";
import { indexFaces, type FaceIndex } from "./face";
import type { MLWorkerElectron } from "./worker-electron";
import type { MLWorkerDelegate, MLWorkerElectron } from "./worker-types";
const idleDurationStart = 5; /* 5 seconds */
const idleDurationMax = 16 * 60; /* 16 minutes */
@@ -56,6 +56,7 @@ interface IndexableItem {
*/
export class MLWorker {
private electron: MLWorkerElectron | undefined;
private delegate: MLWorkerDelegate | undefined;
private userAgent: string | undefined;
private state: "idle" | "pull" | "indexing" = "idle";
private shouldPull = false;
@@ -73,9 +74,13 @@ export class MLWorker {
* @param electron The {@link MLWorkerElectron} that allows the worker to
* use the functionality provided by our Node.js layer when running in the
* context of our desktop app
*
* @param delegate The {@link MLWorkerDelegate} the worker can use to inform
* the main thread of interesting events.
*/
async init(electron: MLWorkerElectron) {
async init(electron: MLWorkerElectron, delegate?: MLWorkerDelegate) {
this.electron = electron;
this.delegate = delegate;
// Set the user agent that'll be set in the generated embeddings.
this.userAgent = `${clientPackageName}/${await electron.appVersion()}`;
// Initialize the downloadManager running in the web worker with the
@@ -202,6 +207,7 @@ export class MLWorker {
items,
ensure(this.electron),
ensure(this.userAgent),
this.delegate,
);
if (allSuccess) {
// Everything is running smoothly. Reset the idle duration.
@@ -276,6 +282,7 @@ const indexNextBatch = async (
items: IndexableItem[],
electron: MLWorkerElectron,
userAgent: string,
delegate: MLWorkerDelegate | undefined,
) => {
// Don't try to index if we wouldn't be able to upload them anyway. The
// liveQ has already been drained, but that's fine, it'll be rare that we
@@ -293,6 +300,7 @@ const indexNextBatch = async (
for (const { enteFile, uploadItem } of items) {
try {
await index(enteFile, uploadItem, electron, userAgent);
delegate?.workerDidProcessFile();
// Possibly unnecessary, but let us drain the microtask queue.
await wait(0);
} catch {

View File

@@ -0,0 +1,44 @@
import { authenticatedRequestHeaders, ensureOk } from "@/next/http";
import { apiURL } from "@/next/origins";
import { z } from "zod";
/**
* Fetch the value for the given {@link key} from remote store.
*
* If the key is not present in the remote store, return `undefined`.
*/
export const getRemoteValue = async (key: string) => {
const url = await apiURL("/remote-store");
const params = new URLSearchParams({ key });
const res = await fetch(`${url}?${params.toString()}`, {
headers: await authenticatedRequestHeaders(),
});
ensureOk(res);
return GetRemoteStoreResponse.parse(await res.json())?.value;
};
const GetRemoteStoreResponse = z.object({ value: z.string() }).nullable();
/**
* Convenience wrapper over {@link getRemoteValue} that returns booleans.
*/
export const getRemoteFlag = async (key: string) =>
(await getRemoteValue(key)) == "true";
/**
* Update or insert {@link value} for the given {@link key} into remote store.
*/
export const updateRemoteValue = async (key: string, value: string) =>
ensureOk(
await fetch(await apiURL("/remote-store/update"), {
method: "POST",
headers: await authenticatedRequestHeaders(),
body: JSON.stringify({ key, value }),
}),
);
/**
* Convenience wrapper over {@link updateRemoteValue} that sets booleans.
*/
export const updateRemoteFlag = (key: string, value: boolean) =>
updateRemoteValue(key, JSON.stringify(value));

View File

@@ -0,0 +1,18 @@
import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
/**
* A subset of the AppContext type used by the photos app.
*
* [Note: Migrating components that need the app context]
*
* This only exists to make it easier to migrate code into the @/new package.
* Once we move this code back (after TypeScript strict mode migration is done),
* then the code that uses this can start directly using the actual app context
* instead of needing to explicitly pass a prop of this type.
* */
export interface NewAppContextPhotos {
startLoading: () => void;
finishLoading: () => void;
setDialogBoxAttributesV2: (attrs: DialogBoxAttributesV2) => void;
somethingWentWrong: () => void;
}

View File

@@ -7,7 +7,7 @@
*/
import type { Electron, ZipItem } from "@/next/types/ipc";
import type { MLWorkerElectron } from "../services/ml/worker-electron";
import type { MLWorkerElectron } from "../services/ml/worker-types";
/**
* Stream the given file or zip entry from the user's local file system.

View File

@@ -1,3 +1,16 @@
/**
* Open the given {@link url} in a new browser tab.
*
* @param url The URL to open.
*/
export const openURL = (url: string) => {
const a = document.createElement("a");
a.href = url;
a.target = "_blank";
a.rel = "noopener";
a.click();
};
/**
* Open the system configured email client, initiating a new email to the given
* {@link email} address.
@@ -5,6 +18,6 @@
export const initiateEmail = (email: string) => {
const a = document.createElement("a");
a.href = "mailto:" + email;
a.rel = "noreferrer noopener";
a.rel = "noopener";
a.click();
};

View File

@@ -0,0 +1,70 @@
import { VerticallyCenteredFlex } from "@ente/shared/components/Container";
import { Divider, styled, Typography } from "@mui/material";
import React from "react";
interface MenuSectionTitleProps {
title: string;
icon?: JSX.Element;
}
export const MenuSectionTitle: React.FC<MenuSectionTitleProps> = ({
title,
icon,
}) => {
return (
<VerticallyCenteredFlex
px="8px"
py={"6px"}
gap={"8px"}
sx={{
"& > svg": {
fontSize: "17px",
color: (theme) => theme.colors.stroke.muted,
},
}}
>
{icon && icon}
<Typography variant="small" color="text.muted">
{title}
</Typography>
</VerticallyCenteredFlex>
);
};
interface MenuItemDividerProps {
hasIcon?: boolean;
}
export const MenuItemDivider: React.FC<MenuItemDividerProps> = ({
hasIcon,
}) => {
return (
<Divider
sx={{
"&&&": {
my: 0,
ml: hasIcon ? "48px" : "16px",
},
}}
/>
);
};
export const MenuItemGroup = styled("div")(
({ theme }) => `
& > .MuiMenuItem-root{
border-radius: 8px;
background-color: transparent;
}
& > .MuiMenuItem-root:not(:last-of-type) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
& > .MuiMenuItem-root:not(:first-of-type) {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
background-color: ${theme.colors.fill.faint};
border-radius: 8px;
`,
);

View File

@@ -2,8 +2,9 @@ import { FlexWrapper } from "@ente/shared/components/Container";
import ArrowBack from "@mui/icons-material/ArrowBack";
import Close from "@mui/icons-material/Close";
import { Box, IconButton, Typography } from "@mui/material";
import React from "react";
interface Iprops {
interface TitlebarProps {
title: string;
caption?: string;
onClose: () => void;
@@ -12,14 +13,14 @@ interface Iprops {
actionButton?: JSX.Element;
}
export default function Titlebar({
export const Titlebar: React.FC<TitlebarProps> = ({
title,
caption,
onClose,
backIsClose,
actionButton,
onRootClose,
}: Iprops): JSX.Element {
}) => {
return (
<>
<FlexWrapper
@@ -56,4 +57,4 @@ export default function Titlebar({
</Box>
</>
);
}
};

View File

@@ -136,7 +136,7 @@ const savedLocaleStringMigratingIfNeeded = (): SupportedLocale | undefined => {
// This migration is dated Feb 2024. And it can be removed after a few
// months, because by then either customers would've opened the app and
// their setting migrated to the new format, or the browser would've cleared
// the older local storage entry anyway.
// the older local storage entry anyway (tag: Migration).
if (!ls) {
// Nothing found

View File

@@ -1,4 +1,4 @@
import { ButtonProps } from "@mui/material";
import type { ButtonProps } from "@mui/material";
export interface DialogBoxAttributes {
icon?: React.ReactNode;

View File

@@ -1,10 +0,0 @@
import { Drawer, styled } from "@mui/material";
export const EnteDrawer = styled(Drawer)(({ theme }) => ({
"& .MuiPaper-root": {
maxWidth: "375px",
width: "100%",
scrollbarWidth: "thin",
padding: theme.spacing(1),
},
}));

View File

@@ -72,6 +72,7 @@ export function EnteMenuItem({
<MenuItem
disabled={disabled}
onClick={handleButtonClick}
disableRipple={variant == "toggle"}
sx={{
width: "100%",
color: (theme) =>

View File

@@ -1,16 +0,0 @@
import { Divider } from "@mui/material";
interface Iprops {
hasIcon?: boolean;
}
export default function MenuItemDivider({ hasIcon = false }: Iprops) {
return (
<Divider
sx={{
"&&&": {
my: 0,
ml: hasIcon ? "48px" : "16px",
},
}}
/>
);
}

View File

@@ -1,20 +0,0 @@
import { styled } from "@mui/material";
export const MenuItemGroup = styled("div")(
({ theme }) => `
& > .MuiMenuItem-root{
border-radius: 8px;
background-color: transparent;
}
& > .MuiMenuItem-root:not(:last-of-type) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
& > .MuiMenuItem-root:not(:first-of-type) {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
background-color: ${theme.colors.fill.faint};
border-radius: 8px;
`,
);

View File

@@ -1,28 +0,0 @@
import { VerticallyCenteredFlex } from "@ente/shared/components/Container";
import { Typography } from "@mui/material";
interface Iprops {
title: string;
icon?: JSX.Element;
}
export default function MenuSectionTitle({ title, icon }: Iprops) {
return (
<VerticallyCenteredFlex
px="8px"
py={"6px"}
gap={"8px"}
sx={{
"& > svg": {
fontSize: "17px",
color: (theme) => theme.colors.stroke.muted,
},
}}
>
{icon && icon}
<Typography variant="small" color="text.muted">
{title}
</Typography>
</VerticallyCenteredFlex>
);
}