diff --git a/desktop/docs/dev.md b/desktop/docs/dev.md index af459e555..507438fdc 100644 --- a/desktop/docs/dev.md +++ b/desktop/docs/dev.md @@ -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 diff --git a/web/README.md b/web/README.md index 36eb1fb25..908676c55 100644 --- a/web/README.md +++ b/web/README.md @@ -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 diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx index 03d675a2f..8ff4c6a9f 100644 --- a/web/apps/accounts/src/pages/_app.tsx +++ b/web/apps/accounts/src/pages/_app.tsx @@ -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({} 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(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 ( - + <> {APP_TITLES.get(APPS.ACCOUNTS)} )} {showNavbar && } - + {isI18nReady && } - + ); } diff --git a/web/apps/accounts/src/pages/_document.tsx b/web/apps/accounts/src/pages/_document.tsx index 09d4d5782..3c6c2a959 100644 --- a/web/apps/accounts/src/pages/_document.tsx +++ b/web/apps/accounts/src/pages/_document.tsx @@ -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 ; -} +export default DocumentPage; diff --git a/web/apps/accounts/src/styles/global.css b/web/apps/accounts/src/styles/global.css index 0ea6c125d..98ad85a9b 100644 --- a/web/apps/accounts/src/styles/global.css +++ b/web/apps/accounts/src/styles/global.css @@ -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; diff --git a/web/apps/auth/src/pages/_app.tsx b/web/apps/auth/src/pages/_app.tsx index c06531ab4..06dfc2402 100644 --- a/web/apps/auth/src/pages/_app.tsx +++ b/web/apps/auth/src/pages/_app.tsx @@ -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(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(false); const [loading, setLoading] = useState(false); @@ -141,7 +130,7 @@ export default function App(props: EnteAppProps) { }); return ( - + <> {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> + </> ); } diff --git a/web/apps/auth/src/pages/_document.tsx b/web/apps/auth/src/pages/_document.tsx index 09d4d5782..3c6c2a959 100644 --- a/web/apps/auth/src/pages/_document.tsx +++ b/web/apps/auth/src/pages/_document.tsx @@ -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; diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index e0098cd36..0be17f3e2 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -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", diff --git a/web/apps/photos/src/components/ExportInProgress.tsx b/web/apps/photos/src/components/ExportInProgress.tsx index 3324be5c4..ce2da895c 100644 --- a/web/apps/photos/src/components/ExportInProgress.tsx +++ b/web/apps/photos/src/components/ExportInProgress.tsx @@ -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> diff --git a/web/apps/photos/src/components/FixCreationTime.tsx b/web/apps/photos/src/components/FixCreationTime.tsx new file mode 100644 index 000000000..6814cdf0e --- /dev/null +++ b/web/apps/photos/src/components/FixCreationTime.tsx @@ -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> + </> + ); +}; diff --git a/web/apps/photos/src/components/FixCreationTime/footer.tsx b/web/apps/photos/src/components/FixCreationTime/footer.tsx deleted file mode 100644 index 61c6f572d..000000000 --- a/web/apps/photos/src/components/FixCreationTime/footer.tsx +++ /dev/null @@ -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> - ) - ); -} diff --git a/web/apps/photos/src/components/FixCreationTime/index.tsx b/web/apps/photos/src/components/FixCreationTime/index.tsx deleted file mode 100644 index fd4022e19..000000000 --- a/web/apps/photos/src/components/FixCreationTime/index.tsx +++ /dev/null @@ -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> - ); -} diff --git a/web/apps/photos/src/components/FixCreationTime/options.tsx b/web/apps/photos/src/components/FixCreationTime/options.tsx deleted file mode 100644 index 880ea0539..000000000 --- a/web/apps/photos/src/components/FixCreationTime/options.tsx +++ /dev/null @@ -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> - ); -} diff --git a/web/apps/photos/src/components/FixCreationTime/running.tsx b/web/apps/photos/src/components/FixCreationTime/running.tsx deleted file mode 100644 index dbceb6c12..000000000 --- a/web/apps/photos/src/components/FixCreationTime/running.tsx +++ /dev/null @@ -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> - </> - ); -} diff --git a/web/apps/photos/src/components/FixLargeThumbnail.tsx b/web/apps/photos/src/components/FixLargeThumbnail.tsx deleted file mode 100644 index e62fafda3..000000000 --- a/web/apps/photos/src/components/FixLargeThumbnail.tsx +++ /dev/null @@ -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> - ); -} diff --git a/web/apps/photos/src/components/MachineLearning/MLFileDebugView.tsx b/web/apps/photos/src/components/MachineLearning/MLFileDebugView.tsx deleted file mode 100644 index a6c96476b..000000000 --- a/web/apps/photos/src/components/MachineLearning/MLFileDebugView.tsx +++ /dev/null @@ -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> - ); -} diff --git a/web/apps/photos/src/components/MachineLearning/MLServiceFileInfoButton.tsx b/web/apps/photos/src/components/MachineLearning/MLServiceFileInfoButton.tsx deleted file mode 100644 index 8146e239d..000000000 --- a/web/apps/photos/src/components/MachineLearning/MLServiceFileInfoButton.tsx +++ /dev/null @@ -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; diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx index df2da85b7..0b4054e27 100644 --- a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx @@ -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> diff --git a/web/apps/photos/src/components/Sidebar/UtilitySection.tsx b/web/apps/photos/src/components/Sidebar/UtilitySection.tsx index b297f36ee..9160fe4a2 100644 --- a/web/apps/photos/src/components/Sidebar/UtilitySection.tsx +++ b/web/apps/photos/src/components/Sidebar/UtilitySection.tsx @@ -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, diff --git a/web/apps/photos/src/components/ml-debug/index.tsx b/web/apps/photos/src/components/ml-debug/index.tsx deleted file mode 100644 index a5b53a684..000000000 --- a/web/apps/photos/src/components/ml-debug/index.tsx +++ /dev/null @@ -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>; -} diff --git a/web/apps/photos/src/components/pages/sharedAlbum/ReportAbuse.tsx b/web/apps/photos/src/components/pages/sharedAlbum/ReportAbuse.tsx deleted file mode 100644 index ba6b0418f..000000000 --- a/web/apps/photos/src/components/pages/sharedAlbum/ReportAbuse.tsx +++ /dev/null @@ -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> - ); -} diff --git a/web/apps/photos/src/constants/publicCollection.ts b/web/apps/photos/src/constants/publicCollection.ts deleted file mode 100644 index 5a1faad75..000000000 --- a/web/apps/photos/src/constants/publicCollection.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum REPORT_REASON { - COPYRIGHT = "COPYRIGHT", - MALICIOUS_CONTENT = "MALICIOUS_CONTENT", -} diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index fe941f79c..ada902cbc 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -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> + </> ); } diff --git a/web/apps/photos/src/pages/_document.tsx b/web/apps/photos/src/pages/_document.tsx index 09d4d5782..3c6c2a959 100644 --- a/web/apps/photos/src/pages/_document.tsx +++ b/web/apps/photos/src/pages/_document.tsx @@ -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; diff --git a/web/apps/photos/src/pages/index.tsx b/web/apps/photos/src/pages/index.tsx index fc888af83..c789e0ae4 100644 --- a/web/apps/photos/src/pages/index.tsx +++ b/web/apps/photos/src/pages/index.tsx @@ -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; +`; diff --git a/web/apps/photos/src/services/machineLearning/machineLearningService.ts b/web/apps/photos/src/services/machineLearning/machineLearningService.ts index c95effc76..a76c740e1 100644 --- a/web/apps/photos/src/services/machineLearning/machineLearningService.ts +++ b/web/apps/photos/src/services/machineLearning/machineLearningService.ts @@ -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, diff --git a/web/apps/photos/src/services/migrateThumbnailService.ts b/web/apps/photos/src/services/migrateThumbnailService.ts deleted file mode 100644 index ad83f4e4b..000000000 --- a/web/apps/photos/src/services/migrateThumbnailService.ts +++ /dev/null @@ -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; - } -} diff --git a/web/apps/photos/src/services/publicCollectionService.ts b/web/apps/photos/src/services/publicCollectionService.ts index 353281f66..580382dc4 100644 --- a/web/apps/photos/src/services/publicCollectionService.ts +++ b/web/apps/photos/src/services/publicCollectionService.ts @@ -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, diff --git a/web/apps/photos/src/services/updateCreationTimeWithExif.ts b/web/apps/photos/src/services/updateCreationTimeWithExif.ts index 4cc4cc3d6..387188660 100644 --- a/web/apps/photos/src/services/updateCreationTimeWithExif.ts +++ b/web/apps/photos/src/services/updateCreationTimeWithExif.ts @@ -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, diff --git a/web/apps/photos/src/types/machineLearning/index.ts b/web/apps/photos/src/types/machineLearning/index.ts index 64cf8f2f4..2634a0e0f 100644 --- a/web/apps/photos/src/types/machineLearning/index.ts +++ b/web/apps/photos/src/types/machineLearning/index.ts @@ -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"; diff --git a/web/apps/photos/src/types/publicCollection/index.ts b/web/apps/photos/src/types/publicCollection/index.ts index 7e3203e77..11ee4a2d1 100644 --- a/web/apps/photos/src/types/publicCollection/index.ts +++ b/web/apps/photos/src/types/publicCollection/index.ts @@ -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> >; diff --git a/web/apps/photos/src/utils/machineLearning/clustering.ts b/web/apps/photos/src/utils/machineLearning/clustering.ts deleted file mode 100644 index 26d8f803d..000000000 --- a/web/apps/photos/src/utils/machineLearning/clustering.ts +++ /dev/null @@ -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, -// }; -// } diff --git a/web/apps/photos/src/utils/machineLearning/compatibility.ts b/web/apps/photos/src/utils/machineLearning/compatibility.ts deleted file mode 100644 index 47916707d..000000000 --- a/web/apps/photos/src/utils/machineLearning/compatibility.ts +++ /dev/null @@ -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; -} diff --git a/web/apps/photos/src/utils/machineLearning/index.ts b/web/apps/photos/src/utils/machineLearning/index.ts index d6be9e63f..57845937b 100644 --- a/web/apps/photos/src/utils/machineLearning/index.ts +++ b/web/apps/photos/src/utils/machineLearning/index.ts @@ -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"; diff --git a/web/apps/photos/src/utils/machineLearning/migrations.ts b/web/apps/photos/src/utils/machineLearning/migrations.ts deleted file mode 100644 index 111fabfc9..000000000 --- a/web/apps/photos/src/utils/machineLearning/migrations.ts +++ /dev/null @@ -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"); -} diff --git a/web/apps/photos/src/utils/machineLearning/mldataExport.ts b/web/apps/photos/src/utils/machineLearning/mldataExport.ts deleted file mode 100644 index 4cd115e12..000000000 --- a/web/apps/photos/src/utils/machineLearning/mldataExport.ts +++ /dev/null @@ -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); -} diff --git a/web/apps/photos/src/utils/machineLearning/visualization.ts b/web/apps/photos/src/utils/machineLearning/visualization.ts deleted file mode 100644 index 949dde6f2..000000000 --- a/web/apps/photos/src/utils/machineLearning/visualization.ts +++ /dev/null @@ -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 {}; diff --git a/web/apps/photos/src/utils/storage/mlStorage.ts b/web/apps/photos/src/utils/storage/mlStorage.ts deleted file mode 100644 index 31fc38fab..000000000 --- a/web/apps/photos/src/utils/storage/mlStorage.ts +++ /dev/null @@ -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; -} diff --git a/web/apps/photos/src/worker/ml.worker.ts b/web/apps/photos/src/worker/ml.worker.ts index a66188d43..33c2e5583 100644 --- a/web/apps/photos/src/worker/ml.worker.ts +++ b/web/apps/photos/src/worker/ml.worker.ts @@ -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(); diff --git a/web/docs/dev.md b/web/docs/dev.md index 58841e9e0..0b980c1e2 100644 --- a/web/docs/dev.md +++ b/web/docs/dev.md @@ -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 diff --git a/web/package.json b/web/package.json index c4b3172c8..58515a959 100644 --- a/web/package.json +++ b/web/package.json @@ -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" diff --git a/web/packages/shared/apps/types.ts b/web/packages/shared/apps/types.ts index 364342516..fa2f97232 100644 --- a/web/packages/shared/apps/types.ts +++ b/web/packages/shared/apps/types.ts @@ -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; diff --git a/web/packages/shared/next/pages/_document.tsx b/web/packages/shared/next/pages/_document.tsx index a18df1a2a..77fa25197 100644 --- a/web/packages/shared/next/pages/_document.tsx +++ b/web/packages/shared/next/pages/_document.tsx @@ -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, - }; -}; diff --git a/web/packages/shared/themes/createEmotionCache.ts b/web/packages/shared/themes/createEmotionCache.ts deleted file mode 100644 index 216e285f6..000000000 --- a/web/packages/shared/themes/createEmotionCache.ts +++ /dev/null @@ -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 }); -} diff --git a/web/yarn.lock b/web/yarn.lock index 02262b17e..af80a9a98 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -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"