Merge pull request #240 from ente-io/master

release
This commit is contained in:
Vishnu Mohandas 2021-11-19 02:01:41 +05:30 committed by GitHub
commit d5ba497fc0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 432 additions and 226 deletions

View 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;

View file

@ -1,172 +0,0 @@
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>
);
}

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

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

View file

@ -0,0 +1,79 @@
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
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>
<Option
value={FIX_OPTIONS.DATE_TIME_ORIGINAL}
onChange={handleChange('option')}
label={constants.DATE_TIME_ORIGINAL}
selected={Number(values.option)}
/>
<Option
value={FIX_OPTIONS.DATE_TIME_DIGITIZED}
onChange={handleChange('option')}
label={constants.DATE_TIME_DIGITIZED}
selected={Number(values.option)}
/>
<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>
);
}

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

View file

@ -8,11 +8,8 @@ import {
removeFromFavorites,
} from 'services/collectionService';
import {
ALL_TIME,
File,
MAX_EDITED_FILE_NAME_LENGTH,
MAX_EDITED_CREATION_TIME,
MIN_EDITED_CREATION_TIME,
updatePublicMagicMetadata,
} from 'services/fileService';
import constants from 'utils/strings/constants';
@ -41,14 +38,13 @@ import {
} from 'components/Container';
import { logError } from 'utils/sentry';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import CloseIcon from 'components/icons/CloseIcon';
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';
import EnteDateTimePicker from 'components/EnteDateTimePicker';
interface Iprops {
isOpen: boolean;
@ -87,11 +83,6 @@ const renderInfoItem = (label: string, value: string | JSX.Element) => (
</Row>
);
const isSameDay = (first, second) =>
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate();
function RenderCreationTime({
file,
scheduleUpdate,
@ -145,24 +136,11 @@ function RenderCreationTime({
<Label width="30%">{constants.CREATION_TIME}</Label>
<Value width={isInEditMode ? '50%' : '60%'}>
{isInEditMode ? (
<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>
<EnteDateTimePicker
isInEditMode={isInEditMode}
pickedTime={pickedTime}
handleChange={handleChange}
/>
) : (
formatDateTime(pickedTime)
)}

View file

@ -19,7 +19,10 @@ import RemoveIcon from 'components/icons/RemoveIcon';
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';
import {
FIX_CREATION_TIME_VISIBLE_TO_USER_IDS,
User,
} from 'services/userService';
interface Props {
addToCollectionHelper: (collection: Collection) => void;
@ -86,7 +89,8 @@ const SelectedFileOptions = ({
const [showFixCreationTime, setShowFixCreationTime] = useState(false);
useEffect(() => {
const user: User = getData(LS_KEYS.USER);
const showFixCreationTime = user?.id === FIX_CREATION_TIME_USER_ID;
const showFixCreationTime =
FIX_CREATION_TIME_VISIBLE_TO_USER_IDS.includes(user?.id);
setShowFixCreationTime(showFixCreationTime);
}, []);
const addToCollection = () =>

View file

@ -1,3 +1,4 @@
import { FIX_OPTIONS } from 'components/FixCreationTime';
import { SetProgressTracker } from 'components/FixLargeThumbnail';
import CryptoWorker from 'utils/crypto';
import {
@ -8,11 +9,13 @@ import {
import { logError } from 'utils/sentry';
import downloadManager from './downloadManager';
import { File, FILE_TYPE, updatePublicMagicMetadata } from './fileService';
import { getExifData } from './upload/exifService';
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;
@ -26,18 +29,30 @@ export async function updateCreationTimeWithExif(
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);
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 (
exifData?.creationTime &&
exifData?.creationTime !== file.metadata.creationTime
correctCreationTime &&
correctCreationTime !== file.metadata.creationTime
) {
let updatedFile = await changeFileCreationTime(
file,
exifData.creationTime
correctCreationTime
);
updatedFile = (
await updatePublicMagicMetadata([updatedFile])

View file

@ -13,6 +13,15 @@ const EXIF_TAGS_NEEDED = [
'GPSLatitudeRef',
'GPSLongitudeRef',
];
interface Exif {
DateTimeOriginal?: Date;
CreateDate?: Date;
ModifyDate?: Date;
GPSLatitude?: number;
GPSLongitude?: number;
GPSLatitudeRef?: number;
GPSLongitudeRef?: number;
}
interface ParsedEXIFData {
location: Location;
creationTime: number;
@ -22,7 +31,26 @@ export async function getExifData(
receivedFile: globalThis.File,
fileTypeInfo: FileTypeInfo
): Promise<ParsedEXIFData> {
let exifData;
const exifData = await getRawExif(receivedFile, fileTypeInfo);
if (!exifData) {
return { location: NULL_LOCATION, creationTime: null };
}
const parsedEXIFData = {
location: getEXIFLocation(exifData),
creationTime: getUNIXTime(
exifData.DateTimeOriginal ??
exifData.CreateDate ??
exifData.ModifyDate
),
};
return parsedEXIFData;
}
export async function getRawExif(
receivedFile: File,
fileTypeInfo: FileTypeInfo
) {
let exifData: Exif;
try {
exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED);
} catch (e) {
@ -31,20 +59,10 @@ export async function getExifData(
});
// ignore exif parsing errors
}
if (!exifData) {
return { location: NULL_LOCATION, creationTime: null };
}
const parsedEXIFData = {
location: getEXIFLocation(exifData),
creationTime: getUNIXTime(exifData),
};
return parsedEXIFData;
return exifData;
}
function getUNIXTime(exifData: any) {
const dateTime: Date =
exifData.DateTimeOriginal ?? exifData.CreateDate ?? exifData.ModifyDate;
export function getUNIXTime(dateTime: Date) {
if (!dateTime) {
return null;
}

View file

@ -28,7 +28,7 @@ const ENDPOINT = getEndpoint();
const HAS_SET_KEYS = 'hasSetKeys';
export const FIX_CREATION_TIME_USER_ID = 341;
export const FIX_CREATION_TIME_VISIBLE_TO_USER_IDS = [1, 125, 341];
export interface User {
id: number;

View file

@ -601,7 +601,7 @@ const englishConstants = {
CREATION_TIME_UPDATED: `file time updated`,
UPDATE_CREATION_TIME_NOT_STARTED: () => (
<>do you want to fix time with the values found in EXIF</>
<>select the option you want to use</>
),
UPDATE_CREATION_TIME_COMPLETED: () => <>successfully updated all files</>,
@ -609,6 +609,10 @@ const englishConstants = {
<>file time updation failed for some files, please retry</>
),
FILE_NAME_CHARACTER_LIMIT: '100 characters max',
DATE_TIME_ORIGINAL: 'EXIF:DateTimeOriginal',
DATE_TIME_DIGITIZED: 'EXIF:DateTimeDigitized',
CUSTOM_TIME: 'custom time',
};
export default englishConstants;