fix build

This commit is contained in:
Abhinav-grd 2021-08-11 13:30:59 +05:30
parent a6652c105a
commit d65f6cb494
19 changed files with 1034 additions and 629 deletions

View file

@ -1,4 +1,3 @@
import { Formik, FormikHelpers } from 'formik';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { Button, Col, Form, FormControl } from 'react-bootstrap';
@ -13,10 +12,10 @@ import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
interface formValues {
email: string;
ott?:string;
ott?: string;
}
const EmailRow =styled.div`
const EmailRow = styled.div`
display: flex;
flex-wrap: wrap;
border: 1px solid grey;
@ -26,15 +25,15 @@ const EmailRow =styled.div`
color: #fff;
`;
interface Props{
showMessage:(value:boolean)=>void;
setEmail:(email:string)=>void;
interface Props {
showMessage: (value: boolean) => void;
setEmail: (email: string) => void;
}
function ChangeEmailForm(props:Props) {
const [loading, setLoading]=useState(false);
const [ottInputVisible, setShowOttInputVisibility]=useState(false);
function ChangeEmailForm(props: Props) {
const [loading, setLoading] = useState(false);
const [ottInputVisible, setShowOttInputVisibility] = useState(false);
const emailInputElement = useRef(null);
const ottInputRef=useRef(null);
const ottInputRef = useRef(null);
const appContext = useContext(AppContext);
useEffect(() => {
@ -43,14 +42,16 @@ function ChangeEmailForm(props:Props) {
}, 250);
}, []);
useEffect(()=>{
useEffect(() => {
if (!ottInputVisible) {
props.showMessage(false);
}
}, [ottInputVisible]);
const requestOTT= async( { email }: formValues,
{ setFieldError }: FormikHelpers<formValues>)=>{
const requestOTT = async (
{ email }: formValues,
{ setFieldError }: FormikHelpers<formValues>,
) => {
try {
setLoading(true);
await getOTTForEmailChange(email);
@ -66,14 +67,18 @@ function ChangeEmailForm(props:Props) {
setLoading(false);
};
const requestEmailChange= async( { email, ott }: formValues,
{ setFieldError }: FormikHelpers<formValues>)=>{
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, severity: 'success' });
appContext.setDisappearingFlashMessage({
message: constants.EMAIL_UDPATE_SUCCESSFUL,
severity: 'success',
});
router.push('/gallery');
} catch (e) {
setFieldError('ott', `${constants.INCORRECT_CODE}`);
@ -91,17 +96,10 @@ function ChangeEmailForm(props:Props) {
})}
validateOnChange={false}
validateOnBlur={false}
onSubmit={!ottInputVisible?requestOTT:requestEmailChange}
>
{({
values,
errors,
touched,
handleChange,
handleSubmit,
}) => (
onSubmit={!ottInputVisible ? requestOTT : requestEmailChange}>
{({ values, errors, touched, handleChange, handleSubmit }) => (
<Form noValidate onSubmit={handleSubmit}>
{!ottInputVisible ?
{!ottInputVisible ? (
<Form.Group controlId="formBasicEmail">
<Form.Control
ref={emailInputElement}
@ -118,14 +116,17 @@ function ChangeEmailForm(props:Props) {
<FormControl.Feedback type="invalid">
{errors.email}
</FormControl.Feedback>
</Form.Group> :
</Form.Group>
) : (
<>
<EmailRow>
<Col xs="8">
{values.email}
</Col>
<Col xs ="4" >
<Button variant="link" onClick={()=>setShowOttInputVisibility(false)}>
<Col xs="8">{values.email}</Col>
<Col xs="4">
<Button
variant="link"
onClick={() =>
setShowOttInputVisibility(false)
}>
{constants.CHANGE}
</Button>
</Col>
@ -146,14 +147,23 @@ function ChangeEmailForm(props:Props) {
{errors.ott}
</FormControl.Feedback>
</Form.Group>
</>}
</>
)}
<SubmitButton
buttonText={!ottInputVisible?constants.SEND_OTT:constants.VERIFY}
buttonText={
!ottInputVisible
? constants.SEND_OTT
: constants.VERIFY
}
loading={loading}
/>
<br />
<Button block variant="link" className="text-center" onClick={router.back}>
<Button
block
variant="link"
className="text-center"
onClick={router.back}>
{constants.GO_BACK}
</Button>
</Form>

View file

@ -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>
</>
);

View file

@ -5,42 +5,83 @@ 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>
</>

View file

@ -1,7 +1,12 @@
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';
@ -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,21 @@ 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(getFileUID(file)),
);
exportService.addFilesQueuedRecord(
exportFolder,
unExportedFiles,
);
updateExportStage(ExportStage.PAUSED);
}
}
@ -107,7 +134,6 @@ export default function ExportModal(props: Props) {
main();
}, [props.show]);
useEffect(() => {
setExportSize(props.usage);
}, [props.usage]);
@ -162,7 +188,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 +213,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 +230,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 +258,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 +270,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 +295,8 @@ export default function ExportModal(props: Props) {
/>
);
default: return (<></>);
default:
return <></>;
}
};
@ -269,34 +306,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%', width: '450px' }}>
}}>
<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}` : <InProgressIcon />}</Value>
<Label width="40%">{constants.TOTAL_EXPORT_SIZE} </Label>
<Value width="60%">
{exportSize ? `${exportSize}` : <InProgressIcon />}
</Value>
</Row>
</div>
<ExportDynamicState />
</MessageDialog >
</MessageDialog>
);
}

View file

@ -21,8 +21,12 @@ import { isInsideBox, isSameDay as isSameDayAnyYear } from 'utils/search';
import { SetDialogMessage } from './MessageDialog';
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 +72,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 +146,7 @@ interface Props {
search: Search;
setSearchStats: setSearchStats;
deleted?: number[];
setDialogMessage: SetDialogMessage
setDialogMessage: SetDialogMessage;
}
const PhotoFrame = ({
@ -303,14 +310,13 @@ const PhotoFrame = ({
video.preload = 'metadata';
video.src = url;
video.currentTime = 3;
const t = setTimeout(
() => {
reject(
Error(`${CustomError.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 +338,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,
@ -397,11 +404,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 +416,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 +432,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 +456,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 +491,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 +501,7 @@ const PhotoFrame = ({
paddingRight: '32px',
paddingTop: '12px',
paddingBottom: '12px',
}}
>
}}>
{constants.UPLOAD_FIRST_PHOTO}
</Button>
</EmptyScreen>
@ -493,7 +509,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,27 +524,36 @@ 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(
)
? 'Today'
: isSameDay(
new Date(currentDate),
new Date(Date.now() - A_DAY),
) ?
'Yesterday' :
dateTimeFormat.format(
)
? 'Yesterday'
: dateTimeFormat.format(
currentDate,
),
id: currentDate.toString(),
@ -553,7 +580,10 @@ const PhotoFrame = ({
});
if (!skipMerge) {
timeStampList = mergeTimeStampList(timeStampList, columns);
timeStampList = mergeTimeStampList(
timeStampList,
columns,
);
}
const getItemSize = (index) => {
@ -567,68 +597,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 +699,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>
)}

