[web] General (non-functional) rearrangement of code (#3957)

This commit is contained in:
Manav Rathi
2024-11-06 11:36:34 +05:30
committed by GitHub
44 changed files with 550 additions and 532 deletions

View File

@@ -2,12 +2,12 @@ import { staticAppTitle } from "@/base/app";
import { CustomHead } from "@/base/components/Head";
import { AttributedMiniDialog } from "@/base/components/MiniDialog";
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
import { Overlay } from "@/base/components/mui/Container";
import { AppNavbar } from "@/base/components/Navbar";
import { useAttributedMiniDialog } from "@/base/components/utils/dialog";
import { setupI18n } from "@/base/i18n";
import { disableDiskLogs } from "@/base/log";
import { logUnhandledErrorsAndRejections } from "@/base/log-web";
import { Overlay } from "@ente/shared/components/Container";
import { getTheme } from "@ente/shared/themes";
import { THEME_COLOR } from "@ente/shared/themes/constants";
import { CssBaseline } from "@mui/material";

View File

@@ -4,6 +4,7 @@ import { clientPackageName, staticAppTitle } from "@/base/app";
import { CustomHead } from "@/base/components/Head";
import { AttributedMiniDialog } from "@/base/components/MiniDialog";
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
import { Overlay } from "@/base/components/mui/Container";
import { AppNavbar } from "@/base/components/Navbar";
import {
genericErrorDialogAttributes,
@@ -15,7 +16,6 @@ import {
logUnhandledErrorsAndRejections,
} from "@/base/log-web";
import { ensure } from "@/utils/ensure";
import { Overlay } from "@ente/shared/components/Container";
import { MessageContainer } from "@ente/shared/components/MessageContainer";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
import HTTPService from "@ente/shared/network/HTTPService";

View File

@@ -5,6 +5,7 @@
"dependencies": {
"@/accounts": "*",
"@/base": "*",
"@/gallery": "*",
"@/media": "*",
"@/new": "*",
"@ente/eslint-config": "*",

View File

@@ -1,4 +1,5 @@
import { useIsSmallWidth } from "@/base/hooks";
import { getFamilyPlanAdmin } from "@/new/photos/services/user";
import { AppContext } from "@/new/photos/types/context";
import {
FlexWrapper,
@@ -9,7 +10,6 @@ import { Box, Button, Dialog, DialogContent, Typography } from "@mui/material";
import { t } from "i18next";
import { useContext } from "react";
import billingService from "services/billingService";
import { getFamilyPlanAdmin } from "utils/user/family";
export function MemberSubscriptionManage({ open, userDetails, onClose }) {
const { setDialogMessage } = useContext(AppContext);

View File

@@ -1,9 +1,10 @@
import { isDesktop } from "@/base/app";
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
import { Overlay } from "@/base/components/mui/Container";
import { lowercaseExtension } from "@/base/file";
import log from "@/base/log";
import type { LoadedLivePhotoSourceURL } from "@/media/file";
import { type EnteFile, fileLogID } from "@/media/file";
import { fileLogID, type EnteFile } from "@/media/file";
import { FileType } from "@/media/file-type";
import { isHEICExtension, needsJPEGConversion } from "@/media/formats";
import downloadManager from "@/new/photos/services/download";
@@ -25,7 +26,16 @@ import FullscreenOutlinedIcon from "@mui/icons-material/FullscreenOutlined";
import InfoIcon from "@mui/icons-material/InfoOutlined";
import ReplayIcon from "@mui/icons-material/Replay";
import ZoomInOutlinedIcon from "@mui/icons-material/ZoomInOutlined";
import { Box, Button, styled } from "@mui/material";
import {
Box,
Button,
CircularProgress,
Paper,
styled,
Typography,
type CircularProgressProps,
} from "@mui/material";
import Notification from "components/Notification";
import { t } from "i18next";
import isElectron from "is-electron";
import { GalleryContext } from "pages/gallery";
@@ -48,9 +58,6 @@ import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
import { getTrashFileMessage } from "utils/ui";
import { FileInfo, type FileInfoExif, type FileInfoProps } from "./FileInfo";
import ImageEditorOverlay from "./ImageEditorOverlay";
import CircularProgressWithLabel from "./styledComponents/CircularProgressWithLabel";
import { ConversionFailedNotification } from "./styledComponents/ConversionFailedNotification";
import { LivePhotoBtnContainer } from "./styledComponents/LivePhotoBtn";
interface PhotoswipeFullscreenAPI {
enter: () => void;
@@ -983,3 +990,60 @@ function PhotoViewer(props: PhotoViewerProps) {
}
export default PhotoViewer;
function CircularProgressWithLabel(
props: CircularProgressProps & { value: number },
) {
return (
<>
<CircularProgress variant="determinate" {...props} color="accent" />
<Overlay
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "40px",
}}
>
<Typography
variant="mini"
component="div"
color="text.secondary"
>{`${Math.round(props.value)}%`}</Typography>
</Overlay>
</>
);
}
interface ConversionFailedNotificationProps {
open: boolean;
onClose: () => void;
onClick: () => void;
}
const ConversionFailedNotification: React.FC<
ConversionFailedNotificationProps
> = ({ open, onClose, onClick }) => {
return (
<Notification
open={open}
onClose={onClose}
attributes={{
variant: "secondary",
subtext: t("CONVERSION_FAILED_NOTIFICATION_MESSAGE"),
onClick: onClick,
}}
horizontal="right"
vertical="bottom"
sx={{ zIndex: 4000 }}
/>
);
};
const LivePhotoBtnContainer = styled(Paper)`
border-radius: 4px;
position: absolute;
bottom: 10vh;
right: 6vh;
z-index: 10;
`;

View File

@@ -1,32 +0,0 @@
import { Overlay } from "@ente/shared/components/Container";
import {
CircularProgress,
Typography,
type CircularProgressProps,
} from "@mui/material";
function CircularProgressWithLabel(
props: CircularProgressProps & { value: number },
) {
return (
<>
<CircularProgress variant="determinate" {...props} color="accent" />
<Overlay
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "40px",
}}
>
<Typography
variant="mini"
component="div"
color="text.secondary"
>{`${Math.round(props.value)}%`}</Typography>
</Overlay>
</>
);
}
export default CircularProgressWithLabel;

View File

@@ -1,29 +0,0 @@
import Notification from "components/Notification";
import { t } from "i18next";
interface Iprops {
open: boolean;
onClose: () => void;
onClick: () => void;
}
export const ConversionFailedNotification = ({
open,
onClose,
onClick,
}: Iprops) => {
return (
<Notification
open={open}
onClose={onClose}
attributes={{
variant: "secondary",
subtext: t("CONVERSION_FAILED_NOTIFICATION_MESSAGE"),
onClick: onClick,
}}
horizontal="right"
vertical="bottom"
sx={{ zIndex: 4000 }}
/>
);
};

View File

@@ -1,9 +0,0 @@
import { Paper, styled } from "@mui/material";
export const LivePhotoBtnContainer = styled(Paper)`
border-radius: 4px;
position: absolute;
bottom: 10vh;
right: 6vh;
z-index: 10;
`;

View File

@@ -1,5 +0,0 @@
import { styled } from "@mui/material";
export const Pre = styled("pre")`
color: #aaa;
padding: 7px 15px;
`;

View File

@@ -0,0 +1,358 @@
import { Overlay } from "@/base/components/mui/Container";
import type { ButtonishProps } from "@/new/photos/components/mui";
import {
hasNonAdminFamilyMembers,
isPartOfFamily,
} from "@/new/photos/services/user";
import { bytesInGB, formattedStorageByteSize } from "@/new/photos/utils/units";
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import CircleIcon from "@mui/icons-material/Circle";
import {
Box,
LinearProgress,
Skeleton,
Stack,
Typography,
styled,
type LinearProgressProps,
} from "@mui/material";
import { t } from "i18next";
import type React from "react";
import { useMemo } from "react";
import type { UserDetails } from "types/user";
interface SubscriptionCardProps {
userDetails: UserDetails;
onClick: () => void;
}
export const SubscriptionCard: React.FC<SubscriptionCardProps> = ({
userDetails,
onClick,
}) => {
if (!userDetails) {
return (
<Skeleton
animation="wave"
variant="rectangular"
height={152}
sx={{ borderRadius: "8px" }}
/>
);
}
return (
<Box position="relative">
<BackgroundOverlay />
<SubscriptionCardContentOverlay userDetails={userDetails} />
<ClickOverlay onClick={onClick} />
</Box>
);
};
const BackgroundOverlay: React.FC = () => {
return (
<img
style={{ aspectRatio: "2/1" }}
width="100%"
src="/images/subscription-card-background/1x.png"
srcSet="/images/subscription-card-background/2x.png 2x,
/images/subscription-card-background/3x.png 3x"
/>
);
};
const ClickOverlay: React.FC<ButtonishProps> = ({ onClick }) => (
<Overlay
sx={{
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
cursor: "pointer",
}}
onClick={onClick}
>
<ChevronRightIcon />
</Overlay>
);
interface SubscriptionCardContentOverlayProps {
userDetails: UserDetails;
}
export const SubscriptionCardContentOverlay: React.FC<
SubscriptionCardContentOverlayProps
> = ({ userDetails }) => {
return (
<Overlay>
<SpaceBetweenFlex
height={"100%"}
flexDirection={"column"}
padding={"20px 16px"}
>
{hasNonAdminFamilyMembers(userDetails.familyData) ? (
<FamilySubscriptionCardContent userDetails={userDetails} />
) : (
<IndividualSubscriptionCardContent
userDetails={userDetails}
/>
)}
</SpaceBetweenFlex>
</Overlay>
);
};
interface IndividualSubscriptionCardContentProps {
userDetails: UserDetails;
}
const IndividualSubscriptionCardContent: React.FC<
IndividualSubscriptionCardContentProps
> = ({ userDetails }) => {
const totalStorage =
userDetails.subscription.storage + (userDetails.storageBonus ?? 0);
return (
<>
<StorageSection storage={totalStorage} usage={userDetails.usage} />
<IndividualUsageSection
usage={userDetails.usage}
fileCount={userDetails.fileCount}
storage={totalStorage}
/>
</>
);
};
const MobileSmallBox = styled(Box)`
display: none;
@media (max-width: 359px) {
display: block;
}
`;
const DefaultBox = styled(Box)`
display: none;
@media (min-width: 360px) {
display: block;
}
`;
interface StorageSectionProps {
usage: number;
storage: number;
}
const StorageSection: React.FC<StorageSectionProps> = ({ usage, storage }) => {
return (
<Box width="100%">
<Typography variant="small" color={"text.muted"}>
{t("STORAGE")}
</Typography>
<DefaultBox>
<Typography
fontWeight={"bold"}
sx={{ fontSize: "24px", lineHeight: "30px" }}
>
{`${formattedStorageByteSize(usage, { round: true })} ${t(
"OF",
)} ${formattedStorageByteSize(storage)} ${t("USED")}`}
</Typography>
</DefaultBox>
<MobileSmallBox>
<Typography
fontWeight={"bold"}
sx={{ fontSize: "24px", lineHeight: "30px" }}
>
{`${bytesInGB(usage)} / ${bytesInGB(storage)} ${t("storage_unit.gb")} ${t("USED")}`}
</Typography>
</MobileSmallBox>
</Box>
);
};
interface IndividualUsageSectionProps {
usage: number;
fileCount: number;
storage: number;
}
const IndividualUsageSection: React.FC<IndividualUsageSectionProps> = ({
usage,
storage,
fileCount,
}) => {
// [Note: Fallback translation for languages with multiple plurals]
//
// Languages like Polish and Arabian have multiple plural forms, and
// currently i18n falls back to the base language translation instead of the
// "_other" form if all the plural forms are not listed out.
//
// As a workaround, name the _other form as the unprefixed name. That is,
// instead of calling the most general plural form as foo_count_other, call
// it foo_count (To keep our heads straight, we adopt the convention that
// all such pluralizable strings use the _count suffix, but that's not a
// requirement from the library).
return (
<Box width="100%">
<UsageBar used={usage} total={storage} />
<SpaceBetweenFlex
sx={{
marginTop: 1.5,
}}
>
<Typography variant="mini">{`${formattedStorageByteSize(
storage - usage,
)} ${t("FREE")}`}</Typography>
<Typography variant="mini" fontWeight={"bold"}>
{t("photos_count", { count: fileCount ?? 0 })}
</Typography>
</SpaceBetweenFlex>
</Box>
);
};
interface FamilySubscriptionCardContentProps {
userDetails: UserDetails;
}
const FamilySubscriptionCardContent: React.FC<
FamilySubscriptionCardContentProps
> = ({ userDetails }) => {
const totalUsage = useMemo(() => {
if (isPartOfFamily(userDetails.familyData)) {
return userDetails.familyData.members.reduce(
(sum, currentMember) => sum + currentMember.usage,
0,
);
} else {
return userDetails.usage;
}
}, [userDetails]);
const totalStorage =
userDetails.familyData.storage + (userDetails.storageBonus ?? 0);
return (
<>
<StorageSection storage={totalStorage} usage={totalUsage} />
<FamilyUsageSection
userUsage={userDetails.usage}
fileCount={userDetails.fileCount}
totalUsage={totalUsage}
totalStorage={totalStorage}
/>
</>
);
};
interface FamilyUsageSectionProps {
userUsage: number;
totalUsage: number;
fileCount: number;
totalStorage: number;
}
const FamilyUsageSection: React.FC<FamilyUsageSectionProps> = ({
userUsage,
totalUsage,
fileCount,
totalStorage,
}) => {
return (
<Box width="100%">
<FamilyUsageBar
totalUsage={totalUsage}
userUsage={userUsage}
totalStorage={totalStorage}
/>
<SpaceBetweenFlex
sx={{
marginTop: 1.5,
}}
>
<Stack direction={"row"} spacing={1.5}>
<Legend label={t("YOU")} color="text.base" />
<Legend label={t("FAMILY")} color="text.muted" />
</Stack>
<Typography variant="mini" fontWeight={"bold"}>
{t("photos_count", { count: fileCount ?? 0 })}
</Typography>
</SpaceBetweenFlex>
</Box>
);
};
interface FamilyUsageBarProps {
userUsage: number;
totalUsage: number;
totalStorage: number;
}
const FamilyUsageBar: React.FC<FamilyUsageBarProps> = ({
userUsage,
totalUsage,
totalStorage,
}) => (
<Box sx={{ position: "relative", width: "100%" }}>
<UsageBar
used={userUsage}
total={totalStorage}
sx={{ backgroundColor: "transparent" }}
/>
<UsageBar
used={totalUsage}
total={totalStorage}
sx={{
position: "absolute",
top: 0,
zIndex: 1,
".MuiLinearProgress-bar ": {
backgroundColor: "text.muted",
},
width: "100%",
}}
/>
</Box>
);
type UsageBarProps = Pick<LinearProgressProps, "sx"> & {
used: number;
total: number;
};
const UsageBar: React.FC<UsageBarProps> = ({ used, total, sx }) => (
<UsageBar_
variant="determinate"
sx={sx}
value={Math.min(used / total, 1) * 100}
/>
);
const UsageBar_ = styled(LinearProgress)(() => ({
".MuiLinearProgress-bar": {
borderRadius: "2px",
},
borderRadius: "2px",
backgroundColor: "rgba(255, 255, 255, 0.2)",
}));
interface LegendProps {
label: string;
color: string;
}
const Legend: React.FC<LegendProps> = ({ label, color }) => (
<Box sx={{ display: "flex", alignItems: "center" }}>
<LegendDot sx={{ color }} />
<Typography variant="mini" fontWeight={"bold"}>
{label}
</Typography>
</Box>
);
const LegendDot = styled(CircleIcon)`
font-size: 8.71px;
margin: 0;
margin-inline-end: 4px;
color: inherit;
`;

View File

@@ -1,35 +0,0 @@
import { useMemo } from "react";
import { UserDetails } from "types/user";
import { isPartOfFamily } from "utils/user/family";
import StorageSection from "../storageSection";
import { FamilyUsageSection } from "./usageSection";
interface Iprops {
userDetails: UserDetails;
}
export function FamilySubscriptionCardContent({ userDetails }: Iprops) {
const totalUsage = useMemo(() => {
if (isPartOfFamily(userDetails.familyData)) {
return userDetails.familyData.members.reduce(
(sum, currentMember) => sum + currentMember.usage,
0,
);
} else {
return userDetails.usage;
}
}, [userDetails]);
const totalStorage =
userDetails.familyData.storage + (userDetails.storageBonus ?? 0);
return (
<>
<StorageSection storage={totalStorage} usage={totalUsage} />
<FamilyUsageSection
userUsage={userDetails.usage}
fileCount={userDetails.fileCount}
totalUsage={totalUsage}
totalStorage={totalStorage}
/>
</>
);
}

View File

@@ -1,42 +0,0 @@
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
import { Box, Stack, Typography } from "@mui/material";
import { t } from "i18next";
import { Legend } from "./legend";
import { FamilyUsageProgressBar } from "./progressBar";
interface Iprops {
userUsage: number;
totalUsage: number;
fileCount: number;
totalStorage: number;
}
export function FamilyUsageSection({
userUsage,
totalUsage,
fileCount,
totalStorage,
}: Iprops) {
return (
<Box width="100%">
<FamilyUsageProgressBar
totalUsage={totalUsage}
userUsage={userUsage}
totalStorage={totalStorage}
/>
<SpaceBetweenFlex
sx={{
marginTop: 1.5,
}}
>
<Stack direction={"row"} spacing={1.5}>
<Legend label={t("YOU")} color="text.base" />
<Legend label={t("FAMILY")} color="text.muted" />
</Stack>
<Typography variant="mini" fontWeight={"bold"}>
{t("photos_count", { count: fileCount ?? 0 })}
</Typography>
</SpaceBetweenFlex>
</Box>
);
}

View File

@@ -1,18 +0,0 @@
import { FlexWrapper } from "@ente/shared/components/Container";
import { Typography } from "@mui/material";
import { LegendIndicator } from "../../../styledComponents";
interface Iprops {
label: string;
color: string;
}
export function Legend({ label, color }: Iprops) {
return (
<FlexWrapper>
<LegendIndicator sx={{ color }} />
<Typography variant="mini" fontWeight={"bold"}>
{label}
</Typography>
</FlexWrapper>
);
}

View File

@@ -1,34 +0,0 @@
import { Box } from "@mui/material";
import { Progressbar } from "../../../styledComponents";
interface Iprops {
userUsage: number;
totalUsage: number;
totalStorage: number;
}
export function FamilyUsageProgressBar({
userUsage,
totalUsage,
totalStorage,
}: Iprops) {
return (
<Box position={"relative"} width="100%">
<Progressbar
sx={{ backgroundColor: "transparent" }}
value={Math.min((userUsage * 100) / totalStorage, 100)}
/>
<Progressbar
sx={{
position: "absolute",
top: 0,
zIndex: 1,
".MuiLinearProgress-bar ": {
backgroundColor: "text.muted",
},
width: "100%",
}}
value={Math.min((totalUsage * 100) / totalStorage, 100)}
/>
</Box>
);
}

View File

@@ -1,29 +0,0 @@
import { Overlay, SpaceBetweenFlex } from "@ente/shared/components/Container";
import { UserDetails } from "types/user";
import { hasNonAdminFamilyMembers } from "utils/user/family";
import { FamilySubscriptionCardContent } from "./family";
import { IndividualSubscriptionCardContent } from "./individual";
interface Iprops {
userDetails: UserDetails;
}
export function SubscriptionCardContentOverlay({ userDetails }: Iprops) {
return (
<Overlay>
<SpaceBetweenFlex
height={"100%"}
flexDirection={"column"}
padding={"20px 16px"}
>
{hasNonAdminFamilyMembers(userDetails.familyData) ? (
<FamilySubscriptionCardContent userDetails={userDetails} />
) : (
<IndividualSubscriptionCardContent
userDetails={userDetails}
/>
)}
</SpaceBetweenFlex>
</Overlay>
);
}

View File

@@ -1,22 +0,0 @@
import { UserDetails } from "types/user";
import StorageSection from "../storageSection";
import { IndividualUsageSection } from "./usageSection";
interface Iprops {
userDetails: UserDetails;
}
export function IndividualSubscriptionCardContent({ userDetails }: Iprops) {
const totalStorage =
userDetails.subscription.storage + (userDetails.storageBonus ?? 0);
return (
<>
<StorageSection storage={totalStorage} usage={userDetails.usage} />
<IndividualUsageSection
usage={userDetails.usage}
fileCount={userDetails.fileCount}
storage={totalStorage}
/>
</>
);
}

View File

@@ -1,42 +0,0 @@
import { formattedStorageByteSize } from "@/new/photos/utils/units";
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
import { Box, Typography } from "@mui/material";
import { t } from "i18next";
import { Progressbar } from "../../styledComponents";
interface Iprops {
usage: number;
fileCount: number;
storage: number;
}
export function IndividualUsageSection({ usage, storage, fileCount }: Iprops) {
// [Note: Fallback translation for languages with multiple plurals]
//
// Languages like Polish and Arabian have multiple plural forms, and
// currently i18n falls back to the base language translation instead of the
// "_other" form if all the plural forms are not listed out.
//
// As a workaround, name the _other form as the unprefixed name. That is,
// instead of calling the most general plural form as foo_count_other, call
// it foo_count (To keep our heads straight, we adopt the convention that
// all such pluralizable strings use the _count suffix, but that's not a
// requirement from the library).
return (
<Box width="100%">
<Progressbar value={Math.min((usage * 100) / storage, 100)} />
<SpaceBetweenFlex
sx={{
marginTop: 1.5,
}}
>
<Typography variant="mini">{`${formattedStorageByteSize(
storage - usage,
)} ${t("FREE")}`}</Typography>
<Typography variant="mini" fontWeight={"bold"}>
{t("photos_count", { count: fileCount ?? 0 })}
</Typography>
</SpaceBetweenFlex>
</Box>
);
}

View File

@@ -1,48 +0,0 @@
import { bytesInGB, formattedStorageByteSize } from "@/new/photos/utils/units";
import { Box, Typography, styled } from "@mui/material";
import { t } from "i18next";
const MobileSmallBox = styled(Box)`
display: none;
@media (max-width: 359px) {
display: block;
}
`;
const DefaultBox = styled(Box)`
display: none;
@media (min-width: 360px) {
display: block;
}
`;
interface Iprops {
usage: number;
storage: number;
}
export default function StorageSection({ usage, storage }: Iprops) {
return (
<Box width="100%">
<Typography variant="small" color={"text.muted"}>
{t("STORAGE")}
</Typography>
<DefaultBox>
<Typography
fontWeight={"bold"}
sx={{ fontSize: "24px", lineHeight: "30px" }}
>
{`${formattedStorageByteSize(usage, { round: true })} ${t(
"OF",
)} ${formattedStorageByteSize(storage)} ${t("USED")}`}
</Typography>
</DefaultBox>
<MobileSmallBox>
<Typography
fontWeight={"bold"}
sx={{ fontSize: "24px", lineHeight: "30px" }}
>
{`${bytesInGB(usage)} / ${bytesInGB(storage)} ${t("storage_unit.gb")} ${t("USED")}`}
</Typography>
</MobileSmallBox>
</Box>
);
}

View File

@@ -1,59 +0,0 @@
import { FlexWrapper, Overlay } from "@ente/shared/components/Container";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import { Box, Skeleton } from "@mui/material";
import { UserDetails } from "types/user";
import { SubscriptionCardContentOverlay } from "./contentOverlay";
const SUBSCRIPTION_CARD_SIZE = 152;
interface Iprops {
userDetails: UserDetails;
onClick: () => void;
}
export default function SubscriptionCard({ userDetails, onClick }: Iprops) {
if (!userDetails) {
return (
<Skeleton
animation="wave"
variant="rectangular"
height={SUBSCRIPTION_CARD_SIZE}
sx={{ borderRadius: "8px" }}
/>
);
}
return (
<Box position="relative">
<BackgroundOverlay />
<SubscriptionCardContentOverlay userDetails={userDetails} />
<ClickOverlay onClick={onClick} />
</Box>
);
}
function BackgroundOverlay() {
return (
<img
style={{ aspectRatio: "2/1" }}
width="100%"
src="/images/subscription-card-background/1x.png"
srcSet="/images/subscription-card-background/2x.png 2x,
/images/subscription-card-background/3x.png 3x"
/>
);
}
function ClickOverlay({ onClick }) {
return (
<Overlay display="flex">
<FlexWrapper
onClick={onClick}
justifyContent={"flex-end"}
sx={{ cursor: "pointer" }}
>
<ChevronRightIcon />
</FlexWrapper>
</Overlay>
);
}

View File

@@ -1,27 +0,0 @@
import CircleIcon from "@mui/icons-material/Circle";
import { LinearProgress, styled } from "@mui/material";
export const Progressbar = styled(LinearProgress)(() => ({
".MuiLinearProgress-bar": {
borderRadius: "2px",
},
borderRadius: "2px",
backgroundColor: "rgba(255, 255, 255, 0.2)",
}));
Progressbar.defaultProps = {
variant: "determinate",
};
const DotSeparator = styled(CircleIcon)`
font-size: 4px;
margin: 0 ${({ theme }) => theme.spacing(1)};
color: inherit;
`;
export const LegendIndicator = styled(DotSeparator)`
font-size: 8.71px;
margin: 0;
margin-right: 4px;
color: inherit;
`;

View File

@@ -18,6 +18,7 @@ import {
} from "@/new/photos/services/collection";
import type { CollectionSummaries } from "@/new/photos/services/collection/ui";
import { isInternalUser } from "@/new/photos/services/settings";
import { isFamilyAdmin, isPartOfFamily } from "@/new/photos/services/user";
import { AppContext, useAppContext } from "@/new/photos/types/context";
import { initiateEmail, openURL } from "@/new/photos/utils/web";
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
@@ -77,17 +78,17 @@ import {
isSubscriptionCancelled,
isSubscriptionPastDue,
} from "utils/billing";
import { isFamilyAdmin, isPartOfFamily } from "utils/user/family";
import { testUpload } from "../../../tests/upload.test";
import { MemberSubscriptionManage } from "../MemberSubscriptionManage";
import { Preferences } from "./Preferences";
import SubscriptionCard from "./SubscriptionCard";
import { SubscriptionCard } from "./SubscriptionCard";
interface Iprops {
collectionSummaries: CollectionSummaries;
sidebarView: boolean;
closeSidebar: () => void;
}
export default function Sidebar({
collectionSummaries,
sidebarView,

View File

@@ -1,5 +1,9 @@
import { genericRetriableErrorDialogAttributes } from "@/base/components/utils/dialog";
import log from "@/base/log";
import {
getTotalFamilyUsage,
isPartOfFamily,
} from "@/new/photos/services/user";
import { AppContext } from "@/new/photos/types/context";
import { bytesInGB, formattedStorageByteSize } from "@/new/photos/utils/units";
import { openURL } from "@/new/photos/utils/web";
@@ -8,6 +12,7 @@ import {
FluidContainer,
SpaceBetweenFlex,
} from "@ente/shared/components/Container";
import { getData, LS_KEYS } from "@ente/shared/storage/localStorage";
import ArrowForward from "@mui/icons-material/ArrowForward";
import ChevronRight from "@mui/icons-material/ChevronRight";
import Close from "@mui/icons-material/Close";
@@ -34,7 +39,7 @@ import billingService, { type PlansResponse } from "services/billingService";
import { getFamilyPortalRedirectURL } from "services/userService";
import { Plan, PLAN_PERIOD, Subscription } from "types/billing";
import { SetLoading } from "types/gallery";
import { BonusData } from "types/user";
import { BonusData, UserDetails } from "types/user";
import {
activateSubscription,
cancelSubscription,
@@ -52,8 +57,6 @@ import {
updatePaymentMethod,
updateSubscription,
} from "utils/billing";
import { getLocalUserDetails } from "utils/user";
import { getTotalFamilyUsage, isPartOfFamily } from "utils/user/family";
interface PlanSelectorProps {
modalView: boolean;
@@ -796,3 +799,7 @@ const ManageSubscriptionButton = ({ children, ...props }: ButtonProps) => (
<FluidContainer>{children}</FluidContainer>
</Button>
);
function getLocalUserDetails(): UserDetails {
return getData(LS_KEYS.USER_DETAILS)?.value;
}

View File

@@ -1,3 +1,4 @@
import { Overlay } from "@/base/components/mui/Container";
import log from "@/base/log";
import { EnteFile } from "@/media/file";
import { FileType } from "@/media/file-type";
@@ -11,7 +12,6 @@ import {
} from "@/new/photos/components/PlaceholderThumbnails";
import { TRASH_SECTION } from "@/new/photos/services/collection";
import DownloadManager from "@/new/photos/services/download";
import { Overlay } from "@ente/shared/components/Container";
import { CustomError } from "@ente/shared/error";
import useLongPress from "@ente/shared/hooks/useLongPress";
import AlbumOutlined from "@mui/icons-material/AlbumOutlined";

View File

@@ -2,6 +2,7 @@ import { clientPackageName, staticAppTitle } from "@/base/app";
import { CustomHead } from "@/base/components/Head";
import { AttributedMiniDialog } from "@/base/components/MiniDialog";
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
import { Overlay } from "@/base/components/mui/Container";
import { AppNavbar } from "@/base/components/Navbar";
import {
genericErrorDialogAttributes,
@@ -24,7 +25,6 @@ import DownloadManager from "@/new/photos/services/download";
import { runMigrations } from "@/new/photos/services/migrations";
import { initML, isMLSupported } from "@/new/photos/services/ml";
import { AppContext } from "@/new/photos/types/context";
import { Overlay } from "@ente/shared/components/Container";
import DialogBox from "@ente/shared/components/DialogBox";
import { DialogBoxAttributes } from "@ente/shared/components/DialogBox/types";
import { MessageContainer } from "@ente/shared/components/MessageContainer";

View File

@@ -1,3 +1,4 @@
import { sessionExpiredDialogAttributes } from "@/accounts/components/LoginComponents";
import { stashRedirect } from "@/accounts/services/redirect";
import { NavbarBase } from "@/base/components/Navbar";
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
@@ -44,6 +45,7 @@ import {
} from "@/new/photos/services/search";
import type { SearchOption } from "@/new/photos/services/search/types";
import { initSettings } from "@/new/photos/services/settings";
import { getLocalFamilyData } from "@/new/photos/services/user";
import { useAppContext } from "@/new/photos/types/context";
import { splitByPredicate } from "@/utils/array";
import { ensure } from "@/utils/ensure";
@@ -129,8 +131,6 @@ import {
handleCollectionOps,
} from "utils/collection";
import { FILE_OPS_TYPE, getSelectedFiles, handleFileOps } from "utils/file";
import { getSessionExpiredMessage } from "utils/ui";
import { getLocalFamilyData } from "utils/user/family";
const defaultGalleryContext: GalleryContextType = {
showPlanSelectorModal: () => null,
@@ -228,6 +228,7 @@ export default function Gallery() {
showLoadingBar,
hideLoadingBar,
setDialogMessage,
showMiniDialog,
logout,
...appContext
} = useAppContext();
@@ -548,9 +549,8 @@ export default function Gallery() {
};
}, [selectAll, clearSelection]);
const showSessionExpiredMessage = () => {
setDialogMessage(getSessionExpiredMessage(logout));
};
const showSessionExpiredDialog = () =>
showMiniDialog(sessionExpiredDialogAttributes(logout));
const syncWithRemote = async (force = false, silent = false) => {
if (!navigator.onLine) return;
@@ -609,7 +609,7 @@ export default function Gallery() {
} catch (e) {
switch (e.message) {
case CustomError.SESSION_EXPIRED:
showSessionExpiredMessage();
showSessionExpiredDialog();
break;
case CustomError.KEY_MISSING:
clearKeys();
@@ -1026,6 +1026,7 @@ export default function Gallery() {
isFirstUpload={areOnlySystemCollections(
collectionSummaries,
)}
showSessionExpiredMessage={showSessionExpiredDialog}
{...{
dragAndDropFiles,
openFileSelector,
@@ -1036,7 +1037,6 @@ export default function Gallery() {
fileSelectorZipFiles,
uploadTypeSelectorIntent,
uploadTypeSelectorView,
showSessionExpiredMessage,
}}
/>
<Sidebar

View File

@@ -5,9 +5,9 @@ import { NavbarBase, SelectionBar } from "@/base/components/Navbar";
import { sharedCryptoWorker } from "@/base/crypto";
import { useIsSmallWidth, useIsTouchscreen } from "@/base/hooks";
import log from "@/base/log";
import { updateShouldDisableCFUploadProxy } from "@/gallery/upload";
import type { Collection } from "@/media/collection";
import { type EnteFile, mergeMetadata } from "@/media/file";
import { updateShouldDisableCFUploadProxy } from "@/media/upload";
import {
GalleryItemsHeaderAdapter,
GalleryItemsSummary,

View File

@@ -1,6 +1,6 @@
import { accountLogout } from "@/accounts/services/logout";
import log from "@/base/log";
import { resetUploadState } from "@/media/upload";
import { resetUploadState } from "@/gallery/upload";
import DownloadManager from "@/new/photos/services/download";
import { logoutML, terminateMLWorker } from "@/new/photos/services/ml";
import { logoutSearch } from "@/new/photos/services/search";

View File

@@ -4,11 +4,11 @@ import { lowercaseExtension, nameAndExtension } from "@/base/file";
import log from "@/base/log";
import type { Electron } from "@/base/types/ipc";
import { ComlinkWorker } from "@/base/worker/comlink-worker";
import { shouldDisableCFUploadProxy } from "@/gallery/upload";
import type { Collection } from "@/media/collection";
import { EncryptedEnteFile, EnteFile } from "@/media/file";
import { FileType } from "@/media/file-type";
import { potentialFileTypeFromExtension } from "@/media/live-photo";
import { shouldDisableCFUploadProxy } from "@/media/upload";
import { getLocalFiles } from "@/new/photos/services/files";
import { indexNewUpload } from "@/new/photos/services/ml";
import type { UploadItem } from "@/new/photos/services/upload/types";

View File

@@ -1,13 +1,13 @@
import { putAttributes } from "@/accounts/api/user";
import log from "@/base/log";
import { apiURL, familyAppOrigin } from "@/base/origins";
import { getLocalFamilyData, isPartOfFamily } from "@/new/photos/services/user";
import { ApiError } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
import { HttpStatusCode } from "axios";
import { DeleteChallengeResponse, UserDetails } from "types/user";
import { getLocalFamilyData, isPartOfFamily } from "utils/user/family";
const HAS_SET_KEYS = "hasSetKeys";

View File

@@ -1,4 +1,8 @@
import log from "@/base/log";
import {
getTotalFamilyUsage,
isPartOfFamily,
} from "@/new/photos/services/user";
import { SetDialogBoxAttributes } from "@ente/shared/components/DialogBox/types";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { t } from "i18next";
@@ -8,7 +12,6 @@ import { Plan, Subscription } from "types/billing";
import { SetLoading } from "types/gallery";
import { BonusData, UserDetails } from "types/user";
import { getSubscriptionPurchaseSuccessMessage } from "utils/ui";
import { getTotalFamilyUsage, isPartOfFamily } from "utils/user/family";
const PAYMENT_PROVIDER_STRIPE = "stripe";
const FREE_PLAN = "free";

View File

@@ -60,17 +60,3 @@ export const getSubscriptionPurchaseSuccessMessage = (
/>
),
});
export const getSessionExpiredMessage = (
action: () => void,
): DialogBoxAttributes => ({
title: t("session_expired"),
content: t("session_expired_message"),
nonClosable: true,
proceed: {
text: t("login"),
action,
variant: "accent",
},
});

View File

@@ -1,43 +0,0 @@
import log from "@/base/log";
import type { FamilyData, FamilyMember } from "@/new/photos/services/user";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import type { User } from "@ente/shared/user/types";
export function getLocalFamilyData(): FamilyData {
return getData(LS_KEYS.FAMILY_DATA);
}
// isPartOfFamily return true if the current user is part of some family plan
export function isPartOfFamily(familyData: FamilyData): boolean {
return Boolean(
familyData && familyData.members && familyData.members.length > 0,
);
}
// hasNonAdminFamilyMembers return true if the admin user has members in his family
export function hasNonAdminFamilyMembers(familyData: FamilyData): boolean {
return Boolean(isPartOfFamily(familyData) && familyData.members.length > 1);
}
export function isFamilyAdmin(familyData: FamilyData): boolean {
const familyAdmin: FamilyMember = getFamilyPlanAdmin(familyData);
const user: User = getData(LS_KEYS.USER);
return familyAdmin.email === user.email;
}
export function getFamilyPlanAdmin(familyData: FamilyData): FamilyMember {
if (isPartOfFamily(familyData)) {
return familyData.members.find((x) => x.isAdmin);
} else {
log.error(
"invalid getFamilyPlanAdmin call - verify user is part of family plan before calling this method",
);
}
}
export function getTotalFamilyUsage(familyData: FamilyData): number {
return familyData.members.reduce(
(sum, currentMember) => sum + currentMember.usage,
0,
);
}

View File

@@ -1,6 +0,0 @@
import { getData, LS_KEYS } from "@ente/shared/storage/localStorage";
import { UserDetails } from "types/user";
export function getLocalUserDetails(): UserDetails {
return getData(LS_KEYS.USER_DETAILS)?.value;
}

View File

@@ -24,3 +24,15 @@ export const CenteredBox = styled("div")`
justify-content: center;
align-items: center;
`;
/**
* An absolute positioned div that fills the entire nearest relatively
* positioned ancestor.
*/
export const Overlay = styled("div")`
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
`;

View File

@@ -0,0 +1,3 @@
module.exports = {
extends: ["@/build-config/eslintrc-next"],
};

View File

@@ -0,0 +1,12 @@
## @/gallery
A package for sharing code between our apps that show media (photos, videos) in
a gallery like view.
Specifically, this is the intersection of code required by both the photos and
public albums apps.
### Packaging
This (internal) package exports a React TypeScript library. We rely on the
importing project to transpile and bundle it.

View File

@@ -0,0 +1,6 @@
{
"name": "@/gallery",
"version": "0.0.0",
"private": true,
"dependencies": {}
}

View File

@@ -0,0 +1,4 @@
{
"extends": "@/build-config/tsconfig-next.json",
"include": [".", "../../packages/base/global-electron.d.ts"]
}

View File

@@ -1,5 +1,5 @@
import { Overlay } from "@/base/components/mui/Container";
import { FileType } from "@/media/file-type";
import { Overlay } from "@ente/shared/components/Container";
import PhotoOutlined from "@mui/icons-material/PhotoOutlined";
import PlayCircleOutlineOutlined from "@mui/icons-material/PlayCircleOutlineOutlined";
import { styled } from "@mui/material";

View File

@@ -1,3 +1,4 @@
import { Overlay } from "@/base/components/mui/Container";
import { useIsSmallWidth } from "@/base/hooks";
import { CollectionsSortOptions } from "@/new/photos/components/CollectionsSortOptions";
import { FilledIconButton } from "@/new/photos/components/mui";
@@ -18,7 +19,6 @@ import type {
} from "@/new/photos/services/collection/ui";
import type { Person } from "@/new/photos/services/ml/people";
import { ensure } from "@/utils/ensure";
import { Overlay } from "@ente/shared/components/Container";
import ArchiveIcon from "@mui/icons-material/Archive";
import ExpandMore from "@mui/icons-material/ExpandMore";
import Favorite from "@mui/icons-material/FavoriteRounded";

View File

@@ -6,7 +6,7 @@
import { localUser } from "@/base/local-user";
import log from "@/base/log";
import { updateShouldDisableCFUploadProxy } from "@/media/upload";
import { updateShouldDisableCFUploadProxy } from "@/gallery/upload";
import { nullToUndefined } from "@/utils/transform";
import { z } from "zod";
import { fetchFeatureFlags, updateRemoteFlag } from "./remote-store";

View File

@@ -1,5 +1,8 @@
import { authenticatedRequestHeaders, ensureOk } from "@/base/http";
import log from "@/base/log";
import { apiURL } from "@/base/origins";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import type { User } from "@ente/shared/user/types";
import { z } from "zod";
export interface FamilyMember {
@@ -15,6 +18,52 @@ export interface FamilyData {
members: FamilyMember[];
}
export function getLocalFamilyData(): FamilyData {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return getData(LS_KEYS.FAMILY_DATA);
}
// isPartOfFamily return true if the current user is part of some family plan
export function isPartOfFamily(familyData: FamilyData): boolean {
return Boolean(
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain, @typescript-eslint/no-unnecessary-condition
familyData && familyData.members && familyData.members.length > 0,
);
}
// hasNonAdminFamilyMembers return true if the admin user has members in his family
export function hasNonAdminFamilyMembers(familyData: FamilyData): boolean {
return Boolean(isPartOfFamily(familyData) && familyData.members.length > 1);
}
export function isFamilyAdmin(familyData: FamilyData): boolean {
const familyAdmin: FamilyMember = getFamilyPlanAdmin(familyData);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const user: User = getData(LS_KEYS.USER);
return familyAdmin.email === user.email;
}
export function getFamilyPlanAdmin(familyData: FamilyData): FamilyMember {
if (isPartOfFamily(familyData)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return familyData.members.find((x) => x.isAdmin)!;
} else {
log.error(
"invalid getFamilyPlanAdmin call - verify user is part of family plan before calling this method",
);
throw new Error(
"invalid getFamilyPlanAdmin call - verify user is part of family plan before calling this method",
);
}
}
export function getTotalFamilyUsage(familyData: FamilyData): number {
return familyData.members.reduce(
(sum, currentMember) => sum + currentMember.usage,
0,
);
}
/**
* Fetch the two-factor status (whether or not it is enabled) from remote.
*/

View File

@@ -32,14 +32,6 @@ export const FluidContainer = styled(FlexWrapper)`
flex: 1;
`;
export const Overlay = styled(Box)`
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
`;
export const HorizontalFlex = styled(Box)({
display: "flex",
});