[web] General refactoring (#6072)

This commit is contained in:
Manav Rathi
2025-05-28 13:15:12 +05:30
committed by GitHub
11 changed files with 132 additions and 217 deletions

View File

@@ -20,7 +20,6 @@ import {
} from "ente-new/photos/components/Tiles";
import type { CollectionSummary } from "ente-new/photos/services/collection/ui";
import { CollectionsSortBy } from "ente-new/photos/services/collection/ui";
import { FlexWrapper } from "ente-shared/components/Container";
import { t } from "i18next";
import memoize from "memoize-one";
import React, { useEffect, useRef, useState } from "react";
@@ -168,7 +167,7 @@ const AlbumsRow = React.memo(
const collectionRow = collectionRowList[index];
return (
<div style={style}>
<FlexWrapper gap={"4px"} padding={"16px"}>
<Stack direction="row" sx={{ p: 2, gap: 0.5 }}>
{collectionRow.map((item: any) => (
<AlbumCard
isScrolling={isScrolling}
@@ -177,7 +176,7 @@ const AlbumsRow = React.memo(
key={item.id}
/>
))}
</FlexWrapper>
</Stack>
</div>
);
},

View File

@@ -12,18 +12,10 @@ import Photo, { default as PhotoIcon } from "@mui/icons-material/Photo";
import PublicIcon from "@mui/icons-material/Public";
import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline";
import WorkspacesIcon from "@mui/icons-material/Workspaces";
import {
Dialog,
DialogProps,
FormHelperText,
Stack,
styled,
Typography,
} from "@mui/material";
import { Dialog, DialogProps, Stack, styled, Typography } from "@mui/material";
import NumberAvatar from "@mui/material/Avatar";
import TextField from "@mui/material/TextField";
import Avatar from "components/pages/gallery/Avatar";
import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton";
import { LoadingButton } from "ente-base/components/mui/LoadingButton";
import {
NestedSidebarDrawer,
@@ -43,6 +35,7 @@ import { useClipboardCopy } from "ente-base/components/utils/hooks";
import { useModalVisibility } from "ente-base/components/utils/modal";
import { useBaseContext } from "ente-base/context";
import { sharedCryptoWorker } from "ente-base/crypto";
import { isHTTP4xxError } from "ente-base/http";
import { formattedDateTime } from "ente-base/i18n-date";
import log from "ente-base/log";
import { appendCollectionKeyToShareURL } from "ente-gallery/services/share";
@@ -522,6 +515,9 @@ const AddParticipant: React.FC<AddParticipantProps> = ({
await shareCollection(collection, email, type);
await syncWithRemote(false, true);
} catch (e) {
if (isHTTP4xxError(e)) {
throw new Error(t("sharing_user_does_not_exist"));
}
const errorMessage = handleSharingErrors(e);
throw new Error(errorMessage);
}
@@ -549,15 +545,11 @@ const AddParticipant: React.FC<AddParticipantProps> = ({
onClose={onClose}
callback={collectionShare}
optionsList={nonSharedEmails}
placeholder={t("enter_email")}
fieldType="email"
buttonText={
type == "VIEWER"
? t("add_viewers")
: t("add_collaborators")
}
submitButtonProps={{ size: "large", sx: { mt: 1, mb: 2 } }}
disableAutoFocus
/>
</Stack>
</NestedSidebarDrawer>
@@ -571,27 +563,12 @@ interface AddParticipantFormValues {
interface AddParticipantFormProps {
callback: (props: { email?: string; emails?: string[] }) => Promise<void>;
fieldType: "text" | "email" | "password";
placeholder: string;
buttonText: string;
submitButtonProps?: any;
initialValue?: string;
secondaryButtonAction?: () => void;
disableAutoFocus?: boolean;
hiddenPreInput?: any;
caption?: any;
hiddenPostInput?: any;
autoComplete?: string;
blockButton?: boolean;
hiddenLabel?: boolean;
onClose?: () => void;
optionsList?: string[];
}
const AddParticipantForm: React.FC<AddParticipantFormProps> = (props) => {
const { submitButtonProps } = props;
const { sx: buttonSx, ...restSubmitButtonProps } = submitButtonProps ?? {};
const [loading, SetLoading] = useState(false);
const submitForm = async (
@@ -615,17 +592,10 @@ const AddParticipantForm: React.FC<AddParticipantFormProps> = (props) => {
};
const validationSchema = useMemo(() => {
switch (props.fieldType) {
case "text":
return Yup.object().shape({
inputValue: Yup.string().required(t("required")),
});
case "email":
return Yup.object().shape({
inputValue: Yup.string().email(t("invalid_email_error")),
});
}
}, [props.fieldType]);
return Yup.object().shape({
inputValue: Yup.string().email(t("invalid_email_error")),
});
}, []);
const handleInputFieldClick = (setFieldValue) => {
setFieldValue("selectedOptions", []);
@@ -633,10 +603,7 @@ const AddParticipantForm: React.FC<AddParticipantFormProps> = (props) => {
return (
<Formik<AddParticipantFormValues>
initialValues={{
inputValue: props.initialValue ?? "",
selectedOptions: [],
}}
initialValues={{ inputValue: "", selectedOptions: [] }}
onSubmit={submitForm}
validationSchema={validationSchema}
validateOnChange={false}
@@ -651,31 +618,25 @@ const AddParticipantForm: React.FC<AddParticipantFormProps> = (props) => {
}) => (
<form noValidate onSubmit={handleSubmit}>
<Stack sx={{ gap: "24px", py: "20px", px: "12px" }}>
{props.hiddenPreInput}
<Stack>
<RowButtonGroupTitle>
{t("add_new_email")}
</RowButtonGroupTitle>
<TextField
sx={{ marginTop: 0 }}
hiddenLabel={props.hiddenLabel}
fullWidth
type={props.fieldType}
id={props.fieldType}
id={"email"}
name={"email"}
type={"email"}
label={t("enter_email")}
sx={{ mt: 0 }}
disabled={loading}
onChange={handleChange("inputValue")}
onClick={() =>
handleInputFieldClick(setFieldValue)
}
name={props.fieldType}
{...(props.hiddenLabel
? { placeholder: props.placeholder }
: { label: props.placeholder })}
error={Boolean(errors.inputValue)}
helperText={errors.inputValue}
value={values.inputValue}
disabled={loading}
autoFocus={!props.disableAutoFocus}
autoComplete={props.autoComplete}
/>
</Stack>
@@ -735,51 +696,19 @@ const AddParticipantForm: React.FC<AddParticipantFormProps> = (props) => {
</RowButtonGroup>
</Stack>
)}
<FormHelperText
sx={{
position: "relative",
top: errors.inputValue ? "-22px" : "0",
float: "right",
padding: "0 8px",
}}
>
{props.caption}
</FormHelperText>
{props.hiddenPostInput}
</Stack>
<FlexWrapper
px={"8px"}
justifyContent={"center"}
flexWrap={props.blockButton ? "wrap-reverse" : "nowrap"}
flexWrap={"nowrap"}
>
<Stack sx={{ px: "8px", width: "100%" }}>
{props.secondaryButtonAction && (
<FocusVisibleButton
onClick={props.secondaryButtonAction}
fullWidth
color="secondary"
sx={{
"&&&": {
mt: !props.blockButton ? 2 : 0.5,
mb: !props.blockButton ? 4 : 0,
mr: !props.blockButton ? 1 : 0,
...buttonSx,
},
}}
{...restSubmitButtonProps}
>
{t("cancel")}
</FocusVisibleButton>
)}
<LoadingButton
type="submit"
color="accent"
fullWidth
loading={loading}
sx={{ mt: 2, mb: 4 }}
{...restSubmitButtonProps}
sx={{ mt: 4, mb: 4 }}
>
{props.buttonText}
</LoadingButton>

View File

@@ -27,6 +27,7 @@ import { useBaseContext } from "ente-base/context";
import { basename, dirname, joinPath } from "ente-base/file-name";
import log from "ente-base/log";
import type { CollectionMapping, Electron, ZipItem } from "ente-base/types/ipc";
import { type UploadTypeSelectorIntent } from "ente-gallery/components/Upload";
import { useFileInput } from "ente-gallery/components/utils/use-file-input";
import {
groupItemsBasedOnParentFolder,
@@ -77,8 +78,6 @@ import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
import { SetCollectionNamerAttributes } from "./Collections/CollectionNamer";
import { UploadProgress } from "./UploadProgress";
export type UploadTypeSelectorIntent = "upload" | "import" | "collect";
interface UploadProps {
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
closeUploadTypeSelector: () => void;
@@ -551,7 +550,7 @@ export const Upload: React.FC<UploadProps> = ({
const preCollectionCreationAction = async () => {
props.onCloseCollectionSelector?.();
props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload());
props.setShouldDisableDropzone(uploadManager.isUploadInProgress());
setUploadPhase("preparing");
setUploadProgressView(true);
};

View File

@@ -15,7 +15,7 @@ import {
} from "components/FilesDownloadProgress";
import { FixCreationTime } from "components/FixCreationTime";
import { Sidebar } from "components/Sidebar";
import { Upload, type UploadTypeSelectorIntent } from "components/Upload";
import { Upload } from "components/Upload";
import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions";
import { sessionExpiredDialogAttributes } from "ente-accounts/components/utils/dialog";
import { stashRedirect } from "ente-accounts/services/redirect";
@@ -37,6 +37,7 @@ import {
masterKeyFromSessionIfLoggedIn,
} from "ente-base/session";
import { FullScreenDropZone } from "ente-gallery/components/FullScreenDropZone";
import { type UploadTypeSelectorIntent } from "ente-gallery/components/Upload";
import { type Collection } from "ente-media/collection";
import { type EnteFile } from "ente-media/file";
import {
@@ -785,9 +786,7 @@ const Page: React.FC = () => {
};
const openUploader = (intent?: UploadTypeSelectorIntent) => {
if (!uploadManager.shouldAllowNewUpload()) {
return;
}
if (uploadManager.isUploadInProgress()) return;
setUploadTypeSelectorView(true);
setUploadTypeSelectorIntent(intent ?? "upload");
};
@@ -1095,8 +1094,8 @@ const Page: React.FC = () => {
!hiddenFiles?.length &&
activeCollectionID === ALL_SECTION ? (
<GalleryEmptyState
openUploader={openUploader}
shouldAllowNewUpload={uploadManager.shouldAllowNewUpload()}
isUploadInProgress={uploadManager.isUploadInProgress()}
onUpload={openUploader}
/>
) : !isInSearchMode &&
!isFirstLoad &&
@@ -1218,7 +1217,7 @@ const SidebarButton: React.FC<ButtonishProps> = ({ onClick }) => (
);
const UploadButton: React.FC<ButtonishProps> = ({ onClick }) => {
const disabled = !uploadManager.shouldAllowNewUpload();
const disabled = uploadManager.isUploadInProgress();
const isSmallWidth = useIsSmallWidth();
const icon = <FileUploadOutlinedIcon />;

View File

@@ -558,7 +558,7 @@ const EnteLogoLink = styled("a")(({ theme }) => ({
}));
const AddPhotosButton: React.FC<ButtonishProps> = ({ onClick }) => {
const disabled = !uploadManager.shouldAllowNewUpload();
const disabled = uploadManager.isUploadInProgress();
const isSmallWidth = useIsSmallWidth();
const icon = <AddPhotoAlternateOutlinedIcon />;
@@ -585,7 +585,7 @@ const AddPhotosButton: React.FC<ButtonishProps> = ({ onClick }) => {
* shrink on mobile sized screens.
*/
const AddMorePhotosButton: React.FC<ButtonishProps> = ({ onClick }) => {
const disabled = !uploadManager.shouldAllowNewUpload();
const disabled = uploadManager.isUploadInProgress();
return (
<FocusVisibleButton

View File

@@ -682,8 +682,13 @@ class UploadManager {
this.onUploadFile(decryptedFile);
}
public shouldAllowNewUpload = () => {
return !this.uploadInProgress || watcher.isUploadRunning();
/**
* `true` if an upload is currently in-progress (either a bunch of files
* directly uploaded by the user, or files being uploaded by the folder
* watch functionality).
*/
public isUploadInProgress = () => {
return this.uploadInProgress || watcher.isUploadRunning();
};
}

View File

@@ -22,7 +22,6 @@ import { ShowHidePasswordInputAdornment } from "ente-base/components/mui/Passwor
import { isMuseumHTTPError } from "ente-base/http";
import log from "ente-base/log";
import { setLSUser } from "ente-shared//storage/localStorage";
import { VerticallyCentered } from "ente-shared/components/Container";
import {
generateAndSaveIntermediateKeyAttributes,
saveKeyInSessionStore,
@@ -149,7 +148,7 @@ export const SignUpContents: React.FC<SignUpContentsProps> = ({
handleSubmit,
}): React.JSX.Element => (
<form noValidate onSubmit={handleSubmit}>
<VerticallyCentered sx={{ mb: 2 }}>
<Stack sx={{ mb: 2 }}>
<TextField
fullWidth
id="email"
@@ -290,7 +289,7 @@ export const SignUpContents: React.FC<SignUpContentsProps> = ({
}
/>
</FormGroup>
</VerticallyCentered>
</Stack>
<Box sx={{ mb: 1 }}>
<LoadingButton
fullWidth

View File

@@ -1,4 +1,4 @@
import { Alert, Box, TextField } from "@mui/material";
import { Alert, Box, Stack, TextField } from "@mui/material";
import {
AccountsPageContents,
AccountsPageFooter,
@@ -10,7 +10,6 @@ import { LinkButton } from "ente-base/components/LinkButton";
import { LoadingButton } from "ente-base/components/mui/LoadingButton";
import { isHTTPErrorWithStatus } from "ente-base/http";
import log from "ente-base/log";
import { VerticallyCentered } from "ente-shared/components/Container";
import { getData, setLSUser } from "ente-shared/storage/localStorage";
import { Formik, type FormikHelpers } from "formik";
import { t } from "i18next";
@@ -141,7 +140,7 @@ const ChangeEmailForm: React.FC = () => {
</Alert>
)}
<form noValidate onSubmit={handleSubmit}>
<VerticallyCentered>
<Stack>
<TextField
fullWidth
type="email"
@@ -177,7 +176,7 @@ const ChangeEmailForm: React.FC = () => {
>
{!ottInputVisible ? t("send_otp") : t("verify")}
</LoadingButton>
</VerticallyCentered>
</Stack>
</form>
<AccountsPageFooter>

View File

@@ -0,0 +1,7 @@
/**
* The upload can be triggered by different buttons and flows in the UI, each of
* which is referred to as an "intent".
*
* The "intent" does not change the eventual upload outcome, only the UX flow.
*/
export type UploadTypeSelectorIntent = "upload" | "import" | "collect";

View File

@@ -7,11 +7,17 @@
* there.
*/
import { Paper, Stack, Typography } from "@mui/material";
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternateOutlined";
import FolderIcon from "@mui/icons-material/FolderOutlined";
import { Paper, Stack, styled, Typography } from "@mui/material";
import { CenteredFill } from "ente-base/components/containers";
import { EnteLogo } from "ente-base/components/EnteLogo";
import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton";
import { type UploadTypeSelectorIntent } from "ente-gallery/components/Upload";
import type { SearchSuggestion } from "ente-new/photos/services/search/types";
import { t } from "i18next";
import React, { useState } from "react";
import { Trans } from "react-i18next";
import { enableML } from "../../services/ml";
import { EnableML, FaceConsent } from "../sidebar/MLSettings";
import { useMLStatusSnapshot } from "../utils/use-snapshot";
@@ -51,93 +57,84 @@ export const SearchResultsHeader: React.FC<SearchResultsHeaderProps> = ({
</GalleryItemsHeaderAdapter>
);
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternateOutlined";
import FolderIcon from "@mui/icons-material/FolderOutlined";
import { Button, styled } from "@mui/material";
import { EnteLogo } from "ente-base/components/EnteLogo";
import {
FlexWrapper,
VerticallyCentered,
} from "ente-shared/components/Container";
import { Trans } from "react-i18next";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export function GalleryEmptyState({ openUploader, shouldAllowNewUpload }) {
return (
<Wrapper>
<Stack sx={{ flex: "none", paddingBlock: "12px 32px" }}>
<VerticallyCentered sx={{ flex: "none" }}>
<Typography
variant="h3"
sx={{
color: "text.muted",
userSelect: "none",
marginBlockEnd: 1,
svg: {
color: "text.base",
verticalAlign: "middle",
marginBlockEnd: "2px",
},
}}
>
<Trans
i18nKey="welcome_to_ente_title"
components={{ a: <EnteLogo /> }}
/>
</Typography>
<Typography variant="h2">
{t("welcome_to_ente_subtitle")}
</Typography>
</VerticallyCentered>
</Stack>
<NonDraggableImage
height={287.57}
alt=""
src="/images/empty-state/ente_duck.png"
srcSet="/images/empty-state/ente_duck@2x.png, /images/empty-state/ente_duck@3x.png"
/>
<VerticallyCentered paddingTop={1.5} paddingBottom={1.5}>
<Button
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
style={{ cursor: !shouldAllowNewUpload && "not-allowed" }}
color="accent"
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
onClick={() => openUploader("upload")}
disabled={!shouldAllowNewUpload}
sx={{ mt: 1.5, p: 1, width: 320, borderRadius: 0.5 }}
>
<FlexWrapper sx={{ gap: 1 }} justifyContent="center">
<AddPhotoAlternateIcon />
{t("upload_first_photo")}
</FlexWrapper>
</Button>
<Button
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
style={{ cursor: !shouldAllowNewUpload && "not-allowed" }}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
onClick={() => openUploader("import")}
disabled={!shouldAllowNewUpload}
sx={{ mt: 1.5, p: 1, width: 320, borderRadius: 0.5 }}
>
<FlexWrapper sx={{ gap: 1 }} justifyContent="center">
<FolderIcon />
{t("import_your_folders")}
</FlexWrapper>
</Button>
</VerticallyCentered>
</Wrapper>
);
interface GalleryEmptyStateProps {
/**
* If `true`, then an upload is already in progress (the empty state will
* then disable the prompts for uploads).
*/
isUploadInProgress: boolean;
/**
* Called when the user selects one of the upload buttons. It is passed the
* "intent" of the user.
*/
onUpload: (intent: UploadTypeSelectorIntent) => void;
}
const Wrapper = styled("div")`
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
`;
export const GalleryEmptyState: React.FC<GalleryEmptyStateProps> = ({
isUploadInProgress,
onUpload,
}) => (
<Stack sx={{ alignItems: "center" }}>
<Stack
sx={{
alignItems: "center",
textAlign: "center",
paddingBlock: "12px 32px",
userSelect: "none",
}}
>
<Typography
variant="h3"
sx={{
color: "text.muted",
mb: 1,
svg: {
color: "text.base",
verticalAlign: "middle",
mb: "2px",
},
}}
>
<Trans
i18nKey="welcome_to_ente_title"
components={{ a: <EnteLogo /> }}
/>
</Typography>
<Typography variant="h2">
{t("welcome_to_ente_subtitle")}
</Typography>
</Stack>
<NonDraggableImage
height={287.57}
alt=""
src="/images/empty-state/ente_duck.png"
srcSet="/images/empty-state/ente_duck@2x.png, /images/empty-state/ente_duck@3x.png"
/>
<Stack sx={{ py: 3, width: 320, gap: 1 }}>
<FocusVisibleButton
color="accent"
onClick={() => onUpload("upload")}
disabled={isUploadInProgress}
sx={{ p: 1 }}
>
<Stack direction="row" sx={{ gap: 1, alignItems: "center" }}>
<AddPhotoAlternateIcon />
{t("upload_first_photo")}
</Stack>
</FocusVisibleButton>
<FocusVisibleButton
onClick={() => onUpload("import")}
disabled={isUploadInProgress}
sx={{ p: 1 }}
>
<Stack direction="row" sx={{ gap: 1, alignItems: "center" }}>
<FolderIcon />
{t("import_your_folders")}
</Stack>
</FocusVisibleButton>
</Stack>
</Stack>
);
/**
* Prevent the image from being selected _and_ dragged, since dragging it

View File

@@ -1,25 +1,7 @@
import { Box, styled } from "@mui/material";
export const VerticallyCentered = styled(Box)`
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
overflow: auto;
`;
export const FlexWrapper = styled(Box)`
display: flex;
width: 100%;
align-items: center;
`;
/**
* Deprecated, use {@link SpacedRow} from ente-base/components/mui/container
* instead
*/
export const SpaceBetweenFlex = styled(FlexWrapper)`
justify-content: space-between;
`;