diff --git a/package.json b/package.json
index 0f589b5a6..b2ce5421a 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"axios": "^0.21.3",
+ "bip39": "^3.0.4",
"bootstrap": "^4.5.2",
"chrono-node": "^2.2.6",
"comlink": "^4.3.0",
diff --git a/src/components/EnteDateTimePicker.tsx b/src/components/EnteDateTimePicker.tsx
new file mode 100644
index 000000000..de4d37f71
--- /dev/null
+++ b/src/components/EnteDateTimePicker.tsx
@@ -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 }) => (
+
+);
+
+export default EnteDateTimePicker;
diff --git a/src/components/FixCreationTime/footer.tsx b/src/components/FixCreationTime/footer.tsx
new file mode 100644
index 000000000..133e9a92f
--- /dev/null
+++ b/src/components/FixCreationTime/footer.tsx
@@ -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 && (
+
+ {(fixState === FIX_STATE.NOT_STARTED ||
+ fixState === FIX_STATE.COMPLETED_WITH_ERRORS) && (
+
+ )}
+ {fixState === FIX_STATE.COMPLETED && (
+
+ )}
+ {(fixState === FIX_STATE.NOT_STARTED ||
+ fixState === FIX_STATE.COMPLETED_WITH_ERRORS) && (
+ <>
+
+
+
+ >
+ )}
+
+ )
+ );
+}
diff --git a/src/components/FixCreationTime/index.tsx b/src/components/FixCreationTime/index.tsx
new file mode 100644
index 000000000..1e39fde44
--- /dev/null
+++ b/src/components/FixCreationTime/index.tsx
@@ -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 ? {message}
: <>>;
+}
+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 (
+
+
+
+
+ {fixState === FIX_STATE.RUNNING && (
+
+ )}
+
+ 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) && (
+
+
+
+ )}
+
+ >
+ )}
+
+
+
+ );
+}
diff --git a/src/components/FixCreationTime/options.tsx b/src/components/FixCreationTime/options.tsx
new file mode 100644
index 000000000..673260aef
--- /dev/null
+++ b/src/components/FixCreationTime/options.tsx
@@ -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) => void;
+ label: string;
+}) => (
+
+
+
+ {label}
+
+
+);
+
+export default function FixCreationTimeOptions({ handleChange, values }) {
+ return (
+
+ );
+}
diff --git a/src/components/FixCreationTime/running.tsx b/src/components/FixCreationTime/running.tsx
new file mode 100644
index 000000000..c04a20733
--- /dev/null
+++ b/src/components/FixCreationTime/running.tsx
@@ -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 (
+ <>
+
+
+ {' '}
+ {progressTracker.current} / {progressTracker.total}{' '}
+ {' '}
+
+ {' '}
+ {constants.CREATION_TIME_UPDATED}
+
+
+
+ >
+ );
+}
diff --git a/src/components/FixLargeThumbnail.tsx b/src/components/FixLargeThumbnail.tsx
index bc9f6f2c2..9d574901e 100644
--- a/src/components/FixLargeThumbnail.tsx
+++ b/src/components/FixLargeThumbnail.tsx
@@ -189,7 +189,8 @@ export default function FixLargeThumbnails(props: Props) {
display: 'flex',
justifyContent: 'space-around',
}}>
- {fixState === FIX_STATE.NOT_STARTED ? (
+ {fixState === FIX_STATE.NOT_STARTED ||
+ fixState === FIX_STATE.FIX_LATER ? (
) : (
>
)}
diff --git a/src/components/PhotoFrame.tsx b/src/components/PhotoFrame.tsx
index ffd4bf8ad..b91ea03ca 100644
--- a/src/components/PhotoFrame.tsx
+++ b/src/components/PhotoFrame.tsx
@@ -152,6 +152,8 @@ const PhotoFrame = ({
.map((item, index) => ({
...item,
dataIndex: index,
+ w: window.innerWidth,
+ h: window.innerHeight,
...(item.deleteBy && {
title: constants.AUTOMATIC_BIN_DELETE_MESSAGE(
formatDateRelative(item.deleteBy / 1000)
@@ -352,7 +354,7 @@ const PhotoFrame = ({
if (galleryContext.thumbs.has(item.id)) {
url = galleryContext.thumbs.get(item.id);
} else {
- url = await DownloadManager.getPreview(item);
+ url = await DownloadManager.getThumbnail(item);
galleryContext.thumbs.set(item.id, url);
}
updateUrl(item.dataIndex)(url);
diff --git a/src/components/PhotoSwipe/PhotoSwipe.tsx b/src/components/PhotoSwipe/PhotoSwipe.tsx
index a7f90d77a..58fd6852b 100644
--- a/src/components/PhotoSwipe/PhotoSwipe.tsx
+++ b/src/components/PhotoSwipe/PhotoSwipe.tsx
@@ -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) => (
);
-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({
{isInEditMode ? (
-
+
) : (
formatDateTime(pickedTime)
)}
diff --git a/src/components/RecoveryKeyModal.tsx b/src/components/RecoveryKeyModal.tsx
index d4cd01682..6a4afffb9 100644
--- a/src/components/RecoveryKeyModal.tsx
+++ b/src/components/RecoveryKeyModal.tsx
@@ -5,7 +5,9 @@ import constants from 'utils/strings/constants';
import MessageDialog from './MessageDialog';
import EnteSpinner from './EnteSpinner';
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 }>`
display: flex;
align-items: center;
@@ -42,7 +44,7 @@ function RecoveryKeyModal({ somethingWentWrong, ...props }: Props) {
somethingWentWrong();
props.onHide();
}
- setRecoveryKey(recoveryKey);
+ setRecoveryKey(bip39.entropyToMnemonic(recoveryKey));
};
main();
}, [props.show]);
diff --git a/src/components/icons/ClockIcon.tsx b/src/components/icons/ClockIcon.tsx
new file mode 100644
index 000000000..468d165a0
--- /dev/null
+++ b/src/components/icons/ClockIcon.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+
+export default function ClockIcon(props) {
+ return (
+
+ );
+}
+
+ClockIcon.defaultProps = {
+ height: 20,
+ width: 20,
+ viewBox: '0 0 24 24',
+};
diff --git a/src/components/icons/DownloadIcon.tsx b/src/components/icons/DownloadIcon.tsx
new file mode 100644
index 000000000..0172c6dc2
--- /dev/null
+++ b/src/components/icons/DownloadIcon.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+export default function DownloadIcon(props) {
+ return (
+
+ );
+}
+
+DownloadIcon.defaultProps = {
+ height: 24,
+ width: 24,
+ viewBox: '0 0 24 24',
+};
diff --git a/src/components/pages/gallery/PreviewCard.tsx b/src/components/pages/gallery/PreviewCard.tsx
index b04963826..ddb4f7de0 100644
--- a/src/components/pages/gallery/PreviewCard.tsx
+++ b/src/components/pages/gallery/PreviewCard.tsx
@@ -177,7 +177,7 @@ export default function PreviewCard(props: IProps) {
if (file && !file.msrc) {
const main = async () => {
try {
- const url = await DownloadManager.getPreview(file);
+ const url = await DownloadManager.getThumbnail(file);
if (isMounted.current) {
setImgSrc(url);
thumbs.set(file.id, url);
diff --git a/src/components/pages/gallery/SelectedFileOptions.tsx b/src/components/pages/gallery/SelectedFileOptions.tsx
index 2c9276954..7985e5a1c 100644
--- a/src/components/pages/gallery/SelectedFileOptions.tsx
+++ b/src/components/pages/gallery/SelectedFileOptions.tsx
@@ -1,5 +1,5 @@
import { SetDialogMessage } from 'components/MessageDialog';
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import { SetCollectionSelectorAttributes } from './CollectionSelector';
import styled from 'styled-components';
import Navbar from 'components/Navbar';
@@ -17,6 +17,13 @@ import { OverlayTrigger } from 'react-bootstrap';
import { Collection } from 'services/collectionService';
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_VISIBLE_TO_USER_IDS,
+ User,
+} from 'services/userService';
+import DownloadIcon from 'components/icons/DownloadIcon';
interface Props {
addToCollectionHelper: (collection: Collection) => void;
@@ -27,6 +34,8 @@ interface Props {
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
deleteFileHelper: (permanent?: boolean) => void;
removeFromCollectionHelper: () => void;
+ fixTimeHelper: () => void;
+ downloadHelper: () => void;
count: number;
clearSelection: () => void;
archiveFilesHelper: () => void;
@@ -68,9 +77,11 @@ const SelectedFileOptions = ({
restoreToCollectionHelper,
showCreateCollectionModal,
removeFromCollectionHelper,
+ fixTimeHelper,
setDialogMessage,
setCollectionSelectorAttributes,
deleteFileHelper,
+ downloadHelper,
count,
clearSelection,
archiveFilesHelper,
@@ -78,6 +89,13 @@ const SelectedFileOptions = ({
activeCollection,
isFavoriteCollection,
}: 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 = () =>
setCollectionSelectorAttributes({
callback: addToCollectionHelper,
@@ -168,6 +186,23 @@ const SelectedFileOptions = ({
>
) : (
<>
+ {showFixCreationTime && (
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
{activeCollection === ARCHIVE_SECTION && (
@@ -182,11 +217,7 @@ const SelectedFileOptions = ({
)}
-
-
-
-
-
+
{activeCollection !== ALL_SECTION &&
activeCollection !== ARCHIVE_SECTION &&
!isFavoriteCollection && (
diff --git a/src/components/pages/gallery/Upload.tsx b/src/components/pages/gallery/Upload.tsx
index 7d4095f0a..df694188b 100644
--- a/src/components/pages/gallery/Upload.tsx
+++ b/src/components/pages/gallery/Upload.tsx
@@ -134,7 +134,8 @@ export default function Upload(props: Props) {
return null;
}
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 lastPath = paths[paths.length - 1];
const L = firstPath.length;
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 8ba26963f..5d170715b 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -479,6 +479,8 @@ type AppContextType = {
sharedFiles: File[];
resetSharedFiles: () => void;
setDisappearingFlashMessage: (message: FlashMessage) => void;
+ redirectUrl: string;
+ setRedirectUrl: (url: string) => void;
};
export enum FLASH_MESSAGE_TYPE {
@@ -508,6 +510,7 @@ export default function App({ Component, err }) {
const [sharedFiles, setSharedFiles] = useState(null);
const [redirectName, setRedirectName] = useState(null);
const [flashMessage, setFlashMessage] = useState(null);
+ const [redirectUrl, setRedirectUrl] = useState(null);
useEffect(() => {
if (
!('serviceWorker' in navigator) ||
@@ -641,6 +644,8 @@ export default function App({ Component, err }) {
sharedFiles,
resetSharedFiles,
setDisappearingFlashMessage,
+ redirectUrl,
+ setRedirectUrl,
}}>
{loading ? (
diff --git a/src/pages/credentials/index.tsx b/src/pages/credentials/index.tsx
index 9b92b07ed..fff9be33f 100644
--- a/src/pages/credentials/index.tsx
+++ b/src/pages/credentials/index.tsx
@@ -75,8 +75,9 @@ export default function Credentials() {
}
await SaveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, key);
await decryptAndStoreToken(key);
-
- router.push(PAGES.GALLERY);
+ const redirectUrl = appContext.redirectUrl;
+ appContext.setRedirectUrl(null);
+ router.push(redirectUrl ?? PAGES.GALLERY);
} catch (e) {
logError(e, 'user entered a wrong password');
setFieldError('passphrase', constants.INCORRECT_PASSPHRASE);
diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx
index e16145f72..3f3976357 100644
--- a/src/pages/gallery/index.tsx
+++ b/src/pages/gallery/index.tsx
@@ -50,6 +50,8 @@ import { LoadingOverlay } from 'components/LoadingOverlay';
import PhotoFrame from 'components/PhotoFrame';
import {
changeFilesVisibility,
+ downloadFiles,
+ getNonTrashedUniqueUserFiles,
getSelectedFiles,
mergeMetadata,
sortFiles,
@@ -93,6 +95,9 @@ import {
Trash,
} from 'services/trashService';
import DeleteBtn from 'components/DeleteBtn';
+import FixCreationTime, {
+ FixCreationTimeAttributes,
+} from 'components/FixCreationTime';
export const DeadCenter = styled.div`
flex: 1;
@@ -204,10 +209,14 @@ export default function Gallery() {
useState