Merge branch 'main' into watch

This commit is contained in:
Rushikesh Tote 2022-07-23 22:30:43 +05:30
commit 1a5d47b11f
281 changed files with 4165 additions and 3986 deletions

View file

@ -1,6 +1,6 @@
{
"name": "bada-frame",
"version": "0.9.1",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
@ -22,8 +22,6 @@
"@mui/x-date-pickers": "^5.0.0-alpha.6",
"@sentry/nextjs": "^6.7.1",
"@stripe/stripe-js": "^1.13.2",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"axios": "^0.21.3",
"bip39": "^3.0.4",
"bootstrap": "^4.5.2",
@ -31,16 +29,11 @@
"chrono-node": "^2.2.6",
"comlink": "^4.3.0",
"debounce-promise": "^3.1.2",
"eslint-config-airbnb": "^18.2.1",
"eslint-plugin-import": "^2.23.3",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react-hooks": "^4.2.0",
"exifr": "^7.1.3",
"ffmpeg-wasm": "file:./thirdparty/ffmpeg-wasm",
"file-type": "^16.5.3",
"file-type": "^16.5.4",
"formik": "^2.1.5",
"heic-convert": "^1.2.4",
"http-proxy-middleware": "^1.0.5",
"is-electron": "^2.2.0",
"jszip": "3.7.1",
"libsodium-wrappers": "^0.7.8",
@ -58,8 +51,6 @@
"react-top-loading-bar": "^2.0.1",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.6",
"react-window-infinite-loader": "^1.0.5",
"scrypt-js": "^3.0.1",
"styled-components": "^5.3.5",
"workbox-precaching": "^6.1.5",
"workbox-recipes": "^6.1.5",
@ -83,11 +74,17 @@
"@types/react-window-infinite-loader": "^1.0.3",
"@types/styled-components": "^5.1.25",
"@types/yup": "^0.29.7",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"babel-plugin-styled-components": "^1.11.1",
"eslint": "^7.27.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.3",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "^7.0.1",
"lint-staged": "^11.1.2",
"prettier": "2.3.2",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,28 +0,0 @@
export const BLACK_THUMBNAIL_BASE64 =
'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB' +
'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ' +
'EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC' +
'ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF' +
'BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk' +
'6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL' +
'W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA' +
'AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVY' +
'nLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImK' +
'kpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oAD' +
'AMBAAIRAxEAPwD/AD/6ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAC' +
'gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA' +
'KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAK' +
'ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA' +
'KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k=';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View file

Before

Width:  |  Height:  |  Size: 408 KiB

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 472 B

View file

@ -3,17 +3,17 @@
"name": "ente | encrypted photo storage",
"icons": [
{
"src": "/images/ente-192.png",
"src": "/images/ente/192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/images/ente-256.png",
"src": "/images/ente/256.png",
"type": "image/png",
"sizes": "256x256"
},
{
"src": "/images/ente-512.png",
"src": "/images/ente/512.png",
"type": "image/png",
"sizes": "512x512"
}

View file

@ -49,7 +49,7 @@
</head>
<body>
<nav>
<img src='/icon.svg' />
<img src="/images/ente.svg" />
</nav>
<div>
<h2>seems like you are offline :(</h2>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 B

10
src/components/Badge.tsx Normal file
View file

@ -0,0 +1,10 @@
import { Paper, styled } from '@mui/material';
import { CSSProperties } from '@mui/styled-engine';
export const Badge = styled(Paper)(({ theme }) => ({
padding: '2px 4px',
backgroundColor: theme.palette.glass.main,
color: theme.palette.glass.contrastText,
textTransform: 'uppercase',
...(theme.typography.mini as CSSProperties),
}));

View file

@ -1,18 +1,17 @@
import { Formik, FormikHelpers } from 'formik';
import React, { useContext, useRef, useState } from 'react';
import React, { useRef, useState } from 'react';
import * as Yup from 'yup';
import constants from 'utils/strings/constants';
import SubmitButton from 'components/SubmitButton';
import router from 'next/router';
import { changeEmail, getOTTForEmailChange } from 'services/userService';
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
import { changeEmail, sendOTTForEmailChange } from 'services/userService';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { PAGES } from 'constants/pages';
import { TextField } from '@mui/material';
import { Alert, TextField } from '@mui/material';
import Container from './Container';
import LinkButton from './pages/gallery/LinkButton';
import { Alert } from 'react-bootstrap';
import FormPaperFooter from './Form/FormPaper/Footer';
import { sleep } from 'utils/common';
interface formValues {
email: string;
@ -23,9 +22,9 @@ function ChangeEmailForm() {
const [loading, setLoading] = useState(false);
const [ottInputVisible, setShowOttInputVisibility] = useState(false);
const ottInputRef = useRef(null);
const appContext = useContext(AppContext);
const [email, setEmail] = useState(null);
const [showMessage, setShowMessage] = useState(false);
const [success, setSuccess] = useState(false);
const requestOTT = async (
{ email }: formValues,
@ -33,7 +32,7 @@ function ChangeEmailForm() {
) => {
try {
setLoading(true);
await getOTTForEmailChange(email);
await sendOTTForEmailChange(email);
setEmail(email);
setShowOttInputVisibility(true);
setShowMessage(true);
@ -54,15 +53,14 @@ function ChangeEmailForm() {
setLoading(true);
await changeEmail(email, ott);
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), email });
appContext.setDisappearingFlashMessage({
message: constants.EMAIL_UDPATE_SUCCESSFUL,
type: FLASH_MESSAGE_TYPE.SUCCESS,
});
setLoading(false);
setSuccess(true);
await sleep(1000);
router.push(PAGES.GALLERY);
} catch (e) {
setLoading(false);
setFieldError('ott', `${constants.INCORRECT_CODE}`);
}
setLoading(false);
};
const goToGallery = () => router.push(PAGES.GALLERY);
@ -83,15 +81,13 @@ function ChangeEmailForm() {
onSubmit={!ottInputVisible ? requestOTT : requestEmailChange}>
{({ values, errors, handleChange, handleSubmit }) => (
<>
{showMessage && (
<Alert
variant="success"
show={showMessage}
style={{ paddingBottom: 0 }}
transition
dismissible
color="accent"
onClose={() => setShowMessage(false)}>
{constants.EMAIL_SENT({ email })}
</Alert>
)}
<form noValidate onSubmit={handleSubmit}>
<Container>
<TextField
@ -121,6 +117,7 @@ function ChangeEmailForm() {
/>
)}
<SubmitButton
success={success}
sx={{ mt: 2 }}
loading={loading}
buttonText={

View file

@ -11,7 +11,7 @@ export const CopyButtonWrapper = styled(IconButton)`
position: absolute;
top: 0px;
right: 0px;
margin: ${({ theme }) => theme.spacing(1)};
margin-top: ${({ theme }) => theme.spacing(1)};
`;
export const CodeWrapper = styled('div')`

View file

@ -7,13 +7,28 @@ import OverflowMenu from 'components/OverflowMenu/menu';
export interface CollectionSortProps {
setCollectionSortBy: (sortBy: COLLECTION_SORT_BY) => void;
activeSortBy: COLLECTION_SORT_BY;
nestedInDialog?: boolean;
disableBG?: boolean;
}
export default function CollectionSort(props: CollectionSortProps) {
return (
<OverflowMenu
ariaControls="collection-sort"
triggerButtonIcon={<SortIcon />}>
triggerButtonIcon={<SortIcon />}
menuPaperProps={{
sx: {
backgroundColor: (theme) =>
props.nestedInDialog &&
theme.palette.background.overPaper,
},
}}
triggerButtonProps={{
sx: {
background: (theme) =>
!props.disableBG && theme.palette.fill.dark,
},
}}>
<CollectionSortOptions {...props} />
</OverflowMenu>
);

View file

@ -1,24 +1,23 @@
import React, { useContext } from 'react';
import React from 'react';
import { COLLECTION_SORT_BY } from 'constants/collection';
import TickIcon from '@mui/icons-material/Done';
import { CollectionSortProps } from '.';
import { OverflowMenuContext } from 'contexts/overflowMenu';
import { OverflowMenuOption } from 'components/OverflowMenu/option';
import { SvgIcon } from '@mui/material';
const SortByOptionCreator =
({ setCollectionSortBy, activeSortBy }: CollectionSortProps) =>
(props: { sortBy: COLLECTION_SORT_BY; children: any }) => {
const { close } = useContext(OverflowMenuContext);
const handleClick = () => {
setCollectionSortBy(props.sortBy);
close();
};
return (
<OverflowMenuOption
onClick={handleClick}
startIcon={activeSortBy === props.sortBy && <TickIcon />}>
endIcon={
activeSortBy === props.sortBy ? <TickIcon /> : <SvgIcon />
}>
{props.children}
</OverflowMenuOption>
);

View file

@ -1,29 +1,29 @@
import { Box } from '@mui/material';
import { Typography } from '@mui/material';
import constants from 'utils/strings/constants';
import React from 'react';
import CollectionCard from '../CollectionCard';
import { CollectionSummary } from 'types/collection';
import { AllCollectionTileText } from '../styledComponents';
import { AllCollectionTile, AllCollectionTileText } from '../styledComponents';
interface Iprops {
collectionTile: any;
collectionSummary: CollectionSummary;
onCollectionClick: (collectionID: number) => void;
}
export default function AllCollectionCard({
collectionTile,
onCollectionClick,
collectionSummary,
}: Iprops) {
return (
<CollectionCard
collectionTile={collectionTile}
collectionTile={AllCollectionTile}
latestFile={collectionSummary.latestFile}
onClick={() => onCollectionClick(collectionSummary.id)}>
<AllCollectionTileText>
<Box fontWeight={'bold'}>{collectionSummary.name}</Box>
<Box>{constants.PHOTO_COUNT(collectionSummary.fileCount)}</Box>
<Typography>{collectionSummary.name}</Typography>
<Typography variant="body2" color="text.secondary">
{constants.PHOTO_COUNT(collectionSummary.fileCount)}
</Typography>
</AllCollectionTileText>
</CollectionCard>
);

View file

@ -1,28 +1,22 @@
import React from 'react';
import { DialogContent } from '@mui/material';
import { FlexWrapper } from 'components/Container';
import AllCollectionCard from './CollectionCard';
import AllCollectionCard from './collectionCard';
import { CollectionSummary } from 'types/collection';
interface Iprops {
collectionSummaries: CollectionSummary[];
onCollectionClick: (id?: number) => void;
collectionTile: any;
}
export default function AllCollectionContent({
collectionSummaries,
onCollectionClick,
collectionTile,
}: Iprops) {
return (
<DialogContent>
<FlexWrapper
style={{
flexWrap: 'wrap',
}}>
<FlexWrapper flexWrap="wrap" gap={0.5}>
{collectionSummaries.map((collectionSummary) => (
<AllCollectionCard
collectionTile={collectionTile}
onCollectionClick={onCollectionClick}
collectionSummary={collectionSummary}
key={collectionSummary.id}

View file

@ -2,22 +2,33 @@ import { Dialog, Slide, styled } from '@mui/material';
import React from 'react';
import PropTypes from 'prop-types';
export const AllCollectionContainer = styled(Dialog)(({ theme }) => ({
export const AllCollectionDialog = styled(Dialog)<{
position: 'flex-start' | 'center' | 'flex-end';
}>(({ theme, position }) => ({
'& .MuiDialog-container': {
justifyContent: 'flex-end',
justifyContent: position,
},
'& .MuiPaper-root': {
maxWidth: '498px',
maxWidth: '494px',
},
'& .MuiDialogTitle-root': {
padding: theme.spacing(3, 2),
padding: theme.spacing(2),
paddingRight: theme.spacing(1),
},
'& .MuiDialogContent-root': {
padding: theme.spacing(2),
},
[theme.breakpoints.down(559)]: {
'& .MuiPaper-root': {
width: '324px',
},
'& .MuiDialogContent-root': {
padding: 6,
},
},
}));
AllCollectionContainer.propTypes = {
AllCollectionDialog.propTypes = {
children: PropTypes.node,
onClose: PropTypes.func.isRequired,
};

View file

@ -1,6 +1,10 @@
import React from 'react';
import { DialogTitle, IconButton, Typography } from '@mui/material';
import { SpaceBetweenFlex } from 'components/Container';
import { Box, DialogTitle, Stack, Typography } from '@mui/material';
import {
FlexWrapper,
FluidContainer,
IconButtonWithBG,
} from 'components/Container';
import CollectionSort from 'components/Collections/AllCollections/CollectionSort';
import constants from 'utils/strings/constants';
import Close from '@mui/icons-material/Close';
@ -13,23 +17,28 @@ export default function AllCollectionsHeader({
}) {
return (
<DialogTitle>
<SpaceBetweenFlex>
<Typography variant="subtitle">
<FlexWrapper>
<FluidContainer mr={1.5}>
<Box>
<Typography variant="h3">
{constants.ALL_ALBUMS}
</Typography>
<IconButton onClick={onClose}>
<Close />
</IconButton>
</SpaceBetweenFlex>
<SpaceBetweenFlex>
<Typography variant="subtitle" color={'text.secondary'}>
<Typography variant="body2" color={'text.secondary'}>
{`${collectionCount} ${constants.ALBUMS}`}
</Typography>
</Box>
</FluidContainer>
<Stack direction="row" spacing={1.5}>
<CollectionSort
activeSortBy={collectionSortBy}
setCollectionSortBy={setCollectionSortBy}
nestedInDialog
/>
</SpaceBetweenFlex>
<IconButtonWithBG onClick={onClose}>
<Close />
</IconButtonWithBG>
</Stack>
</FlexWrapper>
</DialogTitle>
);
}

View file

@ -1,70 +1,60 @@
import React, { useMemo } from 'react';
import React, { useContext } from 'react';
import Divider from '@mui/material/Divider';
import { COLLECTION_SORT_BY } from 'constants/collection';
import { sortCollectionSummaries } from 'services/collectionService';
import {
Transition,
AllCollectionContainer,
} from 'components/Collections/AllCollections/Container';
import { useLocalState } from 'hooks/useLocalState';
import { LS_KEYS } from 'utils/storage/localStorage';
AllCollectionDialog,
} from 'components/Collections/AllCollections/dialog';
import AllCollectionsHeader from './header';
import { CollectionSummaries } from 'types/collection';
import { CollectionSummary } from 'types/collection';
import AllCollectionContent from './content';
import { AllCollectionTile } from '../styledComponents';
import { isSystemCollection } from 'utils/collection';
import { AppContext } from 'pages/_app';
interface Iprops {
isOpen: boolean;
close: () => void;
collectionSummaries: CollectionSummaries;
open: boolean;
onClose: () => void;
collectionSummaries: CollectionSummary[];
setActiveCollection: (id?: number) => void;
collectionSortBy: COLLECTION_SORT_BY;
setCollectionSortBy: (v: COLLECTION_SORT_BY) => void;
}
const LeftSlideTransition = Transition('up');
export default function AllCollections(props: Iprops) {
const { collectionSummaries, isOpen, close, setActiveCollection } = props;
const [collectionSortBy, setCollectionSortBy] =
useLocalState<COLLECTION_SORT_BY>(
LS_KEYS.COLLECTION_SORT_BY,
COLLECTION_SORT_BY.UPDATION_TIME_DESCENDING
);
const sortedCollectionSummaries = useMemo(
() =>
sortCollectionSummaries(
[...collectionSummaries.values()].filter(
(x) => !isSystemCollection(x.type)
),
collectionSortBy
),
[collectionSortBy, collectionSummaries]
);
const {
collectionSummaries,
open,
onClose,
setActiveCollection,
collectionSortBy,
setCollectionSortBy,
} = props;
const { isMobile } = useContext(AppContext);
const onCollectionClick = (collectionID: number) => {
setActiveCollection(collectionID);
close();
onClose();
};
return (
<AllCollectionContainer
<AllCollectionDialog
position="flex-end"
TransitionComponent={LeftSlideTransition}
onClose={close}
open={isOpen}>
onClose={onClose}
open={open}
fullScreen={isMobile}>
<AllCollectionsHeader
onClose={close}
collectionCount={props.collectionSummaries.size}
onClose={onClose}
collectionCount={props.collectionSummaries.length}
collectionSortBy={collectionSortBy}
setCollectionSortBy={setCollectionSortBy}
/>
<Divider />
<AllCollectionContent
collectionTile={AllCollectionTile}
collectionSummaries={sortedCollectionSummaries}
collectionSummaries={collectionSummaries}
onCollectionClick={onCollectionClick}
/>
</AllCollectionContainer>
</AllCollectionDialog>
);
}

View file

@ -1,36 +0,0 @@
import React from 'react';
import { EnteFile } from 'types/file';
import {
CollectionTileWrapper,
ActiveIndicator,
CollectionBarTile,
CollectionBarTileText,
} from '../styledComponents';
import CollectionCard from '../CollectionCard';
import TruncateText from 'components/TruncateText';
interface Iprops {
active: boolean;
latestFile: EnteFile;
collectionName: string;
onClick: () => void;
}
const CollectionCardWithActiveIndicator = React.forwardRef(
(props: Iprops, ref: any) => {
const { active, collectionName, ...others } = props;
return (
<CollectionTileWrapper ref={ref}>
<CollectionCard collectionTile={CollectionBarTile} {...others}>
<CollectionBarTileText>
<TruncateText text={collectionName} />
</CollectionBarTileText>
</CollectionCard>
{active && <ActiveIndicator />}
</CollectionTileWrapper>
);
}
);
export default CollectionCardWithActiveIndicator;

View file

@ -1,12 +0,0 @@
import React from 'react';
import constants from 'utils/strings/englishConstants';
import { CollectionTitleWithDashedBorder } from '../styledComponents';
export const CreateNewCollectionTile = (props) => {
return (
<CollectionTitleWithDashedBorder {...props}>
<div>{constants.NEW} </div>
<div>{'+'}</div>
</CollectionTitleWithDashedBorder>
);
};

View file

@ -1,114 +0,0 @@
import ScrollButton from 'components/Collections/CollectionBar/ScrollButton';
import React, { useEffect, useMemo } from 'react';
import { CollectionSummaries } from 'types/collection';
import constants from 'utils/strings/constants';
import { ALL_SECTION, COLLECTION_SORT_BY } from 'constants/collection';
import { Typography } from '@mui/material';
import {
CollectionListBarWrapper,
ScrollContainer,
CollectionListWrapper,
} from 'components/Collections/styledComponents';
import CollectionCardWithActiveIndicator from 'components/Collections/CollectionBar/CollectionCardWithActiveIndicator';
import useComponentScroll, { SCROLL_DIRECTION } from 'hooks/useComponentScroll';
import useWindowSize from 'hooks/useWindowSize';
import LinkButton from 'components/pages/gallery/LinkButton';
import { SpaceBetweenFlex } from 'components/Container';
import { sortCollectionSummaries } from 'services/collectionService';
interface IProps {
activeCollection?: number;
setActiveCollection: (id?: number) => void;
collectionSummaries: CollectionSummaries;
showAllCollections: () => void;
}
export default function CollectionListBar(props: IProps) {
const {
activeCollection,
setActiveCollection,
collectionSummaries,
showAllCollections,
} = props;
const sortedCollectionSummary = useMemo(
() =>
sortCollectionSummaries(
[...collectionSummaries.values()].filter(
(c) => c.fileCount > 0
),
COLLECTION_SORT_BY.UPDATION_TIME_DESCENDING
),
[collectionSummaries]
);
const windowSize = useWindowSize();
const {
componentRef,
scrollComponent,
hasScrollBar,
onFarLeft,
onFarRight,
} = useComponentScroll({
dependencies: [windowSize, collectionSummaries],
});
const collectionChipsRef = sortedCollectionSummary.reduce(
(refMap, collectionSummary) => {
refMap[collectionSummary.id] = React.createRef();
return refMap;
},
{}
);
useEffect(() => {
collectionChipsRef[activeCollection]?.current.scrollIntoView({
inline: 'center',
});
}, [activeCollection]);
const clickHandler = (collectionID?: number) => () => {
setActiveCollection(collectionID ?? ALL_SECTION);
};
return (
<CollectionListBarWrapper>
<SpaceBetweenFlex mb={1}>
<Typography>{constants.ALBUMS}</Typography>
{hasScrollBar && (
<LinkButton onClick={showAllCollections}>
{constants.VIEW_ALL_ALBUMS}
</LinkButton>
)}
</SpaceBetweenFlex>
<CollectionListWrapper>
{!onFarLeft && (
<ScrollButton
scrollDirection={SCROLL_DIRECTION.LEFT}
onClick={scrollComponent(SCROLL_DIRECTION.LEFT)}
/>
)}
<ScrollContainer ref={componentRef}>
{sortedCollectionSummary.map((item) => (
<CollectionCardWithActiveIndicator
key={item.id}
latestFile={item.latestFile}
ref={collectionChipsRef[item.id]}
active={activeCollection === item.id}
onClick={clickHandler(item.id)}
collectionName={item.name}
/>
))}
</ScrollContainer>
{!onFarRight && (
<ScrollButton
scrollDirection={SCROLL_DIRECTION.RIGHT}
onClick={scrollComponent(SCROLL_DIRECTION.RIGHT)}
/>
)}
</CollectionListWrapper>
</CollectionListBarWrapper>
);
}

View file

@ -1,10 +1,20 @@
import { Typography } from '@mui/material';
import { Box, Typography } from '@mui/material';
import { FlexWrapper } from 'components/Container';
import React from 'react';
import constants from 'utils/strings/constants';
export function CollectionInfo({ name, fileCount }) {
interface Iprops {
name: string;
fileCount: number;
endIcon?: React.ReactNode;
}
export function CollectionInfo({ name, fileCount, endIcon }: Iprops) {
return (
<div>
<FlexWrapper>
<Typography variant="subtitle">{name}</Typography>
{endIcon && <Box ml={1.5}>{endIcon}</Box>}
</FlexWrapper>
<Typography variant="body2" color="text.secondary">
{constants.PHOTO_COUNT(fileCount)}
</Typography>

View file

@ -5,7 +5,11 @@ import CollectionOptions from 'components/Collections/CollectionOptions';
import { SetCollectionNamerAttributes } from 'components/Collections/CollectionNamer';
import { SpaceBetweenFlex } from 'components/Container';
import { CollectionInfoBarWrapper } from './styledComponents';
import { isSystemCollection } from 'utils/collection';
import { shouldShowOptions } from 'utils/collection';
import { CollectionSummaryType } from 'constants/collection';
import Favorite from '@mui/icons-material/FavoriteRounded';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import Delete from '@mui/icons-material/Delete';
interface Iprops {
activeCollection: Collection;
@ -19,6 +23,7 @@ interface Iprops {
collectionSummary: CollectionSummary;
setCollectionNamerAttributes: SetCollectionNamerAttributes;
activeCollection: Collection;
activeCollectionID: number;
showCollectionShareModal: () => void;
redirectToAll: () => void;
}
@ -32,11 +37,33 @@ export default function CollectionInfoWithOptions({
const { name, type, fileCount } = collectionSummary;
const EndIcon = ({ type }: { type: CollectionSummaryType }) => {
switch (type) {
case CollectionSummaryType.favorites:
return <Favorite />;
case CollectionSummaryType.archived:
case CollectionSummaryType.archive:
return <VisibilityOff />;
case CollectionSummaryType.trash:
return <Delete />;
default:
return <></>;
}
};
return (
<CollectionInfoBarWrapper>
<SpaceBetweenFlex>
<CollectionInfo name={name} fileCount={fileCount} />
{!isSystemCollection(type) && <CollectionOptions {...props} />}
<CollectionInfo
name={name}
fileCount={fileCount}
endIcon={<EndIcon type={type} />}
/>
{shouldShowOptions(type) && (
<CollectionOptions
{...props}
collectionSummaryType={type}
/>
)}
</SpaceBetweenFlex>
</CollectionInfoBarWrapper>
);

View file

@ -0,0 +1,57 @@
import React from 'react';
import { EnteFile } from 'types/file';
import {
ActiveIndicator,
CollectionBarTile,
CollectionBarTileIcon,
CollectionBarTileText,
} from '../styledComponents';
import CollectionCard from '../CollectionCard';
import TruncateText from 'components/TruncateText';
import { Box } from '@mui/material';
import { CollectionSummaryType } from 'constants/collection';
import Favorite from '@mui/icons-material/FavoriteRounded';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
interface Iprops {
active: boolean;
latestFile: EnteFile;
collectionName: string;
collectionType: CollectionSummaryType;
onClick: () => void;
}
const CollectionListBarCard = React.forwardRef((props: Iprops, ref: any) => {
const { active, collectionName, collectionType, ...others } = props;
return (
<Box ref={ref}>
<CollectionCard collectionTile={CollectionBarTile} {...others}>
<CollectionCardText collectionName={collectionName} />
<CollectionCardIcon collectionType={collectionType} />
</CollectionCard>
{active && <ActiveIndicator />}
</Box>
);
});
export default CollectionListBarCard;
function CollectionCardText({ collectionName }) {
return (
<CollectionBarTileText>
<TruncateText text={collectionName} />
</CollectionBarTileText>
);
}
function CollectionCardIcon({ collectionType }) {
return (
<CollectionBarTileIcon>
{collectionType === CollectionSummaryType.favorites && <Favorite />}
{collectionType === CollectionSummaryType.archived && (
<VisibilityOff />
)}
</CollectionBarTileIcon>
);
}

View file

@ -15,7 +15,7 @@ const Wrapper = styled('button')<{ direction: SCROLL_DIRECTION }>`
border-radius: 50%;
background-color: ${({ theme }) => theme.palette.background.paper};
color: ${({ theme }) => theme.palette.text.primary};
color: ${({ theme }) => theme.palette.primary.main};
${(props) =>
props.direction === SCROLL_DIRECTION.LEFT

View file

@ -0,0 +1,125 @@
import ScrollButton from 'components/Collections/CollectionListBar/ScrollButton';
import React, { useContext, useEffect } from 'react';
import constants from 'utils/strings/constants';
import { ALL_SECTION, COLLECTION_SORT_BY } from 'constants/collection';
import { Box, IconButton, Typography } from '@mui/material';
import {
CollectionListBarWrapper,
ScrollContainer,
CollectionListWrapper,
} from 'components/Collections/styledComponents';
import CollectionListBarCard from 'components/Collections/CollectionListBar/CollectionCard';
import useComponentScroll, { SCROLL_DIRECTION } from 'hooks/useComponentScroll';
import useWindowSize from 'hooks/useWindowSize';
import { IconButtonWithBG, SpaceBetweenFlex } from 'components/Container';
import ExpandMore from '@mui/icons-material/ExpandMore';
import { AppContext } from 'pages/_app';
import { CollectionSummary } from 'types/collection';
import CollectionSort from '../AllCollections/CollectionSort';
interface IProps {
activeCollection?: number;
setActiveCollection: (id?: number) => void;
collectionSummaries: CollectionSummary[];
showAllCollections: () => void;
collectionSortBy: COLLECTION_SORT_BY;
setCollectionSortBy: (v: COLLECTION_SORT_BY) => void;
}
export default function CollectionListBar(props: IProps) {
const {
activeCollection,
setActiveCollection,
collectionSummaries,
showAllCollections,
} = props;
const appContext = useContext(AppContext);
const windowSize = useWindowSize();
const { componentRef, scrollComponent, onFarLeft, onFarRight } =
useComponentScroll({
dependencies: [windowSize, collectionSummaries],
});
const collectionChipsRef = collectionSummaries.reduce(
(refMap, collectionSummary) => {
refMap[collectionSummary.id] = React.createRef();
return refMap;
},
{}
);
useEffect(() => {
collectionChipsRef[activeCollection]?.current.scrollIntoView();
}, [activeCollection]);
const clickHandler = (collectionID?: number) => () => {
setActiveCollection(collectionID ?? ALL_SECTION);
};
return (
<CollectionListBarWrapper>
<SpaceBetweenFlex mb={1}>
<Typography>{constants.ALBUMS}</Typography>
{appContext.isMobile && (
<Box display="flex" alignItems={'center'} gap={1}>
<CollectionSort
setCollectionSortBy={props.setCollectionSortBy}
activeSortBy={props.collectionSortBy}
disableBG
/>
<IconButton onClick={showAllCollections}>
<ExpandMore />
</IconButton>
</Box>
)}
</SpaceBetweenFlex>
<Box display="flex" alignItems="flex-start" gap={2}>
<CollectionListWrapper>
{!onFarLeft && (
<ScrollButton
scrollDirection={SCROLL_DIRECTION.LEFT}
onClick={scrollComponent(SCROLL_DIRECTION.LEFT)}
/>
)}
<ScrollContainer ref={componentRef}>
{collectionSummaries.map((item) => (
<CollectionListBarCard
key={item.id}
latestFile={item.latestFile}
ref={collectionChipsRef[item.id]}
active={activeCollection === item.id}
onClick={clickHandler(item.id)}
collectionType={item.type}
collectionName={item.name}
/>
))}
</ScrollContainer>
{!onFarRight && (
<ScrollButton
scrollDirection={SCROLL_DIRECTION.RIGHT}
onClick={scrollComponent(SCROLL_DIRECTION.RIGHT)}
/>
)}
</CollectionListWrapper>
{!appContext.isMobile && (
<Box
display="flex"
alignItems={'center'}
gap={1}
height={'64px'}>
<CollectionSort
setCollectionSortBy={props.setCollectionSortBy}
activeSortBy={props.collectionSortBy}
/>
<IconButtonWithBG onClick={showAllCollections}>
<ExpandMore />
</IconButtonWithBG>
</Box>
)}
</Box>
</CollectionListBarWrapper>
);
}

View file

@ -1,9 +1,10 @@
import React from 'react';
import constants from 'utils/strings/constants';
import DialogBox from 'components/DialogBox';
import SingleInputForm, {
SingleInputFormProps,
} from 'components/SingleInputForm';
import DialogBoxBase from 'components/DialogBox/base';
import { DialogContent, DialogTitle } from '@mui/material';
export interface CollectionNamerAttributes {
callback: (name: string) => void;
@ -39,19 +40,19 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
};
return (
<DialogBox
open={props.show}
attributes={{ title: attributes.title }}
onClose={props.onHide}
titleCloseButton
maxWidth="xs">
<DialogBoxBase open={props.show} onClose={props.onHide}>
<DialogTitle>{attributes.title}</DialogTitle>
<DialogContent>
<SingleInputForm
callback={onSubmit}
fieldType="text"
buttonText={attributes.buttonText}
placeholder={constants.ENTER_ALBUM_NAME}
initialValue={attributes.autoFilledName}
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
secondaryButtonAction={props.onHide}
/>
</DialogBox>
</DialogContent>
</DialogBoxBase>
);
}

View file

@ -0,0 +1,76 @@
import { OverflowMenuOption } from 'components/OverflowMenu/option';
import React from 'react';
import EditIcon from '@mui/icons-material/Edit';
import IosShareIcon from '@mui/icons-material/IosShare';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import VisibilityOnOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import constants from 'utils/strings/constants';
import { CollectionActions } from '.';
interface Iprops {
IsArchived: boolean;
handleCollectionAction: (
action: CollectionActions,
loader?: boolean
) => (...args: any[]) => Promise<void>;
}
export function AlbumCollectionOption({
IsArchived,
handleCollectionAction,
}: Iprops) {
return (
<>
<OverflowMenuOption
onClick={handleCollectionAction(
CollectionActions.SHOW_SHARE_DIALOG,
false
)}
startIcon={<IosShareIcon />}>
{constants.SHARE}
</OverflowMenuOption>
<OverflowMenuOption
onClick={handleCollectionAction(
CollectionActions.CONFIRM_DOWNLOAD,
false
)}
startIcon={<FileDownloadOutlinedIcon />}>
{constants.DOWNLOAD}
</OverflowMenuOption>
<OverflowMenuOption
onClick={handleCollectionAction(
CollectionActions.SHOW_RENAME_DIALOG,
false
)}
startIcon={<EditIcon />}>
{constants.RENAME}
</OverflowMenuOption>
{IsArchived ? (
<OverflowMenuOption
onClick={handleCollectionAction(
CollectionActions.UNARCHIVE
)}
startIcon={<VisibilityOnOutlinedIcon />}>
{constants.UNARCHIVE}
</OverflowMenuOption>
) : (
<OverflowMenuOption
onClick={handleCollectionAction(CollectionActions.ARCHIVE)}
startIcon={<VisibilityOffOutlinedIcon />}>
{constants.ARCHIVE}
</OverflowMenuOption>
)}
<OverflowMenuOption
startIcon={<DeleteOutlinedIcon />}
onClick={handleCollectionAction(
CollectionActions.CONFIRM_DELETE,
false
)}>
{constants.DELETE}
</OverflowMenuOption>
</>
);
}

View file

@ -0,0 +1,27 @@
import { OverflowMenuOption } from 'components/OverflowMenu/option';
import React from 'react';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import constants from 'utils/strings/constants';
import { CollectionActions } from '.';
interface Iprops {
handleCollectionAction: (
action: CollectionActions,
loader?: boolean
) => (...args: any[]) => Promise<void>;
}
export function TrashCollectionOption({ handleCollectionAction }: Iprops) {
return (
<OverflowMenuOption
color="danger"
startIcon={<DeleteOutlinedIcon />}
onClick={handleCollectionAction(
CollectionActions.CONFIRM_EMPTY_TRASH,
false
)}>
{constants.EMPTY_TRASH}
</OverflowMenuOption>
);
}

View file

@ -1,11 +1,13 @@
import { AlbumCollectionOption } from './AlbumCollectionOption';
import React, { useContext } from 'react';
import * as CollectionAPI from 'services/collectionService';
import * as TrashService from 'services/trashService';
import {
changeCollectionVisibility,
downloadAllCollectionFiles,
} from 'utils/collection';
import constants from 'utils/strings/constants';
import { SetCollectionNamerAttributes } from './CollectionNamer';
import { SetCollectionNamerAttributes } from '../CollectionNamer';
import { Collection } from 'types/collection';
import { IsArchived } from 'utils/magicMetadata';
import { GalleryContext } from 'pages/gallery';
@ -13,41 +15,60 @@ import { logError } from 'utils/sentry';
import { VISIBILITY_STATE } from 'types/magicMetadata';
import { AppContext } from 'pages/_app';
import OverflowMenu from 'components/OverflowMenu/menu';
import { OverflowMenuOption } from 'components/OverflowMenu/option';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { CollectionSummaryType } from 'constants/collection';
import { TrashCollectionOption } from './TrashCollectionOption';
import MoreHoriz from '@mui/icons-material/MoreHoriz';
interface CollectionOptionsProps {
setCollectionNamerAttributes: SetCollectionNamerAttributes;
activeCollection: Collection;
collectionSummaryType: CollectionSummaryType;
showCollectionShareModal: () => void;
redirectToAll: () => void;
}
enum CollectionActions {
export enum CollectionActions {
SHOW_RENAME_DIALOG,
RENAME,
CONFIRM_DOWNLOAD,
DOWNLOAD,
ARCHIVE,
UNARCHIVE,
CONFIRM_DELETE,
DELETE,
SHOW_SHARE_DIALOG,
CONFIRM_EMPTY_TRASH,
EMPTY_TRASH,
}
const CollectionOptions = (props: CollectionOptionsProps) => {
const {
activeCollection,
collectionSummaryType,
redirectToAll,
setCollectionNamerAttributes,
showCollectionShareModal,
} = props;
const { startLoading, finishLoading, setDialogMessage } =
useContext(AppContext);
const { syncWithRemote } = useContext(GalleryContext);
const handleCollectionAction = (action: CollectionActions) => {
const handleCollectionAction = (
action: CollectionActions,
loader = true
) => {
let callback;
switch (action) {
case CollectionActions.SHOW_RENAME_DIALOG:
callback = showRenameCollectionModal;
break;
case CollectionActions.RENAME:
callback = renameCollection;
break;
case CollectionActions.CONFIRM_DOWNLOAD:
callback = confirmDownloadCollection;
break;
case CollectionActions.DOWNLOAD:
callback = downloadCollection;
break;
@ -57,9 +78,21 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
case CollectionActions.UNARCHIVE:
callback = unArchiveCollection;
break;
case CollectionActions.CONFIRM_DELETE:
callback = confirmDeleteCollection;
break;
case CollectionActions.DELETE:
callback = deleteCollection;
break;
case CollectionActions.SHOW_SHARE_DIALOG:
callback = showCollectionShareModal;
break;
case CollectionActions.CONFIRM_EMPTY_TRASH:
callback = confirmEmptyTrash;
break;
case CollectionActions.EMPTY_TRASH:
callback = emptyTrash;
break;
default:
logError(
Error('invalid collection action '),
@ -70,8 +103,8 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
}
}
return async (...args) => {
startLoading();
try {
loader && startLoading();
await callback(...args);
} catch (e) {
setDialogMessage({
@ -79,10 +112,10 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
content: constants.UNKNOWN_ERROR,
close: { variant: 'danger' },
});
} finally {
syncWithRemote(false, true);
loader && finishLoading();
}
syncWithRemote();
finishLoading();
};
};
@ -109,6 +142,12 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
downloadAllCollectionFiles(activeCollection.id);
};
const emptyTrash = async () => {
await TrashService.emptyTrash();
await TrashService.clearLocalTrash();
redirectToAll();
};
const showRenameCollectionModal = () => {
setCollectionNamerAttributes({
title: constants.RENAME_COLLECTION,
@ -120,8 +159,8 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
const confirmDeleteCollection = () => {
setDialogMessage({
title: constants.CONFIRM_DELETE_COLLECTION,
content: constants.DELETE_COLLECTION_MESSAGE(),
title: constants.DELETE_COLLECTION_TITLE,
content: constants.DELETE_COLLECTION_MESSAGE,
proceed: {
text: constants.DELETE_COLLECTION,
action: handleCollectionAction(CollectionActions.DELETE),
@ -148,42 +187,38 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
});
};
const confirmEmptyTrash = () =>
setDialogMessage({
title: constants.EMPTY_TRASH_TITLE,
content: constants.EMPTY_TRASH_MESSAGE,
proceed: {
action: handleCollectionAction(CollectionActions.EMPTY_TRASH),
text: constants.EMPTY_TRASH,
variant: 'danger',
},
close: { text: constants.CANCEL },
});
return (
<OverflowMenu
ariaControls={`collection-options-${props.activeCollection.id}`}
triggerButtonIcon={<MoreVertIcon />}
ariaControls={'collection-options'}
triggerButtonIcon={<MoreHoriz />}
triggerButtonProps={{
sx: {
background: (theme) => theme.palette.background.paper,
background: (theme) => theme.palette.fill.dark,
},
}}>
<OverflowMenuOption onClick={showRenameCollectionModal}>
{constants.RENAME}
</OverflowMenuOption>
<OverflowMenuOption onClick={showCollectionShareModal}>
{constants.SHARE}
</OverflowMenuOption>
<OverflowMenuOption onClick={confirmDownloadCollection}>
{constants.DOWNLOAD}
</OverflowMenuOption>
{IsArchived(activeCollection) ? (
<OverflowMenuOption
onClick={handleCollectionAction(
CollectionActions.UNARCHIVE
)}>
{constants.UNARCHIVE}
</OverflowMenuOption>
{collectionSummaryType === CollectionSummaryType.trash ? (
<TrashCollectionOption
handleCollectionAction={handleCollectionAction}
/>
) : (
<OverflowMenuOption
onClick={handleCollectionAction(CollectionActions.ARCHIVE)}>
{constants.ARCHIVE}
</OverflowMenuOption>
<AlbumCollectionOption
IsArchived={IsArchived(activeCollection)}
handleCollectionAction={handleCollectionAction}
/>
)}
<OverflowMenuOption
color="danger"
onClick={confirmDeleteCollection}>
{constants.DELETE}
</OverflowMenuOption>
</OverflowMenu>
);
};

View file

@ -0,0 +1,34 @@
import CollectionCard from 'components/Collections/CollectionCard';
import {
AllCollectionTile,
AllCollectionTileText,
} from 'components/Collections/styledComponents';
import React from 'react';
import { styled } from '@mui/material';
import constants from 'utils/strings/constants';
import { CenteredFlex, Overlay } from 'components/Container';
const ImageContainer = styled(Overlay)`
display: flex;
font-size: 42px;
`;
interface Iprops {
showNextModal: () => void;
}
export default function AddCollectionButton({ showNextModal }: Iprops) {
return (
<CollectionCard
collectionTile={AllCollectionTile}
onClick={() => showNextModal()}
latestFile={null}>
<AllCollectionTileText>
{constants.CREATE_COLLECTION}
</AllCollectionTileText>
<ImageContainer>
<CenteredFlex>+</CenteredFlex>
</ImageContainer>
</CollectionCard>
);
}

View file

@ -0,0 +1,26 @@
import { Typography } from '@mui/material';
import React from 'react';
import CollectionCard from '../CollectionCard';
import { CollectionSummary } from 'types/collection';
import { AllCollectionTile, AllCollectionTileText } from '../styledComponents';
interface Iprops {
collectionSummary: CollectionSummary;
onCollectionClick: (collectionID: number) => void;
}
export default function CollectionSelectorCard({
onCollectionClick,
collectionSummary,
}: Iprops) {
return (
<CollectionCard
collectionTile={AllCollectionTile}
latestFile={collectionSummary.latestFile}
onClick={() => onCollectionClick(collectionSummary.id)}>
<AllCollectionTileText>
<Typography>{collectionSummary.name}</Typography>
</AllCollectionTileText>
</CollectionCard>
);
}

View file

@ -1,13 +1,13 @@
import React, { useEffect, useMemo } from 'react';
import AddCollectionButton from './AddCollectionButton';
import React, { useContext, useEffect, useMemo } from 'react';
import { Collection, CollectionSummaries } from 'types/collection';
import DialogBoxBase from 'components/DialogBox/base';
import DialogTitleWithCloseButton from 'components/DialogBox/titleWithCloseButton';
import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton';
import { isUploadAllowedCollection } from 'utils/collection';
import { AppContext } from 'pages/_app';
import { AllCollectionDialog } from 'components/Collections/AllCollections/dialog';
import { DialogContent } from '@mui/material';
import { FlexWrapper } from 'components/Container';
import { CollectionSelectorTile } from 'components/Collections/styledComponents';
import AllCollectionCard from 'components/Collections/AllCollections/CollectionCard';
import { isSystemCollection } from 'utils/collection';
import CollectionSelectorCard from './CollectionCard';
import AddCollectionButton from './AddCollectionButton';
export interface CollectionSelectorAttributes {
callback: (collection: Collection) => void;
@ -15,9 +15,6 @@ export interface CollectionSelectorAttributes {
title: string;
fromCollection?: number;
}
export type SetCollectionSelectorAttributes = React.Dispatch<
React.SetStateAction<CollectionSelectorAttributes>
>;
interface Props {
open: boolean;
@ -32,12 +29,14 @@ function CollectionSelector({
collections,
...props
}: Props) {
const appContext = useContext(AppContext);
const collectionToShow = useMemo(() => {
const personalCollectionsOtherThanFrom = [
...collectionSummaries.values(),
]?.filter(
({ type, id }) =>
id !== attributes?.fromCollection && !isSystemCollection(type)
id !== attributes?.fromCollection &&
isUploadAllowedCollection(type)
);
return personalCollectionsOtherThanFrom;
}, [collectionSummaries, attributes]);
@ -62,27 +61,24 @@ function CollectionSelector({
props.onClose();
};
const onCloseButtonClick = () => props.onClose(true);
return (
<DialogBoxBase
{...props}
maxWidth="md"
PaperProps={{ sx: { maxWidth: '848px' } }}>
<DialogTitleWithCloseButton onClose={() => props.onClose(true)}>
<AllCollectionDialog
onClose={props.onClose}
open={props.open}
position="center"
fullScreen={appContext.isMobile}>
<DialogTitleWithCloseButton onClose={onCloseButtonClick}>
{attributes.title}
</DialogTitleWithCloseButton>
<DialogContent
sx={{
'&&&': {
px: 0,
},
}}>
<FlexWrapper style={{ flexWrap: 'wrap' }}>
<DialogContent>
<FlexWrapper flexWrap="wrap" gap={0.5}>
<AddCollectionButton
showNextModal={attributes.showNextModal}
/>
{collectionToShow.map((collectionSummary) => (
<AllCollectionCard
collectionTile={CollectionSelectorTile}
<CollectionSelectorCard
onCollectionClick={handleCollectionClick}
collectionSummary={collectionSummary}
key={collectionSummary.id}
@ -90,7 +86,7 @@ function CollectionSelector({
))}
</FlexWrapper>
</DialogContent>
</DialogBoxBase>
</AllCollectionDialog>
);
}

View file

@ -6,7 +6,10 @@ export const CollectionShareContainer = styled(Dialog)(({ theme }) => ({
justifyContent: 'flex-end',
},
'& .MuiPaper-root': {
maxWidth: '414px',
width: '414px',
},
'& .MuiDialog-paperFullScreen': {
maxWidth: '100%',
},
'& .MuiDialogTitle-root': {
padding: theme.spacing(4, 3, 3, 4),

View file

@ -1,34 +1,37 @@
import SingleInputForm from 'components/SingleInputForm';
import SingleInputForm, {
SingleInputFormProps,
} from 'components/SingleInputForm';
import { GalleryContext } from 'pages/gallery';
import React, { useContext } from 'react';
import { shareCollection } from 'services/collectionService';
import { User } from 'types/user';
import { sleep } from 'utils/common';
import { handleSharingErrors } from 'utils/error';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import constants from 'utils/strings/constants';
import { CollectionShareSharees } from './sharees';
import CollectionShareSubmitButton from './submitButton';
export default function EmailShare({ collection }) {
const galleryContext = useContext(GalleryContext);
const collectionShare = async (email, setFieldError) => {
const collectionShare: SingleInputFormProps['callback'] = async (
email,
setFieldError
) => {
try {
const user: User = getData(LS_KEYS.USER);
if (email === user.email) {
setFieldError('email', constants.SHARE_WITH_SELF);
setFieldError(constants.SHARE_WITH_SELF);
} else if (
collection?.sharees?.find((value) => value.email === email)
) {
setFieldError('email', constants.ALREADY_SHARED(email));
setFieldError(constants.ALREADY_SHARED(email));
} else {
await shareCollection(collection, email);
await sleep(2000);
await galleryContext.syncWithRemote(false, true);
}
} catch (e) {
const errorMessage = handleSharingErrors(e);
setFieldError('email', errorMessage);
setFieldError(errorMessage);
}
};
return (
@ -38,7 +41,11 @@ export default function EmailShare({ collection }) {
placeholder={constants.ENTER_EMAIL}
fieldType="email"
buttonText={constants.SHARE}
customSubmitButton={CollectionShareSubmitButton}
submitButtonProps={{
size: 'medium',
sx: { mt: 1, mb: 2 },
}}
disableAutoFocus
/>
<CollectionShareSharees collection={collection} />
</>

View file

@ -1,24 +1,27 @@
import EmailShare from './emailShare';
import React from 'react';
import React, { useContext } from 'react';
import constants from 'utils/strings/constants';
import { Collection } from 'types/collection';
import { dialogCloseHandler } from 'components/DialogBox/base';
import DialogTitleWithCloseButton from 'components/DialogBox/titleWithCloseButton';
import DialogTitleWithCloseButton, {
dialogCloseHandler,
} from 'components/DialogBox/TitleWithCloseButton';
import DialogContent from '@mui/material/DialogContent';
import { Divider } from '@mui/material';
import { CollectionShareContainer } from './container';
import PublicShare from './publicShare';
import { AppContext } from 'pages/_app';
interface Props {
show: boolean;
onHide: () => void;
open: boolean;
onClose: () => void;
collection: Collection;
}
function CollectionShare(props: Props) {
const { isMobile } = useContext(AppContext);
const handleClose = dialogCloseHandler({
onClose: props.onHide,
onClose: props.onClose,
});
if (!props.collection) {
@ -27,7 +30,10 @@ function CollectionShare(props: Props) {
return (
<>
<CollectionShareContainer open={props.show} onClose={handleClose}>
<CollectionShareContainer
open={props.open}
onClose={handleClose}
fullScreen={isMobile}>
<DialogTitleWithCloseButton onClose={handleClose}>
{constants.SHARE_COLLECTION}
</DialogTitleWithCloseButton>

View file

@ -1,37 +1,36 @@
import { Box, Typography } from '@mui/material';
import { FlexWrapper } from 'components/Container';
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
import { GalleryContext } from 'pages/gallery';
import { AppContext } from 'pages/_app';
import React, { useContext } from 'react';
import React, { useContext, useState } from 'react';
import {
createShareableURL,
deleteShareableURL,
} from 'services/collectionService';
import { appendCollectionKeyToShareURL } from 'utils/collection';
import { Collection, PublicURL } from 'types/collection';
import { handleSharingErrors } from 'utils/error';
import constants from 'utils/strings/constants';
import PublicShareSwitch from './switch';
interface Iprops {
collection: Collection;
publicShareActive: boolean;
setPublicShareProp: (value: PublicURL) => void;
}
export default function PublicShareControl({
publicShareUrl,
sharableLinkError,
collection,
setPublicShareUrl,
setSharableLinkError,
}) {
publicShareActive,
setPublicShareProp,
}: Iprops) {
const appContext = useContext(AppContext);
const galleryContext = useContext(GalleryContext);
const [sharableLinkError, setSharableLinkError] = useState(null);
const createSharableURLHelper = async () => {
try {
appContext.startLoading();
const publicURL = await createShareableURL(collection);
const sharableURL = await appendCollectionKeyToShareURL(
publicURL.url,
collection.key
);
setPublicShareUrl(sharableURL);
galleryContext.syncWithRemote(false, true);
setPublicShareProp(publicURL);
} catch (e) {
const errorMessage = handleSharingErrors(e);
setSharableLinkError(errorMessage);
@ -44,8 +43,7 @@ export default function PublicShareControl({
try {
appContext.startLoading();
await deleteShareableURL(collection);
setPublicShareUrl(null);
galleryContext.syncWithRemote(false, true);
setPublicShareProp(null);
} catch (e) {
const errorMessage = handleSharingErrors(e);
setSharableLinkError(errorMessage);
@ -69,8 +67,7 @@ export default function PublicShareControl({
const handleCollectionPublicSharing = () => {
setSharableLinkError(null);
if (publicShareUrl) {
if (publicShareActive) {
confirmDisablePublicSharing();
} else {
createSharableURLHelper();
@ -86,7 +83,7 @@ export default function PublicShareControl({
sx={{
ml: 2,
}}
checked={!!publicShareUrl}
checked={publicShareActive}
onChange={handleCollectionPublicSharing}
/>
</FlexWrapper>

View file

@ -1,51 +1,53 @@
import React, { useEffect, useState } from 'react';
import { PublicURL } from 'types/collection';
import { Collection, PublicURL } from 'types/collection';
import { appendCollectionKeyToShareURL } from 'utils/collection';
import PublicShareControl from './control';
import PublicShareLink from './link';
import PublicShareManage from './manage';
export default function PublicShare({ collection }) {
const [sharableLinkError, setSharableLinkError] = useState(null);
export default function PublicShare({
collection,
}: {
collection: Collection;
}) {
const [publicShareUrl, setPublicShareUrl] = useState<string>(null);
const [publicShareProp, setPublicShareProp] = useState<PublicURL>(null);
useEffect(() => {
const main = async () => {
if (collection?.publicURLs?.[0]?.url) {
const t = await appendCollectionKeyToShareURL(
collection?.publicURLs?.[0]?.url,
if (collection.publicURLs?.length) {
setPublicShareProp(collection.publicURLs[0]);
}
}, [collection]);
useEffect(() => {
if (publicShareProp) {
const url = appendCollectionKeyToShareURL(
publicShareProp.url,
collection.key
);
setPublicShareUrl(t);
setPublicShareProp(collection?.publicURLs?.[0] as PublicURL);
setPublicShareUrl(url);
} else {
setPublicShareUrl(null);
setPublicShareProp(null);
}
};
main();
}, [collection]);
}, [publicShareProp]);
return (
<>
<PublicShareControl
setPublicShareUrl={setPublicShareUrl}
setPublicShareProp={setPublicShareProp}
collection={collection}
publicShareUrl={publicShareUrl}
sharableLinkError={sharableLinkError}
setSharableLinkError={setSharableLinkError}
publicShareActive={!!publicShareProp}
/>
{publicShareUrl && (
<PublicShareLink publicShareUrl={publicShareUrl} />
)}
{publicShareProp && (
<>
<PublicShareLink publicShareUrl={publicShareUrl} />
<PublicShareManage
publicShareProp={publicShareProp}
collection={collection}
setPublicShareProp={setPublicShareProp}
setSharableLinkError={setSharableLinkError}
/>
</>
)}
</>
);

View file

@ -20,7 +20,7 @@ export function ManageDeviceLimit({
return (
<Box>
<Typography>{constants.LINK_DEVICE_LIMIT}</Typography>
<Typography mb={0.5}>{constants.LINK_DEVICE_LIMIT}</Typography>
<Select
menuPosition="fixed"
options={getDeviceLimitOptions()}

View file

@ -25,7 +25,7 @@ export function ManageDownloadAccess({
const disableFileDownload = () => {
appContext.setDialogMessage({
title: constants.DISABLE_FILE_DOWNLOAD,
content: constants.DISABLE_FILE_DOWNLOAD_MESSAGE,
content: constants.DISABLE_FILE_DOWNLOAD_MESSAGE(),
close: { text: constants.CANCEL },
proceed: {
text: constants.DISABLE,
@ -40,7 +40,7 @@ export function ManageDownloadAccess({
};
return (
<Box>
<Typography>{constants.FILE_DOWNLOAD}</Typography>
<Typography mb={0.5}>{constants.FILE_DOWNLOAD}</Typography>
<PublicShareSwitch
checked={publicShareProp?.enableDownload ?? false}
onChange={handleFileDownloadSetting}

View file

@ -2,7 +2,7 @@ import { ManageLinkPassword } from './linkPassword';
import { ManageDeviceLimit } from './deviceLimit';
import { ManageLinkExpiry } from './linkExpiry';
import { PublicLinkSetPassword } from '../setPassword';
import { Stack } from '@mui/material';
import { Stack, Typography } from '@mui/material';
import { GalleryContext } from 'pages/gallery';
import React, { useContext, useState } from 'react';
import { updateShareableURL } from 'services/collectionService';
@ -14,17 +14,17 @@ import {
ManageSectionLabel,
ManageSectionOptions,
} from '../../styledComponents';
import { ManageDownloadAccess } from './downloadAcess';
import { ManageDownloadAccess } from './downloadAccess';
export default function PublicShareManage({
publicShareProp,
collection,
setPublicShareProp,
setSharableLinkError,
}) {
const galleryContext = useContext(GalleryContext);
const [changePasswordView, setChangePasswordView] = useState(false);
const [sharableLinkError, setSharableLinkError] = useState(null);
const closeConfigurePassword = () => setChangePasswordView(false);
@ -33,7 +33,6 @@ export default function PublicShareManage({
galleryContext.setBlockingLoad(true);
const response = await updateShareableURL(req);
setPublicShareProp(response);
galleryContext.syncWithRemote(false, true);
} catch (e) {
const errorMessage = handleSharingErrors(e);
setSharableLinkError(errorMessage);
@ -59,7 +58,7 @@ export default function PublicShareManage({
{constants.MANAGE_LINK}
</ManageSectionLabel>
<ManageSectionOptions>
<Stack spacing={1}>
<Stack spacing={1.5}>
<ManageLinkExpiry
collection={collection}
publicShareProp={publicShareProp}
@ -90,6 +89,17 @@ export default function PublicShareManage({
}
/>
</Stack>
{sharableLinkError && (
<Typography
textAlign={'center'}
variant="body2"
sx={{
color: (theme) => theme.palette.danger.main,
mt: 0.5,
}}>
{sharableLinkError}
</Typography>
)}
</ManageSectionOptions>
</details>
<PublicLinkSetPassword

View file

@ -20,7 +20,7 @@ export function ManageLinkExpiry({
};
return (
<Box>
<Typography>{constants.LINK_EXPIRY}</Typography>
<Typography mb={0.5}>{constants.LINK_EXPIRY}</Typography>
<Select
menuPosition="fixed"
options={shareExpiryOptions}

View file

@ -39,7 +39,7 @@ export function ManageLinkPassword({
return (
<Box>
<Typography> {constants.LINK_PASSWORD_LOCK}</Typography>
<Typography mb={0.5}> {constants.LINK_PASSWORD_LOCK}</Typography>
<PublicShareSwitch
checked={!!publicShareProp?.passwordEnabled}
onChange={handlePasswordChangeSetting}

View file

@ -1,8 +1,11 @@
import DialogBox from 'components/DialogBox';
import SingleInputForm from 'components/SingleInputForm';
import { Dialog, Stack, Typography } from '@mui/material';
import SingleInputForm, {
SingleInputFormProps,
} from 'components/SingleInputForm';
import React from 'react';
import CryptoWorker from 'utils/crypto';
import constants from 'utils/strings/constants';
export function PublicLinkSetPassword({
open,
onClose,
@ -11,13 +14,16 @@ export function PublicLinkSetPassword({
updatePublicShareURLHelper,
setChangePasswordView,
}) {
const savePassword = async (passphrase, setFieldError) => {
const savePassword: SingleInputFormProps['callback'] = async (
passphrase,
setFieldError
) => {
if (passphrase && passphrase.trim().length >= 1) {
await enablePublicUrlPassword(passphrase);
setChangePasswordView(false);
publicShareProp.passwordEnabled = true;
} else {
setFieldError('linkPassword', 'can not be empty');
setFieldError('can not be empty');
}
};
@ -35,20 +41,26 @@ export function PublicLinkSetPassword({
});
};
return (
<DialogBox
<Dialog
open={open}
onClose={onClose}
PaperProps={{ sx: { maxWidth: '350px' } }}
titleCloseButton
attributes={{
title: constants.PASSWORD_LOCK,
}}>
disablePortal
BackdropProps={{ sx: { position: 'absolute' } }}
sx={{ position: 'absolute' }}
PaperProps={{ sx: { p: 1 } }}>
<Stack spacing={3} p={1.5}>
<Typography variant="h3" px={1} py={0.5} fontWeight={'bold'}>
{constants.PASSWORD_LOCK}
</Typography>
<SingleInputForm
callback={savePassword}
placeholder={constants.RETURN_PASSPHRASE_HINT}
buttonText={constants.LOCK}
fieldType="password"
secondaryButtonAction={onClose}
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
/>
</DialogBox>
</Stack>
</Dialog>
);
}

View file

@ -4,7 +4,6 @@ import { AppContext } from 'pages/_app';
import React, { useContext } from 'react';
import { unshareCollection } from 'services/collectionService';
import { Collection } from 'types/collection';
import { sleep } from 'utils/common';
import constants from 'utils/strings/constants';
import ShareeRow from './row';
@ -20,7 +19,6 @@ export function CollectionShareSharees({ collection }: Iprops) {
try {
appContext.startLoading();
await unshareCollection(collection, sharee.email);
await sleep(2000);
await galleryContext.syncWithRemote(false, true);
} finally {
appContext.finishLoading();

View file

@ -18,6 +18,12 @@ const ShareeRow = ({ sharee, collectionUnshare }: IProps) => {
<SpaceBetweenFlex>
{sharee.email}
<OverflowMenu
menuPaperProps={{
sx: {
backgroundColor: (theme) =>
theme.palette.background.overPaper,
},
}}
ariaControls={`email-share-${sharee.email}`}
triggerButtonIcon={<MoreHorizIcon />}>
<OverflowMenuOption

View file

@ -1,10 +0,0 @@
import { FlexWrapper } from 'components/Container';
import SubmitButton, { SubmitButtonProps } from 'components/SubmitButton';
import React from 'react';
export default function CollectionShareSubmitButton(props: SubmitButtonProps) {
return (
<FlexWrapper style={{ justifyContent: 'flex-end' }}>
<SubmitButton {...props} size="medium" inline sx={{ my: 2 }} />
</FlexWrapper>
);
}

View file

@ -1,13 +1,20 @@
import { Collection, CollectionSummaries } from 'types/collection';
import CollectionListBar from 'components/Collections/CollectionBar';
import React, { useEffect, useRef, useState } from 'react';
import CollectionListBar from 'components/Collections/CollectionListBar';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import AllCollections from 'components/Collections/AllCollections';
import CollectionInfoWithOptions from 'components/Collections/CollectionInfoWithOptions';
import { ALL_SECTION } from 'constants/collection';
import { ALL_SECTION, COLLECTION_SORT_BY } from 'constants/collection';
import CollectionShare from 'components/Collections/CollectionShare';
import { SetCollectionNamerAttributes } from 'components/Collections/CollectionNamer';
import { ITEM_TYPE, TimeStampListItem } from 'components/PhotoList';
import { hasNonEmptyCollections } from 'utils/collection';
import {
hasNonEmptyCollections,
isSystemCollection,
shouldBeShownOnCollectionBar,
} from 'utils/collection';
import { useLocalState } from 'hooks/useLocalState';
import { sortCollectionSummaries } from 'services/collectionService';
import { LS_KEYS } from 'utils/storage/localStorage';
interface Iprops {
collections: Collection[];
@ -33,6 +40,12 @@ export default function Collections(props: Iprops) {
const [allCollectionView, setAllCollectionView] = useState(false);
const [collectionShareModalView, setCollectionShareModalView] =
useState(false);
const [collectionSortBy, setCollectionSortBy] =
useLocalState<COLLECTION_SORT_BY>(
LS_KEYS.COLLECTION_SORT_BY,
COLLECTION_SORT_BY.UPDATION_TIME_DESCENDING
);
const collectionsMap = useRef<Map<number, Collection>>(new Map());
const activeCollection = useRef<Collection>(null);
@ -50,6 +63,15 @@ export default function Collections(props: Iprops) {
collectionsMap.current.get(activeCollectionID);
}, [activeCollectionID, collections]);
const sortedCollectionSummaries = useMemo(
() =>
sortCollectionSummaries(
[...collectionSummaries.values()],
collectionSortBy
),
[collectionSortBy, collectionSummaries]
);
useEffect(
() =>
!shouldBeHidden &&
@ -60,6 +82,7 @@ export default function Collections(props: Iprops) {
activeCollectionID
)}
activeCollection={activeCollection.current}
activeCollectionID={activeCollectionID}
setCollectionNamerAttributes={
setCollectionNamerAttributes
}
@ -69,7 +92,7 @@ export default function Collections(props: Iprops) {
}
/>
),
itemType: ITEM_TYPE.STATIC,
itemType: ITEM_TYPE.OTHER,
height: 68,
}),
[collectionSummaries, activeCollectionID, shouldBeHidden]
@ -79,25 +102,37 @@ export default function Collections(props: Iprops) {
return <></>;
}
const closeAllCollections = () => setAllCollectionView(false);
const openAllCollections = () => setAllCollectionView(true);
const closeCollectionShare = () => setCollectionShareModalView(false);
return (
<>
<CollectionListBar
activeCollection={activeCollectionID}
setActiveCollection={setActiveCollectionID}
collectionSummaries={collectionSummaries}
showAllCollections={() => setAllCollectionView(true)}
collectionSummaries={sortedCollectionSummaries.filter((x) =>
shouldBeShownOnCollectionBar(x.type)
)}
showAllCollections={openAllCollections}
setCollectionSortBy={setCollectionSortBy}
collectionSortBy={collectionSortBy}
/>
<AllCollections
isOpen={allCollectionView}
close={() => setAllCollectionView(false)}
collectionSummaries={collectionSummaries}
open={allCollectionView}
onClose={closeAllCollections}
collectionSummaries={sortedCollectionSummaries.filter(
(x) => !isSystemCollection(x.type)
)}
setActiveCollection={setActiveCollectionID}
setCollectionSortBy={setCollectionSortBy}
collectionSortBy={collectionSortBy}
/>
<CollectionShare
show={collectionShareModalView}
onHide={() => setCollectionShareModalView(false)}
open={collectionShareModalView}
onClose={closeCollectionShare}
collection={activeCollection.current}
/>
</>

View file

@ -1,5 +1,6 @@
import { Box } from '@mui/material';
import { styled } from '@mui/material';
import { Overlay } from 'components/Container';
import { SpecialPadding } from 'styles/SpecialPadding';
export const CollectionListWrapper = styled(Box)`
position: relative;
@ -9,10 +10,9 @@ export const CollectionListWrapper = styled(Box)`
`;
export const CollectionListBarWrapper = styled(Box)`
width: 100%;
${SpecialPadding}
margin-bottom: 16px;
border-bottom: 1px solid ${({ theme }) => theme.palette.divider};
${SpecialPadding}
`;
export const CollectionInfoBarWrapper = styled(Box)`
@ -22,10 +22,11 @@ export const CollectionInfoBarWrapper = styled(Box)`
export const ScrollContainer = styled('div')`
width: 100%;
height: 100px;
height: 120px;
overflow: auto;
scroll-behavior: smooth;
display: flex;
gap: 4px;
`;
export const CollectionTile = styled('div')`
@ -39,74 +40,53 @@ export const CollectionTile = styled('div')`
object-fit: cover;
width: 100%;
height: 100%;
flex: 1;
pointer-events: none;
}
`;
export const CollectionTileWrapper = styled('div')`
margin-right: 4px;
`;
export const ActiveIndicator = styled('div')`
height: 3px;
background-color: ${({ theme }) => theme.palette.text.primary};
background-color: ${({ theme }) => theme.palette.primary.main};
margin-top: 18px;
border-radius: 2px;
`;
export const Hider = styled('div')<{ hide: boolean }>`
display: ${(props) => (props.hide ? 'none' : 'block')};
`;
export const CollectionBarTile = styled(CollectionTile)`
width: 80px;
width: 90px;
height: 64px;
`;
export const AllCollectionTile = styled(CollectionTile)`
width: 150px;
height: 150px;
align-items: flex-start;
margin: 2px;
`;
export const CollectionTitleWithDashedBorder = styled(CollectionTile)`
border: 1px dashed ${({ theme }) => theme.palette.grey.A200};
`;
export const CollectionSelectorTile = styled(AllCollectionTile)`
height: 192px;
width: 192px;
margin: 10px;
`;
export const ResultPreviewTile = styled(AllCollectionTile)`
export const ResultPreviewTile = styled(CollectionTile)`
width: 48px;
height: 48px;
border-radius: 4px;
`;
export const CollectionTileTextOverlay = styled('div')`
height: 100%;
width: 100%;
position: absolute;
font-size: 14px;
line-height: 20px;
padding: 4px 6px;
`;
export const CollectionBarTileText = styled(CollectionTileTextOverlay)`
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.1) 0%,
rgba(0, 0, 0, 0.5) 86.46%
);
display: flex;
align-items: flex-end;
`;
export const AllCollectionTileText = styled(CollectionTileTextOverlay)`
export const CollectionBarTileText = styled(Overlay)`
padding: 4px;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.1) 0%,
rgba(0, 0, 0, 0.5) 86.46%
);
`;
export const CollectionBarTileIcon = styled(Overlay)`
padding: 4px;
display: flex;
justify-content: flex-end;
align-items: flex-end;
& > .MuiSvgIcon-root {
font-size: 20px;
}
`;
export const AllCollectionTileText = styled(Overlay)`
padding: 8px;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.1) 0%,

View file

@ -1,4 +1,4 @@
import { Box } from '@mui/material';
import { Box, IconButton } from '@mui/material';
import { styled } from '@mui/material';
const VerticallyCentered = styled(Box)`
@ -19,23 +19,6 @@ export const DisclaimerContainer = styled('div')`
font-size: 14px;
`;
export const IconButton = styled('button')`
background: none;
border: none;
border-radius: 50%;
padding: 5px;
color: inherit;
margin: 0 10px;
display: inline-flex;
align-items: center;
justify-content: center;
&:focus,
&:hover {
background-color: rgba(255, 255, 255, 0.2);
}
`;
export const Row = styled('div')`
min-height: 32px;
display: flex;
@ -80,22 +63,13 @@ export const FluidContainer = styled(FlexWrapper)`
`;
export const Overlay = styled(Box)`
display: flex;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1; ;
`;
export const InvertedIconButton = styled(IconButton)`
background-color: ${({ theme }) => theme.palette.primary.main};
color: ${({ theme }) => theme.palette.background.default};
&:hover {
background-color: ${({ theme }) => theme.palette.grey.A100};
}
&:focus {
background-color: ${({ theme }) => theme.palette.primary.main};
}
`;
export const IconButtonWithBG = styled(IconButton)(({ theme }) => ({
backgroundColor: theme.palette.fill.dark,
}));

View file

@ -0,0 +1,54 @@
import React from 'react';
import {
DialogProps,
DialogTitle,
IconButton,
Typography,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { SpaceBetweenFlex } from 'components/Container';
const DialogTitleWithCloseButton = (props) => {
const { children, onClose, ...other } = props;
return (
<DialogTitle {...other}>
<SpaceBetweenFlex>
<Typography variant="h3" fontWeight={'bold'}>
{children}
</Typography>
{onClose && (
<IconButton
aria-label="close"
onClick={onClose}
sx={{ float: 'right' }}
color="secondary">
<CloseIcon />
</IconButton>
)}
</SpaceBetweenFlex>
</DialogTitle>
);
};
export default DialogTitleWithCloseButton;
export const dialogCloseHandler =
({
staticBackdrop,
nonClosable,
onClose,
}: {
staticBackdrop?: boolean;
nonClosable?: boolean;
onClose: () => void;
}): DialogProps['onClose'] =>
(_, reason) => {
if (nonClosable) {
// no-op
} else if (staticBackdrop && reason === 'backdropClick') {
// no-op
} else {
onClose();
}
};

View file

@ -1,51 +1,23 @@
import { Dialog, DialogProps, styled } from '@mui/material';
import { Dialog, styled } from '@mui/material';
const DialogBoxBase = styled(Dialog)(({ theme }) => ({
'& .MuiDialog-paper': {
padding: theme.spacing(2, 0),
padding: theme.spacing(1, 1.5),
maxWidth: '346px',
},
'& .MuiDialogTitle-root': {
padding: theme.spacing(2, 3),
padding: theme.spacing(2),
paddingBottom: theme.spacing(1),
},
'& .MuiDialogContent-root': {
padding: theme.spacing(2, 3),
},
'& .MuiDialogActions-root': {
padding: theme.spacing(2, 3),
},
'& .MuiDialogActions-root button': {
fontSize: '18px',
lineHeight: '21.78px',
padding: theme.spacing(2),
},
'& .MuiDialogActions-root button:not(:first-child)': {
marginLeft: theme.spacing(2),
'.MuiDialogTitle-root + .MuiDialogContent-root': {
paddingTop: 0,
},
'.MuiDialogTitle-root + .MuiDialogActions-root': {
paddingTop: theme.spacing(3),
},
}));
DialogBoxBase.defaultProps = {
fullWidth: true,
maxWidth: 'sm',
};
export const dialogCloseHandler =
({
staticBackdrop,
nonClosable,
onClose,
}: {
staticBackdrop?: boolean;
nonClosable?: boolean;
onClose: () => void;
}): DialogProps['onClose'] =>
(_, reason) => {
if (nonClosable) {
// no-op
} else if (staticBackdrop && reason === 'backdropClick') {
// no-op
} else {
onClose();
}
};
export default DialogBoxBase;

View file

@ -6,10 +6,12 @@ import {
DialogActions,
DialogContent,
DialogProps,
Typography,
} from '@mui/material';
import DialogTitleWithCloseButton from './titleWithCloseButton';
import MessageText from './messageText';
import DialogBoxBase, { dialogCloseHandler } from './base';
import DialogTitleWithCloseButton, {
dialogCloseHandler,
} from './TitleWithCloseButton';
import DialogBoxBase from './base';
import { DialogBoxAttributes } from 'types/dialogBox';
type IProps = React.PropsWithChildren<
@ -59,7 +61,9 @@ export default function DialogBox({
{(children || attributes?.content) && (
<DialogContent>
{children || (
<MessageText>{attributes.content}</MessageText>
<Typography color="text.secondary">
{attributes.content}
</Typography>
)}
</DialogContent>
)}
@ -68,6 +72,7 @@ export default function DialogBox({
<>
{attributes.close && (
<Button
size="large"
color={attributes.close?.variant ?? 'secondary'}
onClick={() => {
attributes.close.action &&
@ -79,6 +84,7 @@ export default function DialogBox({
)}
{attributes.proceed && (
<Button
size="large"
color={attributes.proceed?.variant}
onClick={() => {
attributes.proceed.action();

View file

@ -1,9 +0,0 @@
import { DialogContentText, styled } from '@mui/material';
const MessageText = styled(DialogContentText)(({ theme }) => ({
paddingBottom: theme.spacing(2),
fontSize: '20px',
lineHeight: '24.2px',
}));
export default MessageText;

View file

@ -1,26 +0,0 @@
import React from 'react';
import { DialogTitle, IconButton, Typography } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { SpaceBetweenFlex } from 'components/Container';
const DialogTitleWithCloseButton = (props) => {
const { children, onClose, ...other } = props;
return (
<DialogTitle {...other}>
<SpaceBetweenFlex>
<Typography variant="title">{children}</Typography>
{onClose && (
<IconButton
aria-label="close"
onClick={onClose}
sx={{ float: 'right' }}>
<CloseIcon />
</IconButton>
)}
</SpaceBetweenFlex>
</DialogTitle>
);
};
export default DialogTitleWithCloseButton;

View file

@ -24,7 +24,12 @@ export default function EmptyScreen({ openUploader }) {
</div>
) : (
<>
<img height={150} src="/images/gallery.png" />
<img
height={150}
src="/images/gallery-locked/1x.png"
srcSet="/images/gallery-locked/2x.png 2x,
/images/gallery-locked/3x.png 3x"
/>
<Typography color="text.secondary" mt={2}>
{constants.UPLOAD_FIRST_PHOTO_DESCRIPTION()}
</Typography>

View file

@ -50,6 +50,14 @@ const EnteDateTimePicker = ({
'.MuiPickersToolbar-penIconButton': {
display: 'none',
},
'.MuiDialog-paper': { width: '320px' },
'.MuiClockPicker-root': {
position: 'relative',
minHeight: '292px',
},
'.PrivatePickersSlideTransition-root': {
minHeight: '200px',
},
},
}}
renderInput={(params) => (

View file

@ -1,15 +1,12 @@
import React from 'react';
import { styled } from '@mui/material';
const LogoImage = styled('img')`
height: 18px;
padding: 0 3px;
margin: 3px 0;
`;
interface Iprops {
height?: string;
width?: string;
}
export function EnteLogo({ height, width }: Iprops) {
return <LogoImage style={{ width, height }} alt="logo" src="/icon.svg" />;
export function EnteLogo(props) {
return (
<LogoImage height={18} alt="logo" src="/images/ente.svg" {...props} />
);
}

View file

@ -1,19 +1,15 @@
import { Button, DialogActions, DialogContent, Stack } from '@mui/material';
import React from 'react';
import { Button } from 'react-bootstrap';
import { ExportStats } from 'types/export';
import { formatDateTime } from 'utils/file';
import constants from 'utils/strings/constants';
import { Label, Row, Value } from './Container';
import { FlexWrapper, Label, Value } from './Container';
import { ComfySpan } from './ExportInProgress';
interface Props {
show: boolean;
onHide: () => void;
exportFolder: string;
exportSize: string;
lastExportTime: number;
exportStats: ExportStats;
updateExportFolder: (newFolder: string) => void;
exportFiles: () => void;
retryFailed: () => void;
}
@ -22,70 +18,58 @@ export default function ExportFinished(props: Props) {
const totalFiles = props.exportStats.failed + props.exportStats.success;
return (
<>
<div
style={{
borderBottom: '1px solid #444',
marginBottom: '20px',
padding: '0 5%',
}}>
<Row>
<Label width="35%">{constants.LAST_EXPORT_TIME}</Label>
<Value width="65%">
<DialogContent>
<Stack spacing={2.5}>
<FlexWrapper>
<Label width="40%">{constants.LAST_EXPORT_TIME}</Label>
<Value width="60%">
{formatDateTime(props.lastExportTime)}
</Value>
</Row>
<Row>
<Label width="60%">
</FlexWrapper>
<FlexWrapper>
<Label width="40%">
{constants.SUCCESSFULLY_EXPORTED_FILES}
</Label>
<Value width="40%">
<Value width="60%">
<ComfySpan>
{props.exportStats.success} / {totalFiles}
</ComfySpan>
</Value>
</Row>
</FlexWrapper>
{props.exportStats.failed > 0 && (
<Row>
<Label width="60%">
<FlexWrapper>
<Label width="40%">
{constants.FAILED_EXPORTED_FILES}
</Label>
<Value width="35%">
<Value width="60%">
<ComfySpan>
{props.exportStats.failed} / {totalFiles}
</ComfySpan>
</Value>
</Row>
</FlexWrapper>
)}
</div>
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-around',
}}>
<Button
block
variant={'outline-secondary'}
onClick={props.onHide}>
{constants.CLOSE}
</Button>
<div style={{ width: '30px' }} />
</Stack>
</DialogContent>
<DialogActions>
{props.exportStats.failed !== 0 ? (
<Button
block
variant={'outline-danger'}
size="large"
color="accent"
onClick={props.retryFailed}>
{constants.RETRY_EXPORT_}
</Button>
) : (
<Button
block
variant={'outline-success'}
size="large"
color="primary"
onClick={props.exportFiles}>
{constants.EXPORT_AGAIN}
</Button>
)}
</div>
<Button color="secondary" size="large" onClick={props.onHide}>
{constants.CLOSE}
</Button>
</DialogActions>
</>
);
}

View file

@ -1,9 +1,16 @@
import React from 'react';
import { Button, ProgressBar } from 'react-bootstrap';
import { ExportProgress } from 'types/export';
import { styled } from '@mui/material';
import {
Box,
Button,
DialogActions,
DialogContent,
styled,
} from '@mui/material';
import constants from 'utils/strings/constants';
import { ExportStage } from 'constants/export';
import VerticallyCentered, { FlexWrapper } from './Container';
import { ProgressBar } from 'react-bootstrap';
export const ComfySpan = styled('span')`
word-spacing: 1rem;
@ -11,10 +18,6 @@ export const ComfySpan = styled('span')`
`;
interface Props {
show: boolean;
onHide: () => void;
exportFolder: string;
exportSize: string;
exportStage: ExportStage;
exportProgress: ExportProgress;
resumeExport: () => void;
@ -25,66 +28,59 @@ interface Props {
export default function ExportInProgress(props: Props) {
return (
<>
<div
style={{
marginBottom: '30px',
padding: '0 5%',
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
}}>
<div style={{ marginBottom: '10px' }}>
<DialogContent>
<VerticallyCentered>
<Box mb={1.5}>
<ComfySpan>
{' '}
{props.exportProgress.current} /{' '}
{props.exportProgress.total}{' '}
</ComfySpan>{' '}
<span style={{ marginLeft: '10px' }}>
<span>
{' '}
files exported{' '}
{props.exportStage === ExportStage.PAUSED && `(paused)`}
{props.exportStage === ExportStage.PAUSED &&
`(paused)`}
</span>
</div>
<div style={{ width: '100%', marginBottom: '30px' }}>
</Box>
<FlexWrapper px={1}>
<ProgressBar
style={{ width: '100%' }}
now={Math.round(
(props.exportProgress.current * 100) /
props.exportProgress.total
)}
animated={!(props.exportStage === ExportStage.PAUSED)}
animated={
!(props.exportStage === ExportStage.PAUSED)
}
variant="upload-progress-bar"
/>
</div>
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-around',
}}>
</FlexWrapper>
</VerticallyCentered>
</DialogContent>
<DialogActions>
{props.exportStage === ExportStage.PAUSED ? (
<Button
block
variant={'outline-secondary'}
onClick={props.resumeExport}>
size="large"
onClick={props.resumeExport}
color="accent">
{constants.RESUME}
</Button>
) : (
<Button
block
variant={'outline-secondary'}
onClick={props.pauseExport}>
size="large"
onClick={props.pauseExport}
color="primary">
{constants.PAUSE}
</Button>
)}
<div style={{ width: '30px' }} />
<Button
block
variant={'outline-danger'}
onClick={props.cancelExport}>
size="large"
onClick={props.cancelExport}
color="secondary">
{constants.CANCEL}
</Button>
</div>
</div>
</DialogActions>
</>
);
}

View file

@ -1,35 +1,18 @@
import { DeadCenter } from 'pages/gallery';
import { Button, DialogActions, DialogContent } from '@mui/material';
import React from 'react';
import { Button } from 'react-bootstrap';
import constants from 'utils/strings/constants';
interface Props {
show: boolean;
onHide: () => void;
updateExportFolder: (newFolder: string) => void;
exportFolder: string;
startExport: () => void;
exportSize: string;
selectExportDirectory: () => void;
}
export default function ExportInit(props: Props) {
export default function ExportInit({ startExport }: Props) {
return (
<>
<DeadCenter>
<Button
variant="outline-success"
size="lg"
style={{
padding: '6px 3em',
margin: '0 20px',
marginBottom: '20px',
flex: 1,
whiteSpace: 'nowrap',
}}
onClick={props.startExport}>
<DialogContent>
<DialogActions>
<Button size="large" color="accent" onClick={startExport}>
{constants.START}
</Button>
</DeadCenter>
</>
</DialogActions>
</DialogContent>
);
}

View file

@ -1,42 +1,43 @@
import isElectron from 'is-electron';
import React, { useEffect, useState } from 'react';
import { Button } from 'react-bootstrap';
import React, { useEffect, useMemo, useState } from 'react';
import exportService from 'services/exportService';
import { ExportProgress, ExportStats } from 'types/export';
import { getLocalFiles } from 'services/fileService';
import { User } from 'types/user';
import { styled } from '@mui/material';
import {
Button,
Dialog,
DialogContent,
Divider,
Stack,
styled,
Tooltip,
} from '@mui/material';
import { sleep } from 'utils/common';
import { getExportRecordFileUID } from 'utils/export';
import { logError } from 'utils/sentry';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import constants from 'utils/strings/constants';
import { Label, Row, Value } from './Container';
import { FlexWrapper, Label, Value } from './Container';
import ExportFinished from './ExportFinished';
import ExportInit from './ExportInit';
import ExportInProgress from './ExportInProgress';
import FolderIcon from './icons/FolderIcon';
import InProgressIcon from './icons/InProgressIcon';
import DialogBox from './DialogBox';
import FolderIcon from '@mui/icons-material/Folder';
import { ExportStage, ExportType } from 'constants/export';
const FolderIconWrapper = styled('div')`
width: 15%;
margin-left: 10px;
cursor: pointer;
padding: 3px;
border: 1px solid #444;
border-radius: 15%;
&:hover {
background-color: #444;
}
`;
import EnteSpinner from './EnteSpinner';
import DialogTitleWithCloseButton from './DialogBox/TitleWithCloseButton';
import MoreHoriz from '@mui/icons-material/MoreHoriz';
import OverflowMenu from './OverflowMenu/menu';
import { OverflowMenuOption } from './OverflowMenu/option';
import { convertBytesToHumanReadable } from 'utils/billing';
import { CustomError } from 'utils/error';
import { getLocalUserDetails } from 'utils/user';
const ExportFolderPathContainer = styled('span')`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 200px;
width: 100%;
/* Beginning of string */
direction: rtl;
@ -46,9 +47,9 @@ const ExportFolderPathContainer = styled('span')`
interface Props {
show: boolean;
onHide: () => void;
usage: string;
}
export default function ExportModal(props: Props) {
const userDetails = useMemo(() => getLocalUserDetails(), []);
const [exportStage, setExportStage] = useState(ExportStage.INIT);
const [exportFolder, setExportFolder] = useState('');
const [exportSize, setExportSize] = useState('');
@ -146,8 +147,8 @@ export default function ExportModal(props: Props) {
}, [props.show]);
useEffect(() => {
setExportSize(props.usage);
}, [props.usage]);
setExportSize(convertBytesToHumanReadable(userDetails?.usage));
}, [userDetails]);
// =============
// STATE UPDATERS
@ -179,11 +180,7 @@ export default function ExportModal(props: Props) {
const preExportRun = async () => {
const exportFolder = getData(LS_KEYS.EXPORT)?.folder;
if (!exportFolder) {
const folderSelected = await selectExportDirectory();
if (!folderSelected) {
// no-op as select folder aborted
return;
}
await selectExportDirectory();
}
updateExportStage(ExportStage.INPROGRESS);
await sleep(100);
@ -193,10 +190,32 @@ export default function ExportModal(props: Props) {
updateExportStage(ExportStage.FINISHED);
await sleep(100);
updateExportTime(Date.now());
syncExportStatsWithReport();
syncExportStatsWithRecord();
}
};
const selectExportDirectory = async () => {
const newFolder = await exportService.selectExportDirectory();
if (newFolder) {
updateExportFolder(newFolder);
} else {
throw Error(CustomError.REQUEST_CANCELLED);
}
};
const syncExportStatsWithRecord = async () => {
const exportRecord = await exportService.getExportRecord();
const failed = exportRecord?.failedFiles?.length ?? 0;
const success = exportRecord?.exportedFiles?.length ?? 0;
setExportStats({ failed, success });
};
// =============
// UI functions
// =============
const startExport = async () => {
try {
await preExportRun();
updateExportProgress({ current: 0, total: 0 });
const exportResult = await exportService.exportFiles(
@ -204,20 +223,38 @@ export default function ExportModal(props: Props) {
ExportType.NEW
);
await postExportRun(exportResult);
} catch (e) {
if (e.message !== CustomError.REQUEST_CANCELLED) {
logError(e, 'startExport failed');
}
}
};
const stopExport = async () => {
try {
exportService.stopRunningExport();
postExportRun();
} catch (e) {
if (e.message !== CustomError.REQUEST_CANCELLED) {
logError(e, 'stopExport failed');
}
}
};
const pauseExport = () => {
try {
updateExportStage(ExportStage.PAUSED);
exportService.pauseRunningExport();
postExportRun({ paused: true });
} catch (e) {
if (e.message !== CustomError.REQUEST_CANCELLED) {
logError(e, 'pauseExport failed');
}
}
};
const resumeExport = async () => {
try {
const exportRecord = await exportService.getExportRecord();
await preExportRun();
@ -235,9 +272,15 @@ export default function ExportModal(props: Props) {
);
await postExportRun(exportResult);
} catch (e) {
if (e.message !== CustomError.REQUEST_CANCELLED) {
logError(e, 'resumeExport failed');
}
}
};
const retryFailedExport = async () => {
try {
await preExportRun();
updateExportProgress({ current: 0, total: exportStats.failed });
@ -246,45 +289,22 @@ export default function ExportModal(props: Props) {
ExportType.RETRY_FAILED
);
await postExportRun(exportResult);
};
const syncExportStatsWithReport = async () => {
const exportRecord = await exportService.getExportRecord();
const failed = exportRecord?.failedFiles?.length ?? 0;
const success = exportRecord?.exportedFiles?.length ?? 0;
setExportStats({ failed, success });
};
const selectExportDirectory = async () => {
const newFolder = await exportService.selectExportDirectory();
if (newFolder) {
updateExportFolder(newFolder);
return true;
} else {
return false;
} catch (e) {
if (e.message !== CustomError.REQUEST_CANCELLED) {
logError(e, 'retryFailedExport failed');
}
}
};
const ExportDynamicState = () => {
const ExportDynamicContent = () => {
switch (exportStage) {
case ExportStage.INIT:
return (
<ExportInit
{...props}
exportFolder={exportFolder}
exportSize={exportSize}
updateExportFolder={updateExportFolder}
startExport={startExport}
selectExportDirectory={selectExportDirectory}
/>
);
return <ExportInit startExport={startExport} />;
case ExportStage.INPROGRESS:
case ExportStage.PAUSED:
return (
<ExportInProgress
{...props}
exportFolder={exportFolder}
exportSize={exportSize}
exportStage={exportStage}
exportProgress={exportProgress}
resumeExport={resumeExport}
@ -295,10 +315,7 @@ export default function ExportModal(props: Props) {
case ExportStage.FINISHED:
return (
<ExportFinished
{...props}
exportFolder={exportFolder}
exportSize={exportSize}
updateExportFolder={updateExportFolder}
onHide={props.onHide}
lastExportTime={lastExportTime}
exportStats={exportStats}
exportFiles={startExport}
@ -312,53 +329,90 @@ export default function ExportModal(props: Props) {
};
return (
<DialogBox
open={props.show}
onClose={props.onHide}
attributes={{
title: constants.EXPORT_DATA,
}}>
<div
style={{
borderBottom: '1px solid #444',
marginBottom: '20px',
padding: '0 5%',
width: '450px',
}}>
<Row>
<Label width="40%">{constants.DESTINATION}</Label>
<Value width="60%">
<Dialog open={props.show} onClose={props.onHide} maxWidth="xs">
<DialogTitleWithCloseButton onClose={props.onHide}>
{constants.EXPORT_DATA}
</DialogTitleWithCloseButton>
<DialogContent>
<Stack spacing={2}>
<ExportDirectory
exportFolder={exportFolder}
selectExportDirectory={selectExportDirectory}
exportStage={exportStage}
/>
<ExportSize exportSize={exportSize} />
</Stack>
</DialogContent>
<Divider />
<ExportDynamicContent />
</Dialog>
);
}
function ExportDirectory({ exportFolder, selectExportDirectory, exportStage }) {
return (
<FlexWrapper>
<Label width="30%">{constants.DESTINATION}</Label>
<Value width="70%">
{!exportFolder ? (
<Button
variant={'outline-success'}
size={'sm'}
onClick={selectExportDirectory}>
<Button color={'accent'} onClick={selectExportDirectory}>
{constants.SELECT_FOLDER}
</Button>
) : (
<>
<Tooltip title={exportFolder}>
<ExportFolderPathContainer>
{exportFolder}
</ExportFolderPathContainer>
</Tooltip>
{(exportStage === ExportStage.FINISHED ||
exportStage === ExportStage.INIT) && (
<FolderIconWrapper
onClick={selectExportDirectory}>
<FolderIcon />
</FolderIconWrapper>
<ExportDirectoryOption
selectExportDirectory={selectExportDirectory}
/>
)}
</>
)}
</Value>
</Row>
<Row>
<Label width="40%">{constants.TOTAL_EXPORT_SIZE} </Label>
<Value width="60%">
{exportSize ? `${exportSize}` : <InProgressIcon />}
</Value>
</Row>
</div>
<ExportDynamicState />
</DialogBox>
</FlexWrapper>
);
}
function ExportSize({ exportSize }) {
return (
<FlexWrapper>
<Label width="30%">{constants.EXPORT_SIZE} </Label>
<Value width="70%">
{exportSize ? `${exportSize}` : <EnteSpinner />}
</Value>
</FlexWrapper>
);
}
function ExportDirectoryOption({ selectExportDirectory }) {
const handleClick = () => {
try {
selectExportDirectory();
} catch (e) {
if (e.message !== CustomError.REQUEST_CANCELLED) {
logError(e, 'startExport failed');
}
}
};
return (
<OverflowMenu
triggerButtonProps={{
sx: {
ml: 1,
},
}}
ariaControls={'export-option'}
triggerButtonIcon={<MoreHoriz />}>
<OverflowMenuOption
onClick={handleClick}
startIcon={<FolderIcon />}>
{constants.CHANGE_FOLDER}
</OverflowMenuOption>
</OverflowMenu>
);
}

View file

@ -1,23 +0,0 @@
import React from 'react';
import { styled } from '@mui/material';
const HeartUI = styled('button')<{
isClick: boolean;
size: number;
}>`
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
float: right;
background: url('/fav-button.png') no-repeat;
cursor: pointer;
background-size: cover;
border: none;
${({ isClick, size }) =>
isClick &&
`background-position: -${
28 * size
}px;transition: background 1s steps(28);`}
`;
export default function FavButton({ isClick, onClick, size }) {
return <HeartUI isClick={isClick} onClick={onClick} size={size} />;
}

View file

@ -17,12 +17,11 @@ const ShowHidePassword = ({
}: Iprops) => (
<InputAdornment position="end">
<IconButton
color="primary"
color="secondary"
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
sx={{ color: 'stroke.secondary' }}>
edge="end">
{showPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
</IconButton>
</InputAdornment>

View file

@ -38,8 +38,7 @@ const Overlay = styled('div')`
`;
type Props = React.PropsWithChildren<{
getRootProps: any;
getInputProps: any;
getDragAndDropRootProps: any;
}>;
export default function FullScreenDropZone(props: Props) {
@ -79,12 +78,9 @@ export default function FullScreenDropZone(props: Props) {
return (
<DropDiv
{...props.getRootProps({
{...props.getDragAndDropRootProps({
onDragEnter,
})}>
{!appContext.watchFolderView ? (
<input {...props.getInputProps()} />
) : null}
{isDragActive && (
<Overlay onDrop={onDragLeave} onDragLeave={onDragLeave}>
<CloseButtonWrapper onClick={onDragLeave}>

View file

@ -1,13 +1,14 @@
import constants from 'utils/strings/constants';
import React, { useEffect } from 'react';
import { useRouter } from 'next/router';
import { getOtt } from 'services/userService';
import { sendOtt } from 'services/userService';
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
import { PAGES } from 'constants/pages';
import FormPaperTitle from './Form/FormPaper/Title';
import FormPaperFooter from './Form/FormPaper/Footer';
import LinkButton from './pages/gallery/LinkButton';
import SingleInputForm, { SingleInputFormProps } from './SingleInputForm';
import { Input } from '@mui/material';
interface LoginProps {
signUp: () => void;
@ -32,7 +33,7 @@ export default function Login(props: LoginProps) {
setFieldError
) => {
try {
await getOtt(email);
await sendOtt(email);
setData(LS_KEYS.USER, { email });
router.push(PAGES.VERIFY);
} catch (e) {
@ -48,6 +49,8 @@ export default function Login(props: LoginProps) {
fieldType="email"
placeholder={constants.ENTER_EMAIL}
buttonText={constants.LOGIN}
autoComplete="username"
hiddenPostInput={<Input hidden type="password" value="" />}
/>
<FormPaperFooter>

View file

@ -1,8 +0,0 @@
import { styled } from '@mui/material';
export default styled('img')`
height: 25px;
vertical-align: bottom;
padding-right: 15px;
border-right: 2px solid #aaa;
margin-right: 15px;
`;

View file

@ -1,14 +1,19 @@
import { Button, DialogContent, Typography } from '@mui/material';
import VerticallyCentered from 'components/Container';
import DialogBoxBase from 'components/DialogBox/base';
import DialogTitleWithCloseButton from 'components/DialogBox/titleWithCloseButton';
import { Box, Button, Dialog, DialogContent, Typography } from '@mui/material';
import VerticallyCentered, { FlexWrapper } from 'components/Container';
import { AppContext } from 'pages/_app';
import React, { useContext } from 'react';
import React, { useContext, useEffect } from 'react';
import billingService from 'services/billingService';
import { getFamilyPlanAdmin } from 'utils/billing';
import { preloadImage } from 'utils/common';
import constants from 'utils/strings/constants';
import DialogTitleWithCloseButton from './DialogBox/TitleWithCloseButton';
export function MemberSubscriptionManage({ open, userDetails, onClose }) {
const { setDialogMessage } = useContext(AppContext);
const { setDialogMessage, isMobile } = useContext(AppContext);
useEffect(() => {
preloadImage('/images/family-plan');
}, []);
async function onLeaveFamilyClick() {
try {
@ -26,7 +31,7 @@ export function MemberSubscriptionManage({ open, userDetails, onClose }) {
title: `${constants.LEAVE_FAMILY_PLAN}`,
content: constants.LEAVE_FAMILY_CONFIRM,
proceed: {
text: constants.LEAVE_FAMILY_PLAN,
text: constants.LEAVE,
action: onLeaveFamilyClick,
variant: 'danger',
},
@ -40,26 +45,38 @@ export function MemberSubscriptionManage({ open, userDetails, onClose }) {
}
return (
<DialogBoxBase open={open} onClose={onClose} maxWidth="xs">
<Dialog
fullWidth
open={open}
onClose={onClose}
maxWidth="xs"
fullScreen={isMobile}>
<DialogTitleWithCloseButton onClose={onClose}>
<Typography variant="h3">{constants.SUBSCRIPTION}</Typography>
<Typography variant="h3" fontWeight={'bold'}>
{constants.SUBSCRIPTION}
</Typography>
<Typography color={'text.secondary'}>
{constants.FAMILY_PLAN}
</Typography>
</DialogTitleWithCloseButton>
<DialogContent>
<VerticallyCentered>
<Box mb={4}>
<Typography color="text.secondary">
{constants.FAMILY_SUBSCRIPTION_INFO}
</Typography>
<Typography>
{getFamilyPlanAdmin(userDetails.familyData)?.email}
</Typography>
</Box>
<img
height="267px"
width="256px"
src="/images/family_plan_leave@3x.png"
height={256}
src="/images/family-plan/1x.png"
srcSet="/images/family-plan/2x.png 2x,
/images/family-plan/3x.png 3x"
/>
<FlexWrapper px={2}>
<Button
size="large"
variant="outlined"
@ -67,8 +84,9 @@ export function MemberSubscriptionManage({ open, userDetails, onClose }) {
onClick={confirmLeaveFamily}>
{constants.LEAVE_FAMILY_PLAN}
</Button>
</FlexWrapper>
</VerticallyCentered>
</DialogContent>
</DialogBoxBase>
</Dialog>
);
}

View file

@ -48,7 +48,7 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
sx={{
textAlign: 'left',
width: '320px',
padding: '12px 16px',
padding: (theme) => theme.spacing(1.5, 2),
}}>
<Stack
flex={'1'}

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import Menu from '@mui/material/Menu';
import { IconButton, styled } from '@mui/material';
import { IconButton, PaperProps, styled } from '@mui/material';
import { OverflowMenuContext } from 'contexts/overflowMenu';
export interface Iprops {
@ -8,6 +8,7 @@ export interface Iprops {
triggerButtonProps?: any;
children?: React.ReactNode;
ariaControls: string;
menuPaperProps?: Partial<PaperProps>;
}
const StyledMenu = styled(Menu)`
@ -27,6 +28,7 @@ export default function OverflowMenu({
ariaControls,
triggerButtonIcon,
triggerButtonProps,
menuPaperProps,
}: Iprops) {
const [sortByEl, setSortByEl] = useState(null);
const handleClose = () => setSortByEl(null);
@ -49,6 +51,7 @@ export default function OverflowMenu({
disablePadding: true,
'aria-labelledby': ariaControls,
}}
PaperProps={menuPaperProps}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',

Some files were not shown because too many files have changed in this diff Show more