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", "name": "bada-frame",
"version": "0.9.1", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@ -22,8 +22,6 @@
"@mui/x-date-pickers": "^5.0.0-alpha.6", "@mui/x-date-pickers": "^5.0.0-alpha.6",
"@sentry/nextjs": "^6.7.1", "@sentry/nextjs": "^6.7.1",
"@stripe/stripe-js": "^1.13.2", "@stripe/stripe-js": "^1.13.2",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"axios": "^0.21.3", "axios": "^0.21.3",
"bip39": "^3.0.4", "bip39": "^3.0.4",
"bootstrap": "^4.5.2", "bootstrap": "^4.5.2",
@ -31,16 +29,11 @@
"chrono-node": "^2.2.6", "chrono-node": "^2.2.6",
"comlink": "^4.3.0", "comlink": "^4.3.0",
"debounce-promise": "^3.1.2", "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", "exifr": "^7.1.3",
"ffmpeg-wasm": "file:./thirdparty/ffmpeg-wasm", "ffmpeg-wasm": "file:./thirdparty/ffmpeg-wasm",
"file-type": "^16.5.3", "file-type": "^16.5.4",
"formik": "^2.1.5", "formik": "^2.1.5",
"heic-convert": "^1.2.4", "heic-convert": "^1.2.4",
"http-proxy-middleware": "^1.0.5",
"is-electron": "^2.2.0", "is-electron": "^2.2.0",
"jszip": "3.7.1", "jszip": "3.7.1",
"libsodium-wrappers": "^0.7.8", "libsodium-wrappers": "^0.7.8",
@ -58,8 +51,6 @@
"react-top-loading-bar": "^2.0.1", "react-top-loading-bar": "^2.0.1",
"react-virtualized-auto-sizer": "^1.0.2", "react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.6", "react-window": "^1.8.6",
"react-window-infinite-loader": "^1.0.5",
"scrypt-js": "^3.0.1",
"styled-components": "^5.3.5", "styled-components": "^5.3.5",
"workbox-precaching": "^6.1.5", "workbox-precaching": "^6.1.5",
"workbox-recipes": "^6.1.5", "workbox-recipes": "^6.1.5",
@ -83,11 +74,17 @@
"@types/react-window-infinite-loader": "^1.0.3", "@types/react-window-infinite-loader": "^1.0.3",
"@types/styled-components": "^5.1.25", "@types/styled-components": "^5.1.25",
"@types/yup": "^0.29.7", "@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", "babel-plugin-styled-components": "^1.11.1",
"eslint": "^7.27.0", "eslint": "^7.27.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.3.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": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "^7.0.1", "husky": "^7.0.1",
"lint-staged": "^11.1.2", "lint-staged": "^11.1.2",
"prettier": "2.3.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", "name": "ente | encrypted photo storage",
"icons": [ "icons": [
{ {
"src": "/images/ente-192.png", "src": "/images/ente/192.png",
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
}, },
{ {
"src": "/images/ente-256.png", "src": "/images/ente/256.png",
"type": "image/png", "type": "image/png",
"sizes": "256x256" "sizes": "256x256"
}, },
{ {
"src": "/images/ente-512.png", "src": "/images/ente/512.png",
"type": "image/png", "type": "image/png",
"sizes": "512x512" "sizes": "512x512"
} }

View file

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

View file

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

View file

@ -7,13 +7,28 @@ import OverflowMenu from 'components/OverflowMenu/menu';
export interface CollectionSortProps { export interface CollectionSortProps {
setCollectionSortBy: (sortBy: COLLECTION_SORT_BY) => void; setCollectionSortBy: (sortBy: COLLECTION_SORT_BY) => void;
activeSortBy: COLLECTION_SORT_BY; activeSortBy: COLLECTION_SORT_BY;
nestedInDialog?: boolean;
disableBG?: boolean;
} }
export default function CollectionSort(props: CollectionSortProps) { export default function CollectionSort(props: CollectionSortProps) {
return ( return (
<OverflowMenu <OverflowMenu
ariaControls="collection-sort" 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} /> <CollectionSortOptions {...props} />
</OverflowMenu> </OverflowMenu>
); );

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,11 @@ import CollectionOptions from 'components/Collections/CollectionOptions';
import { SetCollectionNamerAttributes } from 'components/Collections/CollectionNamer'; import { SetCollectionNamerAttributes } from 'components/Collections/CollectionNamer';
import { SpaceBetweenFlex } from 'components/Container'; import { SpaceBetweenFlex } from 'components/Container';
import { CollectionInfoBarWrapper } from './styledComponents'; 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 { interface Iprops {
activeCollection: Collection; activeCollection: Collection;
@ -19,6 +23,7 @@ interface Iprops {
collectionSummary: CollectionSummary; collectionSummary: CollectionSummary;
setCollectionNamerAttributes: SetCollectionNamerAttributes; setCollectionNamerAttributes: SetCollectionNamerAttributes;
activeCollection: Collection; activeCollection: Collection;
activeCollectionID: number;
showCollectionShareModal: () => void; showCollectionShareModal: () => void;
redirectToAll: () => void; redirectToAll: () => void;
} }
@ -32,11 +37,33 @@ export default function CollectionInfoWithOptions({
const { name, type, fileCount } = collectionSummary; 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 ( return (
<CollectionInfoBarWrapper> <CollectionInfoBarWrapper>
<SpaceBetweenFlex> <SpaceBetweenFlex>
<CollectionInfo name={name} fileCount={fileCount} /> <CollectionInfo
{!isSystemCollection(type) && <CollectionOptions {...props} />} name={name}
fileCount={fileCount}
endIcon={<EndIcon type={type} />}
/>
{shouldShowOptions(type) && (
<CollectionOptions
{...props}
collectionSummaryType={type}
/>
)}
</SpaceBetweenFlex> </SpaceBetweenFlex>
</CollectionInfoBarWrapper> </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%; border-radius: 50%;
background-color: ${({ theme }) => theme.palette.background.paper}; background-color: ${({ theme }) => theme.palette.background.paper};
color: ${({ theme }) => theme.palette.text.primary}; color: ${({ theme }) => theme.palette.primary.main};
${(props) => ${(props) =>
props.direction === SCROLL_DIRECTION.LEFT 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 React from 'react';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import DialogBox from 'components/DialogBox';
import SingleInputForm, { import SingleInputForm, {
SingleInputFormProps, SingleInputFormProps,
} from 'components/SingleInputForm'; } from 'components/SingleInputForm';
import DialogBoxBase from 'components/DialogBox/base';
import { DialogContent, DialogTitle } from '@mui/material';
export interface CollectionNamerAttributes { export interface CollectionNamerAttributes {
callback: (name: string) => void; callback: (name: string) => void;
@ -39,19 +40,19 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
}; };
return ( return (
<DialogBox <DialogBoxBase open={props.show} onClose={props.onHide}>
open={props.show} <DialogTitle>{attributes.title}</DialogTitle>
attributes={{ title: attributes.title }} <DialogContent>
onClose={props.onHide}
titleCloseButton
maxWidth="xs">
<SingleInputForm <SingleInputForm
callback={onSubmit} callback={onSubmit}
fieldType="text" fieldType="text"
buttonText={attributes.buttonText} buttonText={attributes.buttonText}
placeholder={constants.ENTER_ALBUM_NAME} placeholder={constants.ENTER_ALBUM_NAME}
initialValue={attributes.autoFilledName} 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 React, { useContext } from 'react';
import * as CollectionAPI from 'services/collectionService'; import * as CollectionAPI from 'services/collectionService';
import * as TrashService from 'services/trashService';
import { import {
changeCollectionVisibility, changeCollectionVisibility,
downloadAllCollectionFiles, downloadAllCollectionFiles,
} from 'utils/collection'; } from 'utils/collection';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { SetCollectionNamerAttributes } from './CollectionNamer'; import { SetCollectionNamerAttributes } from '../CollectionNamer';
import { Collection } from 'types/collection'; import { Collection } from 'types/collection';
import { IsArchived } from 'utils/magicMetadata'; import { IsArchived } from 'utils/magicMetadata';
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
@ -13,41 +15,60 @@ import { logError } from 'utils/sentry';
import { VISIBILITY_STATE } from 'types/magicMetadata'; import { VISIBILITY_STATE } from 'types/magicMetadata';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import OverflowMenu from 'components/OverflowMenu/menu'; import OverflowMenu from 'components/OverflowMenu/menu';
import { OverflowMenuOption } from 'components/OverflowMenu/option'; import { CollectionSummaryType } from 'constants/collection';
import MoreVertIcon from '@mui/icons-material/MoreVert'; import { TrashCollectionOption } from './TrashCollectionOption';
import MoreHoriz from '@mui/icons-material/MoreHoriz';
interface CollectionOptionsProps { interface CollectionOptionsProps {
setCollectionNamerAttributes: SetCollectionNamerAttributes; setCollectionNamerAttributes: SetCollectionNamerAttributes;
activeCollection: Collection; activeCollection: Collection;
collectionSummaryType: CollectionSummaryType;
showCollectionShareModal: () => void; showCollectionShareModal: () => void;
redirectToAll: () => void; redirectToAll: () => void;
} }
enum CollectionActions { export enum CollectionActions {
SHOW_RENAME_DIALOG,
RENAME, RENAME,
CONFIRM_DOWNLOAD,
DOWNLOAD, DOWNLOAD,
ARCHIVE, ARCHIVE,
UNARCHIVE, UNARCHIVE,
CONFIRM_DELETE,
DELETE, DELETE,
SHOW_SHARE_DIALOG,
CONFIRM_EMPTY_TRASH,
EMPTY_TRASH,
} }
const CollectionOptions = (props: CollectionOptionsProps) => { const CollectionOptions = (props: CollectionOptionsProps) => {
const { const {
activeCollection, activeCollection,
collectionSummaryType,
redirectToAll, redirectToAll,
setCollectionNamerAttributes, setCollectionNamerAttributes,
showCollectionShareModal, showCollectionShareModal,
} = props; } = props;
const { startLoading, finishLoading, setDialogMessage } = const { startLoading, finishLoading, setDialogMessage } =
useContext(AppContext); useContext(AppContext);
const { syncWithRemote } = useContext(GalleryContext); const { syncWithRemote } = useContext(GalleryContext);
const handleCollectionAction = (action: CollectionActions) => { const handleCollectionAction = (
action: CollectionActions,
loader = true
) => {
let callback; let callback;
switch (action) { switch (action) {
case CollectionActions.SHOW_RENAME_DIALOG:
callback = showRenameCollectionModal;
break;
case CollectionActions.RENAME: case CollectionActions.RENAME:
callback = renameCollection; callback = renameCollection;
break; break;
case CollectionActions.CONFIRM_DOWNLOAD:
callback = confirmDownloadCollection;
break;
case CollectionActions.DOWNLOAD: case CollectionActions.DOWNLOAD:
callback = downloadCollection; callback = downloadCollection;
break; break;
@ -57,9 +78,21 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
case CollectionActions.UNARCHIVE: case CollectionActions.UNARCHIVE:
callback = unArchiveCollection; callback = unArchiveCollection;
break; break;
case CollectionActions.CONFIRM_DELETE:
callback = confirmDeleteCollection;
break;
case CollectionActions.DELETE: case CollectionActions.DELETE:
callback = deleteCollection; callback = deleteCollection;
break; 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: default:
logError( logError(
Error('invalid collection action '), Error('invalid collection action '),
@ -70,8 +103,8 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
} }
} }
return async (...args) => { return async (...args) => {
startLoading();
try { try {
loader && startLoading();
await callback(...args); await callback(...args);
} catch (e) { } catch (e) {
setDialogMessage({ setDialogMessage({
@ -79,10 +112,10 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
content: constants.UNKNOWN_ERROR, content: constants.UNKNOWN_ERROR,
close: { variant: 'danger' }, close: { variant: 'danger' },
}); });
} finally {
syncWithRemote(false, true);
loader && finishLoading();
} }
syncWithRemote();
finishLoading();
}; };
}; };
@ -109,6 +142,12 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
downloadAllCollectionFiles(activeCollection.id); downloadAllCollectionFiles(activeCollection.id);
}; };
const emptyTrash = async () => {
await TrashService.emptyTrash();
await TrashService.clearLocalTrash();
redirectToAll();
};
const showRenameCollectionModal = () => { const showRenameCollectionModal = () => {
setCollectionNamerAttributes({ setCollectionNamerAttributes({
title: constants.RENAME_COLLECTION, title: constants.RENAME_COLLECTION,
@ -120,8 +159,8 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
const confirmDeleteCollection = () => { const confirmDeleteCollection = () => {
setDialogMessage({ setDialogMessage({
title: constants.CONFIRM_DELETE_COLLECTION, title: constants.DELETE_COLLECTION_TITLE,
content: constants.DELETE_COLLECTION_MESSAGE(), content: constants.DELETE_COLLECTION_MESSAGE,
proceed: { proceed: {
text: constants.DELETE_COLLECTION, text: constants.DELETE_COLLECTION,
action: handleCollectionAction(CollectionActions.DELETE), 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 ( return (
<OverflowMenu <OverflowMenu
ariaControls={`collection-options-${props.activeCollection.id}`} ariaControls={'collection-options'}
triggerButtonIcon={<MoreVertIcon />} triggerButtonIcon={<MoreHoriz />}
triggerButtonProps={{ triggerButtonProps={{
sx: { sx: {
background: (theme) => theme.palette.background.paper, background: (theme) => theme.palette.fill.dark,
}, },
}}> }}>
<OverflowMenuOption onClick={showRenameCollectionModal}> {collectionSummaryType === CollectionSummaryType.trash ? (
{constants.RENAME} <TrashCollectionOption
</OverflowMenuOption> handleCollectionAction={handleCollectionAction}
<OverflowMenuOption onClick={showCollectionShareModal}> />
{constants.SHARE}
</OverflowMenuOption>
<OverflowMenuOption onClick={confirmDownloadCollection}>
{constants.DOWNLOAD}
</OverflowMenuOption>
{IsArchived(activeCollection) ? (
<OverflowMenuOption
onClick={handleCollectionAction(
CollectionActions.UNARCHIVE
)}>
{constants.UNARCHIVE}
</OverflowMenuOption>
) : ( ) : (
<OverflowMenuOption <AlbumCollectionOption
onClick={handleCollectionAction(CollectionActions.ARCHIVE)}> IsArchived={IsArchived(activeCollection)}
{constants.ARCHIVE} handleCollectionAction={handleCollectionAction}
</OverflowMenuOption> />
)} )}
<OverflowMenuOption
color="danger"
onClick={confirmDeleteCollection}>
{constants.DELETE}
</OverflowMenuOption>
</OverflowMenu> </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 React, { useContext, useEffect, useMemo } from 'react';
import AddCollectionButton from './AddCollectionButton';
import { Collection, CollectionSummaries } from 'types/collection'; 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 { DialogContent } from '@mui/material';
import { FlexWrapper } from 'components/Container'; import { FlexWrapper } from 'components/Container';
import { CollectionSelectorTile } from 'components/Collections/styledComponents'; import CollectionSelectorCard from './CollectionCard';
import AllCollectionCard from 'components/Collections/AllCollections/CollectionCard'; import AddCollectionButton from './AddCollectionButton';
import { isSystemCollection } from 'utils/collection';
export interface CollectionSelectorAttributes { export interface CollectionSelectorAttributes {
callback: (collection: Collection) => void; callback: (collection: Collection) => void;
@ -15,9 +15,6 @@ export interface CollectionSelectorAttributes {
title: string; title: string;
fromCollection?: number; fromCollection?: number;
} }
export type SetCollectionSelectorAttributes = React.Dispatch<
React.SetStateAction<CollectionSelectorAttributes>
>;
interface Props { interface Props {
open: boolean; open: boolean;
@ -32,12 +29,14 @@ function CollectionSelector({
collections, collections,
...props ...props
}: Props) { }: Props) {
const appContext = useContext(AppContext);
const collectionToShow = useMemo(() => { const collectionToShow = useMemo(() => {
const personalCollectionsOtherThanFrom = [ const personalCollectionsOtherThanFrom = [
...collectionSummaries.values(), ...collectionSummaries.values(),
]?.filter( ]?.filter(
({ type, id }) => ({ type, id }) =>
id !== attributes?.fromCollection && !isSystemCollection(type) id !== attributes?.fromCollection &&
isUploadAllowedCollection(type)
); );
return personalCollectionsOtherThanFrom; return personalCollectionsOtherThanFrom;
}, [collectionSummaries, attributes]); }, [collectionSummaries, attributes]);
@ -62,27 +61,24 @@ function CollectionSelector({
props.onClose(); props.onClose();
}; };
const onCloseButtonClick = () => props.onClose(true);
return ( return (
<DialogBoxBase <AllCollectionDialog
{...props} onClose={props.onClose}
maxWidth="md" open={props.open}
PaperProps={{ sx: { maxWidth: '848px' } }}> position="center"
<DialogTitleWithCloseButton onClose={() => props.onClose(true)}> fullScreen={appContext.isMobile}>
<DialogTitleWithCloseButton onClose={onCloseButtonClick}>
{attributes.title} {attributes.title}
</DialogTitleWithCloseButton> </DialogTitleWithCloseButton>
<DialogContent <DialogContent>
sx={{ <FlexWrapper flexWrap="wrap" gap={0.5}>
'&&&': {
px: 0,
},
}}>
<FlexWrapper style={{ flexWrap: 'wrap' }}>
<AddCollectionButton <AddCollectionButton
showNextModal={attributes.showNextModal} showNextModal={attributes.showNextModal}
/> />
{collectionToShow.map((collectionSummary) => ( {collectionToShow.map((collectionSummary) => (
<AllCollectionCard <CollectionSelectorCard
collectionTile={CollectionSelectorTile}
onCollectionClick={handleCollectionClick} onCollectionClick={handleCollectionClick}
collectionSummary={collectionSummary} collectionSummary={collectionSummary}
key={collectionSummary.id} key={collectionSummary.id}
@ -90,7 +86,7 @@ function CollectionSelector({
))} ))}
</FlexWrapper> </FlexWrapper>
</DialogContent> </DialogContent>
</DialogBoxBase> </AllCollectionDialog>
); );
} }

View file

@ -6,7 +6,10 @@ export const CollectionShareContainer = styled(Dialog)(({ theme }) => ({
justifyContent: 'flex-end', justifyContent: 'flex-end',
}, },
'& .MuiPaper-root': { '& .MuiPaper-root': {
maxWidth: '414px', width: '414px',
},
'& .MuiDialog-paperFullScreen': {
maxWidth: '100%',
}, },
'& .MuiDialogTitle-root': { '& .MuiDialogTitle-root': {
padding: theme.spacing(4, 3, 3, 4), 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 { GalleryContext } from 'pages/gallery';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { shareCollection } from 'services/collectionService'; import { shareCollection } from 'services/collectionService';
import { User } from 'types/user'; import { User } from 'types/user';
import { sleep } from 'utils/common';
import { handleSharingErrors } from 'utils/error'; import { handleSharingErrors } from 'utils/error';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { CollectionShareSharees } from './sharees'; import { CollectionShareSharees } from './sharees';
import CollectionShareSubmitButton from './submitButton';
export default function EmailShare({ collection }) { export default function EmailShare({ collection }) {
const galleryContext = useContext(GalleryContext); const galleryContext = useContext(GalleryContext);
const collectionShare = async (email, setFieldError) => { const collectionShare: SingleInputFormProps['callback'] = async (
email,
setFieldError
) => {
try { try {
const user: User = getData(LS_KEYS.USER); const user: User = getData(LS_KEYS.USER);
if (email === user.email) { if (email === user.email) {
setFieldError('email', constants.SHARE_WITH_SELF); setFieldError(constants.SHARE_WITH_SELF);
} else if ( } else if (
collection?.sharees?.find((value) => value.email === email) collection?.sharees?.find((value) => value.email === email)
) { ) {
setFieldError('email', constants.ALREADY_SHARED(email)); setFieldError(constants.ALREADY_SHARED(email));
} else { } else {
await shareCollection(collection, email); await shareCollection(collection, email);
await sleep(2000);
await galleryContext.syncWithRemote(false, true); await galleryContext.syncWithRemote(false, true);
} }
} catch (e) { } catch (e) {
const errorMessage = handleSharingErrors(e); const errorMessage = handleSharingErrors(e);
setFieldError('email', errorMessage); setFieldError(errorMessage);
} }
}; };
return ( return (
@ -38,7 +41,11 @@ export default function EmailShare({ collection }) {
placeholder={constants.ENTER_EMAIL} placeholder={constants.ENTER_EMAIL}
fieldType="email" fieldType="email"
buttonText={constants.SHARE} buttonText={constants.SHARE}
customSubmitButton={CollectionShareSubmitButton} submitButtonProps={{
size: 'medium',
sx: { mt: 1, mb: 2 },
}}
disableAutoFocus
/> />
<CollectionShareSharees collection={collection} /> <CollectionShareSharees collection={collection} />
</> </>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,11 @@
import DialogBox from 'components/DialogBox'; import { Dialog, Stack, Typography } from '@mui/material';
import SingleInputForm from 'components/SingleInputForm'; import SingleInputForm, {
SingleInputFormProps,
} from 'components/SingleInputForm';
import React from 'react'; import React from 'react';
import CryptoWorker from 'utils/crypto'; import CryptoWorker from 'utils/crypto';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
export function PublicLinkSetPassword({ export function PublicLinkSetPassword({
open, open,
onClose, onClose,
@ -11,13 +14,16 @@ export function PublicLinkSetPassword({
updatePublicShareURLHelper, updatePublicShareURLHelper,
setChangePasswordView, setChangePasswordView,
}) { }) {
const savePassword = async (passphrase, setFieldError) => { const savePassword: SingleInputFormProps['callback'] = async (
passphrase,
setFieldError
) => {
if (passphrase && passphrase.trim().length >= 1) { if (passphrase && passphrase.trim().length >= 1) {
await enablePublicUrlPassword(passphrase); await enablePublicUrlPassword(passphrase);
setChangePasswordView(false); setChangePasswordView(false);
publicShareProp.passwordEnabled = true; publicShareProp.passwordEnabled = true;
} else { } else {
setFieldError('linkPassword', 'can not be empty'); setFieldError('can not be empty');
} }
}; };
@ -35,20 +41,26 @@ export function PublicLinkSetPassword({
}); });
}; };
return ( return (
<DialogBox <Dialog
open={open} open={open}
onClose={onClose} onClose={onClose}
PaperProps={{ sx: { maxWidth: '350px' } }} disablePortal
titleCloseButton BackdropProps={{ sx: { position: 'absolute' } }}
attributes={{ sx={{ position: 'absolute' }}
title: constants.PASSWORD_LOCK, 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 <SingleInputForm
callback={savePassword} callback={savePassword}
placeholder={constants.RETURN_PASSPHRASE_HINT} placeholder={constants.RETURN_PASSPHRASE_HINT}
buttonText={constants.LOCK} buttonText={constants.LOCK}
fieldType="password" 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 React, { useContext } from 'react';
import { unshareCollection } from 'services/collectionService'; import { unshareCollection } from 'services/collectionService';
import { Collection } from 'types/collection'; import { Collection } from 'types/collection';
import { sleep } from 'utils/common';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import ShareeRow from './row'; import ShareeRow from './row';
@ -20,7 +19,6 @@ export function CollectionShareSharees({ collection }: Iprops) {
try { try {
appContext.startLoading(); appContext.startLoading();
await unshareCollection(collection, sharee.email); await unshareCollection(collection, sharee.email);
await sleep(2000);
await galleryContext.syncWithRemote(false, true); await galleryContext.syncWithRemote(false, true);
} finally { } finally {
appContext.finishLoading(); appContext.finishLoading();

View file

@ -18,6 +18,12 @@ const ShareeRow = ({ sharee, collectionUnshare }: IProps) => {
<SpaceBetweenFlex> <SpaceBetweenFlex>
{sharee.email} {sharee.email}
<OverflowMenu <OverflowMenu
menuPaperProps={{
sx: {
backgroundColor: (theme) =>
theme.palette.background.overPaper,
},
}}
ariaControls={`email-share-${sharee.email}`} ariaControls={`email-share-${sharee.email}`}
triggerButtonIcon={<MoreHorizIcon />}> triggerButtonIcon={<MoreHorizIcon />}>
<OverflowMenuOption <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 { Collection, CollectionSummaries } from 'types/collection';
import CollectionListBar from 'components/Collections/CollectionBar'; import CollectionListBar from 'components/Collections/CollectionListBar';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import AllCollections from 'components/Collections/AllCollections'; import AllCollections from 'components/Collections/AllCollections';
import CollectionInfoWithOptions from 'components/Collections/CollectionInfoWithOptions'; 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 CollectionShare from 'components/Collections/CollectionShare';
import { SetCollectionNamerAttributes } from 'components/Collections/CollectionNamer'; import { SetCollectionNamerAttributes } from 'components/Collections/CollectionNamer';
import { ITEM_TYPE, TimeStampListItem } from 'components/PhotoList'; 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 { interface Iprops {
collections: Collection[]; collections: Collection[];
@ -33,6 +40,12 @@ export default function Collections(props: Iprops) {
const [allCollectionView, setAllCollectionView] = useState(false); const [allCollectionView, setAllCollectionView] = useState(false);
const [collectionShareModalView, setCollectionShareModalView] = const [collectionShareModalView, setCollectionShareModalView] =
useState(false); 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 collectionsMap = useRef<Map<number, Collection>>(new Map());
const activeCollection = useRef<Collection>(null); const activeCollection = useRef<Collection>(null);
@ -50,6 +63,15 @@ export default function Collections(props: Iprops) {
collectionsMap.current.get(activeCollectionID); collectionsMap.current.get(activeCollectionID);
}, [activeCollectionID, collections]); }, [activeCollectionID, collections]);
const sortedCollectionSummaries = useMemo(
() =>
sortCollectionSummaries(
[...collectionSummaries.values()],
collectionSortBy
),
[collectionSortBy, collectionSummaries]
);
useEffect( useEffect(
() => () =>
!shouldBeHidden && !shouldBeHidden &&
@ -60,6 +82,7 @@ export default function Collections(props: Iprops) {
activeCollectionID activeCollectionID
)} )}
activeCollection={activeCollection.current} activeCollection={activeCollection.current}
activeCollectionID={activeCollectionID}
setCollectionNamerAttributes={ setCollectionNamerAttributes={
setCollectionNamerAttributes setCollectionNamerAttributes
} }
@ -69,7 +92,7 @@ export default function Collections(props: Iprops) {
} }
/> />
), ),
itemType: ITEM_TYPE.STATIC, itemType: ITEM_TYPE.OTHER,
height: 68, height: 68,
}), }),
[collectionSummaries, activeCollectionID, shouldBeHidden] [collectionSummaries, activeCollectionID, shouldBeHidden]
@ -79,25 +102,37 @@ export default function Collections(props: Iprops) {
return <></>; return <></>;
} }
const closeAllCollections = () => setAllCollectionView(false);
const openAllCollections = () => setAllCollectionView(true);
const closeCollectionShare = () => setCollectionShareModalView(false);
return ( return (
<> <>
<CollectionListBar <CollectionListBar
activeCollection={activeCollectionID} activeCollection={activeCollectionID}
setActiveCollection={setActiveCollectionID} setActiveCollection={setActiveCollectionID}
collectionSummaries={collectionSummaries} collectionSummaries={sortedCollectionSummaries.filter((x) =>
showAllCollections={() => setAllCollectionView(true)} shouldBeShownOnCollectionBar(x.type)
)}
showAllCollections={openAllCollections}
setCollectionSortBy={setCollectionSortBy}
collectionSortBy={collectionSortBy}
/> />
<AllCollections <AllCollections
isOpen={allCollectionView} open={allCollectionView}
close={() => setAllCollectionView(false)} onClose={closeAllCollections}
collectionSummaries={collectionSummaries} collectionSummaries={sortedCollectionSummaries.filter(
(x) => !isSystemCollection(x.type)
)}
setActiveCollection={setActiveCollectionID} setActiveCollection={setActiveCollectionID}
setCollectionSortBy={setCollectionSortBy}
collectionSortBy={collectionSortBy}
/> />
<CollectionShare <CollectionShare
show={collectionShareModalView} open={collectionShareModalView}
onHide={() => setCollectionShareModalView(false)} onClose={closeCollectionShare}
collection={activeCollection.current} collection={activeCollection.current}
/> />
</> </>

View file

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

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 }) => ({ const DialogBoxBase = styled(Dialog)(({ theme }) => ({
'& .MuiDialog-paper': { '& .MuiDialog-paper': {
padding: theme.spacing(2, 0), padding: theme.spacing(1, 1.5),
maxWidth: '346px',
}, },
'& .MuiDialogTitle-root': { '& .MuiDialogTitle-root': {
padding: theme.spacing(2, 3), padding: theme.spacing(2),
paddingBottom: theme.spacing(1),
}, },
'& .MuiDialogContent-root': { '& .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), padding: theme.spacing(2),
}, },
'& .MuiDialogActions-root button:not(:first-child)': { '.MuiDialogTitle-root + .MuiDialogContent-root': {
marginLeft: theme.spacing(2), 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; export default DialogBoxBase;

View file

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

View file

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

View file

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

View file

@ -1,19 +1,15 @@
import { Button, DialogActions, DialogContent, Stack } from '@mui/material';
import React from 'react'; import React from 'react';
import { Button } from 'react-bootstrap';
import { ExportStats } from 'types/export'; import { ExportStats } from 'types/export';
import { formatDateTime } from 'utils/file'; import { formatDateTime } from 'utils/file';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { Label, Row, Value } from './Container'; import { FlexWrapper, Label, Value } from './Container';
import { ComfySpan } from './ExportInProgress'; import { ComfySpan } from './ExportInProgress';
interface Props { interface Props {
show: boolean;
onHide: () => void; onHide: () => void;
exportFolder: string;
exportSize: string;
lastExportTime: number; lastExportTime: number;
exportStats: ExportStats; exportStats: ExportStats;
updateExportFolder: (newFolder: string) => void;
exportFiles: () => void; exportFiles: () => void;
retryFailed: () => void; retryFailed: () => void;
} }
@ -22,70 +18,58 @@ export default function ExportFinished(props: Props) {
const totalFiles = props.exportStats.failed + props.exportStats.success; const totalFiles = props.exportStats.failed + props.exportStats.success;
return ( return (
<> <>
<div <DialogContent>
style={{ <Stack spacing={2.5}>
borderBottom: '1px solid #444', <FlexWrapper>
marginBottom: '20px', <Label width="40%">{constants.LAST_EXPORT_TIME}</Label>
padding: '0 5%', <Value width="60%">
}}>
<Row>
<Label width="35%">{constants.LAST_EXPORT_TIME}</Label>
<Value width="65%">
{formatDateTime(props.lastExportTime)} {formatDateTime(props.lastExportTime)}
</Value> </Value>
</Row> </FlexWrapper>
<Row> <FlexWrapper>
<Label width="60%"> <Label width="40%">
{constants.SUCCESSFULLY_EXPORTED_FILES} {constants.SUCCESSFULLY_EXPORTED_FILES}
</Label> </Label>
<Value width="40%"> <Value width="60%">
<ComfySpan> <ComfySpan>
{props.exportStats.success} / {totalFiles} {props.exportStats.success} / {totalFiles}
</ComfySpan> </ComfySpan>
</Value> </Value>
</Row> </FlexWrapper>
{props.exportStats.failed > 0 && ( {props.exportStats.failed > 0 && (
<Row> <FlexWrapper>
<Label width="60%"> <Label width="40%">
{constants.FAILED_EXPORTED_FILES} {constants.FAILED_EXPORTED_FILES}
</Label> </Label>
<Value width="35%"> <Value width="60%">
<ComfySpan> <ComfySpan>
{props.exportStats.failed} / {totalFiles} {props.exportStats.failed} / {totalFiles}
</ComfySpan> </ComfySpan>
</Value> </Value>
</Row> </FlexWrapper>
)} )}
</div> </Stack>
<div </DialogContent>
style={{ <DialogActions>
width: '100%',
display: 'flex',
justifyContent: 'space-around',
}}>
<Button
block
variant={'outline-secondary'}
onClick={props.onHide}>
{constants.CLOSE}
</Button>
<div style={{ width: '30px' }} />
{props.exportStats.failed !== 0 ? ( {props.exportStats.failed !== 0 ? (
<Button <Button
block size="large"
variant={'outline-danger'} color="accent"
onClick={props.retryFailed}> onClick={props.retryFailed}>
{constants.RETRY_EXPORT_} {constants.RETRY_EXPORT_}
</Button> </Button>
) : ( ) : (
<Button <Button
block size="large"
variant={'outline-success'} color="primary"
onClick={props.exportFiles}> onClick={props.exportFiles}>
{constants.EXPORT_AGAIN} {constants.EXPORT_AGAIN}
</Button> </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 React from 'react';
import { Button, ProgressBar } from 'react-bootstrap';
import { ExportProgress } from 'types/export'; 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 constants from 'utils/strings/constants';
import { ExportStage } from 'constants/export'; import { ExportStage } from 'constants/export';
import VerticallyCentered, { FlexWrapper } from './Container';
import { ProgressBar } from 'react-bootstrap';
export const ComfySpan = styled('span')` export const ComfySpan = styled('span')`
word-spacing: 1rem; word-spacing: 1rem;
@ -11,10 +18,6 @@ export const ComfySpan = styled('span')`
`; `;
interface Props { interface Props {
show: boolean;
onHide: () => void;
exportFolder: string;
exportSize: string;
exportStage: ExportStage; exportStage: ExportStage;
exportProgress: ExportProgress; exportProgress: ExportProgress;
resumeExport: () => void; resumeExport: () => void;
@ -25,66 +28,59 @@ interface Props {
export default function ExportInProgress(props: Props) { export default function ExportInProgress(props: Props) {
return ( return (
<> <>
<div <DialogContent>
style={{ <VerticallyCentered>
marginBottom: '30px', <Box mb={1.5}>
padding: '0 5%',
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
}}>
<div style={{ marginBottom: '10px' }}>
<ComfySpan> <ComfySpan>
{' '} {' '}
{props.exportProgress.current} /{' '} {props.exportProgress.current} /{' '}
{props.exportProgress.total}{' '} {props.exportProgress.total}{' '}
</ComfySpan>{' '} </ComfySpan>{' '}
<span style={{ marginLeft: '10px' }}> <span>
{' '} {' '}
files exported{' '} files exported{' '}
{props.exportStage === ExportStage.PAUSED && `(paused)`} {props.exportStage === ExportStage.PAUSED &&
`(paused)`}
</span> </span>
</div> </Box>
<div style={{ width: '100%', marginBottom: '30px' }}> <FlexWrapper px={1}>
<ProgressBar <ProgressBar
style={{ width: '100%' }}
now={Math.round( now={Math.round(
(props.exportProgress.current * 100) / (props.exportProgress.current * 100) /
props.exportProgress.total props.exportProgress.total
)} )}
animated={!(props.exportStage === ExportStage.PAUSED)} animated={
!(props.exportStage === ExportStage.PAUSED)
}
variant="upload-progress-bar" variant="upload-progress-bar"
/> />
</div> </FlexWrapper>
<div </VerticallyCentered>
style={{ </DialogContent>
width: '100%', <DialogActions>
display: 'flex',
justifyContent: 'space-around',
}}>
{props.exportStage === ExportStage.PAUSED ? ( {props.exportStage === ExportStage.PAUSED ? (
<Button <Button
block size="large"
variant={'outline-secondary'} onClick={props.resumeExport}
onClick={props.resumeExport}> color="accent">
{constants.RESUME} {constants.RESUME}
</Button> </Button>
) : ( ) : (
<Button <Button
block size="large"
variant={'outline-secondary'} onClick={props.pauseExport}
onClick={props.pauseExport}> color="primary">
{constants.PAUSE} {constants.PAUSE}
</Button> </Button>
)} )}
<div style={{ width: '30px' }} />
<Button <Button
block size="large"
variant={'outline-danger'} onClick={props.cancelExport}
onClick={props.cancelExport}> color="secondary">
{constants.CANCEL} {constants.CANCEL}
</Button> </Button>
</div> </DialogActions>
</div>
</> </>
); );
} }

View file

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

View file

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

View file

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

View file

@ -1,13 +1,14 @@
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useRouter } from 'next/router'; 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 { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
import { PAGES } from 'constants/pages'; import { PAGES } from 'constants/pages';
import FormPaperTitle from './Form/FormPaper/Title'; import FormPaperTitle from './Form/FormPaper/Title';
import FormPaperFooter from './Form/FormPaper/Footer'; import FormPaperFooter from './Form/FormPaper/Footer';
import LinkButton from './pages/gallery/LinkButton'; import LinkButton from './pages/gallery/LinkButton';
import SingleInputForm, { SingleInputFormProps } from './SingleInputForm'; import SingleInputForm, { SingleInputFormProps } from './SingleInputForm';
import { Input } from '@mui/material';
interface LoginProps { interface LoginProps {
signUp: () => void; signUp: () => void;
@ -32,7 +33,7 @@ export default function Login(props: LoginProps) {
setFieldError setFieldError
) => { ) => {
try { try {
await getOtt(email); await sendOtt(email);
setData(LS_KEYS.USER, { email }); setData(LS_KEYS.USER, { email });
router.push(PAGES.VERIFY); router.push(PAGES.VERIFY);
} catch (e) { } catch (e) {
@ -48,6 +49,8 @@ export default function Login(props: LoginProps) {
fieldType="email" fieldType="email"
placeholder={constants.ENTER_EMAIL} placeholder={constants.ENTER_EMAIL}
buttonText={constants.LOGIN} buttonText={constants.LOGIN}
autoComplete="username"
hiddenPostInput={<Input hidden type="password" value="" />}
/> />
<FormPaperFooter> <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 { Box, Button, Dialog, DialogContent, Typography } from '@mui/material';
import VerticallyCentered from 'components/Container'; import VerticallyCentered, { FlexWrapper } from 'components/Container';
import DialogBoxBase from 'components/DialogBox/base';
import DialogTitleWithCloseButton from 'components/DialogBox/titleWithCloseButton';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import React, { useContext } from 'react'; import React, { useContext, useEffect } from 'react';
import billingService from 'services/billingService'; import billingService from 'services/billingService';
import { getFamilyPlanAdmin } from 'utils/billing'; import { getFamilyPlanAdmin } from 'utils/billing';
import { preloadImage } from 'utils/common';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import DialogTitleWithCloseButton from './DialogBox/TitleWithCloseButton';
export function MemberSubscriptionManage({ open, userDetails, onClose }) { export function MemberSubscriptionManage({ open, userDetails, onClose }) {
const { setDialogMessage } = useContext(AppContext); const { setDialogMessage, isMobile } = useContext(AppContext);
useEffect(() => {
preloadImage('/images/family-plan');
}, []);
async function onLeaveFamilyClick() { async function onLeaveFamilyClick() {
try { try {
@ -26,7 +31,7 @@ export function MemberSubscriptionManage({ open, userDetails, onClose }) {
title: `${constants.LEAVE_FAMILY_PLAN}`, title: `${constants.LEAVE_FAMILY_PLAN}`,
content: constants.LEAVE_FAMILY_CONFIRM, content: constants.LEAVE_FAMILY_CONFIRM,
proceed: { proceed: {
text: constants.LEAVE_FAMILY_PLAN, text: constants.LEAVE,
action: onLeaveFamilyClick, action: onLeaveFamilyClick,
variant: 'danger', variant: 'danger',
}, },
@ -40,26 +45,38 @@ export function MemberSubscriptionManage({ open, userDetails, onClose }) {
} }
return ( return (
<DialogBoxBase open={open} onClose={onClose} maxWidth="xs"> <Dialog
fullWidth
open={open}
onClose={onClose}
maxWidth="xs"
fullScreen={isMobile}>
<DialogTitleWithCloseButton onClose={onClose}> <DialogTitleWithCloseButton onClose={onClose}>
<Typography variant="h3">{constants.SUBSCRIPTION}</Typography> <Typography variant="h3" fontWeight={'bold'}>
{constants.SUBSCRIPTION}
</Typography>
<Typography color={'text.secondary'}> <Typography color={'text.secondary'}>
{constants.FAMILY_PLAN} {constants.FAMILY_PLAN}
</Typography> </Typography>
</DialogTitleWithCloseButton> </DialogTitleWithCloseButton>
<DialogContent> <DialogContent>
<VerticallyCentered> <VerticallyCentered>
<Box mb={4}>
<Typography color="text.secondary"> <Typography color="text.secondary">
{constants.FAMILY_SUBSCRIPTION_INFO} {constants.FAMILY_SUBSCRIPTION_INFO}
</Typography> </Typography>
<Typography> <Typography>
{getFamilyPlanAdmin(userDetails.familyData)?.email} {getFamilyPlanAdmin(userDetails.familyData)?.email}
</Typography> </Typography>
</Box>
<img <img
height="267px" height={256}
width="256px" src="/images/family-plan/1x.png"
src="/images/family_plan_leave@3x.png" srcSet="/images/family-plan/2x.png 2x,
/images/family-plan/3x.png 3x"
/> />
<FlexWrapper px={2}>
<Button <Button
size="large" size="large"
variant="outlined" variant="outlined"
@ -67,8 +84,9 @@ export function MemberSubscriptionManage({ open, userDetails, onClose }) {
onClick={confirmLeaveFamily}> onClick={confirmLeaveFamily}>
{constants.LEAVE_FAMILY_PLAN} {constants.LEAVE_FAMILY_PLAN}
</Button> </Button>
</FlexWrapper>
</VerticallyCentered> </VerticallyCentered>
</DialogContent> </DialogContent>
</DialogBoxBase> </Dialog>
); );
} }

View file

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

View file

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

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