Merge branch 'master' into redirect_to_payements
This commit is contained in:
commit
ac1a59394e
|
@ -8,7 +8,8 @@
|
||||||
"plugin:react/recommended",
|
"plugin:react/recommended",
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
"google"
|
"google",
|
||||||
|
"prettier"
|
||||||
],
|
],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
@ -23,13 +24,7 @@
|
||||||
"@typescript-eslint"
|
"@typescript-eslint"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent": [
|
"indent":"off",
|
||||||
"error",
|
|
||||||
4,
|
|
||||||
{
|
|
||||||
"SwitchCase": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"class-methods-use-this": "off",
|
"class-methods-use-this": "off",
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
"react/display-name": "off",
|
"react/display-name": "off",
|
||||||
|
@ -48,7 +43,8 @@
|
||||||
"error",
|
"error",
|
||||||
"always"
|
"always"
|
||||||
],
|
],
|
||||||
"space-before-function-paren": "off"
|
"space-before-function-paren": "off",
|
||||||
|
"operator-linebreak":["error","after", { "overrides": { "?": "before", ":": "before" } }]
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"react": {
|
"react": {
|
||||||
|
|
1
.husky/.gitignore
vendored
1
.husky/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
_
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
yarn lint-staged
|
npx lint-staged
|
||||||
|
|
6
.prettierrc.json
Normal file
6
.prettierrc.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxBracketSameLine": true
|
||||||
|
}
|
|
@ -2,7 +2,13 @@ Web application for [ente](https://ente.io) built with lots of ❤️ and a litt
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
First, run the development server:
|
First, pull and install dependencies
|
||||||
|
```bash
|
||||||
|
git submodule update --init --recursive
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the development server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
20
package.json
20
package.json
|
@ -9,8 +9,7 @@
|
||||||
"build-analyze": "ANALYZE=true next build",
|
"build-analyze": "ANALYZE=true next build",
|
||||||
"postbuild": "next export",
|
"postbuild": "next export",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint-staged": "lint-staged",
|
"prepare": "husky install"
|
||||||
"postinstall": "husky install"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ente-io/next-with-workbox": "^1.0.3",
|
"@ente-io/next-with-workbox": "^1.0.3",
|
||||||
|
@ -18,7 +17,7 @@
|
||||||
"@stripe/stripe-js": "^1.13.2",
|
"@stripe/stripe-js": "^1.13.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.25.0",
|
"@typescript-eslint/eslint-plugin": "^4.25.0",
|
||||||
"@typescript-eslint/parser": "^4.25.0",
|
"@typescript-eslint/parser": "^4.25.0",
|
||||||
"axios": "^0.20.0",
|
"axios": "^0.21.1",
|
||||||
"bootstrap": "^4.5.2",
|
"bootstrap": "^4.5.2",
|
||||||
"chrono-node": "^2.2.6",
|
"chrono-node": "^2.2.6",
|
||||||
"comlink": "^4.3.0",
|
"comlink": "^4.3.0",
|
||||||
|
@ -32,6 +31,7 @@
|
||||||
"heic2any": "^0.0.3",
|
"heic2any": "^0.0.3",
|
||||||
"http-proxy-middleware": "^1.0.5",
|
"http-proxy-middleware": "^1.0.5",
|
||||||
"is-electron": "^2.2.0",
|
"is-electron": "^2.2.0",
|
||||||
|
"jszip": "3.7.1",
|
||||||
"libsodium-wrappers": "^0.7.8",
|
"libsodium-wrappers": "^0.7.8",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
"next": "^10.2.3",
|
"next": "^10.2.3",
|
||||||
|
@ -62,7 +62,6 @@
|
||||||
"@next/bundle-analyzer": "^9.5.3",
|
"@next/bundle-analyzer": "^9.5.3",
|
||||||
"@types/debounce-promise": "^3.1.3",
|
"@types/debounce-promise": "^3.1.3",
|
||||||
"@types/libsodium-wrappers": "^0.7.8",
|
"@types/libsodium-wrappers": "^0.7.8",
|
||||||
"@types/localforage": "^0.0.34",
|
|
||||||
"@types/node": "^14.6.4",
|
"@types/node": "^14.6.4",
|
||||||
"@types/photoswipe": "^4.1.1",
|
"@types/photoswipe": "^4.1.1",
|
||||||
"@types/react": "^16.9.49",
|
"@types/react": "^16.9.49",
|
||||||
|
@ -74,20 +73,17 @@
|
||||||
"babel-plugin-styled-components": "^1.11.1",
|
"babel-plugin-styled-components": "^1.11.1",
|
||||||
"eslint": "^7.27.0",
|
"eslint": "^7.27.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-react": "^7.23.2",
|
"eslint-plugin-react": "^7.23.2",
|
||||||
"husky": "^6.0.0",
|
"husky": "^7.0.1",
|
||||||
"lint-staged": "^11.0.0",
|
"lint-staged": "^11.1.2",
|
||||||
|
"prettier": "2.3.2",
|
||||||
"typescript": "^4.1.3"
|
"typescript": "^4.1.3"
|
||||||
},
|
},
|
||||||
"standard": {
|
"standard": {
|
||||||
"parser": "babel-eslint"
|
"parser": "babel-eslint"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"src/**/*.{js,jsx,ts,tsx}": "eslint"
|
"src/**/*.{js,jsx,ts,tsx}": ["eslint --fix","prettier --write --ignore-unknown"]
|
||||||
},
|
|
||||||
"husky": {
|
|
||||||
"hooks": {
|
|
||||||
"pre-commit": "yarn run lint-staged"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import * as Sentry from '@sentry/nextjs';
|
import * as Sentry from '@sentry/nextjs';
|
||||||
import { getSentryTunnelUrl } from 'utils/common/apiUtil';
|
import { getSentryTunnelUrl } from 'utils/common/apiUtil';
|
||||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
import { getUserAnonymizedID } from 'utils/user';
|
||||||
|
|
||||||
|
|
||||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN ?? 'https://860186db60c54c7fbacfe255124958e8@errors.ente.io/4';
|
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN ?? 'https://860186db60c54c7fbacfe255124958e8@errors.ente.io/4';
|
||||||
const SENTRY_ENV = process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development';
|
const SENTRY_ENV = process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development';
|
||||||
const userID = getData(LS_KEYS.USER)?.id;
|
|
||||||
|
|
||||||
Sentry.setUser({ id: userID });
|
Sentry.setUser({ id: getUserAnonymizedID() });
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: SENTRY_DSN,
|
dsn: SENTRY_DSN,
|
||||||
enabled: SENTRY_ENV !== 'development',
|
enabled: SENTRY_ENV !== 'development',
|
||||||
|
|
|
@ -25,8 +25,7 @@ export default function AddToCollectionBtn(props) {
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round">
|
||||||
>
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19" />
|
<line x1="12" y1="5" x2="12" y2="19" />
|
||||||
<line x1="5" y1="12" x2="19" y2="12" />
|
<line x1="5" y1="12" x2="19" y2="12" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
175
src/components/ChangeEmail.tsx
Normal file
175
src/components/ChangeEmail.tsx
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
import { Formik, FormikHelpers } from 'formik';
|
||||||
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||||
|
import { Button, Col, Form, FormControl } from 'react-bootstrap';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
import SubmitButton from 'components/SubmitButton';
|
||||||
|
import router from 'next/router';
|
||||||
|
import { changeEmail, getOTTForEmailChange } from 'services/userService';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
|
||||||
|
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||||
|
|
||||||
|
interface formValues {
|
||||||
|
email: string;
|
||||||
|
ott?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmailRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border: 1px solid grey;
|
||||||
|
margin-bottom: 19px;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
color: #fff;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
showMessage: (value: boolean) => void;
|
||||||
|
setEmail: (email: string) => void;
|
||||||
|
}
|
||||||
|
function ChangeEmailForm(props: Props) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [ottInputVisible, setShowOttInputVisibility] = useState(false);
|
||||||
|
const emailInputElement = useRef(null);
|
||||||
|
const ottInputRef = useRef(null);
|
||||||
|
const appContext = useContext(AppContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
emailInputElement.current?.focus();
|
||||||
|
}, 250);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ottInputVisible) {
|
||||||
|
props.showMessage(false);
|
||||||
|
}
|
||||||
|
}, [ottInputVisible]);
|
||||||
|
|
||||||
|
const requestOTT = async (
|
||||||
|
{ email }: formValues,
|
||||||
|
{ setFieldError }: FormikHelpers<formValues>
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await getOTTForEmailChange(email);
|
||||||
|
props.setEmail(email);
|
||||||
|
setShowOttInputVisibility(true);
|
||||||
|
props.showMessage(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
ottInputRef.current?.focus();
|
||||||
|
}, 250);
|
||||||
|
} catch (e) {
|
||||||
|
setFieldError('email', `${constants.EMAIl_ALREADY_OWNED}`);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestEmailChange = async (
|
||||||
|
{ email, ott }: formValues,
|
||||||
|
{ setFieldError }: FormikHelpers<formValues>
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await changeEmail(email, ott);
|
||||||
|
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), email });
|
||||||
|
appContext.setDisappearingFlashMessage({
|
||||||
|
message: constants.EMAIL_UDPATE_SUCCESSFUL,
|
||||||
|
type: FLASH_MESSAGE_TYPE.SUCCESS,
|
||||||
|
});
|
||||||
|
router.push('/gallery');
|
||||||
|
} catch (e) {
|
||||||
|
setFieldError('ott', `${constants.INCORRECT_CODE}`);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik<formValues>
|
||||||
|
initialValues={{ email: '' }}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
email: Yup.string()
|
||||||
|
.email(constants.EMAIL_ERROR)
|
||||||
|
.required(constants.REQUIRED),
|
||||||
|
})}
|
||||||
|
validateOnChange={false}
|
||||||
|
validateOnBlur={false}
|
||||||
|
onSubmit={!ottInputVisible ? requestOTT : requestEmailChange}>
|
||||||
|
{({ values, errors, touched, handleChange, handleSubmit }) => (
|
||||||
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
|
{!ottInputVisible ? (
|
||||||
|
<Form.Group controlId="formBasicEmail">
|
||||||
|
<Form.Control
|
||||||
|
ref={emailInputElement}
|
||||||
|
type="email"
|
||||||
|
placeholder={constants.ENTER_EMAIL}
|
||||||
|
value={values.email}
|
||||||
|
onChange={handleChange('email')}
|
||||||
|
isInvalid={Boolean(
|
||||||
|
touched.email && errors.email
|
||||||
|
)}
|
||||||
|
autoFocus
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<FormControl.Feedback type="invalid">
|
||||||
|
{errors.email}
|
||||||
|
</FormControl.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<EmailRow>
|
||||||
|
<Col xs="8">{values.email}</Col>
|
||||||
|
<Col xs="4">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={() =>
|
||||||
|
setShowOttInputVisibility(false)
|
||||||
|
}>
|
||||||
|
{constants.CHANGE}
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</EmailRow>
|
||||||
|
<Form.Group controlId="formBasicEmail">
|
||||||
|
<Form.Control
|
||||||
|
ref={ottInputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder={constants.ENTER_OTT}
|
||||||
|
value={values.ott}
|
||||||
|
onChange={handleChange('ott')}
|
||||||
|
isInvalid={Boolean(
|
||||||
|
touched.ott && errors.ott
|
||||||
|
)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<FormControl.Feedback type="invalid">
|
||||||
|
{errors.ott}
|
||||||
|
</FormControl.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SubmitButton
|
||||||
|
buttonText={
|
||||||
|
!ottInputVisible
|
||||||
|
? constants.SEND_OTT
|
||||||
|
: constants.VERIFY
|
||||||
|
}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
variant="link"
|
||||||
|
className="text-center"
|
||||||
|
onClick={router.back}>
|
||||||
|
{constants.GO_BACK}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChangeEmailForm;
|
|
@ -34,7 +34,7 @@ function CollectionShare(props: Props) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const collectionShare = async (
|
const collectionShare = async (
|
||||||
{ email }: formValues,
|
{ email }: formValues,
|
||||||
{ resetForm, setFieldError }: FormikHelpers<formValues>,
|
{ resetForm, setFieldError }: FormikHelpers<formValues>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -89,8 +89,7 @@ function CollectionShare(props: Props) {
|
||||||
fontSize: '1.2em',
|
fontSize: '1.2em',
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
}}
|
}}
|
||||||
onClick={() => collectionUnshare(sharee)}
|
onClick={() => collectionUnshare(sharee)}>
|
||||||
>
|
|
||||||
-
|
-
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
|
@ -100,8 +99,7 @@ function CollectionShare(props: Props) {
|
||||||
<MessageDialog
|
<MessageDialog
|
||||||
show={props.show}
|
show={props.show}
|
||||||
onHide={props.onHide}
|
onHide={props.onHide}
|
||||||
attributes={{ title: constants.SHARE_COLLECTION }}
|
attributes={{ title: constants.SHARE_COLLECTION }}>
|
||||||
>
|
|
||||||
<DeadCenter style={{ width: '85%', margin: 'auto' }}>
|
<DeadCenter style={{ width: '85%', margin: 'auto' }}>
|
||||||
<h6>{constants.SHARE_WITH_PEOPLE}</h6>
|
<h6>{constants.SHARE_WITH_PEOPLE}</h6>
|
||||||
<p />
|
<p />
|
||||||
|
@ -114,8 +112,7 @@ function CollectionShare(props: Props) {
|
||||||
})}
|
})}
|
||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}
|
||||||
onSubmit={collectionShare}
|
onSubmit={collectionShare}>
|
||||||
>
|
|
||||||
{({
|
{({
|
||||||
values,
|
values,
|
||||||
errors,
|
errors,
|
||||||
|
@ -128,15 +125,14 @@ function CollectionShare(props: Props) {
|
||||||
<Form.Group
|
<Form.Group
|
||||||
as={Col}
|
as={Col}
|
||||||
xs={10}
|
xs={10}
|
||||||
controlId="formHorizontalEmail"
|
controlId="formHorizontalEmail">
|
||||||
>
|
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="email"
|
type="email"
|
||||||
placeholder={constants.ENTER_EMAIL}
|
placeholder={constants.ENTER_EMAIL}
|
||||||
value={values.email}
|
value={values.email}
|
||||||
onChange={handleChange('email')}
|
onChange={handleChange('email')}
|
||||||
isInvalid={Boolean(
|
isInvalid={Boolean(
|
||||||
touched.email && errors.email,
|
touched.email && errors.email
|
||||||
)}
|
)}
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
@ -148,8 +144,7 @@ function CollectionShare(props: Props) {
|
||||||
<Form.Group
|
<Form.Group
|
||||||
as={Col}
|
as={Col}
|
||||||
xs={2}
|
xs={2}
|
||||||
controlId="formHorizontalEmail"
|
controlId="formHorizontalEmail">
|
||||||
>
|
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
loading={loading}
|
loading={loading}
|
||||||
inline
|
inline
|
||||||
|
|
|
@ -31,26 +31,27 @@ export const IconButton = styled.button`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
&:focus, &:hover {
|
&:focus,
|
||||||
|
&:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Row = styled.div`
|
export const Row = styled.div`
|
||||||
display:flex;
|
display: flex;
|
||||||
align-items:center;
|
align-items: center;
|
||||||
margin-bottom:20px;
|
margin-bottom: 20px;
|
||||||
flex:1
|
flex: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Label = styled.div <{ width?: string }> `
|
export const Label = styled.div<{ width?: string }>`
|
||||||
width:${(props) => props.width ?? '70%'};
|
width: ${(props) => props.width ?? '70%'};
|
||||||
`;
|
`;
|
||||||
export const Value = styled.div <{ width?: string }> `
|
export const Value = styled.div<{ width?: string }>`
|
||||||
display:flex;
|
display: flex;
|
||||||
justify-content:flex-start;
|
justify-content: flex-start;
|
||||||
align-items:center;
|
align-items: center;
|
||||||
width:${(props) => props.width ?? '30%'};
|
width: ${(props) => props.width ?? '30%'};
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -20,8 +20,7 @@ export default function DeleteBtn(props) {
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
height={props.height}
|
height={props.height}
|
||||||
viewBox={props.viewBox}
|
viewBox={props.viewBox}
|
||||||
width={props.width}
|
width={props.width}>
|
||||||
>
|
|
||||||
<path d="M0 0h24v24H0z" fill="none" />
|
<path d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
|
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
39
src/components/EnteCard.tsx
Normal file
39
src/components/EnteCard.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Card } from 'react-bootstrap';
|
||||||
|
|
||||||
|
type Size = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
|
const EnteCard = ({
|
||||||
|
size,
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
size: Size;
|
||||||
|
children?: any;
|
||||||
|
style?: any;
|
||||||
|
}) => {
|
||||||
|
let minWidth: string;
|
||||||
|
let padding: string;
|
||||||
|
switch (size) {
|
||||||
|
case 'sm':
|
||||||
|
minWidth = '320px';
|
||||||
|
padding = '0px';
|
||||||
|
break;
|
||||||
|
case 'md':
|
||||||
|
minWidth = '460px';
|
||||||
|
padding = '10px';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
minWidth = '480px';
|
||||||
|
padding = '10px';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Card style={{ minWidth, padding, ...style }} className="text-center">
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EnteCard;
|
|
@ -6,14 +6,13 @@ import constants from 'utils/strings/constants';
|
||||||
import { Label, Row, Value } from './Container';
|
import { Label, Row, Value } from './Container';
|
||||||
import { ComfySpan } from './ExportInProgress';
|
import { ComfySpan } from './ExportInProgress';
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
show: boolean
|
show: boolean;
|
||||||
onHide: () => void
|
onHide: () => void;
|
||||||
exportFolder: string
|
exportFolder: string;
|
||||||
exportSize: string
|
exportSize: string;
|
||||||
lastExportTime: number
|
lastExportTime: number;
|
||||||
exportStats: ExportStats
|
exportStats: ExportStats;
|
||||||
updateExportFolder: (newFolder: string) => void;
|
updateExportFolder: (newFolder: string) => void;
|
||||||
exportFiles: () => void;
|
exportFiles: () => void;
|
||||||
retryFailed: () => void;
|
retryFailed: () => void;
|
||||||
|
@ -23,30 +22,69 @@ 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 style={{ borderBottom: '1px solid #444', marginBottom: '20px', padding: '0 5%' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
borderBottom: '1px solid #444',
|
||||||
|
marginBottom: '20px',
|
||||||
|
padding: '0 5%',
|
||||||
|
}}>
|
||||||
<Row>
|
<Row>
|
||||||
<Label width="40%">{constants.LAST_EXPORT_TIME}</Label>
|
<Label width="40%">{constants.LAST_EXPORT_TIME}</Label>
|
||||||
<Value width="60%">{formatDateTime(props.lastExportTime)}</Value>
|
<Value width="60%">
|
||||||
</Row>
|
{formatDateTime(props.lastExportTime)}
|
||||||
<Row>
|
|
||||||
<Label width="60%">{constants.SUCCESSFULLY_EXPORTED_FILES}</Label>
|
|
||||||
<Value width="35%"><ComfySpan>{props.exportStats.success} / {totalFiles}</ComfySpan></Value>
|
|
||||||
</Row>
|
|
||||||
{props.exportStats.failed>0 &&
|
|
||||||
<Row>
|
|
||||||
<Label width="60%">{constants.FAILED_EXPORTED_FILES}</Label>
|
|
||||||
<Value width="35%">
|
|
||||||
<ComfySpan>{props.exportStats.failed} / {totalFiles}</ComfySpan>
|
|
||||||
</Value>
|
</Value>
|
||||||
</Row>}
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Label width="60%">
|
||||||
|
{constants.SUCCESSFULLY_EXPORTED_FILES}
|
||||||
|
</Label>
|
||||||
|
<Value width="35%">
|
||||||
|
<ComfySpan>
|
||||||
|
{props.exportStats.success} / {totalFiles}
|
||||||
|
</ComfySpan>
|
||||||
|
</Value>
|
||||||
|
</Row>
|
||||||
|
{props.exportStats.failed > 0 && (
|
||||||
|
<Row>
|
||||||
|
<Label width="60%">
|
||||||
|
{constants.FAILED_EXPORTED_FILES}
|
||||||
|
</Label>
|
||||||
|
<Value width="35%">
|
||||||
|
<ComfySpan>
|
||||||
|
{props.exportStats.failed} / {totalFiles}
|
||||||
|
</ComfySpan>
|
||||||
|
</Value>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-around' }}>
|
<div
|
||||||
<Button block variant={'outline-secondary'} onClick={props.onHide}>{constants.CLOSE}</Button>
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
}}>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
variant={'outline-secondary'}
|
||||||
|
onClick={props.onHide}>
|
||||||
|
{constants.CLOSE}
|
||||||
|
</Button>
|
||||||
<div style={{ width: '30px' }} />
|
<div style={{ width: '30px' }} />
|
||||||
{props.exportStats.failed !== 0 ?
|
{props.exportStats.failed !== 0 ? (
|
||||||
<Button block variant={'outline-danger'} onClick={props.retryFailed}>{constants.RETRY_EXPORT_}</Button> :
|
<Button
|
||||||
<Button block variant={'outline-success'} onClick={props.exportFiles}>{constants.EXPORT_AGAIN}</Button>
|
block
|
||||||
}
|
variant={'outline-danger'}
|
||||||
|
onClick={props.retryFailed}>
|
||||||
|
{constants.RETRY_EXPORT_}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
variant={'outline-success'}
|
||||||
|
onClick={props.exportFiles}>
|
||||||
|
{constants.EXPORT_AGAIN}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,42 +5,82 @@ import styled from 'styled-components';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
|
||||||
export const ComfySpan = styled.span`
|
export const ComfySpan = styled.span`
|
||||||
word-spacing:1rem;
|
word-spacing: 1rem;
|
||||||
color:#ddd;
|
color: #ddd;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
show: boolean
|
show: boolean;
|
||||||
onHide: () => void
|
onHide: () => void;
|
||||||
exportFolder: string
|
exportFolder: string;
|
||||||
exportSize: string
|
exportSize: string;
|
||||||
exportStage: ExportStage
|
exportStage: ExportStage;
|
||||||
exportProgress: ExportProgress
|
exportProgress: ExportProgress;
|
||||||
resumeExport: () => void;
|
resumeExport: () => void;
|
||||||
cancelExport: () => void
|
cancelExport: () => void;
|
||||||
pauseExport: () => void;
|
pauseExport: () => void;
|
||||||
}
|
}
|
||||||
export default function ExportInProgress(props: Props) {
|
export default function ExportInProgress(props: Props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ marginBottom: '30px', padding: '0 5%', display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: '30px',
|
||||||
|
padding: '0 5%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
<div style={{ marginBottom: '10px' }}>
|
<div style={{ marginBottom: '10px' }}>
|
||||||
<ComfySpan> {props.exportProgress.current} / {props.exportProgress.total} </ComfySpan> <span style={{ marginLeft: '10px' }}> files exported {props.exportStage === ExportStage.PAUSED && `(paused)`}</span>
|
<ComfySpan>
|
||||||
|
{' '}
|
||||||
|
{props.exportProgress.current} /{' '}
|
||||||
|
{props.exportProgress.total}{' '}
|
||||||
|
</ComfySpan>{' '}
|
||||||
|
<span style={{ marginLeft: '10px' }}>
|
||||||
|
{' '}
|
||||||
|
files exported{' '}
|
||||||
|
{props.exportStage === ExportStage.PAUSED && `(paused)`}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: '100%', marginBottom: '30px' }}>
|
<div style={{ width: '100%', marginBottom: '30px' }}>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
now={Math.round(props.exportProgress.current * 100 / props.exportProgress.total)}
|
now={Math.round(
|
||||||
|
(props.exportProgress.current * 100) /
|
||||||
|
props.exportProgress.total
|
||||||
|
)}
|
||||||
animated={!(props.exportStage === ExportStage.PAUSED)}
|
animated={!(props.exportStage === ExportStage.PAUSED)}
|
||||||
variant="upload-progress-bar"
|
variant="upload-progress-bar"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-around' }}>
|
<div
|
||||||
{props.exportStage === ExportStage.PAUSED ?
|
style={{
|
||||||
<Button block variant={'outline-secondary'} onClick={props.resumeExport}>{constants.RESUME}</Button> :
|
width: '100%',
|
||||||
<Button block variant={'outline-secondary'} onClick={props.pauseExport}>{constants.PAUSE}</Button>
|
display: 'flex',
|
||||||
}
|
justifyContent: 'space-around',
|
||||||
|
}}>
|
||||||
|
{props.exportStage === ExportStage.PAUSED ? (
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
variant={'outline-secondary'}
|
||||||
|
onClick={props.resumeExport}>
|
||||||
|
{constants.RESUME}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
variant={'outline-secondary'}
|
||||||
|
onClick={props.pauseExport}>
|
||||||
|
{constants.PAUSE}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<div style={{ width: '30px' }} />
|
<div style={{ width: '30px' }} />
|
||||||
<Button block variant={'outline-danger'} onClick={props.cancelExport}>{constants.CANCEL}</Button>
|
<Button
|
||||||
|
block
|
||||||
|
variant={'outline-danger'}
|
||||||
|
onClick={props.cancelExport}>
|
||||||
|
{constants.CANCEL}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -4,18 +4,18 @@ import { Button } from 'react-bootstrap';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
show: boolean
|
show: boolean;
|
||||||
onHide: () => void
|
onHide: () => void;
|
||||||
updateExportFolder: (newFolder: string) => void;
|
updateExportFolder: (newFolder: string) => void;
|
||||||
exportFolder: string
|
exportFolder: string;
|
||||||
startExport: () => void
|
startExport: () => void;
|
||||||
exportSize: string;
|
exportSize: string;
|
||||||
selectExportDirectory: () => void
|
selectExportDirectory: () => void;
|
||||||
}
|
}
|
||||||
export default function ExportInit(props: Props) {
|
export default function ExportInit(props: Props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeadCenter >
|
<DeadCenter>
|
||||||
<Button
|
<Button
|
||||||
variant="outline-success"
|
variant="outline-success"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
@ -26,8 +26,9 @@ export default function ExportInit(props: Props) {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
onClick={props.startExport}
|
onClick={props.startExport}>
|
||||||
>{constants.START}</Button>
|
{constants.START}
|
||||||
|
</Button>
|
||||||
</DeadCenter>
|
</DeadCenter>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button } from 'react-bootstrap';
|
import { Button } from 'react-bootstrap';
|
||||||
import exportService, { ExportProgress, ExportStage, ExportStats, ExportType } from 'services/exportService';
|
import exportService, {
|
||||||
|
ExportProgress,
|
||||||
|
ExportStage,
|
||||||
|
ExportStats,
|
||||||
|
ExportType,
|
||||||
|
} from 'services/exportService';
|
||||||
import { getLocalFiles } from 'services/fileService';
|
import { getLocalFiles } from 'services/fileService';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { sleep } from 'utils/common';
|
import { sleep } from 'utils/common';
|
||||||
import { getFileUID } from 'utils/export';
|
import { getExportRecordFileUID } from 'utils/export';
|
||||||
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 { Label, Row, Value } from './Container';
|
||||||
|
@ -22,13 +27,13 @@ const FolderIconWrapper = styled.div`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
border-radius:15%;
|
border-radius: 15%;
|
||||||
&:hover{
|
&:hover {
|
||||||
background-color:#444;
|
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;
|
||||||
|
@ -40,16 +45,22 @@ const ExportFolderPathContainer =styled.span`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
show: boolean
|
show: boolean;
|
||||||
onHide: () => void
|
onHide: () => void;
|
||||||
usage: string
|
usage: string;
|
||||||
}
|
}
|
||||||
export default function ExportModal(props: Props) {
|
export default function ExportModal(props: Props) {
|
||||||
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('');
|
||||||
const [exportProgress, setExportProgress] = useState<ExportProgress>({ current: 0, total: 0 });
|
const [exportProgress, setExportProgress] = useState<ExportProgress>({
|
||||||
const [exportStats, setExportStats] = useState<ExportStats>({ failed: 0, success: 0 });
|
current: 0,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
const [exportStats, setExportStats] = useState<ExportStats>({
|
||||||
|
failed: 0,
|
||||||
|
success: 0,
|
||||||
|
});
|
||||||
const [lastExportTime, setLastExportTime] = useState(0);
|
const [lastExportTime, setLastExportTime] = useState(0);
|
||||||
|
|
||||||
// ====================
|
// ====================
|
||||||
|
@ -64,7 +75,9 @@ export default function ExportModal(props: Props) {
|
||||||
exportService.ElectronAPIs.registerStopExportListener(stopExport);
|
exportService.ElectronAPIs.registerStopExportListener(stopExport);
|
||||||
exportService.ElectronAPIs.registerPauseExportListener(pauseExport);
|
exportService.ElectronAPIs.registerPauseExportListener(pauseExport);
|
||||||
exportService.ElectronAPIs.registerResumeExportListener(resumeExport);
|
exportService.ElectronAPIs.registerResumeExportListener(resumeExport);
|
||||||
exportService.ElectronAPIs.registerRetryFailedExportListener(retryFailedExport);
|
exportService.ElectronAPIs.registerRetryFailedExportListener(
|
||||||
|
retryFailedExport
|
||||||
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -76,7 +89,10 @@ export default function ExportModal(props: Props) {
|
||||||
setExportStage(exportInfo?.stage ?? ExportStage.INIT);
|
setExportStage(exportInfo?.stage ?? ExportStage.INIT);
|
||||||
setLastExportTime(exportInfo?.lastAttemptTimestamp);
|
setLastExportTime(exportInfo?.lastAttemptTimestamp);
|
||||||
setExportProgress(exportInfo?.progress ?? { current: 0, total: 0 });
|
setExportProgress(exportInfo?.progress ?? { current: 0, total: 0 });
|
||||||
setExportStats({ success: exportInfo?.exportedFiles?.length ?? 0, failed: exportInfo?.failedFiles?.length ?? 0 });
|
setExportStats({
|
||||||
|
success: exportInfo?.exportedFiles?.length ?? 0,
|
||||||
|
failed: exportInfo?.failedFiles?.length ?? 0,
|
||||||
|
});
|
||||||
if (exportInfo?.stage === ExportStage.INPROGRESS) {
|
if (exportInfo?.stage === ExportStage.INPROGRESS) {
|
||||||
resumeExport();
|
resumeExport();
|
||||||
}
|
}
|
||||||
|
@ -96,10 +112,22 @@ export default function ExportModal(props: Props) {
|
||||||
const failedFilesCnt = exportRecord.failedFiles.length;
|
const failedFilesCnt = exportRecord.failedFiles.length;
|
||||||
const syncedFilesCnt = localFiles.length;
|
const syncedFilesCnt = localFiles.length;
|
||||||
if (syncedFilesCnt > exportedFileCnt + failedFilesCnt) {
|
if (syncedFilesCnt > exportedFileCnt + failedFilesCnt) {
|
||||||
updateExportProgress({ current: exportedFileCnt + failedFilesCnt, total: syncedFilesCnt });
|
updateExportProgress({
|
||||||
const exportFileUIDs = new Set([...exportRecord.exportedFiles, ...exportRecord.failedFiles]);
|
current: exportedFileCnt + failedFilesCnt,
|
||||||
const unExportedFiles = localFiles.filter((file) => !exportFileUIDs.has(getFileUID(file)));
|
total: syncedFilesCnt,
|
||||||
exportService.addFilesQueuedRecord(exportFolder, unExportedFiles);
|
});
|
||||||
|
const exportFileUIDs = new Set([
|
||||||
|
...exportRecord.exportedFiles,
|
||||||
|
...exportRecord.failedFiles,
|
||||||
|
]);
|
||||||
|
const unExportedFiles = localFiles.filter(
|
||||||
|
(file) =>
|
||||||
|
!exportFileUIDs.has(getExportRecordFileUID(file))
|
||||||
|
);
|
||||||
|
exportService.addFilesQueuedRecord(
|
||||||
|
exportFolder,
|
||||||
|
unExportedFiles
|
||||||
|
);
|
||||||
updateExportStage(ExportStage.PAUSED);
|
updateExportStage(ExportStage.PAUSED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,7 +135,6 @@ export default function ExportModal(props: Props) {
|
||||||
main();
|
main();
|
||||||
}, [props.show]);
|
}, [props.show]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExportSize(props.usage);
|
setExportSize(props.usage);
|
||||||
}, [props.usage]);
|
}, [props.usage]);
|
||||||
|
@ -162,7 +189,10 @@ export default function ExportModal(props: Props) {
|
||||||
const startExport = async () => {
|
const startExport = async () => {
|
||||||
await preExportRun();
|
await preExportRun();
|
||||||
updateExportProgress({ current: 0, total: 0 });
|
updateExportProgress({ current: 0, total: 0 });
|
||||||
const { paused } = await exportService.exportFiles(updateExportProgress, ExportType.NEW);
|
const { paused } = await exportService.exportFiles(
|
||||||
|
updateExportProgress,
|
||||||
|
ExportType.NEW
|
||||||
|
);
|
||||||
await postExportRun(paused);
|
await postExportRun(paused);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -184,13 +214,15 @@ export default function ExportModal(props: Props) {
|
||||||
const pausedStageProgress = exportRecord.progress;
|
const pausedStageProgress = exportRecord.progress;
|
||||||
setExportProgress(pausedStageProgress);
|
setExportProgress(pausedStageProgress);
|
||||||
|
|
||||||
const updateExportStatsWithOffset = ((progress: ExportProgress) => updateExportProgress(
|
const updateExportStatsWithOffset = (progress: ExportProgress) =>
|
||||||
{
|
updateExportProgress({
|
||||||
current: pausedStageProgress.current + progress.current,
|
current: pausedStageProgress.current + progress.current,
|
||||||
total: pausedStageProgress.current + progress.total,
|
total: pausedStageProgress.current + progress.total,
|
||||||
},
|
});
|
||||||
));
|
const { paused } = await exportService.exportFiles(
|
||||||
const { paused } = await exportService.exportFiles(updateExportStatsWithOffset, ExportType.PENDING);
|
updateExportStatsWithOffset,
|
||||||
|
ExportType.PENDING
|
||||||
|
);
|
||||||
|
|
||||||
await postExportRun(paused);
|
await postExportRun(paused);
|
||||||
};
|
};
|
||||||
|
@ -199,7 +231,10 @@ export default function ExportModal(props: Props) {
|
||||||
await preExportRun();
|
await preExportRun();
|
||||||
updateExportProgress({ current: 0, total: exportStats.failed });
|
updateExportProgress({ current: 0, total: exportStats.failed });
|
||||||
|
|
||||||
const { paused } = await exportService.exportFiles(updateExportProgress, ExportType.RETRY_FAILED);
|
const { paused } = await exportService.exportFiles(
|
||||||
|
updateExportProgress,
|
||||||
|
ExportType.RETRY_FAILED
|
||||||
|
);
|
||||||
await postExportRun(paused);
|
await postExportRun(paused);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -224,7 +259,8 @@ export default function ExportModal(props: Props) {
|
||||||
switch (exportStage) {
|
switch (exportStage) {
|
||||||
case ExportStage.INIT:
|
case ExportStage.INIT:
|
||||||
return (
|
return (
|
||||||
<ExportInit {...props}
|
<ExportInit
|
||||||
|
{...props}
|
||||||
exportFolder={exportFolder}
|
exportFolder={exportFolder}
|
||||||
exportSize={exportSize}
|
exportSize={exportSize}
|
||||||
updateExportFolder={updateExportFolder}
|
updateExportFolder={updateExportFolder}
|
||||||
|
@ -235,7 +271,8 @@ export default function ExportModal(props: Props) {
|
||||||
case ExportStage.INPROGRESS:
|
case ExportStage.INPROGRESS:
|
||||||
case ExportStage.PAUSED:
|
case ExportStage.PAUSED:
|
||||||
return (
|
return (
|
||||||
<ExportInProgress {...props}
|
<ExportInProgress
|
||||||
|
{...props}
|
||||||
exportFolder={exportFolder}
|
exportFolder={exportFolder}
|
||||||
exportSize={exportSize}
|
exportSize={exportSize}
|
||||||
exportStage={exportStage}
|
exportStage={exportStage}
|
||||||
|
@ -259,7 +296,8 @@ export default function ExportModal(props: Props) {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
default: return (<></>);
|
default:
|
||||||
|
return <></>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -269,34 +307,50 @@ export default function ExportModal(props: Props) {
|
||||||
onHide={props.onHide}
|
onHide={props.onHide}
|
||||||
attributes={{
|
attributes={{
|
||||||
title: constants.EXPORT_DATA,
|
title: constants.EXPORT_DATA,
|
||||||
}}
|
}}>
|
||||||
>
|
<div
|
||||||
<div style={{ borderBottom: '1px solid #444', marginBottom: '20px', padding: '0 5%' }}>
|
style={{
|
||||||
|
borderBottom: '1px solid #444',
|
||||||
|
marginBottom: '20px',
|
||||||
|
padding: '0 5%',
|
||||||
|
width: '450px',
|
||||||
|
}}>
|
||||||
<Row>
|
<Row>
|
||||||
<Label width="40%">{constants.DESTINATION}</Label>
|
<Label width="40%">{constants.DESTINATION}</Label>
|
||||||
<Value width="60%">
|
<Value width="60%">
|
||||||
{!exportFolder ?
|
{!exportFolder ? (
|
||||||
(<Button variant={'outline-success'} size={'sm'} onClick={selectExportDirectory}>{constants.SELECT_FOLDER}</Button>) :
|
<Button
|
||||||
(<>
|
variant={'outline-success'}
|
||||||
|
size={'sm'}
|
||||||
|
onClick={selectExportDirectory}>
|
||||||
|
{constants.SELECT_FOLDER}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{/* <span style={{ overflow: 'hidden', direction: 'rtl', height: '1.5rem', width: '90%', whiteSpace: 'nowrap' }}> */}
|
{/* <span style={{ overflow: 'hidden', direction: 'rtl', height: '1.5rem', width: '90%', whiteSpace: 'nowrap' }}> */}
|
||||||
<ExportFolderPathContainer>
|
<ExportFolderPathContainer>
|
||||||
{exportFolder}
|
{exportFolder}
|
||||||
</ExportFolderPathContainer>
|
</ExportFolderPathContainer>
|
||||||
{/* </span> */}
|
{/* </span> */}
|
||||||
{(exportStage === ExportStage.FINISHED || exportStage === ExportStage.INIT) && (
|
{(exportStage === ExportStage.FINISHED ||
|
||||||
<FolderIconWrapper onClick={selectExportDirectory} >
|
exportStage === ExportStage.INIT) && (
|
||||||
|
<FolderIconWrapper
|
||||||
|
onClick={selectExportDirectory}>
|
||||||
<FolderIcon />
|
<FolderIcon />
|
||||||
</FolderIconWrapper>
|
</FolderIconWrapper>
|
||||||
)}
|
)}
|
||||||
</>)
|
</>
|
||||||
}
|
)}
|
||||||
</Value>
|
</Value>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Label width="40%">{constants.TOTAL_EXPORT_SIZE} </Label><Value width="60%">{exportSize ? `${exportSize} GB` : <InProgressIcon />}</Value>
|
<Label width="40%">{constants.TOTAL_EXPORT_SIZE} </Label>
|
||||||
|
<Value width="60%">
|
||||||
|
{exportSize ? `${exportSize}` : <InProgressIcon />}
|
||||||
|
</Value>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
<ExportDynamicState />
|
<ExportDynamicState />
|
||||||
</MessageDialog >
|
</MessageDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,8 @@ const HeartUI = styled.button<{
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
border: none;
|
border: none;
|
||||||
${({ isClick, size }) => isClick &&
|
${({ isClick, size }) =>
|
||||||
|
isClick &&
|
||||||
`background-position: -${
|
`background-position: -${
|
||||||
28 * size
|
28 * size
|
||||||
}px;transition: background 1s steps(28);`}
|
}px;transition: background 1s steps(28);`}
|
||||||
|
|
|
@ -2,15 +2,19 @@ import { FlashMessage } from 'pages/_app';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Alert from 'react-bootstrap/Alert';
|
import Alert from 'react-bootstrap/Alert';
|
||||||
|
|
||||||
|
export default function FlashMessageBar({
|
||||||
export default function FlashMessageBar({ flashMessage, onClose }: { flashMessage: FlashMessage, onClose: () => void }) {
|
flashMessage,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
flashMessage: FlashMessage;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
className="flash-message text-center"
|
className="flash-message text-center"
|
||||||
variant={flashMessage.severity}
|
variant={flashMessage.type}
|
||||||
dismissible
|
dismissible
|
||||||
onClose={onClose}
|
onClose={onClose}>
|
||||||
>
|
|
||||||
<div style={{ maxWidth: '1024px', width: '80%', margin: 'auto' }}>
|
<div style={{ maxWidth: '1024px', width: '80%', margin: 'auto' }}>
|
||||||
{flashMessage.message}
|
{flashMessage.message}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,9 +5,9 @@ import CrossIcon from './icons/CrossIcon';
|
||||||
|
|
||||||
const CloseButtonWrapper = styled.div`
|
const CloseButtonWrapper = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top:10px;
|
top: 10px;
|
||||||
right:10px;
|
right: 10px;
|
||||||
cursor:pointer;
|
cursor: pointer;
|
||||||
`;
|
`;
|
||||||
const DropDiv = styled.div`
|
const DropDiv = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -62,14 +62,10 @@ export default function FullScreenDropZone(props: Props) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
props.showCollectionSelector();
|
props.showCollectionSelector();
|
||||||
},
|
},
|
||||||
})}
|
})}>
|
||||||
>
|
|
||||||
<input {...props.getInputProps()} />
|
<input {...props.getInputProps()} />
|
||||||
{isDragActive && (
|
{isDragActive && (
|
||||||
<Overlay
|
<Overlay onDrop={onDragLeave} onDragLeave={onDragLeave}>
|
||||||
onDrop={onDragLeave}
|
|
||||||
onDragLeave={onDragLeave}
|
|
||||||
>
|
|
||||||
<CloseButtonWrapper onClick={onDragLeave}>
|
<CloseButtonWrapper onClick={onDragLeave}>
|
||||||
<CrossIcon />
|
<CrossIcon />
|
||||||
</CloseButtonWrapper>
|
</CloseButtonWrapper>
|
||||||
|
|
|
@ -11,11 +11,8 @@ export default function IncognitoWarning() {
|
||||||
title: constants.LOCAL_STORAGE_NOT_ACCESSIBLE,
|
title: constants.LOCAL_STORAGE_NOT_ACCESSIBLE,
|
||||||
staticBackdrop: true,
|
staticBackdrop: true,
|
||||||
nonClosable: true,
|
nonClosable: true,
|
||||||
}}
|
}}>
|
||||||
>
|
<div>{constants.LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE}</div>
|
||||||
<div>
|
|
||||||
{constants.LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE}
|
|
||||||
</div>
|
|
||||||
</MessageDialog>
|
</MessageDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ interface formValues {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
signUp: () => void
|
signUp: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Login(props: LoginProps) {
|
export default function Login(props: LoginProps) {
|
||||||
|
@ -39,7 +39,7 @@ export default function Login(props: LoginProps) {
|
||||||
|
|
||||||
const loginUser = async (
|
const loginUser = async (
|
||||||
{ email }: formValues,
|
{ email }: formValues,
|
||||||
{ setFieldError }: FormikHelpers<formValues>,
|
{ setFieldError }: FormikHelpers<formValues>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
setWaiting(true);
|
setWaiting(true);
|
||||||
|
@ -73,15 +73,8 @@ export default function Login(props: LoginProps) {
|
||||||
})}
|
})}
|
||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}
|
||||||
onSubmit={loginUser}
|
onSubmit={loginUser}>
|
||||||
>
|
{({ values, errors, touched, handleChange, handleSubmit }) => (
|
||||||
{({
|
|
||||||
values,
|
|
||||||
errors,
|
|
||||||
touched,
|
|
||||||
handleChange,
|
|
||||||
handleSubmit,
|
|
||||||
}) => (
|
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
<Form.Group controlId="formBasicEmail">
|
<Form.Group controlId="formBasicEmail">
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
@ -91,7 +84,7 @@ export default function Login(props: LoginProps) {
|
||||||
value={values.email}
|
value={values.email}
|
||||||
onChange={handleChange('email')}
|
onChange={handleChange('email')}
|
||||||
isInvalid={Boolean(
|
isInvalid={Boolean(
|
||||||
touched.email && errors.email,
|
touched.email && errors.email
|
||||||
)}
|
)}
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
@ -105,7 +98,11 @@ export default function Login(props: LoginProps) {
|
||||||
loading={waiting}
|
loading={waiting}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<Button block variant="link" className="text-center" onClick={props.signUp}>
|
<Button
|
||||||
|
block
|
||||||
|
variant="link"
|
||||||
|
className="text-center"
|
||||||
|
onClick={props.signUp}>
|
||||||
{constants.NO_ACCOUNT}
|
{constants.NO_ACCOUNT}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -7,7 +7,7 @@ export interface MessageAttributes {
|
||||||
staticBackdrop?: boolean;
|
staticBackdrop?: boolean;
|
||||||
nonClosable?: boolean;
|
nonClosable?: boolean;
|
||||||
content?: any;
|
content?: any;
|
||||||
close?: { text?: string; variant?: string, action?: () => void };
|
close?: { text?: string; variant?: string; action?: () => void };
|
||||||
proceed?: {
|
proceed?: {
|
||||||
text: string;
|
text: string;
|
||||||
action: () => void;
|
action: () => void;
|
||||||
|
@ -38,21 +38,21 @@ export default function MessageDialog({
|
||||||
{...props}
|
{...props}
|
||||||
onHide={attributes.nonClosable ? () => null : props.onHide}
|
onHide={attributes.nonClosable ? () => null : props.onHide}
|
||||||
centered
|
centered
|
||||||
backdrop={attributes.staticBackdrop ? 'static' : 'true'}
|
backdrop={attributes.staticBackdrop ? 'static' : 'true'}>
|
||||||
>
|
|
||||||
<Modal.Header
|
<Modal.Header
|
||||||
style={{ borderBottom: 'none' }}
|
style={{ borderBottom: 'none' }}
|
||||||
closeButton={!attributes.nonClosable}
|
closeButton={!attributes.nonClosable}>
|
||||||
>
|
|
||||||
{attributes.title && (
|
{attributes.title && (
|
||||||
<Modal.Title>
|
<Modal.Title>{attributes.title}</Modal.Title>
|
||||||
{attributes.title}
|
|
||||||
</Modal.Title>
|
|
||||||
)}
|
)}
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
{(children || attributes?.content) && (
|
{(children || attributes?.content) && (
|
||||||
<Modal.Body style={{ borderTop: '1px solid #444' }}>
|
<Modal.Body style={{ borderTop: '1px solid #444' }}>
|
||||||
{children || <p style={{ fontSize: '1.25rem', marginBottom: 0 }}>{attributes.content}</p>}
|
{children || (
|
||||||
|
<p style={{ fontSize: '1.25rem', marginBottom: 0 }}>
|
||||||
|
{attributes.content}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
)}
|
)}
|
||||||
{(attributes.close || attributes.proceed) && (
|
{(attributes.close || attributes.proceed) && (
|
||||||
|
@ -61,13 +61,16 @@ export default function MessageDialog({
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{attributes.close && (
|
{attributes.close && (
|
||||||
<Button
|
<Button
|
||||||
variant={`outline-${attributes.close?.variant ?? 'secondary'}`}
|
variant={`outline-${
|
||||||
|
attributes.close?.variant ?? 'secondary'
|
||||||
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
attributes.close?.action ? attributes.close?.action() : props.onHide();
|
attributes.close.action &&
|
||||||
|
attributes.close?.action();
|
||||||
|
props.onHide();
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
padding: '6px 3em',
|
padding: '6px 3em',
|
||||||
|
@ -75,14 +78,15 @@ export default function MessageDialog({
|
||||||
marginBottom: '20px',
|
marginBottom: '20px',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{attributes.close?.text ?? constants.OK}
|
{attributes.close?.text ?? constants.OK}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{attributes.proceed && (
|
{attributes.proceed && (
|
||||||
<Button
|
<Button
|
||||||
variant={`outline-${attributes.proceed?.variant ?? 'primary'}`}
|
variant={`outline-${
|
||||||
|
attributes.proceed?.variant ?? 'primary'
|
||||||
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
attributes.proceed.action();
|
attributes.proceed.action();
|
||||||
props.onHide();
|
props.onHide();
|
||||||
|
@ -94,8 +98,7 @@ export default function MessageDialog({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
disabled={attributes.proceed.disabled}
|
disabled={attributes.proceed.disabled}>
|
||||||
>
|
|
||||||
{attributes.proceed.text}
|
{attributes.proceed.text}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -15,11 +15,17 @@ const Wrapper = styled.button<{ direction: SCROLL_DIRECTION }>`
|
||||||
color: #eee;
|
color: #eee;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
${(props) => (props.direction === SCROLL_DIRECTION.LEFT ? 'margin-right: 10px;' : 'margin-left: 10px;')}
|
${(props) =>
|
||||||
${(props) => (props.direction === SCROLL_DIRECTION.LEFT ? 'left: 0;' : 'right: 0;')}
|
props.direction === SCROLL_DIRECTION.LEFT
|
||||||
|
? 'margin-right: 10px;'
|
||||||
|
: 'margin-left: 10px;'}
|
||||||
|
${(props) =>
|
||||||
|
props.direction === SCROLL_DIRECTION.LEFT ? 'left: 0;' : 'right: 0;'}
|
||||||
|
|
||||||
& > svg {
|
& > svg {
|
||||||
${(props) => props.direction === SCROLL_DIRECTION.LEFT && 'transform:rotate(180deg);'}
|
${(props) =>
|
||||||
|
props.direction === SCROLL_DIRECTION.LEFT &&
|
||||||
|
'transform:rotate(180deg);'}
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
|
@ -30,25 +36,33 @@ const Wrapper = styled.button<{ direction: SCROLL_DIRECTION }>`
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color:#fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: ' ';
|
content: ' ';
|
||||||
background: linear-gradient(to ${(props) => (props.direction === SCROLL_DIRECTION.LEFT ? 'right' : 'left')}, #191919 5%, rgba(255, 255, 255, 0) 80%);
|
background: linear-gradient(
|
||||||
|
to
|
||||||
|
${(props) =>
|
||||||
|
props.direction === SCROLL_DIRECTION.LEFT
|
||||||
|
? 'right'
|
||||||
|
: 'left'},
|
||||||
|
#191919 5%,
|
||||||
|
rgba(255, 255, 255, 0) 80%
|
||||||
|
);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
${(props) => (props.direction === SCROLL_DIRECTION.LEFT ? 'left: 40px;' : 'right: 40px;')}
|
${(props) =>
|
||||||
|
props.direction === SCROLL_DIRECTION.LEFT
|
||||||
|
? 'left: 40px;'
|
||||||
|
: 'right: 40px;'}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const NavigationButton = ({ scrollDirection, ...rest }) => (
|
const NavigationButton = ({ scrollDirection, ...rest }) => (
|
||||||
<Wrapper
|
<Wrapper direction={scrollDirection} {...rest}>
|
||||||
direction={scrollDirection}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<NavigateNext />
|
<NavigateNext />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import router from 'next/router';
|
import router from 'next/router';
|
||||||
import {
|
import {
|
||||||
DeadCenter,
|
DeadCenter,
|
||||||
FILE_TYPE,
|
|
||||||
GalleryContext,
|
GalleryContext,
|
||||||
Search,
|
Search,
|
||||||
SetFiles,
|
SetFiles,
|
||||||
|
@ -10,7 +9,7 @@ import {
|
||||||
import PreviewCard from './pages/gallery/PreviewCard';
|
import PreviewCard from './pages/gallery/PreviewCard';
|
||||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||||
import { Button } from 'react-bootstrap';
|
import { Button } from 'react-bootstrap';
|
||||||
import { File } from 'services/fileService';
|
import { File, FILE_TYPE } from 'services/fileService';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import DownloadManager from 'services/downloadManager';
|
import DownloadManager from 'services/downloadManager';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
@ -19,10 +18,14 @@ import { VariableSizeList as List } from 'react-window';
|
||||||
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
|
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
|
||||||
import { isInsideBox, isSameDay as isSameDayAnyYear } from 'utils/search';
|
import { isInsideBox, isSameDay as isSameDayAnyYear } from 'utils/search';
|
||||||
import { SetDialogMessage } from './MessageDialog';
|
import { SetDialogMessage } from './MessageDialog';
|
||||||
import { VIDEO_PLAYBACK_FAILED } from 'utils/common/errorUtil';
|
import { CustomError } from 'utils/common/errorUtil';
|
||||||
import {
|
import {
|
||||||
GAP_BTW_TILES, DATE_CONTAINER_HEIGHT, IMAGE_CONTAINER_MAX_HEIGHT,
|
GAP_BTW_TILES,
|
||||||
IMAGE_CONTAINER_MAX_WIDTH, MIN_COLUMNS, SPACE_BTW_DATES,
|
DATE_CONTAINER_HEIGHT,
|
||||||
|
IMAGE_CONTAINER_MAX_HEIGHT,
|
||||||
|
IMAGE_CONTAINER_MAX_WIDTH,
|
||||||
|
MIN_COLUMNS,
|
||||||
|
SPACE_BTW_DATES,
|
||||||
} from 'types';
|
} from 'types';
|
||||||
|
|
||||||
const NO_OF_PAGES = 2;
|
const NO_OF_PAGES = 2;
|
||||||
|
@ -68,21 +71,24 @@ const getTemplateColumns = (columns: number, groups?: number[]): string => {
|
||||||
if (sum < columns) {
|
if (sum < columns) {
|
||||||
groups[groups.length - 1] += columns - sum;
|
groups[groups.length - 1] += columns - sum;
|
||||||
}
|
}
|
||||||
return groups.map((x) => `repeat(${x}, 1fr)`).join(` ${SPACE_BTW_DATES}px `);
|
return groups
|
||||||
|
.map((x) => `repeat(${x}, 1fr)`)
|
||||||
|
.join(` ${SPACE_BTW_DATES}px `);
|
||||||
} else {
|
} else {
|
||||||
return `repeat(${columns}, 1fr)`;
|
return `repeat(${columns}, 1fr)`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const ListContainer = styled.div<{ columns: number, groups?: number[] }>`
|
const ListContainer = styled.div<{ columns: number; groups?: number[] }>`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: ${({ columns, groups }) => getTemplateColumns(columns, groups)};
|
grid-template-columns: ${({ columns, groups }) =>
|
||||||
|
getTemplateColumns(columns, groups)};
|
||||||
grid-column-gap: ${GAP_BTW_TILES}px;
|
grid-column-gap: ${GAP_BTW_TILES}px;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
||||||
@media(max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -139,7 +145,7 @@ interface Props {
|
||||||
search: Search;
|
search: Search;
|
||||||
setSearchStats: setSearchStats;
|
setSearchStats: setSearchStats;
|
||||||
deleted?: number[];
|
deleted?: number[];
|
||||||
setDialogMessage: SetDialogMessage
|
setDialogMessage: SetDialogMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PhotoFrame = ({
|
const PhotoFrame = ({
|
||||||
|
@ -303,14 +309,13 @@ const PhotoFrame = ({
|
||||||
video.preload = 'metadata';
|
video.preload = 'metadata';
|
||||||
video.src = url;
|
video.src = url;
|
||||||
video.currentTime = 3;
|
video.currentTime = 3;
|
||||||
const t = setTimeout(
|
const t = setTimeout(() => {
|
||||||
() => {
|
reject(
|
||||||
reject(
|
Error(
|
||||||
Error(`${VIDEO_PLAYBACK_FAILED} err: wait time exceeded`),
|
`${CustomError.VIDEO_PLAYBACK_FAILED} err: wait time exceeded`
|
||||||
);
|
)
|
||||||
},
|
);
|
||||||
WAIT_FOR_VIDEO_PLAYBACK,
|
}, WAIT_FOR_VIDEO_PLAYBACK);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
item.html = `
|
item.html = `
|
||||||
<video width="320" height="240" controls>
|
<video width="320" height="240" controls>
|
||||||
|
@ -332,7 +337,8 @@ const PhotoFrame = ({
|
||||||
};
|
};
|
||||||
setDialogMessage({
|
setDialogMessage({
|
||||||
title: constants.VIDEO_PLAYBACK_FAILED,
|
title: constants.VIDEO_PLAYBACK_FAILED,
|
||||||
content: constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD,
|
content:
|
||||||
|
constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD,
|
||||||
staticBackdrop: true,
|
staticBackdrop: true,
|
||||||
proceed: {
|
proceed: {
|
||||||
text: constants.DOWNLOAD,
|
text: constants.DOWNLOAD,
|
||||||
|
@ -373,7 +379,7 @@ const PhotoFrame = ({
|
||||||
if (
|
if (
|
||||||
search.date &&
|
search.date &&
|
||||||
!isSameDayAnyYear(search.date)(
|
!isSameDayAnyYear(search.date)(
|
||||||
new Date(item.metadata.creationTime / 1000),
|
new Date(item.metadata.creationTime / 1000)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -397,11 +403,10 @@ const PhotoFrame = ({
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSameDay = (first, second) => (
|
const isSameDay = (first, second) =>
|
||||||
first.getFullYear() === second.getFullYear() &&
|
first.getFullYear() === second.getFullYear() &&
|
||||||
first.getMonth() === second.getMonth() &&
|
first.getMonth() === second.getMonth() &&
|
||||||
first.getDate() === second.getDate()
|
first.getDate() === second.getDate();
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks and merge multiple dates into a single row.
|
* Checks and merge multiple dates into a single row.
|
||||||
|
@ -410,7 +415,10 @@ const PhotoFrame = ({
|
||||||
* @param columns
|
* @param columns
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const mergeTimeStampList = (items: TimeStampListItem[], columns: number): TimeStampListItem[] => {
|
const mergeTimeStampList = (
|
||||||
|
items: TimeStampListItem[],
|
||||||
|
columns: number
|
||||||
|
): TimeStampListItem[] => {
|
||||||
const newList: TimeStampListItem[] = [];
|
const newList: TimeStampListItem[] = [];
|
||||||
let index = 0;
|
let index = 0;
|
||||||
let newIndex = 0;
|
let newIndex = 0;
|
||||||
|
@ -423,12 +431,18 @@ const PhotoFrame = ({
|
||||||
// we can add more items to the same list.
|
// we can add more items to the same list.
|
||||||
if (newList[newIndex]) {
|
if (newList[newIndex]) {
|
||||||
// Check if items can be added to same list
|
// Check if items can be added to same list
|
||||||
if (newList[newIndex + 1].items.length + items[index + 1].items.length <= columns) {
|
if (
|
||||||
|
newList[newIndex + 1].items.length +
|
||||||
|
items[index + 1].items.length <=
|
||||||
|
columns
|
||||||
|
) {
|
||||||
newList[newIndex].dates.push({
|
newList[newIndex].dates.push({
|
||||||
date: currItem.date,
|
date: currItem.date,
|
||||||
span: items[index + 1].items.length,
|
span: items[index + 1].items.length,
|
||||||
});
|
});
|
||||||
newList[newIndex + 1].items = newList[newIndex + 1].items.concat(items[index + 1].items);
|
newList[newIndex + 1].items = newList[
|
||||||
|
newIndex + 1
|
||||||
|
].items.concat(items[index + 1].items);
|
||||||
index += 2;
|
index += 2;
|
||||||
} else {
|
} else {
|
||||||
// Adding items would exceed the number of columns.
|
// Adding items would exceed the number of columns.
|
||||||
|
@ -441,10 +455,12 @@ const PhotoFrame = ({
|
||||||
newList.push({
|
newList.push({
|
||||||
...currItem,
|
...currItem,
|
||||||
date: null,
|
date: null,
|
||||||
dates: [{
|
dates: [
|
||||||
date: currItem.date,
|
{
|
||||||
span: items[index + 1].items.length,
|
date: currItem.date,
|
||||||
}],
|
span: items[index + 1].items.length,
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
newList.push(items[index + 1]);
|
newList.push(items[index + 1]);
|
||||||
index += 2;
|
index += 2;
|
||||||
|
@ -474,7 +490,7 @@ const PhotoFrame = ({
|
||||||
<>
|
<>
|
||||||
{!isFirstLoad && files.length === 0 && !searchMode ? (
|
{!isFirstLoad && files.length === 0 && !searchMode ? (
|
||||||
<EmptyScreen>
|
<EmptyScreen>
|
||||||
<img height={150} src='/images/gallery.png' />
|
<img height={150} src="/images/gallery.png" />
|
||||||
<Button
|
<Button
|
||||||
variant="outline-success"
|
variant="outline-success"
|
||||||
onClick={openFileUploader}
|
onClick={openFileUploader}
|
||||||
|
@ -484,8 +500,7 @@ const PhotoFrame = ({
|
||||||
paddingRight: '32px',
|
paddingRight: '32px',
|
||||||
paddingTop: '12px',
|
paddingTop: '12px',
|
||||||
paddingBottom: '12px',
|
paddingBottom: '12px',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{constants.UPLOAD_FIRST_PHOTO}
|
{constants.UPLOAD_FIRST_PHOTO}
|
||||||
</Button>
|
</Button>
|
||||||
</EmptyScreen>
|
</EmptyScreen>
|
||||||
|
@ -493,7 +508,9 @@ const PhotoFrame = ({
|
||||||
<Container>
|
<Container>
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ height, width }) => {
|
{({ height, width }) => {
|
||||||
let columns = Math.floor(width / IMAGE_CONTAINER_MAX_WIDTH);
|
let columns = Math.floor(
|
||||||
|
width / IMAGE_CONTAINER_MAX_WIDTH
|
||||||
|
);
|
||||||
let listItemHeight = IMAGE_CONTAINER_MAX_HEIGHT;
|
let listItemHeight = IMAGE_CONTAINER_MAX_HEIGHT;
|
||||||
let skipMerge = false;
|
let skipMerge = false;
|
||||||
if (columns < MIN_COLUMNS) {
|
if (columns < MIN_COLUMNS) {
|
||||||
|
@ -506,29 +523,38 @@ const PhotoFrame = ({
|
||||||
let listItemIndex = 0;
|
let listItemIndex = 0;
|
||||||
let currentDate = -1;
|
let currentDate = -1;
|
||||||
filteredData.forEach((item, index) => {
|
filteredData.forEach((item, index) => {
|
||||||
if (!isSameDay(new Date(item.metadata.creationTime / 1000), new Date(currentDate))) {
|
if (
|
||||||
currentDate = item.metadata.creationTime / 1000;
|
!isSameDay(
|
||||||
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
|
new Date(
|
||||||
weekday: 'short',
|
item.metadata.creationTime / 1000
|
||||||
year: 'numeric',
|
),
|
||||||
month: 'short',
|
new Date(currentDate)
|
||||||
day: 'numeric',
|
)
|
||||||
});
|
) {
|
||||||
|
currentDate =
|
||||||
|
item.metadata.creationTime / 1000;
|
||||||
|
const dateTimeFormat =
|
||||||
|
new Intl.DateTimeFormat('en-IN', {
|
||||||
|
weekday: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
timeStampList.push({
|
timeStampList.push({
|
||||||
itemType: ITEM_TYPE.TIME,
|
itemType: ITEM_TYPE.TIME,
|
||||||
date: isSameDay(
|
date: isSameDay(
|
||||||
new Date(currentDate),
|
new Date(currentDate),
|
||||||
new Date(),
|
new Date()
|
||||||
) ?
|
)
|
||||||
'Today' :
|
? 'Today'
|
||||||
isSameDay(
|
: isSameDay(
|
||||||
new Date(currentDate),
|
new Date(currentDate),
|
||||||
new Date(Date.now() - A_DAY),
|
new Date(Date.now() - A_DAY)
|
||||||
) ?
|
)
|
||||||
'Yesterday' :
|
? 'Yesterday'
|
||||||
dateTimeFormat.format(
|
: dateTimeFormat.format(
|
||||||
currentDate,
|
currentDate
|
||||||
),
|
),
|
||||||
id: currentDate.toString(),
|
id: currentDate.toString(),
|
||||||
});
|
});
|
||||||
timeStampList.push({
|
timeStampList.push({
|
||||||
|
@ -553,7 +579,10 @@ const PhotoFrame = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!skipMerge) {
|
if (!skipMerge) {
|
||||||
timeStampList = mergeTimeStampList(timeStampList, columns);
|
timeStampList = mergeTimeStampList(
|
||||||
|
timeStampList,
|
||||||
|
columns
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getItemSize = (index) => {
|
const getItemSize = (index) => {
|
||||||
|
@ -567,68 +596,89 @@ const PhotoFrame = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const photoFrameHeight=(()=>{
|
const photoFrameHeight = (() => {
|
||||||
let sum=0;
|
let sum = 0;
|
||||||
for (let i=0; i<timeStampList.length; i++) {
|
for (let i = 0; i < timeStampList.length; i++) {
|
||||||
sum+=getItemSize(i);
|
sum += getItemSize(i);
|
||||||
}
|
}
|
||||||
return sum;
|
return sum;
|
||||||
})();
|
})();
|
||||||
files.length < 30 && !searchMode &&
|
files.length < 30 &&
|
||||||
|
!searchMode &&
|
||||||
timeStampList.push({
|
timeStampList.push({
|
||||||
itemType: ITEM_TYPE.BANNER,
|
itemType: ITEM_TYPE.BANNER,
|
||||||
banner: (
|
banner: (
|
||||||
<BannerContainer span={columns}>
|
<BannerContainer span={columns}>
|
||||||
<p>{constants.INSTALL_MOBILE_APP()}</p>
|
<p>
|
||||||
|
{constants.INSTALL_MOBILE_APP()}
|
||||||
|
</p>
|
||||||
</BannerContainer>
|
</BannerContainer>
|
||||||
),
|
),
|
||||||
id: 'install-banner',
|
id: 'install-banner',
|
||||||
height: Math.max(48, height-photoFrameHeight),
|
height: Math.max(
|
||||||
|
48,
|
||||||
|
height - photoFrameHeight
|
||||||
|
),
|
||||||
});
|
});
|
||||||
const extraRowsToRender = Math.ceil(
|
const extraRowsToRender = Math.ceil(
|
||||||
(NO_OF_PAGES * height) / IMAGE_CONTAINER_MAX_HEIGHT,
|
(NO_OF_PAGES * height) /
|
||||||
|
IMAGE_CONTAINER_MAX_HEIGHT
|
||||||
);
|
);
|
||||||
|
|
||||||
const generateKey = (index) => {
|
const generateKey = (index) => {
|
||||||
switch (timeStampList[index].itemType) {
|
switch (timeStampList[index].itemType) {
|
||||||
case ITEM_TYPE.TILE:
|
case ITEM_TYPE.TILE:
|
||||||
return `${timeStampList[index].items[0].id}-${timeStampList[index].items.slice(-1)[0].id}`;
|
return `${
|
||||||
|
timeStampList[index].items[0].id
|
||||||
|
}-${
|
||||||
|
timeStampList[index].items.slice(
|
||||||
|
-1
|
||||||
|
)[0].id
|
||||||
|
}`;
|
||||||
default:
|
default:
|
||||||
return `${timeStampList[index].id}-${index}`;
|
return `${timeStampList[index].id}-${index}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderListItem = (
|
||||||
const renderListItem = (listItem: TimeStampListItem) => {
|
listItem: TimeStampListItem
|
||||||
|
) => {
|
||||||
switch (listItem.itemType) {
|
switch (listItem.itemType) {
|
||||||
case ITEM_TYPE.TIME:
|
case ITEM_TYPE.TIME:
|
||||||
return listItem.dates ?
|
return listItem.dates ? (
|
||||||
listItem.dates.map((item) => (
|
listItem.dates.map((item) => (
|
||||||
<>
|
<>
|
||||||
<DateContainer key={item.date} span={item.span}>
|
<DateContainer
|
||||||
|
key={item.date}
|
||||||
|
span={item.span}>
|
||||||
{item.date}
|
{item.date}
|
||||||
</DateContainer>
|
</DateContainer>
|
||||||
<div />
|
<div />
|
||||||
</>
|
</>
|
||||||
)) :
|
))
|
||||||
(
|
) : (
|
||||||
<DateContainer span={columns}>
|
<DateContainer span={columns}>
|
||||||
{listItem.date}
|
{listItem.date}
|
||||||
</DateContainer>
|
</DateContainer>
|
||||||
);
|
);
|
||||||
case ITEM_TYPE.BANNER:
|
case ITEM_TYPE.BANNER:
|
||||||
return listItem.banner;
|
return listItem.banner;
|
||||||
default:
|
default: {
|
||||||
{
|
const ret = listItem.items.map(
|
||||||
const ret = (listItem.items.map(
|
(item, idx) =>
|
||||||
(item, idx) => getThumbnail(
|
getThumbnail(
|
||||||
filteredData,
|
filteredData,
|
||||||
listItem.itemStartIndex + idx,
|
listItem.itemStartIndex +
|
||||||
),
|
idx
|
||||||
));
|
)
|
||||||
|
);
|
||||||
if (listItem.groups) {
|
if (listItem.groups) {
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (let i = 0; i < listItem.groups.length - 1; i++) {
|
for (
|
||||||
|
let i = 0;
|
||||||
|
i < listItem.groups.length - 1;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
sum = sum + listItem.groups[i];
|
sum = sum + listItem.groups[i];
|
||||||
ret.splice(sum, 0, <div />);
|
ret.splice(sum, 0, <div />);
|
||||||
sum += 1;
|
sum += 1;
|
||||||
|
@ -648,12 +698,17 @@ const PhotoFrame = ({
|
||||||
width={width}
|
width={width}
|
||||||
itemCount={timeStampList.length}
|
itemCount={timeStampList.length}
|
||||||
itemKey={generateKey}
|
itemKey={generateKey}
|
||||||
overscanCount={extraRowsToRender}
|
overscanCount={extraRowsToRender}>
|
||||||
>
|
|
||||||
{({ index, style }) => (
|
{({ index, style }) => (
|
||||||
<ListItem style={style}>
|
<ListItem style={style}>
|
||||||
<ListContainer columns={columns} groups={timeStampList[index].groups}>
|
<ListContainer
|
||||||
{renderListItem(timeStampList[index])}
|
columns={columns}
|
||||||
|
groups={
|
||||||
|
timeStampList[index].groups
|
||||||
|
}>
|
||||||
|
{renderListItem(
|
||||||
|
timeStampList[index]
|
||||||
|
)}
|
||||||
</ListContainer>
|
</ListContainer>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
addToFavorites,
|
addToFavorites,
|
||||||
removeFromFavorites,
|
removeFromFavorites,
|
||||||
} from 'services/collectionService';
|
} from 'services/collectionService';
|
||||||
import { File } from 'services/fileService';
|
import { File, FILE_TYPE } from 'services/fileService';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import DownloadManger from 'services/downloadManager';
|
import DownloadManger from 'services/downloadManager';
|
||||||
import EXIF from 'exif-js';
|
import EXIF from 'exif-js';
|
||||||
|
@ -16,7 +16,7 @@ import Button from 'react-bootstrap/Button';
|
||||||
import Form from 'react-bootstrap/Form';
|
import Form from 'react-bootstrap/Form';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import events from './events';
|
import events from './events';
|
||||||
import { formatDateTime } from 'utils/file';
|
import { fileNameWithoutExtension, formatDateTime } from 'utils/file';
|
||||||
import { FormCheck } from 'react-bootstrap';
|
import { FormCheck } from 'react-bootstrap';
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
|
@ -49,8 +49,12 @@ const Pre = styled.pre`
|
||||||
|
|
||||||
const renderInfoItem = (label: string, value: string | JSX.Element) => (
|
const renderInfoItem = (label: string, value: string | JSX.Element) => (
|
||||||
<>
|
<>
|
||||||
<Form.Label column sm="4">{label}</Form.Label>
|
<Form.Label column sm="4">
|
||||||
<Form.Label column sm="8">{value}</Form.Label>
|
{label}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Label column sm="8">
|
||||||
|
{value}
|
||||||
|
</Form.Label>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -62,29 +66,49 @@ function ExifData(props: { exif: any }) {
|
||||||
setShowAll(e.target.checked);
|
setShowAll(e.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAllValues = () => (<Pre>{exif.raw}</Pre>);
|
const renderAllValues = () => <Pre>{exif.raw}</Pre>;
|
||||||
|
|
||||||
const renderSelectedValues = () => (<>
|
const renderSelectedValues = () => (
|
||||||
{exif?.Make && exif?.Model && renderInfoItem(constants.DEVICE, `${exif.Make} ${exif.Model}`)}
|
<>
|
||||||
{exif?.ImageWidth && exif?.ImageHeight && renderInfoItem(constants.IMAGE_SIZE, `${exif.ImageWidth} x ${exif.ImageHeight}`)}
|
{exif?.Make &&
|
||||||
{exif?.Flash && renderInfoItem(constants.FLASH, exif.Flash)}
|
exif?.Model &&
|
||||||
{exif?.FocalLength && renderInfoItem(constants.FOCAL_LENGTH, exif.FocalLength.toString())}
|
renderInfoItem(constants.DEVICE, `${exif.Make} ${exif.Model}`)}
|
||||||
{exif?.ApertureValue && renderInfoItem(constants.APERTURE, exif.ApertureValue.toString())}
|
{exif?.ImageWidth &&
|
||||||
{exif?.ISOSpeedRatings && renderInfoItem(constants.ISO, exif.ISOSpeedRatings.toString())}
|
exif?.ImageHeight &&
|
||||||
</>);
|
renderInfoItem(
|
||||||
|
constants.IMAGE_SIZE,
|
||||||
|
`${exif.ImageWidth} x ${exif.ImageHeight}`
|
||||||
|
)}
|
||||||
|
{exif?.Flash && renderInfoItem(constants.FLASH, exif.Flash)}
|
||||||
|
{exif?.FocalLength &&
|
||||||
|
renderInfoItem(
|
||||||
|
constants.FOCAL_LENGTH,
|
||||||
|
exif.FocalLength.toString()
|
||||||
|
)}
|
||||||
|
{exif?.ApertureValue &&
|
||||||
|
renderInfoItem(
|
||||||
|
constants.APERTURE,
|
||||||
|
exif.ApertureValue.toString()
|
||||||
|
)}
|
||||||
|
{exif?.ISOSpeedRatings &&
|
||||||
|
renderInfoItem(constants.ISO, exif.ISOSpeedRatings.toString())}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (<>
|
return (
|
||||||
<LegendContainer>
|
<>
|
||||||
<Legend>{constants.EXIF}</Legend>
|
<LegendContainer>
|
||||||
<FormCheck>
|
<Legend>{constants.EXIF}</Legend>
|
||||||
<FormCheck.Label>
|
<FormCheck>
|
||||||
<FormCheck.Input onChange={changeHandler} />
|
<FormCheck.Label>
|
||||||
{constants.SHOW_ALL}
|
<FormCheck.Input onChange={changeHandler} />
|
||||||
</FormCheck.Label>
|
{constants.SHOW_ALL}
|
||||||
</FormCheck>
|
</FormCheck.Label>
|
||||||
</LegendContainer>
|
</FormCheck>
|
||||||
{showAll ? renderAllValues() : renderSelectedValues()}
|
</LegendContainer>
|
||||||
</>);
|
{showAll ? renderAllValues() : renderSelectedValues()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PhotoSwipe(props: Iprops) {
|
function PhotoSwipe(props: Iprops) {
|
||||||
|
@ -140,8 +164,14 @@ function PhotoSwipe(props: Iprops) {
|
||||||
const ele = document.getElementById(`thumb-${file.id}`);
|
const ele = document.getElementById(`thumb-${file.id}`);
|
||||||
if (ele) {
|
if (ele) {
|
||||||
const rect = ele.getBoundingClientRect();
|
const rect = ele.getBoundingClientRect();
|
||||||
const pageYScroll = window.pageYOffset || document.documentElement.scrollTop;
|
const pageYScroll =
|
||||||
return { x: rect.left, y: rect.top + pageYScroll, w: rect.width };
|
window.pageYOffset ||
|
||||||
|
document.documentElement.scrollTop;
|
||||||
|
return {
|
||||||
|
x: rect.left,
|
||||||
|
y: rect.top + pageYScroll,
|
||||||
|
w: rect.width,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -153,7 +183,7 @@ function PhotoSwipe(props: Iprops) {
|
||||||
pswpElement.current,
|
pswpElement.current,
|
||||||
PhotoswipeUIDefault,
|
PhotoswipeUIDefault,
|
||||||
items,
|
items,
|
||||||
options,
|
options
|
||||||
);
|
);
|
||||||
events.forEach((event) => {
|
events.forEach((event) => {
|
||||||
const callback = props[event];
|
const callback = props[event];
|
||||||
|
@ -201,7 +231,8 @@ function PhotoSwipe(props: Iprops) {
|
||||||
const { favItemIds } = props;
|
const { favItemIds } = props;
|
||||||
if (favItemIds && file) {
|
if (favItemIds && file) {
|
||||||
return favItemIds.has(file.id);
|
return favItemIds.has(file.id);
|
||||||
} return false;
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFavClick = async (file) => {
|
const onFavClick = async (file) => {
|
||||||
|
@ -232,7 +263,9 @@ function PhotoSwipe(props: Iprops) {
|
||||||
const checkExifAvailable = () => {
|
const checkExifAvailable = () => {
|
||||||
setExif(null);
|
setExif(null);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const img = document.querySelector('.pswp__img:not(.pswp__img--placeholder)');
|
const img = document.querySelector(
|
||||||
|
'.pswp__img:not(.pswp__img--placeholder)'
|
||||||
|
);
|
||||||
if (img) {
|
if (img) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
EXIF.getData(img, function () {
|
EXIF.getData(img, function () {
|
||||||
|
@ -269,7 +302,11 @@ function PhotoSwipe(props: Iprops) {
|
||||||
loadingBar.current.continuousStart();
|
loadingBar.current.continuousStart();
|
||||||
a.href = await DownloadManger.getFile(file);
|
a.href = await DownloadManger.getFile(file);
|
||||||
loadingBar.current.complete();
|
loadingBar.current.complete();
|
||||||
a.download = file.metadata.title;
|
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||||
|
a.download = fileNameWithoutExtension(file.metadata.title) + '.zip';
|
||||||
|
} else {
|
||||||
|
a.download = file.metadata.title;
|
||||||
|
}
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
a.remove();
|
a.remove();
|
||||||
|
@ -285,8 +322,7 @@ function PhotoSwipe(props: Iprops) {
|
||||||
tabIndex={Number('-1')}
|
tabIndex={Number('-1')}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
ref={pswpElement}
|
ref={pswpElement}>
|
||||||
>
|
|
||||||
<div className="pswp__bg" />
|
<div className="pswp__bg" />
|
||||||
<div className="pswp__scroll-wrap">
|
<div className="pswp__scroll-wrap">
|
||||||
<div className="pswp__container">
|
<div className="pswp__container">
|
||||||
|
@ -306,7 +342,9 @@ function PhotoSwipe(props: Iprops) {
|
||||||
<button
|
<button
|
||||||
className="pswp-custom download-btn"
|
className="pswp-custom download-btn"
|
||||||
title={constants.DOWNLOAD}
|
title={constants.DOWNLOAD}
|
||||||
onClick={() => downloadFile(photoSwipe.currItem)}
|
onClick={() =>
|
||||||
|
downloadFile(photoSwipe.currItem)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -363,26 +401,46 @@ function PhotoSwipe(props: Iprops) {
|
||||||
<div>
|
<div>
|
||||||
<Legend>{constants.METADATA}</Legend>
|
<Legend>{constants.METADATA}</Legend>
|
||||||
</div>
|
</div>
|
||||||
{renderInfoItem(constants.FILE_ID, items[photoSwipe?.getCurrentIndex()]?.id)}
|
{renderInfoItem(
|
||||||
{metadata?.title && renderInfoItem(constants.FILE_NAME, metadata.title)}
|
constants.FILE_ID,
|
||||||
{metadata?.creationTime && renderInfoItem(constants.CREATION_TIME, formatDateTime(metadata.creationTime / 1000))}
|
items[photoSwipe?.getCurrentIndex()]?.id
|
||||||
{metadata?.modificationTime && renderInfoItem(constants.UPDATED_ON, formatDateTime(metadata.modificationTime / 1000))}
|
)}
|
||||||
{metadata?.longitude && metadata?.longitude && renderInfoItem(constants.LOCATION, (
|
{metadata?.title &&
|
||||||
<a href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
|
renderInfoItem(constants.FILE_NAME, metadata.title)}
|
||||||
target='_blank' rel='noopener noreferrer'>
|
{metadata?.creationTime &&
|
||||||
{constants.SHOW_MAP}
|
renderInfoItem(
|
||||||
</a>
|
constants.CREATION_TIME,
|
||||||
))}
|
formatDateTime(metadata.creationTime / 1000)
|
||||||
|
)}
|
||||||
|
{metadata?.modificationTime &&
|
||||||
|
renderInfoItem(
|
||||||
|
constants.UPDATED_ON,
|
||||||
|
formatDateTime(metadata.modificationTime / 1000)
|
||||||
|
)}
|
||||||
|
{metadata?.longitude &&
|
||||||
|
metadata?.longitude &&
|
||||||
|
renderInfoItem(
|
||||||
|
constants.LOCATION,
|
||||||
|
<a
|
||||||
|
href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
{constants.SHOW_MAP}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
{exif && (
|
{exif && (
|
||||||
<>
|
<>
|
||||||
<br /><br />
|
<br />
|
||||||
|
<br />
|
||||||
<ExifData exif={exif} />
|
<ExifData exif={exif} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button variant="outline-secondary" onClick={handleCloseInfo}>
|
<Button
|
||||||
|
variant="outline-secondary"
|
||||||
|
onClick={handleCloseInfo}>
|
||||||
{constants.CLOSE}
|
{constants.CLOSE}
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
|
|
|
@ -12,11 +12,11 @@ export const CodeBlock = styled.div<{ height: number }>`
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #1a1919;
|
background: #1a1919;
|
||||||
height: ${(props) => props.height}px;
|
height: ${(props) => props.height}px;
|
||||||
padding-left:30px;
|
padding-left: 30px;
|
||||||
padding-right:20px;
|
padding-right: 20px;
|
||||||
color: white;
|
color: white;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
width:100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FreeFlowText = styled.div`
|
export const FreeFlowText = styled.div`
|
||||||
|
@ -71,20 +71,17 @@ function RecoveryKeyModal({ somethingWentWrong, ...props }: Props) {
|
||||||
disabled: !recoveryKey,
|
disabled: !recoveryKey,
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
},
|
},
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<p>{constants.RECOVERY_KEY_DESCRIPTION}</p>
|
<p>{constants.RECOVERY_KEY_DESCRIPTION}</p>
|
||||||
<CodeBlock height={150}>
|
<CodeBlock height={150}>
|
||||||
{recoveryKey ? (
|
{recoveryKey ? (
|
||||||
<FreeFlowText>
|
<FreeFlowText>{recoveryKey}</FreeFlowText>
|
||||||
{recoveryKey}
|
|
||||||
</FreeFlowText>
|
|
||||||
) : (
|
) : (
|
||||||
<EnteSpinner />
|
<EnteSpinner />
|
||||||
)}
|
)}
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
<p>{constants.KEY_NOT_STORED_DISCLAIMER}</p>
|
<p>{constants.KEY_NOT_STORED_DISCLAIMER}</p>
|
||||||
</MessageDialog >
|
</MessageDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default RecoveryKeyModal;
|
export default RecoveryKeyModal;
|
||||||
|
|
|
@ -97,7 +97,9 @@ export default function SearchBar(props: Props) {
|
||||||
}, [props.isOpen]);
|
}, [props.isOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('resize', () => setWindowWidth(window.innerWidth));
|
window.addEventListener('resize', () =>
|
||||||
|
setWindowWidth(window.innerWidth)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
// = =========================
|
// = =========================
|
||||||
// Functionality
|
// Functionality
|
||||||
|
@ -119,18 +121,19 @@ export default function SearchBar(props: Props) {
|
||||||
type: SuggestionType.DATE,
|
type: SuggestionType.DATE,
|
||||||
value: searchedDate,
|
value: searchedDate,
|
||||||
label: getFormattedDate(searchedDate),
|
label: getFormattedDate(searchedDate),
|
||||||
})),
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchResults = await searchLocation(searchPhrase);
|
const searchResults = await searchLocation(searchPhrase);
|
||||||
option.push(
|
option.push(
|
||||||
...searchResults.map(
|
...searchResults.map(
|
||||||
(searchResult) => ({
|
(searchResult) =>
|
||||||
type: SuggestionType.LOCATION,
|
({
|
||||||
value: searchResult.bbox,
|
type: SuggestionType.LOCATION,
|
||||||
label: searchResult.place,
|
value: searchResult.bbox,
|
||||||
} as Suggestion),
|
label: searchResult.place,
|
||||||
),
|
} as Suggestion)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
return option;
|
return option;
|
||||||
};
|
};
|
||||||
|
@ -174,7 +177,8 @@ export default function SearchBar(props: Props) {
|
||||||
// UI
|
// UI
|
||||||
// = =========================
|
// = =========================
|
||||||
|
|
||||||
const getIconByType = (type: SuggestionType) => (type === SuggestionType.DATE ? <DateIcon /> : <LocationIcon />);
|
const getIconByType = (type: SuggestionType) =>
|
||||||
|
type === SuggestionType.DATE ? <DateIcon /> : <LocationIcon />;
|
||||||
|
|
||||||
const LabelWithIcon = (props: { type: SuggestionType; label: string }) => (
|
const LabelWithIcon = (props: { type: SuggestionType; label: string }) => (
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
@ -198,8 +202,7 @@ export default function SearchBar(props: Props) {
|
||||||
style={{
|
style={{
|
||||||
paddingLeft: '10px',
|
paddingLeft: '10px',
|
||||||
paddingBottom: '4px',
|
paddingBottom: '4px',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{props.getValue().length === 0 || props.menuIsOpen ? (
|
{props.getValue().length === 0 || props.menuIsOpen ? (
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
) : props.getValue()[0].type === SuggestionType.DATE ? (
|
) : props.getValue()[0].type === SuggestionType.DATE ? (
|
||||||
|
@ -215,13 +218,13 @@ export default function SearchBar(props: Props) {
|
||||||
const customStyles = {
|
const customStyles = {
|
||||||
control: (style, { isFocused }) => ({
|
control: (style, { isFocused }) => ({
|
||||||
...style,
|
...style,
|
||||||
'backgroundColor': '#282828',
|
backgroundColor: '#282828',
|
||||||
'color': '#d1d1d1',
|
color: '#d1d1d1',
|
||||||
'borderColor': isFocused ? '#2dc262' : '#444',
|
borderColor: isFocused ? '#2dc262' : '#444',
|
||||||
'boxShadow': 'none',
|
boxShadow: 'none',
|
||||||
':hover': {
|
':hover': {
|
||||||
'borderColor': '#2dc262',
|
borderColor: '#2dc262',
|
||||||
'cursor': 'text',
|
cursor: 'text',
|
||||||
'&>.icon': { color: '#2dc262' },
|
'&>.icon': { color: '#2dc262' },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
@ -276,8 +279,7 @@ export default function SearchBar(props: Props) {
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
margin: '10px',
|
margin: '10px',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<AsyncSelect
|
<AsyncSelect
|
||||||
components={{
|
components={{
|
||||||
Option: OptionWithIcon,
|
Option: OptionWithIcon,
|
||||||
|
@ -297,8 +299,7 @@ export default function SearchBar(props: Props) {
|
||||||
{props.isOpen && (
|
{props.isOpen && (
|
||||||
<div
|
<div
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={resetSearch}
|
onClick={resetSearch}>
|
||||||
>
|
|
||||||
<CrossIcon />
|
<CrossIcon />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -307,8 +308,7 @@ export default function SearchBar(props: Props) {
|
||||||
) : (
|
) : (
|
||||||
<SearchButton
|
<SearchButton
|
||||||
isDisabled={props.isFirstFetch}
|
isDisabled={props.isFirstFetch}
|
||||||
onClick={() => !props.isFirstFetch && props.setOpen(true)}
|
onClick={() => !props.isFirstFetch && props.setOpen(true)}>
|
||||||
>
|
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</SearchButton>
|
</SearchButton>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -21,7 +21,7 @@ function SetPasswordForm(props: Props) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const onSubmit = async (
|
const onSubmit = async (
|
||||||
values: formValues,
|
values: formValues,
|
||||||
{ setFieldError }: FormikHelpers<formValues>,
|
{ setFieldError }: FormikHelpers<formValues>
|
||||||
) => {
|
) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
@ -34,7 +34,7 @@ function SetPasswordForm(props: Props) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setFieldError(
|
setFieldError(
|
||||||
'passphrase',
|
'passphrase',
|
||||||
`${constants.UNKNOWN_ERROR} ${e.message}`,
|
`${constants.UNKNOWN_ERROR} ${e.message}`
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -46,8 +46,7 @@ function SetPasswordForm(props: Props) {
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
<div
|
<div
|
||||||
className="text-center"
|
className="text-center"
|
||||||
style={{ marginBottom: '40px' }}
|
style={{ marginBottom: '40px' }}>
|
||||||
>
|
|
||||||
<p>{constants.ENTER_ENC_PASSPHRASE}</p>
|
<p>{constants.ENTER_ENC_PASSPHRASE}</p>
|
||||||
{constants.PASSPHRASE_DISCLAIMER()}
|
{constants.PASSPHRASE_DISCLAIMER()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,14 +54,13 @@ function SetPasswordForm(props: Props) {
|
||||||
initialValues={{ passphrase: '', confirm: '' }}
|
initialValues={{ passphrase: '', confirm: '' }}
|
||||||
validationSchema={Yup.object().shape({
|
validationSchema={Yup.object().shape({
|
||||||
passphrase: Yup.string().required(
|
passphrase: Yup.string().required(
|
||||||
constants.REQUIRED,
|
constants.REQUIRED
|
||||||
),
|
),
|
||||||
confirm: Yup.string().required(constants.REQUIRED),
|
confirm: Yup.string().required(constants.REQUIRED),
|
||||||
})}
|
})}
|
||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}>
|
||||||
>
|
|
||||||
{({
|
{({
|
||||||
values,
|
values,
|
||||||
touched,
|
touched,
|
||||||
|
@ -79,7 +77,7 @@ function SetPasswordForm(props: Props) {
|
||||||
onChange={handleChange('passphrase')}
|
onChange={handleChange('passphrase')}
|
||||||
isInvalid={Boolean(
|
isInvalid={Boolean(
|
||||||
touched.passphrase &&
|
touched.passphrase &&
|
||||||
errors.passphrase,
|
errors.passphrase
|
||||||
)}
|
)}
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
@ -97,7 +95,7 @@ function SetPasswordForm(props: Props) {
|
||||||
value={values.confirm}
|
value={values.confirm}
|
||||||
onChange={handleChange('confirm')}
|
onChange={handleChange('confirm')}
|
||||||
isInvalid={Boolean(
|
isInvalid={Boolean(
|
||||||
touched.confirm && errors.confirm,
|
touched.confirm && errors.confirm
|
||||||
)}
|
)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
|
@ -115,8 +113,7 @@ function SetPasswordForm(props: Props) {
|
||||||
{props.back && (
|
{props.back && (
|
||||||
<div
|
<div
|
||||||
className="text-center"
|
className="text-center"
|
||||||
style={{ marginTop: '20px' }}
|
style={{ marginTop: '20px' }}>
|
||||||
>
|
|
||||||
<Button variant="link" onClick={props.back}>
|
<Button variant="link" onClick={props.back}>
|
||||||
{constants.GO_BACK}
|
{constants.GO_BACK}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { slide as Menu } from 'react-burger-menu';
|
import { slide as Menu } from 'react-burger-menu';
|
||||||
import billingService, { Subscription } from 'services/billingService';
|
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||||
import { getToken } from 'utils/common/key';
|
import { getToken } from 'utils/common/key';
|
||||||
|
@ -14,6 +13,7 @@ import {
|
||||||
isOnFreePlan,
|
isOnFreePlan,
|
||||||
isSubscriptionCancelled,
|
isSubscriptionCancelled,
|
||||||
isSubscribed,
|
isSubscribed,
|
||||||
|
convertToHumanReadable,
|
||||||
} from 'utils/billingUtil';
|
} from 'utils/billingUtil';
|
||||||
|
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
|
@ -21,7 +21,7 @@ import { Collection } from 'services/collectionService';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import LinkButton from './pages/gallery/LinkButton';
|
import LinkButton from './pages/gallery/LinkButton';
|
||||||
import { downloadApp } from 'utils/common';
|
import { downloadApp } from 'utils/common';
|
||||||
import { logoutUser } from 'services/userService';
|
import { getUserDetails, logoutUser } from 'services/userService';
|
||||||
import { LogoImage } from 'pages/_app';
|
import { LogoImage } from 'pages/_app';
|
||||||
import { SetDialogMessage } from './MessageDialog';
|
import { SetDialogMessage } from './MessageDialog';
|
||||||
import EnteSpinner from './EnteSpinner';
|
import EnteSpinner from './EnteSpinner';
|
||||||
|
@ -31,11 +31,12 @@ import ExportModal from './ExportModal';
|
||||||
import { SetLoading } from 'pages/gallery';
|
import { SetLoading } from 'pages/gallery';
|
||||||
import InProgressIcon from './icons/InProgressIcon';
|
import InProgressIcon from './icons/InProgressIcon';
|
||||||
import exportService from 'services/exportService';
|
import exportService from 'services/exportService';
|
||||||
|
import { Subscription } from 'services/billingService';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collections: Collection[];
|
collections: Collection[];
|
||||||
setDialogMessage: SetDialogMessage;
|
setDialogMessage: SetDialogMessage;
|
||||||
setLoading: SetLoading,
|
setLoading: SetLoading;
|
||||||
showPlanSelectorModal: () => void;
|
showPlanSelectorModal: () => void;
|
||||||
}
|
}
|
||||||
export default function Sidebar(props: Props) {
|
export default function Sidebar(props: Props) {
|
||||||
|
@ -55,16 +56,23 @@ export default function Sidebar(props: Props) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const usage = await billingService.getUsage();
|
const userDetails = await getUserDetails();
|
||||||
|
setUser({ ...user, email: userDetails.email });
|
||||||
SetUsage(usage);
|
SetUsage(convertToHumanReadable(userDetails.usage));
|
||||||
setSubscription(getUserSubscription());
|
setSubscription(userDetails.subscription);
|
||||||
|
setData(LS_KEYS.USER, {
|
||||||
|
...getData(LS_KEYS.USER),
|
||||||
|
email: userDetails.email,
|
||||||
|
});
|
||||||
|
setData(LS_KEYS.SUBSCRIPTION, userDetails.subscription);
|
||||||
};
|
};
|
||||||
main();
|
main();
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
function openFeedbackURL() {
|
function openFeedbackURL() {
|
||||||
const feedbackURL: string = `${getEndpoint()}/users/feedback?token=${encodeURIComponent(getToken())}`;
|
const feedbackURL: string = `${getEndpoint()}/users/feedback?token=${encodeURIComponent(
|
||||||
|
getToken()
|
||||||
|
)}`;
|
||||||
const win = window.open(feedbackURL, '_blank');
|
const win = window.open(feedbackURL, '_blank');
|
||||||
win.focus();
|
win.focus();
|
||||||
}
|
}
|
||||||
|
@ -105,9 +113,13 @@ export default function Sidebar(props: Props) {
|
||||||
<Menu
|
<Menu
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onStateChange={(state) => setIsOpen(state.isOpen)}
|
onStateChange={(state) => setIsOpen(state.isOpen)}
|
||||||
itemListElement="div"
|
itemListElement="div">
|
||||||
>
|
<div
|
||||||
<div style={{ display: 'flex', outline: 'none', textAlign: 'center' }}>
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
outline: 'none',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
<LogoImage
|
<LogoImage
|
||||||
style={{ height: '24px', padding: '3px' }}
|
style={{ height: '24px', padding: '3px' }}
|
||||||
alt="logo"
|
alt="logo"
|
||||||
|
@ -119,11 +131,16 @@ export default function Sidebar(props: Props) {
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
color: 'rgb(45, 194, 98)',
|
color: 'rgb(45, 194, 98)',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{user?.email}
|
{user?.email}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, overflow: 'auto', outline: 'none', paddingTop: '0' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
outline: 'none',
|
||||||
|
paddingTop: '0',
|
||||||
|
}}>
|
||||||
<div style={{ outline: 'none' }}>
|
<div style={{ outline: 'none' }}>
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<h5 style={{ margin: '4px 0 12px 2px' }}>
|
<h5 style={{ margin: '4px 0 12px 2px' }}>
|
||||||
|
@ -134,15 +151,15 @@ export default function Sidebar(props: Props) {
|
||||||
{isSubscriptionActive(subscription) ? (
|
{isSubscriptionActive(subscription) ? (
|
||||||
isOnFreePlan(subscription) ? (
|
isOnFreePlan(subscription) ? (
|
||||||
constants.FREE_SUBSCRIPTION_INFO(
|
constants.FREE_SUBSCRIPTION_INFO(
|
||||||
subscription?.expiryTime,
|
subscription?.expiryTime
|
||||||
)
|
)
|
||||||
) : isSubscriptionCancelled(subscription) ? (
|
) : isSubscriptionCancelled(subscription) ? (
|
||||||
constants.RENEWAL_CANCELLED_SUBSCRIPTION_INFO(
|
constants.RENEWAL_CANCELLED_SUBSCRIPTION_INFO(
|
||||||
subscription?.expiryTime,
|
subscription?.expiryTime
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
constants.RENEWAL_ACTIVE_SUBSCRIPTION_INFO(
|
constants.RENEWAL_ACTIVE_SUBSCRIPTION_INFO(
|
||||||
subscription?.expiryTime,
|
subscription?.expiryTime
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
@ -152,11 +169,10 @@ export default function Sidebar(props: Props) {
|
||||||
variant="outline-success"
|
variant="outline-success"
|
||||||
block
|
block
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onManageClick}
|
onClick={onManageClick}>
|
||||||
>
|
{isSubscribed(subscription)
|
||||||
{isSubscribed(subscription) ?
|
? constants.MANAGE
|
||||||
constants.MANAGE :
|
: constants.SUBSCRIBE}
|
||||||
constants.SUBSCRIBE}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -169,7 +185,7 @@ export default function Sidebar(props: Props) {
|
||||||
{usage ? (
|
{usage ? (
|
||||||
constants.USAGE_INFO(
|
constants.USAGE_INFO(
|
||||||
usage,
|
usage,
|
||||||
Number(convertBytesToGBs(subscription?.storage)),
|
Number(convertBytesToGBs(subscription?.storage))
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
@ -194,29 +210,28 @@ export default function Sidebar(props: Props) {
|
||||||
/>
|
/>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
style={{ marginTop: '30px' }}
|
style={{ marginTop: '30px' }}
|
||||||
onClick={openFeedbackURL}
|
onClick={openFeedbackURL}>
|
||||||
>
|
|
||||||
{constants.REQUEST_FEATURE}
|
{constants.REQUEST_FEATURE}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
style={{ marginTop: '30px' }}
|
style={{ marginTop: '30px' }}
|
||||||
onClick={openSupportMail}
|
onClick={openSupportMail}>
|
||||||
>
|
|
||||||
{constants.SUPPORT}
|
{constants.SUPPORT}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
<>
|
<>
|
||||||
<RecoveryKeyModal
|
<RecoveryKeyModal
|
||||||
show={recoverModalView}
|
show={recoverModalView}
|
||||||
onHide={() => setRecoveryModalView(false)}
|
onHide={() => setRecoveryModalView(false)}
|
||||||
somethingWentWrong={() => props.setDialogMessage({
|
somethingWentWrong={() =>
|
||||||
title: constants.RECOVER_KEY_GENERATION_FAILED,
|
props.setDialogMessage({
|
||||||
close: { variant: 'danger' },
|
title: constants.RECOVER_KEY_GENERATION_FAILED,
|
||||||
})}
|
close: { variant: 'danger' },
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
style={{ marginTop: '30px' }}
|
style={{ marginTop: '30px' }}
|
||||||
onClick={() => setRecoveryModalView(true)}
|
onClick={() => setRecoveryModalView(true)}>
|
||||||
>
|
|
||||||
{constants.DOWNLOAD_RECOVERY_KEY}
|
{constants.DOWNLOAD_RECOVERY_KEY}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
</>
|
</>
|
||||||
|
@ -230,8 +245,7 @@ export default function Sidebar(props: Props) {
|
||||||
/>
|
/>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
style={{ marginTop: '30px' }}
|
style={{ marginTop: '30px' }}
|
||||||
onClick={() => setTwoFactorModalView(true)}
|
onClick={() => setTwoFactorModalView(true)}>
|
||||||
>
|
|
||||||
{constants.TWO_FACTOR}
|
{constants.TWO_FACTOR}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
</>
|
</>
|
||||||
|
@ -240,18 +254,32 @@ export default function Sidebar(props: Props) {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
|
setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
|
||||||
router.push('change-password');
|
router.push('change-password');
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{constants.CHANGE_PASSWORD}
|
{constants.CHANGE_PASSWORD}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
|
<LinkButton
|
||||||
|
style={{ marginTop: '30px' }}
|
||||||
|
onClick={() => {
|
||||||
|
setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
|
||||||
|
router.push('change-email');
|
||||||
|
}}>
|
||||||
|
{constants.UPDATE_EMAIL}
|
||||||
|
</LinkButton>
|
||||||
<>
|
<>
|
||||||
<ExportModal show={exportModalView} onHide={() => setExportModalView(false)} usage={usage} />
|
<ExportModal
|
||||||
<LinkButton style={{ marginTop: '30px' }} onClick={exportFiles}>
|
show={exportModalView}
|
||||||
|
onHide={() => setExportModalView(false)}
|
||||||
|
usage={usage}
|
||||||
|
/>
|
||||||
|
<LinkButton
|
||||||
|
style={{ marginTop: '30px' }}
|
||||||
|
onClick={exportFiles}>
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
{constants.EXPORT}<div style={{ width: '20px' }} />
|
{constants.EXPORT}
|
||||||
{exportService.isExportInProgress() &&
|
<div style={{ width: '20px' }} />
|
||||||
|
{exportService.isExportInProgress() && (
|
||||||
<InProgressIcon />
|
<InProgressIcon />
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
</>
|
</>
|
||||||
|
@ -266,18 +294,19 @@ export default function Sidebar(props: Props) {
|
||||||
<LinkButton
|
<LinkButton
|
||||||
variant="danger"
|
variant="danger"
|
||||||
style={{ marginTop: '30px' }}
|
style={{ marginTop: '30px' }}
|
||||||
onClick={() => props.setDialogMessage({
|
onClick={() =>
|
||||||
title: `${constants.CONFIRM} ${constants.LOGOUT}`,
|
props.setDialogMessage({
|
||||||
content: constants.LOGOUT_MESSAGE,
|
title: `${constants.CONFIRM} ${constants.LOGOUT}`,
|
||||||
staticBackdrop: true,
|
content: constants.LOGOUT_MESSAGE,
|
||||||
proceed: {
|
staticBackdrop: true,
|
||||||
text: constants.LOGOUT,
|
proceed: {
|
||||||
action: logoutUser,
|
text: constants.LOGOUT,
|
||||||
variant: 'danger',
|
action: logoutUser,
|
||||||
},
|
variant: 'danger',
|
||||||
close: { text: constants.CANCEL },
|
},
|
||||||
})}
|
close: { text: constants.CANCEL },
|
||||||
>
|
})
|
||||||
|
}>
|
||||||
{constants.LOGOUT}
|
{constants.LOGOUT}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
<div
|
<div
|
||||||
|
@ -287,6 +316,6 @@ export default function Sidebar(props: Props) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Menu >
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default function SignUp(props: SignUpProps) {
|
||||||
|
|
||||||
const registerUser = async (
|
const registerUser = async (
|
||||||
{ email, passphrase, confirm }: FormValues,
|
{ email, passphrase, confirm }: FormValues,
|
||||||
{ setFieldError }: FormikHelpers<FormValues>,
|
{ setFieldError }: FormikHelpers<FormValues>
|
||||||
) => {
|
) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
@ -47,12 +47,13 @@ export default function SignUp(props: SignUpProps) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (passphrase === confirm) {
|
if (passphrase === confirm) {
|
||||||
const { keyAttributes, masterKey } = await generateKeyAttributes(passphrase);
|
const { keyAttributes, masterKey } =
|
||||||
|
await generateKeyAttributes(passphrase);
|
||||||
setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
|
setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
|
||||||
await generateAndSaveIntermediateKeyAttributes(
|
await generateAndSaveIntermediateKeyAttributes(
|
||||||
passphrase,
|
passphrase,
|
||||||
keyAttributes,
|
keyAttributes,
|
||||||
masterKey,
|
masterKey
|
||||||
);
|
);
|
||||||
|
|
||||||
await setSessionKeys(masterKey);
|
await setSessionKeys(masterKey);
|
||||||
|
@ -68,113 +69,110 @@ export default function SignUp(props: SignUpProps) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (<>
|
return (
|
||||||
<Card.Title style={{ marginBottom: '32px' }}>
|
<>
|
||||||
<LogoImg src="/icon.svg" />
|
<Card.Title style={{ marginBottom: '32px' }}>
|
||||||
{constants.SIGN_UP}
|
<LogoImg src="/icon.svg" />
|
||||||
</Card.Title>
|
{constants.SIGN_UP}
|
||||||
<Formik<FormValues>
|
</Card.Title>
|
||||||
initialValues={{
|
<Formik<FormValues>
|
||||||
email: '',
|
initialValues={{
|
||||||
passphrase: '',
|
email: '',
|
||||||
confirm: '',
|
passphrase: '',
|
||||||
}}
|
confirm: '',
|
||||||
validationSchema={Yup.object().shape({
|
}}
|
||||||
email: Yup.string()
|
validationSchema={Yup.object().shape({
|
||||||
.email(constants.EMAIL_ERROR)
|
email: Yup.string()
|
||||||
.required(constants.REQUIRED),
|
.email(constants.EMAIL_ERROR)
|
||||||
passphrase: Yup.string().required(
|
.required(constants.REQUIRED),
|
||||||
constants.REQUIRED,
|
passphrase: Yup.string().required(constants.REQUIRED),
|
||||||
),
|
confirm: Yup.string().required(constants.REQUIRED),
|
||||||
confirm: Yup.string().required(constants.REQUIRED),
|
})}
|
||||||
})}
|
validateOnChange={false}
|
||||||
validateOnChange={false}
|
validateOnBlur={false}
|
||||||
validateOnBlur={false}
|
onSubmit={registerUser}>
|
||||||
onSubmit={registerUser}
|
{({
|
||||||
>
|
values,
|
||||||
{({
|
errors,
|
||||||
values,
|
touched,
|
||||||
errors,
|
handleChange,
|
||||||
touched,
|
handleSubmit,
|
||||||
handleChange,
|
}): JSX.Element => (
|
||||||
handleSubmit,
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
}): JSX.Element => (
|
<Form.Group controlId="registrationForm.email">
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form.Control
|
||||||
<Form.Group controlId="registrationForm.email">
|
type="email"
|
||||||
<Form.Control
|
placeholder={constants.ENTER_EMAIL}
|
||||||
type="email"
|
value={values.email}
|
||||||
placeholder={constants.ENTER_EMAIL}
|
onChange={handleChange('email')}
|
||||||
value={values.email}
|
isInvalid={Boolean(
|
||||||
onChange={handleChange('email')}
|
touched.email && errors.email
|
||||||
isInvalid={Boolean(
|
)}
|
||||||
touched.email && errors.email,
|
autoFocus
|
||||||
)}
|
disabled={loading}
|
||||||
autoFocus
|
/>
|
||||||
disabled={loading}
|
<FormControl.Feedback type="invalid">
|
||||||
|
{errors.email}
|
||||||
|
</FormControl.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Control
|
||||||
|
type="password"
|
||||||
|
placeholder={constants.PASSPHRASE_HINT}
|
||||||
|
value={values.passphrase}
|
||||||
|
onChange={handleChange('passphrase')}
|
||||||
|
isInvalid={Boolean(
|
||||||
|
touched.passphrase && errors.passphrase
|
||||||
|
)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{errors.passphrase}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Control
|
||||||
|
type="password"
|
||||||
|
placeholder={constants.RE_ENTER_PASSPHRASE}
|
||||||
|
value={values.confirm}
|
||||||
|
onChange={handleChange('confirm')}
|
||||||
|
isInvalid={Boolean(
|
||||||
|
touched.confirm && errors.confirm
|
||||||
|
)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{errors.confirm}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group
|
||||||
|
style={{
|
||||||
|
marginBottom: '0',
|
||||||
|
textAlign: 'left',
|
||||||
|
}}
|
||||||
|
controlId="formBasicCheckbox-1">
|
||||||
|
<Form.Check
|
||||||
|
checked={acceptTerms}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAcceptTerms(e.target.checked)
|
||||||
|
}
|
||||||
|
type="checkbox"
|
||||||
|
label={constants.TERMS_AND_CONDITIONS()}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<br />
|
||||||
|
<SubmitButton
|
||||||
|
buttonText={constants.SUBMIT}
|
||||||
|
loading={loading}
|
||||||
|
disabled={!acceptTerms}
|
||||||
/>
|
/>
|
||||||
<FormControl.Feedback type="invalid">
|
<br />
|
||||||
{errors.email}
|
<Button block variant="link" onClick={props.login}>
|
||||||
</FormControl.Feedback>
|
{constants.ACCOUNT_EXISTS}
|
||||||
</Form.Group>
|
</Button>
|
||||||
<Form.Group>
|
</Form>
|
||||||
<Form.Control
|
)}
|
||||||
type="password"
|
</Formik>
|
||||||
placeholder={constants.PASSPHRASE_HINT}
|
</>
|
||||||
value={values.passphrase}
|
);
|
||||||
onChange={handleChange('passphrase')}
|
|
||||||
isInvalid={Boolean(
|
|
||||||
touched.passphrase &&
|
|
||||||
errors.passphrase,
|
|
||||||
)}
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{errors.passphrase}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group>
|
|
||||||
<Form.Control
|
|
||||||
type="password"
|
|
||||||
placeholder={
|
|
||||||
constants.RE_ENTER_PASSPHRASE
|
|
||||||
}
|
|
||||||
value={values.confirm}
|
|
||||||
onChange={handleChange('confirm')}
|
|
||||||
isInvalid={Boolean(
|
|
||||||
touched.confirm && errors.confirm,
|
|
||||||
)}
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{errors.confirm}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group
|
|
||||||
style={{
|
|
||||||
marginBottom: '0',
|
|
||||||
textAlign: 'left',
|
|
||||||
}}
|
|
||||||
controlId="formBasicCheckbox-1"
|
|
||||||
>
|
|
||||||
<Form.Check
|
|
||||||
checked={acceptTerms}
|
|
||||||
onChange={(e) => setAcceptTerms(e.target.checked)}
|
|
||||||
type="checkbox"
|
|
||||||
label={constants.TERMS_AND_CONDITIONS()}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<br />
|
|
||||||
<SubmitButton
|
|
||||||
buttonText={constants.SUBMIT}
|
|
||||||
loading={loading}
|
|
||||||
disabled={!acceptTerms}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<Button block variant="link" onClick={props.login}>
|
|
||||||
{constants.ACCOUNT_EXISTS}
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</>);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,9 @@ import { Form } from 'react-bootstrap';
|
||||||
import { Formik, FormikHelpers } from 'formik';
|
import { Formik, FormikHelpers } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import SubmitButton from './SubmitButton';
|
import SubmitButton from './SubmitButton';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import Visibility from './icons/Visibility';
|
||||||
|
import VisibilityOff from './icons/VisibilityOff';
|
||||||
|
|
||||||
interface formValues {
|
interface formValues {
|
||||||
passphrase: string;
|
passphrase: string;
|
||||||
|
@ -15,11 +18,29 @@ interface Props {
|
||||||
buttonText: string;
|
buttonText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Group = styled.div`
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Button = styled.button`
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
width: 46px;
|
||||||
|
height: 34px;
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
right: 1px;
|
||||||
|
border-radius: 5px;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
export default function SingleInputForm(props: Props) {
|
export default function SingleInputForm(props: Props) {
|
||||||
const [loading, SetLoading] = useState(false);
|
const [loading, SetLoading] = useState(false);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
const submitForm = async (
|
const submitForm = async (
|
||||||
values: formValues,
|
values: formValues,
|
||||||
{ setFieldError }: FormikHelpers<formValues>,
|
{ setFieldError }: FormikHelpers<formValues>
|
||||||
) => {
|
) => {
|
||||||
SetLoading(true);
|
SetLoading(true);
|
||||||
await props.callback(values.passphrase, setFieldError);
|
await props.callback(values.passphrase, setFieldError);
|
||||||
|
@ -33,27 +54,39 @@ export default function SingleInputForm(props: Props) {
|
||||||
passphrase: Yup.string().required(constants.REQUIRED),
|
passphrase: Yup.string().required(constants.REQUIRED),
|
||||||
})}
|
})}
|
||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}>
|
||||||
>
|
{({ values, touched, errors, handleChange, handleSubmit }) => (
|
||||||
{({
|
|
||||||
values, touched, errors, handleChange, handleSubmit,
|
|
||||||
}) => (
|
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Control
|
<Group>
|
||||||
type={props.fieldType}
|
<Form.Control
|
||||||
placeholder={props.placeholder}
|
type={showPassword ? 'text' : props.fieldType}
|
||||||
value={values.passphrase}
|
placeholder={props.placeholder}
|
||||||
onChange={handleChange('passphrase')}
|
value={values.passphrase}
|
||||||
isInvalid={Boolean(
|
onChange={handleChange('passphrase')}
|
||||||
touched.passphrase && errors.passphrase,
|
isInvalid={Boolean(
|
||||||
|
touched.passphrase && errors.passphrase
|
||||||
|
)}
|
||||||
|
disabled={loading}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{props.fieldType === 'password' && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setShowPassword(!showPassword)
|
||||||
|
}>
|
||||||
|
{showPassword ? (
|
||||||
|
<VisibilityOff />
|
||||||
|
) : (
|
||||||
|
<Visibility />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
disabled={loading}
|
<Form.Control.Feedback type="invalid">
|
||||||
autoFocus
|
{errors.passphrase}
|
||||||
/>
|
</Form.Control.Feedback>
|
||||||
<Form.Control.Feedback type="invalid">
|
</Group>
|
||||||
{errors.passphrase}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
buttonText={props.buttonText}
|
buttonText={props.buttonText}
|
||||||
|
|
|
@ -7,22 +7,24 @@ interface Props {
|
||||||
inline?: any;
|
inline?: any;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
const SubmitButton = ({
|
const SubmitButton = ({ loading, buttonText, inline, disabled }: Props) => (
|
||||||
loading, buttonText, inline, disabled,
|
|
||||||
}: Props) => (
|
|
||||||
<Button
|
<Button
|
||||||
className="submitButton"
|
className="submitButton"
|
||||||
variant="outline-success"
|
variant="outline-success"
|
||||||
type="submit"
|
type="submit"
|
||||||
block={!inline}
|
block={!inline}
|
||||||
disabled={loading || disabled}
|
disabled={loading || disabled}
|
||||||
style={{ padding: '6px 1em' }}
|
style={{ padding: '6px 1em' }}>
|
||||||
>
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Spinner
|
<Spinner
|
||||||
as="span"
|
as="span"
|
||||||
animation="border"
|
animation="border"
|
||||||
style={{ width: '22px', height: '22px', borderWidth: '0.20em', color: '#2dc262' }}
|
style={{
|
||||||
|
width: '22px',
|
||||||
|
height: '22px',
|
||||||
|
borderWidth: '0.20em',
|
||||||
|
color: '#2dc262',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
buttonText
|
buttonText
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { DeadCenter, SetLoading } from 'pages/gallery';
|
import { DeadCenter, SetLoading } from 'pages/gallery';
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { Button, Row } from 'react-bootstrap';
|
import { Button, Row } from 'react-bootstrap';
|
||||||
import { disableTwoFactor, getTwoFactorStatus } from 'services/userService';
|
import { disableTwoFactor, getTwoFactorStatus } from 'services/userService';
|
||||||
|
@ -13,7 +13,7 @@ interface Props {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
onHide: () => void;
|
onHide: () => void;
|
||||||
setDialogMessage: SetDialogMessage;
|
setDialogMessage: SetDialogMessage;
|
||||||
setLoading: SetLoading
|
setLoading: SetLoading;
|
||||||
closeSidebar: () => void;
|
closeSidebar: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,12 +26,16 @@ function TwoFactorModal(props: Props) {
|
||||||
if (!props.show) {
|
if (!props.show) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isTwoFactorEnabled = getData(LS_KEYS.USER).isTwoFactorEnabled ?? false;
|
const isTwoFactorEnabled =
|
||||||
|
getData(LS_KEYS.USER).isTwoFactorEnabled ?? false;
|
||||||
setTwoFactorStatus(isTwoFactorEnabled);
|
setTwoFactorStatus(isTwoFactorEnabled);
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const isTwoFactorEnabled = await getTwoFactorStatus();
|
const isTwoFactorEnabled = await getTwoFactorStatus();
|
||||||
setTwoFactorStatus(isTwoFactorEnabled);
|
setTwoFactorStatus(isTwoFactorEnabled);
|
||||||
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), isTwoFactorEnabled: false });
|
setData(LS_KEYS.USER, {
|
||||||
|
...getData(LS_KEYS.USER),
|
||||||
|
isTwoFactorEnabled: false,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
main();
|
main();
|
||||||
}, [props.show]);
|
}, [props.show]);
|
||||||
|
@ -51,12 +55,21 @@ function TwoFactorModal(props: Props) {
|
||||||
const twoFactorDisable = async () => {
|
const twoFactorDisable = async () => {
|
||||||
try {
|
try {
|
||||||
await disableTwoFactor();
|
await disableTwoFactor();
|
||||||
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), isTwoFactorEnabled: false });
|
setData(LS_KEYS.USER, {
|
||||||
|
...getData(LS_KEYS.USER),
|
||||||
|
isTwoFactorEnabled: false,
|
||||||
|
});
|
||||||
props.onHide();
|
props.onHide();
|
||||||
props.closeSidebar();
|
props.closeSidebar();
|
||||||
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_DISABLE_SUCCESS, severity: 'info' });
|
appContext.setDisappearingFlashMessage({
|
||||||
|
message: constants.TWO_FACTOR_DISABLE_SUCCESS,
|
||||||
|
type: FLASH_MESSAGE_TYPE.INFO,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_DISABLE_FAILED, severity: 'danger' });
|
appContext.setDisappearingFlashMessage({
|
||||||
|
message: constants.TWO_FACTOR_DISABLE_FAILED,
|
||||||
|
type: FLASH_MESSAGE_TYPE.DANGER,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const warnTwoFactorReconfigure = async () => {
|
const warnTwoFactorReconfigure = async () => {
|
||||||
|
@ -82,38 +95,60 @@ function TwoFactorModal(props: Props) {
|
||||||
attributes={{
|
attributes={{
|
||||||
title: constants.TWO_FACTOR_AUTHENTICATION,
|
title: constants.TWO_FACTOR_AUTHENTICATION,
|
||||||
staticBackdrop: true,
|
staticBackdrop: true,
|
||||||
}}
|
}}>
|
||||||
|
<div
|
||||||
>
|
{...(!isTwoFactorEnabled
|
||||||
<div {...(!isTwoFactorEnabled ? { style: { padding: '10px 10px 30px 10px' } } : { style: { padding: '10px' } })}>
|
? { style: { padding: '10px 10px 30px 10px' } }
|
||||||
{
|
: { style: { padding: '10px' } })}>
|
||||||
isTwoFactorEnabled ?
|
{isTwoFactorEnabled ? (
|
||||||
<>
|
<>
|
||||||
<Row>
|
<Row>
|
||||||
<Label>{constants.UPDATE_TWO_FACTOR_HINT}</Label>
|
<Label>{constants.UPDATE_TWO_FACTOR_HINT}</Label>
|
||||||
<Value>
|
<Value>
|
||||||
<Button variant={'outline-success'} onClick={warnTwoFactorReconfigure}>{constants.RECONFIGURE}</Button>
|
<Button
|
||||||
</Value>
|
variant={'outline-success'}
|
||||||
</Row>
|
onClick={warnTwoFactorReconfigure}>
|
||||||
<Row>
|
{constants.RECONFIGURE}
|
||||||
<Label>{constants.DISABLE_TWO_FACTOR_HINT} </Label>
|
</Button>
|
||||||
<Value>
|
</Value>
|
||||||
<Button variant={'outline-danger'} onClick={warnTwoFactorDisable}>{constants.DISABLE}</Button>
|
</Row>
|
||||||
</Value>
|
<Row>
|
||||||
</Row>
|
<Label>{constants.DISABLE_TWO_FACTOR_HINT} </Label>
|
||||||
|
<Value>
|
||||||
</> : (
|
<Button
|
||||||
<DeadCenter>
|
variant={'outline-danger'}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="36px" viewBox="0 0 24 24" width="36px" fill="#000000"><g fill="none"><path d="M0 0h24v24H0V0z" /><path d="M0 0h24v24H0V0z" opacity=".87" /></g><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z" /></svg>
|
onClick={warnTwoFactorDisable}>
|
||||||
<p />
|
{constants.DISABLE}
|
||||||
<p>{constants.TWO_FACTOR_INFO}</p>
|
</Button>
|
||||||
<div style={{ height: '10px' }} />
|
</Value>
|
||||||
<Button variant="outline-success" onClick={() => router.push('/two-factor/setup')}>{constants.ENABLE_TWO_FACTOR}</Button>
|
</Row>
|
||||||
</DeadCenter>
|
</>
|
||||||
)
|
) : (
|
||||||
}
|
<DeadCenter>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height="36px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="36px"
|
||||||
|
fill="#000000">
|
||||||
|
<g fill="none">
|
||||||
|
<path d="M0 0h24v24H0V0z" />
|
||||||
|
<path d="M0 0h24v24H0V0z" opacity=".87" />
|
||||||
|
</g>
|
||||||
|
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z" />
|
||||||
|
</svg>
|
||||||
|
<p />
|
||||||
|
<p>{constants.TWO_FACTOR_INFO}</p>
|
||||||
|
<div style={{ height: '10px' }} />
|
||||||
|
<Button
|
||||||
|
variant="outline-success"
|
||||||
|
onClick={() => router.push('/two-factor/setup')}>
|
||||||
|
{constants.ENABLE_TWO_FACTOR}
|
||||||
|
</Button>
|
||||||
|
</DeadCenter>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</MessageDialog >
|
</MessageDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default TwoFactorModal;
|
export default TwoFactorModal;
|
||||||
|
|
|
@ -11,8 +11,8 @@ interface formValues {
|
||||||
otp: string;
|
otp: string;
|
||||||
}
|
}
|
||||||
interface Props {
|
interface Props {
|
||||||
onSubmit: any
|
onSubmit: any;
|
||||||
back: any
|
back: any;
|
||||||
buttonText: string;
|
buttonText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export default function VerifyTwoFactor(props: Props) {
|
||||||
const otpInputRef = useRef(null);
|
const otpInputRef = useRef(null);
|
||||||
const submitForm = async (
|
const submitForm = async (
|
||||||
{ otp }: formValues,
|
{ otp }: formValues,
|
||||||
{ setFieldError, resetForm }: FormikHelpers<formValues>,
|
{ setFieldError, resetForm }: FormikHelpers<formValues>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
setWaiting(true);
|
setWaiting(true);
|
||||||
|
@ -36,7 +36,11 @@ export default function VerifyTwoFactor(props: Props) {
|
||||||
setWaiting(false);
|
setWaiting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChange = (otp: string, callback: Function, triggerSubmit: Function) => {
|
const onChange = (
|
||||||
|
otp: string,
|
||||||
|
callback: Function,
|
||||||
|
triggerSubmit: Function
|
||||||
|
) => {
|
||||||
callback(otp);
|
callback(otp);
|
||||||
if (otp.length === 6) {
|
if (otp.length === 6) {
|
||||||
triggerSubmit(otp);
|
triggerSubmit(otp);
|
||||||
|
@ -44,13 +48,14 @@ export default function VerifyTwoFactor(props: Props) {
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p style={{ marginBottom: '30px' }}>enter the 6-digit code from your authenticator app.</p>
|
<p style={{ marginBottom: '30px' }}>
|
||||||
|
enter the 6-digit code from your authenticator app.
|
||||||
|
</p>
|
||||||
<Formik<formValues>
|
<Formik<formValues>
|
||||||
initialValues={{ otp: '' }}
|
initialValues={{ otp: '' }}
|
||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}
|
||||||
onSubmit={submitForm}
|
onSubmit={submitForm}>
|
||||||
>
|
|
||||||
{({
|
{({
|
||||||
values,
|
values,
|
||||||
errors,
|
errors,
|
||||||
|
@ -58,8 +63,13 @@ export default function VerifyTwoFactor(props: Props) {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
submitForm,
|
submitForm,
|
||||||
}) => (
|
}) => (
|
||||||
<Form noValidate onSubmit={handleSubmit} style={{ width: '100%' }}>
|
<Form
|
||||||
<Form.Group style={{ marginBottom: '32px' }} controlId="formBasicEmail">
|
noValidate
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
style={{ width: '100%' }}>
|
||||||
|
<Form.Group
|
||||||
|
style={{ marginBottom: '32px' }}
|
||||||
|
controlId="formBasicEmail">
|
||||||
<DeadCenter>
|
<DeadCenter>
|
||||||
<OtpInput
|
<OtpInput
|
||||||
placeholder="123456"
|
placeholder="123456"
|
||||||
|
@ -67,16 +77,27 @@ export default function VerifyTwoFactor(props: Props) {
|
||||||
shouldAutoFocus
|
shouldAutoFocus
|
||||||
value={values.otp}
|
value={values.otp}
|
||||||
onChange={(otp) => {
|
onChange={(otp) => {
|
||||||
onChange(otp, handleChange('otp'), submitForm);
|
onChange(
|
||||||
|
otp,
|
||||||
|
handleChange('otp'),
|
||||||
|
submitForm
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
numInputs={6}
|
numInputs={6}
|
||||||
separator={'-'}
|
separator={'-'}
|
||||||
isInputNum
|
isInputNum
|
||||||
className={'otp-input'}
|
className={'otp-input'}
|
||||||
/>
|
/>
|
||||||
{errors.otp &&
|
{errors.otp && (
|
||||||
<div style={{ display: 'block', marginTop: '16px' }} className="invalid-feedback">{constants.INCORRECT_CODE}</div>
|
<div
|
||||||
}
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
marginTop: '16px',
|
||||||
|
}}
|
||||||
|
className="invalid-feedback">
|
||||||
|
{constants.INCORRECT_CODE}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DeadCenter>
|
</DeadCenter>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
|
@ -87,10 +108,6 @@ export default function VerifyTwoFactor(props: Props) {
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
|
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,15 @@ export default function AddIcon(props) {
|
||||||
height={props.height}
|
height={props.height}
|
||||||
viewBox={props.viewBox}
|
viewBox={props.viewBox}
|
||||||
width={props.width}
|
width={props.width}
|
||||||
fill='currentColor'
|
fill="currentColor">
|
||||||
>
|
<g>
|
||||||
<g><rect fill="none" height="24" width="24"/></g><g><g><path d="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6V13z"/></g></g>
|
<rect fill="none" height="24" width="24" />
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6V13z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,7 @@ export default function ArrowEast(props) {
|
||||||
height={props.height}
|
height={props.height}
|
||||||
viewBox={props.viewBox}
|
viewBox={props.viewBox}
|
||||||
width={props.width}
|
width={props.width}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
|
||||||
<rect fill="none" height="24" width="24" />
|
<rect fill="none" height="24" width="24" />
|
||||||
<path d="M15,5l-1.41,1.41L18.17,11H2V13h16.17l-4.59,4.59L15,19l7-7L15,5z" />
|
<path d="M15,5l-1.41,1.41L18.17,11H2V13h16.17l-4.59,4.59L15,19l7-7L15,5z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -7,8 +7,7 @@ export default function CloudUpload(props) {
|
||||||
height={props.height}
|
height={props.height}
|
||||||
viewBox={props.viewBox}
|
viewBox={props.viewBox}
|
||||||
width={props.width}
|
width={props.width}
|
||||||
fill="currentColor"
|
fill="currentColor">
|
||||||
>
|
|
||||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||||
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4 0-2.05 1.53-3.76 3.56-3.97l1.07-.11.5-.95C8.08 7.14 9.94 6 12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5 1.53.11c1.56.1 2.78 1.41 2.78 2.96 0 1.65-1.35 3-3 3zM8 13h2.55v3h2.9v-3H16l-4-4z" />
|
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4 0-2.05 1.53-3.76 3.56-3.97l1.07-.11.5-.95C8.08 7.14 9.94 6 12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5 1.53.11c1.56.1 2.78 1.41 2.78 2.96 0 1.65-1.35 3-3 3zM8 13h2.55v3h2.9v-3H16l-4-4z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -6,8 +6,7 @@ export default function DateIcon(props) {
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
height={props.height}
|
height={props.height}
|
||||||
viewBox={props.viewBox}
|
viewBox={props.viewBox}
|
||||||
width={props.width}
|
width={props.width}>
|
||||||
>
|
|
||||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,8 +6,7 @@ export default function DateIcon(props) {
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
height={props.height}
|
height={props.height}
|
||||||
viewBox={props.viewBox}
|
viewBox={props.viewBox}
|
||||||
width={props.width}
|
width={props.width}>
|
||||||
>
|
|
||||||
<path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zm-4.5-7a2.5 2.5 0 0 0 0 5 2.5 2.5 0 0 0 0-5z" />
|
<path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zm-4.5-7a2.5 2.5 0 0 0 0 5 2.5 2.5 0 0 0 0-5z" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,8 +7,7 @@ export default function DeleteIcon(props) {
|
||||||
height={props.height}
|
height={props.height}
|
||||||
viewBox={props.viewBox}
|
viewBox={props.viewBox}
|
||||||
width={props.width}
|
width={props.width}
|
||||||
fill='currentColor'
|
fill="currentColor">
|
||||||
>
|
|
||||||
<path d="M0 0h24v24H0z" fill="none" />
|
<path d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
|
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
23
src/components/icons/ExpandLess.tsx
Normal file
23
src/components/icons/ExpandLess.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react';
|
||||||
|
export default function ExpandLess(props) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={props.height}
|
||||||
|
viewBox={props.viewBox}
|
||||||
|
width={props.width}
|
||||||
|
fill="#000000">
|
||||||
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||||
|
<path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14l-6-6z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpandLess.defaultProps = {
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
open: false,
|
||||||
|
};
|
23
src/components/icons/ExpandMore.tsx
Normal file
23
src/components/icons/ExpandMore.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react';
|
||||||
|
export default function ExpandMore(props) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={props.height}
|
||||||
|
viewBox={props.viewBox}
|
||||||
|
width={props.width}
|
||||||
|
fill="#000000">
|
||||||
|
<path d="M24 24H0V0h24v24z" fill="none" opacity=".87" />
|
||||||
|
<path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpandMore.defaultProps = {
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
open: false,
|
||||||
|
};
|
|
@ -1,9 +1,7 @@
|
||||||
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
export default function FolderIcon(props) {
|
export default function FolderIcon(props) {
|
||||||
return (
|
return (
|
||||||
<div >
|
<div>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
height={props.height}
|
height={props.height}
|
||||||
|
@ -14,7 +12,6 @@ export default function FolderIcon(props) {
|
||||||
<path d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z" />
|
<path d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,16 +4,16 @@ import styled from 'styled-components';
|
||||||
const Rotate = styled.div<{ disabled }>`
|
const Rotate = styled.div<{ disabled }>`
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 27px;
|
height: 27px;
|
||||||
${(props) => !props.disabled && '-webkit-animation: rotation 1s infinite linear'};
|
${(props) =>
|
||||||
cursor:${(props) => props.disabled && 'pointer'};
|
!props.disabled && '-webkit-animation: rotation 1s infinite linear'};
|
||||||
|
cursor: ${(props) => props.disabled && 'pointer'};
|
||||||
transition-duration: 0.8s;
|
transition-duration: 0.8s;
|
||||||
transition-property: transform;
|
transition-property: transform;
|
||||||
&:hover {
|
&:hover {
|
||||||
color:#fff;
|
color: #fff;
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
-webkit-transform: rotate(90deg);
|
-webkit-transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
`;
|
`;
|
||||||
export default function InProgressIcon(props) {
|
export default function InProgressIcon(props) {
|
||||||
return (
|
return (
|
||||||
|
@ -24,9 +24,10 @@ export default function InProgressIcon(props) {
|
||||||
viewBox={props.viewBox}
|
viewBox={props.viewBox}
|
||||||
width={props.width}
|
width={props.width}
|
||||||
fill="#000000">
|
fill="#000000">
|
||||||
<path d="M.01 0h24v24h-24V0z" fill="none" /><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z" />
|
<path d="M.01 0h24v24h-24V0z" fill="none" />
|
||||||
|
<path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z" />
|
||||||
</svg>
|
</svg>
|
||||||
</ Rotate>
|
</Rotate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
InProgressIcon.defaultProps = {
|
InProgressIcon.defaultProps = {
|
||||||
|
@ -35,4 +36,3 @@ InProgressIcon.defaultProps = {
|
||||||
width: 24,
|
width: 24,
|
||||||
viewBox: '0 0 24 24',
|
viewBox: '0 0 24 24',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,7 @@ export default function LocationIcon(props) {
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
height={props.height}
|
height={props.height}
|
||||||
viewBox={props.viewBox}
|
viewBox={props.viewBox}
|
||||||
width={props.width}
|
width={props.width}>
|
||||||
>
|
|
||||||
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zM7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 2.88-2.88 7.19-5 9.88C9.92 16.21 7 11.85 7 9z" />
|
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zM7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 2.88-2.88 7.19-5 9.88C9.92 16.21 7 11.85 7 9z" />
|
||||||
<circle cx="12" cy="9" r="2.5" />
|
<circle cx="12" cy="9" r="2.5" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -8,8 +8,7 @@ export default function NavigateNext(props) {
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
width="24px"
|
width="24px"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
|
||||||
<path d="M0 0h24v24H0z" fill="none" />
|
<path d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
|
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -6,8 +6,7 @@ export default function PlayCircleOutline(props) {
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
height={props.height}
|
height={props.height}
|
||||||
viewBox={props.viewBox}
|
viewBox={props.viewBox}
|
||||||
width={props.width}
|
width={props.width}>
|
||||||
>
|
|
||||||
<path d="M0 0h24v24H0z" fill="none" />
|
<path d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M10 16.5l6-4.5-6-4.5v9zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
|
<path d="M10 16.5l6-4.5-6-4.5v9zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -6,8 +6,7 @@ export default function SadFace(props) {
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
height={props.height}
|
height={props.height}
|
||||||
viewBox={props.viewBox}
|
viewBox={props.viewBox}
|
||||||
width={props.width}
|
width={props.width}>
|
||||||
>
|
|
||||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||||
<circle cx="15.5" cy="9.5" r="1.5" />
|
<circle cx="15.5" cy="9.5" r="1.5" />
|
||||||
<circle cx="8.5" cy="9.5" r="1.5" />
|
<circle cx="8.5" cy="9.5" r="1.5" />
|
||||||
|
|
|
@ -6,8 +6,7 @@ export default function SearchIcon(props) {
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
height={props.height}
|
height={props.height}
|
||||||
viewBox={props.viewBox}
|
viewBox={props.viewBox}
|
||||||
width={props.width}
|
width={props.width}>
|
||||||
>
|
|
||||||
<path d="M20.49 19l-5.73-5.73C15.53 12.2 16 10.91 16 9.5A6.5 6.5 0 1 0 9.5 16c1.41 0 2.7-.47 3.77-1.24L19 20.49 20.49 19zM5 9.5C5 7.01 7.01 5 9.5 5S14 7.01 14 9.5 11.99 14 9.5 14 5 11.99 5 9.5z" />
|
<path d="M20.49 19l-5.73-5.73C15.53 12.2 16 10.91 16 9.5A6.5 6.5 0 1 0 9.5 16c1.41 0 2.7-.47 3.77-1.24L19 20.49 20.49 19zM5 9.5C5 7.01 7.01 5 9.5 5S14 7.01 14 9.5 11.99 14 9.5 14 5 11.99 5 9.5z" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
23
src/components/icons/Visibility.tsx
Normal file
23
src/components/icons/Visibility.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react';
|
||||||
|
export default function Visibility(props) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={props.height}
|
||||||
|
viewBox={props.viewBox}
|
||||||
|
width={props.width}
|
||||||
|
fill="#000000">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Visibility.defaultProps = {
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
open: false,
|
||||||
|
};
|
26
src/components/icons/VisibilityOff.tsx
Normal file
26
src/components/icons/VisibilityOff.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
export default function VisibilityOff(props) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={props.height}
|
||||||
|
viewBox={props.viewBox}
|
||||||
|
width={props.width}
|
||||||
|
fill="#000000">
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
VisibilityOff.defaultProps = {
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
open: false,
|
||||||
|
};
|
|
@ -6,10 +6,12 @@ export default function PowerSettings(props) {
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
height={props.height}
|
height={props.height}
|
||||||
viewBox={props.viewBox}
|
viewBox={props.viewBox}
|
||||||
width={props.width}
|
width={props.width}>
|
||||||
>
|
|
||||||
<path fill="none" d="M0 0h24v24H0z" />
|
<path fill="none" d="M0 0h24v24H0z" />
|
||||||
<path fill="#ff6666" d="M13 3h-2v10h2V3zm4.83 2.17l-1.42 1.42C17.99 7.86 19 9.81 19 12c0 3.87-3.13 7-7 7s-7-3.13-7-7c0-2.19 1.01-4.14 2.58-5.42L6.17 5.17C4.23 6.82 3 9.26 3 12c0 4.97 4.03 9 9 9s9-4.03 9-9c0-2.74-1.23-5.18-3.17-6.83z" />
|
<path
|
||||||
|
fill="#ff6666"
|
||||||
|
d="M13 3h-2v10h2V3zm4.83 2.17l-1.42 1.42C17.99 7.86 19 9.81 19 12c0 3.87-3.13 7-7 7s-7-3.13-7-7c0-2.19 1.01-4.14 2.58-5.42L6.17 5.17C4.23 6.82 3 9.26 3 12c0 4.97 4.03 9 9 9s9-4.03 9-9c0-2.74-1.23-5.18-3.17-6.83z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,31 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Alert from 'react-bootstrap/Alert';
|
import Alert from 'react-bootstrap/Alert';
|
||||||
|
import { getVariantColor } from './LinkButton';
|
||||||
|
|
||||||
export default function AlertBanner({ bannerMessage }) {
|
interface Props {
|
||||||
|
bannerMessage?: any;
|
||||||
|
variant?: string;
|
||||||
|
children?: any;
|
||||||
|
}
|
||||||
|
export default function AlertBanner(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
variant="danger"
|
variant={props.variant ?? 'danger'}
|
||||||
style={{
|
style={{
|
||||||
display: bannerMessage ? 'block' : 'none',
|
display:
|
||||||
|
props.bannerMessage || props.children ? 'block' : 'none',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}
|
|
||||||
>
|
border: 'none',
|
||||||
{bannerMessage}
|
borderBottom: '1px solid',
|
||||||
|
background: 'none',
|
||||||
|
borderRadius: '0px',
|
||||||
|
color: getVariantColor(props.variant),
|
||||||
|
padding: 0,
|
||||||
|
margin: '0 25px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
}}>
|
||||||
|
{props.bannerMessage ? props.bannerMessage : props.children}
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,15 +19,13 @@ function ChoiceModal({
|
||||||
<MessageDialog
|
<MessageDialog
|
||||||
size="lg"
|
size="lg"
|
||||||
{...props}
|
{...props}
|
||||||
attributes={{ title: constants.MULTI_FOLDER_UPLOAD }}
|
attributes={{ title: constants.MULTI_FOLDER_UPLOAD }}>
|
||||||
>
|
|
||||||
<p>{constants.UPLOAD_STRATEGY_CHOICE}</p>
|
<p>{constants.UPLOAD_STRATEGY_CHOICE}</p>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline-success"
|
variant="outline-success"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -38,8 +36,7 @@ function ChoiceModal({
|
||||||
padding: '12px 24px',
|
padding: '12px 24px',
|
||||||
flex: 2,
|
flex: 2,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{constants.UPLOAD_STRATEGY_SINGLE_COLLECTION}
|
{constants.UPLOAD_STRATEGY_SINGLE_COLLECTION}
|
||||||
</Button>
|
</Button>
|
||||||
<div
|
<div
|
||||||
|
@ -48,8 +45,7 @@ function ChoiceModal({
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
minWidth: '100px',
|
minWidth: '100px',
|
||||||
margin: '2% auto',
|
margin: '2% auto',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<strong>{constants.OR}</strong>
|
<strong>{constants.OR}</strong>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
@ -62,8 +58,7 @@ function ChoiceModal({
|
||||||
padding: '12px 24px',
|
padding: '12px 24px',
|
||||||
flex: 2,
|
flex: 2,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{constants.UPLOAD_STRATEGY_COLLECTION_PER_FOLDER}
|
{constants.UPLOAD_STRATEGY_COLLECTION_PER_FOLDER}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -51,8 +51,7 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
|
||||||
size="sm"
|
size="sm"
|
||||||
attributes={{
|
attributes={{
|
||||||
title: attributes?.title,
|
title: attributes?.title,
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Formik<formValues>
|
<Formik<formValues>
|
||||||
initialValues={{ albumName: attributes.autoFilledName }}
|
initialValues={{ albumName: attributes.autoFilledName }}
|
||||||
validationSchema={Yup.object().shape({
|
validationSchema={Yup.object().shape({
|
||||||
|
@ -60,11 +59,8 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
|
||||||
})}
|
})}
|
||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}>
|
||||||
>
|
{({ values, touched, errors, handleChange, handleSubmit }) => (
|
||||||
{({
|
|
||||||
values, touched, errors, handleChange, handleSubmit,
|
|
||||||
}) => (
|
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
@ -73,7 +69,7 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
|
||||||
value={values.albumName}
|
value={values.albumName}
|
||||||
onChange={handleChange('albumName')}
|
onChange={handleChange('albumName')}
|
||||||
isInvalid={Boolean(
|
isInvalid={Boolean(
|
||||||
touched.albumName && errors.albumName,
|
touched.albumName && errors.albumName
|
||||||
)}
|
)}
|
||||||
placeholder={constants.ENTER_ALBUM_NAME}
|
placeholder={constants.ENTER_ALBUM_NAME}
|
||||||
ref={collectionNameInputRef}
|
ref={collectionNameInputRef}
|
||||||
|
@ -82,8 +78,7 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
|
||||||
|
|
||||||
<Form.Control.Feedback
|
<Form.Control.Feedback
|
||||||
type="invalid"
|
type="invalid"
|
||||||
className="text-center"
|
className="text-center">
|
||||||
>
|
|
||||||
{errors.albumName}
|
{errors.albumName}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
|
@ -24,7 +24,7 @@ interface Props {
|
||||||
const CollectionOptions = (props: Props) => {
|
const CollectionOptions = (props: Props) => {
|
||||||
const collectionRename = async (
|
const collectionRename = async (
|
||||||
selectedCollection: Collection,
|
selectedCollection: Collection,
|
||||||
newName: string,
|
newName: string
|
||||||
) => {
|
) => {
|
||||||
if (selectedCollection.name !== newName) {
|
if (selectedCollection.name !== newName) {
|
||||||
await renameCollection(selectedCollection, newName);
|
await renameCollection(selectedCollection, newName);
|
||||||
|
@ -37,16 +37,16 @@ const CollectionOptions = (props: Props) => {
|
||||||
buttonText: constants.RENAME,
|
buttonText: constants.RENAME,
|
||||||
autoFilledName: getSelectedCollection(
|
autoFilledName: getSelectedCollection(
|
||||||
props.selectedCollectionID,
|
props.selectedCollectionID,
|
||||||
props.collections,
|
props.collections
|
||||||
)?.name,
|
)?.name,
|
||||||
callback: (newName) => {
|
callback: (newName) => {
|
||||||
props.startLoadingBar();
|
props.startLoadingBar();
|
||||||
collectionRename(
|
collectionRename(
|
||||||
getSelectedCollection(
|
getSelectedCollection(
|
||||||
props.selectedCollectionID,
|
props.selectedCollectionID,
|
||||||
props.collections,
|
props.collections
|
||||||
),
|
),
|
||||||
newName,
|
newName
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -64,7 +64,7 @@ const CollectionOptions = (props: Props) => {
|
||||||
props.selectedCollectionID,
|
props.selectedCollectionID,
|
||||||
props.syncWithRemote,
|
props.syncWithRemote,
|
||||||
props.redirectToAll,
|
props.redirectToAll,
|
||||||
props.setDialogMessage,
|
props.setDialogMessage
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
|
@ -78,8 +78,7 @@ const CollectionOptions = (props: Props) => {
|
||||||
const MenuLink = (props) => (
|
const MenuLink = (props) => (
|
||||||
<LinkButton
|
<LinkButton
|
||||||
style={{ fontSize: '14px', fontWeight: 700, padding: '8px 1em' }}
|
style={{ fontSize: '14px', fontWeight: 700, padding: '8px 1em' }}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
);
|
);
|
||||||
|
@ -89,8 +88,7 @@ const CollectionOptions = (props: Props) => {
|
||||||
style={{
|
style={{
|
||||||
background: '#282828',
|
background: '#282828',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
);
|
);
|
||||||
|
@ -111,8 +109,7 @@ const CollectionOptions = (props: Props) => {
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<MenuLink
|
<MenuLink
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={confirmDeleteCollection}
|
onClick={confirmDeleteCollection}>
|
||||||
>
|
|
||||||
{constants.DELETE}
|
{constants.DELETE}
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
|
@ -37,7 +37,6 @@ interface Props {
|
||||||
directlyShowNextModal: boolean;
|
directlyShowNextModal: boolean;
|
||||||
collectionsAndTheirLatestFile: CollectionAndItsLatestFile[];
|
collectionsAndTheirLatestFile: CollectionAndItsLatestFile[];
|
||||||
attributes: CollectionSelectorAttributes;
|
attributes: CollectionSelectorAttributes;
|
||||||
syncWithRemote:(force?: boolean, silent?:boolean)=>Promise<void>;
|
|
||||||
}
|
}
|
||||||
function CollectionSelector({
|
function CollectionSelector({
|
||||||
attributes,
|
attributes,
|
||||||
|
@ -62,12 +61,11 @@ function CollectionSelector({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
attributes.callback(item.collection);
|
attributes.callback(item.collection);
|
||||||
props.onHide();
|
props.onHide();
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<CollectionCard>
|
<CollectionCard>
|
||||||
<PreviewCard
|
<PreviewCard
|
||||||
file={item.file}
|
file={item.file}
|
||||||
updateUrl={() => { }}
|
updateUrl={() => {}}
|
||||||
forcedEnable
|
forcedEnable
|
||||||
/>
|
/>
|
||||||
<Card.Text className="text-center">
|
<Card.Text className="text-center">
|
||||||
|
@ -75,11 +73,15 @@ function CollectionSelector({
|
||||||
</Card.Text>
|
</Card.Text>
|
||||||
</CollectionCard>
|
</CollectionCard>
|
||||||
</CollectionIcon>
|
</CollectionIcon>
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal {...props} size="xl" centered>
|
<Modal
|
||||||
|
{...props}
|
||||||
|
size="xl"
|
||||||
|
centered
|
||||||
|
contentClassName="plan-selector-modal-content">
|
||||||
<Modal.Header closeButton onHide={() => props.onHide(true)}>
|
<Modal.Header closeButton onHide={() => props.onHide(true)}>
|
||||||
<Modal.Title>{attributes.title}</Modal.Title>
|
<Modal.Title>{attributes.title}</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
|
@ -88,8 +90,7 @@ function CollectionSelector({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'space-around',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<AddCollectionButton showNextModal={attributes.showNextModal} />
|
<AddCollectionButton showNextModal={attributes.showNextModal} />
|
||||||
{CollectionIcons}
|
{CollectionIcons}
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
|
|
|
@ -3,9 +3,7 @@ import { SetDialogMessage } from 'components/MessageDialog';
|
||||||
import NavigationButton, {
|
import NavigationButton, {
|
||||||
SCROLL_DIRECTION,
|
SCROLL_DIRECTION,
|
||||||
} from 'components/NavigationButton';
|
} from 'components/NavigationButton';
|
||||||
import React, {
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
useEffect, useRef, useState,
|
|
||||||
} from 'react';
|
|
||||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||||
import { Collection, CollectionType } from 'services/collectionService';
|
import { Collection, CollectionType } from 'services/collectionService';
|
||||||
import { User } from 'services/userService';
|
import { User } from 'services/userService';
|
||||||
|
@ -26,7 +24,7 @@ interface CollectionProps {
|
||||||
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
||||||
startLoadingBar: () => void;
|
startLoadingBar: () => void;
|
||||||
searchMode: boolean;
|
searchMode: boolean;
|
||||||
collectionFilesCount: Map<number, number>
|
collectionFilesCount: Map<number, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
|
@ -38,7 +36,7 @@ const Container = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
|
|
||||||
@media(max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -58,7 +56,8 @@ const Chip = styled.button<{ active: boolean }>`
|
||||||
padding-left: 24px;
|
padding-left: 24px;
|
||||||
margin: 3px;
|
margin: 3px;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: ${(props) => (props.active ? '#fff' : 'rgba(255, 255, 255, 0.3)')};
|
background-color: ${(props) =>
|
||||||
|
props.active ? '#fff' : 'rgba(255, 255, 255, 0.3)'};
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: ${(props) => !props.active && '#bbbbbb'};
|
background-color: ${(props) => !props.active && '#bbbbbb'};
|
||||||
|
@ -71,9 +70,11 @@ const Chip = styled.button<{ active: boolean }>`
|
||||||
|
|
||||||
export default function Collections(props: CollectionProps) {
|
export default function Collections(props: CollectionProps) {
|
||||||
const { selected, collections, selectCollection } = props;
|
const { selected, collections, selectCollection } = props;
|
||||||
const [selectedCollectionID, setSelectedCollectionID] = useState<number>(null);
|
const [selectedCollectionID, setSelectedCollectionID] =
|
||||||
|
useState<number>(null);
|
||||||
const collectionRef = useRef<HTMLDivElement>(null);
|
const collectionRef = useRef<HTMLDivElement>(null);
|
||||||
const [collectionShareModalView, setCollectionShareModalView] = useState(false);
|
const [collectionShareModalView, setCollectionShareModalView] =
|
||||||
|
useState(false);
|
||||||
const [scrollObj, setScrollObj] = useState<{
|
const [scrollObj, setScrollObj] = useState<{
|
||||||
scrollLeft?: number;
|
scrollLeft?: number;
|
||||||
scrollWidth?: number;
|
scrollWidth?: number;
|
||||||
|
@ -82,7 +83,8 @@ export default function Collections(props: CollectionProps) {
|
||||||
|
|
||||||
const updateScrollObj = () => {
|
const updateScrollObj = () => {
|
||||||
if (collectionRef.current) {
|
if (collectionRef.current) {
|
||||||
const { scrollLeft, scrollWidth, clientWidth } = collectionRef.current;
|
const { scrollLeft, scrollWidth, clientWidth } =
|
||||||
|
collectionRef.current;
|
||||||
setScrollObj({ scrollLeft, scrollWidth, clientWidth });
|
setScrollObj({ scrollLeft, scrollWidth, clientWidth });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -126,10 +128,13 @@ export default function Collections(props: CollectionProps) {
|
||||||
const renderTooltip = (collectionID) => {
|
const renderTooltip = (collectionID) => {
|
||||||
const fileCount = props.collectionFilesCount?.get(collectionID);
|
const fileCount = props.collectionFilesCount?.get(collectionID);
|
||||||
return (
|
return (
|
||||||
<Tooltip style={{
|
<Tooltip
|
||||||
padding: '0',
|
style={{
|
||||||
paddingBottom: '5px',
|
padding: '0',
|
||||||
}} id="button-tooltip" {...props}>
|
paddingBottom: '5px',
|
||||||
|
}}
|
||||||
|
id="button-tooltip"
|
||||||
|
{...props}>
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
style={{
|
style={{
|
||||||
|
@ -139,8 +144,7 @@ export default function Collections(props: CollectionProps) {
|
||||||
color: '#ddd',
|
color: '#ddd',
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{fileCount} {fileCount > 1 ? 'items' : 'item'}
|
{fileCount} {fileCount > 1 ? 'items' : 'item'}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -155,7 +159,7 @@ export default function Collections(props: CollectionProps) {
|
||||||
onHide={() => setCollectionShareModalView(false)}
|
onHide={() => setCollectionShareModalView(false)}
|
||||||
collection={getSelectedCollection(
|
collection={getSelectedCollection(
|
||||||
selectedCollectionID,
|
selectedCollectionID,
|
||||||
props.collections,
|
props.collections
|
||||||
)}
|
)}
|
||||||
syncWithRemote={props.syncWithRemote}
|
syncWithRemote={props.syncWithRemote}
|
||||||
/>
|
/>
|
||||||
|
@ -181,35 +185,45 @@ export default function Collections(props: CollectionProps) {
|
||||||
key={item.id}
|
key={item.id}
|
||||||
placement="top"
|
placement="top"
|
||||||
delay={{ show: 250, hide: 400 }}
|
delay={{ show: 250, hide: 400 }}
|
||||||
overlay={renderTooltip(item.id)}
|
overlay={renderTooltip(item.id)}>
|
||||||
>
|
|
||||||
<Chip
|
<Chip
|
||||||
|
|
||||||
active={selected === item.id}
|
active={selected === item.id}
|
||||||
onClick={clickHandler(item)}
|
onClick={clickHandler(item)}>
|
||||||
>
|
|
||||||
{item.name}
|
{item.name}
|
||||||
{item.type !== CollectionType.favorites &&
|
{item.type !== CollectionType.favorites &&
|
||||||
item.owner.id === user?.id ? (<OverlayTrigger
|
item.owner.id === user?.id ? (
|
||||||
|
<OverlayTrigger
|
||||||
rootClose
|
rootClose
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
overlay={collectionOptions}
|
overlay={collectionOptions}>
|
||||||
>
|
|
||||||
<OptionIcon
|
<OptionIcon
|
||||||
onClick={() => setSelectedCollectionID(item.id)}
|
onClick={() =>
|
||||||
|
setSelectedCollectionID(
|
||||||
|
item.id
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</OverlayTrigger>) : (<div style={{
|
</OverlayTrigger>
|
||||||
display: 'inline-block',
|
) : (
|
||||||
width: '24px',
|
<div
|
||||||
}}
|
style={{
|
||||||
/>)}
|
display: 'inline-block',
|
||||||
|
width: '24px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Chip>
|
</Chip>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
))}
|
))}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
{scrollObj.scrollLeft <
|
{scrollObj.scrollLeft <
|
||||||
scrollObj.scrollWidth - scrollObj.clientWidth && (<NavigationButton scrollDirection={SCROLL_DIRECTION.RIGHT} onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)} />)}
|
scrollObj.scrollWidth - scrollObj.clientWidth && (
|
||||||
|
<NavigationButton
|
||||||
|
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
||||||
|
onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ enum ButtonVariant {
|
||||||
success = 'success',
|
success = 'success',
|
||||||
danger = 'danger',
|
danger = 'danger',
|
||||||
secondary = 'secondary',
|
secondary = 'secondary',
|
||||||
|
warning = 'warning',
|
||||||
}
|
}
|
||||||
type Props = React.PropsWithChildren<{
|
type Props = React.PropsWithChildren<{
|
||||||
onClick: any;
|
onClick: any;
|
||||||
|
@ -11,29 +12,30 @@ type Props = React.PropsWithChildren<{
|
||||||
style?: any;
|
style?: any;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default function LinkButton(props: Props) {
|
export function getVariantColor(variant: string) {
|
||||||
function getButtonColor(variant: string) {
|
switch (variant) {
|
||||||
switch (variant) {
|
case ButtonVariant.success:
|
||||||
case ButtonVariant.success:
|
return '#2dc262';
|
||||||
return '#2dc262';
|
case ButtonVariant.danger:
|
||||||
case ButtonVariant.danger:
|
return '#c93f3f';
|
||||||
return '#c93f3f';
|
case ButtonVariant.secondary:
|
||||||
case ButtonVariant.secondary:
|
return '#858585';
|
||||||
return '#858585';
|
case ButtonVariant.warning:
|
||||||
default:
|
return '#D7BB63';
|
||||||
return '#d1d1d1';
|
default:
|
||||||
}
|
return '#d1d1d1';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export default function LinkButton(props: Props) {
|
||||||
return (
|
return (
|
||||||
<h5
|
<h5
|
||||||
style={{
|
style={{
|
||||||
color: getButtonColor(props.variant),
|
color: getVariantColor(props.variant),
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
...props.style,
|
...props.style,
|
||||||
}}
|
}}
|
||||||
onClick={props?.onClick ?? (() => null)}
|
onClick={props?.onClick ?? (() => null)}>
|
||||||
>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</h5>
|
</h5>
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,17 +16,18 @@ const OptionIcon = ({ onClick }: Props) => (
|
||||||
onClick();
|
onClick();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
style={{ marginBottom: '2px' }}
|
style={{ marginBottom: '2px' }}>
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
height="20px"
|
height="20px"
|
||||||
width="24px"
|
width="24px"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="#000000"
|
fill="#000000">
|
||||||
>
|
|
||||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||||
<path fill="#666" d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
|
<path
|
||||||
|
fill="#666"
|
||||||
|
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</OptionIconWrapper>
|
</OptionIconWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,7 +5,6 @@ import styled from 'styled-components';
|
||||||
import billingService, { Plan, Subscription } from 'services/billingService';
|
import billingService, { Plan, Subscription } from 'services/billingService';
|
||||||
import {
|
import {
|
||||||
convertBytesToGBs,
|
convertBytesToGBs,
|
||||||
getPlans,
|
|
||||||
getUserSubscription,
|
getUserSubscription,
|
||||||
isUserSubscribedPlan,
|
isUserSubscribedPlan,
|
||||||
isSubscriptionCancelled,
|
isSubscriptionCancelled,
|
||||||
|
@ -16,6 +15,7 @@ import {
|
||||||
hasStripeSubscription,
|
hasStripeSubscription,
|
||||||
hasPaidSubscription,
|
hasPaidSubscription,
|
||||||
isOnFreePlan,
|
isOnFreePlan,
|
||||||
|
planForSubscription,
|
||||||
} from 'utils/billingUtil';
|
} from 'utils/billingUtil';
|
||||||
import { reverseString } from 'utils/common';
|
import { reverseString } from 'utils/common';
|
||||||
import { SetDialogMessage } from 'components/MessageDialog';
|
import { SetDialogMessage } from 'components/MessageDialog';
|
||||||
|
@ -25,7 +25,7 @@ import { DeadCenter, SetLoading } from 'pages/gallery';
|
||||||
|
|
||||||
export const PlanIcon = styled.div<{ selected: boolean }>`
|
export const PlanIcon = styled.div<{ selected: boolean }>`
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
width: 250px;
|
width: 220px;
|
||||||
border: 2px solid #333;
|
border: 2px solid #333;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
@ -55,7 +55,7 @@ export const PlanIcon = styled.div<{ selected: boolean }>`
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(1.2);
|
transform: scale(1.1);
|
||||||
background-color: #ffffff11;
|
background-color: #ffffff11;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,18 +76,33 @@ enum PLAN_PERIOD {
|
||||||
}
|
}
|
||||||
function PlanSelector(props: Props) {
|
function PlanSelector(props: Props) {
|
||||||
const subscription: Subscription = getUserSubscription();
|
const subscription: Subscription = getUserSubscription();
|
||||||
const plans = getPlans();
|
const [plans, setPlans] = useState<Plan[]>(null);
|
||||||
const [planPeriod, setPlanPeriod] = useState<PLAN_PERIOD>(PLAN_PERIOD.YEAR);
|
const [planPeriod, setPlanPeriod] = useState<PLAN_PERIOD>(PLAN_PERIOD.YEAR);
|
||||||
const togglePeriod = () => {
|
const togglePeriod = () => {
|
||||||
setPlanPeriod((prevPeriod) => (prevPeriod === PLAN_PERIOD.MONTH ?
|
setPlanPeriod((prevPeriod) =>
|
||||||
PLAN_PERIOD.YEAR :
|
prevPeriod === PLAN_PERIOD.MONTH
|
||||||
PLAN_PERIOD.MONTH));
|
? PLAN_PERIOD.YEAR
|
||||||
|
: PLAN_PERIOD.MONTH
|
||||||
|
);
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!plans && props.modalView) {
|
if (props.modalView) {
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
props.setLoading(true);
|
props.setLoading(true);
|
||||||
await billingService.updatePlans();
|
let plans = await billingService.getPlans();
|
||||||
|
|
||||||
|
const planNotListed =
|
||||||
|
plans.filter((plan) =>
|
||||||
|
isUserSubscribedPlan(plan, subscription)
|
||||||
|
).length === 0;
|
||||||
|
if (
|
||||||
|
subscription &&
|
||||||
|
!isOnFreePlan(subscription) &&
|
||||||
|
planNotListed
|
||||||
|
) {
|
||||||
|
plans = [planForSubscription(subscription), ...plans];
|
||||||
|
}
|
||||||
|
setPlans(plans);
|
||||||
props.setLoading(false);
|
props.setLoading(false);
|
||||||
};
|
};
|
||||||
main();
|
main();
|
||||||
|
@ -108,7 +123,7 @@ function PlanSelector(props: Props) {
|
||||||
} else if (hasStripeSubscription(subscription)) {
|
} else if (hasStripeSubscription(subscription)) {
|
||||||
props.setDialogMessage({
|
props.setDialogMessage({
|
||||||
title: `${constants.CONFIRM} ${reverseString(
|
title: `${constants.CONFIRM} ${reverseString(
|
||||||
constants.UPDATE_SUBSCRIPTION,
|
constants.UPDATE_SUBSCRIPTION
|
||||||
)}`,
|
)}`,
|
||||||
content: constants.UPDATE_SUBSCRIPTION_MESSAGE,
|
content: constants.UPDATE_SUBSCRIPTION_MESSAGE,
|
||||||
staticBackdrop: true,
|
staticBackdrop: true,
|
||||||
|
@ -119,7 +134,7 @@ function PlanSelector(props: Props) {
|
||||||
plan,
|
plan,
|
||||||
props.setDialogMessage,
|
props.setDialogMessage,
|
||||||
props.setLoading,
|
props.setLoading,
|
||||||
props.closeModal,
|
props.closeModal
|
||||||
),
|
),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
},
|
},
|
||||||
|
@ -148,16 +163,15 @@ function PlanSelector(props: Props) {
|
||||||
key={plan.stripeID}
|
key={plan.stripeID}
|
||||||
className="subscription-plan-selector"
|
className="subscription-plan-selector"
|
||||||
selected={isUserSubscribedPlan(plan, subscription)}
|
selected={isUserSubscribedPlan(plan, subscription)}
|
||||||
>
|
onClick={async () => await onPlanSelect(plan)}>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: '#ECECEC',
|
color: '#ECECEC',
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
fontSize: '72px',
|
fontSize: '40px',
|
||||||
lineHeight: '72px',
|
lineHeight: '40px',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{convertBytesToGBs(plan.storage, 0)}
|
{convertBytesToGBs(plan.storage, 0)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
@ -165,27 +179,32 @@ function PlanSelector(props: Props) {
|
||||||
color: '#858585',
|
color: '#858585',
|
||||||
fontSize: '24px',
|
fontSize: '24px',
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{' '}
|
{' '}
|
||||||
GB
|
GB
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="bold-text"
|
className="bold-text"
|
||||||
style={{ color: '#aaa', lineHeight: '36px', fontSize: '20px' }}
|
style={{
|
||||||
>
|
color: '#aaa',
|
||||||
|
lineHeight: '36px',
|
||||||
|
fontSize: '20px',
|
||||||
|
}}>
|
||||||
{`${plan.price} / ${plan.period}`}
|
{`${plan.price} / ${plan.period}`}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline-success"
|
variant="outline-success"
|
||||||
block
|
block
|
||||||
style={{ marginTop: '30px' }}
|
style={{
|
||||||
disabled={isUserSubscribedPlan(plan, subscription)}
|
marginTop: '20px',
|
||||||
onClick={async () => (await onPlanSelect(plan))}
|
fontSize: '14px',
|
||||||
>
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
disabled={isUserSubscribedPlan(plan, subscription)}>
|
||||||
{constants.CHOOSE_PLAN_BTN}
|
{constants.CHOOSE_PLAN_BTN}
|
||||||
<ArrowEast style={{ marginLeft: '10px' }} />
|
<ArrowEast style={{ marginLeft: '5px' }} />
|
||||||
</Button>
|
</Button>
|
||||||
</PlanIcon>
|
</PlanIcon>
|
||||||
));
|
));
|
||||||
|
@ -196,19 +215,18 @@ function PlanSelector(props: Props) {
|
||||||
size="xl"
|
size="xl"
|
||||||
centered
|
centered
|
||||||
backdrop={hasPaidSubscription(subscription) ? 'true' : 'static'}
|
backdrop={hasPaidSubscription(subscription) ? 'true' : 'static'}
|
||||||
>
|
contentClassName="plan-selector-modal-content">
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title
|
<Modal.Title
|
||||||
style={{
|
style={{
|
||||||
marginLeft: '12px',
|
marginLeft: '12px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<span>
|
<span>
|
||||||
{hasPaidSubscription(subscription) ?
|
{hasPaidSubscription(subscription)
|
||||||
constants.MANAGE_PLAN :
|
? constants.MANAGE_PLAN
|
||||||
constants.CHOOSE_PLAN}
|
: constants.CHOOSE_PLAN}
|
||||||
</span>
|
</span>
|
||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
|
@ -217,22 +235,23 @@ function PlanSelector(props: Props) {
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<span
|
<span
|
||||||
className="bold-text"
|
className="bold-text"
|
||||||
style={{ fontSize: '20px' }}
|
style={{ fontSize: '16px' }}>
|
||||||
>
|
|
||||||
{constants.MONTHLY}
|
{constants.MONTHLY}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Form.Switch
|
<Form.Switch
|
||||||
checked={planPeriod === PLAN_PERIOD.YEAR}
|
checked={planPeriod === PLAN_PERIOD.YEAR}
|
||||||
id="plan-period-toggler"
|
id="plan-period-toggler"
|
||||||
style={{ margin: '-4px 0 20px 15px' }}
|
style={{
|
||||||
|
margin: '-4px 0 20px 15px',
|
||||||
|
fontSize: '10px',
|
||||||
|
}}
|
||||||
className="custom-switch-md"
|
className="custom-switch-md"
|
||||||
onChange={togglePeriod}
|
onChange={togglePeriod}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="bold-text"
|
className="bold-text"
|
||||||
style={{ fontSize: '20px' }}
|
style={{ fontSize: '16px' }}>
|
||||||
>
|
|
||||||
{constants.YEARLY}
|
{constants.YEARLY}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -243,9 +262,8 @@ function PlanSelector(props: Props) {
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'space-around',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
minHeight: '212px',
|
minHeight: '212px',
|
||||||
margin: '24px 0',
|
margin: '5px 0',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{plans && PlanIcons}
|
{plans && PlanIcons}
|
||||||
</div>
|
</div>
|
||||||
<DeadCenter style={{ marginBottom: '30px' }}>
|
<DeadCenter style={{ marginBottom: '30px' }}>
|
||||||
|
@ -254,55 +272,55 @@ function PlanSelector(props: Props) {
|
||||||
{isSubscriptionCancelled(subscription) ? (
|
{isSubscriptionCancelled(subscription) ? (
|
||||||
<LinkButton
|
<LinkButton
|
||||||
variant="success"
|
variant="success"
|
||||||
onClick={() => props.setDialogMessage({
|
onClick={() =>
|
||||||
title:
|
props.setDialogMessage({
|
||||||
constants.CONFIRM_ACTIVATE_SUBSCRIPTION,
|
title: constants.CONFIRM_ACTIVATE_SUBSCRIPTION,
|
||||||
content: constants.ACTIVATE_SUBSCRIPTION_MESSAGE(
|
content:
|
||||||
subscription.expiryTime,
|
constants.ACTIVATE_SUBSCRIPTION_MESSAGE(
|
||||||
),
|
subscription.expiryTime
|
||||||
staticBackdrop: true,
|
),
|
||||||
proceed: {
|
staticBackdrop: true,
|
||||||
text:
|
proceed: {
|
||||||
constants.ACTIVATE_SUBSCRIPTION,
|
text: constants.ACTIVATE_SUBSCRIPTION,
|
||||||
action: activateSubscription.bind(
|
action: activateSubscription.bind(
|
||||||
null,
|
null,
|
||||||
props.setDialogMessage,
|
props.setDialogMessage,
|
||||||
props.closeModal,
|
props.closeModal,
|
||||||
props.setLoading,
|
props.setLoading
|
||||||
),
|
),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
},
|
},
|
||||||
close: {
|
close: {
|
||||||
text: constants.CANCEL,
|
text: constants.CANCEL,
|
||||||
},
|
},
|
||||||
})}
|
})
|
||||||
>
|
}>
|
||||||
{constants.ACTIVATE_SUBSCRIPTION}
|
{constants.ACTIVATE_SUBSCRIPTION}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
) : (
|
) : (
|
||||||
<LinkButton
|
<LinkButton
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => props.setDialogMessage({
|
onClick={() =>
|
||||||
title:
|
props.setDialogMessage({
|
||||||
constants.CONFIRM_CANCEL_SUBSCRIPTION,
|
title: constants.CONFIRM_CANCEL_SUBSCRIPTION,
|
||||||
content: constants.CANCEL_SUBSCRIPTION_MESSAGE(),
|
content:
|
||||||
staticBackdrop: true,
|
constants.CANCEL_SUBSCRIPTION_MESSAGE(),
|
||||||
proceed: {
|
staticBackdrop: true,
|
||||||
text:
|
proceed: {
|
||||||
constants.CANCEL_SUBSCRIPTION,
|
text: constants.CANCEL_SUBSCRIPTION,
|
||||||
action: cancelSubscription.bind(
|
action: cancelSubscription.bind(
|
||||||
null,
|
null,
|
||||||
props.setDialogMessage,
|
props.setDialogMessage,
|
||||||
props.closeModal,
|
props.closeModal,
|
||||||
props.setLoading,
|
props.setLoading
|
||||||
),
|
),
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
},
|
},
|
||||||
close: {
|
close: {
|
||||||
text: constants.CANCEL,
|
text: constants.CANCEL,
|
||||||
},
|
},
|
||||||
})}
|
})
|
||||||
>
|
}>
|
||||||
{constants.CANCEL_SUBSCRIPTION}
|
{constants.CANCEL_SUBSCRIPTION}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
)}
|
)}
|
||||||
|
@ -311,10 +329,9 @@ function PlanSelector(props: Props) {
|
||||||
onClick={updatePaymentMethod.bind(
|
onClick={updatePaymentMethod.bind(
|
||||||
null,
|
null,
|
||||||
props.setDialogMessage,
|
props.setDialogMessage,
|
||||||
props.setLoading,
|
props.setLoading
|
||||||
)}
|
)}
|
||||||
style={{ marginTop: '20px' }}
|
style={{ marginTop: '20px' }}>
|
||||||
>
|
|
||||||
{constants.MANAGEMENT_PORTAL}
|
{constants.MANAGEMENT_PORTAL}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
</>
|
</>
|
||||||
|
@ -322,11 +339,13 @@ function PlanSelector(props: Props) {
|
||||||
<LinkButton
|
<LinkButton
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={props.closeModal}
|
onClick={props.closeModal}
|
||||||
style={{ color: 'rgb(121, 121, 121)', marginTop: '20px' }}
|
style={{
|
||||||
>
|
color: 'rgb(121, 121, 121)',
|
||||||
{isOnFreePlan(subscription) ?
|
marginTop: '20px',
|
||||||
constants.SKIP :
|
}}>
|
||||||
constants.CLOSE}
|
{isOnFreePlan(subscription)
|
||||||
|
? constants.SKIP
|
||||||
|
: constants.CLOSE}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
)}
|
)}
|
||||||
</DeadCenter>
|
</DeadCenter>
|
||||||
|
|
|
@ -25,7 +25,7 @@ const Check = styled.input`
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@media(pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,8 +183,7 @@ export default function PreviewCard(props: IProps) {
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
disabled={!forcedEnable && !file?.msrc && !imgSrc}
|
disabled={!forcedEnable && !file?.msrc && !imgSrc}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
{...(selectable ? useLongPress(longPressCallback, 500) : {})}
|
{...(selectable ? useLongPress(longPressCallback, 500) : {})}>
|
||||||
>
|
|
||||||
{selectable && (
|
{selectable && (
|
||||||
<Check
|
<Check
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
|
@ -7,7 +7,7 @@ import DeleteIcon from 'components/icons/DeleteIcon';
|
||||||
import CrossIcon from 'components/icons/CrossIcon';
|
import CrossIcon from 'components/icons/CrossIcon';
|
||||||
import AddIcon from 'components/icons/AddIcon';
|
import AddIcon from 'components/icons/AddIcon';
|
||||||
import { IconButton } from 'components/Container';
|
import { IconButton } from 'components/Container';
|
||||||
import constants from 'utils/strings/englishConstants';
|
import constants from 'utils/strings/constants';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
addToCollectionHelper: (collectionName, collection) => void;
|
addToCollectionHelper: (collectionName, collection) => void;
|
||||||
|
@ -42,32 +42,42 @@ const SelectedFileOptions = ({
|
||||||
count,
|
count,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const addToCollection = () => setCollectionSelectorAttributes({
|
const addToCollection = () =>
|
||||||
callback: (collection) => addToCollectionHelper(null, collection),
|
setCollectionSelectorAttributes({
|
||||||
showNextModal: showCreateCollectionModal,
|
callback: (collection) => addToCollectionHelper(null, collection),
|
||||||
title: constants.ADD_TO_COLLECTION,
|
showNextModal: showCreateCollectionModal,
|
||||||
});
|
title: constants.ADD_TO_COLLECTION,
|
||||||
|
});
|
||||||
|
|
||||||
const deleteHandler = () => setDialogMessage({
|
const deleteHandler = () =>
|
||||||
title: constants.CONFIRM_DELETE_FILE,
|
setDialogMessage({
|
||||||
content: constants.DELETE_FILE_MESSAGE,
|
title: constants.CONFIRM_DELETE_FILE,
|
||||||
staticBackdrop: true,
|
content: constants.DELETE_FILE_MESSAGE,
|
||||||
proceed: {
|
staticBackdrop: true,
|
||||||
action: deleteFileHelper,
|
proceed: {
|
||||||
text: constants.DELETE,
|
action: deleteFileHelper,
|
||||||
variant: 'danger',
|
text: constants.DELETE,
|
||||||
},
|
variant: 'danger',
|
||||||
close: { text: constants.CANCEL },
|
},
|
||||||
});
|
close: { text: constants.CANCEL },
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectionBar>
|
<SelectionBar>
|
||||||
<SelectionContainer>
|
<SelectionContainer>
|
||||||
<IconButton onClick={clearSelection}><CrossIcon /></IconButton>
|
<IconButton onClick={clearSelection}>
|
||||||
<div>{count} {constants.SELECTED}</div>
|
<CrossIcon />
|
||||||
|
</IconButton>
|
||||||
|
<div>
|
||||||
|
{count} {constants.SELECTED}
|
||||||
|
</div>
|
||||||
</SelectionContainer>
|
</SelectionContainer>
|
||||||
<IconButton onClick={addToCollection}><AddIcon /></IconButton>
|
<IconButton onClick={addToCollection}>
|
||||||
<IconButton onClick={deleteHandler}><DeleteIcon /></IconButton>
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onClick={deleteHandler}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
</SelectionBar>
|
</SelectionBar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import UploadService, { FileWithCollection, UPLOAD_STAGES } from 'services/uploadService';
|
import UploadService, {
|
||||||
|
FileWithCollection,
|
||||||
|
UPLOAD_STAGES,
|
||||||
|
} from 'services/uploadService';
|
||||||
import { createAlbum } from 'services/collectionService';
|
import { createAlbum } from 'services/collectionService';
|
||||||
import { getLocalFiles } from 'services/fileService';
|
import { getLocalFiles } from 'services/fileService';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
@ -13,6 +16,7 @@ import { SetFiles, SetLoading } from 'pages/gallery';
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext } from 'pages/_app';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { FileRejection } from 'react-dropzone';
|
import { FileRejection } from 'react-dropzone';
|
||||||
|
import { METADATA_FOLDER_NAME } from 'services/exportService';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
|
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
|
||||||
|
@ -25,8 +29,8 @@ interface Props {
|
||||||
setDialogMessage: SetDialogMessage;
|
setDialogMessage: SetDialogMessage;
|
||||||
setUploadInProgress: any;
|
setUploadInProgress: any;
|
||||||
showCollectionSelector: () => void;
|
showCollectionSelector: () => void;
|
||||||
fileRejections:FileRejection[];
|
fileRejections: FileRejection[];
|
||||||
setFiles:SetFiles;
|
setFiles: SetFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UPLOAD_STRATEGY {
|
export enum UPLOAD_STRATEGY {
|
||||||
|
@ -38,20 +42,26 @@ interface AnalysisResult {
|
||||||
suggestedCollectionName: string;
|
suggestedCollectionName: string;
|
||||||
multipleFolders: boolean;
|
multipleFolders: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Upload(props: Props) {
|
export default function Upload(props: Props) {
|
||||||
const [progressView, setProgressView] = useState(false);
|
const [progressView, setProgressView] = useState(false);
|
||||||
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(
|
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(
|
||||||
UPLOAD_STAGES.START,
|
UPLOAD_STAGES.START
|
||||||
);
|
);
|
||||||
const [fileCounter, setFileCounter] = useState({ current: 0, total: 0 });
|
const [fileCounter, setFileCounter] = useState({ current: 0, total: 0 });
|
||||||
const [fileProgress, setFileProgress] = useState(new Map<string, number>());
|
const [fileProgress, setFileProgress] = useState(new Map<string, number>());
|
||||||
|
const [uploadResult, setUploadResult] = useState(new Map<string, number>());
|
||||||
const [percentComplete, setPercentComplete] = useState(0);
|
const [percentComplete, setPercentComplete] = useState(0);
|
||||||
const [choiceModalView, setChoiceModalView] = useState(false);
|
const [choiceModalView, setChoiceModalView] = useState(false);
|
||||||
const [fileAnalysisResult, setFileAnalysisResult] = useState<AnalysisResult>(null);
|
const [fileAnalysisResult, setFileAnalysisResult] =
|
||||||
|
useState<AnalysisResult>(null);
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.acceptedFiles?.length > 0 || appContext.sharedFiles?.length > 0) {
|
if (
|
||||||
|
props.acceptedFiles?.length > 0 ||
|
||||||
|
appContext.sharedFiles?.length > 0
|
||||||
|
) {
|
||||||
props.setLoading(true);
|
props.setLoading(true);
|
||||||
|
|
||||||
let fileAnalysisResult;
|
let fileAnalysisResult;
|
||||||
|
@ -77,6 +87,7 @@ export default function Upload(props: Props) {
|
||||||
setUploadStage(UPLOAD_STAGES.START);
|
setUploadStage(UPLOAD_STAGES.START);
|
||||||
setFileCounter({ current: 0, total: 0 });
|
setFileCounter({ current: 0, total: 0 });
|
||||||
setFileProgress(new Map<string, number>());
|
setFileProgress(new Map<string, number>());
|
||||||
|
setUploadResult(new Map<string, number>());
|
||||||
setPercentComplete(0);
|
setPercentComplete(0);
|
||||||
setProgressView(true);
|
setProgressView(true);
|
||||||
};
|
};
|
||||||
|
@ -89,16 +100,16 @@ export default function Upload(props: Props) {
|
||||||
props.closeCollectionSelector();
|
props.closeCollectionSelector();
|
||||||
await uploadFilesToNewCollections(
|
await uploadFilesToNewCollections(
|
||||||
UPLOAD_STRATEGY.SINGLE_COLLECTION,
|
UPLOAD_STRATEGY.SINGLE_COLLECTION,
|
||||||
collectionName,
|
collectionName
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextModal = (fileAnalysisResult: AnalysisResult) => {
|
const nextModal = (fileAnalysisResult: AnalysisResult) => {
|
||||||
fileAnalysisResult?.multipleFolders ?
|
fileAnalysisResult?.multipleFolders
|
||||||
setChoiceModalView(true) :
|
? setChoiceModalView(true)
|
||||||
showCreateCollectionModal(fileAnalysisResult);
|
: showCreateCollectionModal(fileAnalysisResult);
|
||||||
};
|
};
|
||||||
|
|
||||||
function analyseUploadFiles(): AnalysisResult {
|
function analyseUploadFiles(): AnalysisResult {
|
||||||
|
@ -118,7 +129,7 @@ export default function Upload(props: Props) {
|
||||||
if (commonPathPrefix) {
|
if (commonPathPrefix) {
|
||||||
commonPathPrefix = commonPathPrefix.substr(
|
commonPathPrefix = commonPathPrefix.substr(
|
||||||
1,
|
1,
|
||||||
commonPathPrefix.lastIndexOf('/') - 1,
|
commonPathPrefix.lastIndexOf('/') - 1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
@ -129,10 +140,14 @@ export default function Upload(props: Props) {
|
||||||
function getCollectionWiseFiles() {
|
function getCollectionWiseFiles() {
|
||||||
const collectionWiseFiles = new Map<string, globalThis.File[]>();
|
const collectionWiseFiles = new Map<string, globalThis.File[]>();
|
||||||
for (const file of props.acceptedFiles) {
|
for (const file of props.acceptedFiles) {
|
||||||
const filePath = file['path'];
|
const filePath = file['path'] as string;
|
||||||
const folderPath = filePath.substr(0, filePath.lastIndexOf('/'));
|
|
||||||
|
let folderPath = filePath.substr(0, filePath.lastIndexOf('/'));
|
||||||
|
if (folderPath.endsWith(METADATA_FOLDER_NAME)) {
|
||||||
|
folderPath = folderPath.substr(0, folderPath.lastIndexOf('/'));
|
||||||
|
}
|
||||||
const folderName = folderPath.substr(
|
const folderName = folderPath.substr(
|
||||||
folderPath.lastIndexOf('/') + 1,
|
folderPath.lastIndexOf('/') + 1
|
||||||
);
|
);
|
||||||
if (!collectionWiseFiles.has(folderName)) {
|
if (!collectionWiseFiles.has(folderName)) {
|
||||||
collectionWiseFiles.set(folderName, []);
|
collectionWiseFiles.set(folderName, []);
|
||||||
|
@ -145,10 +160,11 @@ export default function Upload(props: Props) {
|
||||||
const uploadFilesToExistingCollection = async (collection) => {
|
const uploadFilesToExistingCollection = async (collection) => {
|
||||||
try {
|
try {
|
||||||
uploadInit();
|
uploadInit();
|
||||||
const filesWithCollectionToUpload: FileWithCollection[] = props.acceptedFiles.map((file) => ({
|
const filesWithCollectionToUpload: FileWithCollection[] =
|
||||||
file,
|
props.acceptedFiles.map((file) => ({
|
||||||
collection,
|
file,
|
||||||
}));
|
collection,
|
||||||
|
}));
|
||||||
await uploadFiles(filesWithCollectionToUpload);
|
await uploadFiles(filesWithCollectionToUpload);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'Failed to upload files to existing collections');
|
logError(e, 'Failed to upload files to existing collections');
|
||||||
|
@ -157,7 +173,7 @@ export default function Upload(props: Props) {
|
||||||
|
|
||||||
const uploadFilesToNewCollections = async (
|
const uploadFilesToNewCollections = async (
|
||||||
strategy: UPLOAD_STRATEGY,
|
strategy: UPLOAD_STRATEGY,
|
||||||
collectionName,
|
collectionName
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
uploadInit();
|
uploadInit();
|
||||||
|
@ -194,13 +210,13 @@ export default function Upload(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadFiles = async (
|
const uploadFiles = async (
|
||||||
filesWithCollectionToUpload: FileWithCollection[],
|
filesWithCollectionToUpload: FileWithCollection[]
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
props.setUploadInProgress(true);
|
props.setUploadInProgress(true);
|
||||||
props.closeCollectionSelector();
|
props.closeCollectionSelector();
|
||||||
await props.syncWithRemote(true, true);
|
await props.syncWithRemote(true, true);
|
||||||
const localFiles= await getLocalFiles();
|
const localFiles = await getLocalFiles();
|
||||||
await UploadService.uploadFiles(
|
await UploadService.uploadFiles(
|
||||||
filesWithCollectionToUpload,
|
filesWithCollectionToUpload,
|
||||||
localFiles,
|
localFiles,
|
||||||
|
@ -209,8 +225,9 @@ export default function Upload(props: Props) {
|
||||||
setFileCounter,
|
setFileCounter,
|
||||||
setUploadStage,
|
setUploadStage,
|
||||||
setFileProgress,
|
setFileProgress,
|
||||||
|
setUploadResult,
|
||||||
},
|
},
|
||||||
props.setFiles,
|
props.setFiles
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
props.setBannerMessage(err.message);
|
props.setBannerMessage(err.message);
|
||||||
|
@ -222,13 +239,12 @@ export default function Upload(props: Props) {
|
||||||
props.syncWithRemote();
|
props.syncWithRemote();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const retryFailed = async (
|
const retryFailed = async () => {
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
props.setUploadInProgress(true);
|
props.setUploadInProgress(true);
|
||||||
uploadInit();
|
uploadInit();
|
||||||
await props.syncWithRemote(true, true);
|
await props.syncWithRemote(true, true);
|
||||||
const localFiles= await getLocalFiles();
|
const localFiles = await getLocalFiles();
|
||||||
await UploadService.retryFailedFiles(localFiles);
|
await UploadService.retryFailedFiles(localFiles);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
props.setBannerMessage(err.message);
|
props.setBannerMessage(err.message);
|
||||||
|
@ -240,14 +256,15 @@ export default function Upload(props: Props) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ChoiceModal
|
<ChoiceModal
|
||||||
show={choiceModalView}
|
show={choiceModalView}
|
||||||
onHide={() => setChoiceModalView(false)}
|
onHide={() => setChoiceModalView(false)}
|
||||||
uploadFiles={uploadFilesToNewCollections}
|
uploadFiles={uploadFilesToNewCollections}
|
||||||
showCollectionCreateModal={() => showCreateCollectionModal(fileAnalysisResult)}
|
showCollectionCreateModal={() =>
|
||||||
|
showCreateCollectionModal(fileAnalysisResult)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<UploadProgress
|
<UploadProgress
|
||||||
now={percentComplete}
|
now={percentComplete}
|
||||||
|
@ -258,6 +275,7 @@ export default function Upload(props: Props) {
|
||||||
closeModal={() => setProgressView(false)}
|
closeModal={() => setProgressView(false)}
|
||||||
retryFailed={retryFailed}
|
retryFailed={retryFailed}
|
||||||
fileRejections={props.fileRejections}
|
fileRejections={props.fileRejections}
|
||||||
|
uploadResult={uploadResult}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,8 +21,7 @@ function UploadButton({ openFileUploader, isFirstFetch }) {
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="green"
|
fill="green"
|
||||||
width="32px"
|
width="32px"
|
||||||
height="32px"
|
height="32px">
|
||||||
>
|
|
||||||
<path fill="none" d="M0 0h24v24H0z" />
|
<path fill="none" d="M0 0h24v24H0z" />
|
||||||
<path
|
<path
|
||||||
fill="#2dc262"
|
fill="#2dc262"
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import React from 'react';
|
import ExpandLess from 'components/icons/ExpandLess';
|
||||||
import {
|
import ExpandMore from 'components/icons/ExpandMore';
|
||||||
Alert, Button, Modal, ProgressBar,
|
import React, { useState } from 'react';
|
||||||
} from 'react-bootstrap';
|
import { Button, Modal, ProgressBar } from 'react-bootstrap';
|
||||||
import { FileRejection } from 'react-dropzone';
|
import { FileRejection } from 'react-dropzone';
|
||||||
import { UPLOAD_STAGES, FileUploadErrorCode } from 'services/uploadService';
|
import { FileUploadResults, UPLOAD_STAGES } from 'services/uploadService';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { DESKTOP_APP_DOWNLOAD_URL } from 'utils/common';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
import AlertBanner from './AlertBanner';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
fileCounter;
|
fileCounter;
|
||||||
|
@ -14,106 +17,222 @@ interface Props {
|
||||||
retryFailed;
|
retryFailed;
|
||||||
fileProgress: Map<string, number>;
|
fileProgress: Map<string, number>;
|
||||||
show;
|
show;
|
||||||
fileRejections:FileRejection[]
|
fileRejections: FileRejection[];
|
||||||
|
uploadResult: Map<string, number>;
|
||||||
}
|
}
|
||||||
interface FileProgressStatuses{
|
interface FileProgresses {
|
||||||
fileName:string;
|
fileName: string;
|
||||||
progress:number;
|
progress: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Content = styled.div<{
|
||||||
|
collapsed: boolean;
|
||||||
|
sm?: boolean;
|
||||||
|
height?: number;
|
||||||
|
}>`
|
||||||
|
overflow: hidden;
|
||||||
|
height: ${(props) => (props.collapsed ? '0px' : props.height + 'px')};
|
||||||
|
transition: ${(props) => 'height ' + 0.001 * props.height + 's ease-out'};
|
||||||
|
margin-bottom: 20px;
|
||||||
|
& > p {
|
||||||
|
padding-left: 35px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const FileList = styled.ul`
|
||||||
|
padding-left: 50px;
|
||||||
|
margin-top: 5px;
|
||||||
|
& > li {
|
||||||
|
padding-left: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SectionTitle = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
|
color: #eee;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface ResultSectionProps {
|
||||||
|
fileUploadResultMap: Map<FileUploadResults, string[]>;
|
||||||
|
fileUploadResult: FileUploadResults;
|
||||||
|
sectionTitle;
|
||||||
|
sectionInfo;
|
||||||
|
infoHeight: number;
|
||||||
|
}
|
||||||
|
const ResultSection = (props: ResultSectionProps) => {
|
||||||
|
const [listView, setListView] = useState(false);
|
||||||
|
const fileList = props.fileUploadResultMap?.get(props.fileUploadResult);
|
||||||
|
if (!fileList?.length) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SectionTitle onClick={() => setListView(!listView)}>
|
||||||
|
{' '}
|
||||||
|
{props.sectionTitle}{' '}
|
||||||
|
{listView ? <ExpandLess /> : <ExpandMore />}
|
||||||
|
</SectionTitle>
|
||||||
|
<Content
|
||||||
|
collapsed={!listView}
|
||||||
|
height={fileList.length * 33 + props.infoHeight}>
|
||||||
|
<p>{props.sectionInfo}</p>
|
||||||
|
<FileList>
|
||||||
|
{fileList.map((fileName) => (
|
||||||
|
<li key={fileName}>{fileName}</li>
|
||||||
|
))}
|
||||||
|
</FileList>
|
||||||
|
</Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function UploadProgress(props: Props) {
|
export default function UploadProgress(props: Props) {
|
||||||
const fileProgressStatuses = [] as FileProgressStatuses[];
|
const fileProgressStatuses = [] as FileProgresses[];
|
||||||
|
const fileUploadResultMap = new Map<FileUploadResults, string[]>();
|
||||||
|
let filesNotUploaded = false;
|
||||||
|
|
||||||
if (props.fileProgress) {
|
if (props.fileProgress) {
|
||||||
for (const [fileName, progress] of props.fileProgress) {
|
for (const [fileName, progress] of props.fileProgress) {
|
||||||
fileProgressStatuses.push({ fileName, progress });
|
fileProgressStatuses.push({ fileName, progress });
|
||||||
}
|
}
|
||||||
for (const { file } of props.fileRejections) {
|
|
||||||
fileProgressStatuses.push({ fileName: file.name, progress: FileUploadErrorCode.UNSUPPORTED });
|
|
||||||
}
|
|
||||||
fileProgressStatuses.sort((a, b) => {
|
|
||||||
if (b.progress !== -1 && a.progress === -1) return 1;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
if (props.uploadResult) {
|
||||||
|
for (const [fileName, progress] of props.uploadResult) {
|
||||||
|
if (!fileUploadResultMap.has(progress)) {
|
||||||
|
fileUploadResultMap.set(progress, []);
|
||||||
|
}
|
||||||
|
if (progress < 0) {
|
||||||
|
filesNotUploaded = true;
|
||||||
|
}
|
||||||
|
const fileList = fileUploadResultMap.get(progress);
|
||||||
|
fileUploadResultMap.set(progress, [...fileList, fileName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
show={props.show}
|
show={props.show}
|
||||||
onHide={
|
onHide={
|
||||||
props.uploadStage !== UPLOAD_STAGES.FINISH ?
|
props.uploadStage !== UPLOAD_STAGES.FINISH
|
||||||
() => null :
|
? () => null
|
||||||
props.closeModal
|
: props.closeModal
|
||||||
}
|
}
|
||||||
aria-labelledby="contained-modal-title-vcenter"
|
aria-labelledby="contained-modal-title-vcenter"
|
||||||
centered
|
centered
|
||||||
backdrop={
|
backdrop={fileProgressStatuses?.length !== 0 ? 'static' : 'true'}>
|
||||||
fileProgressStatuses?.length !== 0 ? 'static' : 'true'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Modal.Header
|
<Modal.Header
|
||||||
style={{ display: 'flex', justifyContent: 'center', textAlign: 'center', borderBottom: 'none', paddingTop: '30px', paddingBottom: '0px' }}
|
style={{
|
||||||
closeButton={props.uploadStage === UPLOAD_STAGES.FINISH}
|
display: 'flex',
|
||||||
>
|
justifyContent: 'center',
|
||||||
|
textAlign: 'center',
|
||||||
|
borderBottom: 'none',
|
||||||
|
paddingTop: '30px',
|
||||||
|
paddingBottom: '0px',
|
||||||
|
}}
|
||||||
|
closeButton={props.uploadStage === UPLOAD_STAGES.FINISH}>
|
||||||
<h4 style={{ width: '100%' }}>
|
<h4 style={{ width: '100%' }}>
|
||||||
{props.uploadStage === UPLOAD_STAGES.UPLOADING ?
|
{props.uploadStage === UPLOAD_STAGES.UPLOADING
|
||||||
constants.UPLOAD[props.uploadStage](
|
? constants.UPLOAD[props.uploadStage](props.fileCounter)
|
||||||
props.fileCounter,
|
: constants.UPLOAD[props.uploadStage]}
|
||||||
) :
|
|
||||||
constants.UPLOAD[props.uploadStage]}
|
|
||||||
</h4>
|
</h4>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{props.uploadStage===UPLOAD_STAGES.FINISH ? (
|
{(props.uploadStage ===
|
||||||
fileProgressStatuses.length !== 0 && (
|
UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES ||
|
||||||
<Alert variant="warning">
|
props.uploadStage === UPLOAD_STAGES.UPLOADING) && (
|
||||||
{constants.FAILED_UPLOAD_FILE_LIST}
|
<ProgressBar
|
||||||
</Alert>
|
now={props.now}
|
||||||
)
|
animated
|
||||||
) :
|
variant="upload-progress-bar"
|
||||||
(props.uploadStage === UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES ||
|
/>
|
||||||
props.uploadStage === UPLOAD_STAGES.UPLOADING) &&
|
)}
|
||||||
(
|
{fileProgressStatuses.length > 0 && (
|
||||||
< ProgressBar
|
<FileList>
|
||||||
now={props.now}
|
|
||||||
animated
|
|
||||||
variant="upload-progress-bar"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{fileProgressStatuses?.length > 0 && (
|
|
||||||
<ul
|
|
||||||
style={{
|
|
||||||
marginTop: '10px',
|
|
||||||
overflow: 'auto',
|
|
||||||
maxHeight: '250px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fileProgressStatuses.map(({ fileName, progress }) => (
|
{fileProgressStatuses.map(({ fileName, progress }) => (
|
||||||
<li key={fileName} style={{ marginTop: '12px' }}>
|
<li key={fileName} style={{ marginTop: '12px' }}>
|
||||||
{props.uploadStage===UPLOAD_STAGES.FINISH ?
|
{props.uploadStage === UPLOAD_STAGES.FINISH
|
||||||
fileName :
|
? fileName
|
||||||
constants.FILE_UPLOAD_PROGRESS(
|
: constants.FILE_UPLOAD_PROGRESS(
|
||||||
fileName,
|
fileName,
|
||||||
progress,
|
progress
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</FileList>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ResultSection
|
||||||
|
fileUploadResultMap={fileUploadResultMap}
|
||||||
|
fileUploadResult={FileUploadResults.UPLOADED}
|
||||||
|
sectionTitle={constants.SUCCESSFUL_UPLOADS}
|
||||||
|
sectionInfo={constants.SUCCESS_INFO}
|
||||||
|
infoHeight={32}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{props.uploadStage === UPLOAD_STAGES.FINISH &&
|
||||||
|
filesNotUploaded && (
|
||||||
|
<AlertBanner variant="warning">
|
||||||
|
{constants.FILE_NOT_UPLOADED_LIST}
|
||||||
|
</AlertBanner>
|
||||||
|
)}
|
||||||
|
<ResultSection
|
||||||
|
fileUploadResultMap={fileUploadResultMap}
|
||||||
|
fileUploadResult={FileUploadResults.BLOCKED}
|
||||||
|
sectionTitle={constants.BLOCKED_UPLOADS}
|
||||||
|
sectionInfo={constants.ETAGS_BLOCKED(
|
||||||
|
DESKTOP_APP_DOWNLOAD_URL
|
||||||
|
)}
|
||||||
|
infoHeight={140}
|
||||||
|
/>
|
||||||
|
<ResultSection
|
||||||
|
fileUploadResultMap={fileUploadResultMap}
|
||||||
|
fileUploadResult={FileUploadResults.FAILED}
|
||||||
|
sectionTitle={constants.FAILED_UPLOADS}
|
||||||
|
sectionInfo={constants.FAILED_INFO}
|
||||||
|
infoHeight={48}
|
||||||
|
/>
|
||||||
|
<ResultSection
|
||||||
|
fileUploadResultMap={fileUploadResultMap}
|
||||||
|
fileUploadResult={FileUploadResults.SKIPPED}
|
||||||
|
sectionTitle={constants.SKIPPED_FILES}
|
||||||
|
sectionInfo={constants.SKIPPED_INFO}
|
||||||
|
infoHeight={32}
|
||||||
|
/>
|
||||||
|
<ResultSection
|
||||||
|
fileUploadResultMap={fileUploadResultMap}
|
||||||
|
fileUploadResult={FileUploadResults.UNSUPPORTED}
|
||||||
|
sectionTitle={constants.UNSUPPORTED_FILES}
|
||||||
|
sectionInfo={constants.UNSUPPORTED_INFO}
|
||||||
|
infoHeight={32}
|
||||||
|
/>
|
||||||
|
|
||||||
{props.uploadStage === UPLOAD_STAGES.FINISH && (
|
{props.uploadStage === UPLOAD_STAGES.FINISH && (
|
||||||
<Modal.Footer style={{ border: 'none' }}>
|
<Modal.Footer style={{ border: 'none' }}>
|
||||||
{props.uploadStage===UPLOAD_STAGES.FINISH && (fileProgressStatuses?.length === 0 ? (
|
{props.uploadStage === UPLOAD_STAGES.FINISH &&
|
||||||
<Button
|
(fileUploadResultMap?.get(FileUploadResults.FAILED)
|
||||||
variant="outline-secondary"
|
?.length > 0 ||
|
||||||
style={{ width: '100%' }}
|
fileUploadResultMap?.get(FileUploadResults.BLOCKED)
|
||||||
onClick={props.closeModal}
|
?.length > 0 ? (
|
||||||
>
|
<Button
|
||||||
{constants.CLOSE}
|
variant="outline-success"
|
||||||
</Button>) : (
|
style={{ width: '100%' }}
|
||||||
<Button
|
onClick={props.retryFailed}>
|
||||||
variant="outline-success"
|
{constants.RETRY_FAILED}
|
||||||
style={{ width: '100%' }}
|
</Button>
|
||||||
onClick={props.retryFailed}
|
) : (
|
||||||
>
|
<Button
|
||||||
{constants.RETRY}
|
variant="outline-secondary"
|
||||||
</Button>))}
|
style={{ width: '100%' }}
|
||||||
|
onClick={props.closeModal}>
|
||||||
|
{constants.CLOSE}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
)}
|
)}
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
|
|
|
@ -128,14 +128,21 @@ const GlobalStyles = createGlobalStyle`
|
||||||
.modal-content {
|
.modal-content {
|
||||||
border-radius:15px;
|
border-radius:15px;
|
||||||
background-color:#202020 !important;
|
background-color:#202020 !important;
|
||||||
color:#aaa;
|
|
||||||
}
|
}
|
||||||
.modal-dialog{
|
.modal-dialog{
|
||||||
margin:5% auto;
|
margin:5% auto;
|
||||||
width:90%;
|
width:90%;
|
||||||
}
|
}
|
||||||
|
.modal-body{
|
||||||
|
max-height:80vh;
|
||||||
|
overflow:auto;
|
||||||
|
}
|
||||||
.modal-xl{
|
.modal-xl{
|
||||||
max-width:960px!important;
|
max-width:90% !important;
|
||||||
|
}
|
||||||
|
.plan-selector-modal-content {
|
||||||
|
width:auto;
|
||||||
|
margin:auto;
|
||||||
}
|
}
|
||||||
.pswp-custom {
|
.pswp-custom {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
|
@ -390,29 +397,35 @@ export interface BannerMessage {
|
||||||
variant: string;
|
variant: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type AppContextType = {
|
type AppContextType = {
|
||||||
showNavBar: (show: boolean) => void;
|
showNavBar: (show: boolean) => void;
|
||||||
sharedFiles: File[];
|
sharedFiles: File[];
|
||||||
resetSharedFiles: () => void;
|
resetSharedFiles: () => void;
|
||||||
setDisappearingFlashMessage: (message: FlashMessage) => void;
|
setDisappearingFlashMessage: (message: FlashMessage) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export enum FLASH_MESSAGE_TYPE {
|
||||||
|
DANGER = 'danger',
|
||||||
|
INFO = 'info',
|
||||||
|
SUCCESS = 'success',
|
||||||
|
WARNING = 'warning',
|
||||||
|
}
|
||||||
export interface FlashMessage {
|
export interface FlashMessage {
|
||||||
message: string;
|
message: string;
|
||||||
severity: string
|
type: FLASH_MESSAGE_TYPE;
|
||||||
}
|
}
|
||||||
export const AppContext = createContext<AppContextType>(null);
|
export const AppContext = createContext<AppContextType>(null);
|
||||||
|
|
||||||
const redirectMap = {
|
const redirectMap = {
|
||||||
roadmap: (token: string) => `${getEndpoint()}/users/roadmap?token=${encodeURIComponent(token)}`,
|
roadmap: (token: string) =>
|
||||||
|
`${getEndpoint()}/users/roadmap?token=${encodeURIComponent(token)}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function App({ Component, err }) {
|
export default function App({ Component, err }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [offline, setOffline] = useState(
|
const [offline, setOffline] = useState(
|
||||||
typeof window !== 'undefined' && !window.navigator.onLine,
|
typeof window !== 'undefined' && !window.navigator.onLine
|
||||||
);
|
);
|
||||||
const [showNavbar, setShowNavBar] = useState(false);
|
const [showNavbar, setShowNavBar] = useState(false);
|
||||||
const [sharedFiles, setSharedFiles] = useState<File[]>(null);
|
const [sharedFiles, setSharedFiles] = useState<File[]>(null);
|
||||||
|
@ -444,7 +457,7 @@ export default function App({ Component, err }) {
|
||||||
(error) => {
|
(error) => {
|
||||||
logError(error);
|
logError(error);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -455,7 +468,7 @@ export default function App({ Component, err }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(
|
console.log(
|
||||||
`%c${constants.CONSOLE_WARNING_STOP}`,
|
`%c${constants.CONSOLE_WARNING_STOP}`,
|
||||||
'color: red; font-size: 52px;',
|
'color: red; font-size: 52px;'
|
||||||
);
|
);
|
||||||
console.log(`%c${constants.CONSOLE_WARNING_DESC}`, 'font-size: 20px;');
|
console.log(`%c${constants.CONSOLE_WARNING_DESC}`, 'font-size: 20px;');
|
||||||
|
|
||||||
|
@ -479,7 +492,9 @@ export default function App({ Component, err }) {
|
||||||
if (redirectName) {
|
if (redirectName) {
|
||||||
const user = getData(LS_KEYS.USER);
|
const user = getData(LS_KEYS.USER);
|
||||||
if (user?.token) {
|
if (user?.token) {
|
||||||
window.location.href = redirectMap[redirectName](user.token);
|
window.location.href = redirectMap[redirectName](
|
||||||
|
user.token
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -506,24 +521,27 @@ export default function App({ Component, err }) {
|
||||||
<Head>
|
<Head>
|
||||||
<title>{constants.TITLE}</title>
|
<title>{constants.TITLE}</title>
|
||||||
{/* Cloudflare Web Analytics */}
|
{/* Cloudflare Web Analytics */}
|
||||||
{pageRootURL?.hostname && (pageRootURL.hostname === 'photos.ente.io' ?
|
{pageRootURL?.hostname &&
|
||||||
<script
|
(pageRootURL.hostname === 'photos.ente.io' ? (
|
||||||
defer
|
<script
|
||||||
src="https://static.cloudflareinsights.com/beacon.min.js"
|
|
||||||
data-cf-beacon='{"token": "6a388287b59c439cb2070f78cc89dde1"}'
|
|
||||||
/> : pageRootURL.hostname === 'web.ente.io' ?
|
|
||||||
< script
|
|
||||||
defer
|
defer
|
||||||
src='https://static.cloudflareinsights.com/beacon.min.js'
|
src="https://static.cloudflareinsights.com/beacon.min.js"
|
||||||
data-cf-beacon='{"token": "dfde128b7bb34a618ad34a08f1ba7609"}' /> :
|
data-cf-beacon='{"token": "6a388287b59c439cb2070f78cc89dde1"}'
|
||||||
|
/>
|
||||||
|
) : pageRootURL.hostname === 'web.ente.io' ? (
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
src="https://static.cloudflareinsights.com/beacon.min.js"
|
||||||
|
data-cf-beacon='{"token": "dfde128b7bb34a618ad34a08f1ba7609"}'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
console.warn('Web analytics is disabled')
|
console.warn('Web analytics is disabled')
|
||||||
)
|
))}
|
||||||
}
|
|
||||||
{/* End Cloudflare Web Analytics */}
|
{/* End Cloudflare Web Analytics */}
|
||||||
</Head>
|
</Head>
|
||||||
<GlobalStyles />
|
<GlobalStyles />
|
||||||
{
|
{showNavbar && (
|
||||||
showNavbar && <Navbar>
|
<Navbar>
|
||||||
<FlexContainer>
|
<FlexContainer>
|
||||||
<LogoImage
|
<LogoImage
|
||||||
style={{ height: '24px', padding: '3px' }}
|
style={{ height: '24px', padding: '3px' }}
|
||||||
|
@ -532,21 +550,33 @@ export default function App({ Component, err }) {
|
||||||
/>
|
/>
|
||||||
</FlexContainer>
|
</FlexContainer>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
}
|
)}
|
||||||
<MessageContainer>{offline && constants.OFFLINE_MSG}</MessageContainer>
|
<MessageContainer>
|
||||||
{
|
{offline && constants.OFFLINE_MSG}
|
||||||
sharedFiles &&
|
</MessageContainer>
|
||||||
(router.pathname === '/gallery' ?
|
{sharedFiles &&
|
||||||
<MessageContainer>{constants.FILES_TO_BE_UPLOADED(sharedFiles.length)}</MessageContainer> :
|
(router.pathname === '/gallery' ? (
|
||||||
<MessageContainer>{constants.LOGIN_TO_UPLOAD_FILES(sharedFiles.length)}</MessageContainer>)
|
<MessageContainer>
|
||||||
}
|
{constants.FILES_TO_BE_UPLOADED(sharedFiles.length)}
|
||||||
{flashMessage && <FlashMessageBar flashMessage={flashMessage} onClose={() => setFlashMessage(null)} />}
|
</MessageContainer>
|
||||||
<AppContext.Provider value={{
|
) : (
|
||||||
showNavBar,
|
<MessageContainer>
|
||||||
sharedFiles,
|
{constants.LOGIN_TO_UPLOAD_FILES(sharedFiles.length)}
|
||||||
resetSharedFiles,
|
</MessageContainer>
|
||||||
setDisappearingFlashMessage,
|
))}
|
||||||
}}>
|
{flashMessage && (
|
||||||
|
<FlashMessageBar
|
||||||
|
flashMessage={flashMessage}
|
||||||
|
onClose={() => setFlashMessage(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<AppContext.Provider
|
||||||
|
value={{
|
||||||
|
showNavBar,
|
||||||
|
sharedFiles,
|
||||||
|
resetSharedFiles,
|
||||||
|
setDisappearingFlashMessage,
|
||||||
|
}}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Container>
|
<Container>
|
||||||
<EnteSpinner>
|
<EnteSpinner>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Document, {
|
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||||
Html, Head, Main, NextScript,
|
|
||||||
} from 'next/document';
|
|
||||||
import { ServerStyleSheet } from 'styled-components';
|
import { ServerStyleSheet } from 'styled-components';
|
||||||
|
|
||||||
export default class MyDocument extends Document {
|
export default class MyDocument extends Document {
|
||||||
|
@ -10,9 +8,11 @@ export default class MyDocument extends Document {
|
||||||
const originalRenderPage = ctx.renderPage;
|
const originalRenderPage = ctx.renderPage;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ctx.renderPage = () => originalRenderPage({
|
ctx.renderPage = () =>
|
||||||
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />),
|
originalRenderPage({
|
||||||
});
|
enhanceApp: (App) => (props) =>
|
||||||
|
sheet.collectStyles(<App {...props} />),
|
||||||
|
});
|
||||||
|
|
||||||
const initialProps = await Document.getInitialProps(ctx);
|
const initialProps = await Document.getInitialProps(ctx);
|
||||||
return {
|
return {
|
||||||
|
@ -37,13 +37,24 @@ export default class MyDocument extends Document {
|
||||||
name="description"
|
name="description"
|
||||||
content="ente is a privacy focussed photo storage service that offers end-to-end encryption."
|
content="ente is a privacy focussed photo storage service that offers end-to-end encryption."
|
||||||
/>
|
/>
|
||||||
<link rel="icon" href="/images/favicon.png" type="image/png" />
|
<link
|
||||||
|
rel="icon"
|
||||||
|
href="/images/favicon.png"
|
||||||
|
type="image/png"
|
||||||
|
/>
|
||||||
<link rel="manifest" href="manifest.json" />
|
<link rel="manifest" href="manifest.json" />
|
||||||
<link rel="apple-touch-icon" href="/images/ente-512.png" />
|
<link rel="apple-touch-icon" href="/images/ente-512.png" />
|
||||||
<meta name="theme-color" content="#111" />
|
<meta name="theme-color" content="#111" />
|
||||||
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
href="/images/favicon.png"
|
||||||
|
/>
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
<meta
|
||||||
|
name="apple-mobile-web-app-status-bar-style"
|
||||||
|
content="black"
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
|
|
|
@ -6,7 +6,8 @@ export const config = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const API_ENDPOINT = process.env.NEXT_PUBLIC_ENTE_ENDPOINT || 'https://api.staging.ente.io';
|
const API_ENDPOINT =
|
||||||
|
process.env.NEXT_PUBLIC_ENTE_ENDPOINT || 'https://api.staging.ente.io';
|
||||||
|
|
||||||
export default createProxyMiddleware({
|
export default createProxyMiddleware({
|
||||||
target: API_ENDPOINT,
|
target: API_ENDPOINT,
|
||||||
|
|
63
src/pages/change-email/index.tsx
Normal file
63
src/pages/change-email/index.tsx
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import Container from 'components/Container';
|
||||||
|
import LogoImg from 'components/LogoImg';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Alert, Card } from 'react-bootstrap';
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
import router from 'next/router';
|
||||||
|
import { getToken } from 'utils/common/key';
|
||||||
|
import EnteSpinner from 'components/EnteSpinner';
|
||||||
|
import ChangeEmailForm from 'components/ChangeEmail';
|
||||||
|
import EnteCard from 'components/EnteCard';
|
||||||
|
|
||||||
|
function ChangeEmailPage() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [waiting, setWaiting] = useState(true);
|
||||||
|
const [showMessage, setShowMessage] = useState(false);
|
||||||
|
const [showBigDialog, setShowBigDialog] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
router.push('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setWaiting(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
{waiting ? (
|
||||||
|
<EnteSpinner>
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</EnteSpinner>
|
||||||
|
) : (
|
||||||
|
<EnteCard size={showBigDialog ? 'md' : 'sm'}>
|
||||||
|
<Card.Body style={{ padding: '40px 30px' }}>
|
||||||
|
<Card.Title style={{ marginBottom: '32px' }}>
|
||||||
|
<LogoImg src="/icon.svg" />
|
||||||
|
{constants.UPDATE_EMAIL}
|
||||||
|
</Card.Title>
|
||||||
|
<Alert
|
||||||
|
variant="success"
|
||||||
|
show={showMessage}
|
||||||
|
style={{ paddingBottom: 0 }}
|
||||||
|
transition
|
||||||
|
dismissible
|
||||||
|
onClose={() => setShowMessage(false)}>
|
||||||
|
{constants.EMAIL_SENT({ email })}
|
||||||
|
</Alert>
|
||||||
|
<ChangeEmailForm
|
||||||
|
showMessage={(value) => {
|
||||||
|
setShowMessage(value);
|
||||||
|
setShowBigDialog(value);
|
||||||
|
}}
|
||||||
|
setEmail={setEmail}
|
||||||
|
/>
|
||||||
|
</Card.Body>
|
||||||
|
</EnteCard>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChangeEmailPage;
|
|
@ -2,10 +2,10 @@ import React, { useState, useEffect, useContext } from 'react';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { B64EncryptionResult } from 'services/uploadService';
|
|
||||||
import CryptoWorker, {
|
import CryptoWorker, {
|
||||||
setSessionKeys,
|
setSessionKeys,
|
||||||
generateAndSaveIntermediateKeyAttributes,
|
generateAndSaveIntermediateKeyAttributes,
|
||||||
|
B64EncryptionResult,
|
||||||
} from 'utils/crypto';
|
} from 'utils/crypto';
|
||||||
import { getActualKey } from 'utils/common/key';
|
import { getActualKey } from 'utils/common/key';
|
||||||
import { setKeys, UpdatedKey } from 'services/userService';
|
import { setKeys, UpdatedKey } from 'services/userService';
|
||||||
|
@ -45,7 +45,8 @@ export default function Generate() {
|
||||||
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
|
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const encryptedKeyAttributes: B64EncryptionResult = await cryptoWorker.encryptToB64(key, kek.key);
|
const encryptedKeyAttributes: B64EncryptionResult =
|
||||||
|
await cryptoWorker.encryptToB64(key, kek.key);
|
||||||
const updatedKey: UpdatedKey = {
|
const updatedKey: UpdatedKey = {
|
||||||
kekSalt,
|
kekSalt,
|
||||||
encryptedKey: encryptedKeyAttributes.encryptedData,
|
encryptedKey: encryptedKeyAttributes.encryptedData,
|
||||||
|
@ -60,7 +61,7 @@ export default function Generate() {
|
||||||
await generateAndSaveIntermediateKeyAttributes(
|
await generateAndSaveIntermediateKeyAttributes(
|
||||||
passphrase,
|
passphrase,
|
||||||
updatedKeyAttributes,
|
updatedKeyAttributes,
|
||||||
key,
|
key
|
||||||
);
|
);
|
||||||
|
|
||||||
setSessionKeys(key);
|
setSessionKeys(key);
|
||||||
|
@ -75,9 +76,9 @@ export default function Generate() {
|
||||||
callback={onSubmit}
|
callback={onSubmit}
|
||||||
buttonText={constants.CHANGE_PASSWORD}
|
buttonText={constants.CHANGE_PASSWORD}
|
||||||
back={
|
back={
|
||||||
getData(LS_KEYS.SHOW_BACK_BUTTON)?.value ?
|
getData(LS_KEYS.SHOW_BACK_BUTTON)?.value
|
||||||
redirectToGallery :
|
? redirectToGallery
|
||||||
null
|
: null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -29,7 +29,10 @@ export default function Credentials() {
|
||||||
const user = getData(LS_KEYS.USER);
|
const user = getData(LS_KEYS.USER);
|
||||||
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
||||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||||
if ((!user?.token && !user?.encryptedToken) || !keyAttributes?.memLimit) {
|
if (
|
||||||
|
(!user?.token && !user?.encryptedToken) ||
|
||||||
|
!keyAttributes?.memLimit
|
||||||
|
) {
|
||||||
clearData();
|
clearData();
|
||||||
router.push('/');
|
router.push('/');
|
||||||
} else if (!keyAttributes) {
|
} else if (!keyAttributes) {
|
||||||
|
@ -51,7 +54,7 @@ export default function Credentials() {
|
||||||
passphrase,
|
passphrase,
|
||||||
keyAttributes.kekSalt,
|
keyAttributes.kekSalt,
|
||||||
keyAttributes.opsLimit,
|
keyAttributes.opsLimit,
|
||||||
keyAttributes.memLimit,
|
keyAttributes.memLimit
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('failed to deriveKey ', e.message);
|
console.error('failed to deriveKey ', e.message);
|
||||||
|
@ -61,13 +64,13 @@ export default function Credentials() {
|
||||||
const key: string = await cryptoWorker.decryptB64(
|
const key: string = await cryptoWorker.decryptB64(
|
||||||
keyAttributes.encryptedKey,
|
keyAttributes.encryptedKey,
|
||||||
keyAttributes.keyDecryptionNonce,
|
keyAttributes.keyDecryptionNonce,
|
||||||
kek,
|
kek
|
||||||
);
|
);
|
||||||
if (isFirstLogin()) {
|
if (isFirstLogin()) {
|
||||||
await generateAndSaveIntermediateKeyAttributes(
|
await generateAndSaveIntermediateKeyAttributes(
|
||||||
passphrase,
|
passphrase,
|
||||||
keyAttributes,
|
keyAttributes,
|
||||||
key,
|
key
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await setSessionKeys(key);
|
await setSessionKeys(key);
|
||||||
|
@ -81,7 +84,7 @@ export default function Credentials() {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setFieldError(
|
setFieldError(
|
||||||
'passphrase',
|
'passphrase',
|
||||||
`${constants.UNKNOWN_ERROR} ${e.message}`,
|
`${constants.UNKNOWN_ERROR} ${e.message}`
|
||||||
);
|
);
|
||||||
console.error('failed to verifyPassphrase ', e.message);
|
console.error('failed to verifyPassphrase ', e.message);
|
||||||
}
|
}
|
||||||
|
@ -90,13 +93,10 @@ export default function Credentials() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container>
|
<Container>
|
||||||
<Card
|
<Card style={{ minWidth: '320px' }} className="text-center">
|
||||||
style={{ minWidth: '320px' }}
|
|
||||||
className="text-center"
|
|
||||||
>
|
|
||||||
<Card.Body style={{ padding: '40px 30px' }}>
|
<Card.Body style={{ padding: '40px 30px' }}>
|
||||||
<Card.Title style={{ marginBottom: '32px' }}>
|
<Card.Title style={{ marginBottom: '32px' }}>
|
||||||
<LogoImg src='/icon.svg' />
|
<LogoImg src="/icon.svg" />
|
||||||
{constants.PASSWORD}
|
{constants.PASSWORD}
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
<SingleInputForm
|
<SingleInputForm
|
||||||
|
@ -110,12 +110,10 @@ export default function Credentials() {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
marginTop: '12px',
|
marginTop: '12px',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
onClick={() => router.push('/recover')}
|
onClick={() => router.push('/recover')}>
|
||||||
>
|
|
||||||
{constants.FORGOT_PASSWORD}
|
{constants.FORGOT_PASSWORD}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="link" onClick={logoutUser}>
|
<Button variant="link" onClick={logoutUser}>
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { clearKeys, getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
import { clearKeys, getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||||
import {
|
import {
|
||||||
|
@ -39,7 +45,6 @@ import { LoadingOverlay } from 'components/LoadingOverlay';
|
||||||
import PhotoFrame from 'components/PhotoFrame';
|
import PhotoFrame from 'components/PhotoFrame';
|
||||||
import { getSelectedFileIds, sortFilesIntoCollections } from 'utils/file';
|
import { getSelectedFileIds, sortFilesIntoCollections } from 'utils/file';
|
||||||
import { addFilesToCollection } from 'utils/collection';
|
import { addFilesToCollection } from 'utils/collection';
|
||||||
import { errorCodes } from 'utils/common/errorUtil';
|
|
||||||
import SearchBar, { DateValue } from 'components/SearchBar';
|
import SearchBar, { DateValue } from 'components/SearchBar';
|
||||||
import { Bbox } from 'services/searchService';
|
import { Bbox } from 'services/searchService';
|
||||||
import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions';
|
import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions';
|
||||||
|
@ -55,12 +60,7 @@ import PlanSelector from 'components/pages/gallery/PlanSelector';
|
||||||
import Upload from 'components/pages/gallery/Upload';
|
import Upload from 'components/pages/gallery/Upload';
|
||||||
import Collections from 'components/pages/gallery/Collections';
|
import Collections from 'components/pages/gallery/Collections';
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext } from 'pages/_app';
|
||||||
|
import { CustomError, ServerErrorCodes } from 'utils/common/errorUtil';
|
||||||
export enum FILE_TYPE {
|
|
||||||
IMAGE,
|
|
||||||
VIDEO,
|
|
||||||
OTHERS,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeadCenter = styled.div`
|
export const DeadCenter = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -98,19 +98,22 @@ export interface SearchStats {
|
||||||
type GalleryContextType = {
|
type GalleryContextType = {
|
||||||
thumbs: Map<number, string>;
|
thumbs: Map<number, string>;
|
||||||
files: Map<number, string>;
|
files: Map<number, string>;
|
||||||
}
|
};
|
||||||
|
|
||||||
const defaultGalleryContext: GalleryContextType = {
|
const defaultGalleryContext: GalleryContextType = {
|
||||||
thumbs: new Map(),
|
thumbs: new Map(),
|
||||||
files: new Map(),
|
files: new Map(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GalleryContext = createContext<GalleryContextType>(defaultGalleryContext);
|
export const GalleryContext = createContext<GalleryContextType>(
|
||||||
|
defaultGalleryContext
|
||||||
|
);
|
||||||
|
|
||||||
export default function Gallery() {
|
export default function Gallery() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [collections, setCollections] = useState<Collection[]>([]);
|
const [collections, setCollections] = useState<Collection[]>([]);
|
||||||
const [collectionsAndTheirLatestFile, setCollectionsAndTheirLatestFile] = useState<CollectionAndItsLatestFile[]>([]);
|
const [collectionsAndTheirLatestFile, setCollectionsAndTheirLatestFile] =
|
||||||
|
useState<CollectionAndItsLatestFile[]>([]);
|
||||||
const [files, setFiles] = useState<File[]>(null);
|
const [files, setFiles] = useState<File[]>(null);
|
||||||
const [favItemIds, setFavItemIds] = useState<Set<number>>();
|
const [favItemIds, setFavItemIds] = useState<Set<number>>();
|
||||||
const [bannerMessage, setBannerMessage] = useState<string>(null);
|
const [bannerMessage, setBannerMessage] = useState<string>(null);
|
||||||
|
@ -121,9 +124,11 @@ export default function Gallery() {
|
||||||
const [dialogView, setDialogView] = useState(false);
|
const [dialogView, setDialogView] = useState(false);
|
||||||
const [planModalView, setPlanModalView] = useState(false);
|
const [planModalView, setPlanModalView] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [collectionSelectorAttributes, setCollectionSelectorAttributes] = useState<CollectionSelectorAttributes>(null);
|
const [collectionSelectorAttributes, setCollectionSelectorAttributes] =
|
||||||
|
useState<CollectionSelectorAttributes>(null);
|
||||||
const [collectionSelectorView, setCollectionSelectorView] = useState(false);
|
const [collectionSelectorView, setCollectionSelectorView] = useState(false);
|
||||||
const [collectionNamerAttributes, setCollectionNamerAttributes] = useState<CollectionNamerAttributes>(null);
|
const [collectionNamerAttributes, setCollectionNamerAttributes] =
|
||||||
|
useState<CollectionNamerAttributes>(null);
|
||||||
const [collectionNamerView, setCollectionNamerView] = useState(false);
|
const [collectionNamerView, setCollectionNamerView] = useState(false);
|
||||||
const [search, setSearch] = useState<Search>({
|
const [search, setSearch] = useState<Search>({
|
||||||
date: null,
|
date: null,
|
||||||
|
@ -150,7 +155,8 @@ export default function Gallery() {
|
||||||
const resync = useRef(false);
|
const resync = useRef(false);
|
||||||
const [deleted, setDeleted] = useState<number[]>([]);
|
const [deleted, setDeleted] = useState<number[]>([]);
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
const [collectionFilesCount, setCollectionFilesCount] = useState<Map<number, number>>();
|
const [collectionFilesCount, setCollectionFilesCount] =
|
||||||
|
useState<Map<number, number>>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||||
|
@ -181,50 +187,46 @@ export default function Gallery() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => setDialogView(true), [dialogMessage]);
|
useEffect(() => setDialogView(true), [dialogMessage]);
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => {
|
if (collectionSelectorAttributes) {
|
||||||
if (collectionSelectorAttributes) {
|
setCollectionSelectorView(true);
|
||||||
setCollectionSelectorView(true);
|
}
|
||||||
}
|
}, [collectionSelectorAttributes]);
|
||||||
},
|
|
||||||
[collectionSelectorAttributes],
|
|
||||||
);
|
|
||||||
useEffect(() => setCollectionNamerView(true), [collectionNamerAttributes]);
|
useEffect(() => setCollectionNamerView(true), [collectionNamerAttributes]);
|
||||||
|
|
||||||
const syncWithRemote = async (force = false, silent=false) => {
|
const syncWithRemote = async (force = false, silent = false) => {
|
||||||
if (syncInProgress.current && !force) {
|
if (syncInProgress.current && !force) {
|
||||||
resync.current= true;
|
resync.current = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
syncInProgress.current=true;
|
syncInProgress.current = true;
|
||||||
try {
|
try {
|
||||||
checkConnectivity();
|
checkConnectivity();
|
||||||
if (!(await isTokenValid())) {
|
if (!(await isTokenValid())) {
|
||||||
throw new Error(errorCodes.ERR_SESSION_EXPIRED);
|
throw new Error(ServerErrorCodes.SESSION_EXPIRED);
|
||||||
}
|
}
|
||||||
!silent && loadingBar.current?.continuousStart();
|
!silent && loadingBar.current?.continuousStart();
|
||||||
await billingService.updatePlans();
|
|
||||||
await billingService.syncSubscription();
|
await billingService.syncSubscription();
|
||||||
const collections = await syncCollections();
|
const collections = await syncCollections();
|
||||||
const { files } = await syncFiles(collections, setFiles);
|
const { files } = await syncFiles(collections, setFiles);
|
||||||
await initDerivativeState(collections, files);
|
await initDerivativeState(collections, files);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
switch (e.message) {
|
switch (e.message) {
|
||||||
case errorCodes.ERR_SESSION_EXPIRED:
|
case ServerErrorCodes.SESSION_EXPIRED:
|
||||||
setBannerMessage(constants.SESSION_EXPIRED_MESSAGE);
|
setBannerMessage(constants.SESSION_EXPIRED_MESSAGE);
|
||||||
setDialogMessage({
|
setDialogMessage({
|
||||||
title: constants.SESSION_EXPIRED,
|
title: constants.SESSION_EXPIRED,
|
||||||
content: constants.SESSION_EXPIRED_MESSAGE,
|
content: constants.SESSION_EXPIRED_MESSAGE,
|
||||||
staticBackdrop: true,
|
staticBackdrop: true,
|
||||||
|
nonClosable: true,
|
||||||
proceed: {
|
proceed: {
|
||||||
text: constants.LOGIN,
|
text: constants.LOGIN,
|
||||||
action: logoutUser,
|
action: logoutUser,
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
},
|
},
|
||||||
nonClosable: true,
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case errorCodes.ERR_KEY_MISSING:
|
case CustomError.KEY_MISSING:
|
||||||
clearKeys();
|
clearKeys();
|
||||||
router.push('/credentials');
|
router.push('/credentials');
|
||||||
break;
|
break;
|
||||||
|
@ -232,22 +234,17 @@ export default function Gallery() {
|
||||||
} finally {
|
} finally {
|
||||||
!silent && loadingBar.current?.complete();
|
!silent && loadingBar.current?.complete();
|
||||||
}
|
}
|
||||||
syncInProgress.current=false;
|
syncInProgress.current = false;
|
||||||
if (resync.current) {
|
if (resync.current) {
|
||||||
resync.current=false;
|
resync.current = false;
|
||||||
syncWithRemote();
|
syncWithRemote();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initDerivativeState = async (collections, files) => {
|
const initDerivativeState = async (collections, files) => {
|
||||||
const nonEmptyCollections = getNonEmptyCollections(
|
const nonEmptyCollections = getNonEmptyCollections(collections, files);
|
||||||
collections,
|
const collectionsAndTheirLatestFile =
|
||||||
files,
|
await getCollectionsAndTheirLatestFile(nonEmptyCollections, files);
|
||||||
);
|
|
||||||
const collectionsAndTheirLatestFile = await getCollectionsAndTheirLatestFile(
|
|
||||||
nonEmptyCollections,
|
|
||||||
files,
|
|
||||||
);
|
|
||||||
const collectionWiseFiles = sortFilesIntoCollections(files);
|
const collectionWiseFiles = sortFilesIntoCollections(files);
|
||||||
const collectionFilesCount = new Map<number, number>();
|
const collectionFilesCount = new Map<number, number>();
|
||||||
for (const [id, files] of collectionWiseFiles) {
|
for (const [id, files] of collectionWiseFiles) {
|
||||||
|
@ -274,7 +271,7 @@ export default function Gallery() {
|
||||||
}
|
}
|
||||||
const addToCollectionHelper = (
|
const addToCollectionHelper = (
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
collection: Collection,
|
collection: Collection
|
||||||
) => {
|
) => {
|
||||||
loadingBar.current?.continuousStart();
|
loadingBar.current?.continuousStart();
|
||||||
addFilesToCollection(
|
addFilesToCollection(
|
||||||
|
@ -285,31 +282,29 @@ export default function Gallery() {
|
||||||
syncWithRemote,
|
syncWithRemote,
|
||||||
selectCollection,
|
selectCollection,
|
||||||
collectionName,
|
collectionName,
|
||||||
collection,
|
collection
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showCreateCollectionModal = () => setCollectionNamerAttributes({
|
const showCreateCollectionModal = () =>
|
||||||
title: constants.CREATE_COLLECTION,
|
setCollectionNamerAttributes({
|
||||||
buttonText: constants.CREATE,
|
title: constants.CREATE_COLLECTION,
|
||||||
autoFilledName: '',
|
buttonText: constants.CREATE,
|
||||||
callback: (collectionName) => addToCollectionHelper(collectionName, null),
|
autoFilledName: '',
|
||||||
});
|
callback: (collectionName) =>
|
||||||
|
addToCollectionHelper(collectionName, null),
|
||||||
|
});
|
||||||
|
|
||||||
const deleteFileHelper = async () => {
|
const deleteFileHelper = async () => {
|
||||||
loadingBar.current?.continuousStart();
|
loadingBar.current?.continuousStart();
|
||||||
try {
|
try {
|
||||||
const fileIds = getSelectedFileIds(selected);
|
const fileIds = getSelectedFileIds(selected);
|
||||||
await deleteFiles(
|
await deleteFiles(fileIds, clearSelection, syncWithRemote);
|
||||||
fileIds,
|
|
||||||
clearSelection,
|
|
||||||
syncWithRemote,
|
|
||||||
);
|
|
||||||
setDeleted([...deleted, ...fileIds]);
|
setDeleted([...deleted, ...fileIds]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadingBar.current.complete();
|
loadingBar.current.complete();
|
||||||
switch (e.status?.toString()) {
|
switch (e.status?.toString()) {
|
||||||
case errorCodes.ERR_FORBIDDEN:
|
case ServerErrorCodes.FORBIDDEN:
|
||||||
setDialogMessage({
|
setDialogMessage({
|
||||||
title: constants.ERROR,
|
title: constants.ERROR,
|
||||||
staticBackdrop: true,
|
staticBackdrop: true,
|
||||||
|
@ -333,7 +328,6 @@ export default function Gallery() {
|
||||||
setSearchStats(null);
|
setSearchStats(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const closeCollectionSelector = (closeBtnClick?: boolean) => {
|
const closeCollectionSelector = (closeBtnClick?: boolean) => {
|
||||||
if (closeBtnClick === true) {
|
if (closeBtnClick === true) {
|
||||||
appContext.resetSharedFiles();
|
appContext.resetSharedFiles();
|
||||||
|
@ -346,8 +340,10 @@ export default function Gallery() {
|
||||||
<FullScreenDropZone
|
<FullScreenDropZone
|
||||||
getRootProps={getRootProps}
|
getRootProps={getRootProps}
|
||||||
getInputProps={getInputProps}
|
getInputProps={getInputProps}
|
||||||
showCollectionSelector={setCollectionSelectorView.bind(null, true)}
|
showCollectionSelector={setCollectionSelectorView.bind(
|
||||||
>
|
null,
|
||||||
|
true
|
||||||
|
)}>
|
||||||
{loading && (
|
{loading && (
|
||||||
<LoadingOverlay>
|
<LoadingOverlay>
|
||||||
<EnteSpinner />
|
<EnteSpinner />
|
||||||
|
@ -399,24 +395,33 @@ export default function Gallery() {
|
||||||
attributes={collectionNamerAttributes}
|
attributes={collectionNamerAttributes}
|
||||||
/>
|
/>
|
||||||
<CollectionSelector
|
<CollectionSelector
|
||||||
show={collectionSelectorView && !(collectionsAndTheirLatestFile?.length === 0)}
|
show={
|
||||||
|
collectionSelectorView &&
|
||||||
|
!(collectionsAndTheirLatestFile?.length === 0)
|
||||||
|
}
|
||||||
onHide={closeCollectionSelector}
|
onHide={closeCollectionSelector}
|
||||||
collectionsAndTheirLatestFile={collectionsAndTheirLatestFile}
|
collectionsAndTheirLatestFile={
|
||||||
|
collectionsAndTheirLatestFile
|
||||||
|
}
|
||||||
directlyShowNextModal={
|
directlyShowNextModal={
|
||||||
collectionsAndTheirLatestFile?.length === 0
|
collectionsAndTheirLatestFile?.length === 0
|
||||||
}
|
}
|
||||||
attributes={collectionSelectorAttributes}
|
attributes={collectionSelectorAttributes}
|
||||||
syncWithRemote={syncWithRemote}
|
|
||||||
/>
|
/>
|
||||||
<Upload
|
<Upload
|
||||||
syncWithRemote={syncWithRemote}
|
syncWithRemote={syncWithRemote}
|
||||||
setBannerMessage={setBannerMessage}
|
setBannerMessage={setBannerMessage}
|
||||||
acceptedFiles={acceptedFiles}
|
acceptedFiles={acceptedFiles}
|
||||||
showCollectionSelector={setCollectionSelectorView.bind(null, true)}
|
showCollectionSelector={setCollectionSelectorView.bind(
|
||||||
setCollectionSelectorAttributes={setCollectionSelectorAttributes}
|
null,
|
||||||
|
true
|
||||||
|
)}
|
||||||
|
setCollectionSelectorAttributes={
|
||||||
|
setCollectionSelectorAttributes
|
||||||
|
}
|
||||||
closeCollectionSelector={setCollectionSelectorView.bind(
|
closeCollectionSelector={setCollectionSelectorView.bind(
|
||||||
null,
|
null,
|
||||||
false,
|
false
|
||||||
)}
|
)}
|
||||||
setLoading={setLoading}
|
setLoading={setLoading}
|
||||||
setCollectionNamerAttributes={setCollectionNamerAttributes}
|
setCollectionNamerAttributes={setCollectionNamerAttributes}
|
||||||
|
@ -431,7 +436,10 @@ export default function Gallery() {
|
||||||
setLoading={setLoading}
|
setLoading={setLoading}
|
||||||
showPlanSelectorModal={() => setPlanModalView(true)}
|
showPlanSelectorModal={() => setPlanModalView(true)}
|
||||||
/>
|
/>
|
||||||
<UploadButton isFirstFetch={isFirstFetch} openFileUploader={openFileUploader} />
|
<UploadButton
|
||||||
|
isFirstFetch={isFirstFetch}
|
||||||
|
openFileUploader={openFileUploader}
|
||||||
|
/>
|
||||||
<PhotoFrame
|
<PhotoFrame
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default function Generate() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||||
const keyAttributes: KeyAttributes = getData(
|
const keyAttributes: KeyAttributes = getData(
|
||||||
LS_KEYS.ORIGINAL_KEY_ATTRIBUTES,
|
LS_KEYS.ORIGINAL_KEY_ATTRIBUTES
|
||||||
);
|
);
|
||||||
router.prefetch('/gallery');
|
router.prefetch('/gallery');
|
||||||
const user = getData(LS_KEYS.USER);
|
const user = getData(LS_KEYS.USER);
|
||||||
|
@ -64,14 +64,14 @@ export default function Generate() {
|
||||||
const onSubmit = async (passphrase, setFieldError) => {
|
const onSubmit = async (passphrase, setFieldError) => {
|
||||||
try {
|
try {
|
||||||
const { keyAttributes, masterKey } = await generateKeyAttributes(
|
const { keyAttributes, masterKey } = await generateKeyAttributes(
|
||||||
passphrase,
|
passphrase
|
||||||
);
|
);
|
||||||
|
|
||||||
await putAttributes(token, keyAttributes);
|
await putAttributes(token, keyAttributes);
|
||||||
await generateAndSaveIntermediateKeyAttributes(
|
await generateAndSaveIntermediateKeyAttributes(
|
||||||
passphrase,
|
passphrase,
|
||||||
keyAttributes,
|
keyAttributes,
|
||||||
masterKey,
|
masterKey
|
||||||
);
|
);
|
||||||
await setSessionKeys(masterKey);
|
await setSessionKeys(masterKey);
|
||||||
setJustSignedUp(true);
|
setJustSignedUp(true);
|
||||||
|
|
|
@ -11,6 +11,7 @@ import SignUp from 'components/SignUp';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import localForage from 'utils/storage/localForage';
|
import localForage from 'utils/storage/localForage';
|
||||||
import IncognitoWarning from 'components/IncognitoWarning';
|
import IncognitoWarning from 'components/IncognitoWarning';
|
||||||
|
import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -19,7 +20,7 @@ const Container = styled.div`
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
|
|
||||||
@media(max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -32,7 +33,7 @@ const SlideContainer = styled.div`
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
@media(max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -46,7 +47,7 @@ const DesktopBox = styled.div`
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: #242424;
|
background-color: #242424;
|
||||||
|
|
||||||
@media(max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -54,7 +55,7 @@ const DesktopBox = styled.div`
|
||||||
const MobileBox = styled.div`
|
const MobileBox = styled.div`
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
@media(max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 40px 10px;
|
padding: 40px 10px;
|
||||||
|
@ -90,7 +91,7 @@ const Img = styled.img`
|
||||||
height: 250px;
|
height: 250px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
|
||||||
@media(max-width: 400px) {
|
@media (max-width: 400px) {
|
||||||
height: 180px;
|
height: 180px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -110,6 +111,7 @@ export default function LandingPage() {
|
||||||
try {
|
try {
|
||||||
await localForage.ready();
|
await localForage.ready();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logError(e, 'usage in incognito mode tried');
|
||||||
setBlockUsage(true);
|
setBlockUsage(true);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -121,56 +123,73 @@ export default function LandingPage() {
|
||||||
const signUp = () => setShowLogin(false);
|
const signUp = () => setShowLogin(false);
|
||||||
const login = () => setShowLogin(true);
|
const login = () => setShowLogin(true);
|
||||||
|
|
||||||
return <Container>
|
return (
|
||||||
{loading ? <EnteSpinner /> :
|
<Container>
|
||||||
(<>
|
{loading ? (
|
||||||
<SlideContainer>
|
<EnteSpinner />
|
||||||
<UpperText>
|
) : (
|
||||||
{constants.HERO_HEADER()}
|
<>
|
||||||
</UpperText>
|
<SlideContainer>
|
||||||
<Carousel controls={false}>
|
<UpperText>{constants.HERO_HEADER()}</UpperText>
|
||||||
<Carousel.Item>
|
<Carousel controls={false}>
|
||||||
<Img src="/images/slide-1.png" />
|
<Carousel.Item>
|
||||||
<FeatureText>{constants.HERO_SLIDE_1_TITLE}</FeatureText>
|
<Img src="/images/slide-1.png" />
|
||||||
<TextContainer>{constants.HERO_SLIDE_1}</TextContainer>
|
<FeatureText>
|
||||||
</Carousel.Item>
|
{constants.HERO_SLIDE_1_TITLE}
|
||||||
<Carousel.Item>
|
</FeatureText>
|
||||||
<Img src="/images/slide-2.png" />
|
<TextContainer>
|
||||||
<FeatureText>{constants.HERO_SLIDE_2_TITLE}</FeatureText>
|
{constants.HERO_SLIDE_1}
|
||||||
<TextContainer>{constants.HERO_SLIDE_2}</TextContainer>
|
</TextContainer>
|
||||||
</Carousel.Item>
|
</Carousel.Item>
|
||||||
<Carousel.Item>
|
<Carousel.Item>
|
||||||
<Img src="/images/slide-3.png" />
|
<Img src="/images/slide-2.png" />
|
||||||
<FeatureText>{constants.HERO_SLIDE_3_TITLE}</FeatureText>
|
<FeatureText>
|
||||||
<TextContainer>{constants.HERO_SLIDE_3}</TextContainer>
|
{constants.HERO_SLIDE_2_TITLE}
|
||||||
</Carousel.Item>
|
</FeatureText>
|
||||||
</Carousel>
|
<TextContainer>
|
||||||
</SlideContainer>
|
{constants.HERO_SLIDE_2}
|
||||||
<MobileBox>
|
</TextContainer>
|
||||||
<Button
|
</Carousel.Item>
|
||||||
variant="outline-success"
|
<Carousel.Item>
|
||||||
size="lg"
|
<Img src="/images/slide-3.png" />
|
||||||
style={{ color: '#fff', padding: '10px 50px' }}
|
<FeatureText>
|
||||||
onClick={() => router.push('signup')}
|
{constants.HERO_SLIDE_3_TITLE}
|
||||||
>
|
</FeatureText>
|
||||||
{constants.SIGN_UP}
|
<TextContainer>
|
||||||
</Button>
|
{constants.HERO_SLIDE_3}
|
||||||
<br />
|
</TextContainer>
|
||||||
<Button
|
</Carousel.Item>
|
||||||
variant="link"
|
</Carousel>
|
||||||
size="lg"
|
</SlideContainer>
|
||||||
style={{ color: '#fff', padding: '10px 50px' }}
|
<MobileBox>
|
||||||
onClick={() => router.push('login')}
|
<Button
|
||||||
>
|
variant="outline-success"
|
||||||
{constants.SIGN_IN}
|
size="lg"
|
||||||
</Button>
|
style={{ color: '#fff', padding: '10px 50px' }}
|
||||||
</MobileBox>
|
onClick={() => router.push('signup')}>
|
||||||
<DesktopBox>
|
{constants.SIGN_UP}
|
||||||
<SideBox>
|
</Button>
|
||||||
{showLogin ? <Login signUp={signUp} /> : <SignUp login={login} />}
|
<br />
|
||||||
</SideBox>
|
<Button
|
||||||
</DesktopBox>
|
variant="link"
|
||||||
{blockUsage && <IncognitoWarning />}
|
size="lg"
|
||||||
</>)}
|
style={{ color: '#fff', padding: '10px 50px' }}
|
||||||
</Container>;
|
onClick={() => router.push('login')}>
|
||||||
|
{constants.SIGN_IN}
|
||||||
|
</Button>
|
||||||
|
</MobileBox>
|
||||||
|
<DesktopBox>
|
||||||
|
<SideBox>
|
||||||
|
{showLogin ? (
|
||||||
|
<Login signUp={signUp} />
|
||||||
|
) : (
|
||||||
|
<SignUp login={login} />
|
||||||
|
)}
|
||||||
|
</SideBox>
|
||||||
|
</DesktopBox>
|
||||||
|
{blockUsage && <IncognitoWarning />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,14 +27,19 @@ export default function Home() {
|
||||||
router.push('/signup');
|
router.push('/signup');
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Container>{loading ?
|
return (
|
||||||
<EnteSpinner>
|
<Container>
|
||||||
<span className="sr-only">Loading...</span>
|
{loading ? (
|
||||||
</EnteSpinner>:
|
<EnteSpinner>
|
||||||
<Card style={{ minWidth: '320px' }} className="text-center">
|
<span className="sr-only">Loading...</span>
|
||||||
<Card.Body style={{ padding: '40px 30px' }}>
|
</EnteSpinner>
|
||||||
<Login signUp={register}/>
|
) : (
|
||||||
</Card.Body>
|
<Card style={{ minWidth: '320px' }} className="text-center">
|
||||||
</Card>}
|
<Card.Body style={{ padding: '40px 30px' }}>
|
||||||
</Container>;
|
<Login signUp={register} />
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default function Recover() {
|
||||||
const masterKey: string = await cryptoWorker.decryptB64(
|
const masterKey: string = await cryptoWorker.decryptB64(
|
||||||
keyAttributes.masterKeyEncryptedWithRecoveryKey,
|
keyAttributes.masterKeyEncryptedWithRecoveryKey,
|
||||||
keyAttributes.masterKeyDecryptionNonce,
|
keyAttributes.masterKeyDecryptionNonce,
|
||||||
await cryptoWorker.fromHex(recoveryKey),
|
await cryptoWorker.fromHex(recoveryKey)
|
||||||
);
|
);
|
||||||
setSessionKeys(masterKey);
|
setSessionKeys(masterKey);
|
||||||
router.push('/changePassword');
|
router.push('/changePassword');
|
||||||
|
@ -51,13 +51,10 @@ export default function Recover() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container>
|
<Container>
|
||||||
<Card
|
<Card style={{ minWidth: '320px' }} className="text-center">
|
||||||
style={{ minWidth: '320px' }}
|
|
||||||
className="text-center"
|
|
||||||
>
|
|
||||||
<Card.Body style={{ padding: '40px 30px' }}>
|
<Card.Body style={{ padding: '40px 30px' }}>
|
||||||
<Card.Title style={{ marginBottom: '32px' }}>
|
<Card.Title style={{ marginBottom: '32px' }}>
|
||||||
<LogoImg src='/icon.svg' />
|
<LogoImg src="/icon.svg" />
|
||||||
{constants.RECOVER_ACCOUNT}
|
{constants.RECOVER_ACCOUNT}
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
<SingleInputForm
|
<SingleInputForm
|
||||||
|
@ -71,12 +68,10 @@ export default function Recover() {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
marginTop: '12px',
|
marginTop: '12px',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
onClick={() => SetMessageDialogView(true)}
|
onClick={() => SetMessageDialogView(true)}>
|
||||||
>
|
|
||||||
{constants.NO_RECOVERY_KEY}
|
{constants.NO_RECOVERY_KEY}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="link" onClick={router.back}>
|
<Button variant="link" onClick={router.back}>
|
||||||
|
|
|
@ -7,7 +7,6 @@ import EnteSpinner from 'components/EnteSpinner';
|
||||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||||
import SignUp from 'components/SignUp';
|
import SignUp from 'components/SignUp';
|
||||||
|
|
||||||
|
|
||||||
export default function SignUpPage() {
|
export default function SignUpPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
|
@ -29,14 +28,16 @@ export default function SignUpPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>{
|
<Container>
|
||||||
loading ? <EnteSpinner /> :
|
{loading ? (
|
||||||
|
<EnteSpinner />
|
||||||
|
) : (
|
||||||
<Card style={{ minWidth: '320px' }} className="text-center">
|
<Card style={{ minWidth: '320px' }} className="text-center">
|
||||||
<Card.Body style={{ padding: '40px 30px' }}>
|
<Card.Body style={{ padding: '40px 30px' }}>
|
||||||
<SignUp login={login} />
|
<SignUp login={login} />
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card>
|
</Card>
|
||||||
}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,20 +2,20 @@ import React, { useEffect, useState } from 'react';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import CryptoWorker from 'utils/crypto';
|
import CryptoWorker, { B64EncryptionResult } from 'utils/crypto';
|
||||||
import SingleInputForm from 'components/SingleInputForm';
|
import SingleInputForm from 'components/SingleInputForm';
|
||||||
import MessageDialog from 'components/MessageDialog';
|
import MessageDialog from 'components/MessageDialog';
|
||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
import { Card, Button } from 'react-bootstrap';
|
import { Card, Button } from 'react-bootstrap';
|
||||||
import LogoImg from 'components/LogoImg';
|
import LogoImg from 'components/LogoImg';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { B64EncryptionResult } from 'services/uploadService';
|
|
||||||
import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
|
import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
|
||||||
|
|
||||||
export default function Recover() {
|
export default function Recover() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [messageDialogView, SetMessageDialogView] = useState(false);
|
const [messageDialogView, SetMessageDialogView] = useState(false);
|
||||||
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] = useState<B64EncryptionResult>(null);
|
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] =
|
||||||
|
useState<B64EncryptionResult>(null);
|
||||||
const [sessionID, setSessionID] = useState(null);
|
const [sessionID, setSessionID] = useState(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
router.prefetch('/gallery');
|
router.prefetch('/gallery');
|
||||||
|
@ -41,7 +41,7 @@ export default function Recover() {
|
||||||
const twoFactorSecret: string = await cryptoWorker.decryptB64(
|
const twoFactorSecret: string = await cryptoWorker.decryptB64(
|
||||||
encryptedTwoFactorSecret.encryptedData,
|
encryptedTwoFactorSecret.encryptedData,
|
||||||
encryptedTwoFactorSecret.nonce,
|
encryptedTwoFactorSecret.nonce,
|
||||||
await cryptoWorker.fromHex(recoveryKey),
|
await cryptoWorker.fromHex(recoveryKey)
|
||||||
);
|
);
|
||||||
const resp = await removeTwoFactor(sessionID, twoFactorSecret);
|
const resp = await removeTwoFactor(sessionID, twoFactorSecret);
|
||||||
const { keyAttributes, encryptedToken, token, id } = resp;
|
const { keyAttributes, encryptedToken, token, id } = resp;
|
||||||
|
@ -63,13 +63,10 @@ export default function Recover() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container>
|
<Container>
|
||||||
<Card
|
<Card style={{ minWidth: '320px' }} className="text-center">
|
||||||
style={{ minWidth: '320px' }}
|
|
||||||
className="text-center"
|
|
||||||
>
|
|
||||||
<Card.Body style={{ padding: '40px 30px' }}>
|
<Card.Body style={{ padding: '40px 30px' }}>
|
||||||
<Card.Title style={{ marginBottom: '32px' }}>
|
<Card.Title style={{ marginBottom: '32px' }}>
|
||||||
<LogoImg src='/icon.svg' />
|
<LogoImg src="/icon.svg" />
|
||||||
{constants.RECOVER_TWO_FACTOR}
|
{constants.RECOVER_TWO_FACTOR}
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
<SingleInputForm
|
<SingleInputForm
|
||||||
|
@ -83,12 +80,10 @@ export default function Recover() {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
marginTop: '12px',
|
marginTop: '12px',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
onClick={() => SetMessageDialogView(true)}
|
onClick={() => SetMessageDialogView(true)}>
|
||||||
>
|
|
||||||
{constants.NO_RECOVERY_KEY}
|
{constants.NO_RECOVERY_KEY}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="link" onClick={router.back}>
|
<Button variant="link" onClick={router.back}>
|
||||||
|
|
|
@ -4,17 +4,20 @@ import { CodeBlock, FreeFlowText } from 'components/RecoveryKeyModal';
|
||||||
import { DeadCenter } from 'pages/gallery';
|
import { DeadCenter } from 'pages/gallery';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { Button, Card } from 'react-bootstrap';
|
import { Button, Card } from 'react-bootstrap';
|
||||||
import { enableTwoFactor, setupTwoFactor, TwoFactorSecret } from 'services/userService';
|
import {
|
||||||
|
enableTwoFactor,
|
||||||
|
setupTwoFactor,
|
||||||
|
TwoFactorSecret,
|
||||||
|
} from 'services/userService';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import VerifyTwoFactor from 'components/VerifyTwoFactor';
|
import VerifyTwoFactor from 'components/VerifyTwoFactor';
|
||||||
import { B64EncryptionResult } from 'services/uploadService';
|
import { B64EncryptionResult } from 'utils/crypto';
|
||||||
import { encryptWithRecoveryKey } from 'utils/crypto';
|
import { encryptWithRecoveryKey } from 'utils/crypto';
|
||||||
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
|
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
|
||||||
|
|
||||||
|
|
||||||
enum SetupMode {
|
enum SetupMode {
|
||||||
QR_CODE,
|
QR_CODE,
|
||||||
|
@ -22,15 +25,19 @@ enum SetupMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
const QRCode = styled.img`
|
const QRCode = styled.img`
|
||||||
height:200px;
|
height: 200px;
|
||||||
width:200px;
|
width: 200px;
|
||||||
margin:1rem;
|
margin: 1rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function SetupTwoFactor() {
|
export default function SetupTwoFactor() {
|
||||||
const [setupMode, setSetupMode] = useState<SetupMode>(SetupMode.QR_CODE);
|
const [setupMode, setSetupMode] = useState<SetupMode>(SetupMode.QR_CODE);
|
||||||
const [twoFactorSecret, setTwoFactorSecret] = useState<TwoFactorSecret>(null);
|
const [twoFactorSecret, setTwoFactorSecret] =
|
||||||
const [recoveryEncryptedTwoFactorSecret, setRecoveryEncryptedTwoFactorSecret] = useState<B64EncryptionResult>(null);
|
useState<TwoFactorSecret>(null);
|
||||||
|
const [
|
||||||
|
recoveryEncryptedTwoFactorSecret,
|
||||||
|
setRecoveryEncryptedTwoFactorSecret,
|
||||||
|
] = useState<B64EncryptionResult>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -40,11 +47,17 @@ export default function SetupTwoFactor() {
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
try {
|
try {
|
||||||
const twoFactorSecret = await setupTwoFactor();
|
const twoFactorSecret = await setupTwoFactor();
|
||||||
const recoveryEncryptedTwoFactorSecret = await encryptWithRecoveryKey(twoFactorSecret.secretCode);
|
const recoveryEncryptedTwoFactorSecret =
|
||||||
|
await encryptWithRecoveryKey(twoFactorSecret.secretCode);
|
||||||
setTwoFactorSecret(twoFactorSecret);
|
setTwoFactorSecret(twoFactorSecret);
|
||||||
setRecoveryEncryptedTwoFactorSecret(recoveryEncryptedTwoFactorSecret);
|
setRecoveryEncryptedTwoFactorSecret(
|
||||||
|
recoveryEncryptedTwoFactorSecret
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_SETUP_FAILED, severity: 'danger' });
|
appContext.setDisappearingFlashMessage({
|
||||||
|
message: constants.TWO_FACTOR_SETUP_FAILED,
|
||||||
|
type: FLASH_MESSAGE_TYPE.DANGER,
|
||||||
|
});
|
||||||
router.push('/gallery');
|
router.push('/gallery');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -52,8 +65,14 @@ export default function SetupTwoFactor() {
|
||||||
}, []);
|
}, []);
|
||||||
const onSubmit = async (otp: string) => {
|
const onSubmit = async (otp: string) => {
|
||||||
await enableTwoFactor(otp, recoveryEncryptedTwoFactorSecret);
|
await enableTwoFactor(otp, recoveryEncryptedTwoFactorSecret);
|
||||||
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), isTwoFactorEnabled: true });
|
setData(LS_KEYS.USER, {
|
||||||
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_SETUP_SUCCESS, severity: 'info' });
|
...getData(LS_KEYS.USER),
|
||||||
|
isTwoFactorEnabled: true,
|
||||||
|
});
|
||||||
|
appContext.setDisappearingFlashMessage({
|
||||||
|
message: constants.TWO_FACTOR_SETUP_SUCCESS,
|
||||||
|
type: FLASH_MESSAGE_TYPE.SUCCESS,
|
||||||
|
});
|
||||||
router.push('/gallery');
|
router.push('/gallery');
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
@ -62,35 +81,67 @@ export default function SetupTwoFactor() {
|
||||||
<Card.Body style={{ padding: '40px 30px', minHeight: '400px' }}>
|
<Card.Body style={{ padding: '40px 30px', minHeight: '400px' }}>
|
||||||
<DeadCenter>
|
<DeadCenter>
|
||||||
<Card.Title style={{ marginBottom: '32px' }}>
|
<Card.Title style={{ marginBottom: '32px' }}>
|
||||||
<LogoImg src='/icon.svg' />
|
<LogoImg src="/icon.svg" />
|
||||||
{constants.TWO_FACTOR}
|
{constants.TWO_FACTOR}
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
{setupMode === SetupMode.QR_CODE ? (
|
{setupMode === SetupMode.QR_CODE ? (
|
||||||
<>
|
<>
|
||||||
<p>{constants.TWO_FACTOR_QR_INSTRUCTION}</p>
|
<p>{constants.TWO_FACTOR_QR_INSTRUCTION}</p>
|
||||||
<DeadCenter>
|
<DeadCenter>
|
||||||
{!twoFactorSecret ? <div style={{ height: '200px', width: '200px', margin: '1rem', display: 'flex', justifyContent: 'center', alignItems: 'center', border: '1px solid #aaa' }}><EnteSpinner /></div> :
|
{!twoFactorSecret ? (
|
||||||
<QRCode src={`data:image/png;base64,${twoFactorSecret.qrCode}`} />
|
<div
|
||||||
}
|
style={{
|
||||||
<Button block variant="link" onClick={() => setSetupMode(SetupMode.MANUAL_CODE)}>
|
height: '200px',
|
||||||
|
width: '200px',
|
||||||
|
margin: '1rem',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
border: '1px solid #aaa',
|
||||||
|
}}>
|
||||||
|
<EnteSpinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<QRCode
|
||||||
|
src={`data:image/png;base64,${twoFactorSecret.qrCode}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
variant="link"
|
||||||
|
onClick={() =>
|
||||||
|
setSetupMode(SetupMode.MANUAL_CODE)
|
||||||
|
}>
|
||||||
{constants.ENTER_CODE_MANUALLY}
|
{constants.ENTER_CODE_MANUALLY}
|
||||||
</Button>
|
</Button>
|
||||||
</DeadCenter>
|
</DeadCenter>
|
||||||
</>
|
</>
|
||||||
) : (<>
|
) : (
|
||||||
<p>{constants.TWO_FACTOR_MANUAL_CODE_INSTRUCTION}</p>
|
<>
|
||||||
<CodeBlock height={100}>
|
<p>
|
||||||
{!twoFactorSecret ? <EnteSpinner /> : (
|
{
|
||||||
<FreeFlowText>
|
constants.TWO_FACTOR_MANUAL_CODE_INSTRUCTION
|
||||||
{twoFactorSecret.secretCode}
|
}
|
||||||
</FreeFlowText>
|
</p>
|
||||||
|
<CodeBlock height={100}>
|
||||||
)}
|
{!twoFactorSecret ? (
|
||||||
</CodeBlock>
|
<EnteSpinner />
|
||||||
<Button block variant="link" style={{ marginBottom: '1rem' }} onClick={() => setSetupMode(SetupMode.QR_CODE)}>
|
) : (
|
||||||
{constants.SCAN_QR_CODE}
|
<FreeFlowText>
|
||||||
</Button>
|
{twoFactorSecret.secretCode}
|
||||||
</>
|
</FreeFlowText>
|
||||||
|
)}
|
||||||
|
</CodeBlock>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
variant="link"
|
||||||
|
style={{ marginBottom: '1rem' }}
|
||||||
|
onClick={() =>
|
||||||
|
setSetupMode(SetupMode.QR_CODE)
|
||||||
|
}>
|
||||||
|
{constants.SCAN_QR_CODE}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -99,13 +150,20 @@ export default function SetupTwoFactor() {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<VerifyTwoFactor onSubmit={onSubmit} back={router.back} buttonText={constants.ENABLE} />
|
<VerifyTwoFactor
|
||||||
<Button style={{ marginTop: '16px' }} variant="link-danger" onClick={router.back}>
|
onSubmit={onSubmit}
|
||||||
|
back={router.back}
|
||||||
|
buttonText={constants.ENABLE}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
style={{ marginTop: '16px' }}
|
||||||
|
variant="link-danger"
|
||||||
|
onClick={router.back}>
|
||||||
{constants.GO_BACK}
|
{constants.GO_BACK}
|
||||||
</Button>
|
</Button>
|
||||||
</DeadCenter>
|
</DeadCenter>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card>
|
</Card>
|
||||||
</Container >
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,21 +52,23 @@ export default function Home() {
|
||||||
<Card style={{ minWidth: '300px' }} className="text-center">
|
<Card style={{ minWidth: '300px' }} className="text-center">
|
||||||
<Card.Body style={{ padding: '40px 30px', minHeight: '400px' }}>
|
<Card.Body style={{ padding: '40px 30px', minHeight: '400px' }}>
|
||||||
<Card.Title style={{ marginBottom: '32px' }}>
|
<Card.Title style={{ marginBottom: '32px' }}>
|
||||||
<LogoImg src='/icon.svg' />
|
<LogoImg src="/icon.svg" />
|
||||||
{constants.TWO_FACTOR}
|
{constants.TWO_FACTOR}
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
<VerifyTwoFactor onSubmit={onSubmit} back={router.back} buttonText={constants.VERIFY} />
|
<VerifyTwoFactor
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
back={router.back}
|
||||||
|
buttonText={constants.VERIFY}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
marginTop: '12px',
|
marginTop: '12px',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
onClick={() => router.push('/two-factor/recover')}
|
onClick={() => router.push('/two-factor/recover')}>
|
||||||
>
|
|
||||||
{constants.LOST_DEVICE}
|
{constants.LOST_DEVICE}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="link" onClick={logoutUser}>
|
<Button variant="link" onClick={logoutUser}>
|
||||||
|
|
|
@ -56,14 +56,24 @@ export default function Verify() {
|
||||||
|
|
||||||
const onSubmit = async (
|
const onSubmit = async (
|
||||||
{ ott }: formValues,
|
{ ott }: formValues,
|
||||||
{ setFieldError }: FormikHelpers<formValues>,
|
{ setFieldError }: FormikHelpers<formValues>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const resp = await verifyOtt(email, ott);
|
const resp = await verifyOtt(email, ott);
|
||||||
const { keyAttributes, encryptedToken, token, id, twoFactorSessionID } = resp.data as EmailVerificationResponse;
|
const {
|
||||||
|
keyAttributes,
|
||||||
|
encryptedToken,
|
||||||
|
token,
|
||||||
|
id,
|
||||||
|
twoFactorSessionID,
|
||||||
|
} = resp.data as EmailVerificationResponse;
|
||||||
if (twoFactorSessionID) {
|
if (twoFactorSessionID) {
|
||||||
setData(LS_KEYS.USER, { email, twoFactorSessionID, isTwoFactorEnabled: true });
|
setData(LS_KEYS.USER, {
|
||||||
|
email,
|
||||||
|
twoFactorSessionID,
|
||||||
|
isTwoFactorEnabled: true,
|
||||||
|
});
|
||||||
router.push('/two-factor/verify');
|
router.push('/two-factor/verify');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -109,7 +119,7 @@ export default function Verify() {
|
||||||
<Card style={{ minWidth: '300px' }} className="text-center">
|
<Card style={{ minWidth: '300px' }} className="text-center">
|
||||||
<Card.Body style={{ padding: '40px 30px' }}>
|
<Card.Body style={{ padding: '40px 30px' }}>
|
||||||
<Card.Title style={{ marginBottom: '32px' }}>
|
<Card.Title style={{ marginBottom: '32px' }}>
|
||||||
<LogoImg src='/icon.svg' />
|
<LogoImg src="/icon.svg" />
|
||||||
{constants.VERIFY_EMAIL}
|
{constants.VERIFY_EMAIL}
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
{constants.EMAIL_SENT({ email })}
|
{constants.EMAIL_SENT({ email })}
|
||||||
|
@ -123,8 +133,7 @@ export default function Verify() {
|
||||||
})}
|
})}
|
||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}>
|
||||||
>
|
|
||||||
{({
|
{({
|
||||||
values,
|
values,
|
||||||
touched,
|
touched,
|
||||||
|
@ -140,7 +149,7 @@ export default function Verify() {
|
||||||
value={values.ott}
|
value={values.ott}
|
||||||
onChange={handleChange('ott')}
|
onChange={handleChange('ott')}
|
||||||
isInvalid={Boolean(
|
isInvalid={Boolean(
|
||||||
touched.ott && errors.ott,
|
touched.ott && errors.ott
|
||||||
)}
|
)}
|
||||||
placeholder={constants.ENTER_OTT}
|
placeholder={constants.ENTER_OTT}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
|
|
@ -8,17 +8,25 @@ pageCache();
|
||||||
precacheAndRoute(self.__WB_MANIFEST);
|
precacheAndRoute(self.__WB_MANIFEST);
|
||||||
cleanupOutdatedCaches();
|
cleanupOutdatedCaches();
|
||||||
|
|
||||||
registerRoute('/share-target', async ({ event }) => {
|
registerRoute(
|
||||||
event.waitUntil(async function() {
|
'/share-target',
|
||||||
const data = await event.request.formData();
|
async ({ event }) => {
|
||||||
const client = await self.clients.get(event.resultingClientId || event.clientId);
|
event.waitUntil(
|
||||||
const files = data.getAll('files');
|
(async function () {
|
||||||
setTimeout(() => {
|
const data = await event.request.formData();
|
||||||
client.postMessage({ files, action: 'upload-files' });
|
const client = await self.clients.get(
|
||||||
}, 1000);
|
event.resultingClientId || event.clientId
|
||||||
}());
|
);
|
||||||
return Response.redirect('./');
|
const files = data.getAll('files');
|
||||||
}, 'POST');
|
setTimeout(() => {
|
||||||
|
client.postMessage({ files, action: 'upload-files' });
|
||||||
|
}, 1000);
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
return Response.redirect('./');
|
||||||
|
},
|
||||||
|
'POST'
|
||||||
|
);
|
||||||
|
|
||||||
// Use a stale-while-revalidate strategy for all other requests.
|
// Use a stale-while-revalidate strategy for all other requests.
|
||||||
setDefaultHandler(new NetworkOnly());
|
setDefaultHandler(new NetworkOnly());
|
||||||
|
|
|
@ -21,7 +21,7 @@ class HTTPService {
|
||||||
}
|
}
|
||||||
const { response } = err;
|
const { response } = err;
|
||||||
return Promise.reject(response);
|
return Promise.reject(response);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +77,9 @@ class HTTPService {
|
||||||
...config.headers,
|
...config.headers,
|
||||||
};
|
};
|
||||||
if (customConfig?.cancel) {
|
if (customConfig?.cancel) {
|
||||||
config.cancelToken=new axios.CancelToken((c)=> (customConfig.cancel.exec=c));
|
config.cancelToken = new axios.CancelToken(
|
||||||
|
(c) => (customConfig.cancel.exec = c)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return await axios({ ...config, ...customConfig });
|
return await axios({ ...config, ...customConfig });
|
||||||
}
|
}
|
||||||
|
@ -89,7 +91,7 @@ class HTTPService {
|
||||||
url: string,
|
url: string,
|
||||||
params?: IQueryPrams,
|
params?: IQueryPrams,
|
||||||
headers?: IHTTPHeaders,
|
headers?: IHTTPHeaders,
|
||||||
customConfig?: any,
|
customConfig?: any
|
||||||
) {
|
) {
|
||||||
return this.request(
|
return this.request(
|
||||||
{
|
{
|
||||||
|
@ -98,7 +100,7 @@ class HTTPService {
|
||||||
params,
|
params,
|
||||||
url,
|
url,
|
||||||
},
|
},
|
||||||
customConfig,
|
customConfig
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +112,7 @@ class HTTPService {
|
||||||
data?: any,
|
data?: any,
|
||||||
params?: IQueryPrams,
|
params?: IQueryPrams,
|
||||||
headers?: IHTTPHeaders,
|
headers?: IHTTPHeaders,
|
||||||
customConfig?: any,
|
customConfig?: any
|
||||||
) {
|
) {
|
||||||
return this.request(
|
return this.request(
|
||||||
{
|
{
|
||||||
|
@ -120,7 +122,7 @@ class HTTPService {
|
||||||
params,
|
params,
|
||||||
url,
|
url,
|
||||||
},
|
},
|
||||||
customConfig,
|
customConfig
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +134,7 @@ class HTTPService {
|
||||||
data: any,
|
data: any,
|
||||||
params?: IQueryPrams,
|
params?: IQueryPrams,
|
||||||
headers?: IHTTPHeaders,
|
headers?: IHTTPHeaders,
|
||||||
customConfig?: any,
|
customConfig?: any
|
||||||
) {
|
) {
|
||||||
return this.request(
|
return this.request(
|
||||||
{
|
{
|
||||||
|
@ -142,7 +144,7 @@ class HTTPService {
|
||||||
params,
|
params,
|
||||||
url,
|
url,
|
||||||
},
|
},
|
||||||
customConfig,
|
customConfig
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +156,7 @@ class HTTPService {
|
||||||
data: any,
|
data: any,
|
||||||
params?: IQueryPrams,
|
params?: IQueryPrams,
|
||||||
headers?: IHTTPHeaders,
|
headers?: IHTTPHeaders,
|
||||||
customConfig?: any,
|
customConfig?: any
|
||||||
) {
|
) {
|
||||||
return this.request(
|
return this.request(
|
||||||
{
|
{
|
||||||
|
@ -164,7 +166,7 @@ class HTTPService {
|
||||||
params,
|
params,
|
||||||
url,
|
url,
|
||||||
},
|
},
|
||||||
customConfig,
|
customConfig
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,9 @@ export enum PAYMENT_INTENT_STATUS {
|
||||||
REQUIRE_ACTION = 'requires_action',
|
REQUIRE_ACTION = 'requires_action',
|
||||||
REQUIRE_PAYMENT_METHOD = 'requires_payment_method',
|
REQUIRE_PAYMENT_METHOD = 'requires_payment_method',
|
||||||
}
|
}
|
||||||
enum PaymentActionType{
|
enum PaymentActionType {
|
||||||
Buy='buy',
|
Buy = 'buy',
|
||||||
Update='update'
|
Update = 'update',
|
||||||
}
|
}
|
||||||
export interface Subscription {
|
export interface Subscription {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -30,6 +30,8 @@ export interface Subscription {
|
||||||
attributes: {
|
attributes: {
|
||||||
isCancelled: boolean;
|
isCancelled: boolean;
|
||||||
};
|
};
|
||||||
|
price: string;
|
||||||
|
period: string;
|
||||||
}
|
}
|
||||||
export interface Plan {
|
export interface Plan {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -66,11 +68,13 @@ class billingService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updatePlans() {
|
public async getPlans(): Promise<Plan[]> {
|
||||||
try {
|
try {
|
||||||
const response = await HTTPService.get(`${ENDPOINT}/billing/plans`);
|
const response = await HTTPService.get(
|
||||||
|
`${ENDPOINT}/billing/plans/v2`
|
||||||
|
);
|
||||||
const { plans } = response.data;
|
const { plans } = response.data;
|
||||||
setData(LS_KEYS.PLANS, plans);
|
return plans;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'failed to get plans');
|
logError(e, 'failed to get plans');
|
||||||
}
|
}
|
||||||
|
@ -83,12 +87,12 @@ class billingService {
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
'X-Auth-Token': getToken(),
|
'X-Auth-Token': getToken(),
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
const { subscription } = response.data;
|
const { subscription } = response.data;
|
||||||
setData(LS_KEYS.SUBSCRIPTION, subscription);
|
setData(LS_KEYS.SUBSCRIPTION, subscription);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'failed to get user\'s subscription details');
|
logError(e, "failed to get user's subscription details");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,8 +102,12 @@ class billingService {
|
||||||
// await this.stripe.redirectToCheckout({
|
// await this.stripe.redirectToCheckout({
|
||||||
// sessionId: response.data.sessionID,
|
// sessionId: response.data.sessionID,
|
||||||
// });
|
// });
|
||||||
const paymentToken =await getPaymentToken();
|
const paymentToken = await getPaymentToken();
|
||||||
await this.redirectToPayments(paymentToken, productID, PaymentActionType.Buy);
|
await this.redirectToPayments(
|
||||||
|
paymentToken,
|
||||||
|
productID,
|
||||||
|
PaymentActionType.Buy
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'unable to buy subscription');
|
logError(e, 'unable to buy subscription');
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -139,8 +147,12 @@ class billingService {
|
||||||
// }
|
// }
|
||||||
// break;
|
// break;
|
||||||
// }
|
// }
|
||||||
const paymentToken =await getPaymentToken();
|
const paymentToken = await getPaymentToken();
|
||||||
await this.redirectToPayments(paymentToken, productID, PaymentActionType.Update);
|
await this.redirectToPayments(
|
||||||
|
paymentToken,
|
||||||
|
productID,
|
||||||
|
PaymentActionType.Update
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'subscription update failed');
|
logError(e, 'subscription update failed');
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -160,7 +172,7 @@ class billingService {
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
'X-Auth-Token': getToken(),
|
'X-Auth-Token': getToken(),
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
const { subscription } = response.data;
|
const { subscription } = response.data;
|
||||||
setData(LS_KEYS.SUBSCRIPTION, subscription);
|
setData(LS_KEYS.SUBSCRIPTION, subscription);
|
||||||
|
@ -178,7 +190,7 @@ class billingService {
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
'X-Auth-Token': getToken(),
|
'X-Auth-Token': getToken(),
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
const { subscription } = response.data;
|
const { subscription } = response.data;
|
||||||
setData(LS_KEYS.SUBSCRIPTION, subscription);
|
setData(LS_KEYS.SUBSCRIPTION, subscription);
|
||||||
|
@ -196,12 +208,12 @@ class billingService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'X-Auth-Token': getToken(),
|
'X-Auth-Token': getToken(),
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async verifySubscription(
|
public async verifySubscription(
|
||||||
sessionID: string = null,
|
sessionID: string = null
|
||||||
): Promise<Subscription> {
|
): Promise<Subscription> {
|
||||||
try {
|
try {
|
||||||
const response = await HTTPService.post(
|
const response = await HTTPService.post(
|
||||||
|
@ -214,7 +226,7 @@ class billingService {
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
'X-Auth-Token': getToken(),
|
'X-Auth-Token': getToken(),
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
const { subscription } = response.data;
|
const { subscription } = response.data;
|
||||||
setData(LS_KEYS.SUBSCRIPTION, subscription);
|
setData(LS_KEYS.SUBSCRIPTION, subscription);
|
||||||
|
@ -225,7 +237,11 @@ class billingService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async redirectToPayments(paymentToken:string, productID:string, action:string) {
|
public async redirectToPayments(
|
||||||
|
paymentToken: string,
|
||||||
|
productID: string,
|
||||||
|
action: string
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
window.location.href = `${getPaymentsUrl()}?productID=${productID}&paymentToken=${paymentToken}&action=${action}&rootURL=${
|
window.location.href = `${getPaymentsUrl()}?productID=${productID}&paymentToken=${paymentToken}&action=${action}&rootURL=${
|
||||||
window.location.origin
|
window.location.origin
|
||||||
|
@ -243,7 +259,7 @@ class billingService {
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
'X-Auth-Token': getToken(),
|
'X-Auth-Token': getToken(),
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
window.location.href = response.data.url;
|
window.location.href = response.data.url;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -259,7 +275,7 @@ class billingService {
|
||||||
{ startTime: 0, endTime: Date.now() * 1000 },
|
{ startTime: 0, endTime: Date.now() * 1000 },
|
||||||
{
|
{
|
||||||
'X-Auth-Token': getToken(),
|
'X-Auth-Token': getToken(),
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
return convertToHumanReadable(response.data.usage);
|
return convertToHumanReadable(response.data.usage);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import CryptoWorker from 'utils/crypto';
|
||||||
import { SetDialogMessage } from 'components/MessageDialog';
|
import { SetDialogMessage } from 'components/MessageDialog';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { getPublicKey, User } from './userService';
|
import { getPublicKey, User } from './userService';
|
||||||
import { B64EncryptionResult } from './uploadService';
|
import { B64EncryptionResult } from 'utils/crypto';
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { File } from './fileService';
|
import { File } from './fileService';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
@ -52,7 +52,7 @@ export interface CollectionAndItsLatestFile {
|
||||||
|
|
||||||
const getCollectionWithSecrets = async (
|
const getCollectionWithSecrets = async (
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
masterKey: string,
|
masterKey: string
|
||||||
) => {
|
) => {
|
||||||
const worker = await new CryptoWorker();
|
const worker = await new CryptoWorker();
|
||||||
const userID = getData(LS_KEYS.USER).id;
|
const userID = getData(LS_KEYS.USER).id;
|
||||||
|
@ -61,26 +61,27 @@ const getCollectionWithSecrets = async (
|
||||||
decryptedKey = await worker.decryptB64(
|
decryptedKey = await worker.decryptB64(
|
||||||
collection.encryptedKey,
|
collection.encryptedKey,
|
||||||
collection.keyDecryptionNonce,
|
collection.keyDecryptionNonce,
|
||||||
masterKey,
|
masterKey
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
||||||
const secretKey = await worker.decryptB64(
|
const secretKey = await worker.decryptB64(
|
||||||
keyAttributes.encryptedSecretKey,
|
keyAttributes.encryptedSecretKey,
|
||||||
keyAttributes.secretKeyDecryptionNonce,
|
keyAttributes.secretKeyDecryptionNonce,
|
||||||
masterKey,
|
masterKey
|
||||||
);
|
);
|
||||||
decryptedKey = await worker.boxSealOpen(
|
decryptedKey = await worker.boxSealOpen(
|
||||||
collection.encryptedKey,
|
collection.encryptedKey,
|
||||||
keyAttributes.publicKey,
|
keyAttributes.publicKey,
|
||||||
secretKey,
|
secretKey
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
collection.name = collection.name ||
|
collection.name =
|
||||||
|
collection.name ||
|
||||||
(await worker.decryptToUTF8(
|
(await worker.decryptToUTF8(
|
||||||
collection.encryptedName,
|
collection.encryptedName,
|
||||||
collection.nameDecryptionNonce,
|
collection.nameDecryptionNonce,
|
||||||
decryptedKey,
|
decryptedKey
|
||||||
));
|
));
|
||||||
return {
|
return {
|
||||||
...collection,
|
...collection,
|
||||||
|
@ -91,7 +92,7 @@ const getCollectionWithSecrets = async (
|
||||||
const getCollections = async (
|
const getCollections = async (
|
||||||
token: string,
|
token: string,
|
||||||
sinceTime: number,
|
sinceTime: number,
|
||||||
key: string,
|
key: string
|
||||||
): Promise<Collection[]> => {
|
): Promise<Collection[]> => {
|
||||||
try {
|
try {
|
||||||
const resp = await HTTPService.get(
|
const resp = await HTTPService.get(
|
||||||
|
@ -99,7 +100,7 @@ const getCollections = async (
|
||||||
{
|
{
|
||||||
sinceTime,
|
sinceTime,
|
||||||
},
|
},
|
||||||
{ 'X-Auth-Token': token },
|
{ 'X-Auth-Token': token }
|
||||||
);
|
);
|
||||||
const promises: Promise<Collection>[] = resp.data.collections.map(
|
const promises: Promise<Collection>[] = resp.data.collections.map(
|
||||||
async (collection: Collection) => {
|
async (collection: Collection) => {
|
||||||
|
@ -110,16 +111,16 @@ const getCollections = async (
|
||||||
try {
|
try {
|
||||||
collectionWithSecrets = await getCollectionWithSecrets(
|
collectionWithSecrets = await getCollectionWithSecrets(
|
||||||
collection,
|
collection,
|
||||||
key,
|
key
|
||||||
);
|
);
|
||||||
return collectionWithSecrets;
|
return collectionWithSecrets;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(
|
logError(
|
||||||
e,
|
e,
|
||||||
`decryption failed for collection with id=${collection.id}`,
|
`decryption failed for collection with id=${collection.id}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
return await Promise.all(promises);
|
return await Promise.all(promises);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -129,18 +130,21 @@ const getCollections = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLocalCollections = async (): Promise<Collection[]> => {
|
export const getLocalCollections = async (): Promise<Collection[]> => {
|
||||||
const collections: Collection[] = (await localForage.getItem(COLLECTIONS)) ?? [];
|
const collections: Collection[] =
|
||||||
|
(await localForage.getItem(COLLECTIONS)) ?? [];
|
||||||
return collections;
|
return collections;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCollectionUpdationTime = async (): Promise<number> => (await localForage.getItem<number>(COLLECTION_UPDATION_TIME)) ?? 0;
|
export const getCollectionUpdationTime = async (): Promise<number> =>
|
||||||
|
(await localForage.getItem<number>(COLLECTION_UPDATION_TIME)) ?? 0;
|
||||||
|
|
||||||
export const syncCollections = async () => {
|
export const syncCollections = async () => {
|
||||||
const localCollections = await getLocalCollections();
|
const localCollections = await getLocalCollections();
|
||||||
const lastCollectionUpdationTime = await getCollectionUpdationTime();
|
const lastCollectionUpdationTime = await getCollectionUpdationTime();
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const key = await getActualKey();
|
const key = await getActualKey();
|
||||||
const updatedCollections = (await getCollections(token, lastCollectionUpdationTime, key)) ?? [];
|
const updatedCollections =
|
||||||
|
(await getCollections(token, lastCollectionUpdationTime, key)) ?? [];
|
||||||
if (updatedCollections.length === 0) {
|
if (updatedCollections.length === 0) {
|
||||||
return localCollections;
|
return localCollections;
|
||||||
}
|
}
|
||||||
|
@ -153,7 +157,7 @@ export const syncCollections = async () => {
|
||||||
if (
|
if (
|
||||||
!latestCollectionsInstances.has(collection.id) ||
|
!latestCollectionsInstances.has(collection.id) ||
|
||||||
latestCollectionsInstances.get(collection.id).updationTime <
|
latestCollectionsInstances.get(collection.id).updationTime <
|
||||||
collection.updationTime
|
collection.updationTime
|
||||||
) {
|
) {
|
||||||
latestCollectionsInstances.set(collection.id, collection);
|
latestCollectionsInstances.set(collection.id, collection);
|
||||||
}
|
}
|
||||||
|
@ -161,7 +165,7 @@ export const syncCollections = async () => {
|
||||||
|
|
||||||
const collections: Collection[] = [];
|
const collections: Collection[] = [];
|
||||||
let updationTime = await localForage.getItem<number>(
|
let updationTime = await localForage.getItem<number>(
|
||||||
COLLECTION_UPDATION_TIME,
|
COLLECTION_UPDATION_TIME
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
for (const [_, collection] of latestCollectionsInstances) {
|
for (const [_, collection] of latestCollectionsInstances) {
|
||||||
|
@ -179,7 +183,7 @@ export const syncCollections = async () => {
|
||||||
|
|
||||||
export const getCollectionsAndTheirLatestFile = (
|
export const getCollectionsAndTheirLatestFile = (
|
||||||
collections: Collection[],
|
collections: Collection[],
|
||||||
files: File[],
|
files: File[]
|
||||||
): CollectionAndItsLatestFile[] => {
|
): CollectionAndItsLatestFile[] => {
|
||||||
const latestFile = new Map<number, File>();
|
const latestFile = new Map<number, File>();
|
||||||
|
|
||||||
|
@ -213,15 +217,16 @@ export const getFavItemIds = async (files: File[]): Promise<Set<number>> => {
|
||||||
return new Set(
|
return new Set(
|
||||||
files
|
files
|
||||||
.filter((file) => file.collectionID === favCollection.id)
|
.filter((file) => file.collectionID === favCollection.id)
|
||||||
.map((file): number => file.id),
|
.map((file): number => file.id)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createAlbum = async (albumName: string) => createCollection(albumName, CollectionType.album);
|
export const createAlbum = async (albumName: string) =>
|
||||||
|
createCollection(albumName, CollectionType.album);
|
||||||
|
|
||||||
export const createCollection = async (
|
export const createCollection = async (
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
type: CollectionType,
|
type: CollectionType
|
||||||
): Promise<Collection> => {
|
): Promise<Collection> => {
|
||||||
try {
|
try {
|
||||||
const existingCollections = await syncCollections();
|
const existingCollections = await syncCollections();
|
||||||
|
@ -239,14 +244,14 @@ export const createCollection = async (
|
||||||
nonce: keyDecryptionNonce,
|
nonce: keyDecryptionNonce,
|
||||||
}: B64EncryptionResult = await worker.encryptToB64(
|
}: B64EncryptionResult = await worker.encryptToB64(
|
||||||
collectionKey,
|
collectionKey,
|
||||||
encryptionKey,
|
encryptionKey
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
encryptedData: encryptedName,
|
encryptedData: encryptedName,
|
||||||
nonce: nameDecryptionNonce,
|
nonce: nameDecryptionNonce,
|
||||||
}: B64EncryptionResult = await worker.encryptUTF8(
|
}: B64EncryptionResult = await worker.encryptUTF8(
|
||||||
collectionName,
|
collectionName,
|
||||||
collectionKey,
|
collectionKey
|
||||||
);
|
);
|
||||||
const newCollection: Collection = {
|
const newCollection: Collection = {
|
||||||
id: null,
|
id: null,
|
||||||
|
@ -263,11 +268,11 @@ export const createCollection = async (
|
||||||
};
|
};
|
||||||
let createdCollection: Collection = await postCollection(
|
let createdCollection: Collection = await postCollection(
|
||||||
newCollection,
|
newCollection,
|
||||||
token,
|
token
|
||||||
);
|
);
|
||||||
createdCollection = await getCollectionWithSecrets(
|
createdCollection = await getCollectionWithSecrets(
|
||||||
createdCollection,
|
createdCollection,
|
||||||
encryptionKey,
|
encryptionKey
|
||||||
);
|
);
|
||||||
return createdCollection;
|
return createdCollection;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -278,14 +283,14 @@ export const createCollection = async (
|
||||||
|
|
||||||
const postCollection = async (
|
const postCollection = async (
|
||||||
collectionData: Collection,
|
collectionData: Collection,
|
||||||
token: string,
|
token: string
|
||||||
): Promise<Collection> => {
|
): Promise<Collection> => {
|
||||||
try {
|
try {
|
||||||
const response = await HTTPService.post(
|
const response = await HTTPService.post(
|
||||||
`${ENDPOINT}/collections`,
|
`${ENDPOINT}/collections`,
|
||||||
collectionData,
|
collectionData,
|
||||||
null,
|
null,
|
||||||
{ 'X-Auth-Token': token },
|
{ 'X-Auth-Token': token }
|
||||||
);
|
);
|
||||||
return response.data.collection;
|
return response.data.collection;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -298,7 +303,7 @@ export const addToFavorites = async (file: File) => {
|
||||||
if (!favCollection) {
|
if (!favCollection) {
|
||||||
favCollection = await createCollection(
|
favCollection = await createCollection(
|
||||||
'Favorites',
|
'Favorites',
|
||||||
CollectionType.favorites,
|
CollectionType.favorites
|
||||||
);
|
);
|
||||||
await localForage.setItem(FAV_COLLECTION, favCollection);
|
await localForage.setItem(FAV_COLLECTION, favCollection);
|
||||||
}
|
}
|
||||||
|
@ -312,7 +317,7 @@ export const removeFromFavorites = async (file: File) => {
|
||||||
|
|
||||||
export const addToCollection = async (
|
export const addToCollection = async (
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
files: File[],
|
files: File[]
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const params = {};
|
const params = {};
|
||||||
|
@ -322,7 +327,8 @@ export const addToCollection = async (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
files.map(async (file) => {
|
files.map(async (file) => {
|
||||||
file.collectionID = collection.id;
|
file.collectionID = collection.id;
|
||||||
const newEncryptedKey: B64EncryptionResult = await worker.encryptToB64(file.key, collection.key);
|
const newEncryptedKey: B64EncryptionResult =
|
||||||
|
await worker.encryptToB64(file.key, collection.key);
|
||||||
file.encryptedKey = newEncryptedKey.encryptedData;
|
file.encryptedKey = newEncryptedKey.encryptedData;
|
||||||
file.keyDecryptionNonce = newEncryptedKey.nonce;
|
file.keyDecryptionNonce = newEncryptedKey.nonce;
|
||||||
if (params['files'] === undefined) {
|
if (params['files'] === undefined) {
|
||||||
|
@ -334,13 +340,13 @@ export const addToCollection = async (
|
||||||
keyDecryptionNonce: file.keyDecryptionNonce,
|
keyDecryptionNonce: file.keyDecryptionNonce,
|
||||||
});
|
});
|
||||||
return file;
|
return file;
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
await HTTPService.post(
|
await HTTPService.post(
|
||||||
`${ENDPOINT}/collections/add-files`,
|
`${ENDPOINT}/collections/add-files`,
|
||||||
params,
|
params,
|
||||||
null,
|
null,
|
||||||
{ 'X-Auth-Token': token },
|
{ 'X-Auth-Token': token }
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'Add to collection Failed ');
|
logError(e, 'Add to collection Failed ');
|
||||||
|
@ -357,13 +363,13 @@ const removeFromCollection = async (collection: Collection, files: File[]) => {
|
||||||
params['fileIDs'] = [];
|
params['fileIDs'] = [];
|
||||||
}
|
}
|
||||||
params['fileIDs'].push(file.id);
|
params['fileIDs'].push(file.id);
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
await HTTPService.post(
|
await HTTPService.post(
|
||||||
`${ENDPOINT}/collections/remove-files`,
|
`${ENDPOINT}/collections/remove-files`,
|
||||||
params,
|
params,
|
||||||
null,
|
null,
|
||||||
{ 'X-Auth-Token': token },
|
{ 'X-Auth-Token': token }
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'remove from collection failed ');
|
logError(e, 'remove from collection failed ');
|
||||||
|
@ -374,7 +380,7 @@ export const deleteCollection = async (
|
||||||
collectionID: number,
|
collectionID: number,
|
||||||
syncWithRemote: () => Promise<void>,
|
syncWithRemote: () => Promise<void>,
|
||||||
redirectToAll: () => void,
|
redirectToAll: () => void,
|
||||||
setDialogMessage: SetDialogMessage,
|
setDialogMessage: SetDialogMessage
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
@ -383,7 +389,7 @@ export const deleteCollection = async (
|
||||||
`${ENDPOINT}/collections/${collectionID}`,
|
`${ENDPOINT}/collections/${collectionID}`,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
{ 'X-Auth-Token': token },
|
{ 'X-Auth-Token': token }
|
||||||
);
|
);
|
||||||
await syncWithRemote();
|
await syncWithRemote();
|
||||||
redirectToAll();
|
redirectToAll();
|
||||||
|
@ -399,7 +405,7 @@ export const deleteCollection = async (
|
||||||
|
|
||||||
export const renameCollection = async (
|
export const renameCollection = async (
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
newCollectionName: string,
|
newCollectionName: string
|
||||||
) => {
|
) => {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const worker = await new CryptoWorker();
|
const worker = await new CryptoWorker();
|
||||||
|
@ -408,7 +414,7 @@ export const renameCollection = async (
|
||||||
nonce: nameDecryptionNonce,
|
nonce: nameDecryptionNonce,
|
||||||
}: B64EncryptionResult = await worker.encryptUTF8(
|
}: B64EncryptionResult = await worker.encryptUTF8(
|
||||||
newCollectionName,
|
newCollectionName,
|
||||||
collection.key,
|
collection.key
|
||||||
);
|
);
|
||||||
const collectionRenameRequest = {
|
const collectionRenameRequest = {
|
||||||
collectionID: collection.id,
|
collectionID: collection.id,
|
||||||
|
@ -421,12 +427,12 @@ export const renameCollection = async (
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
'X-Auth-Token': token,
|
'X-Auth-Token': token,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const shareCollection = async (
|
export const shareCollection = async (
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
withUserEmail: string,
|
withUserEmail: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const worker = await new CryptoWorker();
|
const worker = await new CryptoWorker();
|
||||||
|
@ -435,7 +441,7 @@ export const shareCollection = async (
|
||||||
const publicKey: string = await getPublicKey(withUserEmail);
|
const publicKey: string = await getPublicKey(withUserEmail);
|
||||||
const encryptedKey: string = await worker.boxSeal(
|
const encryptedKey: string = await worker.boxSeal(
|
||||||
collection.key,
|
collection.key,
|
||||||
publicKey,
|
publicKey
|
||||||
);
|
);
|
||||||
const shareCollectionRequest = {
|
const shareCollectionRequest = {
|
||||||
collectionID: collection.id,
|
collectionID: collection.id,
|
||||||
|
@ -448,7 +454,7 @@ export const shareCollection = async (
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
'X-Auth-Token': token,
|
'X-Auth-Token': token,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'share collection failed ');
|
logError(e, 'share collection failed ');
|
||||||
|
@ -458,7 +464,7 @@ export const shareCollection = async (
|
||||||
|
|
||||||
export const unshareCollection = async (
|
export const unshareCollection = async (
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
withUserEmail: string,
|
withUserEmail: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
@ -472,7 +478,7 @@ export const unshareCollection = async (
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
'X-Auth-Token': token,
|
'X-Auth-Token': token,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'unshare collection failed ');
|
logError(e, 'unshare collection failed ');
|
||||||
|
@ -492,11 +498,13 @@ export const getFavCollection = async () => {
|
||||||
|
|
||||||
export const getNonEmptyCollections = (
|
export const getNonEmptyCollections = (
|
||||||
collections: Collection[],
|
collections: Collection[],
|
||||||
files: File[],
|
files: File[]
|
||||||
) => {
|
) => {
|
||||||
const nonEmptyCollectionsIds = new Set<number>();
|
const nonEmptyCollectionsIds = new Set<number>();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
nonEmptyCollectionsIds.add(file.collectionID);
|
nonEmptyCollectionsIds.add(file.collectionID);
|
||||||
}
|
}
|
||||||
return collections.filter((collection) => nonEmptyCollectionsIds.has(collection.id));
|
return collections.filter((collection) =>
|
||||||
|
nonEmptyCollectionsIds.has(collection.id)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
import { getToken } from 'utils/common/key';
|
import { getToken } from 'utils/common/key';
|
||||||
import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil';
|
import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil';
|
||||||
import CryptoWorker from 'utils/crypto';
|
import CryptoWorker from 'utils/crypto';
|
||||||
import { fileIsHEIC, convertHEIC2JPEG } from 'utils/file';
|
import {
|
||||||
|
fileIsHEIC,
|
||||||
|
convertHEIC2JPEG,
|
||||||
|
fileNameWithoutExtension,
|
||||||
|
generateStreamFromArrayBuffer,
|
||||||
|
} from 'utils/file';
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { File } from './fileService';
|
import { File, FILE_TYPE } from './fileService';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
import { decodeMotionPhoto } from './motionPhotoService';
|
||||||
|
|
||||||
class DownloadManager {
|
class DownloadManager {
|
||||||
private fileDownloads = new Map<string, string>();
|
private fileDownloads = new Map<string, string>();
|
||||||
|
@ -36,36 +42,50 @@ class DownloadManager {
|
||||||
getThumbnailUrl(file.id),
|
getThumbnailUrl(file.id),
|
||||||
null,
|
null,
|
||||||
{ 'X-Auth-Token': token },
|
{ 'X-Auth-Token': token },
|
||||||
{ responseType: 'arraybuffer' },
|
{ responseType: 'arraybuffer' }
|
||||||
);
|
);
|
||||||
const worker = await new CryptoWorker();
|
const worker = await new CryptoWorker();
|
||||||
const decrypted: any = await worker.decryptThumbnail(
|
const decrypted: any = await worker.decryptThumbnail(
|
||||||
new Uint8Array(resp.data),
|
new Uint8Array(resp.data),
|
||||||
await worker.fromB64(file.thumbnail.decryptionHeader),
|
await worker.fromB64(file.thumbnail.decryptionHeader),
|
||||||
file.key,
|
file.key
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await cache.put(
|
await cache.put(
|
||||||
file.id.toString(),
|
file.id.toString(),
|
||||||
new Response(new Blob([decrypted])),
|
new Response(new Blob([decrypted]))
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO: handle storage full exception.
|
// TODO: handle storage full exception.
|
||||||
}
|
}
|
||||||
return URL.createObjectURL(new Blob([decrypted]));
|
return URL.createObjectURL(new Blob([decrypted]));
|
||||||
}
|
};
|
||||||
|
|
||||||
getFile = async (file: File, forPreview=false) => {
|
getFile = async (file: File, forPreview = false) => {
|
||||||
try {
|
try {
|
||||||
if (!this.fileDownloads.get(`${file.id}_${forPreview}`)) {
|
if (!this.fileDownloads.get(`${file.id}_${forPreview}`)) {
|
||||||
|
// unzip motion photo and return fileBlob of the image for preview
|
||||||
const fileStream = await this.downloadFile(file);
|
const fileStream = await this.downloadFile(file);
|
||||||
let fileBlob= await new Response(fileStream).blob();
|
let fileBlob = await new Response(fileStream).blob();
|
||||||
if (forPreview) {
|
if (forPreview) {
|
||||||
|
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||||
|
const originalName = fileNameWithoutExtension(
|
||||||
|
file.metadata.title
|
||||||
|
);
|
||||||
|
const motionPhoto = await decodeMotionPhoto(
|
||||||
|
fileBlob,
|
||||||
|
originalName
|
||||||
|
);
|
||||||
|
fileBlob = new Blob([motionPhoto.image]);
|
||||||
|
}
|
||||||
if (fileIsHEIC(file.metadata.title)) {
|
if (fileIsHEIC(file.metadata.title)) {
|
||||||
fileBlob = await convertHEIC2JPEG(fileBlob);
|
fileBlob = await convertHEIC2JPEG(fileBlob);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.fileDownloads.set(`${file.id}_${forPreview}`, URL.createObjectURL(fileBlob));
|
this.fileDownloads.set(
|
||||||
|
`${file.id}_${forPreview}`,
|
||||||
|
URL.createObjectURL(fileBlob)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return this.fileDownloads.get(`${file.id}_${forPreview}`);
|
return this.fileDownloads.get(`${file.id}_${forPreview}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -79,25 +99,22 @@ class DownloadManager {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (file.metadata.fileType === 0) {
|
if (
|
||||||
|
file.metadata.fileType === FILE_TYPE.IMAGE ||
|
||||||
|
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO
|
||||||
|
) {
|
||||||
const resp = await HTTPService.get(
|
const resp = await HTTPService.get(
|
||||||
getFileUrl(file.id),
|
getFileUrl(file.id),
|
||||||
null,
|
null,
|
||||||
{ 'X-Auth-Token': token },
|
{ 'X-Auth-Token': token },
|
||||||
{ responseType: 'arraybuffer' },
|
{ responseType: 'arraybuffer' }
|
||||||
);
|
);
|
||||||
const decrypted: any = await worker.decryptFile(
|
const decrypted: any = await worker.decryptFile(
|
||||||
new Uint8Array(resp.data),
|
new Uint8Array(resp.data),
|
||||||
await worker.fromB64(file.file.decryptionHeader),
|
await worker.fromB64(file.file.decryptionHeader),
|
||||||
file.key,
|
file.key
|
||||||
);
|
);
|
||||||
|
return generateStreamFromArrayBuffer(decrypted);
|
||||||
return new ReadableStream({
|
|
||||||
async start(controller: ReadableStreamDefaultController) {
|
|
||||||
controller.enqueue(decrypted);
|
|
||||||
controller.close();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const resp = await fetch(getFileUrl(file.id), {
|
const resp = await fetch(getFileUrl(file.id), {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -108,13 +125,11 @@ class DownloadManager {
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
const decryptionHeader = await worker.fromB64(
|
const decryptionHeader = await worker.fromB64(
|
||||||
file.file.decryptionHeader,
|
file.file.decryptionHeader
|
||||||
);
|
);
|
||||||
const fileKey = await worker.fromB64(file.key);
|
const fileKey = await worker.fromB64(file.key);
|
||||||
const {
|
const { pullState, decryptionChunkSize } =
|
||||||
pullState,
|
await worker.initDecryption(decryptionHeader, fileKey);
|
||||||
decryptionChunkSize,
|
|
||||||
} = await worker.initDecryption(decryptionHeader, fileKey);
|
|
||||||
let data = new Uint8Array();
|
let data = new Uint8Array();
|
||||||
// The following function handles each data chunk
|
// The following function handles each data chunk
|
||||||
function push() {
|
function push() {
|
||||||
|
@ -123,24 +138,20 @@ class DownloadManager {
|
||||||
// Is there more data to read?
|
// Is there more data to read?
|
||||||
if (!done) {
|
if (!done) {
|
||||||
const buffer = new Uint8Array(
|
const buffer = new Uint8Array(
|
||||||
data.byteLength + value.byteLength,
|
data.byteLength + value.byteLength
|
||||||
);
|
);
|
||||||
buffer.set(new Uint8Array(data), 0);
|
buffer.set(new Uint8Array(data), 0);
|
||||||
buffer.set(
|
buffer.set(new Uint8Array(value), data.byteLength);
|
||||||
new Uint8Array(value),
|
|
||||||
data.byteLength,
|
|
||||||
);
|
|
||||||
if (buffer.length > decryptionChunkSize) {
|
if (buffer.length > decryptionChunkSize) {
|
||||||
const fileData = buffer.slice(
|
const fileData = buffer.slice(
|
||||||
0,
|
0,
|
||||||
decryptionChunkSize,
|
decryptionChunkSize
|
||||||
);
|
|
||||||
const {
|
|
||||||
decryptedData,
|
|
||||||
} = await worker.decryptChunk(
|
|
||||||
fileData,
|
|
||||||
pullState,
|
|
||||||
);
|
);
|
||||||
|
const { decryptedData } =
|
||||||
|
await worker.decryptChunk(
|
||||||
|
fileData,
|
||||||
|
pullState
|
||||||
|
);
|
||||||
controller.enqueue(decryptedData);
|
controller.enqueue(decryptedData);
|
||||||
data = buffer.slice(decryptionChunkSize);
|
data = buffer.slice(decryptionChunkSize);
|
||||||
} else {
|
} else {
|
||||||
|
@ -149,12 +160,8 @@ class DownloadManager {
|
||||||
push();
|
push();
|
||||||
} else {
|
} else {
|
||||||
if (data) {
|
if (data) {
|
||||||
const {
|
const { decryptedData } =
|
||||||
decryptedData,
|
await worker.decryptChunk(data, pullState);
|
||||||
} = await worker.decryptChunk(
|
|
||||||
data,
|
|
||||||
pullState,
|
|
||||||
);
|
|
||||||
controller.enqueue(decryptedData);
|
controller.enqueue(decryptedData);
|
||||||
data = null;
|
data = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,26 @@
|
||||||
import { retryAsyncFunction, runningInBrowser } from 'utils/common';
|
import { retryAsyncFunction, runningInBrowser } from 'utils/common';
|
||||||
import { getExportPendingFiles, getExportFailedFiles, getFilesUploadedAfterLastExport, getFileUID, dedupe, getGoogleLikeMetadataFile } from 'utils/export';
|
import {
|
||||||
|
getExportPendingFiles,
|
||||||
|
getExportFailedFiles,
|
||||||
|
getFilesUploadedAfterLastExport,
|
||||||
|
getExportRecordFileUID,
|
||||||
|
dedupe,
|
||||||
|
getGoogleLikeMetadataFile,
|
||||||
|
} from 'utils/export';
|
||||||
|
import {
|
||||||
|
fileNameWithoutExtension,
|
||||||
|
generateStreamFromArrayBuffer,
|
||||||
|
} from 'utils/file';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||||
import { Collection, getLocalCollections } from './collectionService';
|
import {
|
||||||
|
Collection,
|
||||||
|
getLocalCollections,
|
||||||
|
getNonEmptyCollections,
|
||||||
|
} from './collectionService';
|
||||||
import downloadManager from './downloadManager';
|
import downloadManager from './downloadManager';
|
||||||
import { File, getLocalFiles } from './fileService';
|
import { File, FILE_TYPE, getLocalFiles } from './fileService';
|
||||||
|
import { decodeMotionPhoto } from './motionPhotoService';
|
||||||
|
|
||||||
export interface ExportProgress {
|
export interface ExportProgress {
|
||||||
current: number;
|
current: number;
|
||||||
|
@ -16,7 +32,7 @@ export interface ExportStats {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportRecord {
|
export interface ExportRecord {
|
||||||
stage: ExportStage
|
stage: ExportStage;
|
||||||
lastAttemptTimestamp: number;
|
lastAttemptTimestamp: number;
|
||||||
progress: ExportProgress;
|
progress: ExportProgress;
|
||||||
queuedFiles: string[];
|
queuedFiles: string[];
|
||||||
|
@ -27,7 +43,7 @@ export enum ExportStage {
|
||||||
INIT,
|
INIT,
|
||||||
INPROGRESS,
|
INPROGRESS,
|
||||||
PAUSED,
|
PAUSED,
|
||||||
FINISHED
|
FINISHED,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ExportNotification {
|
enum ExportNotification {
|
||||||
|
@ -37,26 +53,26 @@ enum ExportNotification {
|
||||||
FAILED = 'export failed',
|
FAILED = 'export failed',
|
||||||
ABORT = 'export aborted',
|
ABORT = 'export aborted',
|
||||||
PAUSE = 'export paused',
|
PAUSE = 'export paused',
|
||||||
UP_TO_DATE = `no new files to export`
|
UP_TO_DATE = `no new files to export`,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RecordType {
|
enum RecordType {
|
||||||
SUCCESS = 'success',
|
SUCCESS = 'success',
|
||||||
FAILED = 'failed'
|
FAILED = 'failed',
|
||||||
}
|
}
|
||||||
export enum ExportType {
|
export enum ExportType {
|
||||||
NEW,
|
NEW,
|
||||||
PENDING,
|
PENDING,
|
||||||
RETRY_FAILED
|
RETRY_FAILED,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExportRecordFileName='export_status.json';
|
const EXPORT_RECORD_FILE_NAME = 'export_status.json';
|
||||||
const MetadataFolderName='metadata';
|
export const METADATA_FOLDER_NAME = 'metadata';
|
||||||
|
|
||||||
class ExportService {
|
class ExportService {
|
||||||
ElectronAPIs: any;
|
ElectronAPIs: any;
|
||||||
|
|
||||||
private exportInProgress: Promise<{ paused: boolean; }> = null;
|
private exportInProgress: Promise<{ paused: boolean }> = null;
|
||||||
private recordUpdateInProgress = Promise.resolve();
|
private recordUpdateInProgress = Promise.resolve();
|
||||||
private stopExport: boolean = false;
|
private stopExport: boolean = false;
|
||||||
private pauseExport: boolean = false;
|
private pauseExport: boolean = false;
|
||||||
|
@ -73,7 +89,10 @@ class ExportService {
|
||||||
pauseRunningExport() {
|
pauseRunningExport() {
|
||||||
this.pauseExport = true;
|
this.pauseExport = true;
|
||||||
}
|
}
|
||||||
async exportFiles(updateProgress: (progress: ExportProgress) => void, exportType: ExportType) {
|
async exportFiles(
|
||||||
|
updateProgress: (progress: ExportProgress) => void,
|
||||||
|
exportType: ExportType
|
||||||
|
) {
|
||||||
if (this.exportInProgress) {
|
if (this.exportInProgress) {
|
||||||
this.ElectronAPIs.sendNotification(ExportNotification.IN_PROGRESS);
|
this.ElectronAPIs.sendNotification(ExportNotification.IN_PROGRESS);
|
||||||
return this.exportInProgress;
|
return this.exportInProgress;
|
||||||
|
@ -87,25 +106,44 @@ class ExportService {
|
||||||
let filesToExport: File[];
|
let filesToExport: File[];
|
||||||
const allFiles = await getLocalFiles();
|
const allFiles = await getLocalFiles();
|
||||||
const collections = await getLocalCollections();
|
const collections = await getLocalCollections();
|
||||||
|
const nonEmptyCollections = getNonEmptyCollections(
|
||||||
|
collections,
|
||||||
|
allFiles
|
||||||
|
);
|
||||||
const exportRecord = await this.getExportRecord(exportDir);
|
const exportRecord = await this.getExportRecord(exportDir);
|
||||||
|
|
||||||
if (exportType === ExportType.NEW) {
|
if (exportType === ExportType.NEW) {
|
||||||
filesToExport = await getFilesUploadedAfterLastExport(allFiles, exportRecord);
|
filesToExport = await getFilesUploadedAfterLastExport(
|
||||||
|
allFiles,
|
||||||
|
exportRecord
|
||||||
|
);
|
||||||
} else if (exportType === ExportType.RETRY_FAILED) {
|
} else if (exportType === ExportType.RETRY_FAILED) {
|
||||||
filesToExport = await getExportFailedFiles(allFiles, exportRecord);
|
filesToExport = await getExportFailedFiles(allFiles, exportRecord);
|
||||||
} else {
|
} else {
|
||||||
filesToExport = await getExportPendingFiles(allFiles, exportRecord);
|
filesToExport = await getExportPendingFiles(allFiles, exportRecord);
|
||||||
}
|
}
|
||||||
this.exportInProgress = this.fileExporter(filesToExport, collections, updateProgress, exportDir);
|
this.exportInProgress = this.fileExporter(
|
||||||
|
filesToExport,
|
||||||
|
nonEmptyCollections,
|
||||||
|
updateProgress,
|
||||||
|
exportDir
|
||||||
|
);
|
||||||
const resp = await this.exportInProgress;
|
const resp = await this.exportInProgress;
|
||||||
this.exportInProgress = null;
|
this.exportInProgress = null;
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fileExporter(files: File[], collections: Collection[], updateProgress: (progress: ExportProgress,) => void, dir: string): Promise<{ paused: boolean }> {
|
async fileExporter(
|
||||||
|
files: File[],
|
||||||
|
collections: Collection[],
|
||||||
|
updateProgress: (progress: ExportProgress) => void,
|
||||||
|
dir: string
|
||||||
|
): Promise<{ paused: boolean }> {
|
||||||
try {
|
try {
|
||||||
if (!files?.length) {
|
if (!files?.length) {
|
||||||
this.ElectronAPIs.sendNotification(ExportNotification.UP_TO_DATE);
|
this.ElectronAPIs.sendNotification(
|
||||||
|
ExportNotification.UP_TO_DATE
|
||||||
|
);
|
||||||
return { paused: false };
|
return { paused: false };
|
||||||
}
|
}
|
||||||
this.stopExport = false;
|
this.stopExport = false;
|
||||||
|
@ -114,22 +152,24 @@ class ExportService {
|
||||||
const failedFileCount = 0;
|
const failedFileCount = 0;
|
||||||
|
|
||||||
this.ElectronAPIs.showOnTray({
|
this.ElectronAPIs.showOnTray({
|
||||||
export_progress:
|
export_progress: `0 / ${files.length} files exported`,
|
||||||
`0 / ${files.length} files exported`,
|
|
||||||
});
|
});
|
||||||
updateProgress({
|
updateProgress({
|
||||||
current: 0, total: files.length,
|
current: 0,
|
||||||
|
total: files.length,
|
||||||
});
|
});
|
||||||
this.ElectronAPIs.sendNotification(ExportNotification.START);
|
this.ElectronAPIs.sendNotification(ExportNotification.START);
|
||||||
|
|
||||||
const collectionIDMap = new Map<number, string>();
|
const collectionIDMap = new Map<number, string>();
|
||||||
for (const collection of collections) {
|
for (const collection of collections) {
|
||||||
const collectionFolderPath = `${dir}/${collection.id}_${this.sanitizeName(collection.name)}`;
|
const collectionFolderPath = `${dir}/${
|
||||||
|
collection.id
|
||||||
|
}_${this.sanitizeName(collection.name)}`;
|
||||||
await this.ElectronAPIs.checkExistsAndCreateCollectionDir(
|
await this.ElectronAPIs.checkExistsAndCreateCollectionDir(
|
||||||
collectionFolderPath,
|
collectionFolderPath
|
||||||
);
|
);
|
||||||
await this.ElectronAPIs.checkExistsAndCreateCollectionDir(
|
await this.ElectronAPIs.checkExistsAndCreateCollectionDir(
|
||||||
`${collectionFolderPath}/${MetadataFolderName}`,
|
`${collectionFolderPath}/${METADATA_FOLDER_NAME}`
|
||||||
);
|
);
|
||||||
collectionIDMap.set(collection.id, collectionFolderPath);
|
collectionIDMap.set(collection.id, collectionFolderPath);
|
||||||
}
|
}
|
||||||
|
@ -137,8 +177,7 @@ class ExportService {
|
||||||
if (this.stopExport || this.pauseExport) {
|
if (this.stopExport || this.pauseExport) {
|
||||||
if (this.pauseExport) {
|
if (this.pauseExport) {
|
||||||
this.ElectronAPIs.showOnTray({
|
this.ElectronAPIs.showOnTray({
|
||||||
export_progress:
|
export_progress: `${index} / ${files.length} files exported (paused)`,
|
||||||
`${index} / ${files.length} files exported (paused)`,
|
|
||||||
paused: true,
|
paused: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -147,39 +186,42 @@ class ExportService {
|
||||||
const collectionPath = collectionIDMap.get(file.collectionID);
|
const collectionPath = collectionIDMap.get(file.collectionID);
|
||||||
try {
|
try {
|
||||||
await this.downloadAndSave(file, collectionPath);
|
await this.downloadAndSave(file, collectionPath);
|
||||||
await this.addFileExportRecord(dir, file, RecordType.SUCCESS);
|
await this.addFileExportRecord(
|
||||||
|
dir,
|
||||||
|
file,
|
||||||
|
RecordType.SUCCESS
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.addFileExportRecord(dir, file, RecordType.FAILED);
|
await this.addFileExportRecord(
|
||||||
logError(e, 'download and save failed for file during export');
|
dir,
|
||||||
|
file,
|
||||||
|
RecordType.FAILED
|
||||||
|
);
|
||||||
|
logError(
|
||||||
|
e,
|
||||||
|
'download and save failed for file during export'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.ElectronAPIs.showOnTray({
|
this.ElectronAPIs.showOnTray({
|
||||||
export_progress:
|
export_progress: `${index + 1} / ${
|
||||||
`${index + 1} / ${files.length} files exported`,
|
files.length
|
||||||
|
} files exported`,
|
||||||
});
|
});
|
||||||
updateProgress({ current: index + 1, total: files.length });
|
updateProgress({ current: index + 1, total: files.length });
|
||||||
}
|
}
|
||||||
if (this.stopExport) {
|
if (this.stopExport) {
|
||||||
this.ElectronAPIs.sendNotification(
|
this.ElectronAPIs.sendNotification(ExportNotification.ABORT);
|
||||||
ExportNotification.ABORT,
|
|
||||||
);
|
|
||||||
this.ElectronAPIs.showOnTray();
|
this.ElectronAPIs.showOnTray();
|
||||||
} else if (this.pauseExport) {
|
} else if (this.pauseExport) {
|
||||||
this.ElectronAPIs.sendNotification(
|
this.ElectronAPIs.sendNotification(ExportNotification.PAUSE);
|
||||||
ExportNotification.PAUSE,
|
|
||||||
);
|
|
||||||
return { paused: true };
|
return { paused: true };
|
||||||
} else if (failedFileCount > 0) {
|
} else if (failedFileCount > 0) {
|
||||||
this.ElectronAPIs.sendNotification(
|
this.ElectronAPIs.sendNotification(ExportNotification.FAILED);
|
||||||
ExportNotification.FAILED,
|
|
||||||
);
|
|
||||||
this.ElectronAPIs.showOnTray({
|
this.ElectronAPIs.showOnTray({
|
||||||
retry_export:
|
retry_export: `export failed - retry export`,
|
||||||
`export failed - retry export`,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.ElectronAPIs.sendNotification(
|
this.ElectronAPIs.sendNotification(ExportNotification.FINISH);
|
||||||
ExportNotification.FINISH,
|
|
||||||
);
|
|
||||||
this.ElectronAPIs.showOnTray();
|
this.ElectronAPIs.showOnTray();
|
||||||
}
|
}
|
||||||
return { paused: false };
|
return { paused: false };
|
||||||
|
@ -189,20 +231,25 @@ class ExportService {
|
||||||
}
|
}
|
||||||
async addFilesQueuedRecord(folder: string, files: File[]) {
|
async addFilesQueuedRecord(folder: string, files: File[]) {
|
||||||
const exportRecord = await this.getExportRecord(folder);
|
const exportRecord = await this.getExportRecord(folder);
|
||||||
exportRecord.queuedFiles = files.map(getFileUID);
|
exportRecord.queuedFiles = files.map(getExportRecordFileUID);
|
||||||
await this.updateExportRecord(exportRecord, folder);
|
await this.updateExportRecord(exportRecord, folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addFileExportRecord(folder: string, file: File, type: RecordType) {
|
async addFileExportRecord(folder: string, file: File, type: RecordType) {
|
||||||
const fileUID = getFileUID(file);
|
const fileUID = getExportRecordFileUID(file);
|
||||||
const exportRecord = await this.getExportRecord(folder);
|
const exportRecord = await this.getExportRecord(folder);
|
||||||
exportRecord.queuedFiles = exportRecord.queuedFiles.filter((queuedFilesUID) => queuedFilesUID !== fileUID);
|
exportRecord.queuedFiles = exportRecord.queuedFiles.filter(
|
||||||
|
(queuedFilesUID) => queuedFilesUID !== fileUID
|
||||||
|
);
|
||||||
if (type === RecordType.SUCCESS) {
|
if (type === RecordType.SUCCESS) {
|
||||||
if (!exportRecord.exportedFiles) {
|
if (!exportRecord.exportedFiles) {
|
||||||
exportRecord.exportedFiles = [];
|
exportRecord.exportedFiles = [];
|
||||||
}
|
}
|
||||||
exportRecord.exportedFiles.push(fileUID);
|
exportRecord.exportedFiles.push(fileUID);
|
||||||
exportRecord.failedFiles && (exportRecord.failedFiles = exportRecord.failedFiles.filter((FailedFileUID) => FailedFileUID !== fileUID));
|
exportRecord.failedFiles &&
|
||||||
|
(exportRecord.failedFiles = exportRecord.failedFiles.filter(
|
||||||
|
(FailedFileUID) => FailedFileUID !== fileUID
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
if (!exportRecord.failedFiles) {
|
if (!exportRecord.failedFiles) {
|
||||||
exportRecord.failedFiles = [];
|
exportRecord.failedFiles = [];
|
||||||
|
@ -226,7 +273,10 @@ class ExportService {
|
||||||
}
|
}
|
||||||
const exportRecord = await this.getExportRecord(folder);
|
const exportRecord = await this.getExportRecord(folder);
|
||||||
const newRecord = { ...exportRecord, ...newData };
|
const newRecord = { ...exportRecord, ...newData };
|
||||||
await this.ElectronAPIs.setExportRecord(`${folder}/${ExportRecordFileName}`, JSON.stringify(newRecord, null, 2));
|
await this.ElectronAPIs.setExportRecord(
|
||||||
|
`${folder}/${EXPORT_RECORD_FILE_NAME}`,
|
||||||
|
JSON.stringify(newRecord, null, 2)
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'error updating Export Record');
|
logError(e, 'error updating Export Record');
|
||||||
}
|
}
|
||||||
|
@ -239,7 +289,9 @@ class ExportService {
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
folder = getData(LS_KEYS.EXPORT)?.folder;
|
folder = getData(LS_KEYS.EXPORT)?.folder;
|
||||||
}
|
}
|
||||||
const recordFile = await this.ElectronAPIs.getExportRecord(`${folder}/${ExportRecordFileName}`);
|
const recordFile = await this.ElectronAPIs.getExportRecord(
|
||||||
|
`${folder}/${EXPORT_RECORD_FILE_NAME}`
|
||||||
|
);
|
||||||
if (recordFile) {
|
if (recordFile) {
|
||||||
return JSON.parse(recordFile);
|
return JSON.parse(recordFile);
|
||||||
} else {
|
} else {
|
||||||
|
@ -250,15 +302,49 @@ class ExportService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadAndSave(file: File, collectionPath:string) {
|
async downloadAndSave(file: File, collectionPath: string) {
|
||||||
const uid = `${file.id}_${this.sanitizeName(
|
const uid = `${file.id}_${this.sanitizeName(file.metadata.title)}`;
|
||||||
file.metadata.title,
|
const fileStream = await retryAsyncFunction(() =>
|
||||||
)}`;
|
downloadManager.downloadFile(file)
|
||||||
const fileStream = await retryAsyncFunction(()=>downloadManager.downloadFile(file));
|
);
|
||||||
this.ElectronAPIs.saveStreamToDisk(`${collectionPath}/${uid}`, fileStream);
|
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||||
|
this.exportMotionPhoto(fileStream, file, collectionPath);
|
||||||
|
} else {
|
||||||
|
this.saveMediaFile(collectionPath, uid, fileStream);
|
||||||
|
this.saveMetadataFile(collectionPath, uid, file.metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async exportMotionPhoto(
|
||||||
|
fileStream: ReadableStream<any>,
|
||||||
|
file: File,
|
||||||
|
collectionPath: string
|
||||||
|
) {
|
||||||
|
const fileBlob = await new Response(fileStream).blob();
|
||||||
|
const originalName = fileNameWithoutExtension(file.metadata.title);
|
||||||
|
const motionPhoto = await decodeMotionPhoto(fileBlob, originalName);
|
||||||
|
|
||||||
|
const imageStream = generateStreamFromArrayBuffer(motionPhoto.image);
|
||||||
|
const imageUID = `${file.id}_${motionPhoto.imageNameTitle}`;
|
||||||
|
this.saveMediaFile(collectionPath, imageUID, imageStream);
|
||||||
|
this.saveMetadataFile(collectionPath, imageUID, file.metadata);
|
||||||
|
|
||||||
|
const videoStream = generateStreamFromArrayBuffer(motionPhoto.video);
|
||||||
|
const videoUID = `${file.id}_${motionPhoto.videoNameTitle}`;
|
||||||
|
this.saveMediaFile(collectionPath, videoUID, videoStream);
|
||||||
|
this.saveMetadataFile(collectionPath, videoUID, file.metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveMediaFile(collectionPath, uid, fileStream) {
|
||||||
|
this.ElectronAPIs.saveStreamToDisk(
|
||||||
|
`${collectionPath}/${uid}`,
|
||||||
|
fileStream
|
||||||
|
);
|
||||||
|
}
|
||||||
|
private saveMetadataFile(collectionPath, uid, metadata) {
|
||||||
this.ElectronAPIs.saveFileToDisk(
|
this.ElectronAPIs.saveFileToDisk(
|
||||||
`${collectionPath}/${MetadataFolderName}/${uid}.json`,
|
`${collectionPath}/${METADATA_FOLDER_NAME}/${uid}.json`,
|
||||||
getGoogleLikeMetadataFile(uid, file.metadata),
|
getGoogleLikeMetadataFile(uid, metadata)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,6 +354,6 @@ class ExportService {
|
||||||
|
|
||||||
isExportInProgress = () => {
|
isExportInProgress = () => {
|
||||||
return this.exportInProgress !== null;
|
return this.exportInProgress !== null;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
export default new ExportService();
|
export default new ExportService();
|
||||||
|
|
|
@ -19,6 +19,13 @@ export interface fileAttribute {
|
||||||
decryptionHeader: string;
|
decryptionHeader: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum FILE_TYPE {
|
||||||
|
IMAGE,
|
||||||
|
VIDEO,
|
||||||
|
LIVE_PHOTO,
|
||||||
|
OTHERS,
|
||||||
|
}
|
||||||
|
|
||||||
export interface File {
|
export interface File {
|
||||||
id: number;
|
id: number;
|
||||||
collectionID: number;
|
collectionID: number;
|
||||||
|
@ -43,7 +50,10 @@ export const getLocalFiles = async () => {
|
||||||
return files;
|
return files;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const syncFiles = async (collections: Collection[], setFiles: (files: File[]) => void) => {
|
export const syncFiles = async (
|
||||||
|
collections: Collection[],
|
||||||
|
setFiles: (files: File[]) => void
|
||||||
|
) => {
|
||||||
const localFiles = await getLocalFiles();
|
const localFiles = await getLocalFiles();
|
||||||
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
||||||
if (files.length !== localFiles.length) {
|
if (files.length !== localFiles.length) {
|
||||||
|
@ -54,11 +64,19 @@ export const syncFiles = async (collections: Collection[], setFiles: (files: Fil
|
||||||
if (!getToken()) {
|
if (!getToken()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const lastSyncTime = (await localForage.getItem<number>(`${collection.id}-time`)) ?? 0;
|
const lastSyncTime =
|
||||||
|
(await localForage.getItem<number>(`${collection.id}-time`)) ?? 0;
|
||||||
if (collection.updationTime === lastSyncTime) {
|
if (collection.updationTime === lastSyncTime) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const fetchedFiles = (await getFiles(collection, lastSyncTime, DIFF_LIMIT, files, setFiles)) ?? [];
|
const fetchedFiles =
|
||||||
|
(await getFiles(
|
||||||
|
collection,
|
||||||
|
lastSyncTime,
|
||||||
|
DIFF_LIMIT,
|
||||||
|
files,
|
||||||
|
setFiles
|
||||||
|
)) ?? [];
|
||||||
files.push(...fetchedFiles);
|
files.push(...fetchedFiles);
|
||||||
const latestVersionFiles = new Map<string, File>();
|
const latestVersionFiles = new Map<string, File>();
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
|
@ -78,17 +96,19 @@ export const syncFiles = async (collections: Collection[], setFiles: (files: Fil
|
||||||
}
|
}
|
||||||
files.push(file);
|
files.push(file);
|
||||||
}
|
}
|
||||||
files=sortFiles(files);
|
files = sortFiles(files);
|
||||||
await localForage.setItem('files', files);
|
await localForage.setItem('files', files);
|
||||||
await localForage.setItem(
|
await localForage.setItem(
|
||||||
`${collection.id}-time`,
|
`${collection.id}-time`,
|
||||||
collection.updationTime,
|
collection.updationTime
|
||||||
|
);
|
||||||
|
setFiles(
|
||||||
|
files.map((item) => ({
|
||||||
|
...item,
|
||||||
|
w: window.innerWidth,
|
||||||
|
h: window.innerHeight,
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
setFiles(files.map((item) => ({
|
|
||||||
...item,
|
|
||||||
w: window.innerWidth,
|
|
||||||
h: window.innerHeight,
|
|
||||||
})));
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
files: files.map((item) => ({
|
files: files.map((item) => ({
|
||||||
|
@ -104,11 +124,12 @@ export const getFiles = async (
|
||||||
sinceTime: number,
|
sinceTime: number,
|
||||||
limit: number,
|
limit: number,
|
||||||
files: File[],
|
files: File[],
|
||||||
setFiles: (files: File[]) => void,
|
setFiles: (files: File[]) => void
|
||||||
): Promise<File[]> => {
|
): Promise<File[]> => {
|
||||||
try {
|
try {
|
||||||
const decryptedFiles: File[] = [];
|
const decryptedFiles: File[] = [];
|
||||||
let time = sinceTime ||
|
let time =
|
||||||
|
sinceTime ||
|
||||||
(await localForage.getItem<number>(`${collection.id}-time`)) ||
|
(await localForage.getItem<number>(`${collection.id}-time`)) ||
|
||||||
0;
|
0;
|
||||||
let resp;
|
let resp;
|
||||||
|
@ -126,7 +147,7 @@ export const getFiles = async (
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'X-Auth-Token': token,
|
'X-Auth-Token': token,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
decryptedFiles.push(
|
decryptedFiles.push(
|
||||||
|
@ -136,16 +157,21 @@ export const getFiles = async (
|
||||||
file = await decryptFile(file, collection);
|
file = await decryptFile(file, collection);
|
||||||
}
|
}
|
||||||
return file;
|
return file;
|
||||||
}) as Promise<File>[],
|
}) as Promise<File>[]
|
||||||
)),
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (resp.data.diff.length) {
|
if (resp.data.diff.length) {
|
||||||
time = resp.data.diff.slice(-1)[0].updationTime;
|
time = resp.data.diff.slice(-1)[0].updationTime;
|
||||||
}
|
}
|
||||||
setFiles([...(files || []), ...decryptedFiles].filter((item) => !item.isDeleted).sort(
|
setFiles(
|
||||||
(a, b) => b.metadata.creationTime - a.metadata.creationTime,
|
[...(files || []), ...decryptedFiles]
|
||||||
));
|
.filter((item) => !item.isDeleted)
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
b.metadata.creationTime - a.metadata.creationTime
|
||||||
|
)
|
||||||
|
);
|
||||||
} while (resp.data.diff.length === limit);
|
} while (resp.data.diff.length === limit);
|
||||||
return decryptedFiles;
|
return decryptedFiles;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -155,7 +181,7 @@ export const getFiles = async (
|
||||||
|
|
||||||
const removeDeletedCollectionFiles = async (
|
const removeDeletedCollectionFiles = async (
|
||||||
collections: Collection[],
|
collections: Collection[],
|
||||||
files: File[],
|
files: File[]
|
||||||
) => {
|
) => {
|
||||||
const syncedCollectionIds = new Set<number>();
|
const syncedCollectionIds = new Set<number>();
|
||||||
for (const collection of collections) {
|
for (const collection of collections) {
|
||||||
|
@ -168,7 +194,7 @@ const removeDeletedCollectionFiles = async (
|
||||||
export const deleteFiles = async (
|
export const deleteFiles = async (
|
||||||
filesToDelete: number[],
|
filesToDelete: number[],
|
||||||
clearSelection: Function,
|
clearSelection: Function,
|
||||||
syncWithRemote: Function,
|
syncWithRemote: Function
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
@ -181,7 +207,7 @@ export const deleteFiles = async (
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
'X-Auth-Token': token,
|
'X-Auth-Token': token,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
clearSelection();
|
clearSelection();
|
||||||
syncWithRemote();
|
syncWithRemote();
|
||||||
|
|
34
src/services/motionPhotoService.ts
Normal file
34
src/services/motionPhotoService.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
import { fileExtensionWithDot } from 'utils/file';
|
||||||
|
|
||||||
|
class MotionPhoto {
|
||||||
|
image: Uint8Array;
|
||||||
|
video: Uint8Array;
|
||||||
|
imageNameTitle: String;
|
||||||
|
videoNameTitle: String;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decodeMotionPhoto = async (
|
||||||
|
zipBlob: Blob,
|
||||||
|
originalName: string
|
||||||
|
) => {
|
||||||
|
const zip = await JSZip.loadAsync(zipBlob, { createFolders: true });
|
||||||
|
|
||||||
|
const motionPhoto = new MotionPhoto();
|
||||||
|
for (const zipFilename in zip.files) {
|
||||||
|
if (zipFilename.startsWith('image')) {
|
||||||
|
motionPhoto.imageNameTitle =
|
||||||
|
originalName + fileExtensionWithDot(zipFilename);
|
||||||
|
motionPhoto.image = await zip.files[zipFilename].async(
|
||||||
|
'uint8array'
|
||||||
|
);
|
||||||
|
} else if (zipFilename.startsWith('video')) {
|
||||||
|
motionPhoto.videoNameTitle =
|
||||||
|
originalName + fileExtensionWithDot(zipFilename);
|
||||||
|
motionPhoto.video = await zip.files[zipFilename].async(
|
||||||
|
'uint8array'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return motionPhoto;
|
||||||
|
};
|
|
@ -31,14 +31,15 @@ export function parseHumanDate(humanDate: string): DateValue[] {
|
||||||
return dates.reverse();
|
return dates.reverse();
|
||||||
}
|
}
|
||||||
return dates;
|
return dates;
|
||||||
} if (date1) {
|
}
|
||||||
|
if (date1) {
|
||||||
return [{ month: date1.getMonth() }];
|
return [{ month: date1.getMonth() }];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchLocation(
|
export async function searchLocation(
|
||||||
searchPhrase: string,
|
searchPhrase: string
|
||||||
): Promise<LocationSearchResponse[]> {
|
): Promise<LocationSearchResponse[]> {
|
||||||
const resp = await HTTPService.get(
|
const resp = await HTTPService.get(
|
||||||
`${ENDPOINT}/search/location`,
|
`${ENDPOINT}/search/location`,
|
||||||
|
@ -48,7 +49,7 @@ export async function searchLocation(
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'X-Auth-Token': getToken(),
|
'X-Auth-Token': getToken(),
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
return resp.data.results;
|
return resp.data.results;
|
||||||
}
|
}
|
||||||
|
@ -75,7 +76,9 @@ export function getHolidaySuggestion(searchPhrase: string): Suggestion[] {
|
||||||
value: { month: 11, date: 31 },
|
value: { month: 11, date: 31 },
|
||||||
type: SuggestionType.DATE,
|
type: SuggestionType.DATE,
|
||||||
},
|
},
|
||||||
].filter((suggestion) => suggestion.label.toLowerCase().includes(searchPhrase.toLowerCase()));
|
].filter((suggestion) =>
|
||||||
|
suggestion.label.toLowerCase().includes(searchPhrase.toLowerCase())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getYearSuggestion(searchPhrase: string): Suggestion[] {
|
export function getYearSuggestion(searchPhrase: string): Suggestion[] {
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
import { getEndpoint } from 'utils/common/apiUtil';
|
import { getEndpoint } from 'utils/common/apiUtil';
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import EXIF from 'exif-js';
|
import EXIF from 'exif-js';
|
||||||
import { File, fileAttribute } from './fileService';
|
import { File, fileAttribute, FILE_TYPE } from './fileService';
|
||||||
import { Collection } from './collectionService';
|
import { Collection } from './collectionService';
|
||||||
import { FILE_TYPE, SetFiles } from 'pages/gallery';
|
import { SetFiles } from 'pages/gallery';
|
||||||
import { retryAsyncFunction, sleep } from 'utils/common';
|
import { retryAsyncFunction, sleep } from 'utils/common';
|
||||||
import {
|
import { handleError, parseError, CustomError } from 'utils/common/errorUtil';
|
||||||
handleError,
|
|
||||||
parseError,
|
|
||||||
THUMBNAIL_GENERATION_FAILED,
|
|
||||||
} from 'utils/common/errorUtil';
|
|
||||||
import { ComlinkWorker, getDedicatedCryptoWorker } from 'utils/crypto';
|
import { ComlinkWorker, getDedicatedCryptoWorker } from 'utils/crypto';
|
||||||
import * as convert from 'xml-js';
|
import * as convert from 'xml-js';
|
||||||
import { ENCRYPTION_CHUNK_SIZE } from 'types';
|
import { ENCRYPTION_CHUNK_SIZE } from 'types';
|
||||||
|
@ -46,10 +42,12 @@ const FILE_UPLOAD_COMPLETED = 100;
|
||||||
const EDITED_FILE_SUFFIX = '-edited';
|
const EDITED_FILE_SUFFIX = '-edited';
|
||||||
const TwoSecondInMillSeconds = 2000;
|
const TwoSecondInMillSeconds = 2000;
|
||||||
|
|
||||||
export enum FileUploadErrorCode {
|
export enum FileUploadResults {
|
||||||
FAILED = -1,
|
FAILED = -1,
|
||||||
SKIPPED = -2,
|
SKIPPED = -2,
|
||||||
UNSUPPORTED = -3,
|
UNSUPPORTED = -3,
|
||||||
|
BLOCKED = -4,
|
||||||
|
UPLOADED = 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Location {
|
interface Location {
|
||||||
|
@ -119,7 +117,7 @@ interface ProcessedFile {
|
||||||
metadata: fileAttribute;
|
metadata: fileAttribute;
|
||||||
filename: string;
|
filename: string;
|
||||||
}
|
}
|
||||||
interface BackupedFile extends Omit<ProcessedFile, 'filename'> { }
|
interface BackupedFile extends Omit<ProcessedFile, 'filename'> {}
|
||||||
|
|
||||||
interface uploadFile extends BackupedFile {
|
interface uploadFile extends BackupedFile {
|
||||||
collectionID: number;
|
collectionID: number;
|
||||||
|
@ -142,31 +140,34 @@ class UploadService {
|
||||||
private filesCompleted: number;
|
private filesCompleted: number;
|
||||||
private totalFileCount: number;
|
private totalFileCount: number;
|
||||||
private fileProgress: Map<string, number>;
|
private fileProgress: Map<string, number>;
|
||||||
|
private uploadResult: Map<string, number>;
|
||||||
private metadataMap: Map<string, Object>;
|
private metadataMap: Map<string, Object>;
|
||||||
private filesToBeUploaded: FileWithCollection[];
|
private filesToBeUploaded: FileWithCollection[];
|
||||||
private progressBarProps;
|
private progressBarProps;
|
||||||
private failedFiles: FileWithCollection[];
|
private failedFiles: FileWithCollection[];
|
||||||
private existingFilesCollectionWise: Map<number, File[]>;
|
private existingFilesCollectionWise: Map<number, File[]>;
|
||||||
private existingFiles: File[];
|
private existingFiles: File[];
|
||||||
private setFiles:SetFiles;
|
private setFiles: SetFiles;
|
||||||
public async uploadFiles(
|
public async uploadFiles(
|
||||||
filesWithCollectionToUpload: FileWithCollection[],
|
filesWithCollectionToUpload: FileWithCollection[],
|
||||||
existingFiles: File[],
|
existingFiles: File[],
|
||||||
progressBarProps,
|
progressBarProps,
|
||||||
setFiles:SetFiles,
|
setFiles: SetFiles
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
progressBarProps.setUploadStage(UPLOAD_STAGES.START);
|
progressBarProps.setUploadStage(UPLOAD_STAGES.START);
|
||||||
|
|
||||||
this.filesCompleted = 0;
|
this.filesCompleted = 0;
|
||||||
this.fileProgress = new Map<string, number>();
|
this.fileProgress = new Map<string, number>();
|
||||||
|
this.uploadResult = new Map<string, number>();
|
||||||
this.failedFiles = [];
|
this.failedFiles = [];
|
||||||
this.metadataMap = new Map<string, object>();
|
this.metadataMap = new Map<string, object>();
|
||||||
this.progressBarProps = progressBarProps;
|
this.progressBarProps = progressBarProps;
|
||||||
this.existingFiles=existingFiles;
|
this.existingFiles = existingFiles;
|
||||||
this.existingFilesCollectionWise = sortFilesIntoCollections(existingFiles);
|
this.existingFilesCollectionWise =
|
||||||
|
sortFilesIntoCollections(existingFiles);
|
||||||
this.updateProgressBarUI();
|
this.updateProgressBarUI();
|
||||||
this.setFiles=setFiles;
|
this.setFiles = setFiles;
|
||||||
const metadataFiles: globalThis.File[] = [];
|
const metadataFiles: globalThis.File[] = [];
|
||||||
const actualFiles: FileWithCollection[] = [];
|
const actualFiles: FileWithCollection[] = [];
|
||||||
filesWithCollectionToUpload.forEach((fileWithCollection) => {
|
filesWithCollectionToUpload.forEach((fileWithCollection) => {
|
||||||
|
@ -184,7 +185,7 @@ class UploadService {
|
||||||
this.filesToBeUploaded = actualFiles;
|
this.filesToBeUploaded = actualFiles;
|
||||||
|
|
||||||
progressBarProps.setUploadStage(
|
progressBarProps.setUploadStage(
|
||||||
UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES,
|
UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES
|
||||||
);
|
);
|
||||||
this.totalFileCount = metadataFiles.length;
|
this.totalFileCount = metadataFiles.length;
|
||||||
this.perFileProgress = 100 / metadataFiles.length;
|
this.perFileProgress = 100 / metadataFiles.length;
|
||||||
|
@ -211,20 +212,16 @@ class UploadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const uploadProcesses = [];
|
const uploadProcesses = [];
|
||||||
for (
|
for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) {
|
||||||
let i = 0;
|
if (this.filesToBeUploaded.length > 0) {
|
||||||
i < MAX_CONCURRENT_UPLOADS;
|
const fileWithCollection = this.filesToBeUploaded.pop();
|
||||||
i++
|
|
||||||
) {
|
|
||||||
if (this.filesToBeUploaded.length>0) {
|
|
||||||
const fileWithCollection= this.filesToBeUploaded.pop();
|
|
||||||
this.cryptoWorkers[i] = getDedicatedCryptoWorker();
|
this.cryptoWorkers[i] = getDedicatedCryptoWorker();
|
||||||
uploadProcesses.push(
|
uploadProcesses.push(
|
||||||
this.uploader(
|
this.uploader(
|
||||||
await new this.cryptoWorkers[i].comlink(),
|
await new this.cryptoWorkers[i].comlink(),
|
||||||
new FileReader(),
|
new FileReader(),
|
||||||
fileWithCollection,
|
fileWithCollection
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -246,75 +243,97 @@ class UploadService {
|
||||||
private async uploader(
|
private async uploader(
|
||||||
worker: any,
|
worker: any,
|
||||||
reader: FileReader,
|
reader: FileReader,
|
||||||
fileWithCollection: FileWithCollection,
|
fileWithCollection: FileWithCollection
|
||||||
) {
|
) {
|
||||||
const { file: rawFile, collection } = fileWithCollection;
|
const { file: rawFile, collection } = fileWithCollection;
|
||||||
this.fileProgress.set(rawFile.name, 0);
|
this.fileProgress.set(rawFile.name, 0);
|
||||||
this.updateProgressBarUI();
|
this.updateProgressBarUI();
|
||||||
|
let file: FileInMemory = null;
|
||||||
|
let encryptedFile: EncryptedFile = null;
|
||||||
try {
|
try {
|
||||||
let file: FileInMemory = await this.readFile(reader, rawFile);
|
// read the file into memory
|
||||||
|
file = await this.readFile(reader, rawFile);
|
||||||
|
|
||||||
if (this.fileAlreadyInCollection(file, collection)) {
|
if (this.fileAlreadyInCollection(file, collection)) {
|
||||||
// set progress to -2 indicating that file upload was skipped
|
// set progress to -2 indicating that file upload was skipped
|
||||||
this.fileProgress.set(rawFile.name, FileUploadErrorCode.SKIPPED);
|
this.fileProgress.set(rawFile.name, FileUploadResults.SKIPPED);
|
||||||
this.updateProgressBarUI();
|
this.updateProgressBarUI();
|
||||||
await sleep(TwoSecondInMillSeconds);
|
await sleep(TwoSecondInMillSeconds);
|
||||||
// remove completed files for file progress list
|
|
||||||
this.fileProgress.delete(rawFile.name);
|
|
||||||
} else {
|
} else {
|
||||||
let encryptedFile: EncryptedFile =
|
encryptedFile = await this.encryptFile(
|
||||||
await this.encryptFile(worker, file, collection.key);
|
worker,
|
||||||
|
file,
|
||||||
|
collection.key
|
||||||
|
);
|
||||||
|
|
||||||
let backupedFile: BackupedFile = await this.uploadToBucket(
|
const backupedFile: BackupedFile = await this.uploadToBucket(
|
||||||
encryptedFile.file,
|
encryptedFile.file
|
||||||
);
|
);
|
||||||
|
|
||||||
let uploadFile: uploadFile = this.getUploadFile(
|
let uploadFile: uploadFile = this.getUploadFile(
|
||||||
collection,
|
collection,
|
||||||
backupedFile,
|
backupedFile,
|
||||||
encryptedFile.fileKey,
|
encryptedFile.fileKey
|
||||||
);
|
);
|
||||||
|
|
||||||
encryptedFile = null;
|
const uploadedFile = await this.uploadFile(uploadFile);
|
||||||
backupedFile = null;
|
const decryptedFile = await decryptFile(
|
||||||
|
uploadedFile,
|
||||||
const uploadedFile =await this.uploadFile(uploadFile);
|
collection
|
||||||
const decryptedFile=await decryptFile(uploadedFile, collection);
|
);
|
||||||
|
|
||||||
this.existingFiles.push(decryptedFile);
|
this.existingFiles.push(decryptedFile);
|
||||||
this.existingFiles=sortFiles(this.existingFiles);
|
this.existingFiles = sortFiles(this.existingFiles);
|
||||||
await localForage.setItem('files', removeUnneccessaryFileProps(this.existingFiles));
|
await localForage.setItem(
|
||||||
|
'files',
|
||||||
|
removeUnneccessaryFileProps(this.existingFiles)
|
||||||
|
);
|
||||||
this.setFiles(this.existingFiles);
|
this.setFiles(this.existingFiles);
|
||||||
|
|
||||||
file = null;
|
|
||||||
uploadFile = null;
|
uploadFile = null;
|
||||||
|
this.fileProgress.set(rawFile.name, FileUploadResults.UPLOADED);
|
||||||
this.fileProgress.delete(rawFile.name);
|
|
||||||
this.filesCompleted++;
|
this.filesCompleted++;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'file upload failed');
|
logError(e, 'file upload failed');
|
||||||
this.failedFiles.push(fileWithCollection);
|
|
||||||
// set progress to -1 indicating that file upload failed but keep it to show in the file-upload list progress
|
|
||||||
this.fileProgress.set(rawFile.name, FileUploadErrorCode.FAILED);
|
|
||||||
handleError(e);
|
handleError(e);
|
||||||
|
this.failedFiles.push(fileWithCollection);
|
||||||
|
if (e.message === CustomError.ETAG_MISSING) {
|
||||||
|
this.fileProgress.set(rawFile.name, FileUploadResults.BLOCKED);
|
||||||
|
} else {
|
||||||
|
this.fileProgress.set(rawFile.name, FileUploadResults.FAILED);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
file = null;
|
||||||
|
encryptedFile = null;
|
||||||
}
|
}
|
||||||
|
this.uploadResult.set(
|
||||||
|
rawFile.name,
|
||||||
|
this.fileProgress.get(rawFile.name)
|
||||||
|
);
|
||||||
|
this.fileProgress.delete(rawFile.name);
|
||||||
this.updateProgressBarUI();
|
this.updateProgressBarUI();
|
||||||
|
|
||||||
if (this.filesToBeUploaded.length > 0) {
|
if (this.filesToBeUploaded.length > 0) {
|
||||||
await this.uploader(
|
await this.uploader(worker, reader, this.filesToBeUploaded.pop());
|
||||||
worker,
|
|
||||||
reader,
|
|
||||||
this.filesToBeUploaded.pop(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async retryFailedFiles(localFiles:File[]) {
|
async retryFailedFiles(localFiles: File[]) {
|
||||||
await this.uploadFiles(this.failedFiles, localFiles, this.progressBarProps, this.setFiles);
|
await this.uploadFiles(
|
||||||
|
this.failedFiles,
|
||||||
|
localFiles,
|
||||||
|
this.progressBarProps,
|
||||||
|
this.setFiles
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateProgressBarUI() {
|
private updateProgressBarUI() {
|
||||||
const { setPercentComplete, setFileCounter, setFileProgress } =
|
const {
|
||||||
this.progressBarProps;
|
setPercentComplete,
|
||||||
|
setFileCounter,
|
||||||
|
setFileProgress,
|
||||||
|
setUploadResult,
|
||||||
|
} = this.progressBarProps;
|
||||||
setFileCounter({
|
setFileCounter({
|
||||||
finished: this.filesCompleted,
|
finished: this.filesCompleted,
|
||||||
total: this.totalFileCount,
|
total: this.totalFileCount,
|
||||||
|
@ -332,11 +351,12 @@ class UploadService {
|
||||||
}
|
}
|
||||||
setPercentComplete(percentComplete);
|
setPercentComplete(percentComplete);
|
||||||
setFileProgress(this.fileProgress);
|
setFileProgress(this.fileProgress);
|
||||||
|
setUploadResult(this.uploadResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fileAlreadyInCollection(
|
private fileAlreadyInCollection(
|
||||||
newFile: FileInMemory,
|
newFile: FileInMemory,
|
||||||
collection: Collection,
|
collection: Collection
|
||||||
): boolean {
|
): boolean {
|
||||||
const collectionFiles =
|
const collectionFiles =
|
||||||
this.existingFilesCollectionWise.get(collection.id) ?? [];
|
this.existingFilesCollectionWise.get(collection.id) ?? [];
|
||||||
|
@ -349,7 +369,7 @@ class UploadService {
|
||||||
}
|
}
|
||||||
private areFilesSame(
|
private areFilesSame(
|
||||||
existingFile: MetadataObject,
|
existingFile: MetadataObject,
|
||||||
newFile: MetadataObject,
|
newFile: MetadataObject
|
||||||
): boolean {
|
): boolean {
|
||||||
if (
|
if (
|
||||||
existingFile.fileType === newFile.fileType &&
|
existingFile.fileType === newFile.fileType &&
|
||||||
|
@ -365,10 +385,8 @@ class UploadService {
|
||||||
|
|
||||||
private async readFile(reader: FileReader, receivedFile: globalThis.File) {
|
private async readFile(reader: FileReader, receivedFile: globalThis.File) {
|
||||||
try {
|
try {
|
||||||
const { thumbnail, hasStaticThumbnail } = await this.generateThumbnail(
|
const { thumbnail, hasStaticThumbnail } =
|
||||||
reader,
|
await this.generateThumbnail(reader, receivedFile);
|
||||||
receivedFile,
|
|
||||||
);
|
|
||||||
|
|
||||||
let fileType: FILE_TYPE;
|
let fileType: FILE_TYPE;
|
||||||
switch (receivedFile.type.split('/')[0]) {
|
switch (receivedFile.type.split('/')[0]) {
|
||||||
|
@ -392,13 +410,13 @@ class UploadService {
|
||||||
const { location, creationTime } = await this.getExifData(
|
const { location, creationTime } = await this.getExifData(
|
||||||
reader,
|
reader,
|
||||||
receivedFile,
|
receivedFile,
|
||||||
fileType,
|
fileType
|
||||||
);
|
);
|
||||||
let receivedFileOriginalName = receivedFile.name;
|
let receivedFileOriginalName = receivedFile.name;
|
||||||
if (receivedFile.name.endsWith(EDITED_FILE_SUFFIX)) {
|
if (receivedFile.name.endsWith(EDITED_FILE_SUFFIX)) {
|
||||||
receivedFileOriginalName = receivedFile.name.slice(
|
receivedFileOriginalName = receivedFile.name.slice(
|
||||||
0,
|
0,
|
||||||
-1 * EDITED_FILE_SUFFIX.length,
|
-1 * EDITED_FILE_SUFFIX.length
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const metadata = Object.assign(
|
const metadata = Object.assign(
|
||||||
|
@ -411,15 +429,15 @@ class UploadService {
|
||||||
longitude: location?.latitude,
|
longitude: location?.latitude,
|
||||||
fileType,
|
fileType,
|
||||||
},
|
},
|
||||||
this.metadataMap.get(receivedFileOriginalName),
|
this.metadataMap.get(receivedFileOriginalName)
|
||||||
);
|
);
|
||||||
if (hasStaticThumbnail) {
|
if (hasStaticThumbnail) {
|
||||||
metadata['hasStaticThumbnail'] = hasStaticThumbnail;
|
metadata['hasStaticThumbnail'] = hasStaticThumbnail;
|
||||||
}
|
}
|
||||||
const filedata =
|
const filedata =
|
||||||
receivedFile.size > MIN_STREAM_FILE_SIZE ?
|
receivedFile.size > MIN_STREAM_FILE_SIZE
|
||||||
this.getFileStream(reader, receivedFile) :
|
? this.getFileStream(reader, receivedFile)
|
||||||
await this.getUint8ArrayView(reader, receivedFile);
|
: await this.getUint8ArrayView(reader, receivedFile);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filedata,
|
filedata,
|
||||||
|
@ -435,13 +453,13 @@ class UploadService {
|
||||||
private async encryptFile(
|
private async encryptFile(
|
||||||
worker: any,
|
worker: any,
|
||||||
file: FileInMemory,
|
file: FileInMemory,
|
||||||
encryptionKey: string,
|
encryptionKey: string
|
||||||
): Promise<EncryptedFile> {
|
): Promise<EncryptedFile> {
|
||||||
try {
|
try {
|
||||||
const { key: fileKey, file: encryptedFiledata }: EncryptionResult =
|
const { key: fileKey, file: encryptedFiledata }: EncryptionResult =
|
||||||
isDataStream(file.filedata) ?
|
isDataStream(file.filedata)
|
||||||
await this.encryptFileStream(worker, file.filedata) :
|
? await this.encryptFileStream(worker, file.filedata)
|
||||||
await worker.encryptFile(file.filedata);
|
: await worker.encryptFile(file.filedata);
|
||||||
|
|
||||||
const { file: encryptedThumbnail }: EncryptionResult =
|
const { file: encryptedThumbnail }: EncryptionResult =
|
||||||
await worker.encryptThumbnail(file.thumbnail, fileKey);
|
await worker.encryptThumbnail(file.thumbnail, fileKey);
|
||||||
|
@ -450,7 +468,7 @@ class UploadService {
|
||||||
|
|
||||||
const encryptedKey: B64EncryptionResult = await worker.encryptToB64(
|
const encryptedKey: B64EncryptionResult = await worker.encryptToB64(
|
||||||
fileKey,
|
fileKey,
|
||||||
encryptionKey,
|
encryptionKey
|
||||||
);
|
);
|
||||||
|
|
||||||
const result: EncryptedFile = {
|
const result: EncryptedFile = {
|
||||||
|
@ -481,7 +499,7 @@ class UploadService {
|
||||||
const encryptedFileChunk = await worker.encryptFileChunk(
|
const encryptedFileChunk = await worker.encryptFileChunk(
|
||||||
value,
|
value,
|
||||||
pushState,
|
pushState,
|
||||||
ref.pullCount === chunkCount,
|
ref.pullCount === chunkCount
|
||||||
);
|
);
|
||||||
controller.enqueue(encryptedFileChunk);
|
controller.enqueue(encryptedFileChunk);
|
||||||
if (ref.pullCount === chunkCount) {
|
if (ref.pullCount === chunkCount) {
|
||||||
|
@ -505,30 +523,30 @@ class UploadService {
|
||||||
if (isDataStream(file.file.encryptedData)) {
|
if (isDataStream(file.file.encryptedData)) {
|
||||||
const { chunkCount, stream } = file.file.encryptedData;
|
const { chunkCount, stream } = file.file.encryptedData;
|
||||||
const uploadPartCount = Math.ceil(
|
const uploadPartCount = Math.ceil(
|
||||||
chunkCount / CHUNKS_COMBINED_FOR_UPLOAD,
|
chunkCount / CHUNKS_COMBINED_FOR_UPLOAD
|
||||||
);
|
);
|
||||||
const filePartUploadURLs = await this.fetchMultipartUploadURLs(
|
const filePartUploadURLs = await this.fetchMultipartUploadURLs(
|
||||||
uploadPartCount,
|
uploadPartCount
|
||||||
);
|
);
|
||||||
fileObjectKey = await this.putFileInParts(
|
fileObjectKey = await this.putFileInParts(
|
||||||
filePartUploadURLs,
|
filePartUploadURLs,
|
||||||
stream,
|
stream,
|
||||||
file.filename,
|
file.filename,
|
||||||
uploadPartCount,
|
uploadPartCount
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const fileUploadURL = await this.getUploadURL();
|
const fileUploadURL = await this.getUploadURL();
|
||||||
fileObjectKey = await this.putFile(
|
fileObjectKey = await this.putFile(
|
||||||
fileUploadURL,
|
fileUploadURL,
|
||||||
file.file.encryptedData,
|
file.file.encryptedData,
|
||||||
file.filename,
|
file.filename
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const thumbnailUploadURL = await this.getUploadURL();
|
const thumbnailUploadURL = await this.getUploadURL();
|
||||||
const thumbnailObjectKey = await this.putFile(
|
const thumbnailObjectKey = await this.putFile(
|
||||||
thumbnailUploadURL,
|
thumbnailUploadURL,
|
||||||
file.thumbnail.encryptedData as Uint8Array,
|
file.thumbnail.encryptedData as Uint8Array,
|
||||||
null,
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
const backupedFile: BackupedFile = {
|
const backupedFile: BackupedFile = {
|
||||||
|
@ -552,7 +570,7 @@ class UploadService {
|
||||||
private getUploadFile(
|
private getUploadFile(
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
backupedFile: BackupedFile,
|
backupedFile: BackupedFile,
|
||||||
fileKey: B64EncryptionResult,
|
fileKey: B64EncryptionResult
|
||||||
): uploadFile {
|
): uploadFile {
|
||||||
const uploadFile: uploadFile = {
|
const uploadFile: uploadFile = {
|
||||||
collectionID: collection.id,
|
collectionID: collection.id,
|
||||||
|
@ -564,20 +582,17 @@ class UploadService {
|
||||||
return uploadFile;
|
return uploadFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async uploadFile(uploadFile: uploadFile):Promise<File> {
|
private async uploadFile(uploadFile: uploadFile): Promise<File> {
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const response = await retryAsyncFunction(()=>HTTPService.post(
|
const response = await retryAsyncFunction(() =>
|
||||||
`${ENDPOINT}/files`,
|
HTTPService.post(`${ENDPOINT}/files`, uploadFile, null, {
|
||||||
uploadFile,
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
'X-Auth-Token': token,
|
'X-Auth-Token': token,
|
||||||
},
|
})
|
||||||
));
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'upload Files Failed');
|
logError(e, 'upload Files Failed');
|
||||||
|
@ -590,17 +605,19 @@ class UploadService {
|
||||||
const metadataJSON: object = await new Promise(
|
const metadataJSON: object = await new Promise(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onabort = () => reject(Error('file reading was aborted'));
|
reader.onabort = () =>
|
||||||
reader.onerror = () => reject(Error('file reading has failed'));
|
reject(Error('file reading was aborted'));
|
||||||
|
reader.onerror = () =>
|
||||||
|
reject(Error('file reading has failed'));
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const result =
|
const result =
|
||||||
typeof reader.result !== 'string' ?
|
typeof reader.result !== 'string'
|
||||||
new TextDecoder().decode(reader.result) :
|
? new TextDecoder().decode(reader.result)
|
||||||
reader.result;
|
: reader.result;
|
||||||
resolve(JSON.parse(result));
|
resolve(JSON.parse(result));
|
||||||
};
|
};
|
||||||
reader.readAsText(receivedFile);
|
reader.readAsText(receivedFile);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const metaDataObject = {};
|
const metaDataObject = {};
|
||||||
|
@ -647,8 +664,8 @@ class UploadService {
|
||||||
}
|
}
|
||||||
private async generateThumbnail(
|
private async generateThumbnail(
|
||||||
reader: FileReader,
|
reader: FileReader,
|
||||||
file: globalThis.File,
|
file: globalThis.File
|
||||||
): Promise<{ thumbnail: Uint8Array, hasStaticThumbnail: boolean }> {
|
): Promise<{ thumbnail: Uint8Array; hasStaticThumbnail: boolean }> {
|
||||||
try {
|
try {
|
||||||
let hasStaticThumbnail = false;
|
let hasStaticThumbnail = false;
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
|
@ -662,7 +679,7 @@ class UploadService {
|
||||||
file = new globalThis.File(
|
file = new globalThis.File(
|
||||||
[await convertHEIC2JPEG(file)],
|
[await convertHEIC2JPEG(file)],
|
||||||
null,
|
null,
|
||||||
null,
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let image = new Image();
|
let image = new Image();
|
||||||
|
@ -672,7 +689,8 @@ class UploadService {
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
try {
|
try {
|
||||||
const thumbnailWidth =
|
const thumbnailWidth =
|
||||||
(image.width * THUMBNAIL_HEIGHT) / image.height;
|
(image.width * THUMBNAIL_HEIGHT) /
|
||||||
|
image.height;
|
||||||
canvas.width = thumbnailWidth;
|
canvas.width = thumbnailWidth;
|
||||||
canvas.height = THUMBNAIL_HEIGHT;
|
canvas.height = THUMBNAIL_HEIGHT;
|
||||||
canvas_CTX.drawImage(
|
canvas_CTX.drawImage(
|
||||||
|
@ -680,7 +698,7 @@ class UploadService {
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
thumbnailWidth,
|
thumbnailWidth,
|
||||||
THUMBNAIL_HEIGHT,
|
THUMBNAIL_HEIGHT
|
||||||
);
|
);
|
||||||
image = null;
|
image = null;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
@ -688,15 +706,23 @@ class UploadService {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
logError(e);
|
logError(e);
|
||||||
reject(Error(`${THUMBNAIL_GENERATION_FAILED} err: ${e}`));
|
reject(
|
||||||
|
Error(
|
||||||
|
`${CustomError.THUMBNAIL_GENERATION_FAILED} err: ${e}`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
timeout = setTimeout(
|
timeout = setTimeout(
|
||||||
() =>
|
() =>
|
||||||
reject(
|
reject(
|
||||||
Error(`wait time exceeded for format ${file.name.split('.').slice(-1)[0]}`),
|
Error(
|
||||||
|
`wait time exceeded for format ${
|
||||||
|
file.name.split('.').slice(-1)[0]
|
||||||
|
}`
|
||||||
|
)
|
||||||
),
|
),
|
||||||
WAIT_TIME_THUMBNAIL_GENERATION,
|
WAIT_TIME_THUMBNAIL_GENERATION
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -718,7 +744,7 @@ class UploadService {
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
thumbnailWidth,
|
thumbnailWidth,
|
||||||
THUMBNAIL_HEIGHT,
|
THUMBNAIL_HEIGHT
|
||||||
);
|
);
|
||||||
video = null;
|
video = null;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
@ -726,16 +752,26 @@ class UploadService {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
logError(e);
|
logError(e);
|
||||||
reject(Error(`${THUMBNAIL_GENERATION_FAILED} err: ${e}`));
|
reject(
|
||||||
|
Error(
|
||||||
|
`${CustomError.THUMBNAIL_GENERATION_FAILED} err: ${e}`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
video.preload = 'metadata';
|
video.preload = 'metadata';
|
||||||
video.src = imageURL;
|
video.src = imageURL;
|
||||||
video.currentTime = 3;
|
video.currentTime = 3;
|
||||||
setTimeout(
|
timeout = setTimeout(
|
||||||
() =>
|
() =>
|
||||||
reject(Error(`wait time exceeded for format ${file.name.split('.').slice(-1)[0]}`)),
|
reject(
|
||||||
WAIT_TIME_THUMBNAIL_GENERATION,
|
Error(
|
||||||
|
`wait time exceeded for format ${
|
||||||
|
file.name.split('.').slice(-1)[0]
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
),
|
||||||
|
WAIT_TIME_THUMBNAIL_GENERATION
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -758,7 +794,7 @@ class UploadService {
|
||||||
resolve(blob);
|
resolve(blob);
|
||||||
},
|
},
|
||||||
'image/jpeg',
|
'image/jpeg',
|
||||||
quality,
|
quality
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
thumbnailBlob = thumbnailBlob ?? new Blob([]);
|
thumbnailBlob = thumbnailBlob ?? new Blob([]);
|
||||||
|
@ -768,7 +804,7 @@ class UploadService {
|
||||||
);
|
);
|
||||||
const thumbnail = await this.getUint8ArrayView(
|
const thumbnail = await this.getUint8ArrayView(
|
||||||
reader,
|
reader,
|
||||||
thumbnailBlob,
|
thumbnailBlob
|
||||||
);
|
);
|
||||||
return { thumbnail, hasStaticThumbnail };
|
return { thumbnail, hasStaticThumbnail };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -781,7 +817,7 @@ class UploadService {
|
||||||
const self = this;
|
const self = this;
|
||||||
const fileChunkReader = (async function* fileChunkReaderMaker(
|
const fileChunkReader = (async function* fileChunkReaderMaker(
|
||||||
fileSize,
|
fileSize,
|
||||||
self,
|
self
|
||||||
) {
|
) {
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
while (offset < fileSize) {
|
while (offset < fileSize) {
|
||||||
|
@ -809,18 +845,19 @@ class UploadService {
|
||||||
|
|
||||||
private async getUint8ArrayView(
|
private async getUint8ArrayView(
|
||||||
reader: FileReader,
|
reader: FileReader,
|
||||||
file: Blob,
|
file: Blob
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
try {
|
try {
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
reader.onabort = () => reject(Error('file reading was aborted'));
|
reader.onabort = () =>
|
||||||
|
reject(Error('file reading was aborted'));
|
||||||
reader.onerror = () => reject(Error('file reading has failed'));
|
reader.onerror = () => reject(Error('file reading has failed'));
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
// Do whatever you want with the file contents
|
// Do whatever you want with the file contents
|
||||||
const result =
|
const result =
|
||||||
typeof reader.result === 'string' ?
|
typeof reader.result === 'string'
|
||||||
new TextEncoder().encode(reader.result) :
|
? new TextEncoder().encode(reader.result)
|
||||||
new Uint8Array(reader.result);
|
: new Uint8Array(reader.result);
|
||||||
resolve(result);
|
resolve(result);
|
||||||
};
|
};
|
||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(file);
|
||||||
|
@ -851,10 +888,10 @@ class UploadService {
|
||||||
{
|
{
|
||||||
count: Math.min(
|
count: Math.min(
|
||||||
MAX_URL_REQUESTS,
|
MAX_URL_REQUESTS,
|
||||||
(this.totalFileCount - this.filesCompleted) * 2,
|
(this.totalFileCount - this.filesCompleted) * 2
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ 'X-Auth-Token': token },
|
{ 'X-Auth-Token': token }
|
||||||
);
|
);
|
||||||
const response = await this.uploadURLFetchInProgress;
|
const response = await this.uploadURLFetchInProgress;
|
||||||
this.uploadURLs.push(...response.data['urls']);
|
this.uploadURLs.push(...response.data['urls']);
|
||||||
|
@ -870,7 +907,7 @@ class UploadService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchMultipartUploadURLs(
|
private async fetchMultipartUploadURLs(
|
||||||
count: number,
|
count: number
|
||||||
): Promise<MultipartUploadURLs> {
|
): Promise<MultipartUploadURLs> {
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
@ -882,7 +919,7 @@ class UploadService {
|
||||||
{
|
{
|
||||||
count,
|
count,
|
||||||
},
|
},
|
||||||
{ 'X-Auth-Token': token },
|
{ 'X-Auth-Token': token }
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data['urls'];
|
return response.data['urls'];
|
||||||
|
@ -895,17 +932,17 @@ class UploadService {
|
||||||
private async putFile(
|
private async putFile(
|
||||||
fileUploadURL: UploadURL,
|
fileUploadURL: UploadURL,
|
||||||
file: Uint8Array,
|
file: Uint8Array,
|
||||||
filename: string,
|
filename: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
await retryAsyncFunction(()=>
|
await retryAsyncFunction(() =>
|
||||||
HTTPService.put(
|
HTTPService.put(
|
||||||
fileUploadURL.url,
|
fileUploadURL.url,
|
||||||
file,
|
file,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
this.trackUploadProgress(filename),
|
this.trackUploadProgress(filename)
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
return fileUploadURL.objectKey;
|
return fileUploadURL.objectKey;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -918,12 +955,12 @@ class UploadService {
|
||||||
multipartUploadURLs: MultipartUploadURLs,
|
multipartUploadURLs: MultipartUploadURLs,
|
||||||
file: ReadableStream<Uint8Array>,
|
file: ReadableStream<Uint8Array>,
|
||||||
filename: string,
|
filename: string,
|
||||||
uploadPartCount: number,
|
uploadPartCount: number
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const streamEncryptedFileReader = file.getReader();
|
const streamEncryptedFileReader = file.getReader();
|
||||||
const percentPerPart = Math.round(
|
const percentPerPart = Math.round(
|
||||||
RANDOM_PERCENTAGE_PROGRESS_FOR_PUT() / uploadPartCount,
|
RANDOM_PERCENTAGE_PROGRESS_FOR_PUT() / uploadPartCount
|
||||||
);
|
);
|
||||||
const resParts = [];
|
const resParts = [];
|
||||||
for (const [
|
for (const [
|
||||||
|
@ -942,15 +979,25 @@ class UploadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const uploadChunk = Uint8Array.from(combinedChunks);
|
const uploadChunk = Uint8Array.from(combinedChunks);
|
||||||
const response = await retryAsyncFunction(()=>
|
const response = await retryAsyncFunction(async () => {
|
||||||
HTTPService.put(
|
const resp = await HTTPService.put(
|
||||||
fileUploadURL,
|
fileUploadURL,
|
||||||
uploadChunk,
|
uploadChunk,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
this.trackUploadProgress(filename, percentPerPart, index),
|
this.trackUploadProgress(
|
||||||
),
|
filename,
|
||||||
);
|
percentPerPart,
|
||||||
|
index
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (!resp?.headers?.etag) {
|
||||||
|
const err = Error(CustomError.ETAG_MISSING);
|
||||||
|
logError(err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
});
|
||||||
resParts.push({
|
resParts.push({
|
||||||
PartNumber: index + 1,
|
PartNumber: index + 1,
|
||||||
ETag: response.headers.etag,
|
ETag: response.headers.etag,
|
||||||
|
@ -959,12 +1006,12 @@ class UploadService {
|
||||||
const options = { compact: true, ignoreComment: true, spaces: 4 };
|
const options = { compact: true, ignoreComment: true, spaces: 4 };
|
||||||
const body = convert.js2xml(
|
const body = convert.js2xml(
|
||||||
{ CompleteMultipartUpload: { Part: resParts } },
|
{ CompleteMultipartUpload: { Part: resParts } },
|
||||||
options,
|
options
|
||||||
);
|
);
|
||||||
await retryAsyncFunction(()=>
|
await retryAsyncFunction(() =>
|
||||||
HTTPService.post(multipartUploadURLs.completeURL, body, null, {
|
HTTPService.post(multipartUploadURLs.completeURL, body, null, {
|
||||||
'content-type': 'text/xml',
|
'content-type': 'text/xml',
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
return multipartUploadURLs.objectKey;
|
return multipartUploadURLs.objectKey;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -976,15 +1023,15 @@ class UploadService {
|
||||||
private trackUploadProgress(
|
private trackUploadProgress(
|
||||||
filename,
|
filename,
|
||||||
percentPerPart = RANDOM_PERCENTAGE_PROGRESS_FOR_PUT(),
|
percentPerPart = RANDOM_PERCENTAGE_PROGRESS_FOR_PUT(),
|
||||||
index = 0,
|
index = 0
|
||||||
) {
|
) {
|
||||||
const cancel={ exec: null };
|
const cancel = { exec: null };
|
||||||
let timeout=null;
|
let timeout = null;
|
||||||
const resetTimeout=()=>{
|
const resetTimeout = () => {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
timeout=setTimeout(()=>cancel.exec(), 30*1000);
|
timeout = setTimeout(() => cancel.exec(), 30 * 1000);
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
cancel,
|
cancel,
|
||||||
|
@ -995,14 +1042,14 @@ class UploadService {
|
||||||
Math.min(
|
Math.min(
|
||||||
Math.round(
|
Math.round(
|
||||||
percentPerPart * index +
|
percentPerPart * index +
|
||||||
(percentPerPart * event.loaded) /
|
(percentPerPart * event.loaded) /
|
||||||
event.total,
|
event.total
|
||||||
),
|
),
|
||||||
98,
|
98
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
this.updateProgressBarUI();
|
this.updateProgressBarUI();
|
||||||
if (event.loaded===event.total) {
|
if (event.loaded === event.total) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
} else {
|
} else {
|
||||||
resetTimeout();
|
resetTimeout();
|
||||||
|
@ -1013,7 +1060,7 @@ class UploadService {
|
||||||
private async getExifData(
|
private async getExifData(
|
||||||
reader: FileReader,
|
reader: FileReader,
|
||||||
receivedFile: globalThis.File,
|
receivedFile: globalThis.File,
|
||||||
fileType: FILE_TYPE,
|
fileType: FILE_TYPE
|
||||||
): Promise<ParsedEXIFData> {
|
): Promise<ParsedEXIFData> {
|
||||||
try {
|
try {
|
||||||
if (fileType === FILE_TYPE.VIDEO) {
|
if (fileType === FILE_TYPE.VIDEO) {
|
||||||
|
@ -1039,15 +1086,16 @@ class UploadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private getUNIXTime(exifData: any) {
|
private getUNIXTime(exifData: any) {
|
||||||
const dateString: string = exifData.DateTimeOriginal || exifData.DateTime;
|
const dateString: string =
|
||||||
if (!dateString || dateString==='0000:00:00 00:00:00') {
|
exifData.DateTimeOriginal || exifData.DateTime;
|
||||||
|
if (!dateString || dateString === '0000:00:00 00:00:00') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const parts = dateString.split(' ')[0].split(':');
|
const parts = dateString.split(' ')[0].split(':');
|
||||||
const date = new Date(
|
const date = new Date(
|
||||||
Number(parts[0]),
|
Number(parts[0]),
|
||||||
Number(parts[1]) - 1,
|
Number(parts[1]) - 1,
|
||||||
Number(parts[2]),
|
Number(parts[2])
|
||||||
);
|
);
|
||||||
return date.getTime() * 1000;
|
return date.getTime() * 1000;
|
||||||
}
|
}
|
||||||
|
@ -1072,14 +1120,14 @@ class UploadService {
|
||||||
latDegree,
|
latDegree,
|
||||||
latMinute,
|
latMinute,
|
||||||
latSecond,
|
latSecond,
|
||||||
latDirection,
|
latDirection
|
||||||
);
|
);
|
||||||
|
|
||||||
const lonFinal = this.convertDMSToDD(
|
const lonFinal = this.convertDMSToDD(
|
||||||
lonDegree,
|
lonDegree,
|
||||||
lonMinute,
|
lonMinute,
|
||||||
lonSecond,
|
lonSecond,
|
||||||
lonDirection,
|
lonDirection
|
||||||
);
|
);
|
||||||
return { latitude: latFinal * 1.0, longitude: lonFinal * 1.0 };
|
return { latitude: latFinal * 1.0, longitude: lonFinal * 1.0 };
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,9 @@ import { clearData } from 'utils/storage/localStorage';
|
||||||
import localForage from 'utils/storage/localForage';
|
import localForage from 'utils/storage/localForage';
|
||||||
import { getToken } from 'utils/common/key';
|
import { getToken } from 'utils/common/key';
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { B64EncryptionResult } from './uploadService';
|
import { B64EncryptionResult } from 'utils/crypto';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
import { Subscription } from './billingService';
|
||||||
|
|
||||||
export interface UpdatedKey {
|
export interface UpdatedKey {
|
||||||
kekSalt: string;
|
kekSalt: string;
|
||||||
|
@ -35,7 +36,7 @@ export interface EmailVerificationResponse {
|
||||||
keyAttributes?: KeyAttributes;
|
keyAttributes?: KeyAttributes;
|
||||||
encryptedToken?: string;
|
encryptedToken?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
twoFactorSessionID: string
|
twoFactorSessionID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TwoFactorVerificationResponse {
|
export interface TwoFactorVerificationResponse {
|
||||||
|
@ -46,19 +47,28 @@ export interface TwoFactorVerificationResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TwoFactorSecret {
|
export interface TwoFactorSecret {
|
||||||
secretCode: string
|
secretCode: string;
|
||||||
qrCode: string
|
qrCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TwoFactorRecoveryResponse {
|
export interface TwoFactorRecoveryResponse {
|
||||||
encryptedSecret: string
|
encryptedSecret: string;
|
||||||
secretDecryptionNonce: string
|
secretDecryptionNonce: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOtt = (email: string) => HTTPService.get(`${ENDPOINT}/users/ott`, {
|
export interface UserDetails {
|
||||||
email,
|
email: string;
|
||||||
client: 'web',
|
usage: number;
|
||||||
});
|
fileCount: number;
|
||||||
|
sharedCollectionCount: number;
|
||||||
|
subscription: Subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOtt = (email: string) =>
|
||||||
|
HTTPService.get(`${ENDPOINT}/users/ott`, {
|
||||||
|
email,
|
||||||
|
client: 'web',
|
||||||
|
});
|
||||||
export const getPublicKey = async (email: string) => {
|
export const getPublicKey = async (email: string) => {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|
||||||
|
@ -67,7 +77,7 @@ export const getPublicKey = async (email: string) => {
|
||||||
{ email },
|
{ email },
|
||||||
{
|
{
|
||||||
'X-Auth-Token': token,
|
'X-Auth-Token': token,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
return resp.data.publicKey;
|
return resp.data.publicKey;
|
||||||
};
|
};
|
||||||
|
@ -80,34 +90,28 @@ export const getPaymentToken = async () => {
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
'X-Auth-Token': token,
|
'X-Auth-Token': token,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
return resp.data['paymentToken'];
|
return resp.data['paymentToken'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyOtt = (email: string, ott: string) => HTTPService.post(`${ENDPOINT}/users/verify-email`, { email, ott });
|
export const verifyOtt = (email: string, ott: string) =>
|
||||||
|
HTTPService.post(`${ENDPOINT}/users/verify-email`, { email, ott });
|
||||||
|
|
||||||
export const putAttributes = (token: string, keyAttributes: KeyAttributes) => HTTPService.put(
|
export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
|
||||||
`${ENDPOINT}/users/attributes`,
|
HTTPService.put(`${ENDPOINT}/users/attributes`, { keyAttributes }, null, {
|
||||||
{ keyAttributes },
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
'X-Auth-Token': token,
|
'X-Auth-Token': token,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const setKeys = (token: string, updatedKey: UpdatedKey) => HTTPService.put(`${ENDPOINT}/users/keys`, updatedKey, null, {
|
export const setKeys = (token: string, updatedKey: UpdatedKey) =>
|
||||||
'X-Auth-Token': token,
|
HTTPService.put(`${ENDPOINT}/users/keys`, updatedKey, null, {
|
||||||
});
|
|
||||||
|
|
||||||
export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) => HTTPService.put(
|
|
||||||
`${ENDPOINT}/users/recovery-key`,
|
|
||||||
recoveryKey,
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
'X-Auth-Token': token,
|
'X-Auth-Token': token,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) =>
|
||||||
|
HTTPService.put(`${ENDPOINT}/users/recovery-key`, recoveryKey, null, {
|
||||||
|
'X-Auth-Token': token,
|
||||||
|
});
|
||||||
|
|
||||||
export const logoutUser = async () => {
|
export const logoutUser = async () => {
|
||||||
// ignore server logout result as logoutUser can be triggered before sign up or on token expiry
|
// ignore server logout result as logoutUser can be triggered before sign up or on token expiry
|
||||||
|
@ -135,26 +139,46 @@ export const isTokenValid = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setupTwoFactor = async () => {
|
export const setupTwoFactor = async () => {
|
||||||
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/setup`, null, null, {
|
const resp = await HTTPService.post(
|
||||||
'X-Auth-Token': getToken(),
|
`${ENDPOINT}/users/two-factor/setup`,
|
||||||
});
|
null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
'X-Auth-Token': getToken(),
|
||||||
|
}
|
||||||
|
);
|
||||||
return resp.data as TwoFactorSecret;
|
return resp.data as TwoFactorSecret;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const enableTwoFactor = async (code: string, recoveryEncryptedTwoFactorSecret: B64EncryptionResult) => {
|
export const enableTwoFactor = async (
|
||||||
await HTTPService.post(`${ENDPOINT}/users/two-factor/enable`, {
|
code: string,
|
||||||
code,
|
recoveryEncryptedTwoFactorSecret: B64EncryptionResult
|
||||||
encryptedTwoFactorSecret: recoveryEncryptedTwoFactorSecret.encryptedData,
|
) => {
|
||||||
twoFactorSecretDecryptionNonce: recoveryEncryptedTwoFactorSecret.nonce,
|
await HTTPService.post(
|
||||||
}, null, {
|
`${ENDPOINT}/users/two-factor/enable`,
|
||||||
'X-Auth-Token': getToken(),
|
{
|
||||||
});
|
code,
|
||||||
|
encryptedTwoFactorSecret:
|
||||||
|
recoveryEncryptedTwoFactorSecret.encryptedData,
|
||||||
|
twoFactorSecretDecryptionNonce:
|
||||||
|
recoveryEncryptedTwoFactorSecret.nonce,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
'X-Auth-Token': getToken(),
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyTwoFactor = async (code: string, sessionID: string) => {
|
export const verifyTwoFactor = async (code: string, sessionID: string) => {
|
||||||
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/verify`, {
|
const resp = await HTTPService.post(
|
||||||
code, sessionID,
|
`${ENDPOINT}/users/two-factor/verify`,
|
||||||
}, null);
|
{
|
||||||
|
code,
|
||||||
|
sessionID,
|
||||||
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
return resp.data as TwoFactorVerificationResponse;
|
return resp.data as TwoFactorVerificationResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -167,7 +191,8 @@ export const recoverTwoFactor = async (sessionID: string) => {
|
||||||
|
|
||||||
export const removeTwoFactor = async (sessionID: string, secret: string) => {
|
export const removeTwoFactor = async (sessionID: string, secret: string) => {
|
||||||
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/remove`, {
|
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/remove`, {
|
||||||
sessionID, secret,
|
sessionID,
|
||||||
|
secret,
|
||||||
});
|
});
|
||||||
return resp.data as TwoFactorVerificationResponse;
|
return resp.data as TwoFactorVerificationResponse;
|
||||||
};
|
};
|
||||||
|
@ -179,9 +204,13 @@ export const disableTwoFactor = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTwoFactorStatus = async () => {
|
export const getTwoFactorStatus = async () => {
|
||||||
const resp = await HTTPService.get(`${ENDPOINT}/users/two-factor/status`, null, {
|
const resp = await HTTPService.get(
|
||||||
'X-Auth-Token': getToken(),
|
`${ENDPOINT}/users/two-factor/status`,
|
||||||
});
|
null,
|
||||||
|
{
|
||||||
|
'X-Auth-Token': getToken(),
|
||||||
|
}
|
||||||
|
);
|
||||||
return resp.data['status'];
|
return resp.data['status'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -197,3 +226,40 @@ export const _logout = async () => {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getOTTForEmailChange = async (email: string) => {
|
||||||
|
if (!getToken()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await HTTPService.get(`${ENDPOINT}/users/ott`, {
|
||||||
|
email,
|
||||||
|
client: 'web',
|
||||||
|
purpose: 'change',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changeEmail = async (email: string, ott: string) => {
|
||||||
|
if (!getToken()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await HTTPService.post(
|
||||||
|
`${ENDPOINT}/users/change-email`,
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
ott,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
'X-Auth-Token': getToken(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserDetails = async (): Promise<UserDetails> => {
|
||||||
|
const token = getToken();
|
||||||
|
|
||||||
|
const resp = await HTTPService.get(`${ENDPOINT}/users/details`, null, {
|
||||||
|
'X-Auth-Token': token,
|
||||||
|
});
|
||||||
|
return resp.data['details'];
|
||||||
|
};
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const ENCRYPTION_CHUNK_SIZE = 4 * 1024 * 1024;
|
||||||
export const GAP_BTW_TILES = 4;
|
export const GAP_BTW_TILES = 4;
|
||||||
export const DATE_CONTAINER_HEIGHT = 48;
|
export const DATE_CONTAINER_HEIGHT = 48;
|
||||||
export const IMAGE_CONTAINER_MAX_HEIGHT = 200;
|
export const IMAGE_CONTAINER_MAX_HEIGHT = 200;
|
||||||
export const IMAGE_CONTAINER_MAX_WIDTH = IMAGE_CONTAINER_MAX_HEIGHT - GAP_BTW_TILES;
|
export const IMAGE_CONTAINER_MAX_WIDTH =
|
||||||
|
IMAGE_CONTAINER_MAX_HEIGHT - GAP_BTW_TILES;
|
||||||
export const MIN_COLUMNS = 4;
|
export const MIN_COLUMNS = 4;
|
||||||
export const SPACE_BTW_DATES = 44;
|
export const SPACE_BTW_DATES = 44;
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { NextRouter } from 'next/router';
|
||||||
import { SetDialogMessage } from 'components/MessageDialog';
|
import { SetDialogMessage } from 'components/MessageDialog';
|
||||||
import { SetLoading } from 'pages/gallery';
|
import { SetLoading } from 'pages/gallery';
|
||||||
import { getData, LS_KEYS } from './storage/localStorage';
|
import { getData, LS_KEYS } from './storage/localStorage';
|
||||||
import { SUBSCRIPTION_VERIFICATION_ERROR } from './common/errorUtil';
|
import { CustomError } from './common/errorUtil';
|
||||||
|
|
||||||
const STRIPE = 'stripe';
|
const STRIPE = 'stripe';
|
||||||
|
|
||||||
|
@ -17,14 +17,14 @@ export function convertBytesToGBs(bytes, precision?): string {
|
||||||
return (bytes / (1024 * 1024 * 1024)).toFixed(precision ?? 2);
|
return (bytes / (1024 * 1024 * 1024)).toFixed(precision ?? 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertToHumanReadable(bytes:number, precision=2): string {
|
export function convertToHumanReadable(bytes: number, precision = 2): string {
|
||||||
if (bytes===0) {
|
if (bytes === 0) {
|
||||||
return '0 MB';
|
return '0 MB';
|
||||||
}
|
}
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(precision)+ ' ' + sizes[i];
|
return (bytes / Math.pow(1024, i)).toFixed(precision) + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasPaidSubscription(subscription?: Subscription) {
|
export function hasPaidSubscription(subscription?: Subscription) {
|
||||||
|
@ -89,7 +89,7 @@ export async function updateSubscription(
|
||||||
plan: Plan,
|
plan: Plan,
|
||||||
setDialogMessage: SetDialogMessage,
|
setDialogMessage: SetDialogMessage,
|
||||||
setLoading: SetLoading,
|
setLoading: SetLoading,
|
||||||
closePlanSelectorModal: () => null,
|
closePlanSelectorModal: () => null
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -99,7 +99,7 @@ export async function updateSubscription(
|
||||||
setDialogMessage({
|
setDialogMessage({
|
||||||
title: constants.SUCCESS,
|
title: constants.SUCCESS,
|
||||||
content: constants.SUBSCRIPTION_PURCHASE_SUCCESS(
|
content: constants.SUBSCRIPTION_PURCHASE_SUCCESS(
|
||||||
getUserSubscription().expiryTime,
|
getUserSubscription().expiryTime
|
||||||
),
|
),
|
||||||
close: { variant: 'success' },
|
close: { variant: 'success' },
|
||||||
});
|
});
|
||||||
|
@ -117,13 +117,13 @@ export async function updateSubscription(
|
||||||
null,
|
null,
|
||||||
|
|
||||||
setDialogMessage,
|
setDialogMessage,
|
||||||
setLoading,
|
setLoading
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
close: { text: constants.CANCEL },
|
close: { text: constants.CANCEL },
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case SUBSCRIPTION_VERIFICATION_ERROR:
|
case CustomError.SUBSCRIPTION_VERIFICATION_ERROR:
|
||||||
setDialogMessage({
|
setDialogMessage({
|
||||||
title: constants.ERROR,
|
title: constants.ERROR,
|
||||||
content: constants.SUBSCRIPTION_VERIFICATION_FAILED,
|
content: constants.SUBSCRIPTION_VERIFICATION_FAILED,
|
||||||
|
@ -146,7 +146,7 @@ export async function updateSubscription(
|
||||||
export async function cancelSubscription(
|
export async function cancelSubscription(
|
||||||
setDialogMessage: SetDialogMessage,
|
setDialogMessage: SetDialogMessage,
|
||||||
closePlanSelectorModal: () => null,
|
closePlanSelectorModal: () => null,
|
||||||
setLoading: SetLoading,
|
setLoading: SetLoading
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -171,7 +171,7 @@ export async function cancelSubscription(
|
||||||
export async function activateSubscription(
|
export async function activateSubscription(
|
||||||
setDialogMessage: SetDialogMessage,
|
setDialogMessage: SetDialogMessage,
|
||||||
closePlanSelectorModal: () => null,
|
closePlanSelectorModal: () => null,
|
||||||
setLoading: SetLoading,
|
setLoading: SetLoading
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -195,7 +195,7 @@ export async function activateSubscription(
|
||||||
|
|
||||||
export async function updatePaymentMethod(
|
export async function updatePaymentMethod(
|
||||||
setDialogMessage: SetDialogMessage,
|
setDialogMessage: SetDialogMessage,
|
||||||
setLoading: SetLoading,
|
setLoading: SetLoading
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -213,7 +213,7 @@ export async function updatePaymentMethod(
|
||||||
|
|
||||||
export async function checkSubscriptionPurchase(
|
export async function checkSubscriptionPurchase(
|
||||||
setDialogMessage: SetDialogMessage,
|
setDialogMessage: SetDialogMessage,
|
||||||
router: NextRouter,
|
router: NextRouter
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
@ -227,19 +227,19 @@ export async function checkSubscriptionPurchase(
|
||||||
} else if (sessionId) {
|
} else if (sessionId) {
|
||||||
try {
|
try {
|
||||||
const subscription = await billingService.verifySubscription(
|
const subscription = await billingService.verifySubscription(
|
||||||
sessionId,
|
sessionId
|
||||||
);
|
);
|
||||||
setDialogMessage({
|
setDialogMessage({
|
||||||
title: constants.SUBSCRIPTION_PURCHASE_SUCCESS_TITLE,
|
title: constants.SUBSCRIPTION_PURCHASE_SUCCESS_TITLE,
|
||||||
close: { variant: 'success' },
|
close: { variant: 'success' },
|
||||||
content: constants.SUBSCRIPTION_PURCHASE_SUCCESS(
|
content: constants.SUBSCRIPTION_PURCHASE_SUCCESS(
|
||||||
subscription?.expiryTime,
|
subscription?.expiryTime
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setDialogMessage({
|
setDialogMessage({
|
||||||
title: constants.ERROR,
|
title: constants.ERROR,
|
||||||
content: SUBSCRIPTION_VERIFICATION_ERROR,
|
content: CustomError.SUBSCRIPTION_VERIFICATION_ERROR,
|
||||||
close: {},
|
close: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -250,3 +250,18 @@ export async function checkSubscriptionPurchase(
|
||||||
router.push('gallery', undefined, { shallow: true });
|
router.push('gallery', undefined, { shallow: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function planForSubscription(subscription: Subscription) {
|
||||||
|
if (!subscription) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: subscription.productID,
|
||||||
|
storage: subscription.storage,
|
||||||
|
price: subscription.price,
|
||||||
|
period: subscription.period,
|
||||||
|
stripeID: subscription.productID,
|
||||||
|
iosID: subscription.productID,
|
||||||
|
androidID: subscription.productID,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -15,14 +15,14 @@ export async function addFilesToCollection(
|
||||||
syncWithRemote: () => Promise<void>,
|
syncWithRemote: () => Promise<void>,
|
||||||
selectCollection: (id: number) => void,
|
selectCollection: (id: number) => void,
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
existingCollection: Collection,
|
existingCollection: Collection
|
||||||
) {
|
) {
|
||||||
setCollectionSelectorView(false);
|
setCollectionSelectorView(false);
|
||||||
let collection;
|
let collection;
|
||||||
if (!existingCollection) {
|
if (!existingCollection) {
|
||||||
collection = await createCollection(
|
collection = await createCollection(
|
||||||
collectionName,
|
collectionName,
|
||||||
CollectionType.album,
|
CollectionType.album
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
collection = existingCollection;
|
collection = existingCollection;
|
||||||
|
|
|
@ -1,18 +1,25 @@
|
||||||
export const getEndpoint = () => {
|
export const getEndpoint = () => {
|
||||||
const endPoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? 'https://api.ente.io';
|
const endPoint =
|
||||||
|
process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? 'https://api.ente.io';
|
||||||
return endPoint;
|
return endPoint;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFileUrl = (id: number) => {
|
export const getFileUrl = (id: number) => {
|
||||||
if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT !== undefined) {
|
if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT !== undefined) {
|
||||||
return `${process.env.NEXT_PUBLIC_ENTE_ENDPOINT}/files/download/${id}` ?? `https://api.ente.io/files/download/${id}`;
|
return (
|
||||||
|
`${process.env.NEXT_PUBLIC_ENTE_ENDPOINT}/files/download/${id}` ??
|
||||||
|
'https://api.ente.io'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return `https://files.ente.workers.dev/?fileID=${id}`;
|
return `https://files.ente.workers.dev/?fileID=${id}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getThumbnailUrl = (id: number) => {
|
export const getThumbnailUrl = (id: number) => {
|
||||||
if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT !== undefined) {
|
if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT !== undefined) {
|
||||||
return `${process.env.NEXT_PUBLIC_ENTE_ENDPOINT}/files/preview/${id}` ?? `https://api.ente.io/files/preview/${id}`;
|
return (
|
||||||
|
`${process.env.NEXT_PUBLIC_ENTE_ENDPOINT}/files/preview/${id}` ??
|
||||||
|
'https://api.ente.io'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return `https://thumbnails.ente.workers.dev/?fileID=${id}`;
|
return `https://thumbnails.ente.workers.dev/?fileID=${id}`;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,45 +1,43 @@
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
|
||||||
export const errorCodes = {
|
export const ServerErrorCodes = {
|
||||||
ERR_STORAGE_LIMIT_EXCEEDED: '426',
|
SESSION_EXPIRED: '401',
|
||||||
ERR_NO_ACTIVE_SUBSCRIPTION: '402',
|
NO_ACTIVE_SUBSCRIPTION: '402',
|
||||||
ERR_NO_INTERNET_CONNECTION: '1',
|
FORBIDDEN: '403',
|
||||||
ERR_SESSION_EXPIRED: '401',
|
STORAGE_LIMIT_EXCEEDED: '426',
|
||||||
ERR_KEY_MISSING: '2',
|
|
||||||
ERR_FORBIDDEN: '403',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CustomError = {
|
||||||
export const SUBSCRIPTION_VERIFICATION_ERROR = 'Subscription verification failed';
|
SUBSCRIPTION_VERIFICATION_ERROR: 'Subscription verification failed',
|
||||||
|
THUMBNAIL_GENERATION_FAILED: 'thumbnail generation failed',
|
||||||
export const THUMBNAIL_GENERATION_FAILED = 'thumbnail generation failed';
|
VIDEO_PLAYBACK_FAILED: 'video playback failed',
|
||||||
export const VIDEO_PLAYBACK_FAILED = 'video playback failed';
|
ETAG_MISSING: 'no header/etag present in response body',
|
||||||
|
KEY_MISSING: 'encrypted key missing from localStorage',
|
||||||
|
};
|
||||||
|
|
||||||
export function parseError(error) {
|
export function parseError(error) {
|
||||||
let errorMessage = null;
|
let parsedMessage = null;
|
||||||
if (error?.status) {
|
if (error?.status) {
|
||||||
const errorCode = error.status.toString();
|
const errorCode = error.status.toString();
|
||||||
switch (errorCode) {
|
switch (errorCode) {
|
||||||
case errorCodes.ERR_NO_ACTIVE_SUBSCRIPTION:
|
case ServerErrorCodes.NO_ACTIVE_SUBSCRIPTION:
|
||||||
errorMessage = constants.SUBSCRIPTION_EXPIRED;
|
parsedMessage = constants.SUBSCRIPTION_EXPIRED;
|
||||||
break;
|
break;
|
||||||
case errorCodes.ERR_STORAGE_LIMIT_EXCEEDED:
|
case ServerErrorCodes.STORAGE_LIMIT_EXCEEDED:
|
||||||
errorMessage = constants.STORAGE_QUOTA_EXCEEDED;
|
parsedMessage = constants.STORAGE_QUOTA_EXCEEDED;
|
||||||
break;
|
break;
|
||||||
case errorCodes.ERR_NO_INTERNET_CONNECTION:
|
case ServerErrorCodes.SESSION_EXPIRED:
|
||||||
errorMessage = constants.NO_INTERNET_CONNECTION;
|
parsedMessage = constants.SESSION_EXPIRED_MESSAGE;
|
||||||
break;
|
|
||||||
case errorCodes.ERR_SESSION_EXPIRED:
|
|
||||||
errorMessage = constants.SESSION_EXPIRED_MESSAGE;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (errorMessage) {
|
if (parsedMessage) {
|
||||||
return { parsedError: new Error(errorMessage), parsed: true };
|
return { parsedError: new Error(parsedMessage), parsed: true };
|
||||||
} else {
|
} else {
|
||||||
return ({
|
return {
|
||||||
parsedError: new Error(`${constants.UNKNOWN_ERROR} ${error}`), parsed: false,
|
parsedError: new Error(`${constants.UNKNOWN_ERROR} ${error}`),
|
||||||
});
|
parsed: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +46,6 @@ export function handleError(error) {
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
throw parsedError;
|
throw parsedError;
|
||||||
} else {
|
} else {
|
||||||
// shallow error don't break the caller flow
|
// swallow error don't break the caller flow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
|
||||||
const DESKTOP_APP_DOWNLOAD_URL = 'https://github.com/ente-io/bhari-frame/releases/';
|
export const DESKTOP_APP_DOWNLOAD_URL =
|
||||||
|
'https://github.com/ente-io/bhari-frame/releases/latest';
|
||||||
|
|
||||||
const retrySleepTime = [2000, 5000, 10000];
|
const retrySleepTime = [2000, 5000, 10000];
|
||||||
|
|
||||||
|
@ -32,7 +33,10 @@ export function reverseString(title: string) {
|
||||||
.reduce((reversedString, currWord) => `${currWord} ${reversedString}`);
|
.reduce((reversedString, currWord) => `${currWord} ${reversedString}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function retryAsyncFunction(func: ()=>Promise<any>, retryCount: number = 3) {
|
export async function retryAsyncFunction(
|
||||||
|
func: () => Promise<any>,
|
||||||
|
retryCount: number = 3
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const resp = await func();
|
const resp = await func();
|
||||||
return resp;
|
return resp;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue