Merge branch 'master' into redirect_to_payements
This commit is contained in:
commit
ac1a59394e
|
@ -8,7 +8,8 @@
|
|||
"plugin:react/recommended",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"google"
|
||||
"google",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
|
@ -23,13 +24,7 @@
|
|||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
4,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"indent":"off",
|
||||
"class-methods-use-this": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/display-name": "off",
|
||||
|
@ -48,7 +43,8 @@
|
|||
"error",
|
||||
"always"
|
||||
],
|
||||
"space-before-function-paren": "off"
|
||||
"space-before-function-paren": "off",
|
||||
"operator-linebreak":["error","after", { "overrides": { "?": "before", ":": "before" } }]
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
|
|
1
.husky/.gitignore
vendored
1
.husky/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
_
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/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
|
||||
|
||||
First, run the development server:
|
||||
First, pull and install dependencies
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
yarn install
|
||||
```
|
||||
|
||||
Then run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
|
20
package.json
20
package.json
|
@ -9,8 +9,7 @@
|
|||
"build-analyze": "ANALYZE=true next build",
|
||||
"postbuild": "next export",
|
||||
"start": "next start",
|
||||
"lint-staged": "lint-staged",
|
||||
"postinstall": "husky install"
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ente-io/next-with-workbox": "^1.0.3",
|
||||
|
@ -18,7 +17,7 @@
|
|||
"@stripe/stripe-js": "^1.13.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.25.0",
|
||||
"@typescript-eslint/parser": "^4.25.0",
|
||||
"axios": "^0.20.0",
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^4.5.2",
|
||||
"chrono-node": "^2.2.6",
|
||||
"comlink": "^4.3.0",
|
||||
|
@ -32,6 +31,7 @@
|
|||
"heic2any": "^0.0.3",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"is-electron": "^2.2.0",
|
||||
"jszip": "3.7.1",
|
||||
"libsodium-wrappers": "^0.7.8",
|
||||
"localforage": "^1.9.0",
|
||||
"next": "^10.2.3",
|
||||
|
@ -62,7 +62,6 @@
|
|||
"@next/bundle-analyzer": "^9.5.3",
|
||||
"@types/debounce-promise": "^3.1.3",
|
||||
"@types/libsodium-wrappers": "^0.7.8",
|
||||
"@types/localforage": "^0.0.34",
|
||||
"@types/node": "^14.6.4",
|
||||
"@types/photoswipe": "^4.1.1",
|
||||
"@types/react": "^16.9.49",
|
||||
|
@ -74,20 +73,17 @@
|
|||
"babel-plugin-styled-components": "^1.11.1",
|
||||
"eslint": "^7.27.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"husky": "^6.0.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
"husky": "^7.0.1",
|
||||
"lint-staged": "^11.1.2",
|
||||
"prettier": "2.3.2",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"standard": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{js,jsx,ts,tsx}": "eslint"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "yarn run lint-staged"
|
||||
}
|
||||
"src/**/*.{js,jsx,ts,tsx}": ["eslint --fix","prettier --write --ignore-unknown"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import * as Sentry from '@sentry/nextjs';
|
||||
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_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({
|
||||
dsn: SENTRY_DSN,
|
||||
enabled: SENTRY_ENV !== 'development',
|
||||
|
|
|
@ -25,8 +25,7 @@ export default function AddToCollectionBtn(props) {
|
|||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
strokeLinejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</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 collectionShare = async (
|
||||
{ email }: formValues,
|
||||
{ resetForm, setFieldError }: FormikHelpers<formValues>,
|
||||
{ resetForm, setFieldError }: FormikHelpers<formValues>
|
||||
) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
@ -89,8 +89,7 @@ function CollectionShare(props: Props) {
|
|||
fontSize: '1.2em',
|
||||
fontWeight: 900,
|
||||
}}
|
||||
onClick={() => collectionUnshare(sharee)}
|
||||
>
|
||||
onClick={() => collectionUnshare(sharee)}>
|
||||
-
|
||||
</Button>
|
||||
</td>
|
||||
|
@ -100,8 +99,7 @@ function CollectionShare(props: Props) {
|
|||
<MessageDialog
|
||||
show={props.show}
|
||||
onHide={props.onHide}
|
||||
attributes={{ title: constants.SHARE_COLLECTION }}
|
||||
>
|
||||
attributes={{ title: constants.SHARE_COLLECTION }}>
|
||||
<DeadCenter style={{ width: '85%', margin: 'auto' }}>
|
||||
<h6>{constants.SHARE_WITH_PEOPLE}</h6>
|
||||
<p />
|
||||
|
@ -114,8 +112,7 @@ function CollectionShare(props: Props) {
|
|||
})}
|
||||
validateOnChange={false}
|
||||
validateOnBlur={false}
|
||||
onSubmit={collectionShare}
|
||||
>
|
||||
onSubmit={collectionShare}>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
|
@ -128,15 +125,14 @@ function CollectionShare(props: Props) {
|
|||
<Form.Group
|
||||
as={Col}
|
||||
xs={10}
|
||||
controlId="formHorizontalEmail"
|
||||
>
|
||||
controlId="formHorizontalEmail">
|
||||
<Form.Control
|
||||
type="email"
|
||||
placeholder={constants.ENTER_EMAIL}
|
||||
value={values.email}
|
||||
onChange={handleChange('email')}
|
||||
isInvalid={Boolean(
|
||||
touched.email && errors.email,
|
||||
touched.email && errors.email
|
||||
)}
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
|
@ -148,8 +144,7 @@ function CollectionShare(props: Props) {
|
|||
<Form.Group
|
||||
as={Col}
|
||||
xs={2}
|
||||
controlId="formHorizontalEmail"
|
||||
>
|
||||
controlId="formHorizontalEmail">
|
||||
<SubmitButton
|
||||
loading={loading}
|
||||
inline
|
||||
|
|
|
@ -31,26 +31,27 @@ export const IconButton = styled.button`
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:focus, &:hover {
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
export const Row = styled.div`
|
||||
display:flex;
|
||||
align-items:center;
|
||||
margin-bottom:20px;
|
||||
flex:1
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const Label = styled.div <{ width?: string }> `
|
||||
width:${(props) => props.width ?? '70%'};
|
||||
export const Label = styled.div<{ width?: string }>`
|
||||
width: ${(props) => props.width ?? '70%'};
|
||||
`;
|
||||
export const Value = styled.div <{ width?: string }> `
|
||||
display:flex;
|
||||
justify-content:flex-start;
|
||||
align-items:center;
|
||||
width:${(props) => props.width ?? '30%'};
|
||||
export const Value = styled.div<{ width?: string }>`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: ${(props) => props.width ?? '30%'};
|
||||
text-align: center;
|
||||
color: #ddd;
|
||||
`;
|
||||
|
|
|
@ -20,8 +20,7 @@ export default function DeleteBtn(props) {
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
>
|
||||
width={props.width}>
|
||||
<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" />
|
||||
</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 { ComfySpan } from './ExportInProgress';
|
||||
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
onHide: () => void
|
||||
exportFolder: string
|
||||
exportSize: string
|
||||
lastExportTime: number
|
||||
exportStats: ExportStats
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
exportFolder: string;
|
||||
exportSize: string;
|
||||
lastExportTime: number;
|
||||
exportStats: ExportStats;
|
||||
updateExportFolder: (newFolder: string) => void;
|
||||
exportFiles: () => void;
|
||||
retryFailed: () => void;
|
||||
|
@ -23,30 +22,69 @@ export default function ExportFinished(props: Props) {
|
|||
const totalFiles = props.exportStats.failed + props.exportStats.success;
|
||||
return (
|
||||
<>
|
||||
<div style={{ borderBottom: '1px solid #444', marginBottom: '20px', padding: '0 5%' }}>
|
||||
<div
|
||||
style={{
|
||||
borderBottom: '1px solid #444',
|
||||
marginBottom: '20px',
|
||||
padding: '0 5%',
|
||||
}}>
|
||||
<Row>
|
||||
<Label width="40%">{constants.LAST_EXPORT_TIME}</Label>
|
||||
<Value width="60%">{formatDateTime(props.lastExportTime)}</Value>
|
||||
</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 width="60%">
|
||||
{formatDateTime(props.lastExportTime)}
|
||||
</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 style={{ width: '100%', display: 'flex', justifyContent: 'space-around' }}>
|
||||
<Button block variant={'outline-secondary'} onClick={props.onHide}>{constants.CLOSE}</Button>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
}}>
|
||||
<Button
|
||||
block
|
||||
variant={'outline-secondary'}
|
||||
onClick={props.onHide}>
|
||||
{constants.CLOSE}
|
||||
</Button>
|
||||
<div style={{ width: '30px' }} />
|
||||
{props.exportStats.failed !== 0 ?
|
||||
<Button block variant={'outline-danger'} onClick={props.retryFailed}>{constants.RETRY_EXPORT_}</Button> :
|
||||
<Button block variant={'outline-success'} onClick={props.exportFiles}>{constants.EXPORT_AGAIN}</Button>
|
||||
}
|
||||
{props.exportStats.failed !== 0 ? (
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -5,42 +5,82 @@ import styled from 'styled-components';
|
|||
import constants from 'utils/strings/constants';
|
||||
|
||||
export const ComfySpan = styled.span`
|
||||
word-spacing:1rem;
|
||||
color:#ddd;
|
||||
word-spacing: 1rem;
|
||||
color: #ddd;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
onHide: () => void
|
||||
exportFolder: string
|
||||
exportSize: string
|
||||
exportStage: ExportStage
|
||||
exportProgress: ExportProgress
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
exportFolder: string;
|
||||
exportSize: string;
|
||||
exportStage: ExportStage;
|
||||
exportProgress: ExportProgress;
|
||||
resumeExport: () => void;
|
||||
cancelExport: () => void
|
||||
cancelExport: () => void;
|
||||
pauseExport: () => void;
|
||||
}
|
||||
export default function ExportInProgress(props: Props) {
|
||||
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' }}>
|
||||
<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 style={{ width: '100%', marginBottom: '30px' }}>
|
||||
<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)}
|
||||
variant="upload-progress-bar"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: '100%', 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: '100%',
|
||||
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' }} />
|
||||
<Button block variant={'outline-danger'} onClick={props.cancelExport}>{constants.CANCEL}</Button>
|
||||
<Button
|
||||
block
|
||||
variant={'outline-danger'}
|
||||
onClick={props.cancelExport}>
|
||||
{constants.CANCEL}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -4,18 +4,18 @@ import { Button } from 'react-bootstrap';
|
|||
import constants from 'utils/strings/constants';
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
onHide: () => void
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
updateExportFolder: (newFolder: string) => void;
|
||||
exportFolder: string
|
||||
startExport: () => void
|
||||
exportFolder: string;
|
||||
startExport: () => void;
|
||||
exportSize: string;
|
||||
selectExportDirectory: () => void
|
||||
selectExportDirectory: () => void;
|
||||
}
|
||||
export default function ExportInit(props: Props) {
|
||||
return (
|
||||
<>
|
||||
<DeadCenter >
|
||||
<DeadCenter>
|
||||
<Button
|
||||
variant="outline-success"
|
||||
size="lg"
|
||||
|
@ -26,8 +26,9 @@ export default function ExportInit(props: Props) {
|
|||
flex: 1,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
onClick={props.startExport}
|
||||
>{constants.START}</Button>
|
||||
onClick={props.startExport}>
|
||||
{constants.START}
|
||||
</Button>
|
||||
</DeadCenter>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import isElectron from 'is-electron';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 styled from 'styled-components';
|
||||
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 constants from 'utils/strings/constants';
|
||||
import { Label, Row, Value } from './Container';
|
||||
|
@ -18,38 +23,44 @@ import MessageDialog from './MessageDialog';
|
|||
|
||||
const FolderIconWrapper = styled.div`
|
||||
width: 15%;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
padding: 3px;
|
||||
border: 1px solid #444;
|
||||
border-radius:15%;
|
||||
&:hover{
|
||||
background-color:#444;
|
||||
border-radius: 15%;
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
`;
|
||||
|
||||
const ExportFolderPathContainer =styled.span`
|
||||
white-space: nowrap;
|
||||
const ExportFolderPathContainer = styled.span`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-overflow: ellipsis;
|
||||
width: 200px;
|
||||
|
||||
|
||||
/* Beginning of string */
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
onHide: () => void
|
||||
usage: string
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
usage: string;
|
||||
}
|
||||
export default function ExportModal(props: Props) {
|
||||
const [exportStage, setExportStage] = useState(ExportStage.INIT);
|
||||
const [exportFolder, setExportFolder] = useState('');
|
||||
const [exportSize, setExportSize] = useState('');
|
||||
const [exportProgress, setExportProgress] = useState<ExportProgress>({ current: 0, total: 0 });
|
||||
const [exportStats, setExportStats] = useState<ExportStats>({ failed: 0, success: 0 });
|
||||
const [exportProgress, setExportProgress] = useState<ExportProgress>({
|
||||
current: 0,
|
||||
total: 0,
|
||||
});
|
||||
const [exportStats, setExportStats] = useState<ExportStats>({
|
||||
failed: 0,
|
||||
success: 0,
|
||||
});
|
||||
const [lastExportTime, setLastExportTime] = useState(0);
|
||||
|
||||
// ====================
|
||||
|
@ -64,7 +75,9 @@ export default function ExportModal(props: Props) {
|
|||
exportService.ElectronAPIs.registerStopExportListener(stopExport);
|
||||
exportService.ElectronAPIs.registerPauseExportListener(pauseExport);
|
||||
exportService.ElectronAPIs.registerResumeExportListener(resumeExport);
|
||||
exportService.ElectronAPIs.registerRetryFailedExportListener(retryFailedExport);
|
||||
exportService.ElectronAPIs.registerRetryFailedExportListener(
|
||||
retryFailedExport
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -76,7 +89,10 @@ export default function ExportModal(props: Props) {
|
|||
setExportStage(exportInfo?.stage ?? ExportStage.INIT);
|
||||
setLastExportTime(exportInfo?.lastAttemptTimestamp);
|
||||
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) {
|
||||
resumeExport();
|
||||
}
|
||||
|
@ -96,10 +112,22 @@ export default function ExportModal(props: Props) {
|
|||
const failedFilesCnt = exportRecord.failedFiles.length;
|
||||
const syncedFilesCnt = localFiles.length;
|
||||
if (syncedFilesCnt > exportedFileCnt + failedFilesCnt) {
|
||||
updateExportProgress({ current: exportedFileCnt + failedFilesCnt, total: syncedFilesCnt });
|
||||
const exportFileUIDs = new Set([...exportRecord.exportedFiles, ...exportRecord.failedFiles]);
|
||||
const unExportedFiles = localFiles.filter((file) => !exportFileUIDs.has(getFileUID(file)));
|
||||
exportService.addFilesQueuedRecord(exportFolder, unExportedFiles);
|
||||
updateExportProgress({
|
||||
current: exportedFileCnt + failedFilesCnt,
|
||||
total: syncedFilesCnt,
|
||||
});
|
||||
const exportFileUIDs = new Set([
|
||||
...exportRecord.exportedFiles,
|
||||
...exportRecord.failedFiles,
|
||||
]);
|
||||
const unExportedFiles = localFiles.filter(
|
||||
(file) =>
|
||||
!exportFileUIDs.has(getExportRecordFileUID(file))
|
||||
);
|
||||
exportService.addFilesQueuedRecord(
|
||||
exportFolder,
|
||||
unExportedFiles
|
||||
);
|
||||
updateExportStage(ExportStage.PAUSED);
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +135,6 @@ export default function ExportModal(props: Props) {
|
|||
main();
|
||||
}, [props.show]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setExportSize(props.usage);
|
||||
}, [props.usage]);
|
||||
|
@ -162,7 +189,10 @@ export default function ExportModal(props: Props) {
|
|||
const startExport = async () => {
|
||||
await preExportRun();
|
||||
updateExportProgress({ current: 0, total: 0 });
|
||||
const { paused } = await exportService.exportFiles(updateExportProgress, ExportType.NEW);
|
||||
const { paused } = await exportService.exportFiles(
|
||||
updateExportProgress,
|
||||
ExportType.NEW
|
||||
);
|
||||
await postExportRun(paused);
|
||||
};
|
||||
|
||||
|
@ -184,13 +214,15 @@ export default function ExportModal(props: Props) {
|
|||
const pausedStageProgress = exportRecord.progress;
|
||||
setExportProgress(pausedStageProgress);
|
||||
|
||||
const updateExportStatsWithOffset = ((progress: ExportProgress) => updateExportProgress(
|
||||
{
|
||||
const updateExportStatsWithOffset = (progress: ExportProgress) =>
|
||||
updateExportProgress({
|
||||
current: pausedStageProgress.current + progress.current,
|
||||
total: pausedStageProgress.current + progress.total,
|
||||
},
|
||||
));
|
||||
const { paused } = await exportService.exportFiles(updateExportStatsWithOffset, ExportType.PENDING);
|
||||
});
|
||||
const { paused } = await exportService.exportFiles(
|
||||
updateExportStatsWithOffset,
|
||||
ExportType.PENDING
|
||||
);
|
||||
|
||||
await postExportRun(paused);
|
||||
};
|
||||
|
@ -199,7 +231,10 @@ export default function ExportModal(props: Props) {
|
|||
await preExportRun();
|
||||
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);
|
||||
};
|
||||
|
||||
|
@ -224,7 +259,8 @@ export default function ExportModal(props: Props) {
|
|||
switch (exportStage) {
|
||||
case ExportStage.INIT:
|
||||
return (
|
||||
<ExportInit {...props}
|
||||
<ExportInit
|
||||
{...props}
|
||||
exportFolder={exportFolder}
|
||||
exportSize={exportSize}
|
||||
updateExportFolder={updateExportFolder}
|
||||
|
@ -235,7 +271,8 @@ export default function ExportModal(props: Props) {
|
|||
case ExportStage.INPROGRESS:
|
||||
case ExportStage.PAUSED:
|
||||
return (
|
||||
<ExportInProgress {...props}
|
||||
<ExportInProgress
|
||||
{...props}
|
||||
exportFolder={exportFolder}
|
||||
exportSize={exportSize}
|
||||
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}
|
||||
attributes={{
|
||||
title: constants.EXPORT_DATA,
|
||||
}}
|
||||
>
|
||||
<div style={{ borderBottom: '1px solid #444', marginBottom: '20px', padding: '0 5%' }}>
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
borderBottom: '1px solid #444',
|
||||
marginBottom: '20px',
|
||||
padding: '0 5%',
|
||||
width: '450px',
|
||||
}}>
|
||||
<Row>
|
||||
<Label width="40%">{constants.DESTINATION}</Label>
|
||||
<Value width="60%">
|
||||
{!exportFolder ?
|
||||
(<Button variant={'outline-success'} size={'sm'} onClick={selectExportDirectory}>{constants.SELECT_FOLDER}</Button>) :
|
||||
(<>
|
||||
{!exportFolder ? (
|
||||
<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' }}> */}
|
||||
<ExportFolderPathContainer>
|
||||
{exportFolder}
|
||||
</ExportFolderPathContainer>
|
||||
{/* </span> */}
|
||||
{(exportStage === ExportStage.FINISHED || exportStage === ExportStage.INIT) && (
|
||||
<FolderIconWrapper onClick={selectExportDirectory} >
|
||||
{(exportStage === ExportStage.FINISHED ||
|
||||
exportStage === ExportStage.INIT) && (
|
||||
<FolderIconWrapper
|
||||
onClick={selectExportDirectory}>
|
||||
<FolderIcon />
|
||||
</FolderIconWrapper>
|
||||
)}
|
||||
</>)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</Value>
|
||||
</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>
|
||||
</div>
|
||||
<ExportDynamicState />
|
||||
</MessageDialog >
|
||||
</MessageDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,8 @@ const HeartUI = styled.button<{
|
|||
cursor: pointer;
|
||||
background-size: cover;
|
||||
border: none;
|
||||
${({ isClick, size }) => isClick &&
|
||||
${({ isClick, size }) =>
|
||||
isClick &&
|
||||
`background-position: -${
|
||||
28 * size
|
||||
}px;transition: background 1s steps(28);`}
|
||||
|
|
|
@ -2,15 +2,19 @@ import { FlashMessage } from 'pages/_app';
|
|||
import React from 'react';
|
||||
import Alert from 'react-bootstrap/Alert';
|
||||
|
||||
|
||||
export default function FlashMessageBar({ flashMessage, onClose }: { flashMessage: FlashMessage, onClose: () => void }) {
|
||||
export default function FlashMessageBar({
|
||||
flashMessage,
|
||||
onClose,
|
||||
}: {
|
||||
flashMessage: FlashMessage;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Alert
|
||||
className="flash-message text-center"
|
||||
variant={flashMessage.severity}
|
||||
variant={flashMessage.type}
|
||||
dismissible
|
||||
onClose={onClose}
|
||||
>
|
||||
onClose={onClose}>
|
||||
<div style={{ maxWidth: '1024px', width: '80%', margin: 'auto' }}>
|
||||
{flashMessage.message}
|
||||
</div>
|
||||
|
|
|
@ -5,9 +5,9 @@ import CrossIcon from './icons/CrossIcon';
|
|||
|
||||
const CloseButtonWrapper = styled.div`
|
||||
position: absolute;
|
||||
top:10px;
|
||||
right:10px;
|
||||
cursor:pointer;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
const DropDiv = styled.div`
|
||||
flex: 1;
|
||||
|
@ -62,14 +62,10 @@ export default function FullScreenDropZone(props: Props) {
|
|||
e.preventDefault();
|
||||
props.showCollectionSelector();
|
||||
},
|
||||
})}
|
||||
>
|
||||
})}>
|
||||
<input {...props.getInputProps()} />
|
||||
{isDragActive && (
|
||||
<Overlay
|
||||
onDrop={onDragLeave}
|
||||
onDragLeave={onDragLeave}
|
||||
>
|
||||
<Overlay onDrop={onDragLeave} onDragLeave={onDragLeave}>
|
||||
<CloseButtonWrapper onClick={onDragLeave}>
|
||||
<CrossIcon />
|
||||
</CloseButtonWrapper>
|
||||
|
|
|
@ -11,11 +11,8 @@ export default function IncognitoWarning() {
|
|||
title: constants.LOCAL_STORAGE_NOT_ACCESSIBLE,
|
||||
staticBackdrop: true,
|
||||
nonClosable: true,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{constants.LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE}
|
||||
</div>
|
||||
}}>
|
||||
<div>{constants.LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE}</div>
|
||||
</MessageDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ interface formValues {
|
|||
}
|
||||
|
||||
interface LoginProps {
|
||||
signUp: () => void
|
||||
signUp: () => void;
|
||||
}
|
||||
|
||||
export default function Login(props: LoginProps) {
|
||||
|
@ -39,7 +39,7 @@ export default function Login(props: LoginProps) {
|
|||
|
||||
const loginUser = async (
|
||||
{ email }: formValues,
|
||||
{ setFieldError }: FormikHelpers<formValues>,
|
||||
{ setFieldError }: FormikHelpers<formValues>
|
||||
) => {
|
||||
try {
|
||||
setWaiting(true);
|
||||
|
@ -73,15 +73,8 @@ export default function Login(props: LoginProps) {
|
|||
})}
|
||||
validateOnChange={false}
|
||||
validateOnBlur={false}
|
||||
onSubmit={loginUser}
|
||||
>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
touched,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
}) => (
|
||||
onSubmit={loginUser}>
|
||||
{({ values, errors, touched, handleChange, handleSubmit }) => (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="formBasicEmail">
|
||||
<Form.Control
|
||||
|
@ -91,7 +84,7 @@ export default function Login(props: LoginProps) {
|
|||
value={values.email}
|
||||
onChange={handleChange('email')}
|
||||
isInvalid={Boolean(
|
||||
touched.email && errors.email,
|
||||
touched.email && errors.email
|
||||
)}
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
|
@ -105,7 +98,11 @@ export default function Login(props: LoginProps) {
|
|||
loading={waiting}
|
||||
/>
|
||||
<br />
|
||||
<Button block variant="link" className="text-center" onClick={props.signUp}>
|
||||
<Button
|
||||
block
|
||||
variant="link"
|
||||
className="text-center"
|
||||
onClick={props.signUp}>
|
||||
{constants.NO_ACCOUNT}
|
||||
</Button>
|
||||
</Form>
|
||||
|
|
|
@ -7,7 +7,7 @@ export interface MessageAttributes {
|
|||
staticBackdrop?: boolean;
|
||||
nonClosable?: boolean;
|
||||
content?: any;
|
||||
close?: { text?: string; variant?: string, action?: () => void };
|
||||
close?: { text?: string; variant?: string; action?: () => void };
|
||||
proceed?: {
|
||||
text: string;
|
||||
action: () => void;
|
||||
|
@ -38,21 +38,21 @@ export default function MessageDialog({
|
|||
{...props}
|
||||
onHide={attributes.nonClosable ? () => null : props.onHide}
|
||||
centered
|
||||
backdrop={attributes.staticBackdrop ? 'static' : 'true'}
|
||||
>
|
||||
backdrop={attributes.staticBackdrop ? 'static' : 'true'}>
|
||||
<Modal.Header
|
||||
style={{ borderBottom: 'none' }}
|
||||
closeButton={!attributes.nonClosable}
|
||||
>
|
||||
closeButton={!attributes.nonClosable}>
|
||||
{attributes.title && (
|
||||
<Modal.Title>
|
||||
{attributes.title}
|
||||
</Modal.Title>
|
||||
<Modal.Title>{attributes.title}</Modal.Title>
|
||||
)}
|
||||
</Modal.Header>
|
||||
{(children || attributes?.content) && (
|
||||
<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>
|
||||
)}
|
||||
{(attributes.close || attributes.proceed) && (
|
||||
|
@ -61,13 +61,16 @@ export default function MessageDialog({
|
|||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{attributes.close && (
|
||||
<Button
|
||||
variant={`outline-${attributes.close?.variant ?? 'secondary'}`}
|
||||
variant={`outline-${
|
||||
attributes.close?.variant ?? 'secondary'
|
||||
}`}
|
||||
onClick={() => {
|
||||
attributes.close?.action ? attributes.close?.action() : props.onHide();
|
||||
attributes.close.action &&
|
||||
attributes.close?.action();
|
||||
props.onHide();
|
||||
}}
|
||||
style={{
|
||||
padding: '6px 3em',
|
||||
|
@ -75,14 +78,15 @@ export default function MessageDialog({
|
|||
marginBottom: '20px',
|
||||
flex: 1,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{attributes.close?.text ?? constants.OK}
|
||||
</Button>
|
||||
)}
|
||||
{attributes.proceed && (
|
||||
<Button
|
||||
variant={`outline-${attributes.proceed?.variant ?? 'primary'}`}
|
||||
variant={`outline-${
|
||||
attributes.proceed?.variant ?? 'primary'
|
||||
}`}
|
||||
onClick={() => {
|
||||
attributes.proceed.action();
|
||||
props.onHide();
|
||||
|
@ -94,8 +98,7 @@ export default function MessageDialog({
|
|||
flex: 1,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
disabled={attributes.proceed.disabled}
|
||||
>
|
||||
disabled={attributes.proceed.disabled}>
|
||||
{attributes.proceed.text}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
@ -15,11 +15,17 @@ const Wrapper = styled.button<{ direction: SCROLL_DIRECTION }>`
|
|||
color: #eee;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
${(props) => (props.direction === SCROLL_DIRECTION.LEFT ? 'margin-right: 10px;' : 'margin-left: 10px;')}
|
||||
${(props) => (props.direction === SCROLL_DIRECTION.LEFT ? 'left: 0;' : 'right: 0;')}
|
||||
${(props) =>
|
||||
props.direction === SCROLL_DIRECTION.LEFT
|
||||
? 'margin-right: 10px;'
|
||||
: 'margin-left: 10px;'}
|
||||
${(props) =>
|
||||
props.direction === SCROLL_DIRECTION.LEFT ? 'left: 0;' : 'right: 0;'}
|
||||
|
||||
& > svg {
|
||||
${(props) => props.direction === SCROLL_DIRECTION.LEFT && 'transform:rotate(180deg);'}
|
||||
${(props) =>
|
||||
props.direction === SCROLL_DIRECTION.LEFT &&
|
||||
'transform:rotate(180deg);'}
|
||||
border-radius: 50%;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
|
@ -30,25 +36,33 @@ const Wrapper = styled.button<{ direction: SCROLL_DIRECTION }>`
|
|||
}
|
||||
|
||||
&:hover {
|
||||
color:#fff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&::after {
|
||||
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;
|
||||
top: 0;
|
||||
width: 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 }) => (
|
||||
<Wrapper
|
||||
direction={scrollDirection}
|
||||
{...rest}
|
||||
>
|
||||
<Wrapper direction={scrollDirection} {...rest}>
|
||||
<NavigateNext />
|
||||
</Wrapper>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import router from 'next/router';
|
||||
import {
|
||||
DeadCenter,
|
||||
FILE_TYPE,
|
||||
GalleryContext,
|
||||
Search,
|
||||
SetFiles,
|
||||
|
@ -10,7 +9,7 @@ import {
|
|||
import PreviewCard from './pages/gallery/PreviewCard';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { File } from 'services/fileService';
|
||||
import { File, FILE_TYPE } from 'services/fileService';
|
||||
import styled from 'styled-components';
|
||||
import DownloadManager from 'services/downloadManager';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
@ -19,10 +18,14 @@ import { VariableSizeList as List } from 'react-window';
|
|||
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
|
||||
import { isInsideBox, isSameDay as isSameDayAnyYear } from 'utils/search';
|
||||
import { SetDialogMessage } from './MessageDialog';
|
||||
import { VIDEO_PLAYBACK_FAILED } from 'utils/common/errorUtil';
|
||||
import { CustomError } from 'utils/common/errorUtil';
|
||||
import {
|
||||
GAP_BTW_TILES, DATE_CONTAINER_HEIGHT, IMAGE_CONTAINER_MAX_HEIGHT,
|
||||
IMAGE_CONTAINER_MAX_WIDTH, MIN_COLUMNS, SPACE_BTW_DATES,
|
||||
GAP_BTW_TILES,
|
||||
DATE_CONTAINER_HEIGHT,
|
||||
IMAGE_CONTAINER_MAX_HEIGHT,
|
||||
IMAGE_CONTAINER_MAX_WIDTH,
|
||||
MIN_COLUMNS,
|
||||
SPACE_BTW_DATES,
|
||||
} from 'types';
|
||||
|
||||
const NO_OF_PAGES = 2;
|
||||
|
@ -68,21 +71,24 @@ const getTemplateColumns = (columns: number, groups?: number[]): string => {
|
|||
if (sum < columns) {
|
||||
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 {
|
||||
return `repeat(${columns}, 1fr)`;
|
||||
}
|
||||
};
|
||||
|
||||
const ListContainer = styled.div<{ columns: number, groups?: number[] }>`
|
||||
const ListContainer = styled.div<{ columns: number; groups?: number[] }>`
|
||||
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;
|
||||
padding: 0 24px;
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
|
||||
@media(max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
@ -139,7 +145,7 @@ interface Props {
|
|||
search: Search;
|
||||
setSearchStats: setSearchStats;
|
||||
deleted?: number[];
|
||||
setDialogMessage: SetDialogMessage
|
||||
setDialogMessage: SetDialogMessage;
|
||||
}
|
||||
|
||||
const PhotoFrame = ({
|
||||
|
@ -303,14 +309,13 @@ const PhotoFrame = ({
|
|||
video.preload = 'metadata';
|
||||
video.src = url;
|
||||
video.currentTime = 3;
|
||||
const t = setTimeout(
|
||||
() => {
|
||||
reject(
|
||||
Error(`${VIDEO_PLAYBACK_FAILED} err: wait time exceeded`),
|
||||
);
|
||||
},
|
||||
WAIT_FOR_VIDEO_PLAYBACK,
|
||||
);
|
||||
const t = setTimeout(() => {
|
||||
reject(
|
||||
Error(
|
||||
`${CustomError.VIDEO_PLAYBACK_FAILED} err: wait time exceeded`
|
||||
)
|
||||
);
|
||||
}, WAIT_FOR_VIDEO_PLAYBACK);
|
||||
});
|
||||
item.html = `
|
||||
<video width="320" height="240" controls>
|
||||
|
@ -332,7 +337,8 @@ const PhotoFrame = ({
|
|||
};
|
||||
setDialogMessage({
|
||||
title: constants.VIDEO_PLAYBACK_FAILED,
|
||||
content: constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD,
|
||||
content:
|
||||
constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD,
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text: constants.DOWNLOAD,
|
||||
|
@ -373,7 +379,7 @@ const PhotoFrame = ({
|
|||
if (
|
||||
search.date &&
|
||||
!isSameDayAnyYear(search.date)(
|
||||
new Date(item.metadata.creationTime / 1000),
|
||||
new Date(item.metadata.creationTime / 1000)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
|
@ -397,11 +403,10 @@ const PhotoFrame = ({
|
|||
return false;
|
||||
});
|
||||
|
||||
const isSameDay = (first, second) => (
|
||||
const isSameDay = (first, second) =>
|
||||
first.getFullYear() === second.getFullYear() &&
|
||||
first.getMonth() === second.getMonth() &&
|
||||
first.getDate() === second.getDate()
|
||||
);
|
||||
first.getDate() === second.getDate();
|
||||
|
||||
/**
|
||||
* Checks and merge multiple dates into a single row.
|
||||
|
@ -410,7 +415,10 @@ const PhotoFrame = ({
|
|||
* @param columns
|
||||
* @returns
|
||||
*/
|
||||
const mergeTimeStampList = (items: TimeStampListItem[], columns: number): TimeStampListItem[] => {
|
||||
const mergeTimeStampList = (
|
||||
items: TimeStampListItem[],
|
||||
columns: number
|
||||
): TimeStampListItem[] => {
|
||||
const newList: TimeStampListItem[] = [];
|
||||
let index = 0;
|
||||
let newIndex = 0;
|
||||
|
@ -423,12 +431,18 @@ const PhotoFrame = ({
|
|||
// we can add more items to the same list.
|
||||
if (newList[newIndex]) {
|
||||
// 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({
|
||||
date: currItem.date,
|
||||
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;
|
||||
} else {
|
||||
// Adding items would exceed the number of columns.
|
||||
|
@ -441,10 +455,12 @@ const PhotoFrame = ({
|
|||
newList.push({
|
||||
...currItem,
|
||||
date: null,
|
||||
dates: [{
|
||||
date: currItem.date,
|
||||
span: items[index + 1].items.length,
|
||||
}],
|
||||
dates: [
|
||||
{
|
||||
date: currItem.date,
|
||||
span: items[index + 1].items.length,
|
||||
},
|
||||
],
|
||||
});
|
||||
newList.push(items[index + 1]);
|
||||
index += 2;
|
||||
|
@ -474,7 +490,7 @@ const PhotoFrame = ({
|
|||
<>
|
||||
{!isFirstLoad && files.length === 0 && !searchMode ? (
|
||||
<EmptyScreen>
|
||||
<img height={150} src='/images/gallery.png' />
|
||||
<img height={150} src="/images/gallery.png" />
|
||||
<Button
|
||||
variant="outline-success"
|
||||
onClick={openFileUploader}
|
||||
|
@ -484,8 +500,7 @@ const PhotoFrame = ({
|
|||
paddingRight: '32px',
|
||||
paddingTop: '12px',
|
||||
paddingBottom: '12px',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{constants.UPLOAD_FIRST_PHOTO}
|
||||
</Button>
|
||||
</EmptyScreen>
|
||||
|
@ -493,7 +508,9 @@ const PhotoFrame = ({
|
|||
<Container>
|
||||
<AutoSizer>
|
||||
{({ 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 skipMerge = false;
|
||||
if (columns < MIN_COLUMNS) {
|
||||
|
@ -506,29 +523,38 @@ const PhotoFrame = ({
|
|||
let listItemIndex = 0;
|
||||
let currentDate = -1;
|
||||
filteredData.forEach((item, index) => {
|
||||
if (!isSameDay(new Date(item.metadata.creationTime / 1000), new Date(currentDate))) {
|
||||
currentDate = item.metadata.creationTime / 1000;
|
||||
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
if (
|
||||
!isSameDay(
|
||||
new Date(
|
||||
item.metadata.creationTime / 1000
|
||||
),
|
||||
new Date(currentDate)
|
||||
)
|
||||
) {
|
||||
currentDate =
|
||||
item.metadata.creationTime / 1000;
|
||||
const dateTimeFormat =
|
||||
new Intl.DateTimeFormat('en-IN', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TIME,
|
||||
date: isSameDay(
|
||||
new Date(currentDate),
|
||||
new Date(),
|
||||
) ?
|
||||
'Today' :
|
||||
isSameDay(
|
||||
new Date(currentDate),
|
||||
new Date(Date.now() - A_DAY),
|
||||
) ?
|
||||
'Yesterday' :
|
||||
dateTimeFormat.format(
|
||||
currentDate,
|
||||
),
|
||||
new Date()
|
||||
)
|
||||
? 'Today'
|
||||
: isSameDay(
|
||||
new Date(currentDate),
|
||||
new Date(Date.now() - A_DAY)
|
||||
)
|
||||
? 'Yesterday'
|
||||
: dateTimeFormat.format(
|
||||
currentDate
|
||||
),
|
||||
id: currentDate.toString(),
|
||||
});
|
||||
timeStampList.push({
|
||||
|
@ -553,7 +579,10 @@ const PhotoFrame = ({
|
|||
});
|
||||
|
||||
if (!skipMerge) {
|
||||
timeStampList = mergeTimeStampList(timeStampList, columns);
|
||||
timeStampList = mergeTimeStampList(
|
||||
timeStampList,
|
||||
columns
|
||||
);
|
||||
}
|
||||
|
||||
const getItemSize = (index) => {
|
||||
|
@ -567,68 +596,89 @@ const PhotoFrame = ({
|
|||
}
|
||||
};
|
||||
|
||||
const photoFrameHeight=(()=>{
|
||||
let sum=0;
|
||||
for (let i=0; i<timeStampList.length; i++) {
|
||||
sum+=getItemSize(i);
|
||||
const photoFrameHeight = (() => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < timeStampList.length; i++) {
|
||||
sum += getItemSize(i);
|
||||
}
|
||||
return sum;
|
||||
})();
|
||||
files.length < 30 && !searchMode &&
|
||||
files.length < 30 &&
|
||||
!searchMode &&
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.BANNER,
|
||||
banner: (
|
||||
<BannerContainer span={columns}>
|
||||
<p>{constants.INSTALL_MOBILE_APP()}</p>
|
||||
<p>
|
||||
{constants.INSTALL_MOBILE_APP()}
|
||||
</p>
|
||||
</BannerContainer>
|
||||
),
|
||||
id: 'install-banner',
|
||||
height: Math.max(48, height-photoFrameHeight),
|
||||
height: Math.max(
|
||||
48,
|
||||
height - photoFrameHeight
|
||||
),
|
||||
});
|
||||
const extraRowsToRender = Math.ceil(
|
||||
(NO_OF_PAGES * height) / IMAGE_CONTAINER_MAX_HEIGHT,
|
||||
(NO_OF_PAGES * height) /
|
||||
IMAGE_CONTAINER_MAX_HEIGHT
|
||||
);
|
||||
|
||||
const generateKey = (index) => {
|
||||
switch (timeStampList[index].itemType) {
|
||||
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:
|
||||
return `${timeStampList[index].id}-${index}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const renderListItem = (listItem: TimeStampListItem) => {
|
||||
const renderListItem = (
|
||||
listItem: TimeStampListItem
|
||||
) => {
|
||||
switch (listItem.itemType) {
|
||||
case ITEM_TYPE.TIME:
|
||||
return listItem.dates ?
|
||||
return listItem.dates ? (
|
||||
listItem.dates.map((item) => (
|
||||
<>
|
||||
<DateContainer key={item.date} span={item.span}>
|
||||
<DateContainer
|
||||
key={item.date}
|
||||
span={item.span}>
|
||||
{item.date}
|
||||
</DateContainer>
|
||||
<div />
|
||||
</>
|
||||
)) :
|
||||
(
|
||||
<DateContainer span={columns}>
|
||||
{listItem.date}
|
||||
</DateContainer>
|
||||
);
|
||||
))
|
||||
) : (
|
||||
<DateContainer span={columns}>
|
||||
{listItem.date}
|
||||
</DateContainer>
|
||||
);
|
||||
case ITEM_TYPE.BANNER:
|
||||
return listItem.banner;
|
||||
default:
|
||||
{
|
||||
const ret = (listItem.items.map(
|
||||
(item, idx) => getThumbnail(
|
||||
filteredData,
|
||||
listItem.itemStartIndex + idx,
|
||||
),
|
||||
));
|
||||
default: {
|
||||
const ret = listItem.items.map(
|
||||
(item, idx) =>
|
||||
getThumbnail(
|
||||
filteredData,
|
||||
listItem.itemStartIndex +
|
||||
idx
|
||||
)
|
||||
);
|
||||
if (listItem.groups) {
|
||||
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];
|
||||
ret.splice(sum, 0, <div />);
|
||||
sum += 1;
|
||||
|
@ -648,12 +698,17 @@ const PhotoFrame = ({
|
|||
width={width}
|
||||
itemCount={timeStampList.length}
|
||||
itemKey={generateKey}
|
||||
overscanCount={extraRowsToRender}
|
||||
>
|
||||
overscanCount={extraRowsToRender}>
|
||||
{({ index, style }) => (
|
||||
<ListItem style={style}>
|
||||
<ListContainer columns={columns} groups={timeStampList[index].groups}>
|
||||
{renderListItem(timeStampList[index])}
|
||||
<ListContainer
|
||||
columns={columns}
|
||||
groups={
|
||||
timeStampList[index].groups
|
||||
}>
|
||||
{renderListItem(
|
||||
timeStampList[index]
|
||||
)}
|
||||
</ListContainer>
|
||||
</ListItem>
|
||||
)}
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
addToFavorites,
|
||||
removeFromFavorites,
|
||||
} from 'services/collectionService';
|
||||
import { File } from 'services/fileService';
|
||||
import { File, FILE_TYPE } from 'services/fileService';
|
||||
import constants from 'utils/strings/constants';
|
||||
import DownloadManger from 'services/downloadManager';
|
||||
import EXIF from 'exif-js';
|
||||
|
@ -16,7 +16,7 @@ import Button from 'react-bootstrap/Button';
|
|||
import Form from 'react-bootstrap/Form';
|
||||
import styled from 'styled-components';
|
||||
import events from './events';
|
||||
import { formatDateTime } from 'utils/file';
|
||||
import { fileNameWithoutExtension, formatDateTime } from 'utils/file';
|
||||
import { FormCheck } from 'react-bootstrap';
|
||||
|
||||
interface Iprops {
|
||||
|
@ -49,8 +49,12 @@ const Pre = styled.pre`
|
|||
|
||||
const renderInfoItem = (label: string, value: string | JSX.Element) => (
|
||||
<>
|
||||
<Form.Label column sm="4">{label}</Form.Label>
|
||||
<Form.Label column sm="8">{value}</Form.Label>
|
||||
<Form.Label column sm="4">
|
||||
{label}
|
||||
</Form.Label>
|
||||
<Form.Label column sm="8">
|
||||
{value}
|
||||
</Form.Label>
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -62,29 +66,49 @@ function ExifData(props: { exif: any }) {
|
|||
setShowAll(e.target.checked);
|
||||
};
|
||||
|
||||
const renderAllValues = () => (<Pre>{exif.raw}</Pre>);
|
||||
const renderAllValues = () => <Pre>{exif.raw}</Pre>;
|
||||
|
||||
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?.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())}
|
||||
</>);
|
||||
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?.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 (<>
|
||||
<LegendContainer>
|
||||
<Legend>{constants.EXIF}</Legend>
|
||||
<FormCheck>
|
||||
<FormCheck.Label>
|
||||
<FormCheck.Input onChange={changeHandler} />
|
||||
{constants.SHOW_ALL}
|
||||
</FormCheck.Label>
|
||||
</FormCheck>
|
||||
</LegendContainer>
|
||||
{showAll ? renderAllValues() : renderSelectedValues()}
|
||||
</>);
|
||||
return (
|
||||
<>
|
||||
<LegendContainer>
|
||||
<Legend>{constants.EXIF}</Legend>
|
||||
<FormCheck>
|
||||
<FormCheck.Label>
|
||||
<FormCheck.Input onChange={changeHandler} />
|
||||
{constants.SHOW_ALL}
|
||||
</FormCheck.Label>
|
||||
</FormCheck>
|
||||
</LegendContainer>
|
||||
{showAll ? renderAllValues() : renderSelectedValues()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PhotoSwipe(props: Iprops) {
|
||||
|
@ -140,8 +164,14 @@ function PhotoSwipe(props: Iprops) {
|
|||
const ele = document.getElementById(`thumb-${file.id}`);
|
||||
if (ele) {
|
||||
const rect = ele.getBoundingClientRect();
|
||||
const pageYScroll = window.pageYOffset || document.documentElement.scrollTop;
|
||||
return { x: rect.left, y: rect.top + pageYScroll, w: rect.width };
|
||||
const pageYScroll =
|
||||
window.pageYOffset ||
|
||||
document.documentElement.scrollTop;
|
||||
return {
|
||||
x: rect.left,
|
||||
y: rect.top + pageYScroll,
|
||||
w: rect.width,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
|
@ -153,7 +183,7 @@ function PhotoSwipe(props: Iprops) {
|
|||
pswpElement.current,
|
||||
PhotoswipeUIDefault,
|
||||
items,
|
||||
options,
|
||||
options
|
||||
);
|
||||
events.forEach((event) => {
|
||||
const callback = props[event];
|
||||
|
@ -201,7 +231,8 @@ function PhotoSwipe(props: Iprops) {
|
|||
const { favItemIds } = props;
|
||||
if (favItemIds && file) {
|
||||
return favItemIds.has(file.id);
|
||||
} return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const onFavClick = async (file) => {
|
||||
|
@ -232,7 +263,9 @@ function PhotoSwipe(props: Iprops) {
|
|||
const checkExifAvailable = () => {
|
||||
setExif(null);
|
||||
setTimeout(() => {
|
||||
const img = document.querySelector('.pswp__img:not(.pswp__img--placeholder)');
|
||||
const img = document.querySelector(
|
||||
'.pswp__img:not(.pswp__img--placeholder)'
|
||||
);
|
||||
if (img) {
|
||||
// @ts-expect-error
|
||||
EXIF.getData(img, function () {
|
||||
|
@ -269,7 +302,11 @@ function PhotoSwipe(props: Iprops) {
|
|||
loadingBar.current.continuousStart();
|
||||
a.href = await DownloadManger.getFile(file);
|
||||
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);
|
||||
a.click();
|
||||
a.remove();
|
||||
|
@ -285,8 +322,7 @@ function PhotoSwipe(props: Iprops) {
|
|||
tabIndex={Number('-1')}
|
||||
role="dialog"
|
||||
aria-hidden="true"
|
||||
ref={pswpElement}
|
||||
>
|
||||
ref={pswpElement}>
|
||||
<div className="pswp__bg" />
|
||||
<div className="pswp__scroll-wrap">
|
||||
<div className="pswp__container">
|
||||
|
@ -306,7 +342,9 @@ function PhotoSwipe(props: Iprops) {
|
|||
<button
|
||||
className="pswp-custom download-btn"
|
||||
title={constants.DOWNLOAD}
|
||||
onClick={() => downloadFile(photoSwipe.currItem)}
|
||||
onClick={() =>
|
||||
downloadFile(photoSwipe.currItem)
|
||||
}
|
||||
/>
|
||||
|
||||
<button
|
||||
|
@ -363,26 +401,46 @@ function PhotoSwipe(props: Iprops) {
|
|||
<div>
|
||||
<Legend>{constants.METADATA}</Legend>
|
||||
</div>
|
||||
{renderInfoItem(constants.FILE_ID, items[photoSwipe?.getCurrentIndex()]?.id)}
|
||||
{metadata?.title && renderInfoItem(constants.FILE_NAME, metadata.title)}
|
||||
{metadata?.creationTime && renderInfoItem(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>
|
||||
))}
|
||||
{renderInfoItem(
|
||||
constants.FILE_ID,
|
||||
items[photoSwipe?.getCurrentIndex()]?.id
|
||||
)}
|
||||
{metadata?.title &&
|
||||
renderInfoItem(constants.FILE_NAME, metadata.title)}
|
||||
{metadata?.creationTime &&
|
||||
renderInfoItem(
|
||||
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 && (
|
||||
<>
|
||||
<br /><br />
|
||||
<br />
|
||||
<br />
|
||||
<ExifData exif={exif} />
|
||||
</>
|
||||
)}
|
||||
</Form.Group>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="outline-secondary" onClick={handleCloseInfo}>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
onClick={handleCloseInfo}>
|
||||
{constants.CLOSE}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
|
|
|
@ -12,11 +12,11 @@ export const CodeBlock = styled.div<{ height: number }>`
|
|||
justify-content: center;
|
||||
background: #1a1919;
|
||||
height: ${(props) => props.height}px;
|
||||
padding-left:30px;
|
||||
padding-right:20px;
|
||||
padding-left: 30px;
|
||||
padding-right: 20px;
|
||||
color: white;
|
||||
margin: 20px 0;
|
||||
width:100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const FreeFlowText = styled.div`
|
||||
|
@ -71,20 +71,17 @@ function RecoveryKeyModal({ somethingWentWrong, ...props }: Props) {
|
|||
disabled: !recoveryKey,
|
||||
variant: 'success',
|
||||
},
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<p>{constants.RECOVERY_KEY_DESCRIPTION}</p>
|
||||
<CodeBlock height={150}>
|
||||
{recoveryKey ? (
|
||||
<FreeFlowText>
|
||||
{recoveryKey}
|
||||
</FreeFlowText>
|
||||
<FreeFlowText>{recoveryKey}</FreeFlowText>
|
||||
) : (
|
||||
<EnteSpinner />
|
||||
)}
|
||||
</CodeBlock>
|
||||
<p>{constants.KEY_NOT_STORED_DISCLAIMER}</p>
|
||||
</MessageDialog >
|
||||
</MessageDialog>
|
||||
);
|
||||
}
|
||||
export default RecoveryKeyModal;
|
||||
|
|
|
@ -97,7 +97,9 @@ export default function SearchBar(props: Props) {
|
|||
}, [props.isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', () => setWindowWidth(window.innerWidth));
|
||||
window.addEventListener('resize', () =>
|
||||
setWindowWidth(window.innerWidth)
|
||||
);
|
||||
});
|
||||
// = =========================
|
||||
// Functionality
|
||||
|
@ -119,18 +121,19 @@ export default function SearchBar(props: Props) {
|
|||
type: SuggestionType.DATE,
|
||||
value: searchedDate,
|
||||
label: getFormattedDate(searchedDate),
|
||||
})),
|
||||
}))
|
||||
);
|
||||
|
||||
const searchResults = await searchLocation(searchPhrase);
|
||||
option.push(
|
||||
...searchResults.map(
|
||||
(searchResult) => ({
|
||||
type: SuggestionType.LOCATION,
|
||||
value: searchResult.bbox,
|
||||
label: searchResult.place,
|
||||
} as Suggestion),
|
||||
),
|
||||
(searchResult) =>
|
||||
({
|
||||
type: SuggestionType.LOCATION,
|
||||
value: searchResult.bbox,
|
||||
label: searchResult.place,
|
||||
} as Suggestion)
|
||||
)
|
||||
);
|
||||
return option;
|
||||
};
|
||||
|
@ -174,7 +177,8 @@ export default function SearchBar(props: Props) {
|
|||
// 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 }) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
|
@ -198,8 +202,7 @@ export default function SearchBar(props: Props) {
|
|||
style={{
|
||||
paddingLeft: '10px',
|
||||
paddingBottom: '4px',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{props.getValue().length === 0 || props.menuIsOpen ? (
|
||||
<SearchIcon />
|
||||
) : props.getValue()[0].type === SuggestionType.DATE ? (
|
||||
|
@ -215,13 +218,13 @@ export default function SearchBar(props: Props) {
|
|||
const customStyles = {
|
||||
control: (style, { isFocused }) => ({
|
||||
...style,
|
||||
'backgroundColor': '#282828',
|
||||
'color': '#d1d1d1',
|
||||
'borderColor': isFocused ? '#2dc262' : '#444',
|
||||
'boxShadow': 'none',
|
||||
backgroundColor: '#282828',
|
||||
color: '#d1d1d1',
|
||||
borderColor: isFocused ? '#2dc262' : '#444',
|
||||
boxShadow: 'none',
|
||||
':hover': {
|
||||
'borderColor': '#2dc262',
|
||||
'cursor': 'text',
|
||||
borderColor: '#2dc262',
|
||||
cursor: 'text',
|
||||
'&>.icon': { color: '#2dc262' },
|
||||
},
|
||||
}),
|
||||
|
@ -276,8 +279,7 @@ export default function SearchBar(props: Props) {
|
|||
style={{
|
||||
flex: 1,
|
||||
margin: '10px',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<AsyncSelect
|
||||
components={{
|
||||
Option: OptionWithIcon,
|
||||
|
@ -297,8 +299,7 @@ export default function SearchBar(props: Props) {
|
|||
{props.isOpen && (
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={resetSearch}
|
||||
>
|
||||
onClick={resetSearch}>
|
||||
<CrossIcon />
|
||||
</div>
|
||||
)}
|
||||
|
@ -307,8 +308,7 @@ export default function SearchBar(props: Props) {
|
|||
) : (
|
||||
<SearchButton
|
||||
isDisabled={props.isFirstFetch}
|
||||
onClick={() => !props.isFirstFetch && props.setOpen(true)}
|
||||
>
|
||||
onClick={() => !props.isFirstFetch && props.setOpen(true)}>
|
||||
<SearchIcon />
|
||||
</SearchButton>
|
||||
)}
|
||||
|
|
|
@ -21,7 +21,7 @@ function SetPasswordForm(props: Props) {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const onSubmit = async (
|
||||
values: formValues,
|
||||
{ setFieldError }: FormikHelpers<formValues>,
|
||||
{ setFieldError }: FormikHelpers<formValues>
|
||||
) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
@ -34,7 +34,7 @@ function SetPasswordForm(props: Props) {
|
|||
} catch (e) {
|
||||
setFieldError(
|
||||
'passphrase',
|
||||
`${constants.UNKNOWN_ERROR} ${e.message}`,
|
||||
`${constants.UNKNOWN_ERROR} ${e.message}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
@ -46,8 +46,7 @@ function SetPasswordForm(props: Props) {
|
|||
<Card.Body>
|
||||
<div
|
||||
className="text-center"
|
||||
style={{ marginBottom: '40px' }}
|
||||
>
|
||||
style={{ marginBottom: '40px' }}>
|
||||
<p>{constants.ENTER_ENC_PASSPHRASE}</p>
|
||||
{constants.PASSPHRASE_DISCLAIMER()}
|
||||
</div>
|
||||
|
@ -55,14 +54,13 @@ function SetPasswordForm(props: Props) {
|
|||
initialValues={{ passphrase: '', confirm: '' }}
|
||||
validationSchema={Yup.object().shape({
|
||||
passphrase: Yup.string().required(
|
||||
constants.REQUIRED,
|
||||
constants.REQUIRED
|
||||
),
|
||||
confirm: Yup.string().required(constants.REQUIRED),
|
||||
})}
|
||||
validateOnChange={false}
|
||||
validateOnBlur={false}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
onSubmit={onSubmit}>
|
||||
{({
|
||||
values,
|
||||
touched,
|
||||
|
@ -79,7 +77,7 @@ function SetPasswordForm(props: Props) {
|
|||
onChange={handleChange('passphrase')}
|
||||
isInvalid={Boolean(
|
||||
touched.passphrase &&
|
||||
errors.passphrase,
|
||||
errors.passphrase
|
||||
)}
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
|
@ -97,7 +95,7 @@ function SetPasswordForm(props: Props) {
|
|||
value={values.confirm}
|
||||
onChange={handleChange('confirm')}
|
||||
isInvalid={Boolean(
|
||||
touched.confirm && errors.confirm,
|
||||
touched.confirm && errors.confirm
|
||||
)}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
@ -115,8 +113,7 @@ function SetPasswordForm(props: Props) {
|
|||
{props.back && (
|
||||
<div
|
||||
className="text-center"
|
||||
style={{ marginTop: '20px' }}
|
||||
>
|
||||
style={{ marginTop: '20px' }}>
|
||||
<Button variant="link" onClick={props.back}>
|
||||
{constants.GO_BACK}
|
||||
</Button>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { slide as Menu } from 'react-burger-menu';
|
||||
import billingService, { Subscription } from 'services/billingService';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||
import { getToken } from 'utils/common/key';
|
||||
|
@ -14,6 +13,7 @@ import {
|
|||
isOnFreePlan,
|
||||
isSubscriptionCancelled,
|
||||
isSubscribed,
|
||||
convertToHumanReadable,
|
||||
} from 'utils/billingUtil';
|
||||
|
||||
import isElectron from 'is-electron';
|
||||
|
@ -21,7 +21,7 @@ import { Collection } from 'services/collectionService';
|
|||
import { useRouter } from 'next/router';
|
||||
import LinkButton from './pages/gallery/LinkButton';
|
||||
import { downloadApp } from 'utils/common';
|
||||
import { logoutUser } from 'services/userService';
|
||||
import { getUserDetails, logoutUser } from 'services/userService';
|
||||
import { LogoImage } from 'pages/_app';
|
||||
import { SetDialogMessage } from './MessageDialog';
|
||||
import EnteSpinner from './EnteSpinner';
|
||||
|
@ -31,11 +31,12 @@ import ExportModal from './ExportModal';
|
|||
import { SetLoading } from 'pages/gallery';
|
||||
import InProgressIcon from './icons/InProgressIcon';
|
||||
import exportService from 'services/exportService';
|
||||
import { Subscription } from 'services/billingService';
|
||||
|
||||
interface Props {
|
||||
collections: Collection[];
|
||||
setDialogMessage: SetDialogMessage;
|
||||
setLoading: SetLoading,
|
||||
setLoading: SetLoading;
|
||||
showPlanSelectorModal: () => void;
|
||||
}
|
||||
export default function Sidebar(props: Props) {
|
||||
|
@ -55,16 +56,23 @@ export default function Sidebar(props: Props) {
|
|||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
const usage = await billingService.getUsage();
|
||||
|
||||
SetUsage(usage);
|
||||
setSubscription(getUserSubscription());
|
||||
const userDetails = await getUserDetails();
|
||||
setUser({ ...user, email: userDetails.email });
|
||||
SetUsage(convertToHumanReadable(userDetails.usage));
|
||||
setSubscription(userDetails.subscription);
|
||||
setData(LS_KEYS.USER, {
|
||||
...getData(LS_KEYS.USER),
|
||||
email: userDetails.email,
|
||||
});
|
||||
setData(LS_KEYS.SUBSCRIPTION, userDetails.subscription);
|
||||
};
|
||||
main();
|
||||
}, [isOpen]);
|
||||
|
||||
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');
|
||||
win.focus();
|
||||
}
|
||||
|
@ -105,9 +113,13 @@ export default function Sidebar(props: Props) {
|
|||
<Menu
|
||||
isOpen={isOpen}
|
||||
onStateChange={(state) => setIsOpen(state.isOpen)}
|
||||
itemListElement="div"
|
||||
>
|
||||
<div style={{ display: 'flex', outline: 'none', textAlign: 'center' }}>
|
||||
itemListElement="div">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
outline: 'none',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
<LogoImage
|
||||
style={{ height: '24px', padding: '3px' }}
|
||||
alt="logo"
|
||||
|
@ -119,11 +131,16 @@ export default function Sidebar(props: Props) {
|
|||
outline: 'none',
|
||||
color: 'rgb(45, 194, 98)',
|
||||
fontSize: '16px',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{user?.email}
|
||||
</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={{ display: 'flex' }}>
|
||||
<h5 style={{ margin: '4px 0 12px 2px' }}>
|
||||
|
@ -134,15 +151,15 @@ export default function Sidebar(props: Props) {
|
|||
{isSubscriptionActive(subscription) ? (
|
||||
isOnFreePlan(subscription) ? (
|
||||
constants.FREE_SUBSCRIPTION_INFO(
|
||||
subscription?.expiryTime,
|
||||
subscription?.expiryTime
|
||||
)
|
||||
) : isSubscriptionCancelled(subscription) ? (
|
||||
constants.RENEWAL_CANCELLED_SUBSCRIPTION_INFO(
|
||||
subscription?.expiryTime,
|
||||
subscription?.expiryTime
|
||||
)
|
||||
) : (
|
||||
constants.RENEWAL_ACTIVE_SUBSCRIPTION_INFO(
|
||||
subscription?.expiryTime,
|
||||
subscription?.expiryTime
|
||||
)
|
||||
)
|
||||
) : (
|
||||
|
@ -152,11 +169,10 @@ export default function Sidebar(props: Props) {
|
|||
variant="outline-success"
|
||||
block
|
||||
size="sm"
|
||||
onClick={onManageClick}
|
||||
>
|
||||
{isSubscribed(subscription) ?
|
||||
constants.MANAGE :
|
||||
constants.SUBSCRIBE}
|
||||
onClick={onManageClick}>
|
||||
{isSubscribed(subscription)
|
||||
? constants.MANAGE
|
||||
: constants.SUBSCRIBE}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -169,7 +185,7 @@ export default function Sidebar(props: Props) {
|
|||
{usage ? (
|
||||
constants.USAGE_INFO(
|
||||
usage,
|
||||
Number(convertBytesToGBs(subscription?.storage)),
|
||||
Number(convertBytesToGBs(subscription?.storage))
|
||||
)
|
||||
) : (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
|
@ -194,29 +210,28 @@ export default function Sidebar(props: Props) {
|
|||
/>
|
||||
<LinkButton
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={openFeedbackURL}
|
||||
>
|
||||
onClick={openFeedbackURL}>
|
||||
{constants.REQUEST_FEATURE}
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={openSupportMail}
|
||||
>
|
||||
onClick={openSupportMail}>
|
||||
{constants.SUPPORT}
|
||||
</LinkButton>
|
||||
<>
|
||||
<RecoveryKeyModal
|
||||
show={recoverModalView}
|
||||
onHide={() => setRecoveryModalView(false)}
|
||||
somethingWentWrong={() => props.setDialogMessage({
|
||||
title: constants.RECOVER_KEY_GENERATION_FAILED,
|
||||
close: { variant: 'danger' },
|
||||
})}
|
||||
somethingWentWrong={() =>
|
||||
props.setDialogMessage({
|
||||
title: constants.RECOVER_KEY_GENERATION_FAILED,
|
||||
close: { variant: 'danger' },
|
||||
})
|
||||
}
|
||||
/>
|
||||
<LinkButton
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={() => setRecoveryModalView(true)}
|
||||
>
|
||||
onClick={() => setRecoveryModalView(true)}>
|
||||
{constants.DOWNLOAD_RECOVERY_KEY}
|
||||
</LinkButton>
|
||||
</>
|
||||
|
@ -230,8 +245,7 @@ export default function Sidebar(props: Props) {
|
|||
/>
|
||||
<LinkButton
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={() => setTwoFactorModalView(true)}
|
||||
>
|
||||
onClick={() => setTwoFactorModalView(true)}>
|
||||
{constants.TWO_FACTOR}
|
||||
</LinkButton>
|
||||
</>
|
||||
|
@ -240,18 +254,32 @@ export default function Sidebar(props: Props) {
|
|||
onClick={() => {
|
||||
setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
|
||||
router.push('change-password');
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{constants.CHANGE_PASSWORD}
|
||||
</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} />
|
||||
<LinkButton style={{ marginTop: '30px' }} onClick={exportFiles}>
|
||||
<ExportModal
|
||||
show={exportModalView}
|
||||
onHide={() => setExportModalView(false)}
|
||||
usage={usage}
|
||||
/>
|
||||
<LinkButton
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={exportFiles}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{constants.EXPORT}<div style={{ width: '20px' }} />
|
||||
{exportService.isExportInProgress() &&
|
||||
{constants.EXPORT}
|
||||
<div style={{ width: '20px' }} />
|
||||
{exportService.isExportInProgress() && (
|
||||
<InProgressIcon />
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</LinkButton>
|
||||
</>
|
||||
|
@ -266,18 +294,19 @@ export default function Sidebar(props: Props) {
|
|||
<LinkButton
|
||||
variant="danger"
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={() => props.setDialogMessage({
|
||||
title: `${constants.CONFIRM} ${constants.LOGOUT}`,
|
||||
content: constants.LOGOUT_MESSAGE,
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text: constants.LOGOUT,
|
||||
action: logoutUser,
|
||||
variant: 'danger',
|
||||
},
|
||||
close: { text: constants.CANCEL },
|
||||
})}
|
||||
>
|
||||
onClick={() =>
|
||||
props.setDialogMessage({
|
||||
title: `${constants.CONFIRM} ${constants.LOGOUT}`,
|
||||
content: constants.LOGOUT_MESSAGE,
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text: constants.LOGOUT,
|
||||
action: logoutUser,
|
||||
variant: 'danger',
|
||||
},
|
||||
close: { text: constants.CANCEL },
|
||||
})
|
||||
}>
|
||||
{constants.LOGOUT}
|
||||
</LinkButton>
|
||||
<div
|
||||
|
@ -287,6 +316,6 @@ export default function Sidebar(props: Props) {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
</Menu >
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export default function SignUp(props: SignUpProps) {
|
|||
|
||||
const registerUser = async (
|
||||
{ email, passphrase, confirm }: FormValues,
|
||||
{ setFieldError }: FormikHelpers<FormValues>,
|
||||
{ setFieldError }: FormikHelpers<FormValues>
|
||||
) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
@ -47,12 +47,13 @@ export default function SignUp(props: SignUpProps) {
|
|||
}
|
||||
try {
|
||||
if (passphrase === confirm) {
|
||||
const { keyAttributes, masterKey } = await generateKeyAttributes(passphrase);
|
||||
const { keyAttributes, masterKey } =
|
||||
await generateKeyAttributes(passphrase);
|
||||
setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
|
||||
await generateAndSaveIntermediateKeyAttributes(
|
||||
passphrase,
|
||||
keyAttributes,
|
||||
masterKey,
|
||||
masterKey
|
||||
);
|
||||
|
||||
await setSessionKeys(masterKey);
|
||||
|
@ -68,113 +69,110 @@ export default function SignUp(props: SignUpProps) {
|
|||
setLoading(false);
|
||||
};
|
||||
|
||||
return (<>
|
||||
<Card.Title style={{ marginBottom: '32px' }}>
|
||||
<LogoImg src="/icon.svg" />
|
||||
{constants.SIGN_UP}
|
||||
</Card.Title>
|
||||
<Formik<FormValues>
|
||||
initialValues={{
|
||||
email: '',
|
||||
passphrase: '',
|
||||
confirm: '',
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
email: Yup.string()
|
||||
.email(constants.EMAIL_ERROR)
|
||||
.required(constants.REQUIRED),
|
||||
passphrase: Yup.string().required(
|
||||
constants.REQUIRED,
|
||||
),
|
||||
confirm: Yup.string().required(constants.REQUIRED),
|
||||
})}
|
||||
validateOnChange={false}
|
||||
validateOnBlur={false}
|
||||
onSubmit={registerUser}
|
||||
>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
touched,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
}): JSX.Element => (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="registrationForm.email">
|
||||
<Form.Control
|
||||
type="email"
|
||||
placeholder={constants.ENTER_EMAIL}
|
||||
value={values.email}
|
||||
onChange={handleChange('email')}
|
||||
isInvalid={Boolean(
|
||||
touched.email && errors.email,
|
||||
)}
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
return (
|
||||
<>
|
||||
<Card.Title style={{ marginBottom: '32px' }}>
|
||||
<LogoImg src="/icon.svg" />
|
||||
{constants.SIGN_UP}
|
||||
</Card.Title>
|
||||
<Formik<FormValues>
|
||||
initialValues={{
|
||||
email: '',
|
||||
passphrase: '',
|
||||
confirm: '',
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
email: Yup.string()
|
||||
.email(constants.EMAIL_ERROR)
|
||||
.required(constants.REQUIRED),
|
||||
passphrase: Yup.string().required(constants.REQUIRED),
|
||||
confirm: Yup.string().required(constants.REQUIRED),
|
||||
})}
|
||||
validateOnChange={false}
|
||||
validateOnBlur={false}
|
||||
onSubmit={registerUser}>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
touched,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
}): JSX.Element => (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="registrationForm.email">
|
||||
<Form.Control
|
||||
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>
|
||||
<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">
|
||||
{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}
|
||||
/>
|
||||
<br />
|
||||
<Button block variant="link" onClick={props.login}>
|
||||
{constants.ACCOUNT_EXISTS}
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</>);
|
||||
<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 * as Yup from 'yup';
|
||||
import SubmitButton from './SubmitButton';
|
||||
import styled from 'styled-components';
|
||||
import Visibility from './icons/Visibility';
|
||||
import VisibilityOff from './icons/VisibilityOff';
|
||||
|
||||
interface formValues {
|
||||
passphrase: string;
|
||||
|
@ -15,11 +18,29 @@ interface Props {
|
|||
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) {
|
||||
const [loading, SetLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const submitForm = async (
|
||||
values: formValues,
|
||||
{ setFieldError }: FormikHelpers<formValues>,
|
||||
{ setFieldError }: FormikHelpers<formValues>
|
||||
) => {
|
||||
SetLoading(true);
|
||||
await props.callback(values.passphrase, setFieldError);
|
||||
|
@ -33,27 +54,39 @@ export default function SingleInputForm(props: Props) {
|
|||
passphrase: Yup.string().required(constants.REQUIRED),
|
||||
})}
|
||||
validateOnChange={false}
|
||||
validateOnBlur={false}
|
||||
>
|
||||
{({
|
||||
values, touched, errors, handleChange, handleSubmit,
|
||||
}) => (
|
||||
validateOnBlur={false}>
|
||||
{({ values, touched, errors, handleChange, handleSubmit }) => (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
type={props.fieldType}
|
||||
placeholder={props.placeholder}
|
||||
value={values.passphrase}
|
||||
onChange={handleChange('passphrase')}
|
||||
isInvalid={Boolean(
|
||||
touched.passphrase && errors.passphrase,
|
||||
<Group>
|
||||
<Form.Control
|
||||
type={showPassword ? 'text' : props.fieldType}
|
||||
placeholder={props.placeholder}
|
||||
value={values.passphrase}
|
||||
onChange={handleChange('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}
|
||||
autoFocus
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{errors.passphrase}
|
||||
</Form.Control.Feedback>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{errors.passphrase}
|
||||
</Form.Control.Feedback>
|
||||
</Group>
|
||||
</Form.Group>
|
||||
<SubmitButton
|
||||
buttonText={props.buttonText}
|
||||
|
|
|
@ -7,22 +7,24 @@ interface Props {
|
|||
inline?: any;
|
||||
disabled?: boolean;
|
||||
}
|
||||
const SubmitButton = ({
|
||||
loading, buttonText, inline, disabled,
|
||||
}: Props) => (
|
||||
const SubmitButton = ({ loading, buttonText, inline, disabled }: Props) => (
|
||||
<Button
|
||||
className="submitButton"
|
||||
variant="outline-success"
|
||||
type="submit"
|
||||
block={!inline}
|
||||
disabled={loading || disabled}
|
||||
style={{ padding: '6px 1em' }}
|
||||
>
|
||||
style={{ padding: '6px 1em' }}>
|
||||
{loading ? (
|
||||
<Spinner
|
||||
as="span"
|
||||
animation="border"
|
||||
style={{ width: '22px', height: '22px', borderWidth: '0.20em', color: '#2dc262' }}
|
||||
style={{
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
borderWidth: '0.20em',
|
||||
color: '#2dc262',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
buttonText
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useRouter } from 'next/router';
|
||||
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 { Button, Row } from 'react-bootstrap';
|
||||
import { disableTwoFactor, getTwoFactorStatus } from 'services/userService';
|
||||
|
@ -13,7 +13,7 @@ interface Props {
|
|||
show: boolean;
|
||||
onHide: () => void;
|
||||
setDialogMessage: SetDialogMessage;
|
||||
setLoading: SetLoading
|
||||
setLoading: SetLoading;
|
||||
closeSidebar: () => void;
|
||||
}
|
||||
|
||||
|
@ -26,12 +26,16 @@ function TwoFactorModal(props: Props) {
|
|||
if (!props.show) {
|
||||
return;
|
||||
}
|
||||
const isTwoFactorEnabled = getData(LS_KEYS.USER).isTwoFactorEnabled ?? false;
|
||||
const isTwoFactorEnabled =
|
||||
getData(LS_KEYS.USER).isTwoFactorEnabled ?? false;
|
||||
setTwoFactorStatus(isTwoFactorEnabled);
|
||||
const main = async () => {
|
||||
const isTwoFactorEnabled = await getTwoFactorStatus();
|
||||
setTwoFactorStatus(isTwoFactorEnabled);
|
||||
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), isTwoFactorEnabled: false });
|
||||
setData(LS_KEYS.USER, {
|
||||
...getData(LS_KEYS.USER),
|
||||
isTwoFactorEnabled: false,
|
||||
});
|
||||
};
|
||||
main();
|
||||
}, [props.show]);
|
||||
|
@ -51,12 +55,21 @@ function TwoFactorModal(props: Props) {
|
|||
const twoFactorDisable = async () => {
|
||||
try {
|
||||
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.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) {
|
||||
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 () => {
|
||||
|
@ -82,38 +95,60 @@ function TwoFactorModal(props: Props) {
|
|||
attributes={{
|
||||
title: constants.TWO_FACTOR_AUTHENTICATION,
|
||||
staticBackdrop: true,
|
||||
}}
|
||||
|
||||
>
|
||||
<div {...(!isTwoFactorEnabled ? { style: { padding: '10px 10px 30px 10px' } } : { style: { padding: '10px' } })}>
|
||||
{
|
||||
isTwoFactorEnabled ?
|
||||
<>
|
||||
<Row>
|
||||
<Label>{constants.UPDATE_TWO_FACTOR_HINT}</Label>
|
||||
<Value>
|
||||
<Button variant={'outline-success'} onClick={warnTwoFactorReconfigure}>{constants.RECONFIGURE}</Button>
|
||||
</Value>
|
||||
</Row>
|
||||
<Row>
|
||||
<Label>{constants.DISABLE_TWO_FACTOR_HINT} </Label>
|
||||
<Value>
|
||||
<Button variant={'outline-danger'} onClick={warnTwoFactorDisable}>{constants.DISABLE}</Button>
|
||||
</Value>
|
||||
</Row>
|
||||
|
||||
</> : (
|
||||
<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
|
||||
{...(!isTwoFactorEnabled
|
||||
? { style: { padding: '10px 10px 30px 10px' } }
|
||||
: { style: { padding: '10px' } })}>
|
||||
{isTwoFactorEnabled ? (
|
||||
<>
|
||||
<Row>
|
||||
<Label>{constants.UPDATE_TWO_FACTOR_HINT}</Label>
|
||||
<Value>
|
||||
<Button
|
||||
variant={'outline-success'}
|
||||
onClick={warnTwoFactorReconfigure}>
|
||||
{constants.RECONFIGURE}
|
||||
</Button>
|
||||
</Value>
|
||||
</Row>
|
||||
<Row>
|
||||
<Label>{constants.DISABLE_TWO_FACTOR_HINT} </Label>
|
||||
<Value>
|
||||
<Button
|
||||
variant={'outline-danger'}
|
||||
onClick={warnTwoFactorDisable}>
|
||||
{constants.DISABLE}
|
||||
</Button>
|
||||
</Value>
|
||||
</Row>
|
||||
</>
|
||||
) : (
|
||||
<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>
|
||||
</MessageDialog >
|
||||
</MessageDialog>
|
||||
);
|
||||
}
|
||||
export default TwoFactorModal;
|
||||
|
|
|
@ -11,8 +11,8 @@ interface formValues {
|
|||
otp: string;
|
||||
}
|
||||
interface Props {
|
||||
onSubmit: any
|
||||
back: any
|
||||
onSubmit: any;
|
||||
back: any;
|
||||
buttonText: string;
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ export default function VerifyTwoFactor(props: Props) {
|
|||
const otpInputRef = useRef(null);
|
||||
const submitForm = async (
|
||||
{ otp }: formValues,
|
||||
{ setFieldError, resetForm }: FormikHelpers<formValues>,
|
||||
{ setFieldError, resetForm }: FormikHelpers<formValues>
|
||||
) => {
|
||||
try {
|
||||
setWaiting(true);
|
||||
|
@ -36,7 +36,11 @@ export default function VerifyTwoFactor(props: Props) {
|
|||
setWaiting(false);
|
||||
};
|
||||
|
||||
const onChange = (otp: string, callback: Function, triggerSubmit: Function) => {
|
||||
const onChange = (
|
||||
otp: string,
|
||||
callback: Function,
|
||||
triggerSubmit: Function
|
||||
) => {
|
||||
callback(otp);
|
||||
if (otp.length === 6) {
|
||||
triggerSubmit(otp);
|
||||
|
@ -44,13 +48,14 @@ export default function VerifyTwoFactor(props: Props) {
|
|||
};
|
||||
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>
|
||||
initialValues={{ otp: '' }}
|
||||
validateOnChange={false}
|
||||
validateOnBlur={false}
|
||||
onSubmit={submitForm}
|
||||
>
|
||||
onSubmit={submitForm}>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
|
@ -58,8 +63,13 @@ export default function VerifyTwoFactor(props: Props) {
|
|||
handleSubmit,
|
||||
submitForm,
|
||||
}) => (
|
||||
<Form noValidate onSubmit={handleSubmit} style={{ width: '100%' }}>
|
||||
<Form.Group style={{ marginBottom: '32px' }} controlId="formBasicEmail">
|
||||
<Form
|
||||
noValidate
|
||||
onSubmit={handleSubmit}
|
||||
style={{ width: '100%' }}>
|
||||
<Form.Group
|
||||
style={{ marginBottom: '32px' }}
|
||||
controlId="formBasicEmail">
|
||||
<DeadCenter>
|
||||
<OtpInput
|
||||
placeholder="123456"
|
||||
|
@ -67,16 +77,27 @@ export default function VerifyTwoFactor(props: Props) {
|
|||
shouldAutoFocus
|
||||
value={values.otp}
|
||||
onChange={(otp) => {
|
||||
onChange(otp, handleChange('otp'), submitForm);
|
||||
onChange(
|
||||
otp,
|
||||
handleChange('otp'),
|
||||
submitForm
|
||||
);
|
||||
}}
|
||||
numInputs={6}
|
||||
separator={'-'}
|
||||
isInputNum
|
||||
className={'otp-input'}
|
||||
/>
|
||||
{errors.otp &&
|
||||
<div style={{ display: 'block', marginTop: '16px' }} className="invalid-feedback">{constants.INCORRECT_CODE}</div>
|
||||
}
|
||||
{errors.otp && (
|
||||
<div
|
||||
style={{
|
||||
display: 'block',
|
||||
marginTop: '16px',
|
||||
}}
|
||||
className="invalid-feedback">
|
||||
{constants.INCORRECT_CODE}
|
||||
</div>
|
||||
)}
|
||||
</DeadCenter>
|
||||
</Form.Group>
|
||||
<SubmitButton
|
||||
|
@ -87,10 +108,6 @@ export default function VerifyTwoFactor(props: Props) {
|
|||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -7,9 +7,15 @@ export default function AddIcon(props) {
|
|||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
fill='currentColor'
|
||||
>
|
||||
<g><rect fill="none" height="24" width="24"/></g><g><g><path d="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6V13z"/></g></g>
|
||||
fill="currentColor">
|
||||
<g>
|
||||
<rect fill="none" height="24" width="24" />
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6V13z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,8 +7,7 @@ export default function ArrowEast(props) {
|
|||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
{...props}
|
||||
>
|
||||
{...props}>
|
||||
<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" />
|
||||
</svg>
|
||||
|
|
|
@ -7,8 +7,7 @@ export default function CloudUpload(props) {
|
|||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
fill="currentColor"
|
||||
>
|
||||
fill="currentColor">
|
||||
<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" />
|
||||
</svg>
|
||||
|
|
|
@ -6,8 +6,7 @@ export default function DateIcon(props) {
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
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" />
|
||||
</svg>
|
||||
);
|
||||
|
|
|
@ -6,8 +6,7 @@ export default function DateIcon(props) {
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
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" />
|
||||
</svg>
|
||||
);
|
||||
|
|
|
@ -7,8 +7,7 @@ export default function DeleteIcon(props) {
|
|||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
fill='currentColor'
|
||||
>
|
||||
fill="currentColor">
|
||||
<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" />
|
||||
</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';
|
||||
export default function FolderIcon(props) {
|
||||
return (
|
||||
<div >
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
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" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,16 +4,16 @@ import styled from 'styled-components';
|
|||
const Rotate = styled.div<{ disabled }>`
|
||||
width: 24px;
|
||||
height: 27px;
|
||||
${(props) => !props.disabled && '-webkit-animation: rotation 1s infinite linear'};
|
||||
cursor:${(props) => props.disabled && 'pointer'};
|
||||
${(props) =>
|
||||
!props.disabled && '-webkit-animation: rotation 1s infinite linear'};
|
||||
cursor: ${(props) => props.disabled && 'pointer'};
|
||||
transition-duration: 0.8s;
|
||||
transition-property: transform;
|
||||
&:hover {
|
||||
color:#fff;
|
||||
color: #fff;
|
||||
transform: rotate(90deg);
|
||||
-webkit-transform: rotate(90deg);
|
||||
}
|
||||
|
||||
`;
|
||||
export default function InProgressIcon(props) {
|
||||
return (
|
||||
|
@ -24,9 +24,10 @@ export default function InProgressIcon(props) {
|
|||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
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>
|
||||
</ Rotate>
|
||||
</Rotate>
|
||||
);
|
||||
}
|
||||
InProgressIcon.defaultProps = {
|
||||
|
@ -35,4 +36,3 @@ InProgressIcon.defaultProps = {
|
|||
width: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,8 +6,7 @@ export default function LocationIcon(props) {
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
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" />
|
||||
<circle cx="12" cy="9" r="2.5" />
|
||||
</svg>
|
||||
|
|
|
@ -8,8 +8,7 @@ export default function NavigateNext(props) {
|
|||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
{...props}>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
|
||||
</svg>
|
||||
|
|
|
@ -6,8 +6,7 @@ export default function PlayCircleOutline(props) {
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
>
|
||||
width={props.width}>
|
||||
<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" />
|
||||
</svg>
|
||||
|
|
|
@ -6,8 +6,7 @@ export default function SadFace(props) {
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
>
|
||||
width={props.width}>
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<circle cx="15.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"
|
||||
height={props.height}
|
||||
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" />
|
||||
</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"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
>
|
||||
width={props.width}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,31 @@
|
|||
import React from 'react';
|
||||
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 (
|
||||
<Alert
|
||||
variant="danger"
|
||||
variant={props.variant ?? 'danger'}
|
||||
style={{
|
||||
display: bannerMessage ? 'block' : 'none',
|
||||
display:
|
||||
props.bannerMessage || props.children ? 'block' : 'none',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{bannerMessage}
|
||||
|
||||
border: 'none',
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,15 +19,13 @@ function ChoiceModal({
|
|||
<MessageDialog
|
||||
size="lg"
|
||||
{...props}
|
||||
attributes={{ title: constants.MULTI_FOLDER_UPLOAD }}
|
||||
>
|
||||
attributes={{ title: constants.MULTI_FOLDER_UPLOAD }}>
|
||||
<p>{constants.UPLOAD_STRATEGY_CHOICE}</p>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Button
|
||||
variant="outline-success"
|
||||
onClick={() => {
|
||||
|
@ -38,8 +36,7 @@ function ChoiceModal({
|
|||
padding: '12px 24px',
|
||||
flex: 2,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{constants.UPLOAD_STRATEGY_SINGLE_COLLECTION}
|
||||
</Button>
|
||||
<div
|
||||
|
@ -48,8 +45,7 @@ function ChoiceModal({
|
|||
textAlign: 'center',
|
||||
minWidth: '100px',
|
||||
margin: '2% auto',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<strong>{constants.OR}</strong>
|
||||
</div>
|
||||
<Button
|
||||
|
@ -62,8 +58,7 @@ function ChoiceModal({
|
|||
padding: '12px 24px',
|
||||
flex: 2,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{constants.UPLOAD_STRATEGY_COLLECTION_PER_FOLDER}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -51,8 +51,7 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
|
|||
size="sm"
|
||||
attributes={{
|
||||
title: attributes?.title,
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Formik<formValues>
|
||||
initialValues={{ albumName: attributes.autoFilledName }}
|
||||
validationSchema={Yup.object().shape({
|
||||
|
@ -60,11 +59,8 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
|
|||
})}
|
||||
validateOnChange={false}
|
||||
validateOnBlur={false}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({
|
||||
values, touched, errors, handleChange, handleSubmit,
|
||||
}) => (
|
||||
onSubmit={onSubmit}>
|
||||
{({ values, touched, errors, handleChange, handleSubmit }) => (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
|
@ -73,7 +69,7 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
|
|||
value={values.albumName}
|
||||
onChange={handleChange('albumName')}
|
||||
isInvalid={Boolean(
|
||||
touched.albumName && errors.albumName,
|
||||
touched.albumName && errors.albumName
|
||||
)}
|
||||
placeholder={constants.ENTER_ALBUM_NAME}
|
||||
ref={collectionNameInputRef}
|
||||
|
@ -82,8 +78,7 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
|
|||
|
||||
<Form.Control.Feedback
|
||||
type="invalid"
|
||||
className="text-center"
|
||||
>
|
||||
className="text-center">
|
||||
{errors.albumName}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
|
|
@ -24,7 +24,7 @@ interface Props {
|
|||
const CollectionOptions = (props: Props) => {
|
||||
const collectionRename = async (
|
||||
selectedCollection: Collection,
|
||||
newName: string,
|
||||
newName: string
|
||||
) => {
|
||||
if (selectedCollection.name !== newName) {
|
||||
await renameCollection(selectedCollection, newName);
|
||||
|
@ -37,16 +37,16 @@ const CollectionOptions = (props: Props) => {
|
|||
buttonText: constants.RENAME,
|
||||
autoFilledName: getSelectedCollection(
|
||||
props.selectedCollectionID,
|
||||
props.collections,
|
||||
props.collections
|
||||
)?.name,
|
||||
callback: (newName) => {
|
||||
props.startLoadingBar();
|
||||
collectionRename(
|
||||
getSelectedCollection(
|
||||
props.selectedCollectionID,
|
||||
props.collections,
|
||||
props.collections
|
||||
),
|
||||
newName,
|
||||
newName
|
||||
);
|
||||
},
|
||||
});
|
||||
|
@ -64,7 +64,7 @@ const CollectionOptions = (props: Props) => {
|
|||
props.selectedCollectionID,
|
||||
props.syncWithRemote,
|
||||
props.redirectToAll,
|
||||
props.setDialogMessage,
|
||||
props.setDialogMessage
|
||||
);
|
||||
},
|
||||
variant: 'danger',
|
||||
|
@ -78,8 +78,7 @@ const CollectionOptions = (props: Props) => {
|
|||
const MenuLink = (props) => (
|
||||
<LinkButton
|
||||
style={{ fontSize: '14px', fontWeight: 700, padding: '8px 1em' }}
|
||||
{...props}
|
||||
>
|
||||
{...props}>
|
||||
{props.children}
|
||||
</LinkButton>
|
||||
);
|
||||
|
@ -89,8 +88,7 @@ const CollectionOptions = (props: Props) => {
|
|||
style={{
|
||||
background: '#282828',
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{props.children}
|
||||
</ListGroup.Item>
|
||||
);
|
||||
|
@ -111,8 +109,7 @@ const CollectionOptions = (props: Props) => {
|
|||
<MenuItem>
|
||||
<MenuLink
|
||||
variant="danger"
|
||||
onClick={confirmDeleteCollection}
|
||||
>
|
||||
onClick={confirmDeleteCollection}>
|
||||
{constants.DELETE}
|
||||
</MenuLink>
|
||||
</MenuItem>
|
||||
|
|
|
@ -37,7 +37,6 @@ interface Props {
|
|||
directlyShowNextModal: boolean;
|
||||
collectionsAndTheirLatestFile: CollectionAndItsLatestFile[];
|
||||
attributes: CollectionSelectorAttributes;
|
||||
syncWithRemote:(force?: boolean, silent?:boolean)=>Promise<void>;
|
||||
}
|
||||
function CollectionSelector({
|
||||
attributes,
|
||||
|
@ -62,12 +61,11 @@ function CollectionSelector({
|
|||
onClick={() => {
|
||||
attributes.callback(item.collection);
|
||||
props.onHide();
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<CollectionCard>
|
||||
<PreviewCard
|
||||
file={item.file}
|
||||
updateUrl={() => { }}
|
||||
updateUrl={() => {}}
|
||||
forcedEnable
|
||||
/>
|
||||
<Card.Text className="text-center">
|
||||
|
@ -75,11 +73,15 @@ function CollectionSelector({
|
|||
</Card.Text>
|
||||
</CollectionCard>
|
||||
</CollectionIcon>
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal {...props} size="xl" centered>
|
||||
<Modal
|
||||
{...props}
|
||||
size="xl"
|
||||
centered
|
||||
contentClassName="plan-selector-modal-content">
|
||||
<Modal.Header closeButton onHide={() => props.onHide(true)}>
|
||||
<Modal.Title>{attributes.title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
@ -88,8 +90,7 @@ function CollectionSelector({
|
|||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<AddCollectionButton showNextModal={attributes.showNextModal} />
|
||||
{CollectionIcons}
|
||||
</Modal.Body>
|
||||
|
|
|
@ -3,9 +3,7 @@ import { SetDialogMessage } from 'components/MessageDialog';
|
|||
import NavigationButton, {
|
||||
SCROLL_DIRECTION,
|
||||
} from 'components/NavigationButton';
|
||||
import React, {
|
||||
useEffect, useRef, useState,
|
||||
} from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
import { Collection, CollectionType } from 'services/collectionService';
|
||||
import { User } from 'services/userService';
|
||||
|
@ -26,7 +24,7 @@ interface CollectionProps {
|
|||
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
||||
startLoadingBar: () => void;
|
||||
searchMode: boolean;
|
||||
collectionFilesCount: Map<number, number>
|
||||
collectionFilesCount: Map<number, number>;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
|
@ -38,7 +36,7 @@ const Container = styled.div`
|
|||
position: relative;
|
||||
padding: 0 24px;
|
||||
|
||||
@media(max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
@ -58,7 +56,8 @@ const Chip = styled.button<{ active: boolean }>`
|
|||
padding-left: 24px;
|
||||
margin: 3px;
|
||||
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;
|
||||
&:hover {
|
||||
background-color: ${(props) => !props.active && '#bbbbbb'};
|
||||
|
@ -71,9 +70,11 @@ const Chip = styled.button<{ active: boolean }>`
|
|||
|
||||
export default function Collections(props: CollectionProps) {
|
||||
const { selected, collections, selectCollection } = props;
|
||||
const [selectedCollectionID, setSelectedCollectionID] = useState<number>(null);
|
||||
const [selectedCollectionID, setSelectedCollectionID] =
|
||||
useState<number>(null);
|
||||
const collectionRef = useRef<HTMLDivElement>(null);
|
||||
const [collectionShareModalView, setCollectionShareModalView] = useState(false);
|
||||
const [collectionShareModalView, setCollectionShareModalView] =
|
||||
useState(false);
|
||||
const [scrollObj, setScrollObj] = useState<{
|
||||
scrollLeft?: number;
|
||||
scrollWidth?: number;
|
||||
|
@ -82,7 +83,8 @@ export default function Collections(props: CollectionProps) {
|
|||
|
||||
const updateScrollObj = () => {
|
||||
if (collectionRef.current) {
|
||||
const { scrollLeft, scrollWidth, clientWidth } = collectionRef.current;
|
||||
const { scrollLeft, scrollWidth, clientWidth } =
|
||||
collectionRef.current;
|
||||
setScrollObj({ scrollLeft, scrollWidth, clientWidth });
|
||||
}
|
||||
};
|
||||
|
@ -126,10 +128,13 @@ export default function Collections(props: CollectionProps) {
|
|||
const renderTooltip = (collectionID) => {
|
||||
const fileCount = props.collectionFilesCount?.get(collectionID);
|
||||
return (
|
||||
<Tooltip style={{
|
||||
padding: '0',
|
||||
paddingBottom: '5px',
|
||||
}} id="button-tooltip" {...props}>
|
||||
<Tooltip
|
||||
style={{
|
||||
padding: '0',
|
||||
paddingBottom: '5px',
|
||||
}}
|
||||
id="button-tooltip"
|
||||
{...props}>
|
||||
<div
|
||||
{...props}
|
||||
style={{
|
||||
|
@ -139,8 +144,7 @@ export default function Collections(props: CollectionProps) {
|
|||
color: '#ddd',
|
||||
borderRadius: 3,
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{fileCount} {fileCount > 1 ? 'items' : 'item'}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
@ -155,7 +159,7 @@ export default function Collections(props: CollectionProps) {
|
|||
onHide={() => setCollectionShareModalView(false)}
|
||||
collection={getSelectedCollection(
|
||||
selectedCollectionID,
|
||||
props.collections,
|
||||
props.collections
|
||||
)}
|
||||
syncWithRemote={props.syncWithRemote}
|
||||
/>
|
||||
|
@ -181,35 +185,45 @@ export default function Collections(props: CollectionProps) {
|
|||
key={item.id}
|
||||
placement="top"
|
||||
delay={{ show: 250, hide: 400 }}
|
||||
overlay={renderTooltip(item.id)}
|
||||
>
|
||||
overlay={renderTooltip(item.id)}>
|
||||
<Chip
|
||||
|
||||
active={selected === item.id}
|
||||
onClick={clickHandler(item)}
|
||||
>
|
||||
onClick={clickHandler(item)}>
|
||||
{item.name}
|
||||
{item.type !== CollectionType.favorites &&
|
||||
item.owner.id === user?.id ? (<OverlayTrigger
|
||||
item.owner.id === user?.id ? (
|
||||
<OverlayTrigger
|
||||
rootClose
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
overlay={collectionOptions}
|
||||
>
|
||||
overlay={collectionOptions}>
|
||||
<OptionIcon
|
||||
onClick={() => setSelectedCollectionID(item.id)}
|
||||
onClick={() =>
|
||||
setSelectedCollectionID(
|
||||
item.id
|
||||
)
|
||||
}
|
||||
/>
|
||||
</OverlayTrigger>) : (<div style={{
|
||||
display: 'inline-block',
|
||||
width: '24px',
|
||||
}}
|
||||
/>)}
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '24px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Chip>
|
||||
</OverlayTrigger>
|
||||
))}
|
||||
</Wrapper>
|
||||
{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>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -4,6 +4,7 @@ enum ButtonVariant {
|
|||
success = 'success',
|
||||
danger = 'danger',
|
||||
secondary = 'secondary',
|
||||
warning = 'warning',
|
||||
}
|
||||
type Props = React.PropsWithChildren<{
|
||||
onClick: any;
|
||||
|
@ -11,29 +12,30 @@ type Props = React.PropsWithChildren<{
|
|||
style?: any;
|
||||
}>;
|
||||
|
||||
export default function LinkButton(props: Props) {
|
||||
function getButtonColor(variant: string) {
|
||||
switch (variant) {
|
||||
case ButtonVariant.success:
|
||||
return '#2dc262';
|
||||
case ButtonVariant.danger:
|
||||
return '#c93f3f';
|
||||
case ButtonVariant.secondary:
|
||||
return '#858585';
|
||||
default:
|
||||
return '#d1d1d1';
|
||||
}
|
||||
export function getVariantColor(variant: string) {
|
||||
switch (variant) {
|
||||
case ButtonVariant.success:
|
||||
return '#2dc262';
|
||||
case ButtonVariant.danger:
|
||||
return '#c93f3f';
|
||||
case ButtonVariant.secondary:
|
||||
return '#858585';
|
||||
case ButtonVariant.warning:
|
||||
return '#D7BB63';
|
||||
default:
|
||||
return '#d1d1d1';
|
||||
}
|
||||
}
|
||||
export default function LinkButton(props: Props) {
|
||||
return (
|
||||
<h5
|
||||
style={{
|
||||
color: getButtonColor(props.variant),
|
||||
color: getVariantColor(props.variant),
|
||||
cursor: 'pointer',
|
||||
marginBottom: 0,
|
||||
...props.style,
|
||||
}}
|
||||
onClick={props?.onClick ?? (() => null)}
|
||||
>
|
||||
onClick={props?.onClick ?? (() => null)}>
|
||||
{props.children}
|
||||
</h5>
|
||||
);
|
||||
|
|
|
@ -16,17 +16,18 @@ const OptionIcon = ({ onClick }: Props) => (
|
|||
onClick();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
style={{ marginBottom: '2px' }}
|
||||
>
|
||||
style={{ marginBottom: '2px' }}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="20px"
|
||||
width="24px"
|
||||
viewBox="0 0 24 24"
|
||||
fill="#000000"
|
||||
>
|
||||
fill="#000000">
|
||||
<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>
|
||||
</OptionIconWrapper>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,6 @@ import styled from 'styled-components';
|
|||
import billingService, { Plan, Subscription } from 'services/billingService';
|
||||
import {
|
||||
convertBytesToGBs,
|
||||
getPlans,
|
||||
getUserSubscription,
|
||||
isUserSubscribedPlan,
|
||||
isSubscriptionCancelled,
|
||||
|
@ -16,6 +15,7 @@ import {
|
|||
hasStripeSubscription,
|
||||
hasPaidSubscription,
|
||||
isOnFreePlan,
|
||||
planForSubscription,
|
||||
} from 'utils/billingUtil';
|
||||
import { reverseString } from 'utils/common';
|
||||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
|
@ -25,7 +25,7 @@ import { DeadCenter, SetLoading } from 'pages/gallery';
|
|||
|
||||
export const PlanIcon = styled.div<{ selected: boolean }>`
|
||||
border-radius: 20px;
|
||||
width: 250px;
|
||||
width: 220px;
|
||||
border: 2px solid #333;
|
||||
padding: 30px;
|
||||
margin: 10px;
|
||||
|
@ -55,7 +55,7 @@ export const PlanIcon = styled.div<{ selected: boolean }>`
|
|||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
transform: scale(1.1);
|
||||
background-color: #ffffff11;
|
||||
}
|
||||
|
||||
|
@ -76,18 +76,33 @@ enum PLAN_PERIOD {
|
|||
}
|
||||
function PlanSelector(props: Props) {
|
||||
const subscription: Subscription = getUserSubscription();
|
||||
const plans = getPlans();
|
||||
const [plans, setPlans] = useState<Plan[]>(null);
|
||||
const [planPeriod, setPlanPeriod] = useState<PLAN_PERIOD>(PLAN_PERIOD.YEAR);
|
||||
const togglePeriod = () => {
|
||||
setPlanPeriod((prevPeriod) => (prevPeriod === PLAN_PERIOD.MONTH ?
|
||||
PLAN_PERIOD.YEAR :
|
||||
PLAN_PERIOD.MONTH));
|
||||
setPlanPeriod((prevPeriod) =>
|
||||
prevPeriod === PLAN_PERIOD.MONTH
|
||||
? PLAN_PERIOD.YEAR
|
||||
: PLAN_PERIOD.MONTH
|
||||
);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!plans && props.modalView) {
|
||||
if (props.modalView) {
|
||||
const main = async () => {
|
||||
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);
|
||||
};
|
||||
main();
|
||||
|
@ -108,7 +123,7 @@ function PlanSelector(props: Props) {
|
|||
} else if (hasStripeSubscription(subscription)) {
|
||||
props.setDialogMessage({
|
||||
title: `${constants.CONFIRM} ${reverseString(
|
||||
constants.UPDATE_SUBSCRIPTION,
|
||||
constants.UPDATE_SUBSCRIPTION
|
||||
)}`,
|
||||
content: constants.UPDATE_SUBSCRIPTION_MESSAGE,
|
||||
staticBackdrop: true,
|
||||
|
@ -119,7 +134,7 @@ function PlanSelector(props: Props) {
|
|||
plan,
|
||||
props.setDialogMessage,
|
||||
props.setLoading,
|
||||
props.closeModal,
|
||||
props.closeModal
|
||||
),
|
||||
variant: 'success',
|
||||
},
|
||||
|
@ -148,16 +163,15 @@ function PlanSelector(props: Props) {
|
|||
key={plan.stripeID}
|
||||
className="subscription-plan-selector"
|
||||
selected={isUserSubscribedPlan(plan, subscription)}
|
||||
>
|
||||
onClick={async () => await onPlanSelect(plan)}>
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
color: '#ECECEC',
|
||||
fontWeight: 900,
|
||||
fontSize: '72px',
|
||||
lineHeight: '72px',
|
||||
}}
|
||||
>
|
||||
fontSize: '40px',
|
||||
lineHeight: '40px',
|
||||
}}>
|
||||
{convertBytesToGBs(plan.storage, 0)}
|
||||
</span>
|
||||
<span
|
||||
|
@ -165,27 +179,32 @@ function PlanSelector(props: Props) {
|
|||
color: '#858585',
|
||||
fontSize: '24px',
|
||||
fontWeight: 900,
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{' '}
|
||||
GB
|
||||
GB
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="bold-text"
|
||||
style={{ color: '#aaa', lineHeight: '36px', fontSize: '20px' }}
|
||||
>
|
||||
style={{
|
||||
color: '#aaa',
|
||||
lineHeight: '36px',
|
||||
fontSize: '20px',
|
||||
}}>
|
||||
{`${plan.price} / ${plan.period}`}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline-success"
|
||||
block
|
||||
style={{ marginTop: '30px' }}
|
||||
disabled={isUserSubscribedPlan(plan, subscription)}
|
||||
onClick={async () => (await onPlanSelect(plan))}
|
||||
>
|
||||
style={{
|
||||
marginTop: '20px',
|
||||
fontSize: '14px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
disabled={isUserSubscribedPlan(plan, subscription)}>
|
||||
{constants.CHOOSE_PLAN_BTN}
|
||||
<ArrowEast style={{ marginLeft: '10px' }} />
|
||||
<ArrowEast style={{ marginLeft: '5px' }} />
|
||||
</Button>
|
||||
</PlanIcon>
|
||||
));
|
||||
|
@ -196,19 +215,18 @@ function PlanSelector(props: Props) {
|
|||
size="xl"
|
||||
centered
|
||||
backdrop={hasPaidSubscription(subscription) ? 'true' : 'static'}
|
||||
>
|
||||
contentClassName="plan-selector-modal-content">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title
|
||||
style={{
|
||||
marginLeft: '12px',
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<span>
|
||||
{hasPaidSubscription(subscription) ?
|
||||
constants.MANAGE_PLAN :
|
||||
constants.CHOOSE_PLAN}
|
||||
{hasPaidSubscription(subscription)
|
||||
? constants.MANAGE_PLAN
|
||||
: constants.CHOOSE_PLAN}
|
||||
</span>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
@ -217,22 +235,23 @@ function PlanSelector(props: Props) {
|
|||
<div style={{ display: 'flex' }}>
|
||||
<span
|
||||
className="bold-text"
|
||||
style={{ fontSize: '20px' }}
|
||||
>
|
||||
style={{ fontSize: '16px' }}>
|
||||
{constants.MONTHLY}
|
||||
</span>
|
||||
|
||||
<Form.Switch
|
||||
checked={planPeriod === PLAN_PERIOD.YEAR}
|
||||
id="plan-period-toggler"
|
||||
style={{ margin: '-4px 0 20px 15px' }}
|
||||
style={{
|
||||
margin: '-4px 0 20px 15px',
|
||||
fontSize: '10px',
|
||||
}}
|
||||
className="custom-switch-md"
|
||||
onChange={togglePeriod}
|
||||
/>
|
||||
<span
|
||||
className="bold-text"
|
||||
style={{ fontSize: '20px' }}
|
||||
>
|
||||
style={{ fontSize: '16px' }}>
|
||||
{constants.YEARLY}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -243,9 +262,8 @@ function PlanSelector(props: Props) {
|
|||
justifyContent: 'space-around',
|
||||
flexWrap: 'wrap',
|
||||
minHeight: '212px',
|
||||
margin: '24px 0',
|
||||
}}
|
||||
>
|
||||
margin: '5px 0',
|
||||
}}>
|
||||
{plans && PlanIcons}
|
||||
</div>
|
||||
<DeadCenter style={{ marginBottom: '30px' }}>
|
||||
|
@ -254,55 +272,55 @@ function PlanSelector(props: Props) {
|
|||
{isSubscriptionCancelled(subscription) ? (
|
||||
<LinkButton
|
||||
variant="success"
|
||||
onClick={() => props.setDialogMessage({
|
||||
title:
|
||||
constants.CONFIRM_ACTIVATE_SUBSCRIPTION,
|
||||
content: constants.ACTIVATE_SUBSCRIPTION_MESSAGE(
|
||||
subscription.expiryTime,
|
||||
),
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text:
|
||||
constants.ACTIVATE_SUBSCRIPTION,
|
||||
action: activateSubscription.bind(
|
||||
null,
|
||||
props.setDialogMessage,
|
||||
props.closeModal,
|
||||
props.setLoading,
|
||||
),
|
||||
variant: 'success',
|
||||
},
|
||||
close: {
|
||||
text: constants.CANCEL,
|
||||
},
|
||||
})}
|
||||
>
|
||||
onClick={() =>
|
||||
props.setDialogMessage({
|
||||
title: constants.CONFIRM_ACTIVATE_SUBSCRIPTION,
|
||||
content:
|
||||
constants.ACTIVATE_SUBSCRIPTION_MESSAGE(
|
||||
subscription.expiryTime
|
||||
),
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text: constants.ACTIVATE_SUBSCRIPTION,
|
||||
action: activateSubscription.bind(
|
||||
null,
|
||||
props.setDialogMessage,
|
||||
props.closeModal,
|
||||
props.setLoading
|
||||
),
|
||||
variant: 'success',
|
||||
},
|
||||
close: {
|
||||
text: constants.CANCEL,
|
||||
},
|
||||
})
|
||||
}>
|
||||
{constants.ACTIVATE_SUBSCRIPTION}
|
||||
</LinkButton>
|
||||
) : (
|
||||
<LinkButton
|
||||
variant="danger"
|
||||
onClick={() => props.setDialogMessage({
|
||||
title:
|
||||
constants.CONFIRM_CANCEL_SUBSCRIPTION,
|
||||
content: constants.CANCEL_SUBSCRIPTION_MESSAGE(),
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text:
|
||||
constants.CANCEL_SUBSCRIPTION,
|
||||
action: cancelSubscription.bind(
|
||||
null,
|
||||
props.setDialogMessage,
|
||||
props.closeModal,
|
||||
props.setLoading,
|
||||
),
|
||||
variant: 'danger',
|
||||
},
|
||||
close: {
|
||||
text: constants.CANCEL,
|
||||
},
|
||||
})}
|
||||
>
|
||||
onClick={() =>
|
||||
props.setDialogMessage({
|
||||
title: constants.CONFIRM_CANCEL_SUBSCRIPTION,
|
||||
content:
|
||||
constants.CANCEL_SUBSCRIPTION_MESSAGE(),
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text: constants.CANCEL_SUBSCRIPTION,
|
||||
action: cancelSubscription.bind(
|
||||
null,
|
||||
props.setDialogMessage,
|
||||
props.closeModal,
|
||||
props.setLoading
|
||||
),
|
||||
variant: 'danger',
|
||||
},
|
||||
close: {
|
||||
text: constants.CANCEL,
|
||||
},
|
||||
})
|
||||
}>
|
||||
{constants.CANCEL_SUBSCRIPTION}
|
||||
</LinkButton>
|
||||
)}
|
||||
|
@ -311,10 +329,9 @@ function PlanSelector(props: Props) {
|
|||
onClick={updatePaymentMethod.bind(
|
||||
null,
|
||||
props.setDialogMessage,
|
||||
props.setLoading,
|
||||
props.setLoading
|
||||
)}
|
||||
style={{ marginTop: '20px' }}
|
||||
>
|
||||
style={{ marginTop: '20px' }}>
|
||||
{constants.MANAGEMENT_PORTAL}
|
||||
</LinkButton>
|
||||
</>
|
||||
|
@ -322,11 +339,13 @@ function PlanSelector(props: Props) {
|
|||
<LinkButton
|
||||
variant="primary"
|
||||
onClick={props.closeModal}
|
||||
style={{ color: 'rgb(121, 121, 121)', marginTop: '20px' }}
|
||||
>
|
||||
{isOnFreePlan(subscription) ?
|
||||
constants.SKIP :
|
||||
constants.CLOSE}
|
||||
style={{
|
||||
color: 'rgb(121, 121, 121)',
|
||||
marginTop: '20px',
|
||||
}}>
|
||||
{isOnFreePlan(subscription)
|
||||
? constants.SKIP
|
||||
: constants.CLOSE}
|
||||
</LinkButton>
|
||||
)}
|
||||
</DeadCenter>
|
||||
|
|
|
@ -25,7 +25,7 @@ const Check = styled.input`
|
|||
opacity: 0;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
@media(pointer: coarse) {
|
||||
@media (pointer: coarse) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
@ -183,8 +183,7 @@ export default function PreviewCard(props: IProps) {
|
|||
onClick={handleClick}
|
||||
disabled={!forcedEnable && !file?.msrc && !imgSrc}
|
||||
selected={selected}
|
||||
{...(selectable ? useLongPress(longPressCallback, 500) : {})}
|
||||
>
|
||||
{...(selectable ? useLongPress(longPressCallback, 500) : {})}>
|
||||
{selectable && (
|
||||
<Check
|
||||
type="checkbox"
|
||||
|
|
|
@ -7,7 +7,7 @@ import DeleteIcon from 'components/icons/DeleteIcon';
|
|||
import CrossIcon from 'components/icons/CrossIcon';
|
||||
import AddIcon from 'components/icons/AddIcon';
|
||||
import { IconButton } from 'components/Container';
|
||||
import constants from 'utils/strings/englishConstants';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
||||
interface Props {
|
||||
addToCollectionHelper: (collectionName, collection) => void;
|
||||
|
@ -42,32 +42,42 @@ const SelectedFileOptions = ({
|
|||
count,
|
||||
clearSelection,
|
||||
}: Props) => {
|
||||
const addToCollection = () => setCollectionSelectorAttributes({
|
||||
callback: (collection) => addToCollectionHelper(null, collection),
|
||||
showNextModal: showCreateCollectionModal,
|
||||
title: constants.ADD_TO_COLLECTION,
|
||||
});
|
||||
const addToCollection = () =>
|
||||
setCollectionSelectorAttributes({
|
||||
callback: (collection) => addToCollectionHelper(null, collection),
|
||||
showNextModal: showCreateCollectionModal,
|
||||
title: constants.ADD_TO_COLLECTION,
|
||||
});
|
||||
|
||||
const deleteHandler = () => setDialogMessage({
|
||||
title: constants.CONFIRM_DELETE_FILE,
|
||||
content: constants.DELETE_FILE_MESSAGE,
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
action: deleteFileHelper,
|
||||
text: constants.DELETE,
|
||||
variant: 'danger',
|
||||
},
|
||||
close: { text: constants.CANCEL },
|
||||
});
|
||||
const deleteHandler = () =>
|
||||
setDialogMessage({
|
||||
title: constants.CONFIRM_DELETE_FILE,
|
||||
content: constants.DELETE_FILE_MESSAGE,
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
action: deleteFileHelper,
|
||||
text: constants.DELETE,
|
||||
variant: 'danger',
|
||||
},
|
||||
close: { text: constants.CANCEL },
|
||||
});
|
||||
|
||||
return (
|
||||
<SelectionBar>
|
||||
<SelectionContainer>
|
||||
<IconButton onClick={clearSelection}><CrossIcon /></IconButton>
|
||||
<div>{count} {constants.SELECTED}</div>
|
||||
<IconButton onClick={clearSelection}>
|
||||
<CrossIcon />
|
||||
</IconButton>
|
||||
<div>
|
||||
{count} {constants.SELECTED}
|
||||
</div>
|
||||
</SelectionContainer>
|
||||
<IconButton onClick={addToCollection}><AddIcon /></IconButton>
|
||||
<IconButton onClick={deleteHandler}><DeleteIcon /></IconButton>
|
||||
<IconButton onClick={addToCollection}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={deleteHandler}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</SelectionBar>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
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 { getLocalFiles } from 'services/fileService';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
@ -13,6 +16,7 @@ import { SetFiles, SetLoading } from 'pages/gallery';
|
|||
import { AppContext } from 'pages/_app';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { FileRejection } from 'react-dropzone';
|
||||
import { METADATA_FOLDER_NAME } from 'services/exportService';
|
||||
|
||||
interface Props {
|
||||
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
|
||||
|
@ -25,8 +29,8 @@ interface Props {
|
|||
setDialogMessage: SetDialogMessage;
|
||||
setUploadInProgress: any;
|
||||
showCollectionSelector: () => void;
|
||||
fileRejections:FileRejection[];
|
||||
setFiles:SetFiles;
|
||||
fileRejections: FileRejection[];
|
||||
setFiles: SetFiles;
|
||||
}
|
||||
|
||||
export enum UPLOAD_STRATEGY {
|
||||
|
@ -38,20 +42,26 @@ interface AnalysisResult {
|
|||
suggestedCollectionName: string;
|
||||
multipleFolders: boolean;
|
||||
}
|
||||
|
||||
export default function Upload(props: Props) {
|
||||
const [progressView, setProgressView] = useState(false);
|
||||
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(
|
||||
UPLOAD_STAGES.START,
|
||||
UPLOAD_STAGES.START
|
||||
);
|
||||
const [fileCounter, setFileCounter] = useState({ current: 0, total: 0 });
|
||||
const [fileProgress, setFileProgress] = useState(new Map<string, number>());
|
||||
const [uploadResult, setUploadResult] = useState(new Map<string, number>());
|
||||
const [percentComplete, setPercentComplete] = useState(0);
|
||||
const [choiceModalView, setChoiceModalView] = useState(false);
|
||||
const [fileAnalysisResult, setFileAnalysisResult] = useState<AnalysisResult>(null);
|
||||
const [fileAnalysisResult, setFileAnalysisResult] =
|
||||
useState<AnalysisResult>(null);
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.acceptedFiles?.length > 0 || appContext.sharedFiles?.length > 0) {
|
||||
if (
|
||||
props.acceptedFiles?.length > 0 ||
|
||||
appContext.sharedFiles?.length > 0
|
||||
) {
|
||||
props.setLoading(true);
|
||||
|
||||
let fileAnalysisResult;
|
||||
|
@ -77,6 +87,7 @@ export default function Upload(props: Props) {
|
|||
setUploadStage(UPLOAD_STAGES.START);
|
||||
setFileCounter({ current: 0, total: 0 });
|
||||
setFileProgress(new Map<string, number>());
|
||||
setUploadResult(new Map<string, number>());
|
||||
setPercentComplete(0);
|
||||
setProgressView(true);
|
||||
};
|
||||
|
@ -89,16 +100,16 @@ export default function Upload(props: Props) {
|
|||
props.closeCollectionSelector();
|
||||
await uploadFilesToNewCollections(
|
||||
UPLOAD_STRATEGY.SINGLE_COLLECTION,
|
||||
collectionName,
|
||||
collectionName
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const nextModal = (fileAnalysisResult: AnalysisResult) => {
|
||||
fileAnalysisResult?.multipleFolders ?
|
||||
setChoiceModalView(true) :
|
||||
showCreateCollectionModal(fileAnalysisResult);
|
||||
fileAnalysisResult?.multipleFolders
|
||||
? setChoiceModalView(true)
|
||||
: showCreateCollectionModal(fileAnalysisResult);
|
||||
};
|
||||
|
||||
function analyseUploadFiles(): AnalysisResult {
|
||||
|
@ -118,7 +129,7 @@ export default function Upload(props: Props) {
|
|||
if (commonPathPrefix) {
|
||||
commonPathPrefix = commonPathPrefix.substr(
|
||||
1,
|
||||
commonPathPrefix.lastIndexOf('/') - 1,
|
||||
commonPathPrefix.lastIndexOf('/') - 1
|
||||
);
|
||||
}
|
||||
return {
|
||||
|
@ -129,10 +140,14 @@ export default function Upload(props: Props) {
|
|||
function getCollectionWiseFiles() {
|
||||
const collectionWiseFiles = new Map<string, globalThis.File[]>();
|
||||
for (const file of props.acceptedFiles) {
|
||||
const filePath = file['path'];
|
||||
const folderPath = filePath.substr(0, filePath.lastIndexOf('/'));
|
||||
const filePath = file['path'] as string;
|
||||
|
||||
let folderPath = filePath.substr(0, filePath.lastIndexOf('/'));
|
||||
if (folderPath.endsWith(METADATA_FOLDER_NAME)) {
|
||||
folderPath = folderPath.substr(0, folderPath.lastIndexOf('/'));
|
||||
}
|
||||
const folderName = folderPath.substr(
|
||||
folderPath.lastIndexOf('/') + 1,
|
||||
folderPath.lastIndexOf('/') + 1
|
||||
);
|
||||
if (!collectionWiseFiles.has(folderName)) {
|
||||
collectionWiseFiles.set(folderName, []);
|
||||
|
@ -145,10 +160,11 @@ export default function Upload(props: Props) {
|
|||
const uploadFilesToExistingCollection = async (collection) => {
|
||||
try {
|
||||
uploadInit();
|
||||
const filesWithCollectionToUpload: FileWithCollection[] = props.acceptedFiles.map((file) => ({
|
||||
file,
|
||||
collection,
|
||||
}));
|
||||
const filesWithCollectionToUpload: FileWithCollection[] =
|
||||
props.acceptedFiles.map((file) => ({
|
||||
file,
|
||||
collection,
|
||||
}));
|
||||
await uploadFiles(filesWithCollectionToUpload);
|
||||
} catch (e) {
|
||||
logError(e, 'Failed to upload files to existing collections');
|
||||
|
@ -157,7 +173,7 @@ export default function Upload(props: Props) {
|
|||
|
||||
const uploadFilesToNewCollections = async (
|
||||
strategy: UPLOAD_STRATEGY,
|
||||
collectionName,
|
||||
collectionName
|
||||
) => {
|
||||
try {
|
||||
uploadInit();
|
||||
|
@ -194,13 +210,13 @@ export default function Upload(props: Props) {
|
|||
};
|
||||
|
||||
const uploadFiles = async (
|
||||
filesWithCollectionToUpload: FileWithCollection[],
|
||||
filesWithCollectionToUpload: FileWithCollection[]
|
||||
) => {
|
||||
try {
|
||||
props.setUploadInProgress(true);
|
||||
props.closeCollectionSelector();
|
||||
await props.syncWithRemote(true, true);
|
||||
const localFiles= await getLocalFiles();
|
||||
const localFiles = await getLocalFiles();
|
||||
await UploadService.uploadFiles(
|
||||
filesWithCollectionToUpload,
|
||||
localFiles,
|
||||
|
@ -209,8 +225,9 @@ export default function Upload(props: Props) {
|
|||
setFileCounter,
|
||||
setUploadStage,
|
||||
setFileProgress,
|
||||
setUploadResult,
|
||||
},
|
||||
props.setFiles,
|
||||
props.setFiles
|
||||
);
|
||||
} catch (err) {
|
||||
props.setBannerMessage(err.message);
|
||||
|
@ -222,13 +239,12 @@ export default function Upload(props: Props) {
|
|||
props.syncWithRemote();
|
||||
}
|
||||
};
|
||||
const retryFailed = async (
|
||||
) => {
|
||||
const retryFailed = async () => {
|
||||
try {
|
||||
props.setUploadInProgress(true);
|
||||
uploadInit();
|
||||
await props.syncWithRemote(true, true);
|
||||
const localFiles= await getLocalFiles();
|
||||
const localFiles = await getLocalFiles();
|
||||
await UploadService.retryFailedFiles(localFiles);
|
||||
} catch (err) {
|
||||
props.setBannerMessage(err.message);
|
||||
|
@ -240,14 +256,15 @@ export default function Upload(props: Props) {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChoiceModal
|
||||
show={choiceModalView}
|
||||
onHide={() => setChoiceModalView(false)}
|
||||
uploadFiles={uploadFilesToNewCollections}
|
||||
showCollectionCreateModal={() => showCreateCollectionModal(fileAnalysisResult)}
|
||||
showCollectionCreateModal={() =>
|
||||
showCreateCollectionModal(fileAnalysisResult)
|
||||
}
|
||||
/>
|
||||
<UploadProgress
|
||||
now={percentComplete}
|
||||
|
@ -258,6 +275,7 @@ export default function Upload(props: Props) {
|
|||
closeModal={() => setProgressView(false)}
|
||||
retryFailed={retryFailed}
|
||||
fileRejections={props.fileRejections}
|
||||
uploadResult={uploadResult}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -21,8 +21,7 @@ function UploadButton({ openFileUploader, isFirstFetch }) {
|
|||
viewBox="0 0 24 24"
|
||||
fill="green"
|
||||
width="32px"
|
||||
height="32px"
|
||||
>
|
||||
height="32px">
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="#2dc262"
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Alert, Button, Modal, ProgressBar,
|
||||
} from 'react-bootstrap';
|
||||
import ExpandLess from 'components/icons/ExpandLess';
|
||||
import ExpandMore from 'components/icons/ExpandMore';
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Modal, ProgressBar } from 'react-bootstrap';
|
||||
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 AlertBanner from './AlertBanner';
|
||||
|
||||
interface Props {
|
||||
fileCounter;
|
||||
|
@ -14,106 +17,222 @@ interface Props {
|
|||
retryFailed;
|
||||
fileProgress: Map<string, number>;
|
||||
show;
|
||||
fileRejections:FileRejection[]
|
||||
fileRejections: FileRejection[];
|
||||
uploadResult: Map<string, number>;
|
||||
}
|
||||
interface FileProgressStatuses{
|
||||
fileName:string;
|
||||
progress:number;
|
||||
interface FileProgresses {
|
||||
fileName: string;
|
||||
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) {
|
||||
const fileProgressStatuses = [] as FileProgressStatuses[];
|
||||
const fileProgressStatuses = [] as FileProgresses[];
|
||||
const fileUploadResultMap = new Map<FileUploadResults, string[]>();
|
||||
let filesNotUploaded = false;
|
||||
|
||||
if (props.fileProgress) {
|
||||
for (const [fileName, progress] of props.fileProgress) {
|
||||
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 (
|
||||
<Modal
|
||||
show={props.show}
|
||||
onHide={
|
||||
props.uploadStage !== UPLOAD_STAGES.FINISH ?
|
||||
() => null :
|
||||
props.closeModal
|
||||
props.uploadStage !== UPLOAD_STAGES.FINISH
|
||||
? () => null
|
||||
: props.closeModal
|
||||
}
|
||||
aria-labelledby="contained-modal-title-vcenter"
|
||||
centered
|
||||
backdrop={
|
||||
fileProgressStatuses?.length !== 0 ? 'static' : 'true'
|
||||
}
|
||||
>
|
||||
backdrop={fileProgressStatuses?.length !== 0 ? 'static' : 'true'}>
|
||||
<Modal.Header
|
||||
style={{ display: 'flex', justifyContent: 'center', textAlign: 'center', borderBottom: 'none', paddingTop: '30px', paddingBottom: '0px' }}
|
||||
closeButton={props.uploadStage === UPLOAD_STAGES.FINISH}
|
||||
>
|
||||
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
borderBottom: 'none',
|
||||
paddingTop: '30px',
|
||||
paddingBottom: '0px',
|
||||
}}
|
||||
closeButton={props.uploadStage === UPLOAD_STAGES.FINISH}>
|
||||
<h4 style={{ width: '100%' }}>
|
||||
{props.uploadStage === UPLOAD_STAGES.UPLOADING ?
|
||||
constants.UPLOAD[props.uploadStage](
|
||||
props.fileCounter,
|
||||
) :
|
||||
constants.UPLOAD[props.uploadStage]}
|
||||
{props.uploadStage === UPLOAD_STAGES.UPLOADING
|
||||
? constants.UPLOAD[props.uploadStage](props.fileCounter)
|
||||
: constants.UPLOAD[props.uploadStage]}
|
||||
</h4>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{props.uploadStage===UPLOAD_STAGES.FINISH ? (
|
||||
fileProgressStatuses.length !== 0 && (
|
||||
<Alert variant="warning">
|
||||
{constants.FAILED_UPLOAD_FILE_LIST}
|
||||
</Alert>
|
||||
)
|
||||
) :
|
||||
(props.uploadStage === UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES ||
|
||||
props.uploadStage === UPLOAD_STAGES.UPLOADING) &&
|
||||
(
|
||||
< ProgressBar
|
||||
now={props.now}
|
||||
animated
|
||||
variant="upload-progress-bar"
|
||||
/>
|
||||
)}
|
||||
{fileProgressStatuses?.length > 0 && (
|
||||
<ul
|
||||
style={{
|
||||
marginTop: '10px',
|
||||
overflow: 'auto',
|
||||
maxHeight: '250px',
|
||||
}}
|
||||
>
|
||||
{(props.uploadStage ===
|
||||
UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES ||
|
||||
props.uploadStage === UPLOAD_STAGES.UPLOADING) && (
|
||||
<ProgressBar
|
||||
now={props.now}
|
||||
animated
|
||||
variant="upload-progress-bar"
|
||||
/>
|
||||
)}
|
||||
{fileProgressStatuses.length > 0 && (
|
||||
<FileList>
|
||||
{fileProgressStatuses.map(({ fileName, progress }) => (
|
||||
<li key={fileName} style={{ marginTop: '12px' }}>
|
||||
{props.uploadStage===UPLOAD_STAGES.FINISH ?
|
||||
fileName :
|
||||
constants.FILE_UPLOAD_PROGRESS(
|
||||
fileName,
|
||||
progress,
|
||||
)}
|
||||
{props.uploadStage === UPLOAD_STAGES.FINISH
|
||||
? fileName
|
||||
: constants.FILE_UPLOAD_PROGRESS(
|
||||
fileName,
|
||||
progress
|
||||
)}
|
||||
</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 && (
|
||||
<Modal.Footer style={{ border: 'none' }}>
|
||||
{props.uploadStage===UPLOAD_STAGES.FINISH && (fileProgressStatuses?.length === 0 ? (
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
style={{ width: '100%' }}
|
||||
onClick={props.closeModal}
|
||||
>
|
||||
{constants.CLOSE}
|
||||
</Button>) : (
|
||||
<Button
|
||||
variant="outline-success"
|
||||
style={{ width: '100%' }}
|
||||
onClick={props.retryFailed}
|
||||
>
|
||||
{constants.RETRY}
|
||||
</Button>))}
|
||||
{props.uploadStage === UPLOAD_STAGES.FINISH &&
|
||||
(fileUploadResultMap?.get(FileUploadResults.FAILED)
|
||||
?.length > 0 ||
|
||||
fileUploadResultMap?.get(FileUploadResults.BLOCKED)
|
||||
?.length > 0 ? (
|
||||
<Button
|
||||
variant="outline-success"
|
||||
style={{ width: '100%' }}
|
||||
onClick={props.retryFailed}>
|
||||
{constants.RETRY_FAILED}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
style={{ width: '100%' }}
|
||||
onClick={props.closeModal}>
|
||||
{constants.CLOSE}
|
||||
</Button>
|
||||
))}
|
||||
</Modal.Footer>
|
||||
)}
|
||||
</Modal.Body>
|
||||
|
|
|
@ -128,14 +128,21 @@ const GlobalStyles = createGlobalStyle`
|
|||
.modal-content {
|
||||
border-radius:15px;
|
||||
background-color:#202020 !important;
|
||||
color:#aaa;
|
||||
}
|
||||
.modal-dialog{
|
||||
margin:5% auto;
|
||||
width:90%;
|
||||
}
|
||||
.modal-body{
|
||||
max-height:80vh;
|
||||
overflow:auto;
|
||||
}
|
||||
.modal-xl{
|
||||
max-width:960px!important;
|
||||
max-width:90% !important;
|
||||
}
|
||||
.plan-selector-modal-content {
|
||||
width:auto;
|
||||
margin:auto;
|
||||
}
|
||||
.pswp-custom {
|
||||
opacity: 0.75;
|
||||
|
@ -390,29 +397,35 @@ export interface BannerMessage {
|
|||
variant: string;
|
||||
}
|
||||
|
||||
|
||||
type AppContextType = {
|
||||
showNavBar: (show: boolean) => void;
|
||||
sharedFiles: File[];
|
||||
resetSharedFiles: () => void;
|
||||
setDisappearingFlashMessage: (message: FlashMessage) => void;
|
||||
}
|
||||
};
|
||||
|
||||
export enum FLASH_MESSAGE_TYPE {
|
||||
DANGER = 'danger',
|
||||
INFO = 'info',
|
||||
SUCCESS = 'success',
|
||||
WARNING = 'warning',
|
||||
}
|
||||
export interface FlashMessage {
|
||||
message: string;
|
||||
severity: string
|
||||
type: FLASH_MESSAGE_TYPE;
|
||||
}
|
||||
export const AppContext = createContext<AppContextType>(null);
|
||||
|
||||
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 }) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [offline, setOffline] = useState(
|
||||
typeof window !== 'undefined' && !window.navigator.onLine,
|
||||
typeof window !== 'undefined' && !window.navigator.onLine
|
||||
);
|
||||
const [showNavbar, setShowNavBar] = useState(false);
|
||||
const [sharedFiles, setSharedFiles] = useState<File[]>(null);
|
||||
|
@ -444,7 +457,7 @@ export default function App({ Component, err }) {
|
|||
(error) => {
|
||||
logError(error);
|
||||
return Promise.reject(error);
|
||||
},
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
|
@ -455,7 +468,7 @@ export default function App({ Component, err }) {
|
|||
useEffect(() => {
|
||||
console.log(
|
||||
`%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;');
|
||||
|
||||
|
@ -479,7 +492,9 @@ export default function App({ Component, err }) {
|
|||
if (redirectName) {
|
||||
const user = getData(LS_KEYS.USER);
|
||||
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>
|
||||
<title>{constants.TITLE}</title>
|
||||
{/* Cloudflare Web Analytics */}
|
||||
{pageRootURL?.hostname && (pageRootURL.hostname === 'photos.ente.io' ?
|
||||
<script
|
||||
defer
|
||||
src="https://static.cloudflareinsights.com/beacon.min.js"
|
||||
data-cf-beacon='{"token": "6a388287b59c439cb2070f78cc89dde1"}'
|
||||
/> : pageRootURL.hostname === 'web.ente.io' ?
|
||||
< script
|
||||
{pageRootURL?.hostname &&
|
||||
(pageRootURL.hostname === 'photos.ente.io' ? (
|
||||
<script
|
||||
defer
|
||||
src='https://static.cloudflareinsights.com/beacon.min.js'
|
||||
data-cf-beacon='{"token": "dfde128b7bb34a618ad34a08f1ba7609"}' /> :
|
||||
src="https://static.cloudflareinsights.com/beacon.min.js"
|
||||
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')
|
||||
)
|
||||
}
|
||||
))}
|
||||
{/* End Cloudflare Web Analytics */}
|
||||
</Head>
|
||||
<GlobalStyles />
|
||||
{
|
||||
showNavbar && <Navbar>
|
||||
{showNavbar && (
|
||||
<Navbar>
|
||||
<FlexContainer>
|
||||
<LogoImage
|
||||
style={{ height: '24px', padding: '3px' }}
|
||||
|
@ -532,21 +550,33 @@ export default function App({ Component, err }) {
|
|||
/>
|
||||
</FlexContainer>
|
||||
</Navbar>
|
||||
}
|
||||
<MessageContainer>{offline && constants.OFFLINE_MSG}</MessageContainer>
|
||||
{
|
||||
sharedFiles &&
|
||||
(router.pathname === '/gallery' ?
|
||||
<MessageContainer>{constants.FILES_TO_BE_UPLOADED(sharedFiles.length)}</MessageContainer> :
|
||||
<MessageContainer>{constants.LOGIN_TO_UPLOAD_FILES(sharedFiles.length)}</MessageContainer>)
|
||||
}
|
||||
{flashMessage && <FlashMessageBar flashMessage={flashMessage} onClose={() => setFlashMessage(null)} />}
|
||||
<AppContext.Provider value={{
|
||||
showNavBar,
|
||||
sharedFiles,
|
||||
resetSharedFiles,
|
||||
setDisappearingFlashMessage,
|
||||
}}>
|
||||
)}
|
||||
<MessageContainer>
|
||||
{offline && constants.OFFLINE_MSG}
|
||||
</MessageContainer>
|
||||
{sharedFiles &&
|
||||
(router.pathname === '/gallery' ? (
|
||||
<MessageContainer>
|
||||
{constants.FILES_TO_BE_UPLOADED(sharedFiles.length)}
|
||||
</MessageContainer>
|
||||
) : (
|
||||
<MessageContainer>
|
||||
{constants.LOGIN_TO_UPLOAD_FILES(sharedFiles.length)}
|
||||
</MessageContainer>
|
||||
))}
|
||||
{flashMessage && (
|
||||
<FlashMessageBar
|
||||
flashMessage={flashMessage}
|
||||
onClose={() => setFlashMessage(null)}
|
||||
/>
|
||||
)}
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
showNavBar,
|
||||
sharedFiles,
|
||||
resetSharedFiles,
|
||||
setDisappearingFlashMessage,
|
||||
}}>
|
||||
{loading ? (
|
||||
<Container>
|
||||
<EnteSpinner>
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import React from 'react';
|
||||
import Document, {
|
||||
Html, Head, Main, NextScript,
|
||||
} from 'next/document';
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||
import { ServerStyleSheet } from 'styled-components';
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
|
@ -10,9 +8,11 @@ export default class MyDocument extends Document {
|
|||
const originalRenderPage = ctx.renderPage;
|
||||
|
||||
try {
|
||||
ctx.renderPage = () => originalRenderPage({
|
||||
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />),
|
||||
});
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: (App) => (props) =>
|
||||
sheet.collectStyles(<App {...props} />),
|
||||
});
|
||||
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return {
|
||||
|
@ -37,13 +37,24 @@ export default class MyDocument extends Document {
|
|||
name="description"
|
||||
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="apple-touch-icon" href="/images/ente-512.png" />
|
||||
<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-status-bar-style" content="black" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black"
|
||||
/>
|
||||
</Head>
|
||||
<body>
|
||||
<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({
|
||||
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 { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||
import { useRouter } from 'next/router';
|
||||
import { B64EncryptionResult } from 'services/uploadService';
|
||||
import CryptoWorker, {
|
||||
setSessionKeys,
|
||||
generateAndSaveIntermediateKeyAttributes,
|
||||
B64EncryptionResult,
|
||||
} from 'utils/crypto';
|
||||
import { getActualKey } from 'utils/common/key';
|
||||
import { setKeys, UpdatedKey } from 'services/userService';
|
||||
|
@ -45,7 +45,8 @@ export default function Generate() {
|
|||
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
|
||||
return;
|
||||
}
|
||||
const encryptedKeyAttributes: B64EncryptionResult = await cryptoWorker.encryptToB64(key, kek.key);
|
||||
const encryptedKeyAttributes: B64EncryptionResult =
|
||||
await cryptoWorker.encryptToB64(key, kek.key);
|
||||
const updatedKey: UpdatedKey = {
|
||||
kekSalt,
|
||||
encryptedKey: encryptedKeyAttributes.encryptedData,
|
||||
|
@ -60,7 +61,7 @@ export default function Generate() {
|
|||
await generateAndSaveIntermediateKeyAttributes(
|
||||
passphrase,
|
||||
updatedKeyAttributes,
|
||||
key,
|
||||
key
|
||||
);
|
||||
|
||||
setSessionKeys(key);
|
||||
|
@ -75,9 +76,9 @@ export default function Generate() {
|
|||
callback={onSubmit}
|
||||
buttonText={constants.CHANGE_PASSWORD}
|
||||
back={
|
||||
getData(LS_KEYS.SHOW_BACK_BUTTON)?.value ?
|
||||
redirectToGallery :
|
||||
null
|
||||
getData(LS_KEYS.SHOW_BACK_BUTTON)?.value
|
||||
? redirectToGallery
|
||||
: null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -29,7 +29,10 @@ export default function Credentials() {
|
|||
const user = getData(LS_KEYS.USER);
|
||||
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
if ((!user?.token && !user?.encryptedToken) || !keyAttributes?.memLimit) {
|
||||
if (
|
||||
(!user?.token && !user?.encryptedToken) ||
|
||||
!keyAttributes?.memLimit
|
||||
) {
|
||||
clearData();
|
||||
router.push('/');
|
||||
} else if (!keyAttributes) {
|
||||
|
@ -51,7 +54,7 @@ export default function Credentials() {
|
|||
passphrase,
|
||||
keyAttributes.kekSalt,
|
||||
keyAttributes.opsLimit,
|
||||
keyAttributes.memLimit,
|
||||
keyAttributes.memLimit
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('failed to deriveKey ', e.message);
|
||||
|
@ -61,13 +64,13 @@ export default function Credentials() {
|
|||
const key: string = await cryptoWorker.decryptB64(
|
||||
keyAttributes.encryptedKey,
|
||||
keyAttributes.keyDecryptionNonce,
|
||||
kek,
|
||||
kek
|
||||
);
|
||||
if (isFirstLogin()) {
|
||||
await generateAndSaveIntermediateKeyAttributes(
|
||||
passphrase,
|
||||
keyAttributes,
|
||||
key,
|
||||
key
|
||||
);
|
||||
}
|
||||
await setSessionKeys(key);
|
||||
|
@ -81,7 +84,7 @@ export default function Credentials() {
|
|||
} catch (e) {
|
||||
setFieldError(
|
||||
'passphrase',
|
||||
`${constants.UNKNOWN_ERROR} ${e.message}`,
|
||||
`${constants.UNKNOWN_ERROR} ${e.message}`
|
||||
);
|
||||
console.error('failed to verifyPassphrase ', e.message);
|
||||
}
|
||||
|
@ -90,13 +93,10 @@ export default function Credentials() {
|
|||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Card
|
||||
style={{ minWidth: '320px' }}
|
||||
className="text-center"
|
||||
>
|
||||
<Card style={{ minWidth: '320px' }} className="text-center">
|
||||
<Card.Body style={{ padding: '40px 30px' }}>
|
||||
<Card.Title style={{ marginBottom: '32px' }}>
|
||||
<LogoImg src='/icon.svg' />
|
||||
<LogoImg src="/icon.svg" />
|
||||
{constants.PASSWORD}
|
||||
</Card.Title>
|
||||
<SingleInputForm
|
||||
|
@ -110,12 +110,10 @@ export default function Credentials() {
|
|||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: '12px',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => router.push('/recover')}
|
||||
>
|
||||
onClick={() => router.push('/recover')}>
|
||||
{constants.FORGOT_PASSWORD}
|
||||
</Button>
|
||||
<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 { clearKeys, getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||
import {
|
||||
|
@ -39,7 +45,6 @@ import { LoadingOverlay } from 'components/LoadingOverlay';
|
|||
import PhotoFrame from 'components/PhotoFrame';
|
||||
import { getSelectedFileIds, sortFilesIntoCollections } from 'utils/file';
|
||||
import { addFilesToCollection } from 'utils/collection';
|
||||
import { errorCodes } from 'utils/common/errorUtil';
|
||||
import SearchBar, { DateValue } from 'components/SearchBar';
|
||||
import { Bbox } from 'services/searchService';
|
||||
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 Collections from 'components/pages/gallery/Collections';
|
||||
import { AppContext } from 'pages/_app';
|
||||
|
||||
export enum FILE_TYPE {
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
OTHERS,
|
||||
}
|
||||
import { CustomError, ServerErrorCodes } from 'utils/common/errorUtil';
|
||||
|
||||
export const DeadCenter = styled.div`
|
||||
flex: 1;
|
||||
|
@ -98,19 +98,22 @@ export interface SearchStats {
|
|||
type GalleryContextType = {
|
||||
thumbs: Map<number, string>;
|
||||
files: Map<number, string>;
|
||||
}
|
||||
};
|
||||
|
||||
const defaultGalleryContext: GalleryContextType = {
|
||||
thumbs: new Map(),
|
||||
files: new Map(),
|
||||
};
|
||||
|
||||
export const GalleryContext = createContext<GalleryContextType>(defaultGalleryContext);
|
||||
export const GalleryContext = createContext<GalleryContextType>(
|
||||
defaultGalleryContext
|
||||
);
|
||||
|
||||
export default function Gallery() {
|
||||
const router = useRouter();
|
||||
const [collections, setCollections] = useState<Collection[]>([]);
|
||||
const [collectionsAndTheirLatestFile, setCollectionsAndTheirLatestFile] = useState<CollectionAndItsLatestFile[]>([]);
|
||||
const [collectionsAndTheirLatestFile, setCollectionsAndTheirLatestFile] =
|
||||
useState<CollectionAndItsLatestFile[]>([]);
|
||||
const [files, setFiles] = useState<File[]>(null);
|
||||
const [favItemIds, setFavItemIds] = useState<Set<number>>();
|
||||
const [bannerMessage, setBannerMessage] = useState<string>(null);
|
||||
|
@ -121,9 +124,11 @@ export default function Gallery() {
|
|||
const [dialogView, setDialogView] = useState(false);
|
||||
const [planModalView, setPlanModalView] = 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 [collectionNamerAttributes, setCollectionNamerAttributes] = useState<CollectionNamerAttributes>(null);
|
||||
const [collectionNamerAttributes, setCollectionNamerAttributes] =
|
||||
useState<CollectionNamerAttributes>(null);
|
||||
const [collectionNamerView, setCollectionNamerView] = useState(false);
|
||||
const [search, setSearch] = useState<Search>({
|
||||
date: null,
|
||||
|
@ -150,7 +155,8 @@ export default function Gallery() {
|
|||
const resync = useRef(false);
|
||||
const [deleted, setDeleted] = useState<number[]>([]);
|
||||
const appContext = useContext(AppContext);
|
||||
const [collectionFilesCount, setCollectionFilesCount] = useState<Map<number, number>>();
|
||||
const [collectionFilesCount, setCollectionFilesCount] =
|
||||
useState<Map<number, number>>();
|
||||
|
||||
useEffect(() => {
|
||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
|
@ -181,50 +187,46 @@ export default function Gallery() {
|
|||
}, []);
|
||||
|
||||
useEffect(() => setDialogView(true), [dialogMessage]);
|
||||
useEffect(
|
||||
() => {
|
||||
if (collectionSelectorAttributes) {
|
||||
setCollectionSelectorView(true);
|
||||
}
|
||||
},
|
||||
[collectionSelectorAttributes],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (collectionSelectorAttributes) {
|
||||
setCollectionSelectorView(true);
|
||||
}
|
||||
}, [collectionSelectorAttributes]);
|
||||
useEffect(() => setCollectionNamerView(true), [collectionNamerAttributes]);
|
||||
|
||||
const syncWithRemote = async (force = false, silent=false) => {
|
||||
const syncWithRemote = async (force = false, silent = false) => {
|
||||
if (syncInProgress.current && !force) {
|
||||
resync.current= true;
|
||||
resync.current = true;
|
||||
return;
|
||||
}
|
||||
syncInProgress.current=true;
|
||||
syncInProgress.current = true;
|
||||
try {
|
||||
checkConnectivity();
|
||||
if (!(await isTokenValid())) {
|
||||
throw new Error(errorCodes.ERR_SESSION_EXPIRED);
|
||||
throw new Error(ServerErrorCodes.SESSION_EXPIRED);
|
||||
}
|
||||
!silent && loadingBar.current?.continuousStart();
|
||||
await billingService.updatePlans();
|
||||
await billingService.syncSubscription();
|
||||
const collections = await syncCollections();
|
||||
const { files } = await syncFiles(collections, setFiles);
|
||||
await initDerivativeState(collections, files);
|
||||
} catch (e) {
|
||||
switch (e.message) {
|
||||
case errorCodes.ERR_SESSION_EXPIRED:
|
||||
case ServerErrorCodes.SESSION_EXPIRED:
|
||||
setBannerMessage(constants.SESSION_EXPIRED_MESSAGE);
|
||||
setDialogMessage({
|
||||
title: constants.SESSION_EXPIRED,
|
||||
content: constants.SESSION_EXPIRED_MESSAGE,
|
||||
staticBackdrop: true,
|
||||
nonClosable: true,
|
||||
proceed: {
|
||||
text: constants.LOGIN,
|
||||
action: logoutUser,
|
||||
variant: 'success',
|
||||
},
|
||||
nonClosable: true,
|
||||
});
|
||||
break;
|
||||
case errorCodes.ERR_KEY_MISSING:
|
||||
case CustomError.KEY_MISSING:
|
||||
clearKeys();
|
||||
router.push('/credentials');
|
||||
break;
|
||||
|
@ -232,22 +234,17 @@ export default function Gallery() {
|
|||
} finally {
|
||||
!silent && loadingBar.current?.complete();
|
||||
}
|
||||
syncInProgress.current=false;
|
||||
syncInProgress.current = false;
|
||||
if (resync.current) {
|
||||
resync.current=false;
|
||||
resync.current = false;
|
||||
syncWithRemote();
|
||||
}
|
||||
};
|
||||
|
||||
const initDerivativeState = async (collections, files) => {
|
||||
const nonEmptyCollections = getNonEmptyCollections(
|
||||
collections,
|
||||
files,
|
||||
);
|
||||
const collectionsAndTheirLatestFile = await getCollectionsAndTheirLatestFile(
|
||||
nonEmptyCollections,
|
||||
files,
|
||||
);
|
||||
const nonEmptyCollections = getNonEmptyCollections(collections, files);
|
||||
const collectionsAndTheirLatestFile =
|
||||
await getCollectionsAndTheirLatestFile(nonEmptyCollections, files);
|
||||
const collectionWiseFiles = sortFilesIntoCollections(files);
|
||||
const collectionFilesCount = new Map<number, number>();
|
||||
for (const [id, files] of collectionWiseFiles) {
|
||||
|
@ -274,7 +271,7 @@ export default function Gallery() {
|
|||
}
|
||||
const addToCollectionHelper = (
|
||||
collectionName: string,
|
||||
collection: Collection,
|
||||
collection: Collection
|
||||
) => {
|
||||
loadingBar.current?.continuousStart();
|
||||
addFilesToCollection(
|
||||
|
@ -285,31 +282,29 @@ export default function Gallery() {
|
|||
syncWithRemote,
|
||||
selectCollection,
|
||||
collectionName,
|
||||
collection,
|
||||
collection
|
||||
);
|
||||
};
|
||||
|
||||
const showCreateCollectionModal = () => setCollectionNamerAttributes({
|
||||
title: constants.CREATE_COLLECTION,
|
||||
buttonText: constants.CREATE,
|
||||
autoFilledName: '',
|
||||
callback: (collectionName) => addToCollectionHelper(collectionName, null),
|
||||
});
|
||||
const showCreateCollectionModal = () =>
|
||||
setCollectionNamerAttributes({
|
||||
title: constants.CREATE_COLLECTION,
|
||||
buttonText: constants.CREATE,
|
||||
autoFilledName: '',
|
||||
callback: (collectionName) =>
|
||||
addToCollectionHelper(collectionName, null),
|
||||
});
|
||||
|
||||
const deleteFileHelper = async () => {
|
||||
loadingBar.current?.continuousStart();
|
||||
try {
|
||||
const fileIds = getSelectedFileIds(selected);
|
||||
await deleteFiles(
|
||||
fileIds,
|
||||
clearSelection,
|
||||
syncWithRemote,
|
||||
);
|
||||
await deleteFiles(fileIds, clearSelection, syncWithRemote);
|
||||
setDeleted([...deleted, ...fileIds]);
|
||||
} catch (e) {
|
||||
loadingBar.current.complete();
|
||||
switch (e.status?.toString()) {
|
||||
case errorCodes.ERR_FORBIDDEN:
|
||||
case ServerErrorCodes.FORBIDDEN:
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
staticBackdrop: true,
|
||||
|
@ -333,7 +328,6 @@ export default function Gallery() {
|
|||
setSearchStats(null);
|
||||
};
|
||||
|
||||
|
||||
const closeCollectionSelector = (closeBtnClick?: boolean) => {
|
||||
if (closeBtnClick === true) {
|
||||
appContext.resetSharedFiles();
|
||||
|
@ -346,8 +340,10 @@ export default function Gallery() {
|
|||
<FullScreenDropZone
|
||||
getRootProps={getRootProps}
|
||||
getInputProps={getInputProps}
|
||||
showCollectionSelector={setCollectionSelectorView.bind(null, true)}
|
||||
>
|
||||
showCollectionSelector={setCollectionSelectorView.bind(
|
||||
null,
|
||||
true
|
||||
)}>
|
||||
{loading && (
|
||||
<LoadingOverlay>
|
||||
<EnteSpinner />
|
||||
|
@ -399,24 +395,33 @@ export default function Gallery() {
|
|||
attributes={collectionNamerAttributes}
|
||||
/>
|
||||
<CollectionSelector
|
||||
show={collectionSelectorView && !(collectionsAndTheirLatestFile?.length === 0)}
|
||||
show={
|
||||
collectionSelectorView &&
|
||||
!(collectionsAndTheirLatestFile?.length === 0)
|
||||
}
|
||||
onHide={closeCollectionSelector}
|
||||
collectionsAndTheirLatestFile={collectionsAndTheirLatestFile}
|
||||
collectionsAndTheirLatestFile={
|
||||
collectionsAndTheirLatestFile
|
||||
}
|
||||
directlyShowNextModal={
|
||||
collectionsAndTheirLatestFile?.length === 0
|
||||
}
|
||||
attributes={collectionSelectorAttributes}
|
||||
syncWithRemote={syncWithRemote}
|
||||
/>
|
||||
<Upload
|
||||
syncWithRemote={syncWithRemote}
|
||||
setBannerMessage={setBannerMessage}
|
||||
acceptedFiles={acceptedFiles}
|
||||
showCollectionSelector={setCollectionSelectorView.bind(null, true)}
|
||||
setCollectionSelectorAttributes={setCollectionSelectorAttributes}
|
||||
showCollectionSelector={setCollectionSelectorView.bind(
|
||||
null,
|
||||
true
|
||||
)}
|
||||
setCollectionSelectorAttributes={
|
||||
setCollectionSelectorAttributes
|
||||
}
|
||||
closeCollectionSelector={setCollectionSelectorView.bind(
|
||||
null,
|
||||
false,
|
||||
false
|
||||
)}
|
||||
setLoading={setLoading}
|
||||
setCollectionNamerAttributes={setCollectionNamerAttributes}
|
||||
|
@ -431,7 +436,10 @@ export default function Gallery() {
|
|||
setLoading={setLoading}
|
||||
showPlanSelectorModal={() => setPlanModalView(true)}
|
||||
/>
|
||||
<UploadButton isFirstFetch={isFirstFetch} openFileUploader={openFileUploader} />
|
||||
<UploadButton
|
||||
isFirstFetch={isFirstFetch}
|
||||
openFileUploader={openFileUploader}
|
||||
/>
|
||||
<PhotoFrame
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
|
|
|
@ -35,7 +35,7 @@ export default function Generate() {
|
|||
setLoading(true);
|
||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
const keyAttributes: KeyAttributes = getData(
|
||||
LS_KEYS.ORIGINAL_KEY_ATTRIBUTES,
|
||||
LS_KEYS.ORIGINAL_KEY_ATTRIBUTES
|
||||
);
|
||||
router.prefetch('/gallery');
|
||||
const user = getData(LS_KEYS.USER);
|
||||
|
@ -64,14 +64,14 @@ export default function Generate() {
|
|||
const onSubmit = async (passphrase, setFieldError) => {
|
||||
try {
|
||||
const { keyAttributes, masterKey } = await generateKeyAttributes(
|
||||
passphrase,
|
||||
passphrase
|
||||
);
|
||||
|
||||
await putAttributes(token, keyAttributes);
|
||||
await generateAndSaveIntermediateKeyAttributes(
|
||||
passphrase,
|
||||
keyAttributes,
|
||||
masterKey,
|
||||
masterKey
|
||||
);
|
||||
await setSessionKeys(masterKey);
|
||||
setJustSignedUp(true);
|
||||
|
|
|
@ -11,6 +11,7 @@ import SignUp from 'components/SignUp';
|
|||
import constants from 'utils/strings/constants';
|
||||
import localForage from 'utils/storage/localForage';
|
||||
import IncognitoWarning from 'components/IncognitoWarning';
|
||||
import { logError } from 'utils/sentry';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
|
@ -19,7 +20,7 @@ const Container = styled.div`
|
|||
justify-content: center;
|
||||
background-color: #000;
|
||||
|
||||
@media(max-width: 1024px) {
|
||||
@media (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
@ -32,7 +33,7 @@ const SlideContainer = styled.div`
|
|||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
@media(max-width: 1024px) {
|
||||
@media (max-width: 1024px) {
|
||||
flex-grow: 0;
|
||||
}
|
||||
`;
|
||||
|
@ -46,7 +47,7 @@ const DesktopBox = styled.div`
|
|||
justify-content: center;
|
||||
background-color: #242424;
|
||||
|
||||
@media(max-width: 1024px) {
|
||||
@media (max-width: 1024px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
@ -54,7 +55,7 @@ const DesktopBox = styled.div`
|
|||
const MobileBox = styled.div`
|
||||
display: none;
|
||||
|
||||
@media(max-width: 1024px) {
|
||||
@media (max-width: 1024px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 40px 10px;
|
||||
|
@ -90,7 +91,7 @@ const Img = styled.img`
|
|||
height: 250px;
|
||||
object-fit: contain;
|
||||
|
||||
@media(max-width: 400px) {
|
||||
@media (max-width: 400px) {
|
||||
height: 180px;
|
||||
}
|
||||
`;
|
||||
|
@ -110,6 +111,7 @@ export default function LandingPage() {
|
|||
try {
|
||||
await localForage.ready();
|
||||
} catch (e) {
|
||||
logError(e, 'usage in incognito mode tried');
|
||||
setBlockUsage(true);
|
||||
}
|
||||
setLoading(false);
|
||||
|
@ -121,56 +123,73 @@ export default function LandingPage() {
|
|||
const signUp = () => setShowLogin(false);
|
||||
const login = () => setShowLogin(true);
|
||||
|
||||
return <Container>
|
||||
{loading ? <EnteSpinner /> :
|
||||
(<>
|
||||
<SlideContainer>
|
||||
<UpperText>
|
||||
{constants.HERO_HEADER()}
|
||||
</UpperText>
|
||||
<Carousel controls={false}>
|
||||
<Carousel.Item>
|
||||
<Img src="/images/slide-1.png" />
|
||||
<FeatureText>{constants.HERO_SLIDE_1_TITLE}</FeatureText>
|
||||
<TextContainer>{constants.HERO_SLIDE_1}</TextContainer>
|
||||
</Carousel.Item>
|
||||
<Carousel.Item>
|
||||
<Img src="/images/slide-2.png" />
|
||||
<FeatureText>{constants.HERO_SLIDE_2_TITLE}</FeatureText>
|
||||
<TextContainer>{constants.HERO_SLIDE_2}</TextContainer>
|
||||
</Carousel.Item>
|
||||
<Carousel.Item>
|
||||
<Img src="/images/slide-3.png" />
|
||||
<FeatureText>{constants.HERO_SLIDE_3_TITLE}</FeatureText>
|
||||
<TextContainer>{constants.HERO_SLIDE_3}</TextContainer>
|
||||
</Carousel.Item>
|
||||
</Carousel>
|
||||
</SlideContainer>
|
||||
<MobileBox>
|
||||
<Button
|
||||
variant="outline-success"
|
||||
size="lg"
|
||||
style={{ color: '#fff', padding: '10px 50px' }}
|
||||
onClick={() => router.push('signup')}
|
||||
>
|
||||
{constants.SIGN_UP}
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
variant="link"
|
||||
size="lg"
|
||||
style={{ color: '#fff', padding: '10px 50px' }}
|
||||
onClick={() => router.push('login')}
|
||||
>
|
||||
{constants.SIGN_IN}
|
||||
</Button>
|
||||
</MobileBox>
|
||||
<DesktopBox>
|
||||
<SideBox>
|
||||
{showLogin ? <Login signUp={signUp} /> : <SignUp login={login} />}
|
||||
</SideBox>
|
||||
</DesktopBox>
|
||||
{blockUsage && <IncognitoWarning />}
|
||||
</>)}
|
||||
</Container>;
|
||||
return (
|
||||
<Container>
|
||||
{loading ? (
|
||||
<EnteSpinner />
|
||||
) : (
|
||||
<>
|
||||
<SlideContainer>
|
||||
<UpperText>{constants.HERO_HEADER()}</UpperText>
|
||||
<Carousel controls={false}>
|
||||
<Carousel.Item>
|
||||
<Img src="/images/slide-1.png" />
|
||||
<FeatureText>
|
||||
{constants.HERO_SLIDE_1_TITLE}
|
||||
</FeatureText>
|
||||
<TextContainer>
|
||||
{constants.HERO_SLIDE_1}
|
||||
</TextContainer>
|
||||
</Carousel.Item>
|
||||
<Carousel.Item>
|
||||
<Img src="/images/slide-2.png" />
|
||||
<FeatureText>
|
||||
{constants.HERO_SLIDE_2_TITLE}
|
||||
</FeatureText>
|
||||
<TextContainer>
|
||||
{constants.HERO_SLIDE_2}
|
||||
</TextContainer>
|
||||
</Carousel.Item>
|
||||
<Carousel.Item>
|
||||
<Img src="/images/slide-3.png" />
|
||||
<FeatureText>
|
||||
{constants.HERO_SLIDE_3_TITLE}
|
||||
</FeatureText>
|
||||
<TextContainer>
|
||||
{constants.HERO_SLIDE_3}
|
||||
</TextContainer>
|
||||
</Carousel.Item>
|
||||
</Carousel>
|
||||
</SlideContainer>
|
||||
<MobileBox>
|
||||
<Button
|
||||
variant="outline-success"
|
||||
size="lg"
|
||||
style={{ color: '#fff', padding: '10px 50px' }}
|
||||
onClick={() => router.push('signup')}>
|
||||
{constants.SIGN_UP}
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
variant="link"
|
||||
size="lg"
|
||||
style={{ color: '#fff', padding: '10px 50px' }}
|
||||
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');
|
||||
};
|
||||
|
||||
return <Container>{loading ?
|
||||
<EnteSpinner>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</EnteSpinner>:
|
||||
<Card style={{ minWidth: '320px' }} className="text-center">
|
||||
<Card.Body style={{ padding: '40px 30px' }}>
|
||||
<Login signUp={register}/>
|
||||
</Card.Body>
|
||||
</Card>}
|
||||
</Container>;
|
||||
return (
|
||||
<Container>
|
||||
{loading ? (
|
||||
<EnteSpinner>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</EnteSpinner>
|
||||
) : (
|
||||
<Card style={{ minWidth: '320px' }} className="text-center">
|
||||
<Card.Body style={{ padding: '40px 30px' }}>
|
||||
<Login signUp={register} />
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ export default function Recover() {
|
|||
const masterKey: string = await cryptoWorker.decryptB64(
|
||||
keyAttributes.masterKeyEncryptedWithRecoveryKey,
|
||||
keyAttributes.masterKeyDecryptionNonce,
|
||||
await cryptoWorker.fromHex(recoveryKey),
|
||||
await cryptoWorker.fromHex(recoveryKey)
|
||||
);
|
||||
setSessionKeys(masterKey);
|
||||
router.push('/changePassword');
|
||||
|
@ -51,13 +51,10 @@ export default function Recover() {
|
|||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Card
|
||||
style={{ minWidth: '320px' }}
|
||||
className="text-center"
|
||||
>
|
||||
<Card style={{ minWidth: '320px' }} className="text-center">
|
||||
<Card.Body style={{ padding: '40px 30px' }}>
|
||||
<Card.Title style={{ marginBottom: '32px' }}>
|
||||
<LogoImg src='/icon.svg' />
|
||||
<LogoImg src="/icon.svg" />
|
||||
{constants.RECOVER_ACCOUNT}
|
||||
</Card.Title>
|
||||
<SingleInputForm
|
||||
|
@ -71,12 +68,10 @@ export default function Recover() {
|
|||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: '12px',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => SetMessageDialogView(true)}
|
||||
>
|
||||
onClick={() => SetMessageDialogView(true)}>
|
||||
{constants.NO_RECOVERY_KEY}
|
||||
</Button>
|
||||
<Button variant="link" onClick={router.back}>
|
||||
|
|
|
@ -7,7 +7,6 @@ import EnteSpinner from 'components/EnteSpinner';
|
|||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import SignUp from 'components/SignUp';
|
||||
|
||||
|
||||
export default function SignUpPage() {
|
||||
const router = useRouter();
|
||||
const appContext = useContext(AppContext);
|
||||
|
@ -29,14 +28,16 @@ export default function SignUpPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<Container>{
|
||||
loading ? <EnteSpinner /> :
|
||||
<Container>
|
||||
{loading ? (
|
||||
<EnteSpinner />
|
||||
) : (
|
||||
<Card style={{ minWidth: '320px' }} className="text-center">
|
||||
<Card.Body style={{ padding: '40px 30px' }}>
|
||||
<SignUp login={login} />
|
||||
</Card.Body>
|
||||
</Card>
|
||||
}
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,20 +2,20 @@ import React, { useEffect, useState } from 'react';
|
|||
import constants from 'utils/strings/constants';
|
||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||
import { useRouter } from 'next/router';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import CryptoWorker, { B64EncryptionResult } from 'utils/crypto';
|
||||
import SingleInputForm from 'components/SingleInputForm';
|
||||
import MessageDialog from 'components/MessageDialog';
|
||||
import Container from 'components/Container';
|
||||
import { Card, Button } from 'react-bootstrap';
|
||||
import LogoImg from 'components/LogoImg';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { B64EncryptionResult } from 'services/uploadService';
|
||||
import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
|
||||
|
||||
export default function Recover() {
|
||||
const router = useRouter();
|
||||
const [messageDialogView, SetMessageDialogView] = useState(false);
|
||||
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] = useState<B64EncryptionResult>(null);
|
||||
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] =
|
||||
useState<B64EncryptionResult>(null);
|
||||
const [sessionID, setSessionID] = useState(null);
|
||||
useEffect(() => {
|
||||
router.prefetch('/gallery');
|
||||
|
@ -41,7 +41,7 @@ export default function Recover() {
|
|||
const twoFactorSecret: string = await cryptoWorker.decryptB64(
|
||||
encryptedTwoFactorSecret.encryptedData,
|
||||
encryptedTwoFactorSecret.nonce,
|
||||
await cryptoWorker.fromHex(recoveryKey),
|
||||
await cryptoWorker.fromHex(recoveryKey)
|
||||
);
|
||||
const resp = await removeTwoFactor(sessionID, twoFactorSecret);
|
||||
const { keyAttributes, encryptedToken, token, id } = resp;
|
||||
|
@ -63,13 +63,10 @@ export default function Recover() {
|
|||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Card
|
||||
style={{ minWidth: '320px' }}
|
||||
className="text-center"
|
||||
>
|
||||
<Card style={{ minWidth: '320px' }} className="text-center">
|
||||
<Card.Body style={{ padding: '40px 30px' }}>
|
||||
<Card.Title style={{ marginBottom: '32px' }}>
|
||||
<LogoImg src='/icon.svg' />
|
||||
<LogoImg src="/icon.svg" />
|
||||
{constants.RECOVER_TWO_FACTOR}
|
||||
</Card.Title>
|
||||
<SingleInputForm
|
||||
|
@ -83,12 +80,10 @@ export default function Recover() {
|
|||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: '12px',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => SetMessageDialogView(true)}
|
||||
>
|
||||
onClick={() => SetMessageDialogView(true)}>
|
||||
{constants.NO_RECOVERY_KEY}
|
||||
</Button>
|
||||
<Button variant="link" onClick={router.back}>
|
||||
|
|
|
@ -4,17 +4,20 @@ import { CodeBlock, FreeFlowText } from 'components/RecoveryKeyModal';
|
|||
import { DeadCenter } from 'pages/gallery';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
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 constants from 'utils/strings/constants';
|
||||
import Container from 'components/Container';
|
||||
import { useRouter } from 'next/router';
|
||||
import VerifyTwoFactor from 'components/VerifyTwoFactor';
|
||||
import { B64EncryptionResult } from 'services/uploadService';
|
||||
import { B64EncryptionResult } from 'utils/crypto';
|
||||
import { encryptWithRecoveryKey } from 'utils/crypto';
|
||||
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
|
||||
import { AppContext } from 'pages/_app';
|
||||
|
||||
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
|
||||
|
||||
enum SetupMode {
|
||||
QR_CODE,
|
||||
|
@ -22,15 +25,19 @@ enum SetupMode {
|
|||
}
|
||||
|
||||
const QRCode = styled.img`
|
||||
height:200px;
|
||||
width:200px;
|
||||
margin:1rem;
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
margin: 1rem;
|
||||
`;
|
||||
|
||||
export default function SetupTwoFactor() {
|
||||
const [setupMode, setSetupMode] = useState<SetupMode>(SetupMode.QR_CODE);
|
||||
const [twoFactorSecret, setTwoFactorSecret] = useState<TwoFactorSecret>(null);
|
||||
const [recoveryEncryptedTwoFactorSecret, setRecoveryEncryptedTwoFactorSecret] = useState<B64EncryptionResult>(null);
|
||||
const [twoFactorSecret, setTwoFactorSecret] =
|
||||
useState<TwoFactorSecret>(null);
|
||||
const [
|
||||
recoveryEncryptedTwoFactorSecret,
|
||||
setRecoveryEncryptedTwoFactorSecret,
|
||||
] = useState<B64EncryptionResult>(null);
|
||||
const router = useRouter();
|
||||
const appContext = useContext(AppContext);
|
||||
useEffect(() => {
|
||||
|
@ -40,11 +47,17 @@ export default function SetupTwoFactor() {
|
|||
const main = async () => {
|
||||
try {
|
||||
const twoFactorSecret = await setupTwoFactor();
|
||||
const recoveryEncryptedTwoFactorSecret = await encryptWithRecoveryKey(twoFactorSecret.secretCode);
|
||||
const recoveryEncryptedTwoFactorSecret =
|
||||
await encryptWithRecoveryKey(twoFactorSecret.secretCode);
|
||||
setTwoFactorSecret(twoFactorSecret);
|
||||
setRecoveryEncryptedTwoFactorSecret(recoveryEncryptedTwoFactorSecret);
|
||||
setRecoveryEncryptedTwoFactorSecret(
|
||||
recoveryEncryptedTwoFactorSecret
|
||||
);
|
||||
} 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');
|
||||
}
|
||||
};
|
||||
|
@ -52,8 +65,14 @@ export default function SetupTwoFactor() {
|
|||
}, []);
|
||||
const onSubmit = async (otp: string) => {
|
||||
await enableTwoFactor(otp, recoveryEncryptedTwoFactorSecret);
|
||||
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), isTwoFactorEnabled: true });
|
||||
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_SETUP_SUCCESS, severity: 'info' });
|
||||
setData(LS_KEYS.USER, {
|
||||
...getData(LS_KEYS.USER),
|
||||
isTwoFactorEnabled: true,
|
||||
});
|
||||
appContext.setDisappearingFlashMessage({
|
||||
message: constants.TWO_FACTOR_SETUP_SUCCESS,
|
||||
type: FLASH_MESSAGE_TYPE.SUCCESS,
|
||||
});
|
||||
router.push('/gallery');
|
||||
};
|
||||
return (
|
||||
|
@ -62,35 +81,67 @@ export default function SetupTwoFactor() {
|
|||
<Card.Body style={{ padding: '40px 30px', minHeight: '400px' }}>
|
||||
<DeadCenter>
|
||||
<Card.Title style={{ marginBottom: '32px' }}>
|
||||
<LogoImg src='/icon.svg' />
|
||||
<LogoImg src="/icon.svg" />
|
||||
{constants.TWO_FACTOR}
|
||||
</Card.Title>
|
||||
{setupMode === SetupMode.QR_CODE ? (
|
||||
<>
|
||||
<p>{constants.TWO_FACTOR_QR_INSTRUCTION}</p>
|
||||
<DeadCenter>
|
||||
{!twoFactorSecret ? <div style={{ 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)}>
|
||||
{!twoFactorSecret ? (
|
||||
<div
|
||||
style={{
|
||||
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}
|
||||
</Button>
|
||||
</DeadCenter>
|
||||
</>
|
||||
) : (<>
|
||||
<p>{constants.TWO_FACTOR_MANUAL_CODE_INSTRUCTION}</p>
|
||||
<CodeBlock height={100}>
|
||||
{!twoFactorSecret ? <EnteSpinner /> : (
|
||||
<FreeFlowText>
|
||||
{twoFactorSecret.secretCode}
|
||||
</FreeFlowText>
|
||||
|
||||
)}
|
||||
</CodeBlock>
|
||||
<Button block variant="link" style={{ marginBottom: '1rem' }} onClick={() => setSetupMode(SetupMode.QR_CODE)}>
|
||||
{constants.SCAN_QR_CODE}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
{
|
||||
constants.TWO_FACTOR_MANUAL_CODE_INSTRUCTION
|
||||
}
|
||||
</p>
|
||||
<CodeBlock height={100}>
|
||||
{!twoFactorSecret ? (
|
||||
<EnteSpinner />
|
||||
) : (
|
||||
<FreeFlowText>
|
||||
{twoFactorSecret.secretCode}
|
||||
</FreeFlowText>
|
||||
)}
|
||||
</CodeBlock>
|
||||
<Button
|
||||
block
|
||||
variant="link"
|
||||
style={{ marginBottom: '1rem' }}
|
||||
onClick={() =>
|
||||
setSetupMode(SetupMode.QR_CODE)
|
||||
}>
|
||||
{constants.SCAN_QR_CODE}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
|
@ -99,13 +150,20 @@ export default function SetupTwoFactor() {
|
|||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
<VerifyTwoFactor onSubmit={onSubmit} back={router.back} buttonText={constants.ENABLE} />
|
||||
<Button style={{ marginTop: '16px' }} variant="link-danger" onClick={router.back}>
|
||||
<VerifyTwoFactor
|
||||
onSubmit={onSubmit}
|
||||
back={router.back}
|
||||
buttonText={constants.ENABLE}
|
||||
/>
|
||||
<Button
|
||||
style={{ marginTop: '16px' }}
|
||||
variant="link-danger"
|
||||
onClick={router.back}>
|
||||
{constants.GO_BACK}
|
||||
</Button>
|
||||
</DeadCenter>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Container >
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -52,21 +52,23 @@ export default function Home() {
|
|||
<Card style={{ minWidth: '300px' }} className="text-center">
|
||||
<Card.Body style={{ padding: '40px 30px', minHeight: '400px' }}>
|
||||
<Card.Title style={{ marginBottom: '32px' }}>
|
||||
<LogoImg src='/icon.svg' />
|
||||
<LogoImg src="/icon.svg" />
|
||||
{constants.TWO_FACTOR}
|
||||
</Card.Title>
|
||||
<VerifyTwoFactor onSubmit={onSubmit} back={router.back} buttonText={constants.VERIFY} />
|
||||
<VerifyTwoFactor
|
||||
onSubmit={onSubmit}
|
||||
back={router.back}
|
||||
buttonText={constants.VERIFY}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: '12px',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => router.push('/two-factor/recover')}
|
||||
>
|
||||
onClick={() => router.push('/two-factor/recover')}>
|
||||
{constants.LOST_DEVICE}
|
||||
</Button>
|
||||
<Button variant="link" onClick={logoutUser}>
|
||||
|
|
|
@ -56,14 +56,24 @@ export default function Verify() {
|
|||
|
||||
const onSubmit = async (
|
||||
{ ott }: formValues,
|
||||
{ setFieldError }: FormikHelpers<formValues>,
|
||||
{ setFieldError }: FormikHelpers<formValues>
|
||||
) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
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) {
|
||||
setData(LS_KEYS.USER, { email, twoFactorSessionID, isTwoFactorEnabled: true });
|
||||
setData(LS_KEYS.USER, {
|
||||
email,
|
||||
twoFactorSessionID,
|
||||
isTwoFactorEnabled: true,
|
||||
});
|
||||
router.push('/two-factor/verify');
|
||||
return;
|
||||
}
|
||||
|
@ -109,7 +119,7 @@ export default function Verify() {
|
|||
<Card style={{ minWidth: '300px' }} className="text-center">
|
||||
<Card.Body style={{ padding: '40px 30px' }}>
|
||||
<Card.Title style={{ marginBottom: '32px' }}>
|
||||
<LogoImg src='/icon.svg' />
|
||||
<LogoImg src="/icon.svg" />
|
||||
{constants.VERIFY_EMAIL}
|
||||
</Card.Title>
|
||||
{constants.EMAIL_SENT({ email })}
|
||||
|
@ -123,8 +133,7 @@ export default function Verify() {
|
|||
})}
|
||||
validateOnChange={false}
|
||||
validateOnBlur={false}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
onSubmit={onSubmit}>
|
||||
{({
|
||||
values,
|
||||
touched,
|
||||
|
@ -140,7 +149,7 @@ export default function Verify() {
|
|||
value={values.ott}
|
||||
onChange={handleChange('ott')}
|
||||
isInvalid={Boolean(
|
||||
touched.ott && errors.ott,
|
||||
touched.ott && errors.ott
|
||||
)}
|
||||
placeholder={constants.ENTER_OTT}
|
||||
disabled={loading}
|
||||
|
|
|
@ -8,17 +8,25 @@ pageCache();
|
|||
precacheAndRoute(self.__WB_MANIFEST);
|
||||
cleanupOutdatedCaches();
|
||||
|
||||
registerRoute('/share-target', async ({ event }) => {
|
||||
event.waitUntil(async function() {
|
||||
const data = await event.request.formData();
|
||||
const client = await self.clients.get(event.resultingClientId || event.clientId);
|
||||
const files = data.getAll('files');
|
||||
setTimeout(() => {
|
||||
client.postMessage({ files, action: 'upload-files' });
|
||||
}, 1000);
|
||||
}());
|
||||
return Response.redirect('./');
|
||||
}, 'POST');
|
||||
registerRoute(
|
||||
'/share-target',
|
||||
async ({ event }) => {
|
||||
event.waitUntil(
|
||||
(async function () {
|
||||
const data = await event.request.formData();
|
||||
const client = await self.clients.get(
|
||||
event.resultingClientId || event.clientId
|
||||
);
|
||||
const files = data.getAll('files');
|
||||
setTimeout(() => {
|
||||
client.postMessage({ files, action: 'upload-files' });
|
||||
}, 1000);
|
||||
})()
|
||||
);
|
||||
return Response.redirect('./');
|
||||
},
|
||||
'POST'
|
||||
);
|
||||
|
||||
// Use a stale-while-revalidate strategy for all other requests.
|
||||
setDefaultHandler(new NetworkOnly());
|
||||
|
|
|
@ -21,7 +21,7 @@ class HTTPService {
|
|||
}
|
||||
const { response } = err;
|
||||
return Promise.reject(response);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,9 @@ class HTTPService {
|
|||
...config.headers,
|
||||
};
|
||||
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 });
|
||||
}
|
||||
|
@ -89,7 +91,7 @@ class HTTPService {
|
|||
url: string,
|
||||
params?: IQueryPrams,
|
||||
headers?: IHTTPHeaders,
|
||||
customConfig?: any,
|
||||
customConfig?: any
|
||||
) {
|
||||
return this.request(
|
||||
{
|
||||
|
@ -98,7 +100,7 @@ class HTTPService {
|
|||
params,
|
||||
url,
|
||||
},
|
||||
customConfig,
|
||||
customConfig
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -110,7 +112,7 @@ class HTTPService {
|
|||
data?: any,
|
||||
params?: IQueryPrams,
|
||||
headers?: IHTTPHeaders,
|
||||
customConfig?: any,
|
||||
customConfig?: any
|
||||
) {
|
||||
return this.request(
|
||||
{
|
||||
|
@ -120,7 +122,7 @@ class HTTPService {
|
|||
params,
|
||||
url,
|
||||
},
|
||||
customConfig,
|
||||
customConfig
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -132,7 +134,7 @@ class HTTPService {
|
|||
data: any,
|
||||
params?: IQueryPrams,
|
||||
headers?: IHTTPHeaders,
|
||||
customConfig?: any,
|
||||
customConfig?: any
|
||||
) {
|
||||
return this.request(
|
||||
{
|
||||
|
@ -142,7 +144,7 @@ class HTTPService {
|
|||
params,
|
||||
url,
|
||||
},
|
||||
customConfig,
|
||||
customConfig
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -154,7 +156,7 @@ class HTTPService {
|
|||
data: any,
|
||||
params?: IQueryPrams,
|
||||
headers?: IHTTPHeaders,
|
||||
customConfig?: any,
|
||||
customConfig?: any
|
||||
) {
|
||||
return this.request(
|
||||
{
|
||||
|
@ -164,7 +166,7 @@ class HTTPService {
|
|||
params,
|
||||
url,
|
||||
},
|
||||
customConfig,
|
||||
customConfig
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,9 +15,9 @@ export enum PAYMENT_INTENT_STATUS {
|
|||
REQUIRE_ACTION = 'requires_action',
|
||||
REQUIRE_PAYMENT_METHOD = 'requires_payment_method',
|
||||
}
|
||||
enum PaymentActionType{
|
||||
Buy='buy',
|
||||
Update='update'
|
||||
enum PaymentActionType {
|
||||
Buy = 'buy',
|
||||
Update = 'update',
|
||||
}
|
||||
export interface Subscription {
|
||||
id: number;
|
||||
|
@ -30,6 +30,8 @@ export interface Subscription {
|
|||
attributes: {
|
||||
isCancelled: boolean;
|
||||
};
|
||||
price: string;
|
||||
period: string;
|
||||
}
|
||||
export interface Plan {
|
||||
id: string;
|
||||
|
@ -66,11 +68,13 @@ class billingService {
|
|||
}
|
||||
}
|
||||
|
||||
public async updatePlans() {
|
||||
public async getPlans(): Promise<Plan[]> {
|
||||
try {
|
||||
const response = await HTTPService.get(`${ENDPOINT}/billing/plans`);
|
||||
const response = await HTTPService.get(
|
||||
`${ENDPOINT}/billing/plans/v2`
|
||||
);
|
||||
const { plans } = response.data;
|
||||
setData(LS_KEYS.PLANS, plans);
|
||||
return plans;
|
||||
} catch (e) {
|
||||
logError(e, 'failed to get plans');
|
||||
}
|
||||
|
@ -83,12 +87,12 @@ class billingService {
|
|||
null,
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
},
|
||||
}
|
||||
);
|
||||
const { subscription } = response.data;
|
||||
setData(LS_KEYS.SUBSCRIPTION, subscription);
|
||||
} 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({
|
||||
// sessionId: response.data.sessionID,
|
||||
// });
|
||||
const paymentToken =await getPaymentToken();
|
||||
await this.redirectToPayments(paymentToken, productID, PaymentActionType.Buy);
|
||||
const paymentToken = await getPaymentToken();
|
||||
await this.redirectToPayments(
|
||||
paymentToken,
|
||||
productID,
|
||||
PaymentActionType.Buy
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'unable to buy subscription');
|
||||
throw e;
|
||||
|
@ -139,8 +147,12 @@ class billingService {
|
|||
// }
|
||||
// break;
|
||||
// }
|
||||
const paymentToken =await getPaymentToken();
|
||||
await this.redirectToPayments(paymentToken, productID, PaymentActionType.Update);
|
||||
const paymentToken = await getPaymentToken();
|
||||
await this.redirectToPayments(
|
||||
paymentToken,
|
||||
productID,
|
||||
PaymentActionType.Update
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'subscription update failed');
|
||||
throw e;
|
||||
|
@ -160,7 +172,7 @@ class billingService {
|
|||
null,
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
},
|
||||
}
|
||||
);
|
||||
const { subscription } = response.data;
|
||||
setData(LS_KEYS.SUBSCRIPTION, subscription);
|
||||
|
@ -178,7 +190,7 @@ class billingService {
|
|||
null,
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
},
|
||||
}
|
||||
);
|
||||
const { subscription } = response.data;
|
||||
setData(LS_KEYS.SUBSCRIPTION, subscription);
|
||||
|
@ -196,12 +208,12 @@ class billingService {
|
|||
},
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async verifySubscription(
|
||||
sessionID: string = null,
|
||||
sessionID: string = null
|
||||
): Promise<Subscription> {
|
||||
try {
|
||||
const response = await HTTPService.post(
|
||||
|
@ -214,7 +226,7 @@ class billingService {
|
|||
null,
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
},
|
||||
}
|
||||
);
|
||||
const { subscription } = response.data;
|
||||
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 {
|
||||
window.location.href = `${getPaymentsUrl()}?productID=${productID}&paymentToken=${paymentToken}&action=${action}&rootURL=${
|
||||
window.location.origin
|
||||
|
@ -243,7 +259,7 @@ class billingService {
|
|||
null,
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
},
|
||||
}
|
||||
);
|
||||
window.location.href = response.data.url;
|
||||
} catch (e) {
|
||||
|
@ -259,7 +275,7 @@ class billingService {
|
|||
{ startTime: 0, endTime: Date.now() * 1000 },
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
},
|
||||
}
|
||||
);
|
||||
return convertToHumanReadable(response.data.usage);
|
||||
} catch (e) {
|
||||
|
|
|
@ -7,7 +7,7 @@ import CryptoWorker from 'utils/crypto';
|
|||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { getPublicKey, User } from './userService';
|
||||
import { B64EncryptionResult } from './uploadService';
|
||||
import { B64EncryptionResult } from 'utils/crypto';
|
||||
import HTTPService from './HTTPService';
|
||||
import { File } from './fileService';
|
||||
import { logError } from 'utils/sentry';
|
||||
|
@ -52,7 +52,7 @@ export interface CollectionAndItsLatestFile {
|
|||
|
||||
const getCollectionWithSecrets = async (
|
||||
collection: Collection,
|
||||
masterKey: string,
|
||||
masterKey: string
|
||||
) => {
|
||||
const worker = await new CryptoWorker();
|
||||
const userID = getData(LS_KEYS.USER).id;
|
||||
|
@ -61,26 +61,27 @@ const getCollectionWithSecrets = async (
|
|||
decryptedKey = await worker.decryptB64(
|
||||
collection.encryptedKey,
|
||||
collection.keyDecryptionNonce,
|
||||
masterKey,
|
||||
masterKey
|
||||
);
|
||||
} else {
|
||||
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
||||
const secretKey = await worker.decryptB64(
|
||||
keyAttributes.encryptedSecretKey,
|
||||
keyAttributes.secretKeyDecryptionNonce,
|
||||
masterKey,
|
||||
masterKey
|
||||
);
|
||||
decryptedKey = await worker.boxSealOpen(
|
||||
collection.encryptedKey,
|
||||
keyAttributes.publicKey,
|
||||
secretKey,
|
||||
secretKey
|
||||
);
|
||||
}
|
||||
collection.name = collection.name ||
|
||||
collection.name =
|
||||
collection.name ||
|
||||
(await worker.decryptToUTF8(
|
||||
collection.encryptedName,
|
||||
collection.nameDecryptionNonce,
|
||||
decryptedKey,
|
||||
decryptedKey
|
||||
));
|
||||
return {
|
||||
...collection,
|
||||
|
@ -91,7 +92,7 @@ const getCollectionWithSecrets = async (
|
|||
const getCollections = async (
|
||||
token: string,
|
||||
sinceTime: number,
|
||||
key: string,
|
||||
key: string
|
||||
): Promise<Collection[]> => {
|
||||
try {
|
||||
const resp = await HTTPService.get(
|
||||
|
@ -99,7 +100,7 @@ const getCollections = async (
|
|||
{
|
||||
sinceTime,
|
||||
},
|
||||
{ 'X-Auth-Token': token },
|
||||
{ 'X-Auth-Token': token }
|
||||
);
|
||||
const promises: Promise<Collection>[] = resp.data.collections.map(
|
||||
async (collection: Collection) => {
|
||||
|
@ -110,16 +111,16 @@ const getCollections = async (
|
|||
try {
|
||||
collectionWithSecrets = await getCollectionWithSecrets(
|
||||
collection,
|
||||
key,
|
||||
key
|
||||
);
|
||||
return collectionWithSecrets;
|
||||
} catch (e) {
|
||||
logError(
|
||||
e,
|
||||
`decryption failed for collection with id=${collection.id}`,
|
||||
`decryption failed for collection with id=${collection.id}`
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return await Promise.all(promises);
|
||||
} catch (e) {
|
||||
|
@ -129,18 +130,21 @@ const getCollections = async (
|
|||
};
|
||||
|
||||
export const getLocalCollections = async (): Promise<Collection[]> => {
|
||||
const collections: Collection[] = (await localForage.getItem(COLLECTIONS)) ?? [];
|
||||
const collections: Collection[] =
|
||||
(await localForage.getItem(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 () => {
|
||||
const localCollections = await getLocalCollections();
|
||||
const lastCollectionUpdationTime = await getCollectionUpdationTime();
|
||||
const token = getToken();
|
||||
const key = await getActualKey();
|
||||
const updatedCollections = (await getCollections(token, lastCollectionUpdationTime, key)) ?? [];
|
||||
const updatedCollections =
|
||||
(await getCollections(token, lastCollectionUpdationTime, key)) ?? [];
|
||||
if (updatedCollections.length === 0) {
|
||||
return localCollections;
|
||||
}
|
||||
|
@ -153,7 +157,7 @@ export const syncCollections = async () => {
|
|||
if (
|
||||
!latestCollectionsInstances.has(collection.id) ||
|
||||
latestCollectionsInstances.get(collection.id).updationTime <
|
||||
collection.updationTime
|
||||
collection.updationTime
|
||||
) {
|
||||
latestCollectionsInstances.set(collection.id, collection);
|
||||
}
|
||||
|
@ -161,7 +165,7 @@ export const syncCollections = async () => {
|
|||
|
||||
const collections: Collection[] = [];
|
||||
let updationTime = await localForage.getItem<number>(
|
||||
COLLECTION_UPDATION_TIME,
|
||||
COLLECTION_UPDATION_TIME
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const [_, collection] of latestCollectionsInstances) {
|
||||
|
@ -179,7 +183,7 @@ export const syncCollections = async () => {
|
|||
|
||||
export const getCollectionsAndTheirLatestFile = (
|
||||
collections: Collection[],
|
||||
files: File[],
|
||||
files: File[]
|
||||
): CollectionAndItsLatestFile[] => {
|
||||
const latestFile = new Map<number, File>();
|
||||
|
||||
|
@ -213,15 +217,16 @@ export const getFavItemIds = async (files: File[]): Promise<Set<number>> => {
|
|||
return new Set(
|
||||
files
|
||||
.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 (
|
||||
collectionName: string,
|
||||
type: CollectionType,
|
||||
type: CollectionType
|
||||
): Promise<Collection> => {
|
||||
try {
|
||||
const existingCollections = await syncCollections();
|
||||
|
@ -239,14 +244,14 @@ export const createCollection = async (
|
|||
nonce: keyDecryptionNonce,
|
||||
}: B64EncryptionResult = await worker.encryptToB64(
|
||||
collectionKey,
|
||||
encryptionKey,
|
||||
encryptionKey
|
||||
);
|
||||
const {
|
||||
encryptedData: encryptedName,
|
||||
nonce: nameDecryptionNonce,
|
||||
}: B64EncryptionResult = await worker.encryptUTF8(
|
||||
collectionName,
|
||||
collectionKey,
|
||||
collectionKey
|
||||
);
|
||||
const newCollection: Collection = {
|
||||
id: null,
|
||||
|
@ -263,11 +268,11 @@ export const createCollection = async (
|
|||
};
|
||||
let createdCollection: Collection = await postCollection(
|
||||
newCollection,
|
||||
token,
|
||||
token
|
||||
);
|
||||
createdCollection = await getCollectionWithSecrets(
|
||||
createdCollection,
|
||||
encryptionKey,
|
||||
encryptionKey
|
||||
);
|
||||
return createdCollection;
|
||||
} catch (e) {
|
||||
|
@ -278,14 +283,14 @@ export const createCollection = async (
|
|||
|
||||
const postCollection = async (
|
||||
collectionData: Collection,
|
||||
token: string,
|
||||
token: string
|
||||
): Promise<Collection> => {
|
||||
try {
|
||||
const response = await HTTPService.post(
|
||||
`${ENDPOINT}/collections`,
|
||||
collectionData,
|
||||
null,
|
||||
{ 'X-Auth-Token': token },
|
||||
{ 'X-Auth-Token': token }
|
||||
);
|
||||
return response.data.collection;
|
||||
} catch (e) {
|
||||
|
@ -298,7 +303,7 @@ export const addToFavorites = async (file: File) => {
|
|||
if (!favCollection) {
|
||||
favCollection = await createCollection(
|
||||
'Favorites',
|
||||
CollectionType.favorites,
|
||||
CollectionType.favorites
|
||||
);
|
||||
await localForage.setItem(FAV_COLLECTION, favCollection);
|
||||
}
|
||||
|
@ -312,7 +317,7 @@ export const removeFromFavorites = async (file: File) => {
|
|||
|
||||
export const addToCollection = async (
|
||||
collection: Collection,
|
||||
files: File[],
|
||||
files: File[]
|
||||
) => {
|
||||
try {
|
||||
const params = {};
|
||||
|
@ -322,7 +327,8 @@ export const addToCollection = async (
|
|||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
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.keyDecryptionNonce = newEncryptedKey.nonce;
|
||||
if (params['files'] === undefined) {
|
||||
|
@ -334,13 +340,13 @@ export const addToCollection = async (
|
|||
keyDecryptionNonce: file.keyDecryptionNonce,
|
||||
});
|
||||
return file;
|
||||
}),
|
||||
})
|
||||
);
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/collections/add-files`,
|
||||
params,
|
||||
null,
|
||||
{ 'X-Auth-Token': token },
|
||||
{ 'X-Auth-Token': token }
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'Add to collection Failed ');
|
||||
|
@ -357,13 +363,13 @@ const removeFromCollection = async (collection: Collection, files: File[]) => {
|
|||
params['fileIDs'] = [];
|
||||
}
|
||||
params['fileIDs'].push(file.id);
|
||||
}),
|
||||
})
|
||||
);
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/collections/remove-files`,
|
||||
params,
|
||||
null,
|
||||
{ 'X-Auth-Token': token },
|
||||
{ 'X-Auth-Token': token }
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'remove from collection failed ');
|
||||
|
@ -374,7 +380,7 @@ export const deleteCollection = async (
|
|||
collectionID: number,
|
||||
syncWithRemote: () => Promise<void>,
|
||||
redirectToAll: () => void,
|
||||
setDialogMessage: SetDialogMessage,
|
||||
setDialogMessage: SetDialogMessage
|
||||
) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
@ -383,7 +389,7 @@ export const deleteCollection = async (
|
|||
`${ENDPOINT}/collections/${collectionID}`,
|
||||
null,
|
||||
null,
|
||||
{ 'X-Auth-Token': token },
|
||||
{ 'X-Auth-Token': token }
|
||||
);
|
||||
await syncWithRemote();
|
||||
redirectToAll();
|
||||
|
@ -399,7 +405,7 @@ export const deleteCollection = async (
|
|||
|
||||
export const renameCollection = async (
|
||||
collection: Collection,
|
||||
newCollectionName: string,
|
||||
newCollectionName: string
|
||||
) => {
|
||||
const token = getToken();
|
||||
const worker = await new CryptoWorker();
|
||||
|
@ -408,7 +414,7 @@ export const renameCollection = async (
|
|||
nonce: nameDecryptionNonce,
|
||||
}: B64EncryptionResult = await worker.encryptUTF8(
|
||||
newCollectionName,
|
||||
collection.key,
|
||||
collection.key
|
||||
);
|
||||
const collectionRenameRequest = {
|
||||
collectionID: collection.id,
|
||||
|
@ -421,12 +427,12 @@ export const renameCollection = async (
|
|||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
export const shareCollection = async (
|
||||
collection: Collection,
|
||||
withUserEmail: string,
|
||||
withUserEmail: string
|
||||
) => {
|
||||
try {
|
||||
const worker = await new CryptoWorker();
|
||||
|
@ -435,7 +441,7 @@ export const shareCollection = async (
|
|||
const publicKey: string = await getPublicKey(withUserEmail);
|
||||
const encryptedKey: string = await worker.boxSeal(
|
||||
collection.key,
|
||||
publicKey,
|
||||
publicKey
|
||||
);
|
||||
const shareCollectionRequest = {
|
||||
collectionID: collection.id,
|
||||
|
@ -448,7 +454,7 @@ export const shareCollection = async (
|
|||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'share collection failed ');
|
||||
|
@ -458,7 +464,7 @@ export const shareCollection = async (
|
|||
|
||||
export const unshareCollection = async (
|
||||
collection: Collection,
|
||||
withUserEmail: string,
|
||||
withUserEmail: string
|
||||
) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
@ -472,7 +478,7 @@ export const unshareCollection = async (
|
|||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'unshare collection failed ');
|
||||
|
@ -492,11 +498,13 @@ export const getFavCollection = async () => {
|
|||
|
||||
export const getNonEmptyCollections = (
|
||||
collections: Collection[],
|
||||
files: File[],
|
||||
files: File[]
|
||||
) => {
|
||||
const nonEmptyCollectionsIds = new Set<number>();
|
||||
for (const file of files) {
|
||||
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 { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import { fileIsHEIC, convertHEIC2JPEG } from 'utils/file';
|
||||
import {
|
||||
fileIsHEIC,
|
||||
convertHEIC2JPEG,
|
||||
fileNameWithoutExtension,
|
||||
generateStreamFromArrayBuffer,
|
||||
} from 'utils/file';
|
||||
import HTTPService from './HTTPService';
|
||||
import { File } from './fileService';
|
||||
import { File, FILE_TYPE } from './fileService';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { decodeMotionPhoto } from './motionPhotoService';
|
||||
|
||||
class DownloadManager {
|
||||
private fileDownloads = new Map<string, string>();
|
||||
|
@ -36,36 +42,50 @@ class DownloadManager {
|
|||
getThumbnailUrl(file.id),
|
||||
null,
|
||||
{ 'X-Auth-Token': token },
|
||||
{ responseType: 'arraybuffer' },
|
||||
{ responseType: 'arraybuffer' }
|
||||
);
|
||||
const worker = await new CryptoWorker();
|
||||
const decrypted: any = await worker.decryptThumbnail(
|
||||
new Uint8Array(resp.data),
|
||||
await worker.fromB64(file.thumbnail.decryptionHeader),
|
||||
file.key,
|
||||
file.key
|
||||
);
|
||||
try {
|
||||
await cache.put(
|
||||
file.id.toString(),
|
||||
new Response(new Blob([decrypted])),
|
||||
new Response(new Blob([decrypted]))
|
||||
);
|
||||
} catch (e) {
|
||||
// TODO: handle storage full exception.
|
||||
}
|
||||
return URL.createObjectURL(new Blob([decrypted]));
|
||||
}
|
||||
};
|
||||
|
||||
getFile = async (file: File, forPreview=false) => {
|
||||
getFile = async (file: File, forPreview = false) => {
|
||||
try {
|
||||
if (!this.fileDownloads.get(`${file.id}_${forPreview}`)) {
|
||||
// unzip motion photo and return fileBlob of the image for preview
|
||||
const fileStream = await this.downloadFile(file);
|
||||
let fileBlob= await new Response(fileStream).blob();
|
||||
let fileBlob = await new Response(fileStream).blob();
|
||||
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)) {
|
||||
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}`);
|
||||
} catch (e) {
|
||||
|
@ -79,25 +99,22 @@ class DownloadManager {
|
|||
if (!token) {
|
||||
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(
|
||||
getFileUrl(file.id),
|
||||
null,
|
||||
{ 'X-Auth-Token': token },
|
||||
{ responseType: 'arraybuffer' },
|
||||
{ responseType: 'arraybuffer' }
|
||||
);
|
||||
const decrypted: any = await worker.decryptFile(
|
||||
new Uint8Array(resp.data),
|
||||
await worker.fromB64(file.file.decryptionHeader),
|
||||
file.key,
|
||||
file.key
|
||||
);
|
||||
|
||||
return new ReadableStream({
|
||||
async start(controller: ReadableStreamDefaultController) {
|
||||
controller.enqueue(decrypted);
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
return generateStreamFromArrayBuffer(decrypted);
|
||||
}
|
||||
const resp = await fetch(getFileUrl(file.id), {
|
||||
headers: {
|
||||
|
@ -108,13 +125,11 @@ class DownloadManager {
|
|||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const decryptionHeader = await worker.fromB64(
|
||||
file.file.decryptionHeader,
|
||||
file.file.decryptionHeader
|
||||
);
|
||||
const fileKey = await worker.fromB64(file.key);
|
||||
const {
|
||||
pullState,
|
||||
decryptionChunkSize,
|
||||
} = await worker.initDecryption(decryptionHeader, fileKey);
|
||||
const { pullState, decryptionChunkSize } =
|
||||
await worker.initDecryption(decryptionHeader, fileKey);
|
||||
let data = new Uint8Array();
|
||||
// The following function handles each data chunk
|
||||
function push() {
|
||||
|
@ -123,24 +138,20 @@ class DownloadManager {
|
|||
// Is there more data to read?
|
||||
if (!done) {
|
||||
const buffer = new Uint8Array(
|
||||
data.byteLength + value.byteLength,
|
||||
data.byteLength + value.byteLength
|
||||
);
|
||||
buffer.set(new Uint8Array(data), 0);
|
||||
buffer.set(
|
||||
new Uint8Array(value),
|
||||
data.byteLength,
|
||||
);
|
||||
buffer.set(new Uint8Array(value), data.byteLength);
|
||||
if (buffer.length > decryptionChunkSize) {
|
||||
const fileData = buffer.slice(
|
||||
0,
|
||||
decryptionChunkSize,
|
||||
);
|
||||
const {
|
||||
decryptedData,
|
||||
} = await worker.decryptChunk(
|
||||
fileData,
|
||||
pullState,
|
||||
decryptionChunkSize
|
||||
);
|
||||
const { decryptedData } =
|
||||
await worker.decryptChunk(
|
||||
fileData,
|
||||
pullState
|
||||
);
|
||||
controller.enqueue(decryptedData);
|
||||
data = buffer.slice(decryptionChunkSize);
|
||||
} else {
|
||||
|
@ -149,12 +160,8 @@ class DownloadManager {
|
|||
push();
|
||||
} else {
|
||||
if (data) {
|
||||
const {
|
||||
decryptedData,
|
||||
} = await worker.decryptChunk(
|
||||
data,
|
||||
pullState,
|
||||
);
|
||||
const { decryptedData } =
|
||||
await worker.decryptChunk(data, pullState);
|
||||
controller.enqueue(decryptedData);
|
||||
data = null;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,26 @@
|
|||
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 { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { Collection, getLocalCollections } from './collectionService';
|
||||
import {
|
||||
Collection,
|
||||
getLocalCollections,
|
||||
getNonEmptyCollections,
|
||||
} from './collectionService';
|
||||
import downloadManager from './downloadManager';
|
||||
import { File, getLocalFiles } from './fileService';
|
||||
import { File, FILE_TYPE, getLocalFiles } from './fileService';
|
||||
import { decodeMotionPhoto } from './motionPhotoService';
|
||||
|
||||
export interface ExportProgress {
|
||||
current: number;
|
||||
|
@ -16,7 +32,7 @@ export interface ExportStats {
|
|||
}
|
||||
|
||||
export interface ExportRecord {
|
||||
stage: ExportStage
|
||||
stage: ExportStage;
|
||||
lastAttemptTimestamp: number;
|
||||
progress: ExportProgress;
|
||||
queuedFiles: string[];
|
||||
|
@ -27,7 +43,7 @@ export enum ExportStage {
|
|||
INIT,
|
||||
INPROGRESS,
|
||||
PAUSED,
|
||||
FINISHED
|
||||
FINISHED,
|
||||
}
|
||||
|
||||
enum ExportNotification {
|
||||
|
@ -37,26 +53,26 @@ enum ExportNotification {
|
|||
FAILED = 'export failed',
|
||||
ABORT = 'export aborted',
|
||||
PAUSE = 'export paused',
|
||||
UP_TO_DATE = `no new files to export`
|
||||
UP_TO_DATE = `no new files to export`,
|
||||
}
|
||||
|
||||
enum RecordType {
|
||||
SUCCESS = 'success',
|
||||
FAILED = 'failed'
|
||||
FAILED = 'failed',
|
||||
}
|
||||
export enum ExportType {
|
||||
NEW,
|
||||
PENDING,
|
||||
RETRY_FAILED
|
||||
RETRY_FAILED,
|
||||
}
|
||||
|
||||
const ExportRecordFileName='export_status.json';
|
||||
const MetadataFolderName='metadata';
|
||||
const EXPORT_RECORD_FILE_NAME = 'export_status.json';
|
||||
export const METADATA_FOLDER_NAME = 'metadata';
|
||||
|
||||
class ExportService {
|
||||
ElectronAPIs: any;
|
||||
|
||||
private exportInProgress: Promise<{ paused: boolean; }> = null;
|
||||
private exportInProgress: Promise<{ paused: boolean }> = null;
|
||||
private recordUpdateInProgress = Promise.resolve();
|
||||
private stopExport: boolean = false;
|
||||
private pauseExport: boolean = false;
|
||||
|
@ -73,7 +89,10 @@ class ExportService {
|
|||
pauseRunningExport() {
|
||||
this.pauseExport = true;
|
||||
}
|
||||
async exportFiles(updateProgress: (progress: ExportProgress) => void, exportType: ExportType) {
|
||||
async exportFiles(
|
||||
updateProgress: (progress: ExportProgress) => void,
|
||||
exportType: ExportType
|
||||
) {
|
||||
if (this.exportInProgress) {
|
||||
this.ElectronAPIs.sendNotification(ExportNotification.IN_PROGRESS);
|
||||
return this.exportInProgress;
|
||||
|
@ -87,25 +106,44 @@ class ExportService {
|
|||
let filesToExport: File[];
|
||||
const allFiles = await getLocalFiles();
|
||||
const collections = await getLocalCollections();
|
||||
const nonEmptyCollections = getNonEmptyCollections(
|
||||
collections,
|
||||
allFiles
|
||||
);
|
||||
const exportRecord = await this.getExportRecord(exportDir);
|
||||
|
||||
if (exportType === ExportType.NEW) {
|
||||
filesToExport = await getFilesUploadedAfterLastExport(allFiles, exportRecord);
|
||||
filesToExport = await getFilesUploadedAfterLastExport(
|
||||
allFiles,
|
||||
exportRecord
|
||||
);
|
||||
} else if (exportType === ExportType.RETRY_FAILED) {
|
||||
filesToExport = await getExportFailedFiles(allFiles, exportRecord);
|
||||
} else {
|
||||
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;
|
||||
this.exportInProgress = null;
|
||||
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 {
|
||||
if (!files?.length) {
|
||||
this.ElectronAPIs.sendNotification(ExportNotification.UP_TO_DATE);
|
||||
this.ElectronAPIs.sendNotification(
|
||||
ExportNotification.UP_TO_DATE
|
||||
);
|
||||
return { paused: false };
|
||||
}
|
||||
this.stopExport = false;
|
||||
|
@ -114,22 +152,24 @@ class ExportService {
|
|||
const failedFileCount = 0;
|
||||
|
||||
this.ElectronAPIs.showOnTray({
|
||||
export_progress:
|
||||
`0 / ${files.length} files exported`,
|
||||
export_progress: `0 / ${files.length} files exported`,
|
||||
});
|
||||
updateProgress({
|
||||
current: 0, total: files.length,
|
||||
current: 0,
|
||||
total: files.length,
|
||||
});
|
||||
this.ElectronAPIs.sendNotification(ExportNotification.START);
|
||||
|
||||
const collectionIDMap = new Map<number, string>();
|
||||
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(
|
||||
collectionFolderPath,
|
||||
collectionFolderPath
|
||||
);
|
||||
await this.ElectronAPIs.checkExistsAndCreateCollectionDir(
|
||||
`${collectionFolderPath}/${MetadataFolderName}`,
|
||||
`${collectionFolderPath}/${METADATA_FOLDER_NAME}`
|
||||
);
|
||||
collectionIDMap.set(collection.id, collectionFolderPath);
|
||||
}
|
||||
|
@ -137,8 +177,7 @@ class ExportService {
|
|||
if (this.stopExport || this.pauseExport) {
|
||||
if (this.pauseExport) {
|
||||
this.ElectronAPIs.showOnTray({
|
||||
export_progress:
|
||||
`${index} / ${files.length} files exported (paused)`,
|
||||
export_progress: `${index} / ${files.length} files exported (paused)`,
|
||||
paused: true,
|
||||
});
|
||||
}
|
||||
|
@ -147,39 +186,42 @@ class ExportService {
|
|||
const collectionPath = collectionIDMap.get(file.collectionID);
|
||||
try {
|
||||
await this.downloadAndSave(file, collectionPath);
|
||||
await this.addFileExportRecord(dir, file, RecordType.SUCCESS);
|
||||
await this.addFileExportRecord(
|
||||
dir,
|
||||
file,
|
||||
RecordType.SUCCESS
|
||||
);
|
||||
} catch (e) {
|
||||
await this.addFileExportRecord(dir, file, RecordType.FAILED);
|
||||
logError(e, 'download and save failed for file during export');
|
||||
await this.addFileExportRecord(
|
||||
dir,
|
||||
file,
|
||||
RecordType.FAILED
|
||||
);
|
||||
logError(
|
||||
e,
|
||||
'download and save failed for file during export'
|
||||
);
|
||||
}
|
||||
this.ElectronAPIs.showOnTray({
|
||||
export_progress:
|
||||
`${index + 1} / ${files.length} files exported`,
|
||||
export_progress: `${index + 1} / ${
|
||||
files.length
|
||||
} files exported`,
|
||||
});
|
||||
updateProgress({ current: index + 1, total: files.length });
|
||||
}
|
||||
if (this.stopExport) {
|
||||
this.ElectronAPIs.sendNotification(
|
||||
ExportNotification.ABORT,
|
||||
);
|
||||
this.ElectronAPIs.sendNotification(ExportNotification.ABORT);
|
||||
this.ElectronAPIs.showOnTray();
|
||||
} else if (this.pauseExport) {
|
||||
this.ElectronAPIs.sendNotification(
|
||||
ExportNotification.PAUSE,
|
||||
);
|
||||
this.ElectronAPIs.sendNotification(ExportNotification.PAUSE);
|
||||
return { paused: true };
|
||||
} else if (failedFileCount > 0) {
|
||||
this.ElectronAPIs.sendNotification(
|
||||
ExportNotification.FAILED,
|
||||
);
|
||||
this.ElectronAPIs.sendNotification(ExportNotification.FAILED);
|
||||
this.ElectronAPIs.showOnTray({
|
||||
retry_export:
|
||||
`export failed - retry export`,
|
||||
retry_export: `export failed - retry export`,
|
||||
});
|
||||
} else {
|
||||
this.ElectronAPIs.sendNotification(
|
||||
ExportNotification.FINISH,
|
||||
);
|
||||
this.ElectronAPIs.sendNotification(ExportNotification.FINISH);
|
||||
this.ElectronAPIs.showOnTray();
|
||||
}
|
||||
return { paused: false };
|
||||
|
@ -189,20 +231,25 @@ class ExportService {
|
|||
}
|
||||
async addFilesQueuedRecord(folder: string, files: File[]) {
|
||||
const exportRecord = await this.getExportRecord(folder);
|
||||
exportRecord.queuedFiles = files.map(getFileUID);
|
||||
exportRecord.queuedFiles = files.map(getExportRecordFileUID);
|
||||
await this.updateExportRecord(exportRecord, folder);
|
||||
}
|
||||
|
||||
async addFileExportRecord(folder: string, file: File, type: RecordType) {
|
||||
const fileUID = getFileUID(file);
|
||||
const fileUID = getExportRecordFileUID(file);
|
||||
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 (!exportRecord.exportedFiles) {
|
||||
exportRecord.exportedFiles = [];
|
||||
}
|
||||
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 {
|
||||
if (!exportRecord.failedFiles) {
|
||||
exportRecord.failedFiles = [];
|
||||
|
@ -226,7 +273,10 @@ class ExportService {
|
|||
}
|
||||
const exportRecord = await this.getExportRecord(folder);
|
||||
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) {
|
||||
logError(e, 'error updating Export Record');
|
||||
}
|
||||
|
@ -239,7 +289,9 @@ class ExportService {
|
|||
if (!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) {
|
||||
return JSON.parse(recordFile);
|
||||
} else {
|
||||
|
@ -250,15 +302,49 @@ class ExportService {
|
|||
}
|
||||
}
|
||||
|
||||
async downloadAndSave(file: File, collectionPath:string) {
|
||||
const uid = `${file.id}_${this.sanitizeName(
|
||||
file.metadata.title,
|
||||
)}`;
|
||||
const fileStream = await retryAsyncFunction(()=>downloadManager.downloadFile(file));
|
||||
this.ElectronAPIs.saveStreamToDisk(`${collectionPath}/${uid}`, fileStream);
|
||||
async downloadAndSave(file: File, collectionPath: string) {
|
||||
const uid = `${file.id}_${this.sanitizeName(file.metadata.title)}`;
|
||||
const fileStream = await retryAsyncFunction(() =>
|
||||
downloadManager.downloadFile(file)
|
||||
);
|
||||
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(
|
||||
`${collectionPath}/${MetadataFolderName}/${uid}.json`,
|
||||
getGoogleLikeMetadataFile(uid, file.metadata),
|
||||
`${collectionPath}/${METADATA_FOLDER_NAME}/${uid}.json`,
|
||||
getGoogleLikeMetadataFile(uid, metadata)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -268,6 +354,6 @@ class ExportService {
|
|||
|
||||
isExportInProgress = () => {
|
||||
return this.exportInProgress !== null;
|
||||
}
|
||||
};
|
||||
}
|
||||
export default new ExportService();
|
||||
|
|
|
@ -19,6 +19,13 @@ export interface fileAttribute {
|
|||
decryptionHeader: string;
|
||||
}
|
||||
|
||||
export enum FILE_TYPE {
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
LIVE_PHOTO,
|
||||
OTHERS,
|
||||
}
|
||||
|
||||
export interface File {
|
||||
id: number;
|
||||
collectionID: number;
|
||||
|
@ -43,7 +50,10 @@ export const getLocalFiles = async () => {
|
|||
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();
|
||||
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
||||
if (files.length !== localFiles.length) {
|
||||
|
@ -54,11 +64,19 @@ export const syncFiles = async (collections: Collection[], setFiles: (files: Fil
|
|||
if (!getToken()) {
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
const fetchedFiles = (await getFiles(collection, lastSyncTime, DIFF_LIMIT, files, setFiles)) ?? [];
|
||||
const fetchedFiles =
|
||||
(await getFiles(
|
||||
collection,
|
||||
lastSyncTime,
|
||||
DIFF_LIMIT,
|
||||
files,
|
||||
setFiles
|
||||
)) ?? [];
|
||||
files.push(...fetchedFiles);
|
||||
const latestVersionFiles = new Map<string, File>();
|
||||
files.forEach((file) => {
|
||||
|
@ -78,17 +96,19 @@ export const syncFiles = async (collections: Collection[], setFiles: (files: Fil
|
|||
}
|
||||
files.push(file);
|
||||
}
|
||||
files=sortFiles(files);
|
||||
files = sortFiles(files);
|
||||
await localForage.setItem('files', files);
|
||||
await localForage.setItem(
|
||||
`${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 {
|
||||
files: files.map((item) => ({
|
||||
|
@ -104,11 +124,12 @@ export const getFiles = async (
|
|||
sinceTime: number,
|
||||
limit: number,
|
||||
files: File[],
|
||||
setFiles: (files: File[]) => void,
|
||||
setFiles: (files: File[]) => void
|
||||
): Promise<File[]> => {
|
||||
try {
|
||||
const decryptedFiles: File[] = [];
|
||||
let time = sinceTime ||
|
||||
let time =
|
||||
sinceTime ||
|
||||
(await localForage.getItem<number>(`${collection.id}-time`)) ||
|
||||
0;
|
||||
let resp;
|
||||
|
@ -126,7 +147,7 @@ export const getFiles = async (
|
|||
},
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
decryptedFiles.push(
|
||||
|
@ -136,16 +157,21 @@ export const getFiles = async (
|
|||
file = await decryptFile(file, collection);
|
||||
}
|
||||
return file;
|
||||
}) as Promise<File>[],
|
||||
)),
|
||||
}) as Promise<File>[]
|
||||
))
|
||||
);
|
||||
|
||||
if (resp.data.diff.length) {
|
||||
time = resp.data.diff.slice(-1)[0].updationTime;
|
||||
}
|
||||
setFiles([...(files || []), ...decryptedFiles].filter((item) => !item.isDeleted).sort(
|
||||
(a, b) => b.metadata.creationTime - a.metadata.creationTime,
|
||||
));
|
||||
setFiles(
|
||||
[...(files || []), ...decryptedFiles]
|
||||
.filter((item) => !item.isDeleted)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
b.metadata.creationTime - a.metadata.creationTime
|
||||
)
|
||||
);
|
||||
} while (resp.data.diff.length === limit);
|
||||
return decryptedFiles;
|
||||
} catch (e) {
|
||||
|
@ -155,7 +181,7 @@ export const getFiles = async (
|
|||
|
||||
const removeDeletedCollectionFiles = async (
|
||||
collections: Collection[],
|
||||
files: File[],
|
||||
files: File[]
|
||||
) => {
|
||||
const syncedCollectionIds = new Set<number>();
|
||||
for (const collection of collections) {
|
||||
|
@ -168,7 +194,7 @@ const removeDeletedCollectionFiles = async (
|
|||
export const deleteFiles = async (
|
||||
filesToDelete: number[],
|
||||
clearSelection: Function,
|
||||
syncWithRemote: Function,
|
||||
syncWithRemote: Function
|
||||
) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
@ -181,7 +207,7 @@ export const deleteFiles = async (
|
|||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
},
|
||||
}
|
||||
);
|
||||
clearSelection();
|
||||
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;
|
||||
} if (date1) {
|
||||
}
|
||||
if (date1) {
|
||||
return [{ month: date1.getMonth() }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function searchLocation(
|
||||
searchPhrase: string,
|
||||
searchPhrase: string
|
||||
): Promise<LocationSearchResponse[]> {
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/search/location`,
|
||||
|
@ -48,7 +49,7 @@ export async function searchLocation(
|
|||
},
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
},
|
||||
}
|
||||
);
|
||||
return resp.data.results;
|
||||
}
|
||||
|
@ -75,7 +76,9 @@ export function getHolidaySuggestion(searchPhrase: string): Suggestion[] {
|
|||
value: { month: 11, date: 31 },
|
||||
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[] {
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import { getEndpoint } from 'utils/common/apiUtil';
|
||||
import HTTPService from './HTTPService';
|
||||
import EXIF from 'exif-js';
|
||||
import { File, fileAttribute } from './fileService';
|
||||
import { File, fileAttribute, FILE_TYPE } from './fileService';
|
||||
import { Collection } from './collectionService';
|
||||
import { FILE_TYPE, SetFiles } from 'pages/gallery';
|
||||
import { SetFiles } from 'pages/gallery';
|
||||
import { retryAsyncFunction, sleep } from 'utils/common';
|
||||
import {
|
||||
handleError,
|
||||
parseError,
|
||||
THUMBNAIL_GENERATION_FAILED,
|
||||
} from 'utils/common/errorUtil';
|
||||
import { handleError, parseError, CustomError } from 'utils/common/errorUtil';
|
||||
import { ComlinkWorker, getDedicatedCryptoWorker } from 'utils/crypto';
|
||||
import * as convert from 'xml-js';
|
||||
import { ENCRYPTION_CHUNK_SIZE } from 'types';
|
||||
|
@ -46,10 +42,12 @@ const FILE_UPLOAD_COMPLETED = 100;
|
|||
const EDITED_FILE_SUFFIX = '-edited';
|
||||
const TwoSecondInMillSeconds = 2000;
|
||||
|
||||
export enum FileUploadErrorCode {
|
||||
export enum FileUploadResults {
|
||||
FAILED = -1,
|
||||
SKIPPED = -2,
|
||||
UNSUPPORTED = -3,
|
||||
BLOCKED = -4,
|
||||
UPLOADED = 100,
|
||||
}
|
||||
|
||||
interface Location {
|
||||
|
@ -119,7 +117,7 @@ interface ProcessedFile {
|
|||
metadata: fileAttribute;
|
||||
filename: string;
|
||||
}
|
||||
interface BackupedFile extends Omit<ProcessedFile, 'filename'> { }
|
||||
interface BackupedFile extends Omit<ProcessedFile, 'filename'> {}
|
||||
|
||||
interface uploadFile extends BackupedFile {
|
||||
collectionID: number;
|
||||
|
@ -142,31 +140,34 @@ class UploadService {
|
|||
private filesCompleted: number;
|
||||
private totalFileCount: number;
|
||||
private fileProgress: Map<string, number>;
|
||||
private uploadResult: Map<string, number>;
|
||||
private metadataMap: Map<string, Object>;
|
||||
private filesToBeUploaded: FileWithCollection[];
|
||||
private progressBarProps;
|
||||
private failedFiles: FileWithCollection[];
|
||||
private existingFilesCollectionWise: Map<number, File[]>;
|
||||
private existingFiles: File[];
|
||||
private setFiles:SetFiles;
|
||||
private setFiles: SetFiles;
|
||||
public async uploadFiles(
|
||||
filesWithCollectionToUpload: FileWithCollection[],
|
||||
existingFiles: File[],
|
||||
progressBarProps,
|
||||
setFiles:SetFiles,
|
||||
setFiles: SetFiles
|
||||
) {
|
||||
try {
|
||||
progressBarProps.setUploadStage(UPLOAD_STAGES.START);
|
||||
|
||||
this.filesCompleted = 0;
|
||||
this.fileProgress = new Map<string, number>();
|
||||
this.uploadResult = new Map<string, number>();
|
||||
this.failedFiles = [];
|
||||
this.metadataMap = new Map<string, object>();
|
||||
this.progressBarProps = progressBarProps;
|
||||
this.existingFiles=existingFiles;
|
||||
this.existingFilesCollectionWise = sortFilesIntoCollections(existingFiles);
|
||||
this.existingFiles = existingFiles;
|
||||
this.existingFilesCollectionWise =
|
||||
sortFilesIntoCollections(existingFiles);
|
||||
this.updateProgressBarUI();
|
||||
this.setFiles=setFiles;
|
||||
this.setFiles = setFiles;
|
||||
const metadataFiles: globalThis.File[] = [];
|
||||
const actualFiles: FileWithCollection[] = [];
|
||||
filesWithCollectionToUpload.forEach((fileWithCollection) => {
|
||||
|
@ -184,7 +185,7 @@ class UploadService {
|
|||
this.filesToBeUploaded = actualFiles;
|
||||
|
||||
progressBarProps.setUploadStage(
|
||||
UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES,
|
||||
UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES
|
||||
);
|
||||
this.totalFileCount = metadataFiles.length;
|
||||
this.perFileProgress = 100 / metadataFiles.length;
|
||||
|
@ -211,20 +212,16 @@ class UploadService {
|
|||
}
|
||||
}
|
||||
const uploadProcesses = [];
|
||||
for (
|
||||
let i = 0;
|
||||
i < MAX_CONCURRENT_UPLOADS;
|
||||
i++
|
||||
) {
|
||||
if (this.filesToBeUploaded.length>0) {
|
||||
const fileWithCollection= this.filesToBeUploaded.pop();
|
||||
for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) {
|
||||
if (this.filesToBeUploaded.length > 0) {
|
||||
const fileWithCollection = this.filesToBeUploaded.pop();
|
||||
this.cryptoWorkers[i] = getDedicatedCryptoWorker();
|
||||
uploadProcesses.push(
|
||||
this.uploader(
|
||||
await new this.cryptoWorkers[i].comlink(),
|
||||
new FileReader(),
|
||||
fileWithCollection,
|
||||
),
|
||||
fileWithCollection
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -246,75 +243,97 @@ class UploadService {
|
|||
private async uploader(
|
||||
worker: any,
|
||||
reader: FileReader,
|
||||
fileWithCollection: FileWithCollection,
|
||||
fileWithCollection: FileWithCollection
|
||||
) {
|
||||
const { file: rawFile, collection } = fileWithCollection;
|
||||
this.fileProgress.set(rawFile.name, 0);
|
||||
this.updateProgressBarUI();
|
||||
let file: FileInMemory = null;
|
||||
let encryptedFile: EncryptedFile = null;
|
||||
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)) {
|
||||
// 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();
|
||||
await sleep(TwoSecondInMillSeconds);
|
||||
// remove completed files for file progress list
|
||||
this.fileProgress.delete(rawFile.name);
|
||||
} else {
|
||||
let encryptedFile: EncryptedFile =
|
||||
await this.encryptFile(worker, file, collection.key);
|
||||
encryptedFile = await this.encryptFile(
|
||||
worker,
|
||||
file,
|
||||
collection.key
|
||||
);
|
||||
|
||||
let backupedFile: BackupedFile = await this.uploadToBucket(
|
||||
encryptedFile.file,
|
||||
const backupedFile: BackupedFile = await this.uploadToBucket(
|
||||
encryptedFile.file
|
||||
);
|
||||
|
||||
let uploadFile: uploadFile = this.getUploadFile(
|
||||
collection,
|
||||
backupedFile,
|
||||
encryptedFile.fileKey,
|
||||
encryptedFile.fileKey
|
||||
);
|
||||
|
||||
encryptedFile = null;
|
||||
backupedFile = null;
|
||||
|
||||
const uploadedFile =await this.uploadFile(uploadFile);
|
||||
const decryptedFile=await decryptFile(uploadedFile, collection);
|
||||
const uploadedFile = await this.uploadFile(uploadFile);
|
||||
const decryptedFile = await decryptFile(
|
||||
uploadedFile,
|
||||
collection
|
||||
);
|
||||
|
||||
this.existingFiles.push(decryptedFile);
|
||||
this.existingFiles=sortFiles(this.existingFiles);
|
||||
await localForage.setItem('files', removeUnneccessaryFileProps(this.existingFiles));
|
||||
this.existingFiles = sortFiles(this.existingFiles);
|
||||
await localForage.setItem(
|
||||
'files',
|
||||
removeUnneccessaryFileProps(this.existingFiles)
|
||||
);
|
||||
this.setFiles(this.existingFiles);
|
||||
|
||||
file = null;
|
||||
uploadFile = null;
|
||||
|
||||
this.fileProgress.delete(rawFile.name);
|
||||
this.fileProgress.set(rawFile.name, FileUploadResults.UPLOADED);
|
||||
this.filesCompleted++;
|
||||
}
|
||||
} catch (e) {
|
||||
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);
|
||||
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();
|
||||
|
||||
if (this.filesToBeUploaded.length > 0) {
|
||||
await this.uploader(
|
||||
worker,
|
||||
reader,
|
||||
this.filesToBeUploaded.pop(),
|
||||
);
|
||||
await this.uploader(worker, reader, this.filesToBeUploaded.pop());
|
||||
}
|
||||
}
|
||||
async retryFailedFiles(localFiles:File[]) {
|
||||
await this.uploadFiles(this.failedFiles, localFiles, this.progressBarProps, this.setFiles);
|
||||
async retryFailedFiles(localFiles: File[]) {
|
||||
await this.uploadFiles(
|
||||
this.failedFiles,
|
||||
localFiles,
|
||||
this.progressBarProps,
|
||||
this.setFiles
|
||||
);
|
||||
}
|
||||
|
||||
private updateProgressBarUI() {
|
||||
const { setPercentComplete, setFileCounter, setFileProgress } =
|
||||
this.progressBarProps;
|
||||
const {
|
||||
setPercentComplete,
|
||||
setFileCounter,
|
||||
setFileProgress,
|
||||
setUploadResult,
|
||||
} = this.progressBarProps;
|
||||
setFileCounter({
|
||||
finished: this.filesCompleted,
|
||||
total: this.totalFileCount,
|
||||
|
@ -332,11 +351,12 @@ class UploadService {
|
|||
}
|
||||
setPercentComplete(percentComplete);
|
||||
setFileProgress(this.fileProgress);
|
||||
setUploadResult(this.uploadResult);
|
||||
}
|
||||
|
||||
private fileAlreadyInCollection(
|
||||
newFile: FileInMemory,
|
||||
collection: Collection,
|
||||
collection: Collection
|
||||
): boolean {
|
||||
const collectionFiles =
|
||||
this.existingFilesCollectionWise.get(collection.id) ?? [];
|
||||
|
@ -349,7 +369,7 @@ class UploadService {
|
|||
}
|
||||
private areFilesSame(
|
||||
existingFile: MetadataObject,
|
||||
newFile: MetadataObject,
|
||||
newFile: MetadataObject
|
||||
): boolean {
|
||||
if (
|
||||
existingFile.fileType === newFile.fileType &&
|
||||
|
@ -365,10 +385,8 @@ class UploadService {
|
|||
|
||||
private async readFile(reader: FileReader, receivedFile: globalThis.File) {
|
||||
try {
|
||||
const { thumbnail, hasStaticThumbnail } = await this.generateThumbnail(
|
||||
reader,
|
||||
receivedFile,
|
||||
);
|
||||
const { thumbnail, hasStaticThumbnail } =
|
||||
await this.generateThumbnail(reader, receivedFile);
|
||||
|
||||
let fileType: FILE_TYPE;
|
||||
switch (receivedFile.type.split('/')[0]) {
|
||||
|
@ -392,13 +410,13 @@ class UploadService {
|
|||
const { location, creationTime } = await this.getExifData(
|
||||
reader,
|
||||
receivedFile,
|
||||
fileType,
|
||||
fileType
|
||||
);
|
||||
let receivedFileOriginalName = receivedFile.name;
|
||||
if (receivedFile.name.endsWith(EDITED_FILE_SUFFIX)) {
|
||||
receivedFileOriginalName = receivedFile.name.slice(
|
||||
0,
|
||||
-1 * EDITED_FILE_SUFFIX.length,
|
||||
-1 * EDITED_FILE_SUFFIX.length
|
||||
);
|
||||
}
|
||||
const metadata = Object.assign(
|
||||
|
@ -411,15 +429,15 @@ class UploadService {
|
|||
longitude: location?.latitude,
|
||||
fileType,
|
||||
},
|
||||
this.metadataMap.get(receivedFileOriginalName),
|
||||
this.metadataMap.get(receivedFileOriginalName)
|
||||
);
|
||||
if (hasStaticThumbnail) {
|
||||
metadata['hasStaticThumbnail'] = hasStaticThumbnail;
|
||||
}
|
||||
const filedata =
|
||||
receivedFile.size > MIN_STREAM_FILE_SIZE ?
|
||||
this.getFileStream(reader, receivedFile) :
|
||||
await this.getUint8ArrayView(reader, receivedFile);
|
||||
receivedFile.size > MIN_STREAM_FILE_SIZE
|
||||
? this.getFileStream(reader, receivedFile)
|
||||
: await this.getUint8ArrayView(reader, receivedFile);
|
||||
|
||||
return {
|
||||
filedata,
|
||||
|
@ -435,13 +453,13 @@ class UploadService {
|
|||
private async encryptFile(
|
||||
worker: any,
|
||||
file: FileInMemory,
|
||||
encryptionKey: string,
|
||||
encryptionKey: string
|
||||
): Promise<EncryptedFile> {
|
||||
try {
|
||||
const { key: fileKey, file: encryptedFiledata }: EncryptionResult =
|
||||
isDataStream(file.filedata) ?
|
||||
await this.encryptFileStream(worker, file.filedata) :
|
||||
await worker.encryptFile(file.filedata);
|
||||
isDataStream(file.filedata)
|
||||
? await this.encryptFileStream(worker, file.filedata)
|
||||
: await worker.encryptFile(file.filedata);
|
||||
|
||||
const { file: encryptedThumbnail }: EncryptionResult =
|
||||
await worker.encryptThumbnail(file.thumbnail, fileKey);
|
||||
|
@ -450,7 +468,7 @@ class UploadService {
|
|||
|
||||
const encryptedKey: B64EncryptionResult = await worker.encryptToB64(
|
||||
fileKey,
|
||||
encryptionKey,
|
||||
encryptionKey
|
||||
);
|
||||
|
||||
const result: EncryptedFile = {
|
||||
|
@ -481,7 +499,7 @@ class UploadService {
|
|||
const encryptedFileChunk = await worker.encryptFileChunk(
|
||||
value,
|
||||
pushState,
|
||||
ref.pullCount === chunkCount,
|
||||
ref.pullCount === chunkCount
|
||||
);
|
||||
controller.enqueue(encryptedFileChunk);
|
||||
if (ref.pullCount === chunkCount) {
|
||||
|
@ -505,30 +523,30 @@ class UploadService {
|
|||
if (isDataStream(file.file.encryptedData)) {
|
||||
const { chunkCount, stream } = file.file.encryptedData;
|
||||
const uploadPartCount = Math.ceil(
|
||||
chunkCount / CHUNKS_COMBINED_FOR_UPLOAD,
|
||||
chunkCount / CHUNKS_COMBINED_FOR_UPLOAD
|
||||
);
|
||||
const filePartUploadURLs = await this.fetchMultipartUploadURLs(
|
||||
uploadPartCount,
|
||||
uploadPartCount
|
||||
);
|
||||
fileObjectKey = await this.putFileInParts(
|
||||
filePartUploadURLs,
|
||||
stream,
|
||||
file.filename,
|
||||
uploadPartCount,
|
||||
uploadPartCount
|
||||
);
|
||||
} else {
|
||||
const fileUploadURL = await this.getUploadURL();
|
||||
fileObjectKey = await this.putFile(
|
||||
fileUploadURL,
|
||||
file.file.encryptedData,
|
||||
file.filename,
|
||||
file.filename
|
||||
);
|
||||
}
|
||||
const thumbnailUploadURL = await this.getUploadURL();
|
||||
const thumbnailObjectKey = await this.putFile(
|
||||
thumbnailUploadURL,
|
||||
file.thumbnail.encryptedData as Uint8Array,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
const backupedFile: BackupedFile = {
|
||||
|
@ -552,7 +570,7 @@ class UploadService {
|
|||
private getUploadFile(
|
||||
collection: Collection,
|
||||
backupedFile: BackupedFile,
|
||||
fileKey: B64EncryptionResult,
|
||||
fileKey: B64EncryptionResult
|
||||
): uploadFile {
|
||||
const uploadFile: uploadFile = {
|
||||
collectionID: collection.id,
|
||||
|
@ -564,20 +582,17 @@ class UploadService {
|
|||
return uploadFile;
|
||||
}
|
||||
|
||||
private async uploadFile(uploadFile: uploadFile):Promise<File> {
|
||||
private async uploadFile(uploadFile: uploadFile): Promise<File> {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const response = await retryAsyncFunction(()=>HTTPService.post(
|
||||
`${ENDPOINT}/files`,
|
||||
uploadFile,
|
||||
null,
|
||||
{
|
||||
const response = await retryAsyncFunction(() =>
|
||||
HTTPService.post(`${ENDPOINT}/files`, uploadFile, null, {
|
||||
'X-Auth-Token': token,
|
||||
},
|
||||
));
|
||||
})
|
||||
);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
logError(e, 'upload Files Failed');
|
||||
|
@ -590,17 +605,19 @@ class UploadService {
|
|||
const metadataJSON: object = await new Promise(
|
||||
(resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onabort = () => reject(Error('file reading was aborted'));
|
||||
reader.onerror = () => reject(Error('file reading has failed'));
|
||||
reader.onabort = () =>
|
||||
reject(Error('file reading was aborted'));
|
||||
reader.onerror = () =>
|
||||
reject(Error('file reading has failed'));
|
||||
reader.onload = () => {
|
||||
const result =
|
||||
typeof reader.result !== 'string' ?
|
||||
new TextDecoder().decode(reader.result) :
|
||||
reader.result;
|
||||
typeof reader.result !== 'string'
|
||||
? new TextDecoder().decode(reader.result)
|
||||
: reader.result;
|
||||
resolve(JSON.parse(result));
|
||||
};
|
||||
reader.readAsText(receivedFile);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const metaDataObject = {};
|
||||
|
@ -647,8 +664,8 @@ class UploadService {
|
|||
}
|
||||
private async generateThumbnail(
|
||||
reader: FileReader,
|
||||
file: globalThis.File,
|
||||
): Promise<{ thumbnail: Uint8Array, hasStaticThumbnail: boolean }> {
|
||||
file: globalThis.File
|
||||
): Promise<{ thumbnail: Uint8Array; hasStaticThumbnail: boolean }> {
|
||||
try {
|
||||
let hasStaticThumbnail = false;
|
||||
const canvas = document.createElement('canvas');
|
||||
|
@ -662,7 +679,7 @@ class UploadService {
|
|||
file = new globalThis.File(
|
||||
[await convertHEIC2JPEG(file)],
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
let image = new Image();
|
||||
|
@ -672,7 +689,8 @@ class UploadService {
|
|||
image.onload = () => {
|
||||
try {
|
||||
const thumbnailWidth =
|
||||
(image.width * THUMBNAIL_HEIGHT) / image.height;
|
||||
(image.width * THUMBNAIL_HEIGHT) /
|
||||
image.height;
|
||||
canvas.width = thumbnailWidth;
|
||||
canvas.height = THUMBNAIL_HEIGHT;
|
||||
canvas_CTX.drawImage(
|
||||
|
@ -680,7 +698,7 @@ class UploadService {
|
|||
0,
|
||||
0,
|
||||
thumbnailWidth,
|
||||
THUMBNAIL_HEIGHT,
|
||||
THUMBNAIL_HEIGHT
|
||||
);
|
||||
image = null;
|
||||
clearTimeout(timeout);
|
||||
|
@ -688,15 +706,23 @@ class UploadService {
|
|||
} catch (e) {
|
||||
reject(e);
|
||||
logError(e);
|
||||
reject(Error(`${THUMBNAIL_GENERATION_FAILED} err: ${e}`));
|
||||
reject(
|
||||
Error(
|
||||
`${CustomError.THUMBNAIL_GENERATION_FAILED} err: ${e}`
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
timeout = setTimeout(
|
||||
() =>
|
||||
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 {
|
||||
|
@ -718,7 +744,7 @@ class UploadService {
|
|||
0,
|
||||
0,
|
||||
thumbnailWidth,
|
||||
THUMBNAIL_HEIGHT,
|
||||
THUMBNAIL_HEIGHT
|
||||
);
|
||||
video = null;
|
||||
clearTimeout(timeout);
|
||||
|
@ -726,16 +752,26 @@ class UploadService {
|
|||
} catch (e) {
|
||||
reject(e);
|
||||
logError(e);
|
||||
reject(Error(`${THUMBNAIL_GENERATION_FAILED} err: ${e}`));
|
||||
reject(
|
||||
Error(
|
||||
`${CustomError.THUMBNAIL_GENERATION_FAILED} err: ${e}`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
video.preload = 'metadata';
|
||||
video.src = imageURL;
|
||||
video.currentTime = 3;
|
||||
setTimeout(
|
||||
timeout = setTimeout(
|
||||
() =>
|
||||
reject(Error(`wait time exceeded for format ${file.name.split('.').slice(-1)[0]}`)),
|
||||
WAIT_TIME_THUMBNAIL_GENERATION,
|
||||
reject(
|
||||
Error(
|
||||
`wait time exceeded for format ${
|
||||
file.name.split('.').slice(-1)[0]
|
||||
}`
|
||||
)
|
||||
),
|
||||
WAIT_TIME_THUMBNAIL_GENERATION
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -758,7 +794,7 @@ class UploadService {
|
|||
resolve(blob);
|
||||
},
|
||||
'image/jpeg',
|
||||
quality,
|
||||
quality
|
||||
);
|
||||
});
|
||||
thumbnailBlob = thumbnailBlob ?? new Blob([]);
|
||||
|
@ -768,7 +804,7 @@ class UploadService {
|
|||
);
|
||||
const thumbnail = await this.getUint8ArrayView(
|
||||
reader,
|
||||
thumbnailBlob,
|
||||
thumbnailBlob
|
||||
);
|
||||
return { thumbnail, hasStaticThumbnail };
|
||||
} catch (e) {
|
||||
|
@ -781,7 +817,7 @@ class UploadService {
|
|||
const self = this;
|
||||
const fileChunkReader = (async function* fileChunkReaderMaker(
|
||||
fileSize,
|
||||
self,
|
||||
self
|
||||
) {
|
||||
let offset = 0;
|
||||
while (offset < fileSize) {
|
||||
|
@ -809,18 +845,19 @@ class UploadService {
|
|||
|
||||
private async getUint8ArrayView(
|
||||
reader: FileReader,
|
||||
file: Blob,
|
||||
file: Blob
|
||||
): Promise<Uint8Array> {
|
||||
try {
|
||||
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.onload = () => {
|
||||
// Do whatever you want with the file contents
|
||||
const result =
|
||||
typeof reader.result === 'string' ?
|
||||
new TextEncoder().encode(reader.result) :
|
||||
new Uint8Array(reader.result);
|
||||
typeof reader.result === 'string'
|
||||
? new TextEncoder().encode(reader.result)
|
||||
: new Uint8Array(reader.result);
|
||||
resolve(result);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
|
@ -851,10 +888,10 @@ class UploadService {
|
|||
{
|
||||
count: Math.min(
|
||||
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;
|
||||
this.uploadURLs.push(...response.data['urls']);
|
||||
|
@ -870,7 +907,7 @@ class UploadService {
|
|||
}
|
||||
|
||||
private async fetchMultipartUploadURLs(
|
||||
count: number,
|
||||
count: number
|
||||
): Promise<MultipartUploadURLs> {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
@ -882,7 +919,7 @@ class UploadService {
|
|||
{
|
||||
count,
|
||||
},
|
||||
{ 'X-Auth-Token': token },
|
||||
{ 'X-Auth-Token': token }
|
||||
);
|
||||
|
||||
return response.data['urls'];
|
||||
|
@ -895,17 +932,17 @@ class UploadService {
|
|||
private async putFile(
|
||||
fileUploadURL: UploadURL,
|
||||
file: Uint8Array,
|
||||
filename: string,
|
||||
filename: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
await retryAsyncFunction(()=>
|
||||
await retryAsyncFunction(() =>
|
||||
HTTPService.put(
|
||||
fileUploadURL.url,
|
||||
file,
|
||||
null,
|
||||
null,
|
||||
this.trackUploadProgress(filename),
|
||||
),
|
||||
this.trackUploadProgress(filename)
|
||||
)
|
||||
);
|
||||
return fileUploadURL.objectKey;
|
||||
} catch (e) {
|
||||
|
@ -918,12 +955,12 @@ class UploadService {
|
|||
multipartUploadURLs: MultipartUploadURLs,
|
||||
file: ReadableStream<Uint8Array>,
|
||||
filename: string,
|
||||
uploadPartCount: number,
|
||||
uploadPartCount: number
|
||||
) {
|
||||
try {
|
||||
const streamEncryptedFileReader = file.getReader();
|
||||
const percentPerPart = Math.round(
|
||||
RANDOM_PERCENTAGE_PROGRESS_FOR_PUT() / uploadPartCount,
|
||||
RANDOM_PERCENTAGE_PROGRESS_FOR_PUT() / uploadPartCount
|
||||
);
|
||||
const resParts = [];
|
||||
for (const [
|
||||
|
@ -942,15 +979,25 @@ class UploadService {
|
|||
}
|
||||
}
|
||||
const uploadChunk = Uint8Array.from(combinedChunks);
|
||||
const response = await retryAsyncFunction(()=>
|
||||
HTTPService.put(
|
||||
const response = await retryAsyncFunction(async () => {
|
||||
const resp = await HTTPService.put(
|
||||
fileUploadURL,
|
||||
uploadChunk,
|
||||
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({
|
||||
PartNumber: index + 1,
|
||||
ETag: response.headers.etag,
|
||||
|
@ -959,12 +1006,12 @@ class UploadService {
|
|||
const options = { compact: true, ignoreComment: true, spaces: 4 };
|
||||
const body = convert.js2xml(
|
||||
{ CompleteMultipartUpload: { Part: resParts } },
|
||||
options,
|
||||
options
|
||||
);
|
||||
await retryAsyncFunction(()=>
|
||||
await retryAsyncFunction(() =>
|
||||
HTTPService.post(multipartUploadURLs.completeURL, body, null, {
|
||||
'content-type': 'text/xml',
|
||||
}),
|
||||
})
|
||||
);
|
||||
return multipartUploadURLs.objectKey;
|
||||
} catch (e) {
|
||||
|
@ -976,15 +1023,15 @@ class UploadService {
|
|||
private trackUploadProgress(
|
||||
filename,
|
||||
percentPerPart = RANDOM_PERCENTAGE_PROGRESS_FOR_PUT(),
|
||||
index = 0,
|
||||
index = 0
|
||||
) {
|
||||
const cancel={ exec: null };
|
||||
let timeout=null;
|
||||
const resetTimeout=()=>{
|
||||
const cancel = { exec: null };
|
||||
let timeout = null;
|
||||
const resetTimeout = () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout=setTimeout(()=>cancel.exec(), 30*1000);
|
||||
timeout = setTimeout(() => cancel.exec(), 30 * 1000);
|
||||
};
|
||||
return {
|
||||
cancel,
|
||||
|
@ -995,14 +1042,14 @@ class UploadService {
|
|||
Math.min(
|
||||
Math.round(
|
||||
percentPerPart * index +
|
||||
(percentPerPart * event.loaded) /
|
||||
event.total,
|
||||
(percentPerPart * event.loaded) /
|
||||
event.total
|
||||
),
|
||||
98,
|
||||
),
|
||||
98
|
||||
)
|
||||
);
|
||||
this.updateProgressBarUI();
|
||||
if (event.loaded===event.total) {
|
||||
if (event.loaded === event.total) {
|
||||
clearTimeout(timeout);
|
||||
} else {
|
||||
resetTimeout();
|
||||
|
@ -1013,7 +1060,7 @@ class UploadService {
|
|||
private async getExifData(
|
||||
reader: FileReader,
|
||||
receivedFile: globalThis.File,
|
||||
fileType: FILE_TYPE,
|
||||
fileType: FILE_TYPE
|
||||
): Promise<ParsedEXIFData> {
|
||||
try {
|
||||
if (fileType === FILE_TYPE.VIDEO) {
|
||||
|
@ -1039,15 +1086,16 @@ class UploadService {
|
|||
}
|
||||
}
|
||||
private getUNIXTime(exifData: any) {
|
||||
const dateString: string = exifData.DateTimeOriginal || exifData.DateTime;
|
||||
if (!dateString || dateString==='0000:00:00 00:00:00') {
|
||||
const dateString: string =
|
||||
exifData.DateTimeOriginal || exifData.DateTime;
|
||||
if (!dateString || dateString === '0000:00:00 00:00:00') {
|
||||
return null;
|
||||
}
|
||||
const parts = dateString.split(' ')[0].split(':');
|
||||
const date = new Date(
|
||||
Number(parts[0]),
|
||||
Number(parts[1]) - 1,
|
||||
Number(parts[2]),
|
||||
Number(parts[2])
|
||||
);
|
||||
return date.getTime() * 1000;
|
||||
}
|
||||
|
@ -1072,14 +1120,14 @@ class UploadService {
|
|||
latDegree,
|
||||
latMinute,
|
||||
latSecond,
|
||||
latDirection,
|
||||
latDirection
|
||||
);
|
||||
|
||||
const lonFinal = this.convertDMSToDD(
|
||||
lonDegree,
|
||||
lonMinute,
|
||||
lonSecond,
|
||||
lonDirection,
|
||||
lonDirection
|
||||
);
|
||||
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 { getToken } from 'utils/common/key';
|
||||
import HTTPService from './HTTPService';
|
||||
import { B64EncryptionResult } from './uploadService';
|
||||
import { B64EncryptionResult } from 'utils/crypto';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { Subscription } from './billingService';
|
||||
|
||||
export interface UpdatedKey {
|
||||
kekSalt: string;
|
||||
|
@ -35,7 +36,7 @@ export interface EmailVerificationResponse {
|
|||
keyAttributes?: KeyAttributes;
|
||||
encryptedToken?: string;
|
||||
token?: string;
|
||||
twoFactorSessionID: string
|
||||
twoFactorSessionID: string;
|
||||
}
|
||||
|
||||
export interface TwoFactorVerificationResponse {
|
||||
|
@ -46,19 +47,28 @@ export interface TwoFactorVerificationResponse {
|
|||
}
|
||||
|
||||
export interface TwoFactorSecret {
|
||||
secretCode: string
|
||||
qrCode: string
|
||||
secretCode: string;
|
||||
qrCode: string;
|
||||
}
|
||||
|
||||
export interface TwoFactorRecoveryResponse {
|
||||
encryptedSecret: string
|
||||
secretDecryptionNonce: string
|
||||
encryptedSecret: string;
|
||||
secretDecryptionNonce: string;
|
||||
}
|
||||
|
||||
export const getOtt = (email: string) => HTTPService.get(`${ENDPOINT}/users/ott`, {
|
||||
email,
|
||||
client: 'web',
|
||||
});
|
||||
export interface UserDetails {
|
||||
email: string;
|
||||
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) => {
|
||||
const token = getToken();
|
||||
|
||||
|
@ -67,7 +77,7 @@ export const getPublicKey = async (email: string) => {
|
|||
{ email },
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
},
|
||||
}
|
||||
);
|
||||
return resp.data.publicKey;
|
||||
};
|
||||
|
@ -80,34 +90,28 @@ export const getPaymentToken = async () => {
|
|||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
},
|
||||
}
|
||||
);
|
||||
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(
|
||||
`${ENDPOINT}/users/attributes`,
|
||||
{ keyAttributes },
|
||||
null,
|
||||
{
|
||||
export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
|
||||
HTTPService.put(`${ENDPOINT}/users/attributes`, { keyAttributes }, null, {
|
||||
'X-Auth-Token': token,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
export const setKeys = (token: string, updatedKey: UpdatedKey) => HTTPService.put(`${ENDPOINT}/users/keys`, updatedKey, null, {
|
||||
'X-Auth-Token': token,
|
||||
});
|
||||
|
||||
export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) => HTTPService.put(
|
||||
`${ENDPOINT}/users/recovery-key`,
|
||||
recoveryKey,
|
||||
null,
|
||||
{
|
||||
export const setKeys = (token: string, updatedKey: UpdatedKey) =>
|
||||
HTTPService.put(`${ENDPOINT}/users/keys`, updatedKey, null, {
|
||||
'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 () => {
|
||||
// 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 () => {
|
||||
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/setup`, null, null, {
|
||||
'X-Auth-Token': getToken(),
|
||||
});
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/two-factor/setup`,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
}
|
||||
);
|
||||
return resp.data as TwoFactorSecret;
|
||||
};
|
||||
|
||||
export const enableTwoFactor = async (code: string, recoveryEncryptedTwoFactorSecret: B64EncryptionResult) => {
|
||||
await HTTPService.post(`${ENDPOINT}/users/two-factor/enable`, {
|
||||
code,
|
||||
encryptedTwoFactorSecret: recoveryEncryptedTwoFactorSecret.encryptedData,
|
||||
twoFactorSecretDecryptionNonce: recoveryEncryptedTwoFactorSecret.nonce,
|
||||
}, null, {
|
||||
'X-Auth-Token': getToken(),
|
||||
});
|
||||
export const enableTwoFactor = async (
|
||||
code: string,
|
||||
recoveryEncryptedTwoFactorSecret: B64EncryptionResult
|
||||
) => {
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/users/two-factor/enable`,
|
||||
{
|
||||
code,
|
||||
encryptedTwoFactorSecret:
|
||||
recoveryEncryptedTwoFactorSecret.encryptedData,
|
||||
twoFactorSecretDecryptionNonce:
|
||||
recoveryEncryptedTwoFactorSecret.nonce,
|
||||
},
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const verifyTwoFactor = async (code: string, sessionID: string) => {
|
||||
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/verify`, {
|
||||
code, sessionID,
|
||||
}, null);
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/two-factor/verify`,
|
||||
{
|
||||
code,
|
||||
sessionID,
|
||||
},
|
||||
null
|
||||
);
|
||||
return resp.data as TwoFactorVerificationResponse;
|
||||
};
|
||||
|
||||
|
@ -167,7 +191,8 @@ export const recoverTwoFactor = async (sessionID: string) => {
|
|||
|
||||
export const removeTwoFactor = async (sessionID: string, secret: string) => {
|
||||
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/remove`, {
|
||||
sessionID, secret,
|
||||
sessionID,
|
||||
secret,
|
||||
});
|
||||
return resp.data as TwoFactorVerificationResponse;
|
||||
};
|
||||
|
@ -179,9 +204,13 @@ export const disableTwoFactor = async () => {
|
|||
};
|
||||
|
||||
export const getTwoFactorStatus = async () => {
|
||||
const resp = await HTTPService.get(`${ENDPOINT}/users/two-factor/status`, null, {
|
||||
'X-Auth-Token': getToken(),
|
||||
});
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/two-factor/status`,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
}
|
||||
);
|
||||
return resp.data['status'];
|
||||
};
|
||||
|
||||
|
@ -197,3 +226,40 @@ export const _logout = async () => {
|
|||
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 DATE_CONTAINER_HEIGHT = 48;
|
||||
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 SPACE_BTW_DATES = 44;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { NextRouter } from 'next/router';
|
|||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import { SetLoading } from 'pages/gallery';
|
||||
import { getData, LS_KEYS } from './storage/localStorage';
|
||||
import { SUBSCRIPTION_VERIFICATION_ERROR } from './common/errorUtil';
|
||||
import { CustomError } from './common/errorUtil';
|
||||
|
||||
const STRIPE = 'stripe';
|
||||
|
||||
|
@ -17,14 +17,14 @@ export function convertBytesToGBs(bytes, precision?): string {
|
|||
return (bytes / (1024 * 1024 * 1024)).toFixed(precision ?? 2);
|
||||
}
|
||||
|
||||
export function convertToHumanReadable(bytes:number, precision=2): string {
|
||||
if (bytes===0) {
|
||||
export function convertToHumanReadable(bytes: number, precision = 2): string {
|
||||
if (bytes === 0) {
|
||||
return '0 MB';
|
||||
}
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
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) {
|
||||
|
@ -89,7 +89,7 @@ export async function updateSubscription(
|
|||
plan: Plan,
|
||||
setDialogMessage: SetDialogMessage,
|
||||
setLoading: SetLoading,
|
||||
closePlanSelectorModal: () => null,
|
||||
closePlanSelectorModal: () => null
|
||||
) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
@ -99,7 +99,7 @@ export async function updateSubscription(
|
|||
setDialogMessage({
|
||||
title: constants.SUCCESS,
|
||||
content: constants.SUBSCRIPTION_PURCHASE_SUCCESS(
|
||||
getUserSubscription().expiryTime,
|
||||
getUserSubscription().expiryTime
|
||||
),
|
||||
close: { variant: 'success' },
|
||||
});
|
||||
|
@ -117,13 +117,13 @@ export async function updateSubscription(
|
|||
null,
|
||||
|
||||
setDialogMessage,
|
||||
setLoading,
|
||||
setLoading
|
||||
),
|
||||
},
|
||||
close: { text: constants.CANCEL },
|
||||
});
|
||||
break;
|
||||
case SUBSCRIPTION_VERIFICATION_ERROR:
|
||||
case CustomError.SUBSCRIPTION_VERIFICATION_ERROR:
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
content: constants.SUBSCRIPTION_VERIFICATION_FAILED,
|
||||
|
@ -146,7 +146,7 @@ export async function updateSubscription(
|
|||
export async function cancelSubscription(
|
||||
setDialogMessage: SetDialogMessage,
|
||||
closePlanSelectorModal: () => null,
|
||||
setLoading: SetLoading,
|
||||
setLoading: SetLoading
|
||||
) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
@ -171,7 +171,7 @@ export async function cancelSubscription(
|
|||
export async function activateSubscription(
|
||||
setDialogMessage: SetDialogMessage,
|
||||
closePlanSelectorModal: () => null,
|
||||
setLoading: SetLoading,
|
||||
setLoading: SetLoading
|
||||
) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
@ -195,7 +195,7 @@ export async function activateSubscription(
|
|||
|
||||
export async function updatePaymentMethod(
|
||||
setDialogMessage: SetDialogMessage,
|
||||
setLoading: SetLoading,
|
||||
setLoading: SetLoading
|
||||
) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
@ -213,7 +213,7 @@ export async function updatePaymentMethod(
|
|||
|
||||
export async function checkSubscriptionPurchase(
|
||||
setDialogMessage: SetDialogMessage,
|
||||
router: NextRouter,
|
||||
router: NextRouter
|
||||
) {
|
||||
try {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
@ -227,19 +227,19 @@ export async function checkSubscriptionPurchase(
|
|||
} else if (sessionId) {
|
||||
try {
|
||||
const subscription = await billingService.verifySubscription(
|
||||
sessionId,
|
||||
sessionId
|
||||
);
|
||||
setDialogMessage({
|
||||
title: constants.SUBSCRIPTION_PURCHASE_SUCCESS_TITLE,
|
||||
close: { variant: 'success' },
|
||||
content: constants.SUBSCRIPTION_PURCHASE_SUCCESS(
|
||||
subscription?.expiryTime,
|
||||
subscription?.expiryTime
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
content: SUBSCRIPTION_VERIFICATION_ERROR,
|
||||
content: CustomError.SUBSCRIPTION_VERIFICATION_ERROR,
|
||||
close: {},
|
||||
});
|
||||
}
|
||||
|
@ -250,3 +250,18 @@ export async function checkSubscriptionPurchase(
|
|||
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>,
|
||||
selectCollection: (id: number) => void,
|
||||
collectionName: string,
|
||||
existingCollection: Collection,
|
||||
existingCollection: Collection
|
||||
) {
|
||||
setCollectionSelectorView(false);
|
||||
let collection;
|
||||
if (!existingCollection) {
|
||||
collection = await createCollection(
|
||||
collectionName,
|
||||
CollectionType.album,
|
||||
CollectionType.album
|
||||
);
|
||||
} else {
|
||||
collection = existingCollection;
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
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;
|
||||
};
|
||||
|
||||
export const getFileUrl = (id: number) => {
|
||||
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}`;
|
||||
};
|
||||
|
||||
export const getThumbnailUrl = (id: number) => {
|
||||
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}`;
|
||||
};
|
||||
|
|
|
@ -1,45 +1,43 @@
|
|||
import constants from 'utils/strings/constants';
|
||||
|
||||
export const errorCodes = {
|
||||
ERR_STORAGE_LIMIT_EXCEEDED: '426',
|
||||
ERR_NO_ACTIVE_SUBSCRIPTION: '402',
|
||||
ERR_NO_INTERNET_CONNECTION: '1',
|
||||
ERR_SESSION_EXPIRED: '401',
|
||||
ERR_KEY_MISSING: '2',
|
||||
ERR_FORBIDDEN: '403',
|
||||
export const ServerErrorCodes = {
|
||||
SESSION_EXPIRED: '401',
|
||||
NO_ACTIVE_SUBSCRIPTION: '402',
|
||||
FORBIDDEN: '403',
|
||||
STORAGE_LIMIT_EXCEEDED: '426',
|
||||
};
|
||||
|
||||
|
||||
export const SUBSCRIPTION_VERIFICATION_ERROR = 'Subscription verification failed';
|
||||
|
||||
export const THUMBNAIL_GENERATION_FAILED = 'thumbnail generation failed';
|
||||
export const VIDEO_PLAYBACK_FAILED = 'video playback failed';
|
||||
export const CustomError = {
|
||||
SUBSCRIPTION_VERIFICATION_ERROR: 'Subscription verification failed',
|
||||
THUMBNAIL_GENERATION_FAILED: 'thumbnail generation failed',
|
||||
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) {
|
||||
let errorMessage = null;
|
||||
let parsedMessage = null;
|
||||
if (error?.status) {
|
||||
const errorCode = error.status.toString();
|
||||
switch (errorCode) {
|
||||
case errorCodes.ERR_NO_ACTIVE_SUBSCRIPTION:
|
||||
errorMessage = constants.SUBSCRIPTION_EXPIRED;
|
||||
case ServerErrorCodes.NO_ACTIVE_SUBSCRIPTION:
|
||||
parsedMessage = constants.SUBSCRIPTION_EXPIRED;
|
||||
break;
|
||||
case errorCodes.ERR_STORAGE_LIMIT_EXCEEDED:
|
||||
errorMessage = constants.STORAGE_QUOTA_EXCEEDED;
|
||||
case ServerErrorCodes.STORAGE_LIMIT_EXCEEDED:
|
||||
parsedMessage = constants.STORAGE_QUOTA_EXCEEDED;
|
||||
break;
|
||||
case errorCodes.ERR_NO_INTERNET_CONNECTION:
|
||||
errorMessage = constants.NO_INTERNET_CONNECTION;
|
||||
break;
|
||||
case errorCodes.ERR_SESSION_EXPIRED:
|
||||
errorMessage = constants.SESSION_EXPIRED_MESSAGE;
|
||||
case ServerErrorCodes.SESSION_EXPIRED:
|
||||
parsedMessage = constants.SESSION_EXPIRED_MESSAGE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (errorMessage) {
|
||||
return { parsedError: new Error(errorMessage), parsed: true };
|
||||
if (parsedMessage) {
|
||||
return { parsedError: new Error(parsedMessage), parsed: true };
|
||||
} else {
|
||||
return ({
|
||||
parsedError: new Error(`${constants.UNKNOWN_ERROR} ${error}`), parsed: false,
|
||||
});
|
||||
return {
|
||||
parsedError: new Error(`${constants.UNKNOWN_ERROR} ${error}`),
|
||||
parsed: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,6 +46,6 @@ export function handleError(error) {
|
|||
if (parsed) {
|
||||
throw parsedError;
|
||||
} 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';
|
||||
|
||||
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];
|
||||
|
||||
|
@ -32,7 +33,10 @@ export function reverseString(title: string) {
|
|||
.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 {
|
||||
const resp = await func();
|
||||
return resp;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue