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/EnteSpinner.tsx b/src/components/EnteSpinner.tsx
index 35f195675..054a6c62f 100644
--- a/src/components/EnteSpinner.tsx
+++ b/src/components/EnteSpinner.tsx
@@ -2,16 +2,18 @@ import React from 'react';
import { Spinner } from 'react-bootstrap';
export default function EnteSpinner(props) {
+ const { style, ...others } = props ?? {};
return (
);
diff --git a/src/components/FixCreationTime.tsx b/src/components/FixCreationTime.tsx
new file mode 100644
index 000000000..25ff329ad
--- /dev/null
+++ b/src/components/FixCreationTime.tsx
@@ -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 ?
{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 () => {
+ 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 (
+
+
+
+
+ {fixState === FIX_STATE.RUNNING && (
+ <>
+
+
+ {' '}
+ {progressTracker.current} /{' '}
+ {progressTracker.total}{' '}
+ {' '}
+
+ {' '}
+ {constants.CREATION_TIME_UPDATED}
+
+
+
+ >
+ )}
+ {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/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/PhotoSwipe/PhotoSwipe.tsx b/src/components/PhotoSwipe/PhotoSwipe.tsx
index e688b0afd..a7f90d77a 100644
--- a/src/components/PhotoSwipe/PhotoSwipe.tsx
+++ b/src/components/PhotoSwipe/PhotoSwipe.tsx
@@ -10,6 +10,7 @@ import {
import {
ALL_TIME,
File,
+ MAX_EDITED_FILE_NAME_LENGTH,
MAX_EDITED_CREATION_TIME,
MIN_EDITED_CREATION_TIME,
updatePublicMagicMetadata,
@@ -22,20 +23,32 @@ import styled from 'styled-components';
import events from './events';
import {
changeFileCreationTime,
+ changeFileName,
downloadFile,
formatDateTime,
+ splitFilenameAndExtension,
updateExistingFilePubMetadata,
} from 'utils/file';
-import { FormCheck } from 'react-bootstrap';
+import { Col, Form, FormCheck, FormControl } from 'react-bootstrap';
import { prettyPrintExif } from 'utils/exif';
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 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';
interface Iprops {
isOpen: boolean;
@@ -86,7 +99,7 @@ function RenderCreationTime({
file: File;
scheduleUpdate: () => void;
}) {
- const originalCreationTime = new Date(file.metadata.creationTime / 1000);
+ const originalCreationTime = new Date(file?.metadata.creationTime / 1000);
const [isInEditMode, setIsInEditMode] = useState(false);
const [pickedTime, setPickedTime] = useState(originalCreationTime);
@@ -98,7 +111,8 @@ function RenderCreationTime({
try {
if (isInEditMode && file) {
const unixTimeInMicroSec = pickedTime.getTime() * 1000;
- if (unixTimeInMicroSec === file.metadata.creationTime) {
+ if (unixTimeInMicroSec === file?.metadata.creationTime) {
+ closeEditMode();
return;
}
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 (
+
+ 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 }) => (
+
+
+
+
+ {errors.filename}
+
+
+ {extension && (
+
+
+ {`.${extension}`}
+
+
+ )}
+
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+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 (
+ <>
+
+
+ {!isInEditMode ? (
+ <>
+
+
+ {getFileTitle(filename, extension)}
+
+
+
+
+
+
+
+ >
+ ) : (
+
+ )}
+
+ >
+ );
+}
function ExifData(props: { exif: any }) {
const { exif } = props;
const [showAll, setShowAll] = useState(false);
@@ -250,8 +428,12 @@ function InfoModal({
constants.FILE_ID,
items[photoSwipe?.getCurrentIndex()]?.id
)}
- {metadata?.title &&
- renderInfoItem(constants.FILE_NAME, metadata.title)}
+ {metadata?.title && (
+
+ )}
{metadata?.creationTime && (
`
display: flex;
align-items: center;
@@ -23,6 +25,7 @@ export const FreeFlowText = styled.div`
word-wrap: break-word;
overflow-wrap: break-word;
min-width: 30%;
+ text-align: left;
`;
interface Props {
show: boolean;
@@ -41,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/pages/gallery/SelectedFileOptions.tsx b/src/components/pages/gallery/SelectedFileOptions.tsx
index 2c9276954..f10bdeefa 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,9 @@ 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_USER_ID, User } from 'services/userService';
interface Props {
addToCollectionHelper: (collection: Collection) => void;
@@ -27,6 +30,7 @@ interface Props {
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
deleteFileHelper: (permanent?: boolean) => void;
removeFromCollectionHelper: () => void;
+ fixTimeHelper: () => void;
count: number;
clearSelection: () => void;
archiveFilesHelper: () => void;
@@ -68,6 +72,7 @@ const SelectedFileOptions = ({
restoreToCollectionHelper,
showCreateCollectionModal,
removeFromCollectionHelper,
+ fixTimeHelper,
setDialogMessage,
setCollectionSelectorAttributes,
deleteFileHelper,
@@ -78,6 +83,12 @@ const SelectedFileOptions = ({
activeCollection,
isFavoriteCollection,
}: 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 = () =>
setCollectionSelectorAttributes({
callback: addToCollectionHelper,
@@ -168,6 +179,18 @@ const SelectedFileOptions = ({
>
) : (
<>
+ {showFixCreationTime && (
+
+
+
+
+
+ )}
+
+
+
+
+
{activeCollection === ARCHIVE_SECTION && (
@@ -182,11 +205,7 @@ const SelectedFileOptions = ({
)}
-
-
-
-
-
+
{activeCollection !== ALL_SECTION &&
activeCollection !== ARCHIVE_SECTION &&
!isFavoriteCollection && (
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index b324f1eea..8ba26963f 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -446,6 +446,9 @@ const GlobalStyles = createGlobalStyle`
.react-datepicker__day--disabled:hover {
background-color: #202020;
}
+ .ente-form-group{
+ margin:0;
+ }
`;
export const LogoImage = styled.img`
diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx
index e16145f72..554affbed 100644
--- a/src/pages/gallery/index.tsx
+++ b/src/pages/gallery/index.tsx
@@ -93,6 +93,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,7 +207,9 @@ export default function Gallery() {
useState