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 ( +
+ + + + + + + + {Number(values.option) === FIX_OPTIONS.CUSTOM_TIME && ( + + + handleChange('customTime')(x.toUTCString()) + } + /> + + )} + +
+ ); +} 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>(); const [activeCollection, setActiveCollection] = useState(undefined); const [trash, setTrash] = useState([]); + const [fixCreationTimeView, setFixCreationTimeView] = useState(false); + const [fixCreationTimeAttributes, setFixCreationTimeAttributes] = + useState(null); useEffect(() => { const key = getKey(SESSION_KEYS.ENCRYPTION_KEY); if (!key) { + appContext.setRedirectUrl(router.asPath); router.push(PAGES.ROOT); return; } @@ -227,11 +236,6 @@ export default function Gallery() { setCollections(collections); setTrash(trash); await setDerivativeState(collections, files); - await checkSubscriptionPurchase( - setDialogMessage, - router, - setLoading - ); await syncWithRemote(true); setIsFirstLoad(false); setJustSignedUp(false); @@ -243,13 +247,19 @@ export default function Gallery() { useEffect(() => setDialogView(true), [dialogMessage]); - useEffect(() => { - if (collectionSelectorAttributes) { - setCollectionSelectorView(true); - } - }, [collectionSelectorAttributes]); + useEffect( + () => collectionSelectorAttributes && setCollectionSelectorView(true), + [collectionSelectorAttributes] + ); - useEffect(() => setCollectionNamerView(true), [collectionNamerAttributes]); + useEffect( + () => collectionNamerAttributes && setCollectionNamerView(true), + [collectionNamerAttributes] + ); + useEffect( + () => fixCreationTimeAttributes && setFixCreationTimeView(true), + [fixCreationTimeAttributes] + ); useEffect(() => { if (typeof activeCollection === 'undefined') { @@ -270,6 +280,13 @@ export default function Gallery() { router.push(href, undefined, { shallow: true }); }, [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) => { if (syncInProgress.current && !force) { 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 ( + setFixCreationTimeView(false)} + show={() => setFixCreationTimeView(true)} + attributes={fixCreationTimeAttributes} + /> { 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 masterKey: string = await cryptoWorker.decryptB64( keyAttributes.masterKeyEncryptedWithRecoveryKey, diff --git a/src/pages/two-factor/recover/index.tsx b/src/pages/two-factor/recover/index.tsx index ee0219cdd..30f599eeb 100644 --- a/src/pages/two-factor/recover/index.tsx +++ b/src/pages/two-factor/recover/index.tsx @@ -12,6 +12,9 @@ import { logError } from 'utils/sentry'; import { recoverTwoFactor, removeTwoFactor } from 'services/userService'; import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app'; import { PAGES } from 'types'; +const bip39 = require('bip39'); +// mobile client library only supports english. +bip39.setDefaultWordlist('english'); export default function Recover() { const router = useRouter(); @@ -43,6 +46,13 @@ export default function Recover() { const recover = async (recoveryKey: string, setFieldError) => { 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 twoFactorSecret: string = await cryptoWorker.decryptB64( encryptedTwoFactorSecret.encryptedData, diff --git a/src/services/billingService.ts b/src/services/billingService.ts index 630dc0f34..7c09d5fba 100644 --- a/src/services/billingService.ts +++ b/src/services/billingService.ts @@ -134,6 +134,10 @@ class billingService { sessionID: string = null ): Promise { try { + const token = getToken(); + if (!token) { + return; + } const response = await HTTPService.post( `${ENDPOINT}/billing/verify-subscription`, { @@ -143,7 +147,7 @@ class billingService { }, null, { - 'X-Auth-Token': getToken(), + 'X-Auth-Token': token, } ); const { subscription } = response.data; diff --git a/src/services/downloadManager.ts b/src/services/downloadManager.ts index 77b8f656d..5e4c4b856 100644 --- a/src/services/downloadManager.ts +++ b/src/services/downloadManager.ts @@ -1,7 +1,11 @@ import { getToken } from 'utils/common/key'; import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil'; import CryptoWorker from 'utils/crypto'; -import { generateStreamFromArrayBuffer, convertForPreview } from 'utils/file'; +import { + generateStreamFromArrayBuffer, + convertForPreview, + needsConversionForPreview, +} from 'utils/file'; import HTTPService from './HTTPService'; import { File, FILE_TYPE } from './fileService'; import { logError } from 'utils/sentry'; @@ -10,27 +14,36 @@ class DownloadManager { private fileObjectUrlPromise = new Map>(); private thumbnailObjectUrlPromise = new Map>(); - public async getPreview(file: File) { + public async getThumbnail(file: File) { try { const token = getToken(); if (!token) { 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)) { - const downloadPromise = this.downloadThumb( - token, - thumbnailCache, - file - ); - this.thumbnailObjectUrlPromise.set(file.id, downloadPromise); + const downloadPromise = async () => { + const thumbnailCache = await caches.open('thumbs'); + const cacheResp: Response = await thumbnailCache.match( + file.id.toString() + ); + 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); } catch (e) { this.thumbnailObjectUrlPromise.delete(file.id); @@ -39,24 +52,7 @@ class DownloadManager { } } - private downloadThumb = async ( - 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) => { + downloadThumb = async (token: string, file: File) => { const resp = await HTTPService.get( getThumbnailUrl(file.id), null, @@ -73,32 +69,38 @@ class DownloadManager { }; getFile = async (file: File, forPreview = false) => { - let fileUID: string; - if (file.metadata.fileType === FILE_TYPE.VIDEO) { - fileUID = file.id.toString(); - } else { - fileUID = `${file.id}_forPreview=${forPreview}`; - } + const shouldBeConverted = forPreview && needsConversionForPreview(file); + const fileKey = shouldBeConverted + ? `${file.id}_converted` + : `${file.id}`; try { - const getFilePromise = async () => { + const getFilePromise = async (convert: boolean) => { const fileStream = await this.downloadFile(file); let fileBlob = await new Response(fileStream).blob(); - if (forPreview) { + if (convert) { fileBlob = await convertForPreview(file, fileBlob); } return URL.createObjectURL(fileBlob); }; - if (!this.fileObjectUrlPromise.get(fileUID)) { - this.fileObjectUrlPromise.set(fileUID, getFilePromise()); + if (!this.fileObjectUrlPromise.get(fileKey)) { + this.fileObjectUrlPromise.set( + fileKey, + getFilePromise(shouldBeConverted) + ); } - return await this.fileObjectUrlPromise.get(fileUID); + const fileURL = await this.fileObjectUrlPromise.get(fileKey); + return fileURL; } catch (e) { - this.fileObjectUrlPromise.delete(fileUID); + this.fileObjectUrlPromise.delete(fileKey); logError(e, 'Failed to get File'); throw e; } }; + public async getCachedOriginalFile(file: File) { + return await this.fileObjectUrlPromise.get(file.id.toString()); + } + async downloadFile(file: File) { const worker = await new CryptoWorker(); const token = getToken(); diff --git a/src/services/ffmpegService.ts b/src/services/ffmpegService.ts index 2b4bf2970..1184593b4 100644 --- a/src/services/ffmpegService.ts +++ b/src/services/ffmpegService.ts @@ -19,6 +19,8 @@ class FFmpegService { this.isLoading = null; } catch (e) { logError(e, 'ffmpeg load failed'); + this.ffmpeg = null; + this.isLoading = null; throw e; } } diff --git a/src/services/migrateThumbnailService.ts b/src/services/migrateThumbnailService.ts index d11286ec5..4245a4538 100644 --- a/src/services/migrateThumbnailService.ts +++ b/src/services/migrateThumbnailService.ts @@ -67,7 +67,7 @@ export async function replaceThumbnail( current: idx, total: largeThumbnailFiles.length, }); - const originalThumbnail = await downloadManager.getThumbnail( + const originalThumbnail = await downloadManager.downloadThumb( token, file ); diff --git a/src/services/searchService.ts b/src/services/searchService.ts index 34c5c06b9..7d41fed8a 100644 --- a/src/services/searchService.ts +++ b/src/services/searchService.ts @@ -5,8 +5,7 @@ import { DateValue, Suggestion, SuggestionType } from 'components/SearchBar'; import HTTPService from './HTTPService'; import { Collection } from './collectionService'; import { File } from './fileService'; -import { User } from './userService'; -import { getData, LS_KEYS } from 'utils/storage/localStorage'; +import { logError } from 'utils/sentry'; const ENDPOINT = getEndpoint(); 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( searchPhrase: string ): Promise { - const resp = await HTTPService.get( - `${ENDPOINT}/search/location`, - { - query: searchPhrase, - limit: 4, - }, - { - 'X-Auth-Token': getToken(), - } - ); - return resp.data.results; + try { + const resp = await HTTPService.get( + `${ENDPOINT}/search/location`, + { + query: searchPhrase, + limit: 4, + }, + { + 'X-Auth-Token': getToken(), + } + ); + return resp.data.results ?? []; + } catch (e) { + logError(e, 'location search failed'); + } + return []; } export function getHolidaySuggestion(searchPhrase: string): Suggestion[] { @@ -99,7 +103,7 @@ export function getYearSuggestion(searchPhrase: string): Suggestion[] { ]; } } catch (e) { - // ignore + logError(e, 'getYearSuggestion failed'); } } return []; @@ -115,8 +119,6 @@ export function searchCollection( } export function searchFiles(searchPhrase: string, files: File[]) { - const user: User = getData(LS_KEYS.USER) ?? {}; - const idSet = new Set(); return files .map((file, idx) => ({ title: file.metadata.title, @@ -125,13 +127,6 @@ export function searchFiles(searchPhrase: string, files: File[]) { ownerID: file.ownerID, 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)) .slice(0, 4); } diff --git a/src/services/updateCreationTimeWithExif.ts b/src/services/updateCreationTimeWithExif.ts new file mode 100644 index 000000000..b88004f54 --- /dev/null +++ b/src/services/updateCreationTimeWithExif.ts @@ -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; +} diff --git a/src/services/upload/exifService.ts b/src/services/upload/exifService.ts index 004958b6f..d93a49caf 100644 --- a/src/services/upload/exifService.ts +++ b/src/services/upload/exifService.ts @@ -1,6 +1,8 @@ import exifr from 'exifr'; +import { logError } from 'utils/sentry'; import { NULL_LOCATION, Location } from './metadataService'; +import { FileTypeInfo } from './readFileService'; const EXIF_TAGS_NEEDED = [ 'DateTimeOriginal', @@ -11,37 +13,77 @@ 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; } export async function getExifData( - receivedFile: globalThis.File + receivedFile: globalThis.File, + fileTypeInfo: FileTypeInfo ): Promise { - const exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED); - if (!exifData) { - return { location: NULL_LOCATION, creationTime: null }; - } - const parsedEXIFData = { - location: getEXIFLocation(exifData), - creationTime: getUNIXTime(exifData), + const nullExifData: ParsedEXIFData = { + location: NULL_LOCATION, + creationTime: null, }; - 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) { - const dateTime: Date = - exifData.DateTimeOriginal ?? exifData.CreateDate ?? exifData.ModifyDate; - - if (!dateTime) { - return null; +export async function getRawExif( + receivedFile: File, + fileTypeInfo: FileTypeInfo +) { + let exifData: Exif; + 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; - if (unixTime <= 0) { - return null; - } else { - return unixTime; + return exifData; +} + +export function getUNIXTime(dateTime: Date) { + 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 }); } } diff --git a/src/services/upload/metadataService.ts b/src/services/upload/metadataService.ts index fab027ba6..305b0b202 100644 --- a/src/services/upload/metadataService.ts +++ b/src/services/upload/metadataService.ts @@ -34,14 +34,7 @@ export async function extractMetadata( ) { let exifData = null; if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) { - try { - exifData = await getExifData(receivedFile); - } catch (e) { - logError(e, 'file missing exif data ', { - fileType: fileTypeInfo.exactType, - }); - // ignore exif parsing errors - } + exifData = await getExifData(receivedFile, fileTypeInfo); } const extractedMetadata: MetadataObject = { diff --git a/src/services/upload/thumbnailService.ts b/src/services/upload/thumbnailService.ts index f4c8fd855..8872150df 100644 --- a/src/services/upload/thumbnailService.ts +++ b/src/services/upload/thumbnailService.ts @@ -4,7 +4,7 @@ import { logError } from 'utils/sentry'; import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64'; import FFmpegService from 'services/ffmpegService'; import { convertToHumanReadable } from 'utils/billingUtil'; -import { fileIsHEIC } from 'utils/file'; +import { isFileHEIC } from 'utils/file'; import { FileTypeInfo } from './readFileService'; const MAX_THUMBNAIL_DIMENSION = 720; @@ -31,7 +31,7 @@ export async function generateThumbnail( let thumbnail: Uint8Array; try { if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) { - const isHEIC = fileIsHEIC(fileTypeInfo.exactType); + const isHEIC = isFileHEIC(fileTypeInfo.exactType); canvas = await generateImageThumbnail(worker, file, isHEIC); } else { try { @@ -44,7 +44,7 @@ export async function generateThumbnail( ); } catch (e) { logError(e, 'failed to generate thumbnail using ffmpeg', { - type: fileTypeInfo.exactType, + fileFormat: fileTypeInfo.exactType, }); canvas = await generateVideoThumbnail(file); } diff --git a/src/services/userService.ts b/src/services/userService.ts index c55a9722f..5960b076f 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -28,6 +28,8 @@ const ENDPOINT = getEndpoint(); const HAS_SET_KEYS = 'hasSetKeys'; +export const FIX_CREATION_TIME_VISIBLE_TO_USER_IDS = [1, 125, 243, 341]; + export interface User { id: number; name: string; diff --git a/src/utils/billingUtil.ts b/src/utils/billingUtil.ts index 555bd9016..43e1e1924 100644 --- a/src/utils/billingUtil.ts +++ b/src/utils/billingUtil.ts @@ -191,17 +191,14 @@ export async function checkSubscriptionPurchase( router: NextRouter, setLoading: SetLoading ) { + const { session_id: sessionId, status, reason } = router.query ?? {}; 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) { - handleFailureReason(reason, setDialogMessage, setLoading); + handleFailureReason(reason as string, setDialogMessage, setLoading); } else if (status === RESPONSE_STATUS.success) { try { const subscription = await billingService.verifySubscription( - sessionId + sessionId as string ); setDialogMessage({ title: constants.SUBSCRIPTION_PURCHASE_SUCCESS_TITLE, @@ -220,8 +217,6 @@ export async function checkSubscriptionPurchase( } } catch (e) { // ignore - } finally { - router.push('gallery', undefined, { shallow: true }); } } diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts index 5ea88f252..1e7b08f25 100644 --- a/src/utils/file/index.ts +++ b/src/utils/file/index.ts @@ -11,7 +11,7 @@ import { } from 'services/fileService'; import { decodeMotionPhoto } from 'services/motionPhotoService'; import { getMimeTypeFromBlob } from 'services/upload/readFileService'; -import DownloadManger from 'services/downloadManager'; +import DownloadManager from 'services/downloadManager'; import { logError } from 'utils/sentry'; import { User } from 'services/userService'; import CryptoWorker from 'utils/crypto'; @@ -37,10 +37,16 @@ export function downloadAsFile(filename: string, content: string) { a.remove(); } -export async function downloadFile(file) { +export async function downloadFile(file: File) { const a = document.createElement('a'); 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) { a.download = fileNameWithoutExtension(file.metadata.title) + '.zip'; } else { @@ -51,10 +57,11 @@ export async function downloadFile(file) { a.remove(); } -export function fileIsHEIC(mimeType: string) { +export function isFileHEIC(mimeType: string) { return ( - mimeType.toLowerCase().endsWith(TYPE_HEIC) || - mimeType.toLowerCase().endsWith(TYPE_HEIF) + mimeType && + (mimeType.toLowerCase().endsWith(TYPE_HEIC) || + mimeType.toLowerCase().endsWith(TYPE_HEIF)) ); } @@ -271,7 +278,7 @@ export async function convertForPreview(file: File, fileBlob: Blob) { const mimeType = (await getMimeTypeFromBlob(worker, fileBlob)) ?? typeFromExtension; - if (fileIsHEIC(mimeType)) { + if (isFileHEIC(mimeType)) { fileBlob = await worker.convertHEIC2JPEG(fileBlob); } return fileBlob; @@ -438,3 +445,54 @@ export function updateExistingFilePubMetadata( existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata; 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(); + 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; + } +} diff --git a/src/utils/strings/englishConstants.tsx b/src/utils/strings/englishConstants.tsx index 7f88e8b7d..27e82c1aa 100644 --- a/src/utils/strings/englishConstants.tsx +++ b/src/utils/strings/englishConstants.tsx @@ -579,8 +579,8 @@ const englishConstants = { SORT_BY_COLLECTION_NAME: 'album name', FIX_LARGE_THUMBNAILS: 'compress thumbnails', THUMBNAIL_REPLACED: 'thumbnails compressed', - FIX: 'compress', - FIX_LATER: 'compress later', + FIX_THUMBNAIL: 'compress', + FIX_THUMBNAIL_LATER: 'compress later', REPLACE_THUMBNAIL_NOT_STARTED: () => ( <> some of your videos thumbnails can be compressed to save space. @@ -596,7 +596,23 @@ const englishConstants = { REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR: () => ( <>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', + + DATE_TIME_ORIGINAL: 'EXIF:DateTimeOriginal', + DATE_TIME_DIGITIZED: 'EXIF:DateTimeDigitized', + CUSTOM_TIME: 'custom time', }; export default englishConstants; diff --git a/thirdparty/photoswipe b/thirdparty/photoswipe index 443b1e393..b1766d384 160000 --- a/thirdparty/photoswipe +++ b/thirdparty/photoswipe @@ -1 +1 @@ -Subproject commit 443b1e393aa37899373b71272e4bcf191529bb74 +Subproject commit b1766d38475659c17cf669e2b27787d15f8957b1 diff --git a/yarn.lock b/yarn.lock index 143b09bb9..aefbf3e4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1453,6 +1453,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz" 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": version "14.17.15" 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" 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: version "3.7.2" 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" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pbkdf2@^3.0.3: +pbkdf2@^3.0.3, pbkdf2@^3.0.9: version "3.1.2" resolved "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz" integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== @@ -5033,7 +5048,7 @@ peek-readable@^4.0.1: integrity sha512-7qmhptnR0WMSpxT5rMHG9bW/mYSR1uqaPFj2MHvT+y/aOUu6msJijpKt5SkTDKySwg65OWG2JwTMBlgcbwMHrQ== "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: version "2.3.0"