View file

@ -36,7 +36,7 @@ import { Subscription } from 'services/billingService';
interface Props {
collections: Collection[];
setDialogMessage: SetDialogMessage;
setLoading: SetLoading,
setLoading: SetLoading;
showPlanSelectorModal: () => void;
}
export default function Sidebar(props: Props) {
@ -56,18 +56,23 @@ export default function Sidebar(props: Props) {
if (!isOpen) {
return;
}
const userDetails=await getUserDetails();
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.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();
}
@ -108,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"
@ -122,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' }}>
@ -155,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>
@ -172,7 +185,9 @@ export default function Sidebar(props: Props) {
{usage ? (
constants.USAGE_INFO(
usage,
Number(convertBytesToGBs(subscription?.storage)),
Number(
convertBytesToGBs(subscription?.storage),
),
)
) : (
<div style={{ textAlign: 'center' }}>
@ -197,29 +212,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>
</>
@ -233,8 +247,7 @@ export default function Sidebar(props: Props) {
/>
<LinkButton
style={{ marginTop: '30px' }}
onClick={() => setTwoFactorModalView(true)}
>
onClick={() => setTwoFactorModalView(true)}>
{constants.TWO_FACTOR}
</LinkButton>
</>
@ -243,8 +256,7 @@ export default function Sidebar(props: Props) {
onClick={() => {
setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
router.push('change-password');
}}
>
}}>
{constants.CHANGE_PASSWORD}
</LinkButton>
<LinkButton
@ -252,18 +264,24 @@ export default function Sidebar(props: Props) {
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>
</>
@ -278,18 +296,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
@ -299,6 +318,6 @@ export default function Sidebar(props: Props) {
}}
/>
</div>
</Menu >
</Menu>
);
}

View file

@ -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,
severity: 'info',
});
} catch (e) {
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_DISABLE_FAILED, severity: 'danger' });
appContext.setDisappearingFlashMessage({
message: constants.TWO_FACTOR_DISABLE_FAILED,
severity: '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;

View file

@ -79,19 +79,24 @@ function PlanSelector(props: Props) {
const [plans, setPlans] = useState(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 ( props.modalView) {
if (props.modalView) {
const main = async () => {
props.setLoading(true);
let plans=await billingService.getPlans();
let plans = await billingService.getPlans();
const planNotListed= plans.filter((plan)=>isUserSubscribedPlan(plan, subscription)).length===0;
const planNotListed =
plans.filter((plan) =>
isUserSubscribedPlan(plan, subscription),
).length === 0;
if (!isOnFreePlan(subscription) && planNotListed) {
plans=[planForSubscription(subscription), ...plans];
plans = [planForSubscription(subscription), ...plans];
}
setPlans(plans);
props.setLoading(false);
@ -154,8 +159,7 @@ function PlanSelector(props: Props) {
key={plan.stripeID}
className="subscription-plan-selector"
selected={isUserSubscribedPlan(plan, subscription)}
onClick={async () => (await onPlanSelect(plan))}
>
onClick={async () => await onPlanSelect(plan)}>
<div>
<span
style={{
@ -163,8 +167,7 @@ function PlanSelector(props: Props) {
fontWeight: 900,
fontSize: '40px',
lineHeight: '40px',
}}
>
}}>
{convertBytesToGBs(plan.storage, 0)}
</span>
<span
@ -172,24 +175,30 @@ 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: '20px', fontSize: '14px', display: 'flex', justifyContent: 'center' }}
disabled={isUserSubscribedPlan(plan, subscription)}
>
style={{
marginTop: '20px',
fontSize: '14px',
display: 'flex',
justifyContent: 'center',
}}
disabled={isUserSubscribedPlan(plan, subscription)}>
{constants.CHOOSE_PLAN_BTN}
<ArrowEast style={{ marginLeft: '5px' }} />
</Button>
@ -202,20 +211,18 @@ function PlanSelector(props: Props) {
size="xl"
centered
backdrop={hasPaidSubscription(subscription) ? 'true' : 'static'}
contentClassName="plan-selector-modal-content"
>
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>
@ -224,22 +231,23 @@ function PlanSelector(props: Props) {
<div style={{ display: 'flex' }}>
<span
className="bold-text"
style={{ fontSize: '16px' }}
>
style={{ fontSize: '16px' }}>
{constants.MONTHLY}
</span>
<Form.Switch
checked={planPeriod === PLAN_PERIOD.YEAR}
id="plan-period-toggler"
style={{ margin: '-4px 0 20px 15px', fontSize: '10px' }}
style={{
margin: '-4px 0 20px 15px',
fontSize: '10px',
}}
className="custom-switch-md"
onChange={togglePeriod}
/>
<span
className="bold-text"
style={{ fontSize: '16px' }}
>
style={{ fontSize: '16px' }}>
{constants.YEARLY}
</span>
</div>
@ -251,8 +259,7 @@ function PlanSelector(props: Props) {
flexWrap: 'wrap',
minHeight: '212px',
margin: '5px 0',
}}
>
}}>
{plans && PlanIcons}
</div>
<DeadCenter style={{ marginBottom: '30px' }}>
@ -261,55 +268,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>
)}
@ -320,8 +327,7 @@ function PlanSelector(props: Props) {
props.setDialogMessage,
props.setLoading,
)}
style={{ marginTop: '20px' }}
>
style={{ marginTop: '20px' }}>
{constants.MANAGEMENT_PORTAL}
</LinkButton>
</>
@ -329,11 +335,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>

View file

@ -397,22 +397,22 @@ export interface BannerMessage {
variant: string;
}
type AppContextType = {
showNavBar: (show: boolean) => void;
sharedFiles: File[];
resetSharedFiles: () => void;
setDisappearingFlashMessage: (message: FlashMessage) => void;
}
};
export interface FlashMessage {
message: string;
severity: string
severity: string;
}
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 }) {
@ -486,7 +486,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,
);
}
}
});
@ -513,24 +515,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' }}
@ -539,21 +544,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>

