commit
f63fbcc680
|
@ -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",
|
||||||
|
|
|
@ -2,16 +2,18 @@ import React from 'react';
|
||||||
import { Spinner } from 'react-bootstrap';
|
import { Spinner } from 'react-bootstrap';
|
||||||
|
|
||||||
export default function EnteSpinner(props) {
|
export default function EnteSpinner(props) {
|
||||||
|
const { style, ...others } = props ?? {};
|
||||||
return (
|
return (
|
||||||
<Spinner
|
<Spinner
|
||||||
{...props}
|
|
||||||
animation="border"
|
animation="border"
|
||||||
style={{
|
style={{
|
||||||
width: '36px',
|
width: '36px',
|
||||||
height: '36px',
|
height: '36px',
|
||||||
borderWidth: '0.20em',
|
borderWidth: '0.20em',
|
||||||
color: '#51cd7c',
|
color: '#51cd7c',
|
||||||
|
...(style && style),
|
||||||
}}
|
}}
|
||||||
|
{...others}
|
||||||
role="status"
|
role="status"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
172
src/components/FixCreationTime.tsx
Normal file
172
src/components/FixCreationTime.tsx
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
import MessageDialog from './MessageDialog';
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { ProgressBar, Button } from 'react-bootstrap';
|
||||||
|
import { ComfySpan } from './ExportInProgress';
|
||||||
|
import { updateCreationTimeWithExif } from 'services/updateCreationTimeWithExif';
|
||||||
|
import { GalleryContext } from 'pages/gallery';
|
||||||
|
import { File } from 'services/fileService';
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
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 () => {
|
||||||
|
setFixState(FIX_STATE.RUNNING);
|
||||||
|
const completedWithoutError = await updateCreationTimeWithExif(
|
||||||
|
props.attributes.files,
|
||||||
|
setProgressTracker
|
||||||
|
);
|
||||||
|
if (!completedWithoutError) {
|
||||||
|
setFixState(FIX_STATE.COMPLETED);
|
||||||
|
} else {
|
||||||
|
setFixState(FIX_STATE.COMPLETED_WITH_ERRORS);
|
||||||
|
}
|
||||||
|
await galleryContext.syncWithRemote();
|
||||||
|
};
|
||||||
|
if (!props.attributes) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<Message fixState={fixState} />
|
||||||
|
|
||||||
|
{fixState === FIX_STATE.RUNNING && (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</MessageDialog>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import {
|
import {
|
||||||
ALL_TIME,
|
ALL_TIME,
|
||||||
File,
|
File,
|
||||||
|
MAX_EDITED_FILE_NAME_LENGTH,
|
||||||
MAX_EDITED_CREATION_TIME,
|
MAX_EDITED_CREATION_TIME,
|
||||||
MIN_EDITED_CREATION_TIME,
|
MIN_EDITED_CREATION_TIME,
|
||||||
updatePublicMagicMetadata,
|
updatePublicMagicMetadata,
|
||||||
|
@ -22,20 +23,32 @@ import styled from 'styled-components';
|
||||||
import events from './events';
|
import events from './events';
|
||||||
import {
|
import {
|
||||||
changeFileCreationTime,
|
changeFileCreationTime,
|
||||||
|
changeFileName,
|
||||||
downloadFile,
|
downloadFile,
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
|
splitFilenameAndExtension,
|
||||||
updateExistingFilePubMetadata,
|
updateExistingFilePubMetadata,
|
||||||
} from 'utils/file';
|
} from 'utils/file';
|
||||||
import { FormCheck } from 'react-bootstrap';
|
import { Col, Form, FormCheck, FormControl } from 'react-bootstrap';
|
||||||
import { prettyPrintExif } from 'utils/exif';
|
import { prettyPrintExif } from 'utils/exif';
|
||||||
import EditIcon from 'components/icons/EditIcon';
|
import EditIcon from 'components/icons/EditIcon';
|
||||||
import { IconButton, Label, Row, Value } from 'components/Container';
|
import {
|
||||||
|
FlexWrapper,
|
||||||
|
IconButton,
|
||||||
|
Label,
|
||||||
|
Row,
|
||||||
|
Value,
|
||||||
|
} from 'components/Container';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
import DatePicker from 'react-datepicker';
|
import DatePicker from 'react-datepicker';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
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 { Formik } from 'formik';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import EnteSpinner from 'components/EnteSpinner';
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
@ -86,7 +99,7 @@ function RenderCreationTime({
|
||||||
file: File;
|
file: File;
|
||||||
scheduleUpdate: () => void;
|
scheduleUpdate: () => void;
|
||||||
}) {
|
}) {
|
||||||
const originalCreationTime = new Date(file.metadata.creationTime / 1000);
|
const originalCreationTime = new Date(file?.metadata.creationTime / 1000);
|
||||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||||
|
|
||||||
const [pickedTime, setPickedTime] = useState(originalCreationTime);
|
const [pickedTime, setPickedTime] = useState(originalCreationTime);
|
||||||
|
@ -98,7 +111,8 @@ function RenderCreationTime({
|
||||||
try {
|
try {
|
||||||
if (isInEditMode && file) {
|
if (isInEditMode && file) {
|
||||||
const unixTimeInMicroSec = pickedTime.getTime() * 1000;
|
const unixTimeInMicroSec = pickedTime.getTime() * 1000;
|
||||||
if (unixTimeInMicroSec === file.metadata.creationTime) {
|
if (unixTimeInMicroSec === file?.metadata.creationTime) {
|
||||||
|
closeEditMode();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let updatedFile = await changeFileCreationTime(
|
let updatedFile = await changeFileCreationTime(
|
||||||
|
@ -175,6 +189,170 @@ function RenderCreationTime({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const getFileTitle = (filename, extension) => {
|
||||||
|
if (extension) {
|
||||||
|
return filename + '.' + extension;
|
||||||
|
} else {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
interface formValues {
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileNameEditForm = ({ filename, saveEdits, discardEdits, extension }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const onSubmit = async (values: formValues) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await saveEdits(values.filename);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Formik<formValues>
|
||||||
|
initialValues={{ filename }}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
filename: Yup.string()
|
||||||
|
.required(constants.REQUIRED)
|
||||||
|
.max(
|
||||||
|
MAX_EDITED_FILE_NAME_LENGTH,
|
||||||
|
constants.FILE_NAME_CHARACTER_LIMIT
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
validateOnBlur={false}
|
||||||
|
onSubmit={onSubmit}>
|
||||||
|
{({ values, errors, handleChange, handleSubmit }) => (
|
||||||
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
|
<Form.Row>
|
||||||
|
<Form.Group
|
||||||
|
bsPrefix="ente-form-group"
|
||||||
|
as={Col}
|
||||||
|
xs={extension ? 7 : 8}>
|
||||||
|
<Form.Control
|
||||||
|
as="textarea"
|
||||||
|
placeholder={constants.FILE_NAME}
|
||||||
|
value={values.filename}
|
||||||
|
onChange={handleChange('filename')}
|
||||||
|
isInvalid={Boolean(errors.filename)}
|
||||||
|
autoFocus
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<FormControl.Feedback
|
||||||
|
type="invalid"
|
||||||
|
style={{ textAlign: 'center' }}>
|
||||||
|
{errors.filename}
|
||||||
|
</FormControl.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
{extension && (
|
||||||
|
<Form.Group
|
||||||
|
bsPrefix="ente-form-group"
|
||||||
|
as={Col}
|
||||||
|
xs={1}
|
||||||
|
controlId="formHorizontalFileName">
|
||||||
|
<FlexWrapper style={{ padding: '5px' }}>
|
||||||
|
{`.${extension}`}
|
||||||
|
</FlexWrapper>
|
||||||
|
</Form.Group>
|
||||||
|
)}
|
||||||
|
<Form.Group bsPrefix="ente-form-group" as={Col} xs={2}>
|
||||||
|
<Value width={'16.67%'}>
|
||||||
|
<IconButton type="submit" disabled={loading}>
|
||||||
|
{loading ? (
|
||||||
|
<EnteSpinner
|
||||||
|
style={{
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TickIcon />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={discardEdits}
|
||||||
|
disabled={loading}>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Value>
|
||||||
|
</Form.Group>
|
||||||
|
</Form.Row>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function RenderFileName({
|
||||||
|
file,
|
||||||
|
scheduleUpdate,
|
||||||
|
}: {
|
||||||
|
file: File;
|
||||||
|
scheduleUpdate: () => void;
|
||||||
|
}) {
|
||||||
|
const originalTitle = file?.metadata.title;
|
||||||
|
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||||
|
const [originalFileName, extension] =
|
||||||
|
splitFilenameAndExtension(originalTitle);
|
||||||
|
const [filename, setFilename] = useState(originalFileName);
|
||||||
|
const openEditMode = () => setIsInEditMode(true);
|
||||||
|
const closeEditMode = () => setIsInEditMode(false);
|
||||||
|
|
||||||
|
const saveEdits = async (newFilename: string) => {
|
||||||
|
try {
|
||||||
|
if (file) {
|
||||||
|
if (filename === newFilename) {
|
||||||
|
closeEditMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFilename(newFilename);
|
||||||
|
const newTitle = getFileTitle(newFilename, extension);
|
||||||
|
let updatedFile = await changeFileName(file, newTitle);
|
||||||
|
updatedFile = (
|
||||||
|
await updatePublicMagicMetadata([updatedFile])
|
||||||
|
)[0];
|
||||||
|
updateExistingFilePubMetadata(file, updatedFile);
|
||||||
|
scheduleUpdate();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'failed to update file name');
|
||||||
|
} finally {
|
||||||
|
closeEditMode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Row>
|
||||||
|
<Label width="30%">{constants.FILE_NAME}</Label>
|
||||||
|
{!isInEditMode ? (
|
||||||
|
<>
|
||||||
|
<Value width="60%">
|
||||||
|
<FreeFlowText>
|
||||||
|
{getFileTitle(filename, extension)}
|
||||||
|
</FreeFlowText>
|
||||||
|
</Value>
|
||||||
|
<Value
|
||||||
|
width="10%"
|
||||||
|
style={{ cursor: 'pointer', marginLeft: '10px' }}>
|
||||||
|
<IconButton onClick={openEditMode}>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Value>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<FileNameEditForm
|
||||||
|
extension={extension}
|
||||||
|
filename={filename}
|
||||||
|
saveEdits={saveEdits}
|
||||||
|
discardEdits={closeEditMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
function ExifData(props: { exif: any }) {
|
function ExifData(props: { exif: any }) {
|
||||||
const { exif } = props;
|
const { exif } = props;
|
||||||
const [showAll, setShowAll] = useState(false);
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
@ -250,8 +428,12 @@ function InfoModal({
|
||||||
constants.FILE_ID,
|
constants.FILE_ID,
|
||||||
items[photoSwipe?.getCurrentIndex()]?.id
|
items[photoSwipe?.getCurrentIndex()]?.id
|
||||||
)}
|
)}
|
||||||
{metadata?.title &&
|
{metadata?.title && (
|
||||||
renderInfoItem(constants.FILE_NAME, metadata.title)}
|
<RenderFileName
|
||||||
|
file={items[photoSwipe?.getCurrentIndex()]}
|
||||||
|
scheduleUpdate={scheduleUpdate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{metadata?.creationTime && (
|
{metadata?.creationTime && (
|
||||||
<RenderCreationTime
|
<RenderCreationTime
|
||||||
file={items[photoSwipe?.getCurrentIndex()]}
|
file={items[photoSwipe?.getCurrentIndex()]}
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -23,6 +25,7 @@ export const FreeFlowText = styled.div`
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
min-width: 30%;
|
min-width: 30%;
|
||||||
|
text-align: left;
|
||||||
`;
|
`;
|
||||||
interface Props {
|
interface Props {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
|
@ -41,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',
|
||||||
|
};
|
|
@ -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,9 @@ 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_USER_ID, User } from 'services/userService';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
addToCollectionHelper: (collection: Collection) => void;
|
addToCollectionHelper: (collection: Collection) => void;
|
||||||
|
@ -27,6 +30,7 @@ interface Props {
|
||||||
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
|
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
|
||||||
deleteFileHelper: (permanent?: boolean) => void;
|
deleteFileHelper: (permanent?: boolean) => void;
|
||||||
removeFromCollectionHelper: () => void;
|
removeFromCollectionHelper: () => void;
|
||||||
|
fixTimeHelper: () => void;
|
||||||
count: number;
|
count: number;
|
||||||
clearSelection: () => void;
|
clearSelection: () => void;
|
||||||
archiveFilesHelper: () => void;
|
archiveFilesHelper: () => void;
|
||||||
|
@ -68,6 +72,7 @@ const SelectedFileOptions = ({
|
||||||
restoreToCollectionHelper,
|
restoreToCollectionHelper,
|
||||||
showCreateCollectionModal,
|
showCreateCollectionModal,
|
||||||
removeFromCollectionHelper,
|
removeFromCollectionHelper,
|
||||||
|
fixTimeHelper,
|
||||||
setDialogMessage,
|
setDialogMessage,
|
||||||
setCollectionSelectorAttributes,
|
setCollectionSelectorAttributes,
|
||||||
deleteFileHelper,
|
deleteFileHelper,
|
||||||
|
@ -78,6 +83,12 @@ const SelectedFileOptions = ({
|
||||||
activeCollection,
|
activeCollection,
|
||||||
isFavoriteCollection,
|
isFavoriteCollection,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [showFixCreationTime, setShowFixCreationTime] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const user: User = getData(LS_KEYS.USER);
|
||||||
|
const showFixCreationTime = user?.id === FIX_CREATION_TIME_USER_ID;
|
||||||
|
setShowFixCreationTime(showFixCreationTime);
|
||||||
|
}, []);
|
||||||
const addToCollection = () =>
|
const addToCollection = () =>
|
||||||
setCollectionSelectorAttributes({
|
setCollectionSelectorAttributes({
|
||||||
callback: addToCollectionHelper,
|
callback: addToCollectionHelper,
|
||||||
|
@ -168,6 +179,18 @@ const SelectedFileOptions = ({
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{showFixCreationTime && (
|
||||||
|
<IconWithMessage message={constants.FIX_CREATION_TIME}>
|
||||||
|
<IconButton onClick={fixTimeHelper}>
|
||||||
|
<ClockIcon />
|
||||||
|
</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 +205,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 && (
|
||||||
|
|
|
@ -446,6 +446,9 @@ const GlobalStyles = createGlobalStyle`
|
||||||
.react-datepicker__day--disabled:hover {
|
.react-datepicker__day--disabled:hover {
|
||||||
background-color: #202020;
|
background-color: #202020;
|
||||||
}
|
}
|
||||||
|
.ente-form-group{
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LogoImage = styled.img`
|
export const LogoImage = styled.img`
|
||||||
|
|
|
@ -93,6 +93,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,7 +207,9 @@ 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) {
|
||||||
|
@ -243,13 +248,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') {
|
||||||
|
@ -523,6 +534,12 @@ export default function Gallery() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fixTimeHelper = async () => {
|
||||||
|
const selectedFiles = getSelectedFiles(selected, files);
|
||||||
|
setFixCreationTimeAttributes({ files: selectedFiles });
|
||||||
|
clearSelection();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GalleryContext.Provider
|
<GalleryContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -594,6 +611,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 +708,7 @@ export default function Gallery() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fixTimeHelper={fixTimeHelper}
|
||||||
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,
|
||||||
|
|
|
@ -33,12 +33,17 @@ class FFmpegService {
|
||||||
const response = this.generateThumbnailProcessor.queueUpRequest(
|
const response = this.generateThumbnailProcessor.queueUpRequest(
|
||||||
generateThumbnailHelper.bind(null, this.ffmpeg, file)
|
generateThumbnailHelper.bind(null, this.ffmpeg, file)
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
const thumbnail = await response.promise;
|
return await response.promise;
|
||||||
if (!thumbnail) {
|
} catch (e) {
|
||||||
throw Error(CustomError.THUMBNAIL_GENERATION_FAILED);
|
if (e.message === CustomError.REQUEST_CANCELLED) {
|
||||||
|
// ignore
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
logError(e, 'ffmpeg thumbnail generation failed');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return thumbnail;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,8 @@ export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
|
||||||
export const MAX_EDITED_CREATION_TIME = new Date();
|
export const MAX_EDITED_CREATION_TIME = new Date();
|
||||||
export const ALL_TIME = new Date(1800, 0, 1, 23, 59, 59);
|
export const ALL_TIME = new Date(1800, 0, 1, 23, 59, 59);
|
||||||
|
|
||||||
|
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
|
||||||
|
|
||||||
export interface fileAttribute {
|
export interface fileAttribute {
|
||||||
encryptedData?: DataStream | Uint8Array;
|
encryptedData?: DataStream | Uint8Array;
|
||||||
objectKey?: string;
|
objectKey?: string;
|
||||||
|
@ -72,6 +74,7 @@ export interface MagicMetadata extends Omit<MagicMetadataCore, 'data'> {
|
||||||
|
|
||||||
export interface PublicMagicMetadataProps {
|
export interface PublicMagicMetadataProps {
|
||||||
editedTime?: number;
|
editedTime?: number;
|
||||||
|
editedName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PublicMagicMetadata extends Omit<MagicMetadataCore, 'data'> {
|
export interface PublicMagicMetadata extends Omit<MagicMetadataCore, 'data'> {
|
||||||
|
@ -147,7 +150,7 @@ export const syncFiles = async (
|
||||||
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
||||||
if (files.length !== localFiles.length) {
|
if (files.length !== localFiles.length) {
|
||||||
await setLocalFiles(files);
|
await setLocalFiles(files);
|
||||||
setFiles(sortFiles(mergeMetadata(files)));
|
setFiles([...sortFiles(mergeMetadata(files))]);
|
||||||
}
|
}
|
||||||
for (const collection of collections) {
|
for (const collection of collections) {
|
||||||
if (!getToken()) {
|
if (!getToken()) {
|
||||||
|
@ -183,9 +186,9 @@ export const syncFiles = async (
|
||||||
`${collection.id}-time`,
|
`${collection.id}-time`,
|
||||||
collection.updationTime
|
collection.updationTime
|
||||||
);
|
);
|
||||||
setFiles(sortFiles(mergeMetadata(files)));
|
setFiles([...sortFiles(mergeMetadata(files))]);
|
||||||
}
|
}
|
||||||
return mergeMetadata(files);
|
return sortFiles(mergeMetadata(files));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFiles = async (
|
export const getFiles = async (
|
||||||
|
|
62
src/services/updateCreationTimeWithExif.ts
Normal file
62
src/services/updateCreationTimeWithExif.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
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 { getExifData } from './upload/exifService';
|
||||||
|
import { getFileType } from './upload/readFileService';
|
||||||
|
|
||||||
|
export async function updateCreationTimeWithExif(
|
||||||
|
filesToBeUpdated: File[],
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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 getExifData(fileObject, fileTypeInfo);
|
||||||
|
if (
|
||||||
|
exifData?.creationTime &&
|
||||||
|
exifData?.creationTime !== file.metadata.creationTime
|
||||||
|
) {
|
||||||
|
let updatedFile = await changeFileCreationTime(
|
||||||
|
file,
|
||||||
|
exifData.creationTime
|
||||||
|
);
|
||||||
|
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',
|
||||||
|
@ -17,9 +19,18 @@ interface ParsedEXIFData {
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
let exifData;
|
||||||
|
try {
|
||||||
|
exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'file missing exif data ', {
|
||||||
|
fileType: fileTypeInfo.exactType,
|
||||||
|
});
|
||||||
|
// ignore exif parsing errors
|
||||||
|
}
|
||||||
if (!exifData) {
|
if (!exifData) {
|
||||||
return { location: NULL_LOCATION, creationTime: null };
|
return { location: NULL_LOCATION, creationTime: null };
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import { CustomError } from 'utils/common/errorUtil';
|
||||||
|
|
||||||
interface RequestQueueItem {
|
interface RequestQueueItem {
|
||||||
request: (canceller?: RequestCanceller) => Promise<any>;
|
request: (canceller?: RequestCanceller) => Promise<any>;
|
||||||
callback: (response) => void;
|
successCallback: (response: any) => void;
|
||||||
|
failureCallback: (error: Error) => void;
|
||||||
isCanceled: { status: boolean };
|
isCanceled: { status: boolean };
|
||||||
canceller: { exec: () => void };
|
canceller: { exec: () => void };
|
||||||
}
|
}
|
||||||
|
@ -26,10 +29,11 @@ export default class QueueProcessor<T> {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const promise = new Promise<T>((resolve) => {
|
const promise = new Promise<T>((resolve, reject) => {
|
||||||
this.requestQueue.push({
|
this.requestQueue.push({
|
||||||
request,
|
request,
|
||||||
callback: resolve,
|
successCallback: resolve,
|
||||||
|
failureCallback: reject,
|
||||||
isCanceled,
|
isCanceled,
|
||||||
canceller,
|
canceller,
|
||||||
});
|
});
|
||||||
|
@ -53,15 +57,15 @@ export default class QueueProcessor<T> {
|
||||||
let response = null;
|
let response = null;
|
||||||
|
|
||||||
if (queueItem.isCanceled.status) {
|
if (queueItem.isCanceled.status) {
|
||||||
response = null;
|
queueItem.failureCallback(Error(CustomError.REQUEST_CANCELLED));
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
response = await queueItem.request(queueItem.canceller);
|
response = await queueItem.request(queueItem.canceller);
|
||||||
|
queueItem.successCallback(response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
response = null;
|
queueItem.failureCallback(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
queueItem.callback(response);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ export async function generateThumbnail(
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'uploading static thumbnail', {
|
logError(e, 'uploading static thumbnail', {
|
||||||
type: fileTypeInfo.exactType,
|
fileFormat: fileTypeInfo.exactType,
|
||||||
});
|
});
|
||||||
thumbnail = Uint8Array.from(atob(BLACK_THUMBNAIL_BASE64), (c) =>
|
thumbnail = Uint8Array.from(atob(BLACK_THUMBNAIL_BASE64), (c) =>
|
||||||
c.charCodeAt(0)
|
c.charCodeAt(0)
|
||||||
|
|
|
@ -28,6 +28,8 @@ const ENDPOINT = getEndpoint();
|
||||||
|
|
||||||
const HAS_SET_KEYS = 'hasSetKeys';
|
const HAS_SET_KEYS = 'hasSetKeys';
|
||||||
|
|
||||||
|
export const FIX_CREATION_TIME_USER_ID = 341;
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -28,6 +28,7 @@ export enum CustomError {
|
||||||
FAV_COLLECTION_MISSING = 'favorite collection missing',
|
FAV_COLLECTION_MISSING = 'favorite collection missing',
|
||||||
INVALID_COLLECTION_OPERATION = 'invalid collection operation',
|
INVALID_COLLECTION_OPERATION = 'invalid collection operation',
|
||||||
WAIT_TIME_EXCEEDED = 'thumbnail generation wait time exceeded',
|
WAIT_TIME_EXCEEDED = 'thumbnail generation wait time exceeded',
|
||||||
|
REQUEST_CANCELLED = 'request canceled',
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseUploadError(error: AxiosResponse) {
|
function parseUploadError(error: AxiosResponse) {
|
||||||
|
|
|
@ -240,6 +240,16 @@ export function fileExtensionWithDot(filename) {
|
||||||
else return filename.substr(lastDotPosition);
|
else return filename.substr(lastDotPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function splitFilenameAndExtension(filename): [string, string] {
|
||||||
|
const lastDotPosition = filename.lastIndexOf('.');
|
||||||
|
if (lastDotPosition === -1) return [filename, null];
|
||||||
|
else
|
||||||
|
return [
|
||||||
|
filename.substr(0, lastDotPosition),
|
||||||
|
filename.substr(lastDotPosition + 1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export function generateStreamFromArrayBuffer(data: Uint8Array) {
|
export function generateStreamFromArrayBuffer(data: Uint8Array) {
|
||||||
return new ReadableStream({
|
return new ReadableStream({
|
||||||
async start(controller: ReadableStreamDefaultController) {
|
async start(controller: ReadableStreamDefaultController) {
|
||||||
|
@ -381,6 +391,17 @@ export async function changeFileCreationTime(file: File, editedTime: number) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function changeFileName(file: File, editedName: string) {
|
||||||
|
const updatedPublicMagicMetadataProps: PublicMagicMetadataProps = {
|
||||||
|
editedName,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await updatePublicMagicMetadataProps(
|
||||||
|
file,
|
||||||
|
updatedPublicMagicMetadataProps
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function isSharedFile(file: File) {
|
export function isSharedFile(file: File) {
|
||||||
const user: User = getData(LS_KEYS.USER);
|
const user: User = getData(LS_KEYS.USER);
|
||||||
|
|
||||||
|
@ -400,6 +421,9 @@ export function mergeMetadata(files: File[]): File[] {
|
||||||
...(file.pubMagicMetadata?.data.editedTime && {
|
...(file.pubMagicMetadata?.data.editedTime && {
|
||||||
creationTime: file.pubMagicMetadata.data.editedTime,
|
creationTime: file.pubMagicMetadata.data.editedTime,
|
||||||
}),
|
}),
|
||||||
|
...(file.pubMagicMetadata?.data.editedName && {
|
||||||
|
title: file.pubMagicMetadata.data.editedName,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(file.magicMetadata?.data ? file.magicMetadata.data : {}),
|
...(file.magicMetadata?.data ? file.magicMetadata.data : {}),
|
||||||
|
@ -414,3 +438,9 @@ 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;
|
||||||
|
}
|
||||||
|
|
|
@ -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,6 +596,19 @@ 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: () => (
|
||||||
|
<>do you want to fix time with the values found in EXIF</>
|
||||||
|
),
|
||||||
|
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',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default englishConstants;
|
export default englishConstants;
|
||||||
|
|
17
yarn.lock
17
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==
|
||||||
|
|
Loading…
Reference in a new issue