[web] Remove emotion cache (#1272)

- Still doesn't work in dev mode
- Prepares ground for removing bootstrap
This commit is contained in:
Manav Rathi 2024-04-01 12:14:50 +05:30 committed by GitHub
commit a9b92b9bfa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 575 additions and 2198 deletions

View file

@ -31,7 +31,7 @@ are built against `electron`'s packaged `node` version. We use
to rebuild those modules automatically after each `yarn install` by invoking it
in as the `postinstall` step in our package.json.
### lint and lint-fix
### lint, lint-fix
Use `yarn lint` to check that your code formatting is as expected, and that
there are no linter errors. Use `yarn lint-fix` to try and automatically fix the

View file

@ -32,7 +32,7 @@ yarn dev
That's it. The web app will automatically hot reload when you make changes.
If you're new to web development and unsure about how to get started, or are
If you're new to web development and unsure about how to get started, or are
facing some problems when running the above steps, see [docs/new](docs/new.md).
## Other apps

View file

@ -1,7 +1,5 @@
import { setupI18n } from "@/ui/i18n";
import { CacheProvider } from "@emotion/react";
import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
import { EnteAppProps } from "@ente/shared/apps/types";
import { Overlay } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import {
@ -15,9 +13,9 @@ import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { getTheme } from "@ente/shared/themes";
import { THEME_COLOR } from "@ente/shared/themes/constants";
import createEmotionCache from "@ente/shared/themes/createEmotionCache";
import { CssBaseline, useMediaQuery } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles";
import { AppProps } from "next/app";
import Head from "next/head";
import { useRouter } from "next/router";
import { createContext, useEffect, useState } from "react";
@ -31,10 +29,7 @@ interface AppContextProps {
export const AppContext = createContext<AppContextProps>({} as AppContextProps);
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
export default function App(props: EnteAppProps) {
export default function App(props: AppProps) {
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
const [showNavbar, setShowNavBar] = useState(false);
@ -54,11 +49,7 @@ export default function App(props: EnteAppProps) {
const router = useRouter();
const {
Component,
emotionCache = clientSideEmotionCache,
pageProps,
} = props;
const { Component, pageProps } = props;
const [themeColor] = useLocalState(LS_KEYS.THEME, THEME_COLOR.DARK);
@ -87,7 +78,7 @@ export default function App(props: EnteAppProps) {
// TODO: Localise APP_TITLES
return (
<CacheProvider value={emotionCache}>
<>
<Head>
<title>{APP_TITLES.get(APPS.ACCOUNTS)}</title>
<meta
@ -128,9 +119,9 @@ export default function App(props: EnteAppProps) {
</Overlay>
)}
{showNavbar && <AppNavbar isMobile={isMobile} />}
<Component {...pageProps} />
{isI18nReady && <Component {...pageProps} />}
</AppContext.Provider>
</ThemeProvider>
</CacheProvider>
</>
);
}

View file

@ -1,7 +1,3 @@
import DocumentPage, {
EnteDocumentProps,
} from "@ente/shared/next/pages/_document";
import DocumentPage from "@ente/shared/next/pages/_document";
export default function Document(props: EnteDocumentProps) {
return <DocumentPage {...props} />;
}
export default DocumentPage;

View file

@ -150,21 +150,6 @@ body {
background-color: #51cd7c;
}
.carousel-inner {
padding-bottom: 50px !important;
}
.carousel-indicators li {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 12px;
}
.carousel-indicators .active {
background-color: #51cd7c;
}
div.otp-input input {
width: 36px !important;
height: 36px;

View file

@ -1,7 +1,9 @@
import AppNavbar from "@ente/shared/components/Navbar/app";
import { t } from "i18next";
import { createContext, useEffect, useRef, useState } from "react";
import { setupI18n } from "@/ui/i18n";
import {
APPS,
APP_TITLES,
CLIENT_PACKAGE_NAMES,
} from "@ente/shared/apps/constants";
import { Overlay } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import {
@ -10,32 +12,26 @@ import {
} from "@ente/shared/components/DialogBoxV2/types";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { MessageContainer } from "@ente/shared/components/MessageContainer";
import AppNavbar from "@ente/shared/components/Navbar/app";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
import {
clearLogsIfLocalStorageLimitExceeded,
logStartupMessage,
} from "@ente/shared/logging/web";
import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS } from "@ente/shared/storage/localStorage";
import { CssBaseline, useMediaQuery } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles";
import Head from "next/head";
import { useRouter } from "next/router";
import LoadingBar from "react-top-loading-bar";
import { setupI18n } from "@/ui/i18n";
import { CacheProvider } from "@emotion/react";
import {
APP_TITLES,
APPS,
CLIENT_PACKAGE_NAMES,
} from "@ente/shared/apps/constants";
import { EnteAppProps } from "@ente/shared/apps/types";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
import { getTheme } from "@ente/shared/themes";
import { THEME_COLOR } from "@ente/shared/themes/constants";
import createEmotionCache from "@ente/shared/themes/createEmotionCache";
import { SetTheme } from "@ente/shared/themes/types";
import { CssBaseline, useMediaQuery } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles";
import { t } from "i18next";
import { AppProps } from "next/app";
import Head from "next/head";
import { useRouter } from "next/router";
import { createContext, useEffect, useRef, useState } from "react";
import LoadingBar from "react-top-loading-bar";
import "../../public/css/global.css";
type AppContextType = {
@ -51,15 +47,8 @@ type AppContextType = {
export const AppContext = createContext<AppContextType>(null);
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
export default function App(props: EnteAppProps) {
const {
Component,
emotionCache = clientSideEmotionCache,
pageProps,
} = props;
export default function App(props: AppProps) {
const { Component, pageProps } = props;
const router = useRouter();
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
const [loading, setLoading] = useState(false);
@ -141,7 +130,7 @@ export default function App(props: EnteAppProps) {
});
return (
<CacheProvider value={emotionCache}>
<>
<Head>
<title>
{isI18nReady
@ -195,9 +184,11 @@ export default function App(props: EnteAppProps) {
<EnteSpinner />
</Overlay>
)}
<Component setLoading={setLoading} {...pageProps} />
{isI18nReady && (
<Component setLoading={setLoading} {...pageProps} />
)}
</AppContext.Provider>
</ThemeProvider>
</CacheProvider>
</>
);
}

View file

@ -1,7 +1,3 @@
import DocumentPage, {
EnteDocumentProps,
} from "@ente/shared/next/pages/_document";
import DocumentPage from "@ente/shared/next/pages/_document";
export default function Document(props: EnteDocumentProps) {
return <DocumentPage {...props} />;
}
export default DocumentPage;

View file

@ -16,7 +16,6 @@
"@tensorflow/tfjs-converter": "^4.10.0",
"@tensorflow/tfjs-core": "^4.10.0",
"@tensorflow/tfjs-tflite": "0.0.1-alpha.7",
"@zip.js/zip.js": "2.4.2",
"bip39": "^3.0.4",
"blazeface-back": "^0.0.9",
"bootstrap": "^4.5.2",
@ -45,6 +44,7 @@
"p-queue": "^7.1.0",
"photoswipe": "file:./thirdparty/photoswipe",
"piexifjs": "^1.0.6",
"pure-react-carousel": "^1.30.1",
"react-bootstrap": "^1.3.0",
"react-datepicker": "^4.16.0",
"react-dropzone": "^11.2.4",

View file

@ -7,11 +7,11 @@ import {
Button,
DialogActions,
DialogContent,
LinearProgress,
styled,
} from "@mui/material";
import { ExportStage } from "constants/export";
import { t } from "i18next";
import { ProgressBar } from "react-bootstrap";
import { Trans } from "react-i18next";
import { ExportProgress } from "types/export";
@ -69,21 +69,19 @@ export default function ExportInProgress(props: Props) {
)}
</Box>
<FlexWrapper px={1}>
<ProgressBar
style={{ width: "100%" }}
now={
showIndeterminateProgress()
? 100
: Math.round(
((props.exportProgress.success +
props.exportProgress.failed) *
100) /
props.exportProgress.total,
)
}
animated
variant="upload-progress-bar"
/>
{showIndeterminateProgress() ? (
<LinearProgress />
) : (
<LinearProgress
variant="determinate"
value={Math.round(
((props.exportProgress.success +
props.exportProgress.failed) *
100) /
props.exportProgress.total,
)}
/>
)}
</FlexWrapper>
</VerticallyCentered>
</DialogContent>

View file

@ -0,0 +1,299 @@
import { Row, Value } from "@ente/shared/components/Container";
import DialogBox from "@ente/shared/components/DialogBox/";
import { Button, LinearProgress } from "@mui/material";
import EnteDateTimePicker from "components/EnteDateTimePicker";
import { ComfySpan } from "components/ExportInProgress";
import { Formik } from "formik";
import { t } from "i18next";
import { GalleryContext } from "pages/gallery";
import React, { ChangeEvent, useContext, useEffect, useState } from "react";
import { Form } from "react-bootstrap";
import { updateCreationTimeWithExif } from "services/updateCreationTimeWithExif";
import { EnteFile } from "types/file";
export interface FixCreationTimeAttributes {
files: EnteFile[];
}
type Step = "running" | "completed" | "error";
export enum FIX_OPTIONS {
DATE_TIME_ORIGINAL,
DATE_TIME_DIGITIZED,
METADATA_DATE,
CUSTOM_TIME,
}
interface formValues {
option: FIX_OPTIONS;
customTime: Date;
}
interface FixCreationTimeProps {
isOpen: boolean;
show: () => void;
hide: () => void;
attributes: FixCreationTimeAttributes;
}
const FixCreationTime: React.FC<FixCreationTimeProps> = (props) => {
const [step, setStep] = useState<Step | undefined>();
const [progressTracker, setProgressTracker] = useState({
current: 0,
total: 0,
});
const galleryContext = useContext(GalleryContext);
useEffect(() => {
// TODO (MR): Not sure why this is needed
if (props.attributes && props.isOpen && step !== "running") {
setStep(undefined);
}
}, [props.isOpen]);
const startFix = async (option: FIX_OPTIONS, customTime: Date) => {
setStep("running");
const failed = await updateCreationTimeWithExif(
props.attributes.files,
option,
customTime,
setProgressTracker,
);
setStep(failed ? "error" : "completed");
await galleryContext.syncWithRemote();
};
const onSubmit = (values: formValues) => {
startFix(Number(values.option), new Date(values.customTime));
};
const title =
step === "running"
? t("FIX_CREATION_TIME_IN_PROGRESS")
: t("FIX_CREATION_TIME");
const message = messageForStep(step);
if (!props.attributes) {
return <></>;
}
return (
<DialogBox
open={props.isOpen}
onClose={props.hide}
attributes={{ title, nonClosable: true }}
>
<div
style={{
marginBottom: "10px",
display: "flex",
flexDirection: "column",
...(step === "running" ? { alignItems: "center" } : {}),
}}
>
{message && <div>{message}</div>}
{step === "running" && (
<FixCreationTimeRunning progressTracker={progressTracker} />
)}
<Formik<formValues>
initialValues={{
option: FIX_OPTIONS.DATE_TIME_ORIGINAL,
customTime: new Date(),
}}
validateOnBlur={false}
onSubmit={onSubmit}
>
{({ values, handleChange, handleSubmit }) => (
<>
{(step === undefined || step === "error") && (
<div style={{ marginTop: "10px" }}>
<FixCreationTimeOptions
handleChange={handleChange}
values={values}
/>
</div>
)}
<FixCreationTimeFooter
step={step}
startFix={handleSubmit}
hide={props.hide}
/>
</>
)}
</Formik>
</div>
</DialogBox>
);
};
export default FixCreationTime;
const messageForStep = (step?: Step) => {
switch (step) {
case undefined:
return t("UPDATE_CREATION_TIME_NOT_STARTED");
case "running":
return undefined;
case "completed":
return t("UPDATE_CREATION_TIME_COMPLETED");
case "error":
return t("UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR");
}
};
const Option = ({
value,
selected,
onChange,
label,
}: {
value: FIX_OPTIONS;
selected: FIX_OPTIONS;
onChange: (e: string | ChangeEvent<any>) => void;
label: string;
}) => (
<Form.Check
name="group1"
style={{
margin: "5px 0",
color: value !== Number(selected) ? "#aaa" : "#fff",
}}
>
<Form.Check.Input
id={value.toString()}
type="radio"
value={value}
checked={value === Number(selected)}
onChange={onChange}
/>
<Form.Check.Label
style={{ cursor: "pointer" }}
htmlFor={value.toString()}
>
{label}
</Form.Check.Label>
</Form.Check>
);
function FixCreationTimeOptions({ handleChange, values }) {
return (
<Form noValidate>
<Row style={{ margin: "0" }}>
<Option
value={FIX_OPTIONS.DATE_TIME_ORIGINAL}
onChange={handleChange("option")}
label={t("DATE_TIME_ORIGINAL")}
selected={Number(values.option)}
/>
</Row>
<Row style={{ margin: "0" }}>
<Option
value={FIX_OPTIONS.DATE_TIME_DIGITIZED}
onChange={handleChange("option")}
label={t("DATE_TIME_DIGITIZED")}
selected={Number(values.option)}
/>
</Row>
<Row style={{ margin: "0" }}>
<Option
value={FIX_OPTIONS.METADATA_DATE}
onChange={handleChange("option")}
label={t("METADATA_DATE")}
selected={Number(values.option)}
/>
</Row>
<Row style={{ margin: "0" }}>
<Value width="50%">
<Option
value={FIX_OPTIONS.CUSTOM_TIME}
onChange={handleChange("option")}
label={t("CUSTOM_TIME")}
selected={Number(values.option)}
/>
</Value>
{Number(values.option) === FIX_OPTIONS.CUSTOM_TIME && (
<Value width="40%">
<EnteDateTimePicker
onSubmit={(x: Date) =>
handleChange("customTime")(x.toUTCString())
}
/>
</Value>
)}
</Row>
</Form>
);
}
const FixCreationTimeFooter = ({ step, startFix, ...props }) => {
return (
step !== "running" && (
<div
style={{
width: "100%",
display: "flex",
marginTop: "30px",
justifyContent: "space-around",
}}
>
{(step === undefined || step === "error") && (
<Button
color="secondary"
size="large"
onClick={() => {
props.hide();
}}
>
{t("CANCEL")}
</Button>
)}
{step === "completed" && (
<Button color="primary" size="large" onClick={props.hide}>
{t("CLOSE")}
</Button>
)}
{(step === undefined || step === "error") && (
<>
<div style={{ width: "30px" }} />
<Button color="accent" size="large" onClick={startFix}>
{t("FIX_CREATION_TIME")}
</Button>
</>
)}
</div>
)
);
};
const FixCreationTimeRunning = ({ progressTracker }) => {
const progress = Math.round(
(progressTracker.current * 100) / progressTracker.total,
);
return (
<>
<div style={{ marginBottom: "10px" }}>
<ComfySpan>
{" "}
{progressTracker.current} / {progressTracker.total}{" "}
</ComfySpan>{" "}
<span style={{ marginLeft: "10px" }}>
{" "}
{t("CREATION_TIME_UPDATED")}
</span>
</div>
<div
style={{
width: "100%",
marginTop: "10px",
marginBottom: "20px",
}}
>
<LinearProgress variant="determinate" value={progress} />
</div>
</>
);
};

View file

@ -1,58 +0,0 @@
import { t } from "i18next";
import { Button } from "react-bootstrap";
import { FIX_STATE } from ".";
export default function FixCreationTimeFooter({
fixState,
startFix,
...props
}) {
return (
fixState !== FIX_STATE.RUNNING && (
<div
style={{
width: "100%",
display: "flex",
marginTop: "30px",
justifyContent: "space-around",
}}
>
{(fixState === FIX_STATE.NOT_STARTED ||
fixState === FIX_STATE.COMPLETED_WITH_ERRORS) && (
<Button
block
variant={"outline-secondary"}
onClick={() => {
props.hide();
}}
>
{t("CANCEL")}
</Button>
)}
{fixState === FIX_STATE.COMPLETED && (
<Button
block
variant={"outline-secondary"}
onClick={props.hide}
>
{t("CLOSE")}
</Button>
)}
{(fixState === FIX_STATE.NOT_STARTED ||
fixState === FIX_STATE.COMPLETED_WITH_ERRORS) && (
<>
<div style={{ width: "30px" }} />
<Button
block
variant={"outline-success"}
onClick={startFix}
>
{t("FIX_CREATION_TIME")}
</Button>
</>
)}
</div>
)
);
}

View file

@ -1,154 +0,0 @@
import DialogBox from "@ente/shared/components/DialogBox/";
import { Formik } from "formik";
import { GalleryContext } from "pages/gallery";
import { useContext, useEffect, useState } from "react";
import { updateCreationTimeWithExif } from "services/updateCreationTimeWithExif";
import { EnteFile } from "types/file";
import FixCreationTimeFooter from "./footer";
import FixCreationTimeRunning from "./running";
import { t } from "i18next";
import FixCreationTimeOptions from "./options";
export interface FixCreationTimeAttributes {
files: EnteFile[];
}
interface Props {
isOpen: boolean;
show: () => void;
hide: () => void;
attributes: FixCreationTimeAttributes;
}
export enum FIX_STATE {
NOT_STARTED,
RUNNING,
COMPLETED,
COMPLETED_WITH_ERRORS,
}
export enum FIX_OPTIONS {
DATE_TIME_ORIGINAL,
DATE_TIME_DIGITIZED,
METADATA_DATE,
CUSTOM_TIME,
}
interface formValues {
option: FIX_OPTIONS;
customTime: Date;
}
function Message({ fixState }: { fixState: FIX_STATE }) {
let message = null;
switch (fixState) {
case FIX_STATE.NOT_STARTED:
message = t("UPDATE_CREATION_TIME_NOT_STARTED");
break;
case FIX_STATE.COMPLETED:
message = t("UPDATE_CREATION_TIME_COMPLETED");
break;
case FIX_STATE.COMPLETED_WITH_ERRORS:
message = t("UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR");
break;
}
return message ? <div>{message}</div> : <></>;
}
export default function FixCreationTime(props: Props) {
const [fixState, setFixState] = useState(FIX_STATE.NOT_STARTED);
const [progressTracker, setProgressTracker] = useState({
current: 0,
total: 0,
});
const galleryContext = useContext(GalleryContext);
useEffect(() => {
if (
props.attributes &&
props.isOpen &&
fixState !== FIX_STATE.RUNNING
) {
setFixState(FIX_STATE.NOT_STARTED);
}
}, [props.isOpen]);
const startFix = async (option: FIX_OPTIONS, customTime: Date) => {
setFixState(FIX_STATE.RUNNING);
const completedWithoutError = await updateCreationTimeWithExif(
props.attributes.files,
option,
customTime,
setProgressTracker,
);
if (!completedWithoutError) {
setFixState(FIX_STATE.COMPLETED);
} else {
setFixState(FIX_STATE.COMPLETED_WITH_ERRORS);
}
await galleryContext.syncWithRemote();
};
if (!props.attributes) {
return <></>;
}
const onSubmit = (values: formValues) => {
startFix(Number(values.option), new Date(values.customTime));
};
return (
<DialogBox
open={props.isOpen}
onClose={props.hide}
attributes={{
title:
fixState === FIX_STATE.RUNNING
? t("FIX_CREATION_TIME_IN_PROGRESS")
: t("FIX_CREATION_TIME"),
nonClosable: true,
}}
>
<div
style={{
marginBottom: "10px",
display: "flex",
flexDirection: "column",
...(fixState === FIX_STATE.RUNNING
? { alignItems: "center" }
: {}),
}}
>
<Message fixState={fixState} />
{fixState === FIX_STATE.RUNNING && (
<FixCreationTimeRunning progressTracker={progressTracker} />
)}
<Formik<formValues>
initialValues={{
option: FIX_OPTIONS.DATE_TIME_ORIGINAL,
customTime: new Date(),
}}
validateOnBlur={false}
onSubmit={onSubmit}
>
{({ values, handleChange, handleSubmit }) => (
<>
{(fixState === FIX_STATE.NOT_STARTED ||
fixState ===
FIX_STATE.COMPLETED_WITH_ERRORS) && (
<div style={{ marginTop: "10px" }}>
<FixCreationTimeOptions
handleChange={handleChange}
values={values}
/>
</div>
)}
<FixCreationTimeFooter
fixState={fixState}
startFix={handleSubmit}
hide={props.hide}
/>
</>
)}
</Formik>
</div>
</DialogBox>
);
}

View file

@ -1,90 +0,0 @@
import { Row, Value } from "@ente/shared/components/Container";
import EnteDateTimePicker from "components/EnteDateTimePicker";
import { t } from "i18next";
import { ChangeEvent } from "react";
import { Form } from "react-bootstrap";
import { FIX_OPTIONS } from ".";
const Option = ({
value,
selected,
onChange,
label,
}: {
value: FIX_OPTIONS;
selected: FIX_OPTIONS;
onChange: (e: string | ChangeEvent<any>) => void;
label: string;
}) => (
<Form.Check
name="group1"
style={{
margin: "5px 0",
color: value !== Number(selected) ? "#aaa" : "#fff",
}}
>
<Form.Check.Input
id={value.toString()}
type="radio"
value={value}
checked={value === Number(selected)}
onChange={onChange}
/>
<Form.Check.Label
style={{ cursor: "pointer" }}
htmlFor={value.toString()}
>
{label}
</Form.Check.Label>
</Form.Check>
);
export default function FixCreationTimeOptions({ handleChange, values }) {
return (
<Form noValidate>
<Row style={{ margin: "0" }}>
<Option
value={FIX_OPTIONS.DATE_TIME_ORIGINAL}
onChange={handleChange("option")}
label={t("DATE_TIME_ORIGINAL")}
selected={Number(values.option)}
/>
</Row>
<Row style={{ margin: "0" }}>
<Option
value={FIX_OPTIONS.DATE_TIME_DIGITIZED}
onChange={handleChange("option")}
label={t("DATE_TIME_DIGITIZED")}
selected={Number(values.option)}
/>
</Row>
<Row style={{ margin: "0" }}>
<Option
value={FIX_OPTIONS.METADATA_DATE}
onChange={handleChange("option")}
label={t("METADATA_DATE")}
selected={Number(values.option)}
/>
</Row>
<Row style={{ margin: "0" }}>
<Value width="50%">
<Option
value={FIX_OPTIONS.CUSTOM_TIME}
onChange={handleChange("option")}
label={t("CUSTOM_TIME")}
selected={Number(values.option)}
/>
</Value>
{Number(values.option) === FIX_OPTIONS.CUSTOM_TIME && (
<Value width="40%">
<EnteDateTimePicker
onSubmit={(x: Date) =>
handleChange("customTime")(x.toUTCString())
}
/>
</Value>
)}
</Row>
</Form>
);
}

View file

@ -1,35 +0,0 @@
import { ComfySpan } from "components/ExportInProgress";
import { t } from "i18next";
import { ProgressBar } from "react-bootstrap";
export default function FixCreationTimeRunning({ progressTracker }) {
return (
<>
<div style={{ marginBottom: "10px" }}>
<ComfySpan>
{" "}
{progressTracker.current} / {progressTracker.total}{" "}
</ComfySpan>{" "}
<span style={{ marginLeft: "10px" }}>
{" "}
{t("CREATION_TIME_UPDATED")}
</span>
</div>
<div
style={{
width: "100%",
marginTop: "10px",
marginBottom: "20px",
}}
>
<ProgressBar
now={Math.round(
(progressTracker.current * 100) / progressTracker.total,
)}
animated={true}
variant="upload-progress-bar"
/>
</div>
</>
);
}

View file

@ -1,235 +0,0 @@
import DialogBox from "@ente/shared/components/DialogBox/";
import { logError } from "@ente/shared/sentry";
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
import { t } from "i18next";
import React, { useEffect, useState } from "react";
import { Button, ProgressBar } from "react-bootstrap";
import {
getLargeThumbnailFiles,
replaceThumbnail,
} from "services/migrateThumbnailService";
import { ComfySpan } from "./ExportInProgress";
export type SetProgressTracker = React.Dispatch<
React.SetStateAction<{
current: number;
total: number;
}>
>;
interface Props {
isOpen: boolean;
show: () => void;
hide: () => void;
}
export enum FIX_STATE {
NOT_STARTED,
FIX_LATER,
NOOP,
RUNNING,
COMPLETED,
COMPLETED_WITH_ERRORS,
}
function Message({ fixState }: { fixState: FIX_STATE }) {
let message = null;
switch (fixState) {
case FIX_STATE.NOT_STARTED:
case FIX_STATE.FIX_LATER:
message = t("REPLACE_THUMBNAIL_NOT_STARTED");
break;
case FIX_STATE.COMPLETED:
message = t("REPLACE_THUMBNAIL_COMPLETED");
break;
case FIX_STATE.NOOP:
message = t("REPLACE_THUMBNAIL_NOOP");
break;
case FIX_STATE.COMPLETED_WITH_ERRORS:
message = t("REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR");
break;
}
return message ? (
<div style={{ marginBottom: "30px" }}>{message}</div>
) : (
<></>
);
}
export default function FixLargeThumbnails(props: Props) {
const [fixState, setFixState] = useState(FIX_STATE.NOT_STARTED);
const [progressTracker, setProgressTracker] = useState({
current: 0,
total: 0,
});
const [largeThumbnailFiles, setLargeThumbnailFiles] = useState<number[]>(
[],
);
const init = (): FIX_STATE => {
let fixState = getData(LS_KEYS.THUMBNAIL_FIX_STATE)?.state;
if (!fixState || fixState === FIX_STATE.RUNNING) {
fixState = FIX_STATE.NOT_STARTED;
updateFixState(fixState);
}
if (fixState === FIX_STATE.COMPLETED) {
fixState = FIX_STATE.NOOP;
updateFixState(fixState);
}
setFixState(fixState);
return fixState;
};
const fetchLargeThumbnail = async () => {
const largeThumbnailFiles = (await getLargeThumbnailFiles()) ?? [];
setLargeThumbnailFiles(largeThumbnailFiles);
return largeThumbnailFiles;
};
const main = async () => {
const largeThumbnailFiles = await fetchLargeThumbnail();
if (
fixState === FIX_STATE.NOT_STARTED &&
largeThumbnailFiles.length > 0
) {
props.show();
}
if (
(fixState === FIX_STATE.COMPLETED || fixState === FIX_STATE.NOOP) &&
largeThumbnailFiles.length > 0
) {
updateFixState(FIX_STATE.NOT_STARTED);
logError(Error(), "large thumbnail files left after migration");
}
if (largeThumbnailFiles.length === 0 && fixState !== FIX_STATE.NOOP) {
updateFixState(FIX_STATE.NOOP);
}
};
useEffect(() => {
if (props.isOpen && fixState !== FIX_STATE.RUNNING) {
main();
}
}, [props.isOpen]);
useEffect(() => {
const fixState = init();
if (fixState === FIX_STATE.NOT_STARTED) {
main();
}
}, []);
const startFix = async (newlyFetchedLargeThumbnailFiles?: number[]) => {
updateFixState(FIX_STATE.RUNNING);
const completedWithError = await replaceThumbnail(
setProgressTracker,
new Set(
newlyFetchedLargeThumbnailFiles ?? largeThumbnailFiles ?? [],
),
);
if (typeof completedWithError !== "undefined") {
updateFixState(
completedWithError
? FIX_STATE.COMPLETED_WITH_ERRORS
: FIX_STATE.COMPLETED,
);
}
await fetchLargeThumbnail();
};
const updateFixState = (fixState: FIX_STATE) => {
setFixState(fixState);
setData(LS_KEYS.THUMBNAIL_FIX_STATE, { state: fixState });
};
return (
<DialogBox
open={props.isOpen}
onClose={props.hide}
attributes={{
title: t("COMPRESS_THUMBNAILS"),
}}
>
<div
style={{
marginBottom: "20px",
padding: "0 5%",
display: "flex",
alignItems: "center",
flexDirection: "column",
}}
>
<Message fixState={fixState} />
{fixState === FIX_STATE.RUNNING && (
<>
<div style={{ marginBottom: "10px" }}>
<ComfySpan>
{" "}
{progressTracker.current} /{" "}
{progressTracker.total}{" "}
</ComfySpan>{" "}
<span style={{ marginLeft: "10px" }}>
{" "}
{t("THUMBNAIL_REPLACED")}
</span>
</div>
<div
style={{
width: "100%",
marginTop: "10px",
marginBottom: "20px",
}}
>
<ProgressBar
now={Math.round(
(progressTracker.current * 100) /
progressTracker.total,
)}
animated={true}
variant="upload-progress-bar"
/>
</div>
</>
)}
<div
style={{
width: "100%",
display: "flex",
justifyContent: "space-around",
}}
>
{fixState === FIX_STATE.NOT_STARTED ||
fixState === FIX_STATE.FIX_LATER ? (
<Button
block
variant={"outline-secondary"}
onClick={() => {
updateFixState(FIX_STATE.FIX_LATER);
props.hide();
}}
>
{t("FIX_THUMBNAIL_LATER")}
</Button>
) : (
<Button
block
variant={"outline-secondary"}
onClick={props.hide}
>
{t("CLOSE")}
</Button>
)}
{(fixState === FIX_STATE.NOT_STARTED ||
fixState === FIX_STATE.FIX_LATER ||
fixState === FIX_STATE.COMPLETED_WITH_ERRORS) && (
<>
<div style={{ width: "30px" }} />
<Button
block
variant={"outline-success"}
onClick={() => startFix()}
>
{t("FIX_THUMBNAIL")}
</Button>
</>
)}
</div>
</div>
</DialogBox>
);
}

View file

@ -1,228 +0,0 @@
import { addLogLine } from "@ente/shared/logging";
import "@tensorflow/tfjs-backend-cpu";
import "@tensorflow/tfjs-backend-webgl";
import { DEFAULT_ML_SYNC_CONFIG } from "constants/mlConfig";
import { useEffect, useRef, useState } from "react";
import arcfaceAlignmentService from "services/machineLearning/arcfaceAlignmentService";
import arcfaceCropService from "services/machineLearning/arcfaceCropService";
import blazeFaceDetectionService from "services/machineLearning/blazeFaceDetectionService";
import imageSceneService from "services/machineLearning/imageSceneService";
import ssdMobileNetV2Service from "services/machineLearning/ssdMobileNetV2Service";
import { AlignedFace, FaceCrop, ObjectDetection } from "types/machineLearning";
import { getMLSyncConfig } from "utils/machineLearning/config";
import {
getAlignedFaceBox,
ibExtractFaceImage,
ibExtractFaceImageUsingTransform,
} from "utils/machineLearning/faceAlign";
import { ibExtractFaceImageFromCrop } from "utils/machineLearning/faceCrop";
import { FaceCropsRow, FaceImagesRow, ImageBitmapView } from "./ImageViews";
interface MLFileDebugViewProps {
file: File;
}
function drawFaceDetection(face: AlignedFace, ctx: CanvasRenderingContext2D) {
const pointSize = Math.ceil(
Math.max(ctx.canvas.width / 512, face.detection.box.width / 32),
);
ctx.save();
ctx.strokeStyle = "rgba(255, 0, 0, 0.8)";
ctx.lineWidth = pointSize;
ctx.strokeRect(
face.detection.box.x,
face.detection.box.y,
face.detection.box.width,
face.detection.box.height,
);
ctx.restore();
ctx.save();
ctx.strokeStyle = "rgba(0, 255, 0, 0.8)";
ctx.lineWidth = Math.round(pointSize * 1.5);
const alignedBox = getAlignedFaceBox(face.alignment);
ctx.strokeRect(
alignedBox.x,
alignedBox.y,
alignedBox.width,
alignedBox.height,
);
ctx.restore();
ctx.save();
ctx.fillStyle = "rgba(0, 0, 255, 0.8)";
face.detection.landmarks.forEach((l) => {
ctx.beginPath();
ctx.arc(l.x, l.y, pointSize, 0, Math.PI * 2, true);
ctx.fill();
});
ctx.restore();
}
function drawBbox(object: ObjectDetection, ctx: CanvasRenderingContext2D) {
ctx.font = "100px Arial";
ctx.save();
ctx.restore();
ctx.rect(...object.bbox);
ctx.lineWidth = 10;
ctx.strokeStyle = "green";
ctx.fillStyle = "green";
ctx.stroke();
ctx.fillText(
object.score.toFixed(3) + " " + object.class,
object.bbox[0],
object.bbox[1] > 10 ? object.bbox[1] - 5 : 10,
);
}
export default function MLFileDebugView(props: MLFileDebugViewProps) {
// const [imageBitmap, setImageBitmap] = useState<ImageBitmap>();
const [faceCrops, setFaceCrops] = useState<FaceCrop[]>();
const [facesUsingCrops, setFacesUsingCrops] = useState<ImageBitmap[]>();
const [facesUsingImage, setFacesUsingImage] = useState<ImageBitmap[]>();
const [facesUsingTransform, setFacesUsingTransform] =
useState<ImageBitmap[]>();
const canvasRef = useRef(null);
useEffect(() => {
let didCancel = false;
const loadFile = async () => {
// TODO: go through worker for these apis, to not include ml code in main bundle
const imageBitmap = await createImageBitmap(props.file);
const faceDetections =
await blazeFaceDetectionService.detectFaces(imageBitmap);
addLogLine("detectedFaces: ", faceDetections.length);
const objectDetections = await ssdMobileNetV2Service.detectObjects(
imageBitmap,
DEFAULT_ML_SYNC_CONFIG.objectDetection.maxNumBoxes,
DEFAULT_ML_SYNC_CONFIG.objectDetection.minScore,
);
addLogLine("detectedObjects: ", JSON.stringify(objectDetections));
const sceneDetections = await imageSceneService.detectScenes(
imageBitmap,
DEFAULT_ML_SYNC_CONFIG.sceneDetection.minScore,
);
addLogLine("detectedScenes: ", JSON.stringify(sceneDetections));
const mlSyncConfig = await getMLSyncConfig();
const faceCropPromises = faceDetections.map(async (faceDetection) =>
arcfaceCropService.getFaceCrop(
imageBitmap,
faceDetection,
mlSyncConfig.faceCrop,
),
);
const faceCrops = await Promise.all(faceCropPromises);
if (didCancel) return;
setFaceCrops(faceCrops);
const faceAlignments = faceDetections.map((detection) =>
arcfaceAlignmentService.getFaceAlignment(detection),
);
addLogLine("alignedFaces: ", JSON.stringify(faceAlignments));
const canvas: HTMLCanvasElement = canvasRef.current;
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const ctx = canvas.getContext("2d");
if (didCancel) return;
ctx.drawImage(imageBitmap, 0, 0);
const alignedFaces = faceAlignments.map((alignment, i) => {
return {
detection: faceDetections[i],
alignment,
} as AlignedFace;
});
alignedFaces.forEach((alignedFace) =>
drawFaceDetection(alignedFace, ctx),
);
objectDetections.forEach((object) => drawBbox(object, ctx));
const facesUsingCrops = await Promise.all(
alignedFaces.map((face, i) => {
return ibExtractFaceImageFromCrop(
faceCrops[i],
face.alignment,
112,
);
}),
);
const facesUsingImage = await Promise.all(
alignedFaces.map((face) => {
return ibExtractFaceImage(imageBitmap, face.alignment, 112);
}),
);
const facesUsingTransform = await Promise.all(
alignedFaces.map((face) => {
return ibExtractFaceImageUsingTransform(
imageBitmap,
face.alignment,
112,
);
}),
);
if (didCancel) return;
setFacesUsingCrops(facesUsingCrops);
setFacesUsingImage(facesUsingImage);
setFacesUsingTransform(facesUsingTransform);
};
props.file && loadFile();
return () => {
didCancel = true;
};
}, [props.file]);
return (
<div>
<p></p>
{/* <ImageBitmapView image={imageBitmap}></ImageBitmapView> */}
<canvas
ref={canvasRef}
width={0}
height={0}
style={{ maxWidth: "100%" }}
/>
<p></p>
<div>Face Crops:</div>
<FaceCropsRow>
{faceCrops?.map((faceCrop, i) => (
<ImageBitmapView
key={i}
image={faceCrop.image}
></ImageBitmapView>
))}
</FaceCropsRow>
<p></p>
<div>Face Images using face crops:</div>
<FaceImagesRow>
{facesUsingCrops?.map((image, i) => (
<ImageBitmapView key={i} image={image}></ImageBitmapView>
))}
</FaceImagesRow>
<div>Face Images using original image:</div>
<FaceImagesRow>
{facesUsingImage?.map((image, i) => (
<ImageBitmapView key={i} image={image}></ImageBitmapView>
))}
</FaceImagesRow>
<div>Face Images using transfrom:</div>
<FaceImagesRow>
{facesUsingTransform?.map((image, i) => (
<ImageBitmapView key={i} image={image}></ImageBitmapView>
))}
</FaceImagesRow>
</div>
);
}

View file

@ -1,60 +0,0 @@
import { getToken, getUserID } from "@ente/shared/storage/localStorage/helpers";
import { useState } from "react";
import { Button, Spinner } from "react-bootstrap";
import { EnteFile } from "types/file";
import mlService from "../../services/machineLearning/machineLearningService";
function MLServiceFileInfoButton({
file,
updateMLDataIndex,
setUpdateMLDataIndex,
}: {
file: EnteFile;
updateMLDataIndex: number;
setUpdateMLDataIndex: (num: number) => void;
}) {
const [mlServiceRunning, setMlServiceRunning] = useState(false);
const runMLService = async () => {
setMlServiceRunning(true);
const token = getToken();
const userID = getUserID();
// index 4 is for timeout of 240 seconds
await mlService.syncLocalFile(token, userID, file as EnteFile, null, 4);
setUpdateMLDataIndex(updateMLDataIndex + 1);
setMlServiceRunning(false);
};
return (
<div
style={{
marginTop: "18px",
}}
>
<Button
onClick={runMLService}
disabled={mlServiceRunning}
variant={mlServiceRunning ? "secondary" : "primary"}
>
{!mlServiceRunning ? (
"Run ML Service"
) : (
<>
ML Service Running{" "}
<Spinner
animation="border"
size="sm"
style={{
marginLeft: "5px",
}}
/>
</>
)}
</Button>
</div>
);
}
export default MLServiceFileInfoButton;

View file

@ -10,11 +10,24 @@ 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 { ObjectLabelList } from "components/MachineLearning/ObjectList";
import {
PhotoPeopleList,
UnidentifiedFaces,
} from "components/MachineLearning/PeopleList";
import Titlebar from "components/Titlebar";
import LinkButton from "components/pages/gallery/LinkButton";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { GalleryContext } from "pages/gallery";
import { useContext, useEffect, useMemo, useState } from "react";
import { getEXIFLocation } from "services/upload/exifService";
import { EnteFile } from "types/file";
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
import {
getMapDisableConfirmationDialog,
getMapEnableConfirmationDialog,
} from "utils/ui";
import { ExifData } from "./ExifData";
import InfoItem from "./InfoItem";
import MapBox from "./MapBox";
@ -22,23 +35,6 @@ import { RenderCaption } from "./RenderCaption";
import { RenderCreationTime } from "./RenderCreationTime";
import { RenderFileName } from "./RenderFileName";
import {
PhotoPeopleList,
UnidentifiedFaces,
} from "components/MachineLearning/PeopleList";
import { ObjectLabelList } from "components/MachineLearning/ObjectList";
// import MLServiceFileInfoButton from 'components/MachineLearning/MLServiceFileInfoButton';
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { GalleryContext } from "pages/gallery";
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
import {
getMapDisableConfirmationDialog,
getMapEnableConfirmationDialog,
} from "utils/ui";
export const FileInfoSidebar = styled((props: DialogProps) => (
<EnteDrawer {...props} anchor="right" />
))({
@ -352,14 +348,6 @@ export function FileInfo({
file={file}
updateMLDataIndex={updateMLDataIndex}
/>
{/* <Box pt={1}>
<MLServiceFileInfoButton
file={file}
updateMLDataIndex={updateMLDataIndex}
setUpdateMLDataIndex={setUpdateMLDataIndex}
/>
</Box> */}
</>
)}
</Stack>

View file

@ -1,15 +1,13 @@
import { t } from "i18next";
import { useContext, useState } from "react";
// import FixLargeThumbnails from 'components/FixLargeThumbnail';
import RecoveryKey from "@ente/shared/components/RecoveryKey";
import {
ACCOUNTS_PAGES,
PHOTOS_PAGES as PAGES,
} from "@ente/shared/constants/pages";
import TwoFactorModal from "components/TwoFactor/Modal";
import { t } from "i18next";
import { useRouter } from "next/router";
import { AppContext } from "pages/_app";
import { useContext, useState } from "react";
// import mlIDbStorage from 'utils/storage/mlIDbStorage';
import {
configurePasskeyRecovery,

View file

@ -1,12 +0,0 @@
// import dynamic from 'next/dynamic';
// const MLDebugWithNoSSR = dynamic(
// () => import('components/MachineLearning/MlDebug-disabled'),
// {
// ssr: false,
// }
// );
export default function MLDebug() {
return <div>{/* <MLDebugWithNoSSR></MLDebugWithNoSSR> */}</div>;
}

View file

@ -1,20 +0,0 @@
import { styled } from "@mui/material";
import { Button } from "react-bootstrap";
const Container = styled("div")`
position: fixed;
bottom: 7%;
right: 2%;
align-self: flex-end;
`;
interface Iprops {
onClick: () => void;
}
export default function ReportAbuse(props: Iprops) {
return (
<Container>
<Button onClick={props.onClick}>report abuse?</Button>
</Container>
);
}

View file

@ -1,4 +0,0 @@
export enum REPORT_REASON {
COPYRIGHT = "COPYRIGHT",
MALICIOUS_CONTENT = "MALICIOUS_CONTENT",
}

View file

@ -1,15 +1,9 @@
import AppNavbar from "@ente/shared/components/Navbar/app";
import { t } from "i18next";
import { createContext, useEffect, useRef, useState } from "react";
import { setupI18n } from "@/ui/i18n";
import { CacheProvider } from "@emotion/react";
import {
APP_TITLES,
APPS,
APP_TITLES,
CLIENT_PACKAGE_NAMES,
} from "@ente/shared/apps/constants";
import { EnteAppProps } from "@ente/shared/apps/types";
import { Overlay } from "@ente/shared/components/Container";
import DialogBox from "@ente/shared/components/DialogBox";
import {
@ -23,11 +17,12 @@ import {
} from "@ente/shared/components/DialogBoxV2/types";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { MessageContainer } from "@ente/shared/components/MessageContainer";
import AppNavbar from "@ente/shared/components/Navbar/app";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import ElectronAPIs from "@ente/shared/electron";
import { AppUpdateInfo } from "@ente/shared/electron/types";
import { CustomError } from "@ente/shared/error";
import { eventBus, Events } from "@ente/shared/events";
import { Events, eventBus } from "@ente/shared/events";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
import { addLogLine } from "@ente/shared/logging";
import {
@ -36,7 +31,7 @@ import {
} from "@ente/shared/logging/web";
import HTTPService from "@ente/shared/network/HTTPService";
import { logError } from "@ente/shared/sentry";
import { getData, LS_KEYS } from "@ente/shared/storage/localStorage";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import {
getLocalMapEnabled,
getToken,
@ -44,7 +39,6 @@ import {
} from "@ente/shared/storage/localStorage/helpers";
import { getTheme } from "@ente/shared/themes";
import { THEME_COLOR } from "@ente/shared/themes/constants";
import createEmotionCache from "@ente/shared/themes/createEmotionCache";
import { SetTheme } from "@ente/shared/themes/types";
import ArrowForward from "@mui/icons-material/ArrowForward";
import { CssBaseline, useMediaQuery } from "@mui/material";
@ -52,10 +46,13 @@ import { ThemeProvider } from "@mui/material/styles";
import "bootstrap/dist/css/bootstrap.min.css";
import Notification from "components/Notification";
import { REDIRECTS } from "constants/redirects";
import { t } from "i18next";
import isElectron from "is-electron";
import { AppProps } from "next/app";
import Head from "next/head";
import { useRouter } from "next/router";
import "photoswipe/dist/photoswipe.css";
import { createContext, useEffect, useRef, useState } from "react";
import LoadingBar from "react-top-loading-bar";
import DownloadManager from "services/download";
import exportService from "services/export";
@ -115,15 +112,8 @@ type AppContextType = {
export const AppContext = createContext<AppContextType>(null);
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
export default function App(props: EnteAppProps) {
const {
Component,
emotionCache = clientSideEmotionCache,
pageProps,
} = props;
export default function App(props: AppProps) {
const { Component, pageProps } = props;
const router = useRouter();
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
const [loading, setLoading] = useState(false);
@ -390,7 +380,7 @@ export default function App(props: EnteAppProps) {
});
return (
<CacheProvider value={emotionCache}>
<>
<Head>
<title>
{isI18nReady
@ -491,6 +481,6 @@ export default function App(props: EnteAppProps) {
)}
</AppContext.Provider>
</ThemeProvider>
</CacheProvider>
</>
);
}

View file

@ -1,7 +1,3 @@
import DocumentPage, {
EnteDocumentProps,
} from "@ente/shared/next/pages/_document";
import DocumentPage from "@ente/shared/next/pages/_document";
export default function Document(props: EnteDocumentProps) {
return <DocumentPage {...props} />;
}
export default DocumentPage;

View file

@ -1,26 +1,26 @@
import Login from "@ente/accounts/components/Login";
import SignUp from "@ente/accounts/components/SignUp";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { getData, LS_KEYS } from "@ente/shared/storage/localStorage";
import { Button, styled, Typography, TypographyProps } from "@mui/material";
import { t } from "i18next";
import { useRouter } from "next/router";
import { useContext, useEffect, useState } from "react";
import Carousel from "react-bootstrap/Carousel";
import { AppContext } from "./_app";
import { APPS } from "@ente/shared/apps/constants";
import { EnteLogo } from "@ente/shared/components/EnteLogo";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { saveKeyInSessionStore } from "@ente/shared/crypto/helpers";
import ElectronAPIs from "@ente/shared/electron";
import { getAlbumsURL } from "@ente/shared/network/api";
import { logError } from "@ente/shared/sentry";
import localForage from "@ente/shared/storage/localForage";
import { getData, LS_KEYS } from "@ente/shared/storage/localStorage";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
import { getKey, SESSION_KEYS } from "@ente/shared/storage/sessionStorage";
import { Button, styled, Typography, TypographyProps } from "@mui/material";
import { t } from "i18next";
import isElectron from "is-electron";
import { useRouter } from "next/router";
import { CarouselProvider, DotGroup, Slide, Slider } from "pure-react-carousel";
import "pure-react-carousel/dist/react-carousel.es.css";
import { useContext, useEffect, useState } from "react";
import { Trans } from "react-i18next";
import { AppContext } from "./_app";
const Container = styled("div")`
display: flex;
@ -186,47 +186,7 @@ export default function LandingPage() {
<>
<SlideContainer>
<EnteLogo height={24} sx={{ mb: 8 }} />
<Carousel controls={false}>
<Carousel.Item>
<Img
src="/images/onboarding-lock/1x.png"
srcSet="/images/onboarding-lock/2x.png 2x,
/images/onboarding-lock/3x.png 3x"
/>
<FeatureText>
<Trans i18nKey={"HERO_SLIDE_1_TITLE"} />
</FeatureText>
<TextContainer>
{t("HERO_SLIDE_1")}
</TextContainer>
</Carousel.Item>
<Carousel.Item>
<Img
src="/images/onboarding-safe/1x.png"
srcSet="/images/onboarding-safe/2x.png 2x,
/images/onboarding-safe/3x.png 3x"
/>
<FeatureText>
<Trans i18nKey={"HERO_SLIDE_2_TITLE"} />
</FeatureText>
<TextContainer>
{t("HERO_SLIDE_2")}
</TextContainer>
</Carousel.Item>
<Carousel.Item>
<Img
src="/images/onboarding-sync/1x.png"
srcSet="/images/onboarding-sync/2x.png 2x,
/images/onboarding-sync/3x.png 3x"
/>
<FeatureText>
<Trans i18nKey={"HERO_SLIDE_3_TITLE"} />
</FeatureText>
<TextContainer>
{t("HERO_SLIDE_3")}
</TextContainer>
</Carousel.Item>
</Carousel>
<Slideshow />
</SlideContainer>
<MobileBox>
<Button
@ -258,3 +218,83 @@ export default function LandingPage() {
</Container>
);
}
const Slideshow: React.FC = () => {
return (
<CarouselProvider
naturalSlideWidth={400}
naturalSlideHeight={300}
isIntrinsicHeight={true}
totalSlides={3}
isPlaying={true}
>
<Slider>
<Slide index={0}>
<Img
src="/images/onboarding-lock/1x.png"
srcSet="/images/onboarding-lock/2x.png 2x,
/images/onboarding-lock/3x.png 3x"
/>
<FeatureText>
<Trans i18nKey={"HERO_SLIDE_1_TITLE"} />
</FeatureText>
<TextContainer>{t("HERO_SLIDE_1")}</TextContainer>
</Slide>
<Slide index={1}>
<SlideContents>
<Img
src="/images/onboarding-safe/1x.png"
srcSet="/images/onboarding-safe/2x.png 2x,
/images/onboarding-safe/3x.png 3x"
/>
<FeatureText>
<Trans i18nKey={"HERO_SLIDE_2_TITLE"} />
</FeatureText>
<TextContainer>{t("HERO_SLIDE_2")}</TextContainer>
</SlideContents>
</Slide>
<Slide index={2}>
<SlideContents>
<Img
src="/images/onboarding-sync/1x.png"
srcSet="/images/onboarding-sync/2x.png 2x,
/images/onboarding-sync/3x.png 3x"
/>
<FeatureText>
<Trans i18nKey={"HERO_SLIDE_3_TITLE"} />
</FeatureText>
<TextContainer>{t("HERO_SLIDE_3")}</TextContainer>
</SlideContents>
</Slide>
</Slider>
<CustomDotGroup />
</CarouselProvider>
);
};
const CustomDotGroup = styled(DotGroup)`
margin-block-start: 2px;
margin-block-end: 24px;
button {
margin-inline-end: 14px;
width: 10px;
height: 10px;
border-radius: 50%;
padding: 0;
border: 0;
background-color: #fff;
opacity: 0.5;
transition: opacity 0.6s ease;
}
button.carousel__dot--selected {
background-color: #51cd7c;
opacity: 1;
}
`;
const SlideContents = styled("div")`
display: flex;
flex-direction: column;
`;

View file

@ -1,63 +1,34 @@
import { getLocalFiles } from "services/fileService";
import { EnteFile } from "types/file";
import { APPS } from "@ente/shared/apps/constants";
import { CustomError, parseUploadErrorCodes } from "@ente/shared/error";
import { addLogLine } from "@ente/shared/logging";
import { logError } from "@ente/shared/sentry";
import "@tensorflow/tfjs-backend-cpu";
import "@tensorflow/tfjs-backend-webgl";
import * as tf from "@tensorflow/tfjs-core";
// import '@tensorflow/tfjs-backend-wasm';
// import { setWasmPaths } from '@tensorflow/tfjs-backend-wasm';
// import '@tensorflow/tfjs-backend-cpu';
import { MAX_ML_SYNC_ERROR_COUNT } from "constants/mlConfig";
import downloadManager from "services/download";
import { getLocalFiles } from "services/fileService";
import { EnteFile } from "types/file";
import {
MlFileData,
MLSyncContext,
MLSyncFileContext,
MLSyncResult,
MlFileData,
} from "types/machineLearning";
// import { toTSNE } from 'utils/machineLearning/visualization';
// import {
// incrementIndexVersion,
// mlFilesStore
// } from 'utils/storage/mlStorage';
// import { getAllFacesFromMap } from 'utils/machineLearning';
import { CustomError, parseUploadErrorCodes } from "@ente/shared/error";
import { MAX_ML_SYNC_ERROR_COUNT } from "constants/mlConfig";
import { getMLSyncConfig } from "utils/machineLearning/config";
import mlIDbStorage from "utils/storage/mlIDbStorage";
import FaceService from "./faceService";
import { MLFactory } from "./machineLearningFactory";
import ObjectService from "./objectService";
import PeopleService from "./peopleService";
// import TextService from './textService';
import { APPS } from "@ente/shared/apps/constants";
import { addLogLine } from "@ente/shared/logging";
import { logError } from "@ente/shared/sentry";
import downloadManager from "services/download";
import ReaderService from "./readerService";
class MachineLearningService {
private initialized = false;
// private faceDetectionService: FaceDetectionService;
// private faceLandmarkService: FAPIFaceLandmarksService;
// private faceAlignmentService: FaceAlignmentService;
// private faceEmbeddingService: FaceEmbeddingService;
// private faceEmbeddingService: FAPIFaceEmbeddingService;
// private clusteringService: ClusteringService;
private localSyncContext: Promise<MLSyncContext>;
private syncContext: Promise<MLSyncContext>;
public constructor() {
// setWasmPaths('/js/tfjs/');
// this.faceDetectionService = new TFJSFaceDetectionService();
// this.faceLandmarkService = new FAPIFaceLandmarksService();
// this.faceAlignmentService = new ArcfaceAlignmentService();
// this.faceEmbeddingService = new TFJSFaceEmbeddingService();
// this.faceEmbeddingService = new FAPIFaceEmbeddingService();
// this.clusteringService = new ClusteringService();
}
public async sync(token: string, userID: number): Promise<MLSyncResult> {
if (!token) {
throw Error("Token needed by ml service to sync file");
@ -198,30 +169,6 @@ class MachineLearningService {
addLogLine("syncLocalFiles", Date.now() - startTime, "ms");
}
// TODO: not required if ml data is stored as field inside ente file object
// remove, not required now
// it removes ml data for files in trash, they will be resynced if restored
// private async syncRemovedFiles(syncContext: MLSyncContext) {
// const db = await mlIDbStorage.db;
// const localFileIdMap = await this.getLocalFilesMap(syncContext);
// const removedFileIds: Array<string> = [];
// await mlFilesStore.iterate((file, idStr) => {
// if (!localFileIdMap.has(parseInt(idStr))) {
// removedFileIds.push(idStr);
// }
// });
// if (removedFileIds.length < 1) {
// return;
// }
// removedFileIds.forEach((fileId) => mlFilesStore.removeItem(fileId));
// addLogLine('Removed local file ids: ', removedFileIds);
// await incrementIndexVersion('files');
// }
private async getOutOfSyncFiles(syncContext: MLSyncContext) {
const startTime = Date.now();
const fileIds = await mlIDbStorage.getFileIds(
@ -239,50 +186,6 @@ class MachineLearningService {
addLogLine("getOutOfSyncFiles", Date.now() - startTime, "ms");
}
// TODO: optimize, use indexdb indexes, move facecrops to cache to reduce io
// remove, already done
private async getUniqueOutOfSyncFilesNoIdx(
syncContext: MLSyncContext,
files: EnteFile[],
) {
const limit = syncContext.config.batchSize;
const mlVersion = syncContext.config.mlVersion;
const uniqueFiles: Map<number, EnteFile> = new Map<number, EnteFile>();
for (let i = 0; uniqueFiles.size < limit && i < files.length; i++) {
const mlFileData = await this.getMLFileData(files[i].id);
const mlFileVersion = mlFileData?.mlVersion || 0;
if (
!uniqueFiles.has(files[i].id) &&
(!mlFileData?.errorCount || mlFileData.errorCount < 2) &&
(mlFileVersion < mlVersion ||
syncContext.config.imageSource !== mlFileData.imageSource)
) {
uniqueFiles.set(files[i].id, files[i]);
}
}
return [...uniqueFiles.values()];
}
// private async getOutOfSyncFilesNoIdx(syncContext: MLSyncContext) {
// const existingFilesMap = await this.getLocalFilesMap(syncContext);
// // existingFiles.sort(
// // (a, b) => b.metadata.creationTime - a.metadata.creationTime
// // );
// console.time('getUniqueOutOfSyncFiles');
// syncContext.outOfSyncFiles = await this.getUniqueOutOfSyncFilesNoIdx(
// syncContext,
// [...existingFilesMap.values()]
// );
// addLogLine('getUniqueOutOfSyncFiles');
// addLogLine(
// 'Got unique outOfSyncFiles: ',
// syncContext.outOfSyncFiles.length,
// 'for batchSize: ',
// syncContext.config.batchSize
// );
// }
private async syncFiles(syncContext: MLSyncContext) {
try {
const functions = syncContext.outOfSyncFiles.map(
@ -448,22 +351,12 @@ class MachineLearningService {
try {
await ReaderService.getImageBitmap(syncContext, fileContext);
// await this.syncFaceDetections(syncContext, fileContext);
// await ObjectService.syncFileObjectDetections(
// syncContext,
// fileContext
// );
await Promise.all([
this.syncFaceDetections(syncContext, fileContext),
ObjectService.syncFileObjectDetections(
syncContext,
fileContext,
),
// TextService.syncFileTextDetections(
// syncContext,
// fileContext,
// textDetectionTimeoutIndex
// ),
]);
newMlFile.errorCount = 0;
newMlFile.lastErrorMessage = undefined;
@ -495,30 +388,15 @@ class MachineLearningService {
await tf.ready();
addLogLine("01 TF Memory stats: ", JSON.stringify(tf.memory()));
// await tfjsFaceDetectionService.init();
// // addLogLine('02 TF Memory stats: ',JSON.stringify(tf.memory()));
// await this.faceLandmarkService.init();
// await faceapi.nets.faceLandmark68Net.loadFromUri('/models/face-api/');
// // addLogLine('03 TF Memory stats: ',JSON.stringify(tf.memory()));
// await tfjsFaceEmbeddingService.init();
// await faceapi.nets.faceRecognitionNet.loadFromUri('/models/face-api/');
// addLogLine('04 TF Memory stats: ',JSON.stringify(tf.memory()));
this.initialized = true;
}
public async dispose() {
this.initialized = false;
// await this.faceDetectionService.dispose();
// addLogLine('11 TF Memory stats: ',JSON.stringify(tf.memory()));
// await this.faceLandmarkService.dispose();
// addLogLine('12 TF Memory stats: ',JSON.stringify(tf.memory()));
// await this.faceEmbeddingService.dispose();
// addLogLine('13 TF Memory stats: ',JSON.stringify(tf.memory()));
}
private async getMLFileData(fileId: number) {
// return mlFilesStore.getItem<MlFileData>(fileId);
return mlIDbStorage.getFile(fileId);
}
@ -526,7 +404,6 @@ class MachineLearningService {
syncContext: MLSyncContext,
mlFileData: MlFileData,
) {
// return mlFilesStore.setItem(mlFileData.fileId.toString(), mlFileData);
mlIDbStorage.putFile(mlFileData);
}
@ -559,14 +436,12 @@ class MachineLearningService {
}
private async persistMLLibraryData(syncContext: MLSyncContext) {
// return mlLibraryStore.setItem('data', syncContext.mlLibraryData);
return mlIDbStorage.putLibraryData(syncContext.mlLibraryData);
}
public async syncIndex(syncContext: MLSyncContext) {
await this.getMLLibraryData(syncContext);
// await this.init();
await PeopleService.syncPeopleIndex(syncContext);
await ObjectService.syncThingsIndex(syncContext);
@ -574,17 +449,6 @@ class MachineLearningService {
await this.persistMLLibraryData(syncContext);
}
// private async runTSNE(syncContext: MLSyncContext) {
// const allFacesMap = await FaceService.getAllSyncedFacesMap(syncContext);
// const allFaces = getAllFacesFromMap(allFacesMap);
// const input = allFaces
// .slice(0, syncContext.config.tsne.samples)
// .map((f) => Array.from(f.embedding));
// syncContext.tsne = toTSNE(input, syncContext.config.tsne);
// addLogLine('tsne: ', syncContext.tsne);
// }
private async syncFaceDetections(
syncContext: MLSyncContext,
fileContext: MLSyncFileContext,

View file

@ -1,147 +0,0 @@
import ComlinkCryptoWorker from "@ente/shared/crypto";
import { DedicatedCryptoWorker } from "@ente/shared/crypto/internal/crypto.worker";
import HTTPService from "@ente/shared/network/HTTPService";
import { getEndpoint } from "@ente/shared/network/api";
import { logError } from "@ente/shared/sentry";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
import { Remote } from "comlink";
import { SetProgressTracker } from "components/FixLargeThumbnail";
import downloadManager from "services/download";
import { getLocalFiles } from "services/fileService";
import { getFileType } from "services/typeDetectionService";
import { generateThumbnail } from "services/upload/thumbnailService";
import uploadHttpClient from "services/upload/uploadHttpClient";
import { S3FileAttributes } from "types/file";
import { UploadURL } from "types/upload";
import { getLocalTrashedFiles } from "./trashService";
const ENDPOINT = getEndpoint();
const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB
export async function getLargeThumbnailFiles() {
try {
const token = getToken();
if (!token) {
return;
}
const resp = await HTTPService.get(
`${ENDPOINT}/files/large-thumbnails`,
{
threshold: REPLACE_THUMBNAIL_THRESHOLD,
},
{
"X-Auth-Token": token,
},
);
return resp.data.largeThumbnailFiles as number[];
} catch (e) {
logError(e, "failed to get large thumbnail files");
throw e;
}
}
export async function replaceThumbnail(
setProgressTracker: SetProgressTracker,
largeThumbnailFileIDs: Set<number>,
) {
let completedWithError = false;
try {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const files = await getLocalFiles();
const trashFiles = await getLocalTrashedFiles();
const largeThumbnailFiles = [...files, ...trashFiles].filter((file) =>
largeThumbnailFileIDs.has(file.id),
);
if (largeThumbnailFileIDs.size !== largeThumbnailFiles.length) {
logError(Error(), "all large thumbnail files not found locally");
}
if (largeThumbnailFiles.length === 0) {
return completedWithError;
}
setProgressTracker({ current: 0, total: largeThumbnailFiles.length });
const uploadURLs: UploadURL[] = [];
await uploadHttpClient.fetchUploadURLs(
largeThumbnailFiles.length,
uploadURLs,
);
for (const [idx, file] of largeThumbnailFiles.entries()) {
try {
setProgressTracker({
current: idx,
total: largeThumbnailFiles.length,
});
const originalThumbnail =
await downloadManager.getThumbnail(file);
const dummyImageFile = new File(
[originalThumbnail],
file.metadata.title,
);
const fileTypeInfo = await getFileType(dummyImageFile);
const { thumbnail: newThumbnail } = await generateThumbnail(
dummyImageFile,
fileTypeInfo,
);
const newUploadedThumbnail = await uploadThumbnail(
cryptoWorker,
file.key,
newThumbnail,
uploadURLs.pop(),
);
await updateThumbnail(file.id, newUploadedThumbnail);
} catch (e) {
logError(e, "failed to replace a thumbnail");
completedWithError = true;
}
}
} catch (e) {
logError(e, "replace Thumbnail function failed");
completedWithError = true;
}
return completedWithError;
}
export async function uploadThumbnail(
worker: Remote<DedicatedCryptoWorker>,
fileKey: string,
updatedThumbnail: Uint8Array,
uploadURL: UploadURL,
): Promise<S3FileAttributes> {
const { file: encryptedThumbnail } = await worker.encryptThumbnail(
updatedThumbnail,
fileKey,
);
const thumbnailObjectKey = await uploadHttpClient.putFile(
uploadURL,
encryptedThumbnail.encryptedData,
() => {},
);
return {
objectKey: thumbnailObjectKey,
decryptionHeader: encryptedThumbnail.decryptionHeader,
};
}
export async function updateThumbnail(
fileID: number,
newThumbnail: S3FileAttributes,
) {
try {
const token = getToken();
if (!token) {
return;
}
await HTTPService.put(
`${ENDPOINT}/files/thumbnail`,
{
fileID: fileID,
thumbnail: newThumbnail,
},
null,
{
"X-Auth-Token": token,
},
);
} catch (e) {
logError(e, "failed to update thumbnail");
throw e;
}
}

View file

@ -4,14 +4,9 @@ import HTTPService from "@ente/shared/network/HTTPService";
import { getEndpoint } from "@ente/shared/network/api";
import { logError } from "@ente/shared/sentry";
import localForage from "@ente/shared/storage/localForage";
import { REPORT_REASON } from "constants/publicCollection";
import { Collection, CollectionPublicMagicMetadata } from "types/collection";
import { EncryptedEnteFile, EnteFile } from "types/file";
import {
AbuseReportDetails,
AbuseReportRequest,
LocalSavedPublicCollectionFiles,
} from "types/publicCollection";
import { LocalSavedPublicCollectionFiles } from "types/publicCollection";
import { decryptFile, mergeMetadata, sortFiles } from "utils/file";
const ENDPOINT = getEndpoint();
@ -376,30 +371,6 @@ export const verifyPublicCollectionPassword = async (
}
};
export const reportAbuse = async (
token: string,
url: string,
reason: REPORT_REASON,
details: AbuseReportDetails,
) => {
try {
if (!token) {
return;
}
const abuseReportRequest: AbuseReportRequest = { url, reason, details };
await HTTPService.post(
`${ENDPOINT}/public-collection/report-abuse`,
abuseReportRequest,
null,
{ "X-Auth-Access-Token": token },
);
} catch (e) {
logError(e, "failed to post abuse report");
throw e;
}
};
export const removePublicCollectionWithFiles = async (
collectionUID: string,
collectionKey: string,

View file

@ -1,6 +1,5 @@
import { logError } from "@ente/shared/sentry";
import { FIX_OPTIONS } from "components/FixCreationTime";
import { SetProgressTracker } from "components/FixLargeThumbnail";
import { EnteFile } from "types/file";
import {
changeFileCreationTime,
@ -21,6 +20,13 @@ const EXIF_TIME_TAGS = [
"MetadataDate",
];
export type SetProgressTracker = React.Dispatch<
React.SetStateAction<{
current: number;
total: number;
}>
>;
export async function updateCreationTimeWithExif(
filesToBeUpdated: EnteFile[],
fixOption: FIX_OPTIONS,

View file

@ -1,15 +1,6 @@
import * as tf from "@tensorflow/tfjs-core";
// import {
// FaceDetection,
// FaceLandmarks68,
// WithFaceDescriptor,
// WithFaceLandmarks,
// } from 'face-api.js';
import { DebugInfo } from "hdbscan";
import PQueue from "p-queue";
// import { Point as D3Point, RawNodeDatum } from 'react-d3-tree/lib/types/common';
import { EnteFile } from "types/file";
import { Dimensions } from "types/image";
import { Box, Point } from "../../../thirdparty/face-api/classes";
@ -24,33 +15,9 @@ export interface MLSyncResult {
error?: Error;
}
export interface DebugFace {
fileId: string;
// face: FaceApiResult;
face: AlignedFace;
embedding: FaceEmbedding;
faceImage: FaceImage;
}
// export interface MLDebugResult {
// allFaces: DebugFace[];
// clustersWithNoise: FacesClustersWithNoise;
// tree: RawNodeDatum;
// tsne: TSNEData;
// }
export declare type FaceImage = Array<Array<Array<number>>>;
export declare type FaceImageBlob = Blob;
// export declare type FaceApiResult = WithFaceDescriptor<
// WithFaceLandmarks<
// {
// detection: FaceDetection;
// },
// FaceLandmarks68
// >
// >;
export declare type FaceDescriptor = Float32Array;
export declare type Cluster = Array<number>;
@ -79,12 +46,6 @@ export interface NearestCluster {
distance: number;
}
// export interface TSNEData {
// width: number;
// height: number;
// dataset: D3Point[];
// }
export declare type Landmark = Point;
export declare type ImageType = "Original" | "Preview";

View file

@ -1,5 +1,4 @@
import { TimeStampListItem } from "components/PhotoList";
import { REPORT_REASON } from "constants/publicCollection";
import { PublicURL } from "types/collection";
import { EnteFile } from "types/file";
@ -17,31 +16,6 @@ export interface LocalSavedPublicCollectionFiles {
files: EnteFile[];
}
export interface AbuseReportRequest {
url: string;
reason: REPORT_REASON;
details: AbuseReportDetails;
}
export interface AbuseReportDetails {
fullName: string;
email: string;
comment: string;
signature: string;
onBehalfOf: string;
jobTitle: string;
address: Address;
}
export interface Address {
street: string;
city: string;
state: string;
country: string;
postalCode: string;
phone: string;
}
export type SetPublicShareProp = React.Dispatch<
React.SetStateAction<PublicURL>
>;

View file

@ -1,116 +0,0 @@
import { euclidean } from "hdbscan";
// import { RawNodeDatum } from 'react-d3-tree/lib/types/common';
// import { f32Average, getAllFacesFromMap } from '.';
import { addLogLine } from "@ente/shared/logging";
import {
FacesCluster,
// Cluster,
// FaceDescriptor,
FaceWithEmbedding,
MLSyncContext,
NearestCluster,
} from "types/machineLearning";
// import { getAllFacesMap } from 'utils/storage/mlStorage';
// export function getClusterSummary(cluster: Cluster): FaceDescriptor {
// const faceScore = (f) => f.detection.score; // f.alignedRect.box.width *
// return cluster
// .map((f) => this.allFaces[f].face)
// .sort((f1, f2) => faceScore(f2) - faceScore(f1))[0].descriptor;
// const descriptors = cluster.map((f) => this.allFaces[f].embedding);
// return f32Average(descriptors);
// }
export function updateClusterSummaries(syncContext: MLSyncContext) {
if (
!syncContext.mlLibraryData?.faceClusteringResults?.clusters ||
syncContext.mlLibraryData?.faceClusteringResults?.clusters.length < 1
) {
return;
}
const resultClusters =
syncContext.mlLibraryData.faceClusteringResults.clusters;
resultClusters.forEach((resultCluster) => {
syncContext.mlLibraryData.faceClustersWithNoise.clusters.push({
faces: resultCluster,
// summary: this.getClusterSummary(resultCluster),
});
});
}
export function getNearestCluster(
syncContext: MLSyncContext,
noise: FaceWithEmbedding,
): NearestCluster {
let nearest: FacesCluster = null;
let nearestDist = 100000;
syncContext.mlLibraryData.faceClustersWithNoise.clusters.forEach((c) => {
const dist = euclidean(
Array.from(noise.embedding),
Array.from(c.summary),
);
if (dist < nearestDist) {
nearestDist = dist;
nearest = c;
}
});
addLogLine("nearestDist: ", nearestDist);
return { cluster: nearest, distance: nearestDist };
}
// export async function assignNoiseWithinLimit(syncContext: MLSyncContext) {
// if (
// !syncContext.mlLibraryData?.faceClusteringResults?.noise ||
// syncContext.mlLibraryData?.faceClusteringResults.noise.length < 1
// ) {
// return;
// }
// const noise = syncContext.mlLibraryData.faceClusteringResults.noise;
// const allFacesMap = await getAllFacesMap();
// const allFaces = getAllFacesFromMap(allFacesMap);
// noise.forEach((n) => {
// const noiseFace = allFaces[n];
// const nearest = this.getNearestCluster(syncContext, noiseFace);
// if (nearest.cluster && nearest.distance < this.maxFaceDistance) {
// addLogLine('Adding noise to cluser: ', n, nearest.distance);
// nearest.cluster.faces.push(n);
// } else {
// addLogLine(
// 'No cluster for noise: ',
// n,
// 'within distance: ',
// this.maxFaceDistance
// );
// this.clustersWithNoise.noise.push(n);
// }
// });
// }
// TODO: remove recursion to avoid stack size limits
// export function toD3Tree(
// treeNode: TreeNode<number>,
// allObjects: Array<any>
// ): RawNodeDatum {
// if (!treeNode.left && !treeNode.right) {
// return {
// name: treeNode.data.toString(),
// attributes: {
// face: allObjects[treeNode.data],
// },
// };
// }
// const children = [];
// treeNode.left && children.push(toD3Tree(treeNode.left, allObjects));
// treeNode.right && children.push(toD3Tree(treeNode.right, allObjects));
// return {
// name: treeNode.data.toString(),
// children: children,
// };
// }

View file

@ -1,28 +0,0 @@
import {
offscreenCanvasSupported,
runningInChrome,
webglSupported,
} from "utils/common";
import { addLogLine } from "@ente/shared/logging";
import isElectron from "is-electron";
export function canEnableMlSearch(): boolean {
// check if is chrome or ente desktop
if (!runningInChrome(false) && !isElectron()) {
addLogLine("Not running in Chrome Desktop or Ente Desktop App");
return false;
}
if (!offscreenCanvasSupported()) {
addLogLine("OffscreenCanvas is NOT supported");
return false;
}
if (!webglSupported()) {
addLogLine("webgl is NOT supported");
return false;
}
return true;
}

View file

@ -1,10 +1,15 @@
import { addLogLine } from "@ente/shared/logging";
import { CACHES } from "@ente/shared/storage/cacheStorage/constants";
import { cached } from "@ente/shared/storage/cacheStorage/helpers";
import * as tf from "@tensorflow/tfjs-core";
import { NormalizedFace } from "blazeface-back";
import { FILE_TYPE } from "constants/file";
import { BLAZEFACE_FACE_SIZE } from "constants/mlConfig";
import { euclidean } from "hdbscan";
import PQueue from "p-queue";
import DownloadManager from "services/download";
import { getLocalFiles } from "services/fileService";
import { decodeLivePhoto } from "services/livePhotoService";
import { EnteFile } from "types/file";
import { Dimensions } from "types/image";
import {
@ -18,12 +23,6 @@ import {
RealWorldObject,
Versioned,
} from "types/machineLearning";
// import { mlFilesStore, mlPeopleStore } from 'utils/storage/mlStorage';
import { addLogLine } from "@ente/shared/logging";
import { CACHES } from "@ente/shared/storage/cacheStorage/constants";
import { cached } from "@ente/shared/storage/cacheStorage/helpers";
import { FILE_TYPE } from "constants/file";
import { decodeLivePhoto } from "services/livePhotoService";
import { getRenderableImage } from "utils/file";
import { imageBitmapToBlob } from "utils/image";
import mlIDbStorage from "utils/storage/mlIDbStorage";

View file

@ -1,136 +0,0 @@
import { addLogLine } from "@ente/shared/logging";
import { Face, MlFileData } from "types/machineLearning";
import mlIDbStorage from "utils/storage/mlIDbStorage";
import { mlFilesStore } from "utils/storage/mlStorage";
import { getFaceId } from ".";
import { storeFaceCropForBlob } from "./faceCrop";
// TODO: for migrating existing data, to be removed
export async function migrateExistingFiles() {
const existingFiles: Array<MlFileData> = [];
await mlFilesStore.iterate((mlFileData: MlFileData) => {
if (!mlFileData.errorCount) {
mlFileData.errorCount = 0;
existingFiles.push(mlFileData);
}
});
addLogLine("existing files: ", existingFiles.length);
try {
for (const file of existingFiles) {
await mlIDbStorage.putFile(file);
}
await mlIDbStorage.setIndexVersion("files", 1);
addLogLine("migrateExistingFiles done");
} catch (e) {
console.error(e);
}
}
export async function migrateFaceCropsToCache() {
const startTime = Date.now();
addLogLine("migrateFaceCropsToCache started");
const allFiles = await mlIDbStorage.getAllFiles();
const allFilesWithFaces = allFiles.filter(
(f) => f.faces && f.faces.length > 0,
);
const updatedFacesMap = new Map<number, Array<Face>>();
for (const file of allFilesWithFaces) {
let updated = false;
for (const face of file.faces) {
if (!face["id"]) {
const faceCropBlob = face.crop["image"];
const faceId = getFaceId(face, file.imageDimensions);
face.crop = await storeFaceCropForBlob(
faceId,
face.crop.imageBox,
faceCropBlob,
);
face["id"] = faceId;
updated = true;
}
}
if (updated) {
updatedFacesMap.set(file.fileId, file.faces);
}
}
if (updatedFacesMap.size > 0) {
addLogLine("updating face crops: ", updatedFacesMap.size);
await mlIDbStorage.updateFaces(updatedFacesMap);
} else {
addLogLine("not updating face crops: ", updatedFacesMap.size);
}
addLogLine("migrateFaceCropsToCache", Date.now() - startTime, "ms");
}
export async function migrateFaceInterfaceUpdate() {
const startTime = Date.now();
addLogLine("migrateFaceInterfaceUpdate started");
const faceSchemaVersion = await mlIDbStorage.getIndexVersion("faceSchema");
if (faceSchemaVersion) {
addLogLine("not running migrateFaceInterfaceUpdate");
return;
}
const allFiles = await mlIDbStorage.getAllFiles();
const updatedFiles = allFiles.map((file) => {
const updatedFaces = file.faces?.map((f) => {
const updatedFace = {
id: f["faceId"],
fileId: f.fileId,
detection: {
box: f["box"],
landmarks: f["landmarks"],
probability: f["probability"],
},
crop: f["faceCrop"],
alignment: {
affineMatrix: f["affineMatrix"],
center: f["center"],
rotation: f["rotation"],
size: f["size"],
},
embedding: Float32Array.from(f.embedding),
personId: f.personId,
} as Face;
if (!updatedFace.id) {
updatedFace.id = getFaceId(updatedFace, file.imageDimensions);
}
return updatedFace;
});
const updated: MlFileData = {
fileId: file.fileId,
faceDetectionMethod: file["detectionMethod"],
faceCropMethod: {
value: "ArcFace",
version: 1,
},
faceAlignmentMethod: file["alignmentMethod"],
faceEmbeddingMethod: file["embeddingMethod"],
faces: updatedFaces,
imageDimensions: file.imageDimensions,
imageSource: file.imageSource,
errorCount: file.errorCount,
lastErrorMessage: file.lastErrorMessage,
mlVersion: file.mlVersion,
};
return updated;
});
addLogLine("migrateFaceInterfaceUpdate updating: ", updatedFiles.length);
await mlIDbStorage.putAllFilesInTx(updatedFiles);
await mlIDbStorage.setIndexVersion("faceSchema", 1);
addLogLine("migrateFaceInterfaceUpdate done");
addLogLine("migrateFaceInterfaceUpdate", Date.now() - startTime, "ms");
}

View file

@ -1,161 +0,0 @@
import { addLogLine } from "@ente/shared/logging";
import { CacheStorageService } from "@ente/shared/storage/cacheStorage";
import { CACHES } from "@ente/shared/storage/cacheStorage/constants";
import * as zip from "@zip.js/zip.js";
import { MlFileData } from "types/machineLearning";
import mlIDbStorage from "utils/storage/mlIDbStorage";
class FileSystemWriter extends zip.Writer {
writableStream: FileSystemWritableFileStream;
constructor(writableStream: FileSystemWritableFileStream) {
super();
this.writableStream = writableStream;
}
async writeUint8Array(array: Uint8Array) {
// addLogLine('zipWriter needs to write data: ', array.byteLength);
return this.writableStream.write(array);
}
async getData() {
return undefined;
}
}
class FileReader extends zip.Reader {
file: File;
constructor(file: File) {
super();
this.file = file;
}
public async init() {
this.size = this.file.size;
// addLogLine('zipReader init, size: ', this.size);
}
public async readUint8Array(
index: number,
length: number,
): Promise<Uint8Array> {
// addLogLine('zipReader needs data: ', index, length);
const slicedFile = this.file.slice(index, index + length);
const arrayBuffer = await slicedFile.arrayBuffer();
return new Uint8Array(arrayBuffer);
}
}
export async function exportMlData(
mlDataZipWritable: FileSystemWritableFileStream,
) {
const zipWriter = new zip.ZipWriter(
new FileSystemWriter(mlDataZipWritable),
);
try {
try {
await exportMlDataToZipWriter(zipWriter);
} finally {
await zipWriter.close();
}
} catch (e) {
await mlDataZipWritable.abort();
throw e;
}
await mlDataZipWritable.close();
addLogLine("Ml Data Exported");
}
async function exportMlDataToZipWriter(zipWriter: zip.ZipWriter) {
const mlDbData = await mlIDbStorage.getAllMLData();
const faceClusteringResults =
mlDbData?.library?.data?.faceClusteringResults;
faceClusteringResults && (faceClusteringResults.debugInfo = undefined);
addLogLine(
"Exporting ML DB data: ",
JSON.stringify(Object.keys(mlDbData)),
JSON.stringify(
Object.keys(mlDbData)?.map((k) => Object.keys(mlDbData[k])?.length),
),
);
await zipWriter.add(
"indexeddb/mldata.json",
new zip.TextReader(JSON.stringify(mlDbData)),
);
const faceCropCache = await CacheStorageService.open(CACHES.FACE_CROPS);
const files =
mlDbData["files"] && (Object.values(mlDbData["files"]) as MlFileData[]);
for (const fileData of files || []) {
for (const face of fileData.faces || []) {
const faceCropUrl = face.crop?.imageUrl;
if (!faceCropUrl) {
console.error("face crop not found for faceId: ", face.id);
continue;
}
const response = await faceCropCache.match(faceCropUrl);
if (response && response.ok) {
const blob = await response.blob();
await zipWriter.add(
`caches/${CACHES.FACE_CROPS}${faceCropUrl}`,
new zip.BlobReader(blob),
{ level: 0 },
);
} else {
console.error(
"face crop cache entry not found for faceCropUrl: ",
faceCropUrl,
);
}
}
}
}
export async function importMlData(mlDataZipFile: File) {
const zipReader = new zip.ZipReader(new FileReader(mlDataZipFile));
try {
await importMlDataFromZipReader(zipReader);
} finally {
await zipReader.close();
}
addLogLine("ML Data Imported");
}
async function importMlDataFromZipReader(zipReader: zip.ZipReader) {
const zipEntries = await zipReader.getEntries();
// addLogLine(zipEntries);
const faceCropPath = `caches/${CACHES.FACE_CROPS}`;
const faceCropCache = await CacheStorageService.open(CACHES.FACE_CROPS);
let mldataEntry;
for (const entry of zipEntries) {
if (entry.filename === "indexeddb/mldata.json") {
mldataEntry = entry;
} else if (entry.filename.startsWith(faceCropPath)) {
const faceCropUrl = entry.filename.substring(faceCropPath.length);
// addLogLine('importing faceCropUrl: ', faceCropUrl);
const faceCropCacheBlob: Blob = await entry.getData(
new zip.BlobWriter("image/jpeg"),
);
faceCropCache.put(faceCropUrl, new Response(faceCropCacheBlob));
}
}
const mlDataJsonStr: string = await mldataEntry.getData(
new zip.TextWriter(),
);
const mlDbData = JSON.parse(mlDataJsonStr);
addLogLine(
"importing ML DB data: ",
JSON.stringify(Object.keys(mlDbData)),
JSON.stringify(
Object.keys(mlDbData)?.map((k) => Object.keys(mlDbData[k])?.length),
),
);
await mlIDbStorage.putAllMLData(mlDbData);
}

View file

@ -1,40 +0,0 @@
// // import TSNE from 'tsne-js';
// import { TSNEConfig, TSNEData } from 'types/machineLearning';
// export function toD3Tsne(tsne) {
// const data: TSNEData = {
// width: 800,
// height: 800,
// dataset: [],
// };
// data.dataset = tsne.map((t) => {
// return {
// x: (data.width * (t[0] + 1.0)) / 2,
// y: (data.height * (t[1] + 1.0)) / 2,
// };
// });
// return data;
// }
// export function toTSNE(denseInput: Array<Array<number>>, config: TSNEConfig) {
// if (!denseInput || denseInput.length < 1) {
// return null;
// }
// const model = new TSNE(config);
// model.init({
// data: denseInput,
// type: 'dense',
// });
// // `error`, `iter`: final error and iteration number
// // note: computation-heavy action happens here
// model.run();
// // `outputScaled` is `output` scaled to a range of [-1, 1]
// return model.getOutputScaled();
// }
export {};

View file

@ -1,97 +0,0 @@
import localForage from "localforage";
import { EnteFile } from "types/file";
import {
Face,
MlFileData,
MLIndex,
MLSyncContext,
} from "types/machineLearning";
export const mlFilesStore = localForage.createInstance({
driver: localForage.INDEXEDDB,
name: "ml-data",
version: 1.0,
storeName: "files",
});
export const mlPeopleStore = localForage.createInstance({
driver: localForage.INDEXEDDB,
name: "ml-data",
version: 1.0,
storeName: "people",
});
export const mlLibraryStore = localForage.createInstance({
driver: localForage.INDEXEDDB,
name: "ml-data",
version: 1.0,
storeName: "library",
});
export const mlVersionStore = localForage.createInstance({
driver: localForage.INDEXEDDB,
name: "ml-data",
version: 1.0,
storeName: "versions",
});
export async function clearMLStorage() {
await mlFilesStore.clear();
await mlPeopleStore.clear();
await mlLibraryStore.clear();
await mlVersionStore.clear();
}
export async function getIndexVersion(index: MLIndex): Promise<number> {
return ((await mlVersionStore.getItem(`${index}`)) as number) || 0;
}
export async function setIndexVersion(
index: MLIndex,
version: number,
): Promise<number> {
await mlVersionStore.setItem(`${index}`, version);
return version;
}
export async function incrementIndexVersion(index: MLIndex): Promise<number> {
let currentVersion = await getIndexVersion(index);
currentVersion = currentVersion + 1;
await setIndexVersion(index, currentVersion);
return currentVersion;
}
export async function isVersionOutdated(index: MLIndex, thanIndex: MLIndex) {
const indexVersion = await getIndexVersion(index);
const thanIndexVersion = await getIndexVersion(thanIndex);
return indexVersion < thanIndexVersion;
}
export function newMlData(
syncContext: MLSyncContext,
enteFile: EnteFile,
): MlFileData {
return {
fileId: enteFile.id,
imageSource: syncContext.config.imageSource,
faceDetectionMethod: syncContext.faceDetectionService.method,
faceCropMethod: syncContext.faceCropService.method,
faceAlignmentMethod: syncContext.faceAlignmentService.method,
faceEmbeddingMethod: syncContext.faceEmbeddingService.method,
errorCount: 0,
mlVersion: 0,
};
}
export async function getAllFacesMap() {
const allSyncedFacesMap = new Map<number, Array<Face>>();
await mlFilesStore.iterate((mlFileData: MlFileData) => {
mlFileData.faces &&
allSyncedFacesMap.set(mlFileData.fileId, mlFileData.faces);
});
return allSyncedFacesMap;
}

View file

@ -3,23 +3,12 @@ import { expose } from "comlink";
import mlService from "services/machineLearning/machineLearningService";
import { EnteFile } from "types/file";
import { MachineLearningWorker } from "types/machineLearning";
// import ReverseProxiedElectronCacheStorageProxy from './electronCacheStorageProxy.proxy';
// import { setupResponseComlinkTransferHandler } from 'utils/comlink';
export class DedicatedMLWorker implements MachineLearningWorker {
constructor() {
addLogLine("DedicatedMLWorker constructor called");
// this.init();
}
// public async init() {
// const recp = new ReverseProxiedElectronCacheStorageProxy();
// const cacheProxy = await recp.open('thumbs');
// const thumb = await cacheProxy.match('13578875');
// addLogLine('worker init cache.match', thumb);
// }
public async closeLocalSyncContext() {
return mlService.closeLocalSyncContext();
}
@ -51,5 +40,3 @@ export class DedicatedMLWorker implements MachineLearningWorker {
}
expose(DedicatedMLWorker, self);
// setupResponseComlinkTransferHandler();

View file

@ -1,4 +1,52 @@
# Notes for Developers
# Development
## Editor setup
We recommend VS Code, with the following extensions:
- Prettier - reformats your code automatically (enable format on save),
- ESLint - warns you about issues
Optionally, if you're going to make many changes to the CSS in JS, you might
also find it useful to install the _vscode-styled-components_ extension.
## Yarn commands
Make sure you're on yarn 1.x series (aka yarn "classic").
### yarn install
Installs dependencies. This needs to be done once, and thereafter wherever there
is a change in `yarn.lock` (e.g. when pulling the latest upstream).
### yarn dev:*
Launch the app in development mode. There is one `yarn dev:foo` for each app,
e.g. `yarn dev:auth`. `yarn dev` is a shortcut for `yarn dev:photos`.
The ports are different for the main apps (3000), various sidecars (3001, 3002).
### yarn build:*
Build a production export for the app. This is a bunch of static HTML/JS/CSS
that can be then deployed to any web server.
There is one `yarn build:foo` for each app, e.g. `yarn build:auth`. The output
will be placed in `apps/<foo>/out`, e.g. `apps/auth/out`.
### yarn preview:*
Build a production export and start a local web server to serve it. This uses
Python's built in web server, and is okay for quick testing but should not be
used in production.
The ports are the same as that for `yarn dev:*`
### lint, lint-fix
Use `yarn lint` to check that your code formatting is as expected, and that
there are no linter errors. Use `yarn lint-fix` to try and automatically fix the
issues.
## Monorepo

View file

@ -26,7 +26,11 @@
"dev:payments": "yarn workspace payments next dev -p 3001",
"dev:photos": "yarn workspace photos next dev",
"lint": "yarn prettier --check . && yarn workspaces run eslint .",
"lint-fix": "yarn prettier --write . && yarn workspaces run eslint --fix ."
"lint-fix": "yarn prettier --write . && yarn workspaces run eslint --fix .",
"preview:accounts": "yarn build:accounts && python3 -m http.server -d apps/accounts/out 3001",
"preview:auth": "yarn build:auth && python3 -m http.server -d apps/auth/out 3000",
"preview:cast": "yarn build:cast && python3 -m http.server -d apps/accounts/out 3001",
"preview:photos": "yarn build:photos && python3 -m http.server -d apps/photos/out 3000"
},
"resolutions": {
"libsodium": "0.7.9"

View file

@ -1,14 +1,8 @@
import { EmotionCache } from "@emotion/react";
import { TwoFactorType } from "@ente/accounts/constants/twofactor";
import { SetDialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
import { AppProps } from "next/app";
import { NextRouter } from "next/router";
import { APPS } from "./constants";
export interface EnteAppProps extends AppProps {
emotionCache?: EmotionCache;
}
export interface PageProps {
appContext: {
showNavBar: (show: boolean) => void;

View file

@ -1,23 +1,6 @@
import Document, {
DocumentContext,
DocumentProps,
Head,
Html,
Main,
NextScript,
} from "next/document";
import React from "react";
import { Head, Html, Main, NextScript } from "next/document";
import createEmotionServer from "@emotion/server/create-instance";
import { EnteAppProps } from "@ente/shared/apps/types";
import createEmotionCache from "@ente/shared/themes/createEmotionCache";
import { AppType } from "next/app";
export interface EnteDocumentProps extends DocumentProps {
emotionStyleTags: JSX.Element[];
}
export default function EnteDocument({ emotionStyleTags }: EnteDocumentProps) {
export default function EnteDocument() {
return (
<Html lang="en">
<Head>
@ -27,7 +10,6 @@ export default function EnteDocument({ emotionStyleTags }: EnteDocumentProps) {
/>
<link rel="icon" href="/images/favicon.png" type="image/png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
{emotionStyleTags}
</Head>
<body>
<Main />
@ -36,67 +18,3 @@ export default function EnteDocument({ emotionStyleTags }: EnteDocumentProps) {
</Html>
);
}
// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with static-site generation (SSG).
EnteDocument.getInitialProps = async (ctx: DocumentContext) => {
// Resolution order
//
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render
//
// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render
//
// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render
const originalRenderPage = ctx.renderPage;
// You can consider sharing the same Emotion cache between all the SSR requests to speed up performance.
// However, be aware that it can have global side effects.
const cache = createEmotionCache();
// eslint-disable-next-line @typescript-eslint/unbound-method
const { extractCriticalToChunks } = createEmotionServer(cache);
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (
App: React.ComponentType<
React.ComponentProps<AppType> & EnteAppProps
>,
) =>
function EnhanceApp(props) {
return <App emotionCache={cache} {...props} />;
},
});
const initialProps = await Document.getInitialProps(ctx);
// This is important. It prevents Emotion to render invalid HTML.
// See https://github.com/mui/material-ui/issues/26561#issuecomment-855286153
const emotionStyles = extractCriticalToChunks(initialProps.html);
const emotionStyleTags = emotionStyles.styles.map((style) => (
<style
data-emotion={`${style.key} ${style.ids.join(" ")}`}
key={style.key}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: style.css }}
/>
));
return {
...initialProps,
emotionStyleTags,
};
};

View file

@ -1,19 +0,0 @@
import createCache from "@emotion/cache";
const isBrowser = typeof document !== "undefined";
// On the client side, Create a meta tag at the top of the <head> and set it as insertionPoint.
// This assures that MUI styles are loaded first.
// It allows developers to easily override MUI styles with other styling solutions, like CSS modules.
export default function createEmotionCache() {
let insertionPoint;
if (isBrowser) {
const emotionInsertionPoint = document.querySelector<HTMLMetaElement>(
'meta[name="emotion-insertion-point"]',
);
insertionPoint = emotionInsertionPoint ?? undefined;
}
return createCache({ key: "mui-style", insertionPoint });
}

View file

@ -947,11 +947,6 @@
resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.1.38.tgz#6fda4b410edc753d3213c648320ebcf319669020"
integrity sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==
"@zip.js/zip.js@2.4.2":
version "2.4.2"
resolved "https://registry.yarnpkg.com/@zip.js/zip.js/-/zip.js-2.4.2.tgz#7c2d4b381c0f73dffa1d02ef630c4168ebd5cc4a"
integrity sha512-D4xr9g7U625Q01lQASr9g1sQWEGhndyd+G3v3OvY/qH3pwaJDpbVDy+TO6yEq7c8teQdjjTiiBvlcQcVtT+itg==
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@ -1470,12 +1465,17 @@ debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
dependencies:
ms "2.1.2"
deep-freeze@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/deep-freeze/-/deep-freeze-0.0.1.tgz#3a0b0005de18672819dfd38cd31f91179c893e84"
integrity sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg==
deep-is@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
deepmerge@^2.1.1:
deepmerge@^2.1.1, deepmerge@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
@ -1587,6 +1587,13 @@ enhanced-resolve@^5.12.0:
graceful-fs "^4.2.4"
tapable "^2.2.0"
equals@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/equals/-/equals-1.0.5.tgz#212062dde5e1a510d955f13598efcc6a621b6ace"
integrity sha512-wI15a6ZoaaXPv+55+Vh2Kqn3+efKRv8QPtcGTjW5xmanMnQzESdAt566jevtMZyt3W/jwLDTzXpMph5ECDJ2zg==
dependencies:
jkroso-type "1"
error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@ -2688,6 +2695,11 @@ jackspeak@^2.3.5:
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
jkroso-type@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/jkroso-type/-/jkroso-type-1.1.1.tgz#bc4ced6d6c45fe0745282bafc86a9f8c4fc9ce61"
integrity sha512-zZgay+fPG6PgMUrpyFADmQmvLo39+AZa7Gc5pZhev2RhDxwANEq2etwD8d0e6rTg5NkwOIlQmaEmns3draC6Ng==
jpeg-js@^0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
@ -3354,6 +3366,17 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
pure-react-carousel@^1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/pure-react-carousel/-/pure-react-carousel-1.30.1.tgz#006a333869b51339dafcdee2afa0561eb46d1743"
integrity sha512-B1qi62hZk0OFqRR4cTjtgIeOn/Ls5wo+HsLtrXT4jVf5et8ldBHSt+6LsYRJN86Or8dm+XbnJNEHy6WDJ0/DQw==
dependencies:
"@babel/runtime" "^7.5.5"
deep-freeze "0.0.1"
deepmerge "^2.2.1"
equals "^1.0.5"
prop-types "^15.6.2"
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"