View file

@ -9,15 +9,14 @@ 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);
const [email, setEmail] = useState('');
const [waiting, setWaiting] = useState(true);
const [showMessage, setShowMessage] = useState(false);
const [showBigDialog, setShowBigDialog] = useState(false);
useEffect(() => {
const token=getToken();
const token = getToken();
if (!token) {
router.push('/');
return;
@ -25,39 +24,40 @@ function ChangeEmailPage() {
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>);
<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;

View file

@ -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,
@ -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
}
/>
);

View file

@ -20,7 +20,7 @@ const Container = styled.div`
justify-content: center;
background-color: #000;
@media(max-width: 1024px) {
@media (max-width: 1024px) {
flex-direction: column;
}
`;
@ -33,7 +33,7 @@ const SlideContainer = styled.div`
justify-content: center;
text-align: center;
@media(max-width: 1024px) {
@media (max-width: 1024px) {
flex-grow: 0;
}
`;
@ -47,7 +47,7 @@ const DesktopBox = styled.div`
justify-content: center;
background-color: #242424;
@media(max-width: 1024px) {
@media (max-width: 1024px) {
display: none;
}
`;
@ -55,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;
@ -91,7 +91,7 @@ const Img = styled.img`
height: 250px;
object-fit: contain;
@media(max-width: 400px) {
@media (max-width: 400px) {
height: 180px;
}
`;
@ -123,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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -4,7 +4,11 @@ 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';
@ -15,22 +19,25 @@ import { encryptWithRecoveryKey } from 'utils/crypto';
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
import { AppContext } from 'pages/_app';
enum SetupMode {
QR_CODE,
MANUAL_CODE,
}
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,
severity: '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,
severity: 'info',
});
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>
);
}

