Merge branch 'master' into fix-queue-processing-order
This commit is contained in:
commit
c3e7578019
|
@ -20,6 +20,7 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^4.25.0",
|
"@typescript-eslint/eslint-plugin": "^4.25.0",
|
||||||
"@typescript-eslint/parser": "^4.25.0",
|
"@typescript-eslint/parser": "^4.25.0",
|
||||||
"axios": "^0.21.3",
|
"axios": "^0.21.3",
|
||||||
|
"bip39": "^3.0.4",
|
||||||
"bootstrap": "^4.5.2",
|
"bootstrap": "^4.5.2",
|
||||||
"chrono-node": "^2.2.6",
|
"chrono-node": "^2.2.6",
|
||||||
"comlink": "^4.3.0",
|
"comlink": "^4.3.0",
|
||||||
|
|
37
src/components/EnteDateTimePicker.tsx
Normal file
37
src/components/EnteDateTimePicker.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
MIN_EDITED_CREATION_TIME,
|
||||||
|
MAX_EDITED_CREATION_TIME,
|
||||||
|
ALL_TIME,
|
||||||
|
} from 'services/fileService';
|
||||||
|
|
||||||
|
import DatePicker from 'react-datepicker';
|
||||||
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
|
|
||||||
|
const isSameDay = (first, second) =>
|
||||||
|
first.getFullYear() === second.getFullYear() &&
|
||||||
|
first.getMonth() === second.getMonth() &&
|
||||||
|
first.getDate() === second.getDate();
|
||||||
|
|
||||||
|
const EnteDateTimePicker = ({ isInEditMode, pickedTime, handleChange }) => (
|
||||||
|
<DatePicker
|
||||||
|
open={isInEditMode}
|
||||||
|
selected={pickedTime}
|
||||||
|
onChange={handleChange}
|
||||||
|
timeInputLabel="Time:"
|
||||||
|
dateFormat="dd/MM/yyyy h:mm aa"
|
||||||
|
showTimeSelect
|
||||||
|
autoFocus
|
||||||
|
minDate={MIN_EDITED_CREATION_TIME}
|
||||||
|
maxDate={MAX_EDITED_CREATION_TIME}
|
||||||
|
maxTime={
|
||||||
|
isSameDay(pickedTime, new Date())
|
||||||
|
? MAX_EDITED_CREATION_TIME
|
||||||
|
: ALL_TIME
|
||||||
|
}
|
||||||
|
minTime={MIN_EDITED_CREATION_TIME}
|
||||||
|
fixedHeight
|
||||||
|
withPortal></DatePicker>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default EnteDateTimePicker;
|
55
src/components/FixCreationTime/footer.tsx
Normal file
55
src/components/FixCreationTime/footer.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
import { FIX_STATE } from '.';
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
|
||||||
|
export default function FixCreationTimeFooter({
|
||||||
|
fixState,
|
||||||
|
startFix,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
fixState !== FIX_STATE.RUNNING && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
marginTop: '30px',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
}}>
|
||||||
|
{(fixState === FIX_STATE.NOT_STARTED ||
|
||||||
|
fixState === FIX_STATE.COMPLETED_WITH_ERRORS) && (
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
variant={'outline-secondary'}
|
||||||
|
onClick={() => {
|
||||||
|
props.hide();
|
||||||
|
}}>
|
||||||
|
{constants.CANCEL}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{fixState === FIX_STATE.COMPLETED && (
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
variant={'outline-secondary'}
|
||||||
|
onClick={props.hide}>
|
||||||
|
{constants.CLOSE}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{(fixState === FIX_STATE.NOT_STARTED ||
|
||||||
|
fixState === FIX_STATE.COMPLETED_WITH_ERRORS) && (
|
||||||
|
<>
|
||||||
|
<div style={{ width: '30px' }} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
variant={'outline-success'}
|
||||||
|
onClick={startFix}>
|
||||||
|
{constants.FIX_CREATION_TIME}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
153
src/components/FixCreationTime/index.tsx
Normal file
153
src/components/FixCreationTime/index.tsx
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
import MessageDialog from '../MessageDialog';
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { updateCreationTimeWithExif } from 'services/updateCreationTimeWithExif';
|
||||||
|
import { GalleryContext } from 'pages/gallery';
|
||||||
|
import { File } from 'services/fileService';
|
||||||
|
import FixCreationTimeRunning from './running';
|
||||||
|
import FixCreationTimeFooter from './footer';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
|
||||||
|
import FixCreationTimeOptions from './options';
|
||||||
|
export interface FixCreationTimeAttributes {
|
||||||
|
files: File[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
show: () => void;
|
||||||
|
hide: () => void;
|
||||||
|
attributes: FixCreationTimeAttributes;
|
||||||
|
}
|
||||||
|
export enum FIX_STATE {
|
||||||
|
NOT_STARTED,
|
||||||
|
RUNNING,
|
||||||
|
COMPLETED,
|
||||||
|
COMPLETED_WITH_ERRORS,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FIX_OPTIONS {
|
||||||
|
DATE_TIME_ORIGINAL,
|
||||||
|
DATE_TIME_DIGITIZED,
|
||||||
|
CUSTOM_TIME,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface formValues {
|
||||||
|
option: FIX_OPTIONS;
|
||||||
|
customTime: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Message(props: { fixState: FIX_STATE }) {
|
||||||
|
let message = null;
|
||||||
|
switch (props.fixState) {
|
||||||
|
case FIX_STATE.NOT_STARTED:
|
||||||
|
message = constants.UPDATE_CREATION_TIME_NOT_STARTED();
|
||||||
|
break;
|
||||||
|
case FIX_STATE.COMPLETED:
|
||||||
|
message = constants.UPDATE_CREATION_TIME_COMPLETED();
|
||||||
|
break;
|
||||||
|
case FIX_STATE.COMPLETED_WITH_ERRORS:
|
||||||
|
message = constants.UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return message ? <div>{message}</div> : <></>;
|
||||||
|
}
|
||||||
|
export default function FixCreationTime(props: Props) {
|
||||||
|
const [fixState, setFixState] = useState(FIX_STATE.NOT_STARTED);
|
||||||
|
const [progressTracker, setProgressTracker] = useState({
|
||||||
|
current: 0,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
const galleryContext = useContext(GalleryContext);
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
props.attributes &&
|
||||||
|
props.isOpen &&
|
||||||
|
fixState !== FIX_STATE.RUNNING
|
||||||
|
) {
|
||||||
|
setFixState(FIX_STATE.NOT_STARTED);
|
||||||
|
}
|
||||||
|
}, [props.isOpen]);
|
||||||
|
|
||||||
|
const startFix = async (option: FIX_OPTIONS, customTime: Date) => {
|
||||||
|
setFixState(FIX_STATE.RUNNING);
|
||||||
|
const completedWithoutError = await updateCreationTimeWithExif(
|
||||||
|
props.attributes.files,
|
||||||
|
option,
|
||||||
|
customTime,
|
||||||
|
setProgressTracker
|
||||||
|
);
|
||||||
|
if (!completedWithoutError) {
|
||||||
|
setFixState(FIX_STATE.COMPLETED);
|
||||||
|
} else {
|
||||||
|
setFixState(FIX_STATE.COMPLETED_WITH_ERRORS);
|
||||||
|
}
|
||||||
|
await galleryContext.syncWithRemote();
|
||||||
|
};
|
||||||
|
if (!props.attributes) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = (values: formValues) => {
|
||||||
|
console.log(values);
|
||||||
|
startFix(Number(values.option), new Date(values.customTime));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MessageDialog
|
||||||
|
show={props.isOpen}
|
||||||
|
onHide={props.hide}
|
||||||
|
attributes={{
|
||||||
|
title:
|
||||||
|
fixState === FIX_STATE.RUNNING
|
||||||
|
? constants.FIX_CREATION_TIME_IN_PROGRESS
|
||||||
|
: constants.FIX_CREATION_TIME,
|
||||||
|
staticBackdrop: true,
|
||||||
|
nonClosable: true,
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: '10px',
|
||||||
|
padding: '0 5%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
...(fixState === FIX_STATE.RUNNING
|
||||||
|
? { alignItems: 'center' }
|
||||||
|
: {}),
|
||||||
|
}}>
|
||||||
|
<Message fixState={fixState} />
|
||||||
|
|
||||||
|
{fixState === FIX_STATE.RUNNING && (
|
||||||
|
<FixCreationTimeRunning progressTracker={progressTracker} />
|
||||||
|
)}
|
||||||
|
<Formik<formValues>
|
||||||
|
initialValues={{
|
||||||
|
option: FIX_OPTIONS.DATE_TIME_ORIGINAL,
|
||||||
|
customTime: new Date(),
|
||||||
|
}}
|
||||||
|
validateOnBlur={false}
|
||||||
|
onSubmit={onSubmit}>
|
||||||
|
{({ values, handleChange, handleSubmit }) => (
|
||||||
|
<>
|
||||||
|
{(fixState === FIX_STATE.NOT_STARTED ||
|
||||||
|
fixState ===
|
||||||
|
FIX_STATE.COMPLETED_WITH_ERRORS) && (
|
||||||
|
<div style={{ marginTop: '10px' }}>
|
||||||
|
<FixCreationTimeOptions
|
||||||
|
handleChange={handleChange}
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<FixCreationTimeFooter
|
||||||
|
fixState={fixState}
|
||||||
|
startFix={handleSubmit}
|
||||||
|
hide={props.hide}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</div>
|
||||||
|
</MessageDialog>
|
||||||
|
);
|
||||||
|
}
|
83
src/components/FixCreationTime/options.tsx
Normal file
83
src/components/FixCreationTime/options.tsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import React, { ChangeEvent } from 'react';
|
||||||
|
import { FIX_OPTIONS } from '.';
|
||||||
|
import { Form } from 'react-bootstrap';
|
||||||
|
import EnteDateTimePicker from 'components/EnteDateTimePicker';
|
||||||
|
import { Row, Value } from 'components/Container';
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
|
||||||
|
const Option = ({
|
||||||
|
value,
|
||||||
|
selected,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
value: FIX_OPTIONS;
|
||||||
|
selected: FIX_OPTIONS;
|
||||||
|
onChange: (e: string | ChangeEvent<any>) => void;
|
||||||
|
label: string;
|
||||||
|
}) => (
|
||||||
|
<Form.Check
|
||||||
|
name="group1"
|
||||||
|
style={{
|
||||||
|
margin: '5px 0',
|
||||||
|
color: value !== Number(selected) ? '#aaa' : '#fff',
|
||||||
|
}}>
|
||||||
|
<Form.Check.Input
|
||||||
|
style={{ marginTop: '6px' }}
|
||||||
|
id={value.toString()}
|
||||||
|
type="radio"
|
||||||
|
value={value}
|
||||||
|
checked={value === Number(selected)}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<Form.Check.Label
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
htmlFor={value.toString()}>
|
||||||
|
{label}
|
||||||
|
</Form.Check.Label>
|
||||||
|
</Form.Check>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function FixCreationTimeOptions({ handleChange, values }) {
|
||||||
|
return (
|
||||||
|
<Form noValidate>
|
||||||
|
<Row style={{ margin: '0' }}>
|
||||||
|
<Option
|
||||||
|
value={FIX_OPTIONS.DATE_TIME_ORIGINAL}
|
||||||
|
onChange={handleChange('option')}
|
||||||
|
label={constants.DATE_TIME_ORIGINAL}
|
||||||
|
selected={Number(values.option)}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row style={{ margin: '0' }}>
|
||||||
|
<Option
|
||||||
|
value={FIX_OPTIONS.DATE_TIME_DIGITIZED}
|
||||||
|
onChange={handleChange('option')}
|
||||||
|
label={constants.DATE_TIME_DIGITIZED}
|
||||||
|
selected={Number(values.option)}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row style={{ margin: '0' }}>
|
||||||
|
<Value width="50%">
|
||||||
|
<Option
|
||||||
|
value={FIX_OPTIONS.CUSTOM_TIME}
|
||||||
|
onChange={handleChange('option')}
|
||||||
|
label={constants.CUSTOM_TIME}
|
||||||
|
selected={Number(values.option)}
|
||||||
|
/>
|
||||||
|
</Value>
|
||||||
|
{Number(values.option) === FIX_OPTIONS.CUSTOM_TIME && (
|
||||||
|
<Value width="40%">
|
||||||
|
<EnteDateTimePicker
|
||||||
|
isInEditMode
|
||||||
|
pickedTime={new Date(values.customTime)}
|
||||||
|
handleChange={(x: Date) =>
|
||||||
|
handleChange('customTime')(x.toUTCString())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Value>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
35
src/components/FixCreationTime/running.tsx
Normal file
35
src/components/FixCreationTime/running.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
import { ComfySpan } from 'components/ExportInProgress';
|
||||||
|
import React from 'react';
|
||||||
|
import { ProgressBar } from 'react-bootstrap';
|
||||||
|
|
||||||
|
export default function FixCreationTimeRunning({ progressTracker }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ marginBottom: '10px' }}>
|
||||||
|
<ComfySpan>
|
||||||
|
{' '}
|
||||||
|
{progressTracker.current} / {progressTracker.total}{' '}
|
||||||
|
</ComfySpan>{' '}
|
||||||
|
<span style={{ marginLeft: '10px' }}>
|
||||||
|
{' '}
|
||||||
|
{constants.CREATION_TIME_UPDATED}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
marginTop: '10px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}>
|
||||||
|
<ProgressBar
|
||||||
|
now={Math.round(
|
||||||
|
(progressTracker.current * 100) / progressTracker.total
|
||||||
|
)}
|
||||||
|
animated={true}
|
||||||
|
variant="upload-progress-bar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -189,7 +189,8 @@ export default function FixLargeThumbnails(props: Props) {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'space-around',
|
||||||
}}>
|
}}>
|
||||||
{fixState === FIX_STATE.NOT_STARTED ? (
|
{fixState === FIX_STATE.NOT_STARTED ||
|
||||||
|
fixState === FIX_STATE.FIX_LATER ? (
|
||||||
<Button
|
<Button
|
||||||
block
|
block
|
||||||
variant={'outline-secondary'}
|
variant={'outline-secondary'}
|
||||||
|
@ -197,7 +198,7 @@ export default function FixLargeThumbnails(props: Props) {
|
||||||
updateFixState(FIX_STATE.FIX_LATER);
|
updateFixState(FIX_STATE.FIX_LATER);
|
||||||
props.hide();
|
props.hide();
|
||||||
}}>
|
}}>
|
||||||
{constants.FIX_LATER}
|
{constants.FIX_THUMBNAIL_LATER}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
@ -217,7 +218,7 @@ export default function FixLargeThumbnails(props: Props) {
|
||||||
block
|
block
|
||||||
variant={'outline-success'}
|
variant={'outline-success'}
|
||||||
onClick={() => startFix()}>
|
onClick={() => startFix()}>
|
||||||
{constants.FIX}
|
{constants.FIX_THUMBNAIL}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -152,6 +152,8 @@ const PhotoFrame = ({
|
||||||
.map((item, index) => ({
|
.map((item, index) => ({
|
||||||
...item,
|
...item,
|
||||||
dataIndex: index,
|
dataIndex: index,
|
||||||
|
w: window.innerWidth,
|
||||||
|
h: window.innerHeight,
|
||||||
...(item.deleteBy && {
|
...(item.deleteBy && {
|
||||||
title: constants.AUTOMATIC_BIN_DELETE_MESSAGE(
|
title: constants.AUTOMATIC_BIN_DELETE_MESSAGE(
|
||||||
formatDateRelative(item.deleteBy / 1000)
|
formatDateRelative(item.deleteBy / 1000)
|
||||||
|
@ -352,7 +354,7 @@ const PhotoFrame = ({
|
||||||
if (galleryContext.thumbs.has(item.id)) {
|
if (galleryContext.thumbs.has(item.id)) {
|
||||||
url = galleryContext.thumbs.get(item.id);
|
url = galleryContext.thumbs.get(item.id);
|
||||||
} else {
|
} else {
|
||||||
url = await DownloadManager.getPreview(item);
|
url = await DownloadManager.getThumbnail(item);
|
||||||
galleryContext.thumbs.set(item.id, url);
|
galleryContext.thumbs.set(item.id, url);
|
||||||
}
|
}
|
||||||
updateUrl(item.dataIndex)(url);
|
updateUrl(item.dataIndex)(url);
|
||||||
|
|
|
@ -8,11 +8,8 @@ import {
|
||||||
removeFromFavorites,
|
removeFromFavorites,
|
||||||
} from 'services/collectionService';
|
} from 'services/collectionService';
|
||||||
import {
|
import {
|
||||||
ALL_TIME,
|
|
||||||
File,
|
File,
|
||||||
MAX_EDITED_FILE_NAME_LENGTH,
|
MAX_EDITED_FILE_NAME_LENGTH,
|
||||||
MAX_EDITED_CREATION_TIME,
|
|
||||||
MIN_EDITED_CREATION_TIME,
|
|
||||||
updatePublicMagicMetadata,
|
updatePublicMagicMetadata,
|
||||||
} from 'services/fileService';
|
} from 'services/fileService';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
@ -41,14 +38,13 @@ import {
|
||||||
} from 'components/Container';
|
} from 'components/Container';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
import DatePicker from 'react-datepicker';
|
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
|
||||||
import CloseIcon from 'components/icons/CloseIcon';
|
import CloseIcon from 'components/icons/CloseIcon';
|
||||||
import TickIcon from 'components/icons/TickIcon';
|
import TickIcon from 'components/icons/TickIcon';
|
||||||
import { FreeFlowText } from 'components/RecoveryKeyModal';
|
import { FreeFlowText } from 'components/RecoveryKeyModal';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import EnteSpinner from 'components/EnteSpinner';
|
import EnteSpinner from 'components/EnteSpinner';
|
||||||
|
import EnteDateTimePicker from 'components/EnteDateTimePicker';
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
@ -87,11 +83,6 @@ const renderInfoItem = (label: string, value: string | JSX.Element) => (
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSameDay = (first, second) =>
|
|
||||||
first.getFullYear() === second.getFullYear() &&
|
|
||||||
first.getMonth() === second.getMonth() &&
|
|
||||||
first.getDate() === second.getDate();
|
|
||||||
|
|
||||||
function RenderCreationTime({
|
function RenderCreationTime({
|
||||||
file,
|
file,
|
||||||
scheduleUpdate,
|
scheduleUpdate,
|
||||||
|
@ -145,24 +136,11 @@ function RenderCreationTime({
|
||||||
<Label width="30%">{constants.CREATION_TIME}</Label>
|
<Label width="30%">{constants.CREATION_TIME}</Label>
|
||||||
<Value width={isInEditMode ? '50%' : '60%'}>
|
<Value width={isInEditMode ? '50%' : '60%'}>
|
||||||
{isInEditMode ? (
|
{isInEditMode ? (
|
||||||
<DatePicker
|
<EnteDateTimePicker
|
||||||
open={isInEditMode}
|
isInEditMode={isInEditMode}
|
||||||
selected={pickedTime}
|
pickedTime={pickedTime}
|
||||||
onChange={handleChange}
|
handleChange={handleChange}
|
||||||
timeInputLabel="Time:"
|
/>
|
||||||
dateFormat="dd/MM/yyyy h:mm aa"
|
|
||||||
showTimeSelect
|
|
||||||
autoFocus
|
|
||||||
minDate={MIN_EDITED_CREATION_TIME}
|
|
||||||
maxDate={MAX_EDITED_CREATION_TIME}
|
|
||||||
maxTime={
|
|
||||||
isSameDay(pickedTime, new Date())
|
|
||||||
? MAX_EDITED_CREATION_TIME
|
|
||||||
: ALL_TIME
|
|
||||||
}
|
|
||||||
minTime={MIN_EDITED_CREATION_TIME}
|
|
||||||
fixedHeight
|
|
||||||
withPortal></DatePicker>
|
|
||||||
) : (
|
) : (
|
||||||
formatDateTime(pickedTime)
|
formatDateTime(pickedTime)
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -5,7 +5,9 @@ import constants from 'utils/strings/constants';
|
||||||
import MessageDialog from './MessageDialog';
|
import MessageDialog from './MessageDialog';
|
||||||
import EnteSpinner from './EnteSpinner';
|
import EnteSpinner from './EnteSpinner';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
const bip39 = require('bip39');
|
||||||
|
// mobile client library only supports english.
|
||||||
|
bip39.setDefaultWordlist('english');
|
||||||
export const CodeBlock = styled.div<{ height: number }>`
|
export const CodeBlock = styled.div<{ height: number }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -42,7 +44,7 @@ function RecoveryKeyModal({ somethingWentWrong, ...props }: Props) {
|
||||||
somethingWentWrong();
|
somethingWentWrong();
|
||||||
props.onHide();
|
props.onHide();
|
||||||
}
|
}
|
||||||
setRecoveryKey(recoveryKey);
|
setRecoveryKey(bip39.entropyToMnemonic(recoveryKey));
|
||||||
};
|
};
|
||||||
main();
|
main();
|
||||||
}, [props.show]);
|
}, [props.show]);
|
||||||
|
|
20
src/components/icons/ClockIcon.tsx
Normal file
20
src/components/icons/ClockIcon.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function ClockIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={props.height}
|
||||||
|
viewBox={props.viewBox}
|
||||||
|
width={props.width}
|
||||||
|
{...props}>
|
||||||
|
<path d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm1 12v-6h-2v8h7v-2h-5z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClockIcon.defaultProps = {
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
};
|
25
src/components/icons/DownloadIcon.tsx
Normal file
25
src/components/icons/DownloadIcon.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function DownloadIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={props.height}
|
||||||
|
viewBox={props.viewBox}
|
||||||
|
width={props.width}
|
||||||
|
fill="currentColor">
|
||||||
|
<g>
|
||||||
|
<rect fill="none" height="24" width="24" />
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="M5,20h14v-2H5V20z M19,9h-4V3H9v6H5l7,7L19,9z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadIcon.defaultProps = {
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
};
|
|
@ -177,7 +177,7 @@ export default function PreviewCard(props: IProps) {
|
||||||
if (file && !file.msrc) {
|
if (file && !file.msrc) {
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
try {
|
try {
|
||||||
const url = await DownloadManager.getPreview(file);
|
const url = await DownloadManager.getThumbnail(file);
|
||||||
if (isMounted.current) {
|
if (isMounted.current) {
|
||||||
setImgSrc(url);
|
setImgSrc(url);
|
||||||
thumbs.set(file.id, url);
|
thumbs.set(file.id, url);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { SetDialogMessage } from 'components/MessageDialog';
|
import { SetDialogMessage } from 'components/MessageDialog';
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { SetCollectionSelectorAttributes } from './CollectionSelector';
|
import { SetCollectionSelectorAttributes } from './CollectionSelector';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import Navbar from 'components/Navbar';
|
import Navbar from 'components/Navbar';
|
||||||
|
@ -17,6 +17,13 @@ import { OverlayTrigger } from 'react-bootstrap';
|
||||||
import { Collection } from 'services/collectionService';
|
import { Collection } from 'services/collectionService';
|
||||||
import RemoveIcon from 'components/icons/RemoveIcon';
|
import RemoveIcon from 'components/icons/RemoveIcon';
|
||||||
import RestoreIcon from 'components/icons/RestoreIcon';
|
import RestoreIcon from 'components/icons/RestoreIcon';
|
||||||
|
import ClockIcon from 'components/icons/ClockIcon';
|
||||||
|
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||||
|
import {
|
||||||
|
FIX_CREATION_TIME_VISIBLE_TO_USER_IDS,
|
||||||
|
User,
|
||||||
|
} from 'services/userService';
|
||||||
|
import DownloadIcon from 'components/icons/DownloadIcon';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
addToCollectionHelper: (collection: Collection) => void;
|
addToCollectionHelper: (collection: Collection) => void;
|
||||||
|
@ -27,6 +34,8 @@ interface Props {
|
||||||
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
|
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
|
||||||
deleteFileHelper: (permanent?: boolean) => void;
|
deleteFileHelper: (permanent?: boolean) => void;
|
||||||
removeFromCollectionHelper: () => void;
|
removeFromCollectionHelper: () => void;
|
||||||
|
fixTimeHelper: () => void;
|
||||||
|
downloadHelper: () => void;
|
||||||
count: number;
|
count: number;
|
||||||
clearSelection: () => void;
|
clearSelection: () => void;
|
||||||
archiveFilesHelper: () => void;
|
archiveFilesHelper: () => void;
|
||||||
|
@ -68,9 +77,11 @@ const SelectedFileOptions = ({
|
||||||
restoreToCollectionHelper,
|
restoreToCollectionHelper,
|
||||||
showCreateCollectionModal,
|
showCreateCollectionModal,
|
||||||
removeFromCollectionHelper,
|
removeFromCollectionHelper,
|
||||||
|
fixTimeHelper,
|
||||||
setDialogMessage,
|
setDialogMessage,
|
||||||
setCollectionSelectorAttributes,
|
setCollectionSelectorAttributes,
|
||||||
deleteFileHelper,
|
deleteFileHelper,
|
||||||
|
downloadHelper,
|
||||||
count,
|
count,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
archiveFilesHelper,
|
archiveFilesHelper,
|
||||||
|
@ -78,6 +89,13 @@ const SelectedFileOptions = ({
|
||||||
activeCollection,
|
activeCollection,
|
||||||
isFavoriteCollection,
|
isFavoriteCollection,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [showFixCreationTime, setShowFixCreationTime] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const user: User = getData(LS_KEYS.USER);
|
||||||
|
const showFixCreationTime =
|
||||||
|
FIX_CREATION_TIME_VISIBLE_TO_USER_IDS.includes(user?.id);
|
||||||
|
setShowFixCreationTime(showFixCreationTime);
|
||||||
|
}, []);
|
||||||
const addToCollection = () =>
|
const addToCollection = () =>
|
||||||
setCollectionSelectorAttributes({
|
setCollectionSelectorAttributes({
|
||||||
callback: addToCollectionHelper,
|
callback: addToCollectionHelper,
|
||||||
|
@ -168,6 +186,23 @@ const SelectedFileOptions = ({
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{showFixCreationTime && (
|
||||||
|
<IconWithMessage message={constants.FIX_CREATION_TIME}>
|
||||||
|
<IconButton onClick={fixTimeHelper}>
|
||||||
|
<ClockIcon />
|
||||||
|
</IconButton>
|
||||||
|
</IconWithMessage>
|
||||||
|
)}
|
||||||
|
<IconWithMessage message={constants.DOWNLOAD}>
|
||||||
|
<IconButton onClick={downloadHelper}>
|
||||||
|
<DownloadIcon />
|
||||||
|
</IconButton>
|
||||||
|
</IconWithMessage>
|
||||||
|
<IconWithMessage message={constants.ADD}>
|
||||||
|
<IconButton onClick={addToCollection}>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</IconWithMessage>
|
||||||
{activeCollection === ARCHIVE_SECTION && (
|
{activeCollection === ARCHIVE_SECTION && (
|
||||||
<IconWithMessage message={constants.UNARCHIVE}>
|
<IconWithMessage message={constants.UNARCHIVE}>
|
||||||
<IconButton onClick={unArchiveFilesHelper}>
|
<IconButton onClick={unArchiveFilesHelper}>
|
||||||
|
@ -182,11 +217,7 @@ const SelectedFileOptions = ({
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</IconWithMessage>
|
</IconWithMessage>
|
||||||
)}
|
)}
|
||||||
<IconWithMessage message={constants.ADD}>
|
|
||||||
<IconButton onClick={addToCollection}>
|
|
||||||
<AddIcon />
|
|
||||||
</IconButton>
|
|
||||||
</IconWithMessage>
|
|
||||||
{activeCollection !== ALL_SECTION &&
|
{activeCollection !== ALL_SECTION &&
|
||||||
activeCollection !== ARCHIVE_SECTION &&
|
activeCollection !== ARCHIVE_SECTION &&
|
||||||
!isFavoriteCollection && (
|
!isFavoriteCollection && (
|
||||||
|
|
|
@ -134,7 +134,8 @@ export default function Upload(props: Props) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const paths: string[] = props.acceptedFiles.map((file) => file['path']);
|
const paths: string[] = props.acceptedFiles.map((file) => file['path']);
|
||||||
paths.sort();
|
const getCharCount = (str: string) => (str.match(/\//g) ?? []).length;
|
||||||
|
paths.sort((path1, path2) => getCharCount(path1) - getCharCount(path2));
|
||||||
const firstPath = paths[0];
|
const firstPath = paths[0];
|
||||||
const lastPath = paths[paths.length - 1];
|
const lastPath = paths[paths.length - 1];
|
||||||
const L = firstPath.length;
|
const L = firstPath.length;
|
||||||
|
|
|
@ -479,6 +479,8 @@ type AppContextType = {
|
||||||
sharedFiles: File[];
|
sharedFiles: File[];
|
||||||
resetSharedFiles: () => void;
|
resetSharedFiles: () => void;
|
||||||
setDisappearingFlashMessage: (message: FlashMessage) => void;
|
setDisappearingFlashMessage: (message: FlashMessage) => void;
|
||||||
|
redirectUrl: string;
|
||||||
|
setRedirectUrl: (url: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum FLASH_MESSAGE_TYPE {
|
export enum FLASH_MESSAGE_TYPE {
|
||||||
|
@ -508,6 +510,7 @@ export default function App({ Component, err }) {
|
||||||
const [sharedFiles, setSharedFiles] = useState<File[]>(null);
|
const [sharedFiles, setSharedFiles] = useState<File[]>(null);
|
||||||
const [redirectName, setRedirectName] = useState<string>(null);
|
const [redirectName, setRedirectName] = useState<string>(null);
|
||||||
const [flashMessage, setFlashMessage] = useState<FlashMessage>(null);
|
const [flashMessage, setFlashMessage] = useState<FlashMessage>(null);
|
||||||
|
const [redirectUrl, setRedirectUrl] = useState(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!('serviceWorker' in navigator) ||
|
!('serviceWorker' in navigator) ||
|
||||||
|
@ -641,6 +644,8 @@ export default function App({ Component, err }) {
|
||||||
sharedFiles,
|
sharedFiles,
|
||||||
resetSharedFiles,
|
resetSharedFiles,
|
||||||
setDisappearingFlashMessage,
|
setDisappearingFlashMessage,
|
||||||
|
redirectUrl,
|
||||||
|
setRedirectUrl,
|
||||||
}}>
|
}}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Container>
|
<Container>
|
||||||
|
|
|
@ -75,8 +75,9 @@ export default function Credentials() {
|
||||||
}
|
}
|
||||||
await SaveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, key);
|
await SaveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, key);
|
||||||
await decryptAndStoreToken(key);
|
await decryptAndStoreToken(key);
|
||||||
|
const redirectUrl = appContext.redirectUrl;
|
||||||
router.push(PAGES.GALLERY);
|
appContext.setRedirectUrl(null);
|
||||||
|
router.push(redirectUrl ?? PAGES.GALLERY);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'user entered a wrong password');
|
logError(e, 'user entered a wrong password');
|
||||||
setFieldError('passphrase', constants.INCORRECT_PASSPHRASE);
|
setFieldError('passphrase', constants.INCORRECT_PASSPHRASE);
|
||||||
|
|
|
@ -50,6 +50,8 @@ import { LoadingOverlay } from 'components/LoadingOverlay';
|
||||||
import PhotoFrame from 'components/PhotoFrame';
|
import PhotoFrame from 'components/PhotoFrame';
|
||||||
import {
|
import {
|
||||||
changeFilesVisibility,
|
changeFilesVisibility,
|
||||||
|
downloadFiles,
|
||||||
|
getNonTrashedUniqueUserFiles,
|
||||||
getSelectedFiles,
|
getSelectedFiles,
|
||||||
mergeMetadata,
|
mergeMetadata,
|
||||||
sortFiles,
|
sortFiles,
|
||||||
|
@ -93,6 +95,9 @@ import {
|
||||||
Trash,
|
Trash,
|
||||||
} from 'services/trashService';
|
} from 'services/trashService';
|
||||||
import DeleteBtn from 'components/DeleteBtn';
|
import DeleteBtn from 'components/DeleteBtn';
|
||||||
|
import FixCreationTime, {
|
||||||
|
FixCreationTimeAttributes,
|
||||||
|
} from 'components/FixCreationTime';
|
||||||
|
|
||||||
export const DeadCenter = styled.div`
|
export const DeadCenter = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -204,10 +209,14 @@ export default function Gallery() {
|
||||||
useState<Map<number, number>>();
|
useState<Map<number, number>>();
|
||||||
const [activeCollection, setActiveCollection] = useState<number>(undefined);
|
const [activeCollection, setActiveCollection] = useState<number>(undefined);
|
||||||
const [trash, setTrash] = useState<Trash>([]);
|
const [trash, setTrash] = useState<Trash>([]);
|
||||||
|
const [fixCreationTimeView, setFixCreationTimeView] = useState(false);
|
||||||
|
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
|
||||||
|
useState<FixCreationTimeAttributes>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||||
if (!key) {
|
if (!key) {
|
||||||
|
appContext.setRedirectUrl(router.asPath);
|
||||||
router.push(PAGES.ROOT);
|
router.push(PAGES.ROOT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -227,11 +236,6 @@ export default function Gallery() {
|
||||||
setCollections(collections);
|
setCollections(collections);
|
||||||
setTrash(trash);
|
setTrash(trash);
|
||||||
await setDerivativeState(collections, files);
|
await setDerivativeState(collections, files);
|
||||||
await checkSubscriptionPurchase(
|
|
||||||
setDialogMessage,
|
|
||||||
router,
|
|
||||||
setLoading
|
|
||||||
);
|
|
||||||
await syncWithRemote(true);
|
await syncWithRemote(true);
|
||||||
setIsFirstLoad(false);
|
setIsFirstLoad(false);
|
||||||
setJustSignedUp(false);
|
setJustSignedUp(false);
|
||||||
|
@ -243,13 +247,19 @@ export default function Gallery() {
|
||||||
|
|
||||||
useEffect(() => setDialogView(true), [dialogMessage]);
|
useEffect(() => setDialogView(true), [dialogMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(
|
||||||
if (collectionSelectorAttributes) {
|
() => collectionSelectorAttributes && setCollectionSelectorView(true),
|
||||||
setCollectionSelectorView(true);
|
[collectionSelectorAttributes]
|
||||||
}
|
);
|
||||||
}, [collectionSelectorAttributes]);
|
|
||||||
|
|
||||||
useEffect(() => setCollectionNamerView(true), [collectionNamerAttributes]);
|
useEffect(
|
||||||
|
() => collectionNamerAttributes && setCollectionNamerView(true),
|
||||||
|
[collectionNamerAttributes]
|
||||||
|
);
|
||||||
|
useEffect(
|
||||||
|
() => fixCreationTimeAttributes && setFixCreationTimeView(true),
|
||||||
|
[fixCreationTimeAttributes]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof activeCollection === 'undefined') {
|
if (typeof activeCollection === 'undefined') {
|
||||||
|
@ -270,6 +280,13 @@ export default function Gallery() {
|
||||||
router.push(href, undefined, { shallow: true });
|
router.push(href, undefined, { shallow: true });
|
||||||
}, [activeCollection]);
|
}, [activeCollection]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||||
|
if (router.isReady && key) {
|
||||||
|
checkSubscriptionPurchase(setDialogMessage, router, setLoading);
|
||||||
|
}
|
||||||
|
}, [router.isReady]);
|
||||||
|
|
||||||
const syncWithRemote = async (force = false, silent = false) => {
|
const syncWithRemote = async (force = false, silent = false) => {
|
||||||
if (syncInProgress.current && !force) {
|
if (syncInProgress.current && !force) {
|
||||||
resync.current = true;
|
resync.current = true;
|
||||||
|
@ -523,6 +540,20 @@ export default function Gallery() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fixTimeHelper = async () => {
|
||||||
|
const selectedFiles = getSelectedFiles(selected, files);
|
||||||
|
setFixCreationTimeAttributes({ files: selectedFiles });
|
||||||
|
clearSelection();
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadHelper = async () => {
|
||||||
|
const selectedFiles = getSelectedFiles(selected, files);
|
||||||
|
clearSelection();
|
||||||
|
!syncInProgress.current && loadingBar.current?.continuousStart();
|
||||||
|
await downloadFiles(selectedFiles);
|
||||||
|
!syncInProgress.current && loadingBar.current.complete();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GalleryContext.Provider
|
<GalleryContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -564,7 +595,7 @@ export default function Gallery() {
|
||||||
loadingBar={loadingBar}
|
loadingBar={loadingBar}
|
||||||
isFirstFetch={isFirstFetch}
|
isFirstFetch={isFirstFetch}
|
||||||
collections={collections}
|
collections={collections}
|
||||||
files={files}
|
files={getNonTrashedUniqueUserFiles(files)}
|
||||||
setActiveCollection={setActiveCollection}
|
setActiveCollection={setActiveCollection}
|
||||||
setSearch={updateSearch}
|
setSearch={updateSearch}
|
||||||
searchStats={searchStats}
|
searchStats={searchStats}
|
||||||
|
@ -594,6 +625,12 @@ export default function Gallery() {
|
||||||
}
|
}
|
||||||
attributes={collectionSelectorAttributes}
|
attributes={collectionSelectorAttributes}
|
||||||
/>
|
/>
|
||||||
|
<FixCreationTime
|
||||||
|
isOpen={fixCreationTimeView}
|
||||||
|
hide={() => setFixCreationTimeView(false)}
|
||||||
|
show={() => setFixCreationTimeView(true)}
|
||||||
|
attributes={fixCreationTimeAttributes}
|
||||||
|
/>
|
||||||
<Upload
|
<Upload
|
||||||
syncWithRemote={syncWithRemote}
|
syncWithRemote={syncWithRemote}
|
||||||
setBannerMessage={setBannerMessage}
|
setBannerMessage={setBannerMessage}
|
||||||
|
@ -685,6 +722,8 @@ export default function Gallery() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fixTimeHelper={fixTimeHelper}
|
||||||
|
downloadHelper={downloadHelper}
|
||||||
count={selected.count}
|
count={selected.count}
|
||||||
clearSelection={clearSelection}
|
clearSelection={clearSelection}
|
||||||
activeCollection={activeCollection}
|
activeCollection={activeCollection}
|
||||||
|
|
|
@ -21,6 +21,9 @@ import LogoImg from 'components/LogoImg';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||||
import { User } from 'services/userService';
|
import { User } from 'services/userService';
|
||||||
|
const bip39 = require('bip39');
|
||||||
|
// mobile client library only supports english.
|
||||||
|
bip39.setDefaultWordlist('english');
|
||||||
|
|
||||||
export default function Recover() {
|
export default function Recover() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -51,6 +54,13 @@ export default function Recover() {
|
||||||
|
|
||||||
const recover = async (recoveryKey: string, setFieldError) => {
|
const recover = async (recoveryKey: string, setFieldError) => {
|
||||||
try {
|
try {
|
||||||
|
// check if user is entering mnemonic recovery key
|
||||||
|
if (recoveryKey.trim().indexOf(' ') > 0) {
|
||||||
|
if (recoveryKey.trim().split(' ').length !== 24) {
|
||||||
|
throw new Error('recovery code should have 24 words');
|
||||||
|
}
|
||||||
|
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||||
|
}
|
||||||
const cryptoWorker = await new CryptoWorker();
|
const cryptoWorker = await new CryptoWorker();
|
||||||
const masterKey: string = await cryptoWorker.decryptB64(
|
const masterKey: string = await cryptoWorker.decryptB64(
|
||||||
keyAttributes.masterKeyEncryptedWithRecoveryKey,
|
keyAttributes.masterKeyEncryptedWithRecoveryKey,
|
||||||
|
|
|
@ -12,6 +12,9 @@ import { logError } from 'utils/sentry';
|
||||||
import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
|
import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
|
||||||
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
|
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
|
||||||
import { PAGES } from 'types';
|
import { PAGES } from 'types';
|
||||||
|
const bip39 = require('bip39');
|
||||||
|
// mobile client library only supports english.
|
||||||
|
bip39.setDefaultWordlist('english');
|
||||||
|
|
||||||
export default function Recover() {
|
export default function Recover() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -43,6 +46,13 @@ export default function Recover() {
|
||||||
|
|
||||||
const recover = async (recoveryKey: string, setFieldError) => {
|
const recover = async (recoveryKey: string, setFieldError) => {
|
||||||
try {
|
try {
|
||||||
|
// check if user is entering mnemonic recovery key
|
||||||
|
if (recoveryKey.trim().indexOf(' ') > 0) {
|
||||||
|
if (recoveryKey.trim().split(' ').length !== 24) {
|
||||||
|
throw new Error('recovery code should have 24 words');
|
||||||
|
}
|
||||||
|
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||||
|
}
|
||||||
const cryptoWorker = await new CryptoWorker();
|
const cryptoWorker = await new CryptoWorker();
|
||||||
const twoFactorSecret: string = await cryptoWorker.decryptB64(
|
const twoFactorSecret: string = await cryptoWorker.decryptB64(
|
||||||
encryptedTwoFactorSecret.encryptedData,
|
encryptedTwoFactorSecret.encryptedData,
|
||||||
|
|
|
@ -134,6 +134,10 @@ class billingService {
|
||||||
sessionID: string = null
|
sessionID: string = null
|
||||||
): Promise<Subscription> {
|
): Promise<Subscription> {
|
||||||
try {
|
try {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const response = await HTTPService.post(
|
const response = await HTTPService.post(
|
||||||
`${ENDPOINT}/billing/verify-subscription`,
|
`${ENDPOINT}/billing/verify-subscription`,
|
||||||
{
|
{
|
||||||
|
@ -143,7 +147,7 @@ class billingService {
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
'X-Auth-Token': getToken(),
|
'X-Auth-Token': token,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const { subscription } = response.data;
|
const { subscription } = response.data;
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { getToken } from 'utils/common/key';
|
import { getToken } from 'utils/common/key';
|
||||||
import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil';
|
import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil';
|
||||||
import CryptoWorker from 'utils/crypto';
|
import CryptoWorker from 'utils/crypto';
|
||||||
import { generateStreamFromArrayBuffer, convertForPreview } from 'utils/file';
|
import {
|
||||||
|
generateStreamFromArrayBuffer,
|
||||||
|
convertForPreview,
|
||||||
|
needsConversionForPreview,
|
||||||
|
} from 'utils/file';
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { File, FILE_TYPE } from './fileService';
|
import { File, FILE_TYPE } from './fileService';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
@ -10,12 +14,14 @@ class DownloadManager {
|
||||||
private fileObjectUrlPromise = new Map<string, Promise<string>>();
|
private fileObjectUrlPromise = new Map<string, Promise<string>>();
|
||||||
private thumbnailObjectUrlPromise = new Map<number, Promise<string>>();
|
private thumbnailObjectUrlPromise = new Map<number, Promise<string>>();
|
||||||
|
|
||||||
public async getPreview(file: File) {
|
public async getThumbnail(file: File) {
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (!this.thumbnailObjectUrlPromise.get(file.id)) {
|
||||||
|
const downloadPromise = async () => {
|
||||||
const thumbnailCache = await caches.open('thumbs');
|
const thumbnailCache = await caches.open('thumbs');
|
||||||
const cacheResp: Response = await thumbnailCache.match(
|
const cacheResp: Response = await thumbnailCache.match(
|
||||||
file.id.toString()
|
file.id.toString()
|
||||||
|
@ -23,14 +29,21 @@ class DownloadManager {
|
||||||
if (cacheResp) {
|
if (cacheResp) {
|
||||||
return URL.createObjectURL(await cacheResp.blob());
|
return URL.createObjectURL(await cacheResp.blob());
|
||||||
}
|
}
|
||||||
if (!this.thumbnailObjectUrlPromise.get(file.id)) {
|
const thumb = await this.downloadThumb(token, file);
|
||||||
const downloadPromise = this.downloadThumb(
|
const thumbBlob = new Blob([thumb]);
|
||||||
token,
|
try {
|
||||||
thumbnailCache,
|
await thumbnailCache.put(
|
||||||
file
|
file.id.toString(),
|
||||||
|
new Response(thumbBlob)
|
||||||
);
|
);
|
||||||
this.thumbnailObjectUrlPromise.set(file.id, downloadPromise);
|
} catch (e) {
|
||||||
|
// TODO: handle storage full exception.
|
||||||
}
|
}
|
||||||
|
return URL.createObjectURL(thumbBlob);
|
||||||
|
};
|
||||||
|
this.thumbnailObjectUrlPromise.set(file.id, downloadPromise());
|
||||||
|
}
|
||||||
|
|
||||||
return await this.thumbnailObjectUrlPromise.get(file.id);
|
return await this.thumbnailObjectUrlPromise.get(file.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.thumbnailObjectUrlPromise.delete(file.id);
|
this.thumbnailObjectUrlPromise.delete(file.id);
|
||||||
|
@ -39,24 +52,7 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private downloadThumb = async (
|
downloadThumb = async (token: string, file: File) => {
|
||||||
token: string,
|
|
||||||
thumbnailCache: Cache,
|
|
||||||
file: File
|
|
||||||
) => {
|
|
||||||
const thumb = await this.getThumbnail(token, file);
|
|
||||||
try {
|
|
||||||
await thumbnailCache.put(
|
|
||||||
file.id.toString(),
|
|
||||||
new Response(new Blob([thumb]))
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// TODO: handle storage full exception.
|
|
||||||
}
|
|
||||||
return URL.createObjectURL(new Blob([thumb]));
|
|
||||||
};
|
|
||||||
|
|
||||||
getThumbnail = async (token: string, file: File) => {
|
|
||||||
const resp = await HTTPService.get(
|
const resp = await HTTPService.get(
|
||||||
getThumbnailUrl(file.id),
|
getThumbnailUrl(file.id),
|
||||||
null,
|
null,
|
||||||
|
@ -73,32 +69,38 @@ class DownloadManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
getFile = async (file: File, forPreview = false) => {
|
getFile = async (file: File, forPreview = false) => {
|
||||||
let fileUID: string;
|
const shouldBeConverted = forPreview && needsConversionForPreview(file);
|
||||||
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
|
const fileKey = shouldBeConverted
|
||||||
fileUID = file.id.toString();
|
? `${file.id}_converted`
|
||||||
} else {
|
: `${file.id}`;
|
||||||
fileUID = `${file.id}_forPreview=${forPreview}`;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const getFilePromise = async () => {
|
const getFilePromise = async (convert: boolean) => {
|
||||||
const fileStream = await this.downloadFile(file);
|
const fileStream = await this.downloadFile(file);
|
||||||
let fileBlob = await new Response(fileStream).blob();
|
let fileBlob = await new Response(fileStream).blob();
|
||||||
if (forPreview) {
|
if (convert) {
|
||||||
fileBlob = await convertForPreview(file, fileBlob);
|
fileBlob = await convertForPreview(file, fileBlob);
|
||||||
}
|
}
|
||||||
return URL.createObjectURL(fileBlob);
|
return URL.createObjectURL(fileBlob);
|
||||||
};
|
};
|
||||||
if (!this.fileObjectUrlPromise.get(fileUID)) {
|
if (!this.fileObjectUrlPromise.get(fileKey)) {
|
||||||
this.fileObjectUrlPromise.set(fileUID, getFilePromise());
|
this.fileObjectUrlPromise.set(
|
||||||
|
fileKey,
|
||||||
|
getFilePromise(shouldBeConverted)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return await this.fileObjectUrlPromise.get(fileUID);
|
const fileURL = await this.fileObjectUrlPromise.get(fileKey);
|
||||||
|
return fileURL;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.fileObjectUrlPromise.delete(fileUID);
|
this.fileObjectUrlPromise.delete(fileKey);
|
||||||
logError(e, 'Failed to get File');
|
logError(e, 'Failed to get File');
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public async getCachedOriginalFile(file: File) {
|
||||||
|
return await this.fileObjectUrlPromise.get(file.id.toString());
|
||||||
|
}
|
||||||
|
|
||||||
async downloadFile(file: File) {
|
async downloadFile(file: File) {
|
||||||
const worker = await new CryptoWorker();
|
const worker = await new CryptoWorker();
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|
|
@ -19,6 +19,8 @@ class FFmpegService {
|
||||||
this.isLoading = null;
|
this.isLoading = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'ffmpeg load failed');
|
logError(e, 'ffmpeg load failed');
|
||||||
|
this.ffmpeg = null;
|
||||||
|
this.isLoading = null;
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,7 @@ export async function replaceThumbnail(
|
||||||
current: idx,
|
current: idx,
|
||||||
total: largeThumbnailFiles.length,
|
total: largeThumbnailFiles.length,
|
||||||
});
|
});
|
||||||
const originalThumbnail = await downloadManager.getThumbnail(
|
const originalThumbnail = await downloadManager.downloadThumb(
|
||||||
token,
|
token,
|
||||||
file
|
file
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,8 +5,7 @@ import { DateValue, Suggestion, SuggestionType } from 'components/SearchBar';
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { Collection } from './collectionService';
|
import { Collection } from './collectionService';
|
||||||
import { File } from './fileService';
|
import { File } from './fileService';
|
||||||
import { User } from './userService';
|
import { logError } from 'utils/sentry';
|
||||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
|
const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
|
||||||
|
@ -45,6 +44,7 @@ export function parseHumanDate(humanDate: string): DateValue[] {
|
||||||
export async function searchLocation(
|
export async function searchLocation(
|
||||||
searchPhrase: string
|
searchPhrase: string
|
||||||
): Promise<LocationSearchResponse[]> {
|
): Promise<LocationSearchResponse[]> {
|
||||||
|
try {
|
||||||
const resp = await HTTPService.get(
|
const resp = await HTTPService.get(
|
||||||
`${ENDPOINT}/search/location`,
|
`${ENDPOINT}/search/location`,
|
||||||
{
|
{
|
||||||
|
@ -55,7 +55,11 @@ export async function searchLocation(
|
||||||
'X-Auth-Token': getToken(),
|
'X-Auth-Token': getToken(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return resp.data.results;
|
return resp.data.results ?? [];
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'location search failed');
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHolidaySuggestion(searchPhrase: string): Suggestion[] {
|
export function getHolidaySuggestion(searchPhrase: string): Suggestion[] {
|
||||||
|
@ -99,7 +103,7 @@ export function getYearSuggestion(searchPhrase: string): Suggestion[] {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
logError(e, 'getYearSuggestion failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
@ -115,8 +119,6 @@ export function searchCollection(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function searchFiles(searchPhrase: string, files: File[]) {
|
export function searchFiles(searchPhrase: string, files: File[]) {
|
||||||
const user: User = getData(LS_KEYS.USER) ?? {};
|
|
||||||
const idSet = new Set();
|
|
||||||
return files
|
return files
|
||||||
.map((file, idx) => ({
|
.map((file, idx) => ({
|
||||||
title: file.metadata.title,
|
title: file.metadata.title,
|
||||||
|
@ -125,13 +127,6 @@ export function searchFiles(searchPhrase: string, files: File[]) {
|
||||||
ownerID: file.ownerID,
|
ownerID: file.ownerID,
|
||||||
id: file.id,
|
id: file.id,
|
||||||
}))
|
}))
|
||||||
.filter((file) => {
|
|
||||||
if (file.ownerID === user.id && !idSet.has(file.id)) {
|
|
||||||
idSet.add(file.id);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.filter(({ title }) => title.toLowerCase().includes(searchPhrase))
|
.filter(({ title }) => title.toLowerCase().includes(searchPhrase))
|
||||||
.slice(0, 4);
|
.slice(0, 4);
|
||||||
}
|
}
|
||||||
|
|
77
src/services/updateCreationTimeWithExif.ts
Normal file
77
src/services/updateCreationTimeWithExif.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { FIX_OPTIONS } from 'components/FixCreationTime';
|
||||||
|
import { SetProgressTracker } from 'components/FixLargeThumbnail';
|
||||||
|
import CryptoWorker from 'utils/crypto';
|
||||||
|
import {
|
||||||
|
changeFileCreationTime,
|
||||||
|
getFileFromURL,
|
||||||
|
updateExistingFilePubMetadata,
|
||||||
|
} from 'utils/file';
|
||||||
|
import { logError } from 'utils/sentry';
|
||||||
|
import downloadManager from './downloadManager';
|
||||||
|
import { File, FILE_TYPE, updatePublicMagicMetadata } from './fileService';
|
||||||
|
import { getRawExif, getUNIXTime } from './upload/exifService';
|
||||||
|
import { getFileType } from './upload/readFileService';
|
||||||
|
|
||||||
|
export async function updateCreationTimeWithExif(
|
||||||
|
filesToBeUpdated: File[],
|
||||||
|
fixOption: FIX_OPTIONS,
|
||||||
|
customTime: Date,
|
||||||
|
setProgressTracker: SetProgressTracker
|
||||||
|
) {
|
||||||
|
let completedWithError = false;
|
||||||
|
try {
|
||||||
|
if (filesToBeUpdated.length === 0) {
|
||||||
|
return completedWithError;
|
||||||
|
}
|
||||||
|
setProgressTracker({ current: 0, total: filesToBeUpdated.length });
|
||||||
|
for (const [index, file] of filesToBeUpdated.entries()) {
|
||||||
|
try {
|
||||||
|
if (file.metadata.fileType !== FILE_TYPE.IMAGE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let correctCreationTime: number;
|
||||||
|
if (fixOption === FIX_OPTIONS.CUSTOM_TIME) {
|
||||||
|
correctCreationTime = getUNIXTime(customTime);
|
||||||
|
} else {
|
||||||
|
const fileURL = await downloadManager.getFile(file);
|
||||||
|
const fileObject = await getFileFromURL(fileURL);
|
||||||
|
const worker = await new CryptoWorker();
|
||||||
|
const fileTypeInfo = await getFileType(worker, fileObject);
|
||||||
|
const exifData = await getRawExif(fileObject, fileTypeInfo);
|
||||||
|
if (fixOption === FIX_OPTIONS.DATE_TIME_ORIGINAL) {
|
||||||
|
correctCreationTime = getUNIXTime(
|
||||||
|
exifData?.DateTimeOriginal
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
correctCreationTime = getUNIXTime(exifData?.CreateDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
correctCreationTime &&
|
||||||
|
correctCreationTime !== file.metadata.creationTime
|
||||||
|
) {
|
||||||
|
let updatedFile = await changeFileCreationTime(
|
||||||
|
file,
|
||||||
|
correctCreationTime
|
||||||
|
);
|
||||||
|
updatedFile = (
|
||||||
|
await updatePublicMagicMetadata([updatedFile])
|
||||||
|
)[0];
|
||||||
|
updateExistingFilePubMetadata(file, updatedFile);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'failed to updated a CreationTime With Exif');
|
||||||
|
completedWithError = true;
|
||||||
|
} finally {
|
||||||
|
setProgressTracker({
|
||||||
|
current: index + 1,
|
||||||
|
total: filesToBeUpdated.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'update CreationTime With Exif failed');
|
||||||
|
completedWithError = true;
|
||||||
|
}
|
||||||
|
return completedWithError;
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
import exifr from 'exifr';
|
import exifr from 'exifr';
|
||||||
|
import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
import { NULL_LOCATION, Location } from './metadataService';
|
import { NULL_LOCATION, Location } from './metadataService';
|
||||||
|
import { FileTypeInfo } from './readFileService';
|
||||||
|
|
||||||
const EXIF_TAGS_NEEDED = [
|
const EXIF_TAGS_NEEDED = [
|
||||||
'DateTimeOriginal',
|
'DateTimeOriginal',
|
||||||
|
@ -11,29 +13,66 @@ const EXIF_TAGS_NEEDED = [
|
||||||
'GPSLatitudeRef',
|
'GPSLatitudeRef',
|
||||||
'GPSLongitudeRef',
|
'GPSLongitudeRef',
|
||||||
];
|
];
|
||||||
|
interface Exif {
|
||||||
|
DateTimeOriginal?: Date;
|
||||||
|
CreateDate?: Date;
|
||||||
|
ModifyDate?: Date;
|
||||||
|
GPSLatitude?: number;
|
||||||
|
GPSLongitude?: number;
|
||||||
|
GPSLatitudeRef?: number;
|
||||||
|
GPSLongitudeRef?: number;
|
||||||
|
}
|
||||||
interface ParsedEXIFData {
|
interface ParsedEXIFData {
|
||||||
location: Location;
|
location: Location;
|
||||||
creationTime: number;
|
creationTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getExifData(
|
export async function getExifData(
|
||||||
receivedFile: globalThis.File
|
receivedFile: globalThis.File,
|
||||||
|
fileTypeInfo: FileTypeInfo
|
||||||
): Promise<ParsedEXIFData> {
|
): Promise<ParsedEXIFData> {
|
||||||
const exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED);
|
const nullExifData: ParsedEXIFData = {
|
||||||
|
location: NULL_LOCATION,
|
||||||
|
creationTime: null,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const exifData = await getRawExif(receivedFile, fileTypeInfo);
|
||||||
if (!exifData) {
|
if (!exifData) {
|
||||||
return { location: NULL_LOCATION, creationTime: null };
|
return nullExifData;
|
||||||
}
|
}
|
||||||
const parsedEXIFData = {
|
const parsedEXIFData = {
|
||||||
location: getEXIFLocation(exifData),
|
location: getEXIFLocation(exifData),
|
||||||
creationTime: getUNIXTime(exifData),
|
creationTime: getUNIXTime(
|
||||||
|
exifData.DateTimeOriginal ??
|
||||||
|
exifData.CreateDate ??
|
||||||
|
exifData.ModifyDate
|
||||||
|
),
|
||||||
};
|
};
|
||||||
return parsedEXIFData;
|
return parsedEXIFData;
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'getExifData failed');
|
||||||
|
return nullExifData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUNIXTime(exifData: any) {
|
export async function getRawExif(
|
||||||
const dateTime: Date =
|
receivedFile: File,
|
||||||
exifData.DateTimeOriginal ?? exifData.CreateDate ?? exifData.ModifyDate;
|
fileTypeInfo: FileTypeInfo
|
||||||
|
) {
|
||||||
|
let exifData: Exif;
|
||||||
|
try {
|
||||||
|
exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'file missing exif data ', {
|
||||||
|
fileType: fileTypeInfo.exactType,
|
||||||
|
});
|
||||||
|
// ignore exif parsing errors
|
||||||
|
}
|
||||||
|
return exifData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUNIXTime(dateTime: Date) {
|
||||||
|
try {
|
||||||
if (!dateTime) {
|
if (!dateTime) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -43,6 +82,9 @@ function getUNIXTime(exifData: any) {
|
||||||
} else {
|
} else {
|
||||||
return unixTime;
|
return unixTime;
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'getUNIXTime failed', { dateTime });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEXIFLocation(exifData): Location {
|
function getEXIFLocation(exifData): Location {
|
||||||
|
|
|
@ -34,14 +34,7 @@ export async function extractMetadata(
|
||||||
) {
|
) {
|
||||||
let exifData = null;
|
let exifData = null;
|
||||||
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
||||||
try {
|
exifData = await getExifData(receivedFile, fileTypeInfo);
|
||||||
exifData = await getExifData(receivedFile);
|
|
||||||
} catch (e) {
|
|
||||||
logError(e, 'file missing exif data ', {
|
|
||||||
fileType: fileTypeInfo.exactType,
|
|
||||||
});
|
|
||||||
// ignore exif parsing errors
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractedMetadata: MetadataObject = {
|
const extractedMetadata: MetadataObject = {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { logError } from 'utils/sentry';
|
||||||
import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64';
|
import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64';
|
||||||
import FFmpegService from 'services/ffmpegService';
|
import FFmpegService from 'services/ffmpegService';
|
||||||
import { convertToHumanReadable } from 'utils/billingUtil';
|
import { convertToHumanReadable } from 'utils/billingUtil';
|
||||||
import { fileIsHEIC } from 'utils/file';
|
import { isFileHEIC } from 'utils/file';
|
||||||
import { FileTypeInfo } from './readFileService';
|
import { FileTypeInfo } from './readFileService';
|
||||||
|
|
||||||
const MAX_THUMBNAIL_DIMENSION = 720;
|
const MAX_THUMBNAIL_DIMENSION = 720;
|
||||||
|
@ -31,7 +31,7 @@ export async function generateThumbnail(
|
||||||
let thumbnail: Uint8Array;
|
let thumbnail: Uint8Array;
|
||||||
try {
|
try {
|
||||||
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
||||||
const isHEIC = fileIsHEIC(fileTypeInfo.exactType);
|
const isHEIC = isFileHEIC(fileTypeInfo.exactType);
|
||||||
canvas = await generateImageThumbnail(worker, file, isHEIC);
|
canvas = await generateImageThumbnail(worker, file, isHEIC);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
@ -44,7 +44,7 @@ export async function generateThumbnail(
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'failed to generate thumbnail using ffmpeg', {
|
logError(e, 'failed to generate thumbnail using ffmpeg', {
|
||||||
type: fileTypeInfo.exactType,
|
fileFormat: fileTypeInfo.exactType,
|
||||||
});
|
});
|
||||||
canvas = await generateVideoThumbnail(file);
|
canvas = await generateVideoThumbnail(file);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,8 @@ const ENDPOINT = getEndpoint();
|
||||||
|
|
||||||
const HAS_SET_KEYS = 'hasSetKeys';
|
const HAS_SET_KEYS = 'hasSetKeys';
|
||||||
|
|
||||||
|
export const FIX_CREATION_TIME_VISIBLE_TO_USER_IDS = [1, 125, 243, 341];
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -191,17 +191,14 @@ export async function checkSubscriptionPurchase(
|
||||||
router: NextRouter,
|
router: NextRouter,
|
||||||
setLoading: SetLoading
|
setLoading: SetLoading
|
||||||
) {
|
) {
|
||||||
|
const { session_id: sessionId, status, reason } = router.query ?? {};
|
||||||
try {
|
try {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const sessionId = urlParams.get('session_id');
|
|
||||||
const status = urlParams.get('status');
|
|
||||||
const reason = urlParams.get('reason');
|
|
||||||
if (status === RESPONSE_STATUS.fail) {
|
if (status === RESPONSE_STATUS.fail) {
|
||||||
handleFailureReason(reason, setDialogMessage, setLoading);
|
handleFailureReason(reason as string, setDialogMessage, setLoading);
|
||||||
} else if (status === RESPONSE_STATUS.success) {
|
} else if (status === RESPONSE_STATUS.success) {
|
||||||
try {
|
try {
|
||||||
const subscription = await billingService.verifySubscription(
|
const subscription = await billingService.verifySubscription(
|
||||||
sessionId
|
sessionId as string
|
||||||
);
|
);
|
||||||
setDialogMessage({
|
setDialogMessage({
|
||||||
title: constants.SUBSCRIPTION_PURCHASE_SUCCESS_TITLE,
|
title: constants.SUBSCRIPTION_PURCHASE_SUCCESS_TITLE,
|
||||||
|
@ -220,8 +217,6 @@ export async function checkSubscriptionPurchase(
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
|
||||||
router.push('gallery', undefined, { shallow: true });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from 'services/fileService';
|
} from 'services/fileService';
|
||||||
import { decodeMotionPhoto } from 'services/motionPhotoService';
|
import { decodeMotionPhoto } from 'services/motionPhotoService';
|
||||||
import { getMimeTypeFromBlob } from 'services/upload/readFileService';
|
import { getMimeTypeFromBlob } from 'services/upload/readFileService';
|
||||||
import DownloadManger from 'services/downloadManager';
|
import DownloadManager from 'services/downloadManager';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { User } from 'services/userService';
|
import { User } from 'services/userService';
|
||||||
import CryptoWorker from 'utils/crypto';
|
import CryptoWorker from 'utils/crypto';
|
||||||
|
@ -37,10 +37,16 @@ export function downloadAsFile(filename: string, content: string) {
|
||||||
a.remove();
|
a.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadFile(file) {
|
export async function downloadFile(file: File) {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.style.display = 'none';
|
a.style.display = 'none';
|
||||||
a.href = await DownloadManger.getFile(file);
|
const cachedFileUrl = await DownloadManager.getCachedOriginalFile(file);
|
||||||
|
const fileURL =
|
||||||
|
cachedFileUrl ??
|
||||||
|
URL.createObjectURL(
|
||||||
|
await new Response(await DownloadManager.downloadFile(file)).blob()
|
||||||
|
);
|
||||||
|
a.href = fileURL;
|
||||||
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||||
a.download = fileNameWithoutExtension(file.metadata.title) + '.zip';
|
a.download = fileNameWithoutExtension(file.metadata.title) + '.zip';
|
||||||
} else {
|
} else {
|
||||||
|
@ -51,10 +57,11 @@ export async function downloadFile(file) {
|
||||||
a.remove();
|
a.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fileIsHEIC(mimeType: string) {
|
export function isFileHEIC(mimeType: string) {
|
||||||
return (
|
return (
|
||||||
mimeType.toLowerCase().endsWith(TYPE_HEIC) ||
|
mimeType &&
|
||||||
mimeType.toLowerCase().endsWith(TYPE_HEIF)
|
(mimeType.toLowerCase().endsWith(TYPE_HEIC) ||
|
||||||
|
mimeType.toLowerCase().endsWith(TYPE_HEIF))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,7 +278,7 @@ export async function convertForPreview(file: File, fileBlob: Blob) {
|
||||||
|
|
||||||
const mimeType =
|
const mimeType =
|
||||||
(await getMimeTypeFromBlob(worker, fileBlob)) ?? typeFromExtension;
|
(await getMimeTypeFromBlob(worker, fileBlob)) ?? typeFromExtension;
|
||||||
if (fileIsHEIC(mimeType)) {
|
if (isFileHEIC(mimeType)) {
|
||||||
fileBlob = await worker.convertHEIC2JPEG(fileBlob);
|
fileBlob = await worker.convertHEIC2JPEG(fileBlob);
|
||||||
}
|
}
|
||||||
return fileBlob;
|
return fileBlob;
|
||||||
|
@ -438,3 +445,54 @@ export function updateExistingFilePubMetadata(
|
||||||
existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata;
|
existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata;
|
||||||
existingFile.metadata = mergeMetadata([existingFile])[0].metadata;
|
existingFile.metadata = mergeMetadata([existingFile])[0].metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFileFromURL(fileURL: string) {
|
||||||
|
const fileBlob = await (await fetch(fileURL)).blob();
|
||||||
|
const fileFile = new globalThis.File([fileBlob], 'temp');
|
||||||
|
return fileFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUniqueFiles(files: File[]) {
|
||||||
|
const idSet = new Set<number>();
|
||||||
|
return files.filter((file) => {
|
||||||
|
if (!idSet.has(file.id)) {
|
||||||
|
idSet.add(file.id);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function getNonTrashedUniqueUserFiles(files: File[]) {
|
||||||
|
const user: User = getData(LS_KEYS.USER) ?? {};
|
||||||
|
return getUniqueFiles(
|
||||||
|
files.filter(
|
||||||
|
(file) =>
|
||||||
|
(typeof file.isTrashed === 'undefined' || !file.isTrashed) &&
|
||||||
|
(!user.id || file.ownerID === user.id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadFiles(files: File[]) {
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
await downloadFile(file);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'download fail for file');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function needsConversionForPreview(file: File) {
|
||||||
|
const fileExtension = splitFilenameAndExtension(file.metadata.title)[1];
|
||||||
|
if (
|
||||||
|
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO ||
|
||||||
|
(file.metadata.fileType === FILE_TYPE.IMAGE &&
|
||||||
|
isFileHEIC(fileExtension))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -579,8 +579,8 @@ const englishConstants = {
|
||||||
SORT_BY_COLLECTION_NAME: 'album name',
|
SORT_BY_COLLECTION_NAME: 'album name',
|
||||||
FIX_LARGE_THUMBNAILS: 'compress thumbnails',
|
FIX_LARGE_THUMBNAILS: 'compress thumbnails',
|
||||||
THUMBNAIL_REPLACED: 'thumbnails compressed',
|
THUMBNAIL_REPLACED: 'thumbnails compressed',
|
||||||
FIX: 'compress',
|
FIX_THUMBNAIL: 'compress',
|
||||||
FIX_LATER: 'compress later',
|
FIX_THUMBNAIL_LATER: 'compress later',
|
||||||
REPLACE_THUMBNAIL_NOT_STARTED: () => (
|
REPLACE_THUMBNAIL_NOT_STARTED: () => (
|
||||||
<>
|
<>
|
||||||
some of your videos thumbnails can be compressed to save space.
|
some of your videos thumbnails can be compressed to save space.
|
||||||
|
@ -596,7 +596,23 @@ const englishConstants = {
|
||||||
REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR: () => (
|
REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR: () => (
|
||||||
<>could not compress some of your thumbnails, please retry</>
|
<>could not compress some of your thumbnails, please retry</>
|
||||||
),
|
),
|
||||||
|
FIX_CREATION_TIME: 'fix time',
|
||||||
|
FIX_CREATION_TIME_IN_PROGRESS: 'fixing time',
|
||||||
|
CREATION_TIME_UPDATED: `file time updated`,
|
||||||
|
|
||||||
|
UPDATE_CREATION_TIME_NOT_STARTED: () => (
|
||||||
|
<>select the option you want to use</>
|
||||||
|
),
|
||||||
|
UPDATE_CREATION_TIME_COMPLETED: () => <>successfully updated all files</>,
|
||||||
|
|
||||||
|
UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR: () => (
|
||||||
|
<>file time updation failed for some files, please retry</>
|
||||||
|
),
|
||||||
FILE_NAME_CHARACTER_LIMIT: '100 characters max',
|
FILE_NAME_CHARACTER_LIMIT: '100 characters max',
|
||||||
|
|
||||||
|
DATE_TIME_ORIGINAL: 'EXIF:DateTimeOriginal',
|
||||||
|
DATE_TIME_DIGITIZED: 'EXIF:DateTimeDigitized',
|
||||||
|
CUSTOM_TIME: 'custom time',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default englishConstants;
|
export default englishConstants;
|
||||||
|
|
2
thirdparty/photoswipe
vendored
2
thirdparty/photoswipe
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 443b1e393aa37899373b71272e4bcf191529bb74
|
Subproject commit b1766d38475659c17cf669e2b27787d15f8957b1
|
19
yarn.lock
19
yarn.lock
|
@ -1453,6 +1453,11 @@
|
||||||
resolved "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz"
|
resolved "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz"
|
||||||
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
|
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
|
||||||
|
|
||||||
|
"@types/node@11.11.6":
|
||||||
|
version "11.11.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a"
|
||||||
|
integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==
|
||||||
|
|
||||||
"@types/node@^14.6.4":
|
"@types/node@^14.6.4":
|
||||||
version "14.17.15"
|
version "14.17.15"
|
||||||
resolved "https://registry.npmjs.org/@types/node/-/node-14.17.15.tgz"
|
resolved "https://registry.npmjs.org/@types/node/-/node-14.17.15.tgz"
|
||||||
|
@ -2085,6 +2090,16 @@ binary-extensions@^2.0.0:
|
||||||
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
|
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
|
||||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||||
|
|
||||||
|
bip39@^3.0.4:
|
||||||
|
version "3.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.4.tgz#5b11fed966840b5e1b8539f0f54ab6392969b2a0"
|
||||||
|
integrity sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "11.11.6"
|
||||||
|
create-hash "^1.1.0"
|
||||||
|
pbkdf2 "^3.0.9"
|
||||||
|
randombytes "^2.0.1"
|
||||||
|
|
||||||
bluebird@^3.5.5:
|
bluebird@^3.5.5:
|
||||||
version "3.7.2"
|
version "3.7.2"
|
||||||
resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz"
|
resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz"
|
||||||
|
@ -5016,7 +5031,7 @@ path-type@^4.0.0:
|
||||||
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
|
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
|
||||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
||||||
|
|
||||||
pbkdf2@^3.0.3:
|
pbkdf2@^3.0.3, pbkdf2@^3.0.9:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz"
|
resolved "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz"
|
||||||
integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==
|
integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==
|
||||||
|
@ -5033,7 +5048,7 @@ peek-readable@^4.0.1:
|
||||||
integrity sha512-7qmhptnR0WMSpxT5rMHG9bW/mYSR1uqaPFj2MHvT+y/aOUu6msJijpKt5SkTDKySwg65OWG2JwTMBlgcbwMHrQ==
|
integrity sha512-7qmhptnR0WMSpxT5rMHG9bW/mYSR1uqaPFj2MHvT+y/aOUu6msJijpKt5SkTDKySwg65OWG2JwTMBlgcbwMHrQ==
|
||||||
|
|
||||||
"photoswipe@file:./thirdparty/photoswipe":
|
"photoswipe@file:./thirdparty/photoswipe":
|
||||||
version "4.1.3"
|
version "4.1.4"
|
||||||
|
|
||||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3:
|
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
|
|
Loading…
Reference in a new issue