Merge branch 'master' into fix-queue-processing-order

This commit is contained in:
Abhinav 2021-12-07 13:19:26 +05:30
commit c3e7578019
35 changed files with 867 additions and 177 deletions

View file

@ -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",

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

@ -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,83 @@
import React, { ChangeEvent } from 'react';
import { FIX_OPTIONS } from '.';
import { Form } from 'react-bootstrap';
import EnteDateTimePicker from 'components/EnteDateTimePicker';
import { Row, Value } from 'components/Container';
import constants from 'utils/strings/constants';
const Option = ({
value,
selected,
onChange,
label,
}: {
value: FIX_OPTIONS;
selected: FIX_OPTIONS;
onChange: (e: string | ChangeEvent<any>) => void;
label: string;
}) => (
<Form.Check
name="group1"
style={{
margin: '5px 0',
color: value !== Number(selected) ? '#aaa' : '#fff',
}}>
<Form.Check.Input
style={{ marginTop: '6px' }}
id={value.toString()}
type="radio"
value={value}
checked={value === Number(selected)}
onChange={onChange}
/>
<Form.Check.Label
style={{ cursor: 'pointer' }}
htmlFor={value.toString()}>
{label}
</Form.Check.Label>
</Form.Check>
);
export default function FixCreationTimeOptions({ handleChange, values }) {
return (
<Form noValidate>
<Row style={{ margin: '0' }}>
<Option
value={FIX_OPTIONS.DATE_TIME_ORIGINAL}
onChange={handleChange('option')}
label={constants.DATE_TIME_ORIGINAL}
selected={Number(values.option)}
/>
</Row>
<Row style={{ margin: '0' }}>
<Option
value={FIX_OPTIONS.DATE_TIME_DIGITIZED}
onChange={handleChange('option')}
label={constants.DATE_TIME_DIGITIZED}
selected={Number(values.option)}
/>
</Row>
<Row style={{ margin: '0' }}>
<Value width="50%">
<Option
value={FIX_OPTIONS.CUSTOM_TIME}
onChange={handleChange('option')}
label={constants.CUSTOM_TIME}
selected={Number(values.option)}
/>
</Value>
{Number(values.option) === FIX_OPTIONS.CUSTOM_TIME && (
<Value width="40%">
<EnteDateTimePicker
isInEditMode
pickedTime={new Date(values.customTime)}
handleChange={(x: Date) =>
handleChange('customTime')(x.toUTCString())
}
/>
</Value>
)}
</Row>
</Form>
);
}

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

@ -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>
</> </>
)} )}

View file