View file

@ -1,5 +1,13 @@
import { retryAsyncFunction, runningInBrowser } from 'utils/common';
import { getExportPendingFiles, getExportFailedFiles, getFilesUploadedAfterLastExport, getFileUID, dedupe, getGoogleLikeMetadataFile } from 'utils/export';
import { runningInBrowser } from 'utils/common';
import {
getExportPendingFiles,
getExportFailedFiles,
getFilesUploadedAfterLastExport,
getFileUID,
dedupe,
getGoogleLikeMetadataFile,
} from 'utils/export';
import { retryAsyncFunction } from 'utils/network';
import { logError } from 'utils/sentry';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { Collection, getLocalCollections } from './collectionService';
@ -16,7 +24,7 @@ export interface ExportStats {
}
export interface ExportRecord {
stage: ExportStage
stage: ExportStage;
lastAttemptTimestamp: number;
progress: ExportProgress;
queuedFiles: string[];
@ -27,7 +35,7 @@ export enum ExportStage {
INIT,
INPROGRESS,
PAUSED,
FINISHED
FINISHED,
}
enum ExportNotification {
@ -37,26 +45,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 ExportRecordFileName = 'export_status.json';
const MetadataFolderName = '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 +81,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;
@ -90,22 +101,37 @@ class ExportService {
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,
collections,
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,17 +140,19 @@ 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,
);
@ -137,8 +165,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 +174,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 };
@ -196,13 +226,18 @@ class ExportService {
async addFileExportRecord(folder: string, file: File, type: RecordType) {
const fileUID = getFileUID(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 +261,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}/${ExportRecordFileName}`,
JSON.stringify(newRecord, null, 2),
);
} catch (e) {
logError(e, 'error updating Export Record');
}
@ -239,7 +277,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}/${ExportRecordFileName}`,
);
if (recordFile) {
return JSON.parse(recordFile);
} else {
@ -250,12 +290,15 @@ 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),
);
this.ElectronAPIs.saveStreamToDisk(
`${collectionPath}/${uid}`,
fileStream,
);
this.ElectronAPIs.saveFileToDisk(
`${collectionPath}/${MetadataFolderName}/${uid}.json`,
getGoogleLikeMetadataFile(uid, file.metadata),
@ -268,6 +311,6 @@ class ExportService {
isExportInProgress = () => {
return this.exportInProgress !== null;
}
};
}
export default new ExportService();

View file

@ -30,8 +30,11 @@ async function encryptFileStream(worker, fileData: DataStream) {
};
}
export async function encryptFiledata(worker, filedata:Uint8Array | DataStream) :Promise<EncryptionResult> {
return isDataStream(filedata) ?
await encryptFileStream(worker, filedata) :
await worker.encryptFile(filedata);
export async function encryptFiledata(
worker,
filedata: Uint8Array | DataStream,
): Promise<EncryptionResult> {
return isDataStream(filedata)
? await encryptFileStream(worker, filedata)
: await worker.encryptFile(filedata);
}

View file

@ -2,10 +2,11 @@ import HTTPService from 'services/HTTPService';
import { getEndpoint } from 'utils/common/apiUtil';
import { getToken } from 'utils/common/key';
import { logError } from 'utils/sentry';
import { MultipartUploadURLs, UploadFile, UploadURL } from './uploadService';
import { UploadFile, UploadURL } from './uploadService';
import { File } from '../fileService';
import { CustomError } from 'utils/common/errorUtil';
import { retryAsyncFunction } from 'utils/network';
import { MultipartUploadURLs } from './s3Service';
const ENDPOINT = getEndpoint();
const MAX_URL_REQUESTS = 50;
@ -116,7 +117,7 @@ class NetworkClient {
filePart,
null,
null,
progressTracker(),
progressTracker,
);
if (!resp?.headers?.etag) {
const err = Error(CustomError.ETAG_MISSING);

View file

@ -29,7 +29,8 @@ export async function decryptChaCha(
header,
await fromB64(key),
);
const decryptionChunkSize = ENCRYPTION_CHUNK_SIZE +
const decryptionChunkSize =
ENCRYPTION_CHUNK_SIZE +
sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
let bytesRead = 0;
const decryptedData = [];
@ -59,7 +60,8 @@ export async function initChunkDecryption(header: Uint8Array, key: Uint8Array) {
header,
key,
);
const decryptionChunkSize = ENCRYPTION_CHUNK_SIZE +
const decryptionChunkSize =
ENCRYPTION_CHUNK_SIZE +
sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
const tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
return { pullState, decryptionChunkSize, tag };
@ -78,12 +80,11 @@ export async function decryptChunk(data: Uint8Array, pullState: StateAddress) {
export async function encryptChaChaOneShot(data: Uint8Array, key?: string) {
await sodium.ready;
const uintkey: Uint8Array = key ?
await fromB64(key) :
sodium.crypto_secretstream_xchacha20poly1305_keygen();
const initPushResult = sodium.crypto_secretstream_xchacha20poly1305_init_push(
uintkey,
);
const uintkey: Uint8Array = key
? await fromB64(key)
: sodium.crypto_secretstream_xchacha20poly1305_keygen();
const initPushResult =
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
const [pushState, header] = [initPushResult.state, initPushResult.header];
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
@ -104,13 +105,12 @@ export async function encryptChaChaOneShot(data: Uint8Array, key?: string) {
export async function encryptChaCha(data: Uint8Array, key?: string) {
await sodium.ready;
const uintkey: Uint8Array = key ?
await fromB64(key) :
sodium.crypto_secretstream_xchacha20poly1305_keygen();
const uintkey: Uint8Array = key
? await fromB64(key)
: sodium.crypto_secretstream_xchacha20poly1305_keygen();
const initPushResult = sodium.crypto_secretstream_xchacha20poly1305_init_push(
uintkey,
);
const initPushResult =
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
const [pushState, header] = [initPushResult.state, initPushResult.header];
let bytesRead = 0;
let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
@ -148,9 +148,8 @@ export async function encryptChaCha(data: Uint8Array, key?: string) {
export async function initChunkEncryption() {
await sodium.ready;
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const initPushResult = sodium.crypto_secretstream_xchacha20poly1305_init_push(
key,
);
const initPushResult =
sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
const [pushState, header] = [initPushResult.state, initPushResult.header];
return {
key: await toB64(key),
@ -165,9 +164,9 @@ export async function encryptFileChunk(
) {
await sodium.ready;
const tag = finalChunk ?
sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL :
sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
const tag = finalChunk
? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
: sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
pushState,
data,