diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx
index e52d4abc28..3b425cb965 100644
--- a/web/apps/accounts/src/pages/_app.tsx
+++ b/web/apps/accounts/src/pages/_app.tsx
@@ -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";
diff --git a/web/apps/auth/src/pages/_app.tsx b/web/apps/auth/src/pages/_app.tsx
index 5557ace23d..b8cb12239d 100644
--- a/web/apps/auth/src/pages/_app.tsx
+++ b/web/apps/auth/src/pages/_app.tsx
@@ -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";
diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json
index 1bfcec9104..ad2f3323d2 100644
--- a/web/apps/photos/package.json
+++ b/web/apps/photos/package.json
@@ -5,6 +5,7 @@
"dependencies": {
"@/accounts": "*",
"@/base": "*",
+ "@/gallery": "*",
"@/media": "*",
"@/new": "*",
"@ente/eslint-config": "*",
diff --git a/web/apps/photos/src/components/MemberSubscriptionManage.tsx b/web/apps/photos/src/components/MemberSubscriptionManage.tsx
index 5fec26cbdf..62be7e4473 100644
--- a/web/apps/photos/src/components/MemberSubscriptionManage.tsx
+++ b/web/apps/photos/src/components/MemberSubscriptionManage.tsx
@@ -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);
diff --git a/web/apps/photos/src/components/PhotoViewer/index.tsx b/web/apps/photos/src/components/PhotoViewer/index.tsx
index 366a377545..d1ee5f59de 100644
--- a/web/apps/photos/src/components/PhotoViewer/index.tsx
+++ b/web/apps/photos/src/components/PhotoViewer/index.tsx
@@ -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 (
+ <>
+
+
+ {`${Math.round(props.value)}%`}
+
+ >
+ );
+}
+
+interface ConversionFailedNotificationProps {
+ open: boolean;
+ onClose: () => void;
+ onClick: () => void;
+}
+
+const ConversionFailedNotification: React.FC<
+ ConversionFailedNotificationProps
+> = ({ open, onClose, onClick }) => {
+ return (
+
+ );
+};
+
+const LivePhotoBtnContainer = styled(Paper)`
+ border-radius: 4px;
+ position: absolute;
+ bottom: 10vh;
+ right: 6vh;
+ z-index: 10;
+`;
diff --git a/web/apps/photos/src/components/PhotoViewer/styledComponents/CircularProgressWithLabel.tsx b/web/apps/photos/src/components/PhotoViewer/styledComponents/CircularProgressWithLabel.tsx
deleted file mode 100644
index 9556a3a855..0000000000
--- a/web/apps/photos/src/components/PhotoViewer/styledComponents/CircularProgressWithLabel.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
- {`${Math.round(props.value)}%`}
-
- >
- );
-}
-
-export default CircularProgressWithLabel;
diff --git a/web/apps/photos/src/components/PhotoViewer/styledComponents/ConversionFailedNotification.tsx b/web/apps/photos/src/components/PhotoViewer/styledComponents/ConversionFailedNotification.tsx
deleted file mode 100644
index fe504d1805..0000000000
--- a/web/apps/photos/src/components/PhotoViewer/styledComponents/ConversionFailedNotification.tsx
+++ /dev/null
@@ -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 (
-
- );
-};
diff --git a/web/apps/photos/src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx b/web/apps/photos/src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx
deleted file mode 100644
index 00b8979d5a..0000000000
--- a/web/apps/photos/src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx
+++ /dev/null
@@ -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;
-`;
diff --git a/web/apps/photos/src/components/PhotoViewer/styledComponents/Pre.tsx b/web/apps/photos/src/components/PhotoViewer/styledComponents/Pre.tsx
deleted file mode 100644
index b088ec9f83..0000000000
--- a/web/apps/photos/src/components/PhotoViewer/styledComponents/Pre.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { styled } from "@mui/material";
-export const Pre = styled("pre")`
- color: #aaa;
- padding: 7px 15px;
-`;
diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard.tsx
new file mode 100644
index 0000000000..090b4c949f
--- /dev/null
+++ b/web/apps/photos/src/components/Sidebar/SubscriptionCard.tsx
@@ -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 = ({
+ userDetails,
+ onClick,
+}) => {
+ if (!userDetails) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+};
+
+const BackgroundOverlay: React.FC = () => {
+ return (
+
+ );
+};
+
+const ClickOverlay: React.FC = ({ onClick }) => (
+
+
+
+);
+
+interface SubscriptionCardContentOverlayProps {
+ userDetails: UserDetails;
+}
+
+export const SubscriptionCardContentOverlay: React.FC<
+ SubscriptionCardContentOverlayProps
+> = ({ userDetails }) => {
+ return (
+
+
+ {hasNonAdminFamilyMembers(userDetails.familyData) ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+interface IndividualSubscriptionCardContentProps {
+ userDetails: UserDetails;
+}
+
+const IndividualSubscriptionCardContent: React.FC<
+ IndividualSubscriptionCardContentProps
+> = ({ userDetails }) => {
+ const totalStorage =
+ userDetails.subscription.storage + (userDetails.storageBonus ?? 0);
+ return (
+ <>
+
+
+ >
+ );
+};
+
+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 = ({ usage, storage }) => {
+ return (
+
+
+ {t("STORAGE")}
+
+
+
+ {`${formattedStorageByteSize(usage, { round: true })} ${t(
+ "OF",
+ )} ${formattedStorageByteSize(storage)} ${t("USED")}`}
+
+
+
+
+ {`${bytesInGB(usage)} / ${bytesInGB(storage)} ${t("storage_unit.gb")} ${t("USED")}`}
+
+
+
+ );
+};
+
+interface IndividualUsageSectionProps {
+ usage: number;
+ fileCount: number;
+ storage: number;
+}
+
+const IndividualUsageSection: React.FC = ({
+ 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 (
+
+
+
+ {`${formattedStorageByteSize(
+ storage - usage,
+ )} ${t("FREE")}`}
+
+ {t("photos_count", { count: fileCount ?? 0 })}
+
+
+
+ );
+};
+
+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 (
+ <>
+
+
+ >
+ );
+};
+
+interface FamilyUsageSectionProps {
+ userUsage: number;
+ totalUsage: number;
+ fileCount: number;
+ totalStorage: number;
+}
+
+const FamilyUsageSection: React.FC = ({
+ userUsage,
+ totalUsage,
+ fileCount,
+ totalStorage,
+}) => {
+ return (
+
+
+
+
+
+
+
+
+ {t("photos_count", { count: fileCount ?? 0 })}
+
+
+
+ );
+};
+
+interface FamilyUsageBarProps {
+ userUsage: number;
+ totalUsage: number;
+ totalStorage: number;
+}
+
+const FamilyUsageBar: React.FC = ({
+ userUsage,
+ totalUsage,
+ totalStorage,
+}) => (
+
+
+
+
+);
+
+type UsageBarProps = Pick & {
+ used: number;
+ total: number;
+};
+
+const UsageBar: React.FC = ({ used, total, sx }) => (
+
+);
+
+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 = ({ label, color }) => (
+
+
+
+ {label}
+
+
+);
+
+const LegendDot = styled(CircleIcon)`
+ font-size: 8.71px;
+ margin: 0;
+ margin-inline-end: 4px;
+ color: inherit;
+`;
diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/index.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/index.tsx
deleted file mode 100644
index 77776745dc..0000000000
--- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/index.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
- >
- );
-}
diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/index.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/index.tsx
deleted file mode 100644
index 4c0b1904f2..0000000000
--- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/index.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
-
-
- {t("photos_count", { count: fileCount ?? 0 })}
-
-
-
- );
-}
diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/legend.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/legend.tsx
deleted file mode 100644
index 6caaa2374d..0000000000
--- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/legend.tsx
+++ /dev/null
@@ -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 (
-
-
-
- {label}
-
-
- );
-}
diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/progressBar.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/progressBar.tsx
deleted file mode 100644
index ab28b5b8f1..0000000000
--- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/progressBar.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
- );
-}
diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/index.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/index.tsx
deleted file mode 100644
index 238058534c..0000000000
--- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/index.tsx
+++ /dev/null
@@ -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 (
-
-
- {hasNonAdminFamilyMembers(userDetails.familyData) ? (
-
- ) : (
-
- )}
-
-
- );
-}
diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/index.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/index.tsx
deleted file mode 100644
index 9bdc3292c1..0000000000
--- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/index.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
- >
- );
-}
diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx
deleted file mode 100644
index 857ac9c633..0000000000
--- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx
+++ /dev/null
@@ -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 (
-
-
-
- {`${formattedStorageByteSize(
- storage - usage,
- )} ${t("FREE")}`}
-
- {t("photos_count", { count: fileCount ?? 0 })}
-
-
-
- );
-}
diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx
deleted file mode 100644
index 4ad0ed2149..0000000000
--- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx
+++ /dev/null
@@ -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 (
-
-
- {t("STORAGE")}
-
-
-
- {`${formattedStorageByteSize(usage, { round: true })} ${t(
- "OF",
- )} ${formattedStorageByteSize(storage)} ${t("USED")}`}
-
-
-
-
- {`${bytesInGB(usage)} / ${bytesInGB(storage)} ${t("storage_unit.gb")} ${t("USED")}`}
-
-
-
- );
-}
diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/index.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/index.tsx
deleted file mode 100644
index 514c43df81..0000000000
--- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/index.tsx
+++ /dev/null
@@ -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 (
-
- );
- }
-
- return (
-
-
-
-
-
- );
-}
-
-function BackgroundOverlay() {
- return (
-
- );
-}
-
-function ClickOverlay({ onClick }) {
- return (
-
-
-
-
-
- );
-}
diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/styledComponents.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/styledComponents.tsx
deleted file mode 100644
index 90bea72ce7..0000000000
--- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/styledComponents.tsx
+++ /dev/null
@@ -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;
-`;
diff --git a/web/apps/photos/src/components/Sidebar/index.tsx b/web/apps/photos/src/components/Sidebar/index.tsx
index 922e6a2148..425c7d291e 100644
--- a/web/apps/photos/src/components/Sidebar/index.tsx
+++ b/web/apps/photos/src/components/Sidebar/index.tsx
@@ -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,
diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx
index 3e840eaa03..a64a302e46 100644
--- a/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx
+++ b/web/apps/photos/src/components/pages/gallery/PlanSelector.tsx
@@ -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) => (
{children}
);
+
+function getLocalUserDetails(): UserDetails {
+ return getData(LS_KEYS.USER_DETAILS)?.value;
+}
diff --git a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx
index 0f66a27ed6..64a44ff335 100644
--- a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx
+++ b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx
@@ -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";
diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx
index 361a0be901..7a1582b8e4 100644
--- a/web/apps/photos/src/pages/_app.tsx
+++ b/web/apps/photos/src/pages/_app.tsx
@@ -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";
diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx
index 9b5c433337..ff039f7a67 100644
--- a/web/apps/photos/src/pages/gallery.tsx
+++ b/web/apps/photos/src/pages/gallery.tsx
@@ -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,
}}
/>
),
});
-
-export const getSessionExpiredMessage = (
- action: () => void,
-): DialogBoxAttributes => ({
- title: t("session_expired"),
- content: t("session_expired_message"),
-
- nonClosable: true,
- proceed: {
- text: t("login"),
- action,
- variant: "accent",
- },
-});
diff --git a/web/apps/photos/src/utils/user/family.ts b/web/apps/photos/src/utils/user/family.ts
deleted file mode 100644
index 0456976cc2..0000000000
--- a/web/apps/photos/src/utils/user/family.ts
+++ /dev/null
@@ -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,
- );
-}
diff --git a/web/apps/photos/src/utils/user/index.ts b/web/apps/photos/src/utils/user/index.ts
deleted file mode 100644
index 7404fb5887..0000000000
--- a/web/apps/photos/src/utils/user/index.ts
+++ /dev/null
@@ -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;
-}
diff --git a/web/packages/base/components/mui/Container.tsx b/web/packages/base/components/mui/Container.tsx
index d985fc033a..abaa0e2dce 100644
--- a/web/packages/base/components/mui/Container.tsx
+++ b/web/packages/base/components/mui/Container.tsx
@@ -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;
+`;
diff --git a/web/packages/gallery/.eslintrc.js b/web/packages/gallery/.eslintrc.js
new file mode 100644
index 0000000000..348075cd4f
--- /dev/null
+++ b/web/packages/gallery/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: ["@/build-config/eslintrc-next"],
+};
diff --git a/web/packages/gallery/README.md b/web/packages/gallery/README.md
new file mode 100644
index 0000000000..49cddae88d
--- /dev/null
+++ b/web/packages/gallery/README.md
@@ -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.
diff --git a/web/packages/gallery/package.json b/web/packages/gallery/package.json
new file mode 100644
index 0000000000..4281b87fd9
--- /dev/null
+++ b/web/packages/gallery/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@/gallery",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {}
+}
diff --git a/web/packages/gallery/tsconfig.json b/web/packages/gallery/tsconfig.json
new file mode 100644
index 0000000000..b2a1203623
--- /dev/null
+++ b/web/packages/gallery/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "@/build-config/tsconfig-next.json",
+ "include": [".", "../../packages/base/global-electron.d.ts"]
+}
diff --git a/web/packages/media/upload.ts b/web/packages/gallery/upload.ts
similarity index 100%
rename from web/packages/media/upload.ts
rename to web/packages/gallery/upload.ts
diff --git a/web/packages/new/photos/components/PlaceholderThumbnails.tsx b/web/packages/new/photos/components/PlaceholderThumbnails.tsx
index 0fcab06569..d6fab83a28 100644
--- a/web/packages/new/photos/components/PlaceholderThumbnails.tsx
+++ b/web/packages/new/photos/components/PlaceholderThumbnails.tsx
@@ -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";
diff --git a/web/packages/new/photos/components/gallery/BarImpl.tsx b/web/packages/new/photos/components/gallery/BarImpl.tsx
index 6821b622b0..29c9d7987c 100644
--- a/web/packages/new/photos/components/gallery/BarImpl.tsx
+++ b/web/packages/new/photos/components/gallery/BarImpl.tsx
@@ -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";
diff --git a/web/packages/new/photos/services/settings.ts b/web/packages/new/photos/services/settings.ts
index 063fb378e0..92a6a2d149 100644
--- a/web/packages/new/photos/services/settings.ts
+++ b/web/packages/new/photos/services/settings.ts
@@ -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";
diff --git a/web/packages/new/photos/services/user.ts b/web/packages/new/photos/services/user.ts
index 58bd087e74..5c20573cab 100644
--- a/web/packages/new/photos/services/user.ts
+++ b/web/packages/new/photos/services/user.ts
@@ -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.
*/
diff --git a/web/packages/shared/components/Container.tsx b/web/packages/shared/components/Container.tsx
index 9a1eb9666e..7852439cb4 100644
--- a/web/packages/shared/components/Container.tsx
+++ b/web/packages/shared/components/Container.tsx
@@ -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",
});