@ -152,6 +152,8 @@ const PhotoFrame = ({
.map((item, index) => ({ .map((item, index) => ({
...item, ...item,
dataIndex: index, dataIndex: index,
w: window.innerWidth,
h: window.innerHeight,
...(item.deleteBy && { ...(item.deleteBy && {
title: constants.AUTOMATIC_BIN_DELETE_MESSAGE( title: constants.AUTOMATIC_BIN_DELETE_MESSAGE(
formatDateRelative(item.deleteBy / 1000) formatDateRelative(item.deleteBy / 1000)
@ -352,7 +354,7 @@ const PhotoFrame = ({
if (galleryContext.thumbs.has(item.id)) { if (galleryContext.thumbs.has(item.id)) {
url = galleryContext.thumbs.get(item.id); url = galleryContext.thumbs.get(item.id);
} else { } else {
url = await DownloadManager.getPreview(item); url = await DownloadManager.getThumbnail(item);
galleryContext.thumbs.set(item.id, url); galleryContext.thumbs.set(item.id, url);
} }
updateUrl(item.dataIndex)(url); updateUrl(item.dataIndex)(url);

View file

@ -8,11 +8,8 @@ import {
removeFromFavorites, removeFromFavorites,
} from 'services/collectionService'; } from 'services/collectionService';
import { import {
ALL_TIME,
File, File,
MAX_EDITED_FILE_NAME_LENGTH, MAX_EDITED_FILE_NAME_LENGTH,
MAX_EDITED_CREATION_TIME,
MIN_EDITED_CREATION_TIME,
updatePublicMagicMetadata, updatePublicMagicMetadata,
} from 'services/fileService'; } from 'services/fileService';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
@ -41,14 +38,13 @@ import {
} from 'components/Container'; } from 'components/Container';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import CloseIcon from 'components/icons/CloseIcon'; import CloseIcon from 'components/icons/CloseIcon';
import TickIcon from 'components/icons/TickIcon'; import TickIcon from 'components/icons/TickIcon';
import { FreeFlowText } from 'components/RecoveryKeyModal'; import { FreeFlowText } from 'components/RecoveryKeyModal';
import { Formik } from 'formik'; import { Formik } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import EnteSpinner from 'components/EnteSpinner'; import EnteSpinner from 'components/EnteSpinner';
import EnteDateTimePicker from 'components/EnteDateTimePicker';
interface Iprops { interface Iprops {
isOpen: boolean; isOpen: boolean;
@ -87,11 +83,6 @@ const renderInfoItem = (label: string, value: string | JSX.Element) => (
</Row> </Row>
); );
const isSameDay = (first, second) =>
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate();
function RenderCreationTime({ function RenderCreationTime({
file, file,
scheduleUpdate, scheduleUpdate,
@ -145,24 +136,11 @@ function RenderCreationTime({
<Label width="30%">{constants.CREATION_TIME}</Label> <Label width="30%">{constants.CREATION_TIME}</Label>
<Value width={isInEditMode ? '50%' : '60%'}> <Value width={isInEditMode ? '50%' : '60%'}>
{isInEditMode ? ( {isInEditMode ? (
<DatePicker <EnteDateTimePicker
open={isInEditMode} isInEditMode={isInEditMode}
selected={pickedTime} pickedTime={pickedTime}
onChange={handleChange} handleChange={handleChange}
timeInputLabel="Time:" />
dateFormat="dd/MM/yyyy h:mm aa"
showTimeSelect
autoFocus
minDate={MIN_EDITED_CREATION_TIME}
maxDate={MAX_EDITED_CREATION_TIME}
maxTime={
isSameDay(pickedTime, new Date())
? MAX_EDITED_CREATION_TIME
: ALL_TIME
}
minTime={MIN_EDITED_CREATION_TIME}
fixedHeight
withPortal></DatePicker>
) : ( ) : (
formatDateTime(pickedTime) formatDateTime(pickedTime)
)} )}

View file

@ -5,7 +5,9 @@ import constants from 'utils/strings/constants';
import MessageDialog from './MessageDialog'; import MessageDialog from './MessageDialog';
import EnteSpinner from './EnteSpinner'; import EnteSpinner from './EnteSpinner';
import styled from 'styled-components'; import styled from 'styled-components';
const bip39 = require('bip39');
// mobile client library only supports english.
bip39.setDefaultWordlist('english');
export const CodeBlock = styled.div<{ height: number }>` export const CodeBlock = styled.div<{ height: number }>`
display: flex; display: flex;
align-items: center; align-items: center;
@ -42,7 +44,7 @@ function RecoveryKeyModal({ somethingWentWrong, ...props }: Props) {
somethingWentWrong(); somethingWentWrong();
props.onHide(); props.onHide();
} }
setRecoveryKey(recoveryKey); setRecoveryKey(bip39.entropyToMnemonic(recoveryKey));
}; };
main(); main();
}, [props.show]); }, [props.show]);

View 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',
};

View file

@ -0,0 +1,25 @@
import React from 'react';
export default function DownloadIcon(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
fill="currentColor">
<g>
<rect fill="none" height="24" width="24" />
</g>
<g>
<path d="M5,20h14v-2H5V20z M19,9h-4V3H9v6H5l7,7L19,9z" />
</g>
</svg>
);
}
DownloadIcon.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
};

View file

@ -177,7 +177,7 @@ export default function PreviewCard(props: IProps) {
if (file && !file.msrc) { if (file && !file.msrc) {
const main = async () => { const main = async () => {
try { try {
const url = await DownloadManager.getPreview(file); const url = await DownloadManager.getThumbnail(file);
if (isMounted.current) { if (isMounted.current) {
setImgSrc(url); setImgSrc(url);
thumbs.set(file.id, url); thumbs.set(file.id, url);

View file

@ -1,5 +1,5 @@
import { SetDialogMessage } from 'components/MessageDialog'; import { SetDialogMessage } from 'components/MessageDialog';
import React from 'react'; import React, { useEffect, useState } from 'react';
import { SetCollectionSelectorAttributes } from './CollectionSelector'; import { SetCollectionSelectorAttributes } from './CollectionSelector';
import styled from 'styled-components'; import styled from 'styled-components';
import Navbar from 'components/Navbar'; import Navbar from 'components/Navbar';
@ -17,6 +17,13 @@ import { OverlayTrigger } from 'react-bootstrap';
import { Collection } from 'services/collectionService'; import { Collection } from 'services/collectionService';
import RemoveIcon from 'components/icons/RemoveIcon'; import RemoveIcon from 'components/icons/RemoveIcon';
import RestoreIcon from 'components/icons/RestoreIcon'; import RestoreIcon from 'components/icons/RestoreIcon';
import ClockIcon from 'components/icons/ClockIcon';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import {
FIX_CREATION_TIME_VISIBLE_TO_USER_IDS,
User,
} from 'services/userService';
import DownloadIcon from 'components/icons/DownloadIcon';
interface Props { interface Props {
addToCollectionHelper: (collection: Collection) => void; addToCollectionHelper: (collection: Collection) => void;
@ -27,6 +34,8 @@ interface Props {
setCollectionSelectorAttributes: SetCollectionSelectorAttributes; setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
deleteFileHelper: (permanent?: boolean) => void; deleteFileHelper: (permanent?: boolean) => void;
removeFromCollectionHelper: () => void; removeFromCollectionHelper: () => void;
fixTimeHelper: () => void;
downloadHelper: () => void;
count: number; count: number;
clearSelection: () => void; clearSelection: () => void;
archiveFilesHelper: () => void; archiveFilesHelper: () => void;
@ -68,9 +77,11 @@ const SelectedFileOptions = ({
restoreToCollectionHelper, restoreToCollectionHelper,
showCreateCollectionModal, showCreateCollectionModal,
removeFromCollectionHelper, removeFromCollectionHelper,
fixTimeHelper,
setDialogMessage, setDialogMessage,
setCollectionSelectorAttributes, setCollectionSelectorAttributes,
deleteFileHelper, deleteFileHelper,
downloadHelper,
count, count,
clearSelection, clearSelection,
archiveFilesHelper, archiveFilesHelper,
@ -78,6 +89,13 @@ const SelectedFileOptions = ({
activeCollection, activeCollection,
isFavoriteCollection, isFavoriteCollection,
}: Props) => { }: Props) => {
const [showFixCreationTime, setShowFixCreationTime] = useState(false);
useEffect(() => {
const user: User = getData(LS_KEYS.USER);
const showFixCreationTime =
FIX_CREATION_TIME_VISIBLE_TO_USER_IDS.includes(user?.id);
setShowFixCreationTime(showFixCreationTime);
}, []);
const addToCollection = () => const addToCollection = () =>
setCollectionSelectorAttributes({ setCollectionSelectorAttributes({
callback: addToCollectionHelper, callback: addToCollectionHelper,
@ -168,6 +186,23 @@ const SelectedFileOptions = ({
</> </>
) : ( ) : (
<> <>
{showFixCreationTime && (
<IconWithMessage message={constants.FIX_CREATION_TIME}>
<IconButton onClick={fixTimeHelper}>
<ClockIcon />
</IconButton>
</IconWithMessage>
)}
<IconWithMessage message={constants.DOWNLOAD}>
<IconButton onClick={downloadHelper}>
<DownloadIcon />
</IconButton>
</IconWithMessage>
<IconWithMessage message={constants.ADD}>
<IconButton onClick={addToCollection}>
<AddIcon />
</IconButton>
</IconWithMessage>
{activeCollection === ARCHIVE_SECTION && ( {activeCollection === ARCHIVE_SECTION && (
<IconWithMessage message={constants.UNARCHIVE}> <IconWithMessage message={constants.UNARCHIVE}>
<IconButton onClick={unArchiveFilesHelper}> <IconButton onClick={unArchiveFilesHelper}>
@ -182,11 +217,7 @@ const SelectedFileOptions = ({
</IconButton> </IconButton>
</IconWithMessage> </IconWithMessage>
)} )}
<IconWithMessage message={constants.ADD}>
<IconButton onClick={addToCollection}>
<AddIcon />
</IconButton>
</IconWithMessage>
{activeCollection !== ALL_SECTION && {activeCollection !== ALL_SECTION &&
activeCollection !== ARCHIVE_SECTION && activeCollection !== ARCHIVE_SECTION &&
!isFavoriteCollection && ( !isFavoriteCollection && (

View file

@ -134,7 +134,8 @@ export default function Upload(props: Props) {
return null; return null;
} }
const paths: string[] = props.acceptedFiles.map((file) => file['path']); const paths: string[] = props.acceptedFiles.map((file) => file['path']);
paths.sort(); const getCharCount = (str: string) => (str.match(/\//g) ?? []).length;
paths.sort((path1, path2) => getCharCount(path1) - getCharCount(path2));
const firstPath = paths[0]; const firstPath = paths[0];
const lastPath = paths[paths.length - 1]; const lastPath = paths[paths.length - 1];
const L = firstPath.length; const L = firstPath.length;

View file

@ -479,6 +479,8 @@ type AppContextType = {
sharedFiles: File[]; sharedFiles: File[];
resetSharedFiles: () => void; resetSharedFiles: () => void;
setDisappearingFlashMessage: (message: FlashMessage) => void; setDisappearingFlashMessage: (message: FlashMessage) => void;
redirectUrl: string;
setRedirectUrl: (url: string) => void;
}; };
export enum FLASH_MESSAGE_TYPE { export enum FLASH_MESSAGE_TYPE {
@ -508,6 +510,7 @@ export default function App({ Component, err }) {
const [sharedFiles, setSharedFiles] = useState<File[]>(null); const [sharedFiles, setSharedFiles] = useState<File[]>(null);
const [redirectName, setRedirectName] = useState<string>(null); const [redirectName, setRedirectName] = useState<string>(null);
const [flashMessage, setFlashMessage] = useState<FlashMessage>(null); const [flashMessage, setFlashMessage] = useState<FlashMessage>(null);
const [redirectUrl, setRedirectUrl] = useState(null);
useEffect(() => { useEffect(() => {
if ( if (
!('serviceWorker' in navigator) || !('serviceWorker' in navigator) ||
@ -641,6 +644,8 @@ export default function App({ Component, err }) {
sharedFiles, sharedFiles,
resetSharedFiles, resetSharedFiles,
setDisappearingFlashMessage, setDisappearingFlashMessage,
redirectUrl,
setRedirectUrl,
}}> }}>
{loading ? ( {loading ? (
<Container> <Container>

View file

@ -75,8 +75,9 @@ export default function Credentials() {
} }
await SaveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, key); await SaveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, key);
await decryptAndStoreToken(key); await decryptAndStoreToken(key);
const redirectUrl = appContext.redirectUrl;
router.push(PAGES.GALLERY); appContext.setRedirectUrl(null);
router.push(redirectUrl ?? PAGES.GALLERY);
} catch (e) { } catch (e) {
logError(e, 'user entered a wrong password'); logError(e, 'user entered a wrong password');
setFieldError('passphrase', constants.INCORRECT_PASSPHRASE); setFieldError('passphrase', constants.INCORRECT_PASSPHRASE);

View file

@ -50,6 +50,8 @@ import { LoadingOverlay } from 'components/LoadingOverlay';
import PhotoFrame from 'components/PhotoFrame'; import PhotoFrame from 'components/PhotoFrame';
import { import {
changeFilesVisibility, changeFilesVisibility,
downloadFiles,
getNonTrashedUniqueUserFiles,
getSelectedFiles, getSelectedFiles,
mergeMetadata, mergeMetadata,
sortFiles, sortFiles,
@ -93,6 +95,9 @@ import {
Trash, Trash,
} from 'services/trashService'; } from 'services/trashService';
import DeleteBtn from 'components/DeleteBtn'; import DeleteBtn from 'components/DeleteBtn';
import FixCreationTime, {
FixCreationTimeAttributes,
} from 'components/FixCreationTime';
export const DeadCenter = styled.div` export const DeadCenter = styled.div`
flex: 1; flex: 1;
@ -204,10 +209,14 @@ export default function Gallery() {
useState<Map<number, number>>(); useState<Map<number, number>>();
const [activeCollection, setActiveCollection] = useState<number>(undefined); const [activeCollection, setActiveCollection] = useState<number>(undefined);
const [trash, setTrash] = useState<Trash>([]); const [trash, setTrash] = useState<Trash>([]);
const [fixCreationTimeView, setFixCreationTimeView] = useState(false);
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
useState<FixCreationTimeAttributes>(null);
useEffect(() => { useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY); const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if (!key) { if (!key) {
appContext.setRedirectUrl(router.asPath);
router.push(PAGES.ROOT); router.push(PAGES.ROOT);
return; return;
} }
@ -227,11 +236,6 @@ export default function Gallery() {
setCollections(collections); setCollections(collections);
setTrash(trash); setTrash(trash);
await setDerivativeState(collections, files); await setDerivativeState(collections, files);
await checkSubscriptionPurchase(
setDialogMessage,
router,
setLoading
);
await syncWithRemote(true); await syncWithRemote(true);
setIsFirstLoad(false); setIsFirstLoad(false);
setJustSignedUp(false); setJustSignedUp(false);
@ -243,13 +247,19 @@ export default function Gallery() {
useEffect(() => setDialogView(true), [dialogMessage]); useEffect(() => setDialogView(true), [dialogMessage]);
useEffect(() => { useEffect(
if (collectionSelectorAttributes) { () => collectionSelectorAttributes && setCollectionSelectorView(true),
setCollectionSelectorView(true); [collectionSelectorAttributes]
} );
}, [collectionSelectorAttributes]);
useEffect(() => setCollectionNamerView(true), [collectionNamerAttributes]); useEffect(
() => collectionNamerAttributes && setCollectionNamerView(true),
[collectionNamerAttributes]
);
useEffect(
() => fixCreationTimeAttributes && setFixCreationTimeView(true),
[fixCreationTimeAttributes]
);
useEffect(() => { useEffect(() => {
if (typeof activeCollection === 'undefined') { if (typeof activeCollection === 'undefined') {
@ -270,6 +280,13 @@ export default function Gallery() {
router.push(href, undefined, { shallow: true }); router.push(href, undefined, { shallow: true });
}, [activeCollection]); }, [activeCollection]);
useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if (router.isReady && key) {
checkSubscriptionPurchase(setDialogMessage, router, setLoading);
}
}, [router.isReady]);
const syncWithRemote = async (force = false, silent = false) => { const syncWithRemote = async (force = false, silent = false) => {
if (syncInProgress.current && !force) { if (syncInProgress.current && !force) {
resync.current = true; resync.current = true;
@ -523,6 +540,20 @@ export default function Gallery() {
} }
}; };
const fixTimeHelper = async () => {
const selectedFiles = getSelectedFiles(selected, files);
setFixCreationTimeAttributes({ files: selectedFiles });
clearSelection();
};
const downloadHelper = async () => {
const selectedFiles = getSelectedFiles(selected, files);
clearSelection();
!syncInProgress.current && loadingBar.current?.continuousStart();
await downloadFiles(selectedFiles);
!syncInProgress.current && loadingBar.current.complete();
};
return ( return (
<GalleryContext.Provider <GalleryContext.Provider
value={{ value={{
@ -564,7 +595,7 @@ export default function Gallery() {
loadingBar={loadingBar} loadingBar={loadingBar}
isFirstFetch={isFirstFetch} isFirstFetch={isFirstFetch}
collections={collections} collections={collections}
files={files} files={getNonTrashedUniqueUserFiles(files)}
setActiveCollection={setActiveCollection} setActiveCollection={setActiveCollection}
setSearch={updateSearch} setSearch={updateSearch}
searchStats={searchStats} searchStats={searchStats}
@ -594,6 +625,12 @@ export default function Gallery() {
} }
attributes={collectionSelectorAttributes} attributes={collectionSelectorAttributes}
/> />
<FixCreationTime
isOpen={fixCreationTimeView}
hide={() => setFixCreationTimeView(false)}
show={() => setFixCreationTimeView(true)}
attributes={fixCreationTimeAttributes}
/>
<Upload <Upload
syncWithRemote={syncWithRemote} syncWithRemote={syncWithRemote}
setBannerMessage={setBannerMessage} setBannerMessage={setBannerMessage}
@ -685,6 +722,8 @@ export default function Gallery() {
) )
) )
} }
fixTimeHelper={fixTimeHelper}
downloadHelper={downloadHelper}
count={selected.count} count={selected.count}
clearSelection={clearSelection} clearSelection={clearSelection}
activeCollection={activeCollection} activeCollection={activeCollection}

View file

@ -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,

View file

@ -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,

View file

@ -134,6 +134,10 @@ class billingService {
sessionID: string = null sessionID: string = null
): Promise<Subscription> { ): Promise<Subscription> {
try { try {
const token = getToken();
if (!token) {
return;
}
const response = await HTTPService.post( const response = await HTTPService.post(
`${ENDPOINT}/billing/verify-subscription`, `${ENDPOINT}/billing/verify-subscription`,
{ {
@ -143,7 +147,7 @@ class billingService {
}, },
null, null,
{ {
'X-Auth-Token': getToken(), 'X-Auth-Token': token,
} }
); );
const { subscription } = response.data; const { subscription } = response.data;

View file

@ -1,7 +1,11 @@
import { getToken } from 'utils/common/key'; import { getToken } from 'utils/common/key';
import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil'; import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil';
import CryptoWorker from 'utils/crypto'; import CryptoWorker from 'utils/crypto';
import { generateStreamFromArrayBuffer, convertForPreview } from 'utils/file'; import {
generateStreamFromArrayBuffer,
convertForPreview,
needsConversionForPreview,
} from 'utils/file';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { File, FILE_TYPE } from './fileService'; import { File, FILE_TYPE } from './fileService';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
@ -10,27 +14,36 @@ class DownloadManager {
private fileObjectUrlPromise = new Map<string, Promise<string>>(); private fileObjectUrlPromise = new Map<string, Promise<string>>();
private thumbnailObjectUrlPromise = new Map<number, Promise<string>>(); private thumbnailObjectUrlPromise = new Map<number, Promise<string>>();
public async getPreview(file: File) { public async getThumbnail(file: File) {
try { try {
const token = getToken(); const token = getToken();
if (!token) { if (!token) {
return null; return null;
} }
const thumbnailCache = await caches.open('thumbs');
const cacheResp: Response = await thumbnailCache.match(
file.id.toString()
);
if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob());
}
if (!this.thumbnailObjectUrlPromise.get(file.id)) { if (!this.thumbnailObjectUrlPromise.get(file.id)) {
const downloadPromise = this.downloadThumb( const downloadPromise = async () => {
token, const thumbnailCache = await caches.open('thumbs');
thumbnailCache, const cacheResp: Response = await thumbnailCache.match(
file file.id.toString()
); );
this.thumbnailObjectUrlPromise.set(file.id, downloadPromise); if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob());
}
const thumb = await this.downloadThumb(token, file);
const thumbBlob = new Blob([thumb]);
try {
await thumbnailCache.put(
file.id.toString(),
new Response(thumbBlob)
);
} catch (e) {
// TODO: handle storage full exception.
}
return URL.createObjectURL(thumbBlob);
};
this.thumbnailObjectUrlPromise.set(file.id, downloadPromise());
} }
return await this.thumbnailObjectUrlPromise.get(file.id); return await this.thumbnailObjectUrlPromise.get(file.id);
} catch (e) { } catch (e) {
this.thumbnailObjectUrlPromise.delete(file.id); this.thumbnailObjectUrlPromise.delete(file.id);
@ -39,24 +52,7 @@ class DownloadManager {
} }
} }
private downloadThumb = async ( downloadThumb = async (token: string, file: File) => {
token: string,
thumbnailCache: Cache,
file: File
) => {
const thumb = await this.getThumbnail(token, file);
try {
await thumbnailCache.put(
file.id.toString(),
new Response(new Blob([thumb]))
);
} catch (e) {
// TODO: handle storage full exception.
}
return URL.createObjectURL(new Blob([thumb]));
};
getThumbnail = async (token: string, file: File) => {
const resp = await HTTPService.get( const resp = await HTTPService.get(
getThumbnailUrl(file.id), getThumbnailUrl(file.id),
null, null,
@ -73,32 +69,38 @@ class DownloadManager {
}; };
getFile = async (file: File, forPreview = false) => { getFile = async (file: File, forPreview = false) => {
let fileUID: string; const shouldBeConverted = forPreview && needsConversionForPreview(file);
if (file.metadata.fileType === FILE_TYPE.VIDEO) { const fileKey = shouldBeConverted
fileUID = file.id.toString(); ? `${file.id}_converted`
} else { : `${file.id}`;
fileUID = `${file.id}_forPreview=${forPreview}`;
}
try { try {
const getFilePromise = async () => { const getFilePromise = async (convert: boolean) => {
const fileStream = await this.downloadFile(file); const fileStream = await this.downloadFile(file);
let fileBlob = await new Response(fileStream).blob(); let fileBlob = await new Response(fileStream).blob();
if (forPreview) { if (convert) {
fileBlob = await convertForPreview(file, fileBlob); fileBlob = await convertForPreview(file, fileBlob);
} }
return URL.createObjectURL(fileBlob); return URL.createObjectURL(fileBlob);
}; };
if (!this.fileObjectUrlPromise.get(fileUID)) { if (!this.fileObjectUrlPromise.get(fileKey)) {
this.fileObjectUrlPromise.set(fileUID, getFilePromise()); this.fileObjectUrlPromise.set(
fileKey,
getFilePromise(shouldBeConverted)
);
} }
return await this.fileObjectUrlPromise.get(fileUID); const fileURL = await this.fileObjectUrlPromise.get(fileKey);
return fileURL;
} catch (e) { } catch (e) {
this.fileObjectUrlPromise.delete(fileUID); this.fileObjectUrlPromise.delete(fileKey);
logError(e, 'Failed to get File'); logError(e, 'Failed to get File');
throw e; throw e;
} }
}; };
public async getCachedOriginalFile(file: File) {
return await this.fileObjectUrlPromise.get(file.id.toString());
}
async downloadFile(file: File) { async downloadFile(file: File) {
const worker = await new CryptoWorker(); const worker = await new CryptoWorker();
const token = getToken(); const token = getToken();

View file

@ -19,6 +19,8 @@ class FFmpegService {
this.isLoading = null; this.isLoading = null;
} catch (e) { } catch (e) {
logError(e, 'ffmpeg load failed'); logError(e, 'ffmpeg load failed');
this.ffmpeg = null;
this.isLoading = null;
throw e; throw e;
} }
} }

View file

@ -67,7 +67,7 @@ export async function replaceThumbnail(
current: idx, current: idx,
total: largeThumbnailFiles.length, total: largeThumbnailFiles.length,
}); });
const originalThumbnail = await downloadManager.getThumbnail( const originalThumbnail = await downloadManager.downloadThumb(
token, token,
file file
); );

View file

@ -5,8 +5,7 @@ import { DateValue, Suggestion, SuggestionType } from 'components/SearchBar';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { Collection } from './collectionService'; import { Collection } from './collectionService';
import { File } from './fileService'; import { File } from './fileService';
import { User } from './userService'; import { logError } from 'utils/sentry';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']); const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
@ -45,17 +44,22 @@ export function parseHumanDate(humanDate: string): DateValue[] {
export async function searchLocation( export async function searchLocation(
searchPhrase: string searchPhrase: string
): Promise<LocationSearchResponse[]> { ): Promise<LocationSearchResponse[]> {
const resp = await HTTPService.get( try {
`${ENDPOINT}/search/location`, const resp = await HTTPService.get(
{ `${ENDPOINT}/search/location`,
query: searchPhrase, {
limit: 4, query: searchPhrase,
}, limit: 4,
{ },
'X-Auth-Token': getToken(), {
} 'X-Auth-Token': getToken(),
); }
return resp.data.results; );
return resp.data.results ?? [];
} catch (e) {
logError(e, 'location search failed');
}
return [];
} }
export function getHolidaySuggestion(searchPhrase: string): Suggestion[] { export function getHolidaySuggestion(searchPhrase: string): Suggestion[] {
@ -99,7 +103,7 @@ export function getYearSuggestion(searchPhrase: string): Suggestion[] {
]; ];
} }
} catch (e) { } catch (e) {
// ignore logError(e, 'getYearSuggestion failed');
} }
} }
return []; return [];
@ -115,8 +119,6 @@ export function searchCollection(
} }
export function searchFiles(searchPhrase: string, files: File[]) { export function searchFiles(searchPhrase: string, files: File[]) {
const user: User = getData(LS_KEYS.USER) ?? {};
const idSet = new Set();
return files return files
.map((file, idx) => ({ .map((file, idx) => ({
title: file.metadata.title, title: file.metadata.title,
@ -125,13 +127,6 @@ export function searchFiles(searchPhrase: string, files: File[]) {
ownerID: file.ownerID, ownerID: file.ownerID,
id: file.id, id: file.id,
})) }))
.filter((file) => {
if (file.ownerID === user.id && !idSet.has(file.id)) {
idSet.add(file.id);
return true;
}
return false;
})
.filter(({ title }) => title.toLowerCase().includes(searchPhrase)) .filter(({ title }) => title.toLowerCase().includes(searchPhrase))
.slice(0, 4); .slice(0, 4);
} }

View file

@ -0,0 +1,77 @@
import { FIX_OPTIONS } from 'components/FixCreationTime';
import { SetProgressTracker } from 'components/FixLargeThumbnail';
import CryptoWorker from 'utils/crypto';
import {
changeFileCreationTime,
getFileFromURL,
updateExistingFilePubMetadata,
} from 'utils/file';
import { logError } from 'utils/sentry';
import downloadManager from './downloadManager';
import { File, FILE_TYPE, updatePublicMagicMetadata } from './fileService';
import { getRawExif, getUNIXTime } from './upload/exifService';
import { getFileType } from './upload/readFileService';
export async function updateCreationTimeWithExif(
filesToBeUpdated: File[],
fixOption: FIX_OPTIONS,
customTime: Date,
setProgressTracker: SetProgressTracker
) {
let completedWithError = false;
try {
if (filesToBeUpdated.length === 0) {
return completedWithError;
}
setProgressTracker({ current: 0, total: filesToBeUpdated.length });
for (const [index, file] of filesToBeUpdated.entries()) {
try {
if (file.metadata.fileType !== FILE_TYPE.IMAGE) {
continue;
}
let correctCreationTime: number;
if (fixOption === FIX_OPTIONS.CUSTOM_TIME) {
correctCreationTime = getUNIXTime(customTime);
} else {
const fileURL = await downloadManager.getFile(file);
const fileObject = await getFileFromURL(fileURL);
const worker = await new CryptoWorker();
const fileTypeInfo = await getFileType(worker, fileObject);
const exifData = await getRawExif(fileObject, fileTypeInfo);
if (fixOption === FIX_OPTIONS.DATE_TIME_ORIGINAL) {
correctCreationTime = getUNIXTime(
exifData?.DateTimeOriginal
);
} else {
correctCreationTime = getUNIXTime(exifData?.CreateDate);
}
}
if (
correctCreationTime &&
correctCreationTime !== file.metadata.creationTime
) {
let updatedFile = await changeFileCreationTime(
file,
correctCreationTime
);
updatedFile = (
await updatePublicMagicMetadata([updatedFile])
)[0];
updateExistingFilePubMetadata(file, updatedFile);
}
} catch (e) {
logError(e, 'failed to updated a CreationTime With Exif');
completedWithError = true;
} finally {
setProgressTracker({
current: index + 1,
total: filesToBeUpdated.length,
});
}
}
} catch (e) {
logError(e, 'update CreationTime With Exif failed');
completedWithError = true;
}
return completedWithError;
}

View file

@ -1,6 +1,8 @@
import exifr from 'exifr'; import exifr from 'exifr';
import { logError } from 'utils/sentry';
import { NULL_LOCATION, Location } from './metadataService'; import { NULL_LOCATION, Location } from './metadataService';
import { FileTypeInfo } from './readFileService';
const EXIF_TAGS_NEEDED = [ const EXIF_TAGS_NEEDED = [
'DateTimeOriginal', 'DateTimeOriginal',
@ -11,37 +13,77 @@ const EXIF_TAGS_NEEDED = [
'GPSLatitudeRef', 'GPSLatitudeRef',
'GPSLongitudeRef', 'GPSLongitudeRef',
]; ];
interface Exif {
DateTimeOriginal?: Date;
CreateDate?: Date;
ModifyDate?: Date;
GPSLatitude?: number;
GPSLongitude?: number;
GPSLatitudeRef?: number;
GPSLongitudeRef?: number;
}
interface ParsedEXIFData { interface ParsedEXIFData {
location: Location; location: Location;
creationTime: number; creationTime: number;
} }
export async function getExifData( export async function getExifData(
receivedFile: globalThis.File receivedFile: globalThis.File,
fileTypeInfo: FileTypeInfo
): Promise<ParsedEXIFData> { ): Promise<ParsedEXIFData> {
const exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED); const nullExifData: ParsedEXIFData = {
if (!exifData) { location: NULL_LOCATION,
return { location: NULL_LOCATION, creationTime: null }; creationTime: null,
}
const parsedEXIFData = {
location: getEXIFLocation(exifData),
creationTime: getUNIXTime(exifData),
}; };
return parsedEXIFData; try {
const exifData = await getRawExif(receivedFile, fileTypeInfo);
if (!exifData) {
return nullExifData;
}
const parsedEXIFData = {
location: getEXIFLocation(exifData),
creationTime: getUNIXTime(
exifData.DateTimeOriginal ??
exifData.CreateDate ??
exifData.ModifyDate
),
};
return parsedEXIFData;
} catch (e) {
logError(e, 'getExifData failed');
return nullExifData;
}
} }
function getUNIXTime(exifData: any) { export async function getRawExif(
const dateTime: Date = receivedFile: File,
exifData.DateTimeOriginal ?? exifData.CreateDate ?? exifData.ModifyDate; fileTypeInfo: FileTypeInfo
) {
if (!dateTime) { let exifData: Exif;
return null; try {
exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED);
} catch (e) {
logError(e, 'file missing exif data ', {
fileType: fileTypeInfo.exactType,
});
// ignore exif parsing errors
} }
const unixTime = dateTime.getTime() * 1000; return exifData;
if (unixTime <= 0) { }
return null;
} else { export function getUNIXTime(dateTime: Date) {
return unixTime; try {
if (!dateTime) {
return null;
}
const unixTime = dateTime.getTime() * 1000;
if (unixTime <= 0) {
return null;
} else {
return unixTime;
}
} catch (e) {
logError(e, 'getUNIXTime failed', { dateTime });
} }
} }

View file

@ -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 = {

View file

@ -4,7 +4,7 @@ import { logError } from 'utils/sentry';
import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64'; import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64';
import FFmpegService from 'services/ffmpegService'; import FFmpegService from 'services/ffmpegService';
import { convertToHumanReadable } from 'utils/billingUtil'; import { convertToHumanReadable } from 'utils/billingUtil';
import { fileIsHEIC } from 'utils/file'; import { isFileHEIC } from 'utils/file';
import { FileTypeInfo } from './readFileService'; import { FileTypeInfo } from './readFileService';
const MAX_THUMBNAIL_DIMENSION = 720; const MAX_THUMBNAIL_DIMENSION = 720;
@ -31,7 +31,7 @@ export async function generateThumbnail(
let thumbnail: Uint8Array; let thumbnail: Uint8Array;
try { try {
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) { if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
const isHEIC = fileIsHEIC(fileTypeInfo.exactType); const isHEIC = isFileHEIC(fileTypeInfo.exactType);
canvas = await generateImageThumbnail(worker, file, isHEIC); canvas = await generateImageThumbnail(worker, file, isHEIC);
} else { } else {
try { try {
@ -44,7 +44,7 @@ export async function generateThumbnail(
); );
} catch (e) { } catch (e) {
logError(e, 'failed to generate thumbnail using ffmpeg', { logError(e, 'failed to generate thumbnail using ffmpeg', {
type: fileTypeInfo.exactType, fileFormat: fileTypeInfo.exactType,
}); });
canvas = await generateVideoThumbnail(file); canvas = await generateVideoThumbnail(file);
} }

View file

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

View file

@ -191,17 +191,14 @@ export async function checkSubscriptionPurchase(
router: NextRouter, router: NextRouter,
setLoading: SetLoading setLoading: SetLoading
) { ) {
const { session_id: sessionId, status, reason } = router.query ?? {};
try { try {
const urlParams = new URLSearchParams(window.location.search);
const sessionId = urlParams.get('session_id');
const status = urlParams.get('status');
const reason = urlParams.get('reason');
if (status === RESPONSE_STATUS.fail) { if (status === RESPONSE_STATUS.fail) {
handleFailureReason(reason, setDialogMessage, setLoading); handleFailureReason(reason as string, setDialogMessage, setLoading);
} else if (status === RESPONSE_STATUS.success) { } else if (status === RESPONSE_STATUS.success) {
try { try {
const subscription = await billingService.verifySubscription( const subscription = await billingService.verifySubscription(
sessionId sessionId as string
); );
setDialogMessage({ setDialogMessage({
title: constants.SUBSCRIPTION_PURCHASE_SUCCESS_TITLE, title: constants.SUBSCRIPTION_PURCHASE_SUCCESS_TITLE,
@ -220,8 +217,6 @@ export async function checkSubscriptionPurchase(
} }
} catch (e) { } catch (e) {
// ignore // ignore
} finally {
router.push('gallery', undefined, { shallow: true });
} }
} }

View file

@ -11,7 +11,7 @@ import {
} from 'services/fileService'; } from 'services/fileService';
import { decodeMotionPhoto } from 'services/motionPhotoService'; import { decodeMotionPhoto } from 'services/motionPhotoService';
import { getMimeTypeFromBlob } from 'services/upload/readFileService'; import { getMimeTypeFromBlob } from 'services/upload/readFileService';
import DownloadManger from 'services/downloadManager'; import DownloadManager from 'services/downloadManager';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { User } from 'services/userService'; import { User } from 'services/userService';
import CryptoWorker from 'utils/crypto'; import CryptoWorker from 'utils/crypto';
@ -37,10 +37,16 @@ export function downloadAsFile(filename: string, content: string) {
a.remove(); a.remove();
} }
export async function downloadFile(file) { export async function downloadFile(file: File) {
const a = document.createElement('a'); const a = document.createElement('a');
a.style.display = 'none'; a.style.display = 'none';
a.href = await DownloadManger.getFile(file); const cachedFileUrl = await DownloadManager.getCachedOriginalFile(file);
const fileURL =
cachedFileUrl ??
URL.createObjectURL(
await new Response(await DownloadManager.downloadFile(file)).blob()
);
a.href = fileURL;
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
a.download = fileNameWithoutExtension(file.metadata.title) + '.zip'; a.download = fileNameWithoutExtension(file.metadata.title) + '.zip';
} else { } else {
@ -51,10 +57,11 @@ export async function downloadFile(file) {
a.remove(); a.remove();
} }
export function fileIsHEIC(mimeType: string) { export function isFileHEIC(mimeType: string) {
return ( return (
mimeType.toLowerCase().endsWith(TYPE_HEIC) || mimeType &&
mimeType.toLowerCase().endsWith(TYPE_HEIF) (mimeType.toLowerCase().endsWith(TYPE_HEIC) ||
mimeType.toLowerCase().endsWith(TYPE_HEIF))
); );
} }
@ -271,7 +278,7 @@ export async function convertForPreview(file: File, fileBlob: Blob) {
const mimeType = const mimeType =
(await getMimeTypeFromBlob(worker, fileBlob)) ?? typeFromExtension; (await getMimeTypeFromBlob(worker, fileBlob)) ?? typeFromExtension;
if (fileIsHEIC(mimeType)) { if (isFileHEIC(mimeType)) {
fileBlob = await worker.convertHEIC2JPEG(fileBlob); fileBlob = await worker.convertHEIC2JPEG(fileBlob);
} }
return fileBlob; return fileBlob;
@ -438,3 +445,54 @@ export function updateExistingFilePubMetadata(
existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata; existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata;
existingFile.metadata = mergeMetadata([existingFile])[0].metadata; existingFile.metadata = mergeMetadata([existingFile])[0].metadata;
} }
export async function getFileFromURL(fileURL: string) {
const fileBlob = await (await fetch(fileURL)).blob();
const fileFile = new globalThis.File([fileBlob], 'temp');
return fileFile;
}
export function getUniqueFiles(files: File[]) {
const idSet = new Set<number>();
return files.filter((file) => {
if (!idSet.has(file.id)) {
idSet.add(file.id);
return true;
} else {
return false;
}
});
}
export function getNonTrashedUniqueUserFiles(files: File[]) {
const user: User = getData(LS_KEYS.USER) ?? {};
return getUniqueFiles(
files.filter(
(file) =>
(typeof file.isTrashed === 'undefined' || !file.isTrashed) &&
(!user.id || file.ownerID === user.id)
)
);
}
export async function downloadFiles(files: File[]) {
for (const file of files) {
try {
await downloadFile(file);
} catch (e) {
logError(e, 'download fail for file');
}
}
}
export function needsConversionForPreview(file: File) {
const fileExtension = splitFilenameAndExtension(file.metadata.title)[1];
if (
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO ||
(file.metadata.fileType === FILE_TYPE.IMAGE &&
isFileHEIC(fileExtension))
) {
return true;
} else {
return false;
}
}

View file

@ -579,8 +579,8 @@ const englishConstants = {
SORT_BY_COLLECTION_NAME: 'album name', SORT_BY_COLLECTION_NAME: 'album name',
FIX_LARGE_THUMBNAILS: 'compress thumbnails', FIX_LARGE_THUMBNAILS: 'compress thumbnails',
THUMBNAIL_REPLACED: 'thumbnails compressed', THUMBNAIL_REPLACED: 'thumbnails compressed',
FIX: 'compress', FIX_THUMBNAIL: 'compress',
FIX_LATER: 'compress later', FIX_THUMBNAIL_LATER: 'compress later',
REPLACE_THUMBNAIL_NOT_STARTED: () => ( REPLACE_THUMBNAIL_NOT_STARTED: () => (
<> <>
some of your videos thumbnails can be compressed to save space. some of your videos thumbnails can be compressed to save space.
@ -596,7 +596,23 @@ const englishConstants = {
REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR: () => ( REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR: () => (
<>could not compress some of your thumbnails, please retry</> <>could not compress some of your thumbnails, please retry</>
), ),
FIX_CREATION_TIME: 'fix time',
FIX_CREATION_TIME_IN_PROGRESS: 'fixing time',
CREATION_TIME_UPDATED: `file time updated`,
UPDATE_CREATION_TIME_NOT_STARTED: () => (
<>select the option you want to use</>
),
UPDATE_CREATION_TIME_COMPLETED: () => <>successfully updated all files</>,
UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR: () => (
<>file time updation failed for some files, please retry</>
),
FILE_NAME_CHARACTER_LIMIT: '100 characters max', FILE_NAME_CHARACTER_LIMIT: '100 characters max',
DATE_TIME_ORIGINAL: 'EXIF:DateTimeOriginal',
DATE_TIME_DIGITIZED: 'EXIF:DateTimeDigitized',
CUSTOM_TIME: 'custom time',
}; };
export default englishConstants; export default englishConstants;

@ -1 +1 @@
Subproject commit 443b1e393aa37899373b71272e4bcf191529bb74 Subproject commit b1766d38475659c17cf669e2b27787d15f8957b1

View file

@ -1453,6 +1453,11 @@
resolved "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz" resolved "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz"
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g== integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
"@types/node@11.11.6":
version "11.11.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a"
integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==
"@types/node@^14.6.4": "@types/node@^14.6.4":
version "14.17.15" version "14.17.15"
resolved "https://registry.npmjs.org/@types/node/-/node-14.17.15.tgz" resolved "https://registry.npmjs.org/@types/node/-/node-14.17.15.tgz"
@ -2085,6 +2090,16 @@ binary-extensions@^2.0.0:
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
bip39@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.4.tgz#5b11fed966840b5e1b8539f0f54ab6392969b2a0"
integrity sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==
dependencies:
"@types/node" "11.11.6"
create-hash "^1.1.0"
pbkdf2 "^3.0.9"
randombytes "^2.0.1"
bluebird@^3.5.5: bluebird@^3.5.5:
version "3.7.2" version "3.7.2"
resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz"
@ -5016,7 +5031,7 @@ path-type@^4.0.0:
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pbkdf2@^3.0.3: pbkdf2@^3.0.3, pbkdf2@^3.0.9:
version "3.1.2" version "3.1.2"
resolved "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz" resolved "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz"
integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==
@ -5033,7 +5048,7 @@ peek-readable@^4.0.1:
integrity sha512-7qmhptnR0WMSpxT5rMHG9bW/mYSR1uqaPFj2MHvT+y/aOUu6msJijpKt5SkTDKySwg65OWG2JwTMBlgcbwMHrQ== integrity sha512-7qmhptnR0WMSpxT5rMHG9bW/mYSR1uqaPFj2MHvT+y/aOUu6msJijpKt5SkTDKySwg65OWG2JwTMBlgcbwMHrQ==
"photoswipe@file:./thirdparty/photoswipe": "photoswipe@file:./thirdparty/photoswipe":
version "4.1.3" version "4.1.4"
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3: picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3:
version "2.3.0" version "2.3.0"