[web] General (non-functional) rearrangement of code (#3957)
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"dependencies": {
|
||||
"@/accounts": "*",
|
||||
"@/base": "*",
|
||||
"@/gallery": "*",
|
||||
"@/media": "*",
|
||||
"@/new": "*",
|
||||
"@ente/eslint-config": "*",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
@@ -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 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { styled } from "@mui/material";
|
||||
export const Pre = styled("pre")`
|
||||
color: #aaa;
|
||||
padding: 7px 15px;
|
||||
`;
|
||||
358
web/apps/photos/src/components/Sidebar/SubscriptionCard.tsx
Normal file
358
web/apps/photos/src/components/Sidebar/SubscriptionCard.tsx
Normal 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;
|
||||
`;
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
3
web/packages/gallery/.eslintrc.js
Normal file
3
web/packages/gallery/.eslintrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: ["@/build-config/eslintrc-next"],
|
||||
};
|
||||
12
web/packages/gallery/README.md
Normal file
12
web/packages/gallery/README.md
Normal 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.
|
||||
6
web/packages/gallery/package.json
Normal file
6
web/packages/gallery/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@/gallery",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {}
|
||||
}
|
||||
4
web/packages/gallery/tsconfig.json
Normal file
4
web/packages/gallery/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@/build-config/tsconfig-next.json",
|
||||
"include": [".", "../../packages/base/global-electron.d.ts"]
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user