Merge remote-tracking branch 'origin/main' into mobile-ffprobe
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*
|
||||
* The runtime used is ONNX.
|
||||
*/
|
||||
|
||||
import * as ort from "onnxruntime-node";
|
||||
import log from "../log";
|
||||
import { ensure } from "../utils/common";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
4
mobile/lib/generated/intl/messages_all.dart
generated
4
mobile/lib/generated/intl/messages_all.dart
generated
@@ -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
1710
mobile/lib/generated/intl/messages_tr.dart
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
mobile/lib/generated/l10n.dart
generated
1
mobile/lib/generated/l10n.dart
generated
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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
1279
mobile/lib/l10n/intl_tr.arb
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
`,
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 ".";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 ".";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
416
web/packages/new/photos/components/MLSettings.tsx
Normal file
416
web/packages/new/photos/components/MLSettings.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
60
web/packages/new/photos/components/MLSettingsBeta.tsx
Normal file
60
web/packages/new/photos/components/MLSettingsBeta.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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 ? (
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
44
web/packages/new/photos/services/remote-store.ts
Normal file
44
web/packages/new/photos/services/remote-store.ts
Normal 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));
|
||||
18
web/packages/new/photos/types/context.ts
Normal file
18
web/packages/new/photos/types/context.ts
Normal 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;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
70
web/packages/new/shared/components/Menu.tsx
Normal file
70
web/packages/new/shared/components/Menu.tsx
Normal 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;
|
||||
`,
|
||||
);
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ButtonProps } from "@mui/material";
|
||||
import type { ButtonProps } from "@mui/material";
|
||||
|
||||
export interface DialogBoxAttributes {
|
||||
icon?: React.ReactNode;
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}));
|
||||
@@ -72,6 +72,7 @@ export function EnteMenuItem({
|
||||
<MenuItem
|
||||
disabled={disabled}
|
||||
onClick={handleButtonClick}
|
||||
disableRipple={variant == "toggle"}
|
||||
sx={{
|
||||
width: "100%",
|
||||
color: (theme) =>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
`,
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user