Merge branch 'main' into update-is-sentry-enabled
This commit is contained in:
commit
a8a7a1b37f
|
@ -1,4 +1,11 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
branch="$(git rev-parse --abbrev-ref HEAD)"
|
||||
|
||||
if [ "$branch" = "main" ]; then
|
||||
echo "You can't commit directly to main branch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
yarn lint-staged
|
||||
|
|
|
@ -33,7 +33,7 @@ const TOTPDisplay = ({ issuer, account, code, nextCode, period }) => {
|
|||
<p
|
||||
style={{
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0px',
|
||||
margin: '0px',
|
||||
fontSize: '14px',
|
||||
textAlign: 'left',
|
||||
}}>
|
||||
|
@ -41,6 +41,7 @@ const TOTPDisplay = ({ issuer, account, code, nextCode, period }) => {
|
|||
</p>
|
||||
<p
|
||||
style={{
|
||||
marginTop: '0px',
|
||||
marginBottom: '8px',
|
||||
textAlign: 'left',
|
||||
fontSize: '12px',
|
||||
|
@ -52,6 +53,8 @@ const TOTPDisplay = ({ issuer, account, code, nextCode, period }) => {
|
|||
</p>
|
||||
<p
|
||||
style={{
|
||||
margin: '0px',
|
||||
marginBottom: '1rem',
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'left',
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"bs58": "^5.0.0",
|
||||
"chrono-node": "^2.2.6",
|
||||
"comlink": "^4.3.0",
|
||||
"debounce-promise": "^3.1.2",
|
||||
"debounce": "^2.0.0",
|
||||
"density-clustering": "^1.3.0",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"exifr": "^7.1.3",
|
||||
|
@ -37,7 +37,7 @@
|
|||
"formik": "^2.1.5",
|
||||
"get-user-locale": "^2.1.3",
|
||||
"hdbscan": "0.0.1-alpha.5",
|
||||
"heic-convert": "^1.2.4",
|
||||
"heic-convert": "^2.0.0",
|
||||
"i18next": "^23.4.1",
|
||||
"i18next-http-backend": "^2.1.1",
|
||||
"idb": "^7.1.1",
|
||||
|
@ -51,6 +51,7 @@
|
|||
"ml-matrix": "^6.10.4",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"otpauth": "^9.0.2",
|
||||
"p-debounce": "^4.0.0",
|
||||
"p-queue": "^7.1.0",
|
||||
"photoswipe": "file:./thirdparty/photoswipe",
|
||||
"piexifjs": "^1.0.6",
|
||||
|
@ -81,7 +82,6 @@
|
|||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^13.4.12",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/debounce-promise": "^3.1.3",
|
||||
"@types/leaflet": "^1.9.3",
|
||||
"@types/libsodium-wrappers": "^0.7.8",
|
||||
"@types/node": "^14.6.4",
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generierung von Verschlüsselungsschlüsseln...",
|
||||
"PASSPHRASE_HINT": "Passwort",
|
||||
"CONFIRM_PASSPHRASE": "Passwort bestätigen",
|
||||
"REFERRAL_CODE_HINT": "",
|
||||
"REFERRAL_INFO": "",
|
||||
"PASSPHRASE_MATCH_ERROR": "Die Passwörter stimmen nicht überein",
|
||||
"CONSOLE_WARNING_STOP": "STOPP!",
|
||||
"CONSOLE_WARNING_DESC": "",
|
||||
|
@ -423,7 +425,6 @@
|
|||
"FILES": "Dateien",
|
||||
"EACH": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "",
|
||||
"STOP_UPLOADS_HEADER": "Hochladen stoppen?",
|
||||
"YES_STOP_UPLOADS": "Ja, Hochladen stoppen",
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generating encryption keys...",
|
||||
"PASSPHRASE_HINT": "Password",
|
||||
"CONFIRM_PASSPHRASE": "Confirm password",
|
||||
"REFERRAL_CODE_HINT":"How did you hear about Ente? (optional)",
|
||||
"REFERRAL_INFO":"We don't track app installs, It'd help us if you told us where you found us!",
|
||||
"PASSPHRASE_MATCH_ERROR": "Passwords don't match",
|
||||
"CONSOLE_WARNING_STOP": "STOP!",
|
||||
"CONSOLE_WARNING_DESC": "This is a browser feature intended for developers. Please don't copy-paste unverified code here.",
|
||||
|
@ -423,7 +425,6 @@
|
|||
"FILES": "Files",
|
||||
"EACH": "Each",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "The following files were clubbed based on their sizes and capture time, please review and delete items you believe are duplicates",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?",
|
||||
"STOP_UPLOADS_HEADER": "Stop uploads?",
|
||||
"YES_STOP_UPLOADS": "Yes, stop uploads",
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generando claves de encriptación...",
|
||||
"PASSPHRASE_HINT": "Contraseña",
|
||||
"CONFIRM_PASSPHRASE": "Confirmar contraseña",
|
||||
"REFERRAL_CODE_HINT": "",
|
||||
"REFERRAL_INFO": "",
|
||||
"PASSPHRASE_MATCH_ERROR": "Las contraseñas no coinciden",
|
||||
"CONSOLE_WARNING_STOP": "STOP!",
|
||||
"CONSOLE_WARNING_DESC": "Esta es una característica del navegador destinada a los desarrolladores. Por favor, no copie y pegue código sin verificar aquí.",
|
||||
|
@ -423,7 +425,6 @@
|
|||
"FILES": "Archivos",
|
||||
"EACH": "Cada",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "Los siguientes archivos fueron organizados en base a sus tamaños, por favor revise y elimine elementos que cree que son duplicados",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "Los siguientes archivos fueron organizados en base a sus tamaños y tiempo de captura, por favor revise y elimine elementos que cree que son duplicados",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "¿Está seguro que desea detener todas las subidas en curso?",
|
||||
"STOP_UPLOADS_HEADER": "Detener las subidas?",
|
||||
"YES_STOP_UPLOADS": "Sí, detener las subidas",
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "",
|
||||
"PASSPHRASE_HINT": "",
|
||||
"CONFIRM_PASSPHRASE": "",
|
||||
"REFERRAL_CODE_HINT": "",
|
||||
"REFERRAL_INFO": "",
|
||||
"PASSPHRASE_MATCH_ERROR": "",
|
||||
"CONSOLE_WARNING_STOP": "",
|
||||
"CONSOLE_WARNING_DESC": "",
|
||||
|
@ -423,7 +425,6 @@
|
|||
"FILES": "",
|
||||
"EACH": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "",
|
||||
"STOP_UPLOADS_HEADER": "",
|
||||
"YES_STOP_UPLOADS": "",
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "",
|
||||
"PASSPHRASE_HINT": "",
|
||||
"CONFIRM_PASSPHRASE": "",
|
||||
"REFERRAL_CODE_HINT": "",
|
||||
"REFERRAL_INFO": "",
|
||||
"PASSPHRASE_MATCH_ERROR": "",
|
||||
"CONSOLE_WARNING_STOP": "",
|
||||
"CONSOLE_WARNING_DESC": "",
|
||||
|
@ -423,7 +425,6 @@
|
|||
"FILES": "",
|
||||
"EACH": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "",
|
||||
"STOP_UPLOADS_HEADER": "",
|
||||
"YES_STOP_UPLOADS": "",
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Génération des clés de chiffrement...",
|
||||
"PASSPHRASE_HINT": "Mot de passe",
|
||||
"CONFIRM_PASSPHRASE": "Confirmer le mot de passe",
|
||||
"REFERRAL_CODE_HINT": "",
|
||||
"REFERRAL_INFO": "",
|
||||
"PASSPHRASE_MATCH_ERROR": "Les mots de passe ne correspondent pas",
|
||||
"CONSOLE_WARNING_STOP": "STOP!",
|
||||
"CONSOLE_WARNING_DESC": "Ceci est une fonction de navigateur dédiée aux développeurs. Veuillez ne pas copier-coller un code non vérifié à cet endroit.",
|
||||
|
@ -423,7 +425,6 @@
|
|||
"FILES": "Fichiers",
|
||||
"EACH": "Chacun",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "Les fichiers suivants ont été clubbed, basé sur leurs tailles, veuillez corriger et supprimer les objets que vous pensez être dupliqués",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "Les fichiers suivants ont été clubbed, basé sur leurs tailles et de l'heure de capture, veuillez corriger et supprimer les objets que vous pensez être dupliqués",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "Êtes-vous certains de vouloir arrêter tous les chargements en cours?",
|
||||
"STOP_UPLOADS_HEADER": "Arrêter les chargements ?",
|
||||
"YES_STOP_UPLOADS": "Oui, arrêter tout",
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generazione delle chiavi di crittografia...",
|
||||
"PASSPHRASE_HINT": "Password",
|
||||
"CONFIRM_PASSPHRASE": "Conferma la password",
|
||||
"REFERRAL_CODE_HINT": "",
|
||||
"REFERRAL_INFO": "",
|
||||
"PASSPHRASE_MATCH_ERROR": "Le password non corrispondono",
|
||||
"CONSOLE_WARNING_STOP": "STOP!",
|
||||
"CONSOLE_WARNING_DESC": "Questa è una funzionalità del browser destinata agli sviluppatori. Non copiare né incollare codice non verificato qui.",
|
||||
|
@ -423,7 +425,6 @@
|
|||
"FILES": "",
|
||||
"EACH": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "",
|
||||
"STOP_UPLOADS_HEADER": "",
|
||||
"YES_STOP_UPLOADS": "",
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Encryptiecodes worden gegenereerd...",
|
||||
"PASSPHRASE_HINT": "Wachtwoord",
|
||||
"CONFIRM_PASSPHRASE": "Wachtwoord bevestigen",
|
||||
"REFERRAL_CODE_HINT": "Hoe hoorde je over Ente? (optioneel)",
|
||||
"REFERRAL_INFO": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!",
|
||||
"PASSPHRASE_MATCH_ERROR": "Wachtwoorden komen niet overeen",
|
||||
"CONSOLE_WARNING_STOP": "STOP!",
|
||||
"CONSOLE_WARNING_DESC": "Dit is een browserfunctie bedoeld voor ontwikkelaars. Gelieve hier geen niet-geverifieerde code te kopiëren/plakken.",
|
||||
|
@ -157,7 +159,7 @@
|
|||
"RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Vernieuwt op {{date, dateTime}}",
|
||||
"RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Eindigt op {{date, dateTime}}",
|
||||
"RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Uw abonnement loopt af op {{date, dateTime}}",
|
||||
"ADD_ON_AVAILABLE_TILL": "",
|
||||
"ADD_ON_AVAILABLE_TILL": "Jouw {{storage, string}} add-on is geldig tot {{date, dateTime}}",
|
||||
"STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "U heeft uw opslaglimiet overschreden, gelieve <a>upgraden</a>",
|
||||
"SUBSCRIPTION_PURCHASE_SUCCESS": "<p>We hebben uw betaling ontvangen</p><p>Uw abonnement is geldig tot <strong>{{date, dateTime}}</strong></p>",
|
||||
"SUBSCRIPTION_PURCHASE_CANCELLED": "Uw aankoop is geannuleerd, probeer het opnieuw als u zich wilt abonneren",
|
||||
|
@ -172,7 +174,7 @@
|
|||
"UPDATE_SUBSCRIPTION": "Abonnement wijzigen",
|
||||
"CANCEL_SUBSCRIPTION": "Abonnement opzeggen",
|
||||
"CANCEL_SUBSCRIPTION_MESSAGE": "<p>Al je gegevens zullen worden verwijderd van onze servers aan het einde van deze factureringsperiode.</p><p>Weet u zeker dat u uw abonnement wilt opzeggen?</p>",
|
||||
"CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "",
|
||||
"CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "<p>Weet je zeker dat je je abonnement wilt opzeggen?</p>",
|
||||
"SUBSCRIPTION_CANCEL_FAILED": "Abonnement opzeggen mislukt",
|
||||
"SUBSCRIPTION_CANCEL_SUCCESS": "Abonnement succesvol geannuleerd",
|
||||
"REACTIVATE_SUBSCRIPTION": "Abonnement opnieuw activeren",
|
||||
|
@ -423,7 +425,6 @@
|
|||
"FILES": "Bestanden",
|
||||
"EACH": "Elke",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "De volgende bestanden zijn samengevoegd op basis van hun groottes. Controleer en verwijder items waarvan je denkt dat ze dubbel zijn",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "De volgende bestanden zijn samengevoegd op basis van hun groottes en opnametijd, bekijk en verwijder items waarvan je denkt dat ze dubbel zijn",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "Weet u zeker dat u wilt stoppen met alle uploads die worden uitgevoerd?",
|
||||
"STOP_UPLOADS_HEADER": "Stoppen met uploaden?",
|
||||
"YES_STOP_UPLOADS": "Ja, stop uploaden",
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "",
|
||||
"PASSPHRASE_HINT": "",
|
||||
"CONFIRM_PASSPHRASE": "",
|
||||
"REFERRAL_CODE_HINT": "",
|
||||
"REFERRAL_INFO": "",
|
||||
"PASSPHRASE_MATCH_ERROR": "",
|
||||
"CONSOLE_WARNING_STOP": "PARAR!",
|
||||
"CONSOLE_WARNING_DESC": "",
|
||||
|
@ -423,7 +425,6 @@
|
|||
"FILES": "",
|
||||
"EACH": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "",
|
||||
"STOP_UPLOADS_HEADER": "",
|
||||
"YES_STOP_UPLOADS": "",
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "",
|
||||
"PASSPHRASE_HINT": "",
|
||||
"CONFIRM_PASSPHRASE": "",
|
||||
"REFERRAL_CODE_HINT": "",
|
||||
"REFERRAL_INFO": "",
|
||||
"PASSPHRASE_MATCH_ERROR": "",
|
||||
"CONSOLE_WARNING_STOP": "",
|
||||
"CONSOLE_WARNING_DESC": "",
|
||||
|
@ -423,7 +425,6 @@
|
|||
"FILES": "",
|
||||
"EACH": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "",
|
||||
"STOP_UPLOADS_HEADER": "",
|
||||
"YES_STOP_UPLOADS": "",
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "",
|
||||
"PASSPHRASE_HINT": "",
|
||||
"CONFIRM_PASSPHRASE": "",
|
||||
"REFERRAL_CODE_HINT": "",
|
||||
"REFERRAL_INFO": "",
|
||||
"PASSPHRASE_MATCH_ERROR": "",
|
||||
"CONSOLE_WARNING_STOP": "",
|
||||
"CONSOLE_WARNING_DESC": "",
|
||||
|
@ -423,7 +425,6 @@
|
|||
"FILES": "",
|
||||
"EACH": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "",
|
||||
"STOP_UPLOADS_HEADER": "",
|
||||
"YES_STOP_UPLOADS": "",
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "正在生成加密密钥...",
|
||||
"PASSPHRASE_HINT": "密码",
|
||||
"CONFIRM_PASSPHRASE": "请确认密码",
|
||||
"REFERRAL_CODE_HINT": "您是如何知道Ente的? (可选的)",
|
||||
"REFERRAL_INFO": "我们不跟踪应用程序安装情况,如果您告诉我们您是在哪里找到我们的,将会有所帮助!",
|
||||
"PASSPHRASE_MATCH_ERROR": "两次输入的密码不一致",
|
||||
"CONSOLE_WARNING_STOP": "停止!",
|
||||
"CONSOLE_WARNING_DESC": "这是专为开发人员设计的浏览器功能。 请不要在此处复制粘贴未经验证的代码。",
|
||||
|
@ -423,7 +425,6 @@
|
|||
"FILES": "文件",
|
||||
"EACH": "每个",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "以下文件根据大小进行了合并,请检查并删除您认为重复的项目",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "以下文件是根据它们的大小和捕获时间合并的,请检查并删除您认为重复的项目",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "您确定要停止所有正在进行的上传吗?",
|
||||
"STOP_UPLOADS_HEADER": "要停止上传吗?",
|
||||
"YES_STOP_UPLOADS": "是的,停止上传",
|
||||
|
|
|
@ -8,7 +8,6 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
|||
import PhotoViewer from 'components/PhotoViewer';
|
||||
import { TRASH_SECTION } from 'constants/collection';
|
||||
import { updateFileMsrcProps, updateFileSrcProps } from 'utils/photoFrame';
|
||||
import { PhotoList } from './PhotoList';
|
||||
import { MergedSourceURL, SelectedState } from 'types/gallery';
|
||||
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
|
||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
|
@ -19,6 +18,10 @@ import PhotoSwipe from 'photoswipe';
|
|||
import useMemoSingleThreaded from '@ente/shared/hooks/useMemoSingleThreaded';
|
||||
import { getPlayableVideo } from 'utils/file';
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
import { PHOTOS_PAGES } from '@ente/shared/constants/pages';
|
||||
import { PhotoList } from './PhotoList';
|
||||
import { DedupePhotoList } from './PhotoList/dedupe';
|
||||
import { Duplicate } from 'services/deduplicationService';
|
||||
|
||||
const Container = styled('div')`
|
||||
display: block;
|
||||
|
@ -36,7 +39,12 @@ const Container = styled('div')`
|
|||
const PHOTOSWIPE_HASH_SUFFIX = '&opened';
|
||||
|
||||
interface Props {
|
||||
page:
|
||||
| PHOTOS_PAGES.GALLERY
|
||||
| PHOTOS_PAGES.DEDUPLICATE
|
||||
| PHOTOS_PAGES.SHARED_ALBUMS;
|
||||
files: EnteFile[];
|
||||
duplicates?: Duplicate[];
|
||||
syncWithRemote: () => Promise<void>;
|
||||
favItemIds?: Set<number>;
|
||||
setSelected: (
|
||||
|
@ -55,6 +63,8 @@ interface Props {
|
|||
}
|
||||
|
||||
const PhotoFrame = ({
|
||||
page,
|
||||
duplicates,
|
||||
files,
|
||||
syncWithRemote,
|
||||
favItemIds,
|
||||
|
@ -605,16 +615,27 @@ const PhotoFrame = ({
|
|||
return (
|
||||
<Container>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<PhotoList
|
||||
width={width}
|
||||
height={height}
|
||||
getThumbnail={getThumbnail}
|
||||
displayFiles={displayFiles}
|
||||
activeCollectionID={activeCollectionID}
|
||||
showAppDownloadBanner={showAppDownloadBanner}
|
||||
/>
|
||||
)}
|
||||
{({ height, width }) =>
|
||||
page === PHOTOS_PAGES.DEDUPLICATE ? (
|
||||
<DedupePhotoList
|
||||
width={width}
|
||||
height={height}
|
||||
getThumbnail={getThumbnail}
|
||||
duplicates={duplicates}
|
||||
activeCollectionID={activeCollectionID}
|
||||
showAppDownloadBanner={showAppDownloadBanner}
|
||||
/>
|
||||
) : (
|
||||
<PhotoList
|
||||
width={width}
|
||||
height={height}
|
||||
getThumbnail={getThumbnail}
|
||||
displayFiles={displayFiles}
|
||||
activeCollectionID={activeCollectionID}
|
||||
showAppDownloadBanner={showAppDownloadBanner}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</AutoSizer>
|
||||
<PhotoViewer
|
||||
isOpen={open}
|
||||
|
|
365
apps/photos/src/components/PhotoList/dedupe.tsx
Normal file
365
apps/photos/src/components/PhotoList/dedupe.tsx
Normal file
|
@ -0,0 +1,365 @@
|
|||
import React, { useRef, useEffect, useState, useMemo } from 'react';
|
||||
import {
|
||||
VariableSizeList as List,
|
||||
ListChildComponentProps,
|
||||
areEqual,
|
||||
} from 'react-window';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { EnteFile } from 'types/file';
|
||||
import {
|
||||
IMAGE_CONTAINER_MAX_HEIGHT,
|
||||
MIN_COLUMNS,
|
||||
DATE_CONTAINER_HEIGHT,
|
||||
GAP_BTW_TILES,
|
||||
SPACE_BTW_DATES,
|
||||
SIZE_AND_COUNT_CONTAINER_HEIGHT,
|
||||
IMAGE_CONTAINER_MAX_WIDTH,
|
||||
} from 'constants/gallery';
|
||||
import { convertBytesToHumanReadable } from '@ente/shared/utils/size';
|
||||
import { FlexWrapper } from '@ente/shared/components/Container';
|
||||
import { t } from 'i18next';
|
||||
import memoize from 'memoize-one';
|
||||
import { Duplicate } from 'services/deduplicationService';
|
||||
|
||||
export enum ITEM_TYPE {
|
||||
TIME = 'TIME',
|
||||
FILE = 'FILE',
|
||||
SIZE_AND_COUNT = 'SIZE_AND_COUNT',
|
||||
HEADER = 'HEADER',
|
||||
FOOTER = 'FOOTER',
|
||||
MARKETING_FOOTER = 'MARKETING_FOOTER',
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
|
||||
export interface TimeStampListItem {
|
||||
itemType: ITEM_TYPE;
|
||||
items?: EnteFile[];
|
||||
itemStartIndex?: number;
|
||||
date?: string;
|
||||
dates?: {
|
||||
date: string;
|
||||
span: number;
|
||||
}[];
|
||||
groups?: number[];
|
||||
item?: any;
|
||||
id?: string;
|
||||
height?: number;
|
||||
fileSize?: number;
|
||||
fileCount?: number;
|
||||
}
|
||||
|
||||
const ListItem = styled('div')`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const getTemplateColumns = (
|
||||
columns: number,
|
||||
shrinkRatio: number,
|
||||
groups?: number[]
|
||||
): string => {
|
||||
if (groups) {
|
||||
// need to confirm why this was there
|
||||
// const sum = groups.reduce((acc, item) => acc + item, 0);
|
||||
// if (sum < columns) {
|
||||
// groups[groups.length - 1] += columns - sum;
|
||||
// }
|
||||
return groups
|
||||
.map(
|
||||
(x) =>
|
||||
`repeat(${x}, ${IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio}px)`
|
||||
)
|
||||
.join(` ${SPACE_BTW_DATES}px `);
|
||||
} else {
|
||||
return `repeat(${columns},${
|
||||
IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio
|
||||
}px)`;
|
||||
}
|
||||
};
|
||||
|
||||
function getFractionFittableColumns(width: number): number {
|
||||
return (
|
||||
(width - 2 * getGapFromScreenEdge(width) + GAP_BTW_TILES) /
|
||||
(IMAGE_CONTAINER_MAX_WIDTH + GAP_BTW_TILES)
|
||||
);
|
||||
}
|
||||
|
||||
function getGapFromScreenEdge(width: number) {
|
||||
if (width > MIN_COLUMNS * IMAGE_CONTAINER_MAX_WIDTH) {
|
||||
return 24;
|
||||
} else {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
function getShrinkRatio(width: number, columns: number) {
|
||||
return (
|
||||
(width -
|
||||
2 * getGapFromScreenEdge(width) -
|
||||
(columns - 1) * GAP_BTW_TILES) /
|
||||
(columns * IMAGE_CONTAINER_MAX_WIDTH)
|
||||
);
|
||||
}
|
||||
|
||||
const ListContainer = styled(Box)<{
|
||||
columns: number;
|
||||
shrinkRatio: number;
|
||||
groups?: number[];
|
||||
}>`
|
||||
display: grid;
|
||||
grid-template-columns: ${({ columns, shrinkRatio, groups }) =>
|
||||
getTemplateColumns(columns, shrinkRatio, groups)};
|
||||
grid-column-gap: ${GAP_BTW_TILES}px;
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
padding: 0 24px;
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ListItemContainer = styled(FlexWrapper)<{ span: number }>`
|
||||
grid-column: span ${(props) => props.span};
|
||||
`;
|
||||
|
||||
const DateContainer = styled(ListItemContainer)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: ${DATE_CONTAINER_HEIGHT}px;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
`;
|
||||
|
||||
const SizeAndCountContainer = styled(DateContainer)`
|
||||
margin-top: 1rem;
|
||||
height: ${SIZE_AND_COUNT_CONTAINER_HEIGHT}px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
height: number;
|
||||
width: number;
|
||||
duplicates: Duplicate[];
|
||||
showAppDownloadBanner: boolean;
|
||||
getThumbnail: (
|
||||
file: EnteFile,
|
||||
index: number,
|
||||
isScrolling?: boolean
|
||||
) => JSX.Element;
|
||||
activeCollectionID: number;
|
||||
}
|
||||
|
||||
interface ItemData {
|
||||
timeStampList: TimeStampListItem[];
|
||||
columns: number;
|
||||
shrinkRatio: number;
|
||||
renderListItem: (
|
||||
timeStampListItem: TimeStampListItem,
|
||||
isScrolling?: boolean
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
const createItemData = memoize(
|
||||
(
|
||||
timeStampList: TimeStampListItem[],
|
||||
columns: number,
|
||||
shrinkRatio: number,
|
||||
renderListItem: (
|
||||
timeStampListItem: TimeStampListItem,
|
||||
isScrolling?: boolean
|
||||
) => JSX.Element
|
||||
): ItemData => ({
|
||||
timeStampList,
|
||||
columns,
|
||||
shrinkRatio,
|
||||
renderListItem,
|
||||
})
|
||||
);
|
||||
const PhotoListRow = React.memo(
|
||||
({
|
||||
index,
|
||||
style,
|
||||
isScrolling,
|
||||
data,
|
||||
}: ListChildComponentProps<ItemData>) => {
|
||||
const { timeStampList, columns, shrinkRatio, renderListItem } = data;
|
||||
return (
|
||||
<ListItem style={style}>
|
||||
<ListContainer
|
||||
columns={columns}
|
||||
shrinkRatio={shrinkRatio}
|
||||
groups={timeStampList[index].groups}>
|
||||
{renderListItem(timeStampList[index], isScrolling)}
|
||||
</ListContainer>
|
||||
</ListItem>
|
||||
);
|
||||
},
|
||||
areEqual
|
||||
);
|
||||
|
||||
const getTimeStampListFromDuplicates = (duplicates: Duplicate[], columns) => {
|
||||
const timeStampList: TimeStampListItem[] = [];
|
||||
for (let index = 0; index < duplicates.length; index++) {
|
||||
const dupes = duplicates[index];
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.SIZE_AND_COUNT,
|
||||
fileSize: dupes.size,
|
||||
fileCount: dupes.files.length,
|
||||
});
|
||||
let lastIndex = 0;
|
||||
while (lastIndex < dupes.files.length) {
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.FILE,
|
||||
items: dupes.files.slice(lastIndex, lastIndex + columns),
|
||||
itemStartIndex: index,
|
||||
});
|
||||
lastIndex += columns;
|
||||
}
|
||||
}
|
||||
return timeStampList;
|
||||
};
|
||||
|
||||
export function DedupePhotoList({
|
||||
height,
|
||||
width,
|
||||
duplicates,
|
||||
getThumbnail,
|
||||
activeCollectionID,
|
||||
}: Props) {
|
||||
const [timeStampList, setTimeStampList] = useState<TimeStampListItem[]>([]);
|
||||
const refreshInProgress = useRef(false);
|
||||
const shouldRefresh = useRef(false);
|
||||
const listRef = useRef(null);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const fittableColumns = getFractionFittableColumns(width);
|
||||
let columns = Math.floor(fittableColumns);
|
||||
if (columns < MIN_COLUMNS) {
|
||||
columns = MIN_COLUMNS;
|
||||
}
|
||||
return columns;
|
||||
}, [width]);
|
||||
|
||||
const shrinkRatio = getShrinkRatio(width, columns);
|
||||
const listItemHeight =
|
||||
IMAGE_CONTAINER_MAX_HEIGHT * shrinkRatio + GAP_BTW_TILES;
|
||||
|
||||
const refreshList = () => {
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const main = () => {
|
||||
if (refreshInProgress.current) {
|
||||
shouldRefresh.current = true;
|
||||
return;
|
||||
}
|
||||
refreshInProgress.current = true;
|
||||
const timeStampList = getTimeStampListFromDuplicates(
|
||||
duplicates,
|
||||
columns
|
||||
);
|
||||
setTimeStampList(timeStampList);
|
||||
refreshInProgress.current = false;
|
||||
if (shouldRefresh.current) {
|
||||
shouldRefresh.current = false;
|
||||
setTimeout(main, 0);
|
||||
}
|
||||
};
|
||||
main();
|
||||
}, [columns, duplicates]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshList();
|
||||
}, [timeStampList]);
|
||||
|
||||
const getItemSize = (timeStampList) => (index) => {
|
||||
switch (timeStampList[index].itemType) {
|
||||
case ITEM_TYPE.TIME:
|
||||
return DATE_CONTAINER_HEIGHT;
|
||||
case ITEM_TYPE.SIZE_AND_COUNT:
|
||||
return SIZE_AND_COUNT_CONTAINER_HEIGHT;
|
||||
case ITEM_TYPE.FILE:
|
||||
return listItemHeight;
|
||||
default:
|
||||
return timeStampList[index].height;
|
||||
}
|
||||
};
|
||||
|
||||
const generateKey = (index) => {
|
||||
switch (timeStampList[index].itemType) {
|
||||
case ITEM_TYPE.FILE:
|
||||
return `${timeStampList[index].items[0].id}-${
|
||||
timeStampList[index].items.slice(-1)[0].id
|
||||
}`;
|
||||
default:
|
||||
return `${timeStampList[index].id}-${index}`;
|
||||
}
|
||||
};
|
||||
|
||||
const renderListItem = (
|
||||
listItem: TimeStampListItem,
|
||||
isScrolling: boolean
|
||||
) => {
|
||||
switch (listItem.itemType) {
|
||||
case ITEM_TYPE.SIZE_AND_COUNT:
|
||||
return (
|
||||
<SizeAndCountContainer span={columns}>
|
||||
{listItem.fileCount} {t('FILES')},{' '}
|
||||
{convertBytesToHumanReadable(listItem.fileSize || 0)}{' '}
|
||||
{t('EACH')}
|
||||
</SizeAndCountContainer>
|
||||
);
|
||||
case ITEM_TYPE.FILE: {
|
||||
const ret = listItem.items.map((item, idx) =>
|
||||
getThumbnail(
|
||||
item,
|
||||
listItem.itemStartIndex + idx,
|
||||
isScrolling
|
||||
)
|
||||
);
|
||||
if (listItem.groups) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < listItem.groups.length - 1; i++) {
|
||||
sum = sum + listItem.groups[i];
|
||||
ret.splice(
|
||||
sum,
|
||||
0,
|
||||
<div key={`${listItem.items[0].id}-gap-${i}`} />
|
||||
);
|
||||
sum += 1;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
default:
|
||||
return listItem.item;
|
||||
}
|
||||
};
|
||||
|
||||
if (!timeStampList?.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const itemData = createItemData(
|
||||
timeStampList,
|
||||
columns,
|
||||
shrinkRatio,
|
||||
renderListItem
|
||||
);
|
||||
|
||||
return (
|
||||
<List
|
||||
key={`${activeCollectionID}`}
|
||||
itemData={itemData}
|
||||
ref={listRef}
|
||||
itemSize={getItemSize(timeStampList)}
|
||||
height={height}
|
||||
width={width}
|
||||
itemCount={timeStampList.length}
|
||||
itemKey={generateKey}
|
||||
overscanCount={3}
|
||||
useIsScrolling>
|
||||
{PhotoListRow}
|
||||
</List>
|
||||
);
|
||||
}
|
|
@ -19,14 +19,12 @@ import {
|
|||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import { ENTE_WEBSITE_LINK } from '@ente/shared/constants/urls';
|
||||
import { convertBytesToHumanReadable } from '@ente/shared/utils/size';
|
||||
import { DeduplicateContext } from 'pages/deduplicate';
|
||||
import { FlexWrapper } from '@ente/shared/components/Container';
|
||||
import { Typography } from '@mui/material';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { formatDate } from '@ente/shared/time/format';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { t } from 'i18next';
|
||||
import { areFilesWithFileHashSame, hasFileHash } from 'utils/upload';
|
||||
import memoize from 'memoize-one';
|
||||
|
||||
const A_DAY = 24 * 60 * 60 * 1000;
|
||||
|
@ -261,7 +259,6 @@ export function PhotoList({
|
|||
const publicCollectionGalleryContext = useContext(
|
||||
PublicCollectionGalleryContext
|
||||
);
|
||||
const deduplicateContext = useContext(DeduplicateContext);
|
||||
|
||||
const [timeStampList, setTimeStampList] = useState<TimeStampListItem[]>([]);
|
||||
const refreshInProgress = useRef(false);
|
||||
|
@ -306,9 +303,6 @@ export function PhotoList({
|
|||
}
|
||||
if (galleryContext.isClipSearchResult) {
|
||||
noGrouping(timeStampList);
|
||||
} else if (deduplicateContext.isOnDeduplicatePage) {
|
||||
skipMerge = true;
|
||||
groupByFileSize(timeStampList);
|
||||
} else {
|
||||
groupByTime(timeStampList);
|
||||
}
|
||||
|
@ -345,9 +339,6 @@ export function PhotoList({
|
|||
width,
|
||||
height,
|
||||
displayFiles,
|
||||
deduplicateContext.isOnDeduplicatePage,
|
||||
deduplicateContext.fileSizeMap,
|
||||
deduplicateContext.clubSameTimeFilesOnly,
|
||||
galleryContext.photoListHeader,
|
||||
publicCollectionGalleryContext.photoListHeader,
|
||||
galleryContext.isClipSearchResult,
|
||||
|
@ -420,67 +411,6 @@ export function PhotoList({
|
|||
refreshList();
|
||||
}, [timeStampList]);
|
||||
|
||||
const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
|
||||
let index = 0;
|
||||
while (index < displayFiles.length) {
|
||||
const firstFile = displayFiles[index];
|
||||
const firstFileSize = deduplicateContext.fileSizeMap.get(
|
||||
firstFile.id
|
||||
);
|
||||
const firstFileCreationTime = firstFile.metadata.creationTime;
|
||||
let lastFileIndex = index;
|
||||
|
||||
while (lastFileIndex < displayFiles.length) {
|
||||
const lastFile = displayFiles[lastFileIndex];
|
||||
|
||||
const lastFileSize = deduplicateContext.fileSizeMap.get(
|
||||
lastFile.id
|
||||
);
|
||||
if (lastFileSize !== firstFileSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
const lastFileCreationTime = lastFile.metadata.creationTime;
|
||||
if (
|
||||
deduplicateContext.clubSameTimeFilesOnly &&
|
||||
lastFileCreationTime !== firstFileCreationTime
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
const eitherFileHasFileHash =
|
||||
hasFileHash(lastFile.metadata) ||
|
||||
hasFileHash(firstFile.metadata);
|
||||
if (
|
||||
eitherFileHasFileHash &&
|
||||
!areFilesWithFileHashSame(
|
||||
lastFile.metadata,
|
||||
firstFile.metadata
|
||||
)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
lastFileIndex++;
|
||||
}
|
||||
lastFileIndex--;
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.SIZE_AND_COUNT,
|
||||
fileSize: firstFileSize,
|
||||
fileCount: lastFileIndex - index + 1,
|
||||
});
|
||||
|
||||
while (index <= lastFileIndex) {
|
||||
const tileSize = Math.min(columns, lastFileIndex - index + 1);
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.FILE,
|
||||
items: displayFiles.slice(index, index + tileSize),
|
||||
itemStartIndex: index,
|
||||
});
|
||||
index += tileSize;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const groupByTime = (timeStampList: TimeStampListItem[]) => {
|
||||
let listItemIndex = 0;
|
||||
let currentDate;
|
|
@ -137,6 +137,8 @@ function PhotoViewer(props: Iprops) {
|
|||
|
||||
const [showEditButton, setShowEditButton] = useState(false);
|
||||
|
||||
const [showZoomButton, setShowZoomButton] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (publicCollectionGalleryContext.accessedThroughSharedURL) {
|
||||
publicCollectionDownloadManager.setProgressUpdater(
|
||||
|
@ -338,6 +340,10 @@ function PhotoViewer(props: Iprops) {
|
|||
);
|
||||
}
|
||||
|
||||
function updateShowZoomButton(file: EnteFile) {
|
||||
setShowZoomButton(file.metadata.fileType === FILE_TYPE.IMAGE);
|
||||
}
|
||||
|
||||
const openPhotoSwipe = () => {
|
||||
const { items, currentIndex } = props;
|
||||
const options = {
|
||||
|
@ -411,6 +417,7 @@ function PhotoViewer(props: Iprops) {
|
|||
updateShowConvertBtn(currItem);
|
||||
updateIsSourceLoaded(currItem);
|
||||
updateShowEditButton(currItem);
|
||||
updateShowZoomButton(currItem);
|
||||
});
|
||||
photoSwipe.listen('resize', () => {
|
||||
if (!photoSwipe?.currItem) return;
|
||||
|
@ -766,12 +773,14 @@ function PhotoViewer(props: Iprops) {
|
|||
<DeleteIcon />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="pswp__button pswp__button--custom"
|
||||
onClick={toggleZoomInAndOut}
|
||||
title={t('ZOOM_IN_OUT')}>
|
||||
<ZoomInOutlinedIcon />
|
||||
</button>
|
||||
{showZoomButton && (
|
||||
<button
|
||||
className="pswp__button pswp__button--custom"
|
||||
onClick={toggleZoomInAndOut}
|
||||
title={t('ZOOM_IN_OUT')}>
|
||||
<ZoomInOutlinedIcon />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="pswp__button pswp__button--custom"
|
||||
onClick={() => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { IconButton } from '@mui/material';
|
||||
import debounce from 'debounce-promise';
|
||||
import pDebounce from 'p-debounce';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import React, {
|
||||
useCallback,
|
||||
|
@ -83,7 +83,7 @@ export default function SearchInput(props: Iprops) {
|
|||
}
|
||||
};
|
||||
|
||||
const getOptions = debounce(
|
||||
const getOptions = pDebounce(
|
||||
getAutoCompleteSuggestions(props.files, props.collections),
|
||||
250
|
||||
);
|
||||
|
|
|
@ -64,8 +64,6 @@ export default function UtilitySection({ closeSidebar }) {
|
|||
|
||||
const redirectToDeduplicatePage = () => router.push(PAGES.DEDUPLICATE);
|
||||
|
||||
const redirectToAuthenticatorPage = () => router.push(PAGES.AUTH);
|
||||
|
||||
const somethingWentWrong = () =>
|
||||
setDialogMessage({
|
||||
title: t('ERROR'),
|
||||
|
@ -132,13 +130,6 @@ export default function UtilitySection({ closeSidebar }) {
|
|||
label={t('DEDUPLICATE_FILES')}
|
||||
/>
|
||||
|
||||
{isInternalUser() && (
|
||||
<EnteMenuItem
|
||||
variant="secondary"
|
||||
onClick={redirectToAuthenticatorPage}
|
||||
label={t('AUTHENTICATOR_SECTION')}
|
||||
/>
|
||||
)}
|
||||
<EnteMenuItem
|
||||
variant="secondary"
|
||||
onClick={openPreferencesOptions}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { UPLOAD_STRATEGY } from 'constants/upload';
|
|||
import { getImportSuggestion } from 'utils/upload';
|
||||
import ElectronAPIs from '@ente/shared/electron';
|
||||
import { PICKED_UPLOAD_TYPE } from 'constants/upload';
|
||||
import isElectron from 'is-electron';
|
||||
|
||||
interface Iprops {
|
||||
open: boolean;
|
||||
|
@ -25,6 +26,9 @@ export default function WatchFolder({ open, onClose }: Iprops) {
|
|||
const appContext = useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron()) {
|
||||
return;
|
||||
}
|
||||
setMappings(watchFolderService.getWatchMappings());
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -7,12 +7,14 @@ import { PeriodToggler } from '../periodToggler';
|
|||
import Plans from '../plans';
|
||||
import { hasAddOnBonus } from 'utils/billing';
|
||||
import { BFAddOnRow } from '../plans/BfAddOnRow';
|
||||
import { ManageSubscription } from '../manageSubscription';
|
||||
|
||||
export default function FreeSubscriptionPlanSelectorCard({
|
||||
plans,
|
||||
subscription,
|
||||
bonusData,
|
||||
closeModal,
|
||||
setLoading,
|
||||
planPeriod,
|
||||
togglePeriod,
|
||||
onPlanSelect,
|
||||
|
@ -48,6 +50,14 @@ export default function FreeSubscriptionPlanSelectorCard({
|
|||
closeModal={closeModal}
|
||||
/>
|
||||
)}
|
||||
{hasAddOnBonus(bonusData) && (
|
||||
<ManageSubscription
|
||||
subscription={subscription}
|
||||
bonusData={bonusData}
|
||||
closeModal={closeModal}
|
||||
setLoading={setLoading}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</>
|
||||
|
|
|
@ -191,6 +191,7 @@ function PlanSelectorCard(props: Props) {
|
|||
subscription={subscription}
|
||||
bonusData={bonusData}
|
||||
closeModal={props.closeModal}
|
||||
setLoading={props.setLoading}
|
||||
planPeriod={planPeriod}
|
||||
togglePeriod={togglePeriod}
|
||||
onPlanSelect={onPlanSelect}
|
||||
|
|
|
@ -4,12 +4,8 @@ import PhotoFrame from 'components/PhotoFrame';
|
|||
import { ALL_SECTION } from 'constants/collection';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
getDuplicateFiles,
|
||||
clubDuplicatesByTime,
|
||||
} from 'services/deduplicationService';
|
||||
import { syncFiles, trashFiles } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { getDuplicates, Duplicate } from 'services/deduplicationService';
|
||||
import { getLocalFiles, trashFiles } from 'services/fileService';
|
||||
import { SelectedState } from 'types/gallery';
|
||||
|
||||
import { ApiError } from '@ente/shared/error';
|
||||
|
@ -24,7 +20,7 @@ import { PHOTOS_PAGES as PAGES } from '@ente/shared/constants/pages';
|
|||
import router from 'next/router';
|
||||
import { getKey, SESSION_KEYS } from '@ente/shared/storage/sessionStorage';
|
||||
import { styled } from '@mui/material';
|
||||
import { getLatestCollections } from 'services/collectionService';
|
||||
import { getLocalCollections } from 'services/collectionService';
|
||||
import EnteSpinner from '@ente/shared/components/EnteSpinner';
|
||||
import { VerticallyCentered } from '@ente/shared/components/Container';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
@ -43,9 +39,7 @@ export const Info = styled('div')`
|
|||
export default function Deduplicate() {
|
||||
const { setDialogMessage, startLoading, finishLoading, showNavBar } =
|
||||
useContext(AppContext);
|
||||
const [duplicateFiles, setDuplicateFiles] = useState<EnteFile[]>(null);
|
||||
const [clubSameTimeFilesOnly, setClubSameTimeFilesOnly] = useState(false);
|
||||
const [fileSizeMap, setFileSizeMap] = useState(new Map<number, number>());
|
||||
const [duplicates, setDuplicates] = useState<Duplicate[]>(null);
|
||||
const [collectionNameMap, setCollectionNameMap] = useState(
|
||||
new Map<number, string>()
|
||||
);
|
||||
|
@ -69,31 +63,22 @@ export default function Deduplicate() {
|
|||
|
||||
useEffect(() => {
|
||||
syncWithRemote();
|
||||
}, [clubSameTimeFilesOnly]);
|
||||
|
||||
const fileToCollectionsMap = useMemoSingleThreaded(() => {
|
||||
return constructFileToCollectionMap(duplicateFiles);
|
||||
}, [duplicateFiles]);
|
||||
}, []);
|
||||
|
||||
const syncWithRemote = async () => {
|
||||
startLoading();
|
||||
const collections = await getLatestCollections();
|
||||
const collections = await getLocalCollections();
|
||||
const collectionNameMap = new Map<number, string>();
|
||||
for (const collection of collections) {
|
||||
collectionNameMap.set(collection.id, collection.name);
|
||||
}
|
||||
setCollectionNameMap(collectionNameMap);
|
||||
const files = await syncFiles('normal', collections, () => null);
|
||||
let duplicates = await getDuplicateFiles(files, collectionNameMap);
|
||||
if (clubSameTimeFilesOnly) {
|
||||
duplicates = clubDuplicatesByTime(duplicates);
|
||||
}
|
||||
const files = await getLocalFiles();
|
||||
const duplicateFiles = await getDuplicates(files, collectionNameMap);
|
||||
const currFileSizeMap = new Map<number, number>();
|
||||
let allDuplicateFiles: EnteFile[] = [];
|
||||
let toSelectFileIDs: number[] = [];
|
||||
let count = 0;
|
||||
for (const dupe of duplicates) {
|
||||
allDuplicateFiles = [...allDuplicateFiles, ...dupe.files];
|
||||
for (const dupe of duplicateFiles) {
|
||||
// select all except first file
|
||||
toSelectFileIDs = [
|
||||
...toSelectFileIDs,
|
||||
|
@ -105,8 +90,7 @@ export default function Deduplicate() {
|
|||
currFileSizeMap.set(file.id, dupe.size);
|
||||
}
|
||||
}
|
||||
setDuplicateFiles(allDuplicateFiles);
|
||||
setFileSizeMap(currFileSizeMap);
|
||||
setDuplicates(duplicateFiles);
|
||||
const selectedFiles = {
|
||||
count: count,
|
||||
ownCount: count,
|
||||
|
@ -119,6 +103,16 @@ export default function Deduplicate() {
|
|||
finishLoading();
|
||||
};
|
||||
|
||||
const duplicateFiles = useMemoSingleThreaded(() => {
|
||||
return (duplicates ?? []).reduce((acc, dupe) => {
|
||||
return [...acc, ...dupe.files];
|
||||
}, []);
|
||||
}, [duplicates]);
|
||||
|
||||
const fileToCollectionsMap = useMemoSingleThreaded(() => {
|
||||
return constructFileToCollectionMap(duplicateFiles);
|
||||
}, [duplicateFiles]);
|
||||
|
||||
const deleteFileHelper = async () => {
|
||||
try {
|
||||
startLoading();
|
||||
|
@ -153,7 +147,7 @@ export default function Deduplicate() {
|
|||
setSelected({ count: 0, collectionID: 0, ownCount: 0 });
|
||||
};
|
||||
|
||||
if (!duplicateFiles) {
|
||||
if (!duplicates) {
|
||||
return (
|
||||
<VerticallyCentered>
|
||||
<EnteSpinner />
|
||||
|
@ -166,19 +160,10 @@ export default function Deduplicate() {
|
|||
value={{
|
||||
...DefaultDeduplicateContext,
|
||||
collectionNameMap,
|
||||
clubSameTimeFilesOnly,
|
||||
setClubSameTimeFilesOnly,
|
||||
fileSizeMap,
|
||||
isOnDeduplicatePage: true,
|
||||
}}>
|
||||
{duplicateFiles.length > 0 && (
|
||||
<Info>
|
||||
{t('DEDUPLICATE_BASED_ON', {
|
||||
context: clubSameTimeFilesOnly
|
||||
? 'SIZE_AND_CAPTURE_TIME'
|
||||
: 'SIZE',
|
||||
})}
|
||||
</Info>
|
||||
<Info>{t('DEDUPLICATE_BASED_ON_SIZE')}</Info>
|
||||
)}
|
||||
{duplicateFiles.length === 0 ? (
|
||||
<VerticallyCentered>
|
||||
|
@ -188,7 +173,9 @@ export default function Deduplicate() {
|
|||
</VerticallyCentered>
|
||||
) : (
|
||||
<PhotoFrame
|
||||
page={PAGES.DEDUPLICATE}
|
||||
files={duplicateFiles}
|
||||
duplicates={duplicates}
|
||||
syncWithRemote={syncWithRemote}
|
||||
setSelected={setSelected}
|
||||
selected={selected}
|
||||
|
|
|
@ -1096,6 +1096,7 @@ export default function Gallery() {
|
|||
<GalleryEmptyState openUploader={openUploader} />
|
||||
) : (
|
||||
<PhotoFrame
|
||||
page={PAGES.GALLERY}
|
||||
files={filteredData}
|
||||
syncWithRemote={syncWithRemote}
|
||||
favItemIds={favItemIds}
|
||||
|
|
|
@ -132,7 +132,11 @@ export default function LandingPage() {
|
|||
const user = getData(LS_KEYS.USER);
|
||||
let key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
if (!key && isElectron()) {
|
||||
key = await ElectronAPIs.getEncryptionKey();
|
||||
try {
|
||||
key = await ElectronAPIs.getEncryptionKey();
|
||||
} catch (e) {
|
||||
logError(e, 'getEncryptionKey failed');
|
||||
}
|
||||
if (key) {
|
||||
await saveKeyInSessionStore(
|
||||
SESSION_KEYS.ENCRYPTION_KEY,
|
||||
|
|
|
@ -463,6 +463,7 @@ export default function PublicCollectionGallery() {
|
|||
openUploader={openUploader}
|
||||
/>
|
||||
<PhotoFrame
|
||||
page={PAGES.SHARED_ALBUMS}
|
||||
files={publicFiles}
|
||||
syncWithRemote={syncWithRemote}
|
||||
setSelected={() => null}
|
||||
|
|
|
@ -18,6 +18,7 @@ import { FILE_TYPE } from 'constants/file';
|
|||
import ComlinkCryptoWorker from '@ente/shared/crypto';
|
||||
import { Embedding, Model } from 'types/embedding';
|
||||
import { getToken } from '@ente/shared/storage/localStorage/helpers';
|
||||
import isElectron from 'is-electron';
|
||||
|
||||
const CLIP_EMBEDDING_LENGTH = 512;
|
||||
|
||||
|
@ -48,7 +49,7 @@ class ClipServiceImpl {
|
|||
}
|
||||
|
||||
isPlatformSupported = () => {
|
||||
return !this.unsupportedPlatform;
|
||||
return isElectron() && !this.unsupportedPlatform;
|
||||
};
|
||||
|
||||
setupOnFileUploadListener = async () => {
|
||||
|
@ -145,6 +146,9 @@ class ClipServiceImpl {
|
|||
try {
|
||||
return ElectronAPIs.computeTextEmbedding(text);
|
||||
} catch (e) {
|
||||
if (e?.message?.includes(CustomError.UNSUPPORTED_PLATFORM)) {
|
||||
this.unsupportedPlatform = true;
|
||||
}
|
||||
logError(e, 'failed to compute text embedding');
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -16,12 +16,12 @@ interface DuplicatesResponse {
|
|||
}>;
|
||||
}
|
||||
|
||||
interface DuplicateFiles {
|
||||
export interface Duplicate {
|
||||
files: EnteFile[];
|
||||
size: number;
|
||||
}
|
||||
|
||||
export async function getDuplicateFiles(
|
||||
export async function getDuplicates(
|
||||
files: EnteFile[],
|
||||
collectionNameMap: Map<number, string>
|
||||
) {
|
||||
|
@ -33,7 +33,7 @@ export async function getDuplicateFiles(
|
|||
fileMap.set(file.id, file);
|
||||
}
|
||||
|
||||
let result: DuplicateFiles[] = [];
|
||||
let result: Duplicate[] = [];
|
||||
|
||||
for (const dupe of dupes) {
|
||||
let duplicateFiles: EnteFile[] = [];
|
||||
|
@ -64,8 +64,8 @@ export async function getDuplicateFiles(
|
|||
}
|
||||
}
|
||||
|
||||
function getDupesGroupedBySameFileHashes(dupe: DuplicateFiles) {
|
||||
const result: DuplicateFiles[] = [];
|
||||
function getDupesGroupedBySameFileHashes(dupe: Duplicate) {
|
||||
const result: Duplicate[] = [];
|
||||
|
||||
const fileWithHashes: EnteFile[] = [];
|
||||
const fileWithoutHashes: EnteFile[] = [];
|
||||
|
@ -95,8 +95,8 @@ function getDupesGroupedBySameFileHashes(dupe: DuplicateFiles) {
|
|||
return result;
|
||||
}
|
||||
|
||||
function groupDupesByFileHashes(dupe: DuplicateFiles) {
|
||||
const result: DuplicateFiles[] = [];
|
||||
function groupDupesByFileHashes(dupe: Duplicate) {
|
||||
const result: Duplicate[] = [];
|
||||
|
||||
const filesSortedByFileHash = dupe.files
|
||||
.map((file) => {
|
||||
|
@ -141,51 +141,6 @@ function groupDupesByFileHashes(dupe: DuplicateFiles) {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function clubDuplicatesByTime(dupes: DuplicateFiles[]) {
|
||||
const result: DuplicateFiles[] = [];
|
||||
for (const dupe of dupes) {
|
||||
let files: EnteFile[] = [];
|
||||
const creationTimeCounter = new Map<number, number>();
|
||||
|
||||
let mostFreqCreationTime = 0;
|
||||
let mostFreqCreationTimeCount = 0;
|
||||
for (const file of dupe.files) {
|
||||
const creationTime = file.metadata.creationTime;
|
||||
if (creationTimeCounter.has(creationTime)) {
|
||||
creationTimeCounter.set(
|
||||
creationTime,
|
||||
creationTimeCounter.get(creationTime) + 1
|
||||
);
|
||||
} else {
|
||||
creationTimeCounter.set(creationTime, 1);
|
||||
}
|
||||
if (
|
||||
creationTimeCounter.get(creationTime) >
|
||||
mostFreqCreationTimeCount
|
||||
) {
|
||||
mostFreqCreationTime = creationTime;
|
||||
mostFreqCreationTimeCount =
|
||||
creationTimeCounter.get(creationTime);
|
||||
}
|
||||
|
||||
files.push(file);
|
||||
}
|
||||
|
||||
files = files.filter((file) => {
|
||||
return file.metadata.creationTime === mostFreqCreationTime;
|
||||
});
|
||||
|
||||
if (files.length > 1) {
|
||||
result.push({
|
||||
files,
|
||||
size: dupe.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function fetchDuplicateFileIDs() {
|
||||
try {
|
||||
const response = await HTTPService.get(
|
||||
|
|
|
@ -4,16 +4,18 @@ import { logError } from '@ente/shared/sentry';
|
|||
import { ElectronFile } from 'types/upload';
|
||||
import { CustomError } from '@ente/shared/error';
|
||||
import { convertBytesToHumanReadable } from '@ente/shared/utils/size';
|
||||
import { WorkerSafeElectronService } from '@ente/shared/electron/service';
|
||||
|
||||
class ElectronImageProcessorService {
|
||||
async convertToJPEG(fileBlob: Blob, filename: string): Promise<Blob> {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const inputFileData = new Uint8Array(await fileBlob.arrayBuffer());
|
||||
const convertedFileData = await ElectronAPIs.convertToJPEG(
|
||||
inputFileData,
|
||||
filename
|
||||
);
|
||||
const convertedFileData =
|
||||
await WorkerSafeElectronService.convertToJPEG(
|
||||
inputFileData,
|
||||
filename
|
||||
);
|
||||
addLogLine(
|
||||
`originalFileSize:${convertBytesToHumanReadable(
|
||||
fileBlob?.size
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import debounce from 'debounce-promise';
|
||||
import debounce from 'debounce';
|
||||
import PQueue from 'p-queue';
|
||||
import { eventBus, Events } from '@ente/shared/events';
|
||||
import { EnteFile } from 'types/file';
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
computeClipMatchScore,
|
||||
getLocalClipImageEmbeddings,
|
||||
} from './clipService';
|
||||
import { CustomError } from '@ente/shared/error';
|
||||
|
||||
const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
|
||||
|
||||
|
@ -290,15 +291,23 @@ async function getThingSuggestion(searchPhrase: string): Promise<Suggestion[]> {
|
|||
}
|
||||
|
||||
async function getClipSuggestion(searchPhrase: string): Promise<Suggestion> {
|
||||
if (!ClipService.isPlatformSupported()) {
|
||||
try {
|
||||
if (!ClipService.isPlatformSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const clipResults = await searchClip(searchPhrase);
|
||||
return {
|
||||
type: SuggestionType.CLIP,
|
||||
value: clipResults,
|
||||
label: searchPhrase,
|
||||
};
|
||||
} catch (e) {
|
||||
if (!e.message?.includes(CustomError.MODEL_DOWNLOAD_PENDING)) {
|
||||
logError(e, 'getClipSuggestion failed');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const clipResults = await searchClip(searchPhrase);
|
||||
return {
|
||||
type: SuggestionType.CLIP,
|
||||
value: clipResults,
|
||||
label: searchPhrase,
|
||||
};
|
||||
}
|
||||
|
||||
function searchCollection(
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
WatchMapping,
|
||||
WatchMappingSyncedFile,
|
||||
} from 'types/watchFolder';
|
||||
import debounce from 'debounce-promise';
|
||||
import debounce from 'debounce';
|
||||
import {
|
||||
diskFileAddedCallback,
|
||||
diskFileRemovedCallback,
|
||||
|
@ -37,6 +37,11 @@ class watchFolderService {
|
|||
private setCollectionName: (collectionName: string) => void;
|
||||
private syncWithRemote: () => void;
|
||||
private setWatchFolderServiceIsRunning: (isRunning: boolean) => void;
|
||||
private debouncedRunNextEvent: () => void;
|
||||
|
||||
constructor() {
|
||||
this.debouncedRunNextEvent = debounce(() => this.runNextEvent(), 1000);
|
||||
}
|
||||
|
||||
isUploadRunning() {
|
||||
return this.uploadRunning;
|
||||
|
@ -160,7 +165,7 @@ class watchFolderService {
|
|||
|
||||
pushEvent(event: EventQueueItem) {
|
||||
this.eventQueue.push(event);
|
||||
debounce(this.runNextEvent.bind(this), 300)();
|
||||
this.debouncedRunNextEvent();
|
||||
}
|
||||
|
||||
async pushTrashedDir(path: string) {
|
||||
|
@ -255,7 +260,7 @@ class watchFolderService {
|
|||
} else {
|
||||
await this.processTrashEvent();
|
||||
this.setIsEventRunning(false);
|
||||
this.runNextEvent();
|
||||
setTimeout(() => this.runNextEvent(), 0);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'runNextEvent failed');
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
export type DeduplicateContextType = {
|
||||
clubSameTimeFilesOnly: boolean;
|
||||
setClubSameTimeFilesOnly: (clubSameTimeFilesOnly: boolean) => void;
|
||||
fileSizeMap: Map<number, number>;
|
||||
isOnDeduplicatePage: boolean;
|
||||
collectionNameMap: Map<number, string>;
|
||||
};
|
||||
|
|
|
@ -140,11 +140,12 @@ export function hasMobileSubscription(subscription: Subscription) {
|
|||
}
|
||||
|
||||
export function hasExceededStorageQuota(userDetails: UserDetails) {
|
||||
const bonusStorage = userDetails.storageBonus ?? 0;
|
||||
if (isPartOfFamily(userDetails.familyData)) {
|
||||
const usage = getTotalFamilyUsage(userDetails.familyData);
|
||||
return usage > userDetails.familyData.storage;
|
||||
return usage > (userDetails.familyData.storage + bonusStorage);
|
||||
} else {
|
||||
return userDetails.usage > userDetails.subscription.storage;
|
||||
return userDetails.usage > (userDetails.subscription.storage + bonusStorage);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,13 @@ const DATE_TIME_PARSING_TEST_FILE_NAMES = [
|
|||
},
|
||||
];
|
||||
|
||||
const DATE_TIME_PARSING_TEST_FILE_NAMES_MUST_FAIL = [
|
||||
'Snapchat-431959199.mp4.',
|
||||
'Snapchat-400000000.mp4',
|
||||
'Snapchat-900000000.mp4',
|
||||
'Snapchat-100-10-20-19-15-12',
|
||||
];
|
||||
|
||||
const FILE_NAME_TO_JSON_NAME = [
|
||||
{
|
||||
filename: 'IMG20210211125718-edited.jpg',
|
||||
|
@ -387,6 +394,16 @@ function parseDateTimeFromFileNameTest() {
|
|||
}
|
||||
}
|
||||
);
|
||||
DATE_TIME_PARSING_TEST_FILE_NAMES_MUST_FAIL.forEach((fileName) => {
|
||||
const dateTime = tryToParseDateTime(fileName);
|
||||
if (dateTime) {
|
||||
throw Error(
|
||||
`parseDateTimeFromFileNameTest failed ❌ ,
|
||||
for ${fileName}
|
||||
expected: null got: ${dateTime}`
|
||||
);
|
||||
}
|
||||
});
|
||||
console.log('parseDateTimeFromFileNameTest passed ✅');
|
||||
}
|
||||
|
||||
|
|
|
@ -25,8 +25,14 @@ export const sendOtt = (appName: APPS, email: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const verifyOtt = (email: string, ott: string) =>
|
||||
HTTPService.post(`${ENDPOINT}/users/verify-email`, { email, ott });
|
||||
export const verifyOtt = (email: string, ott: string, referral: string) => {
|
||||
const cleanedReferral = `web:${referral?.trim() || ''}`;
|
||||
return HTTPService.post(`${ENDPOINT}/users/verify-email`, {
|
||||
email,
|
||||
ott,
|
||||
source: cleanedReferral,
|
||||
});
|
||||
};
|
||||
|
||||
export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
|
||||
HTTPService.put(
|
||||
|
|
|
@ -11,7 +11,10 @@ import {
|
|||
import { isWeakPassword } from '@ente/accounts/utils';
|
||||
import { generateKeyAndSRPAttributes } from '@ente/accounts/utils/srp';
|
||||
|
||||
import { setJustSignedUp } from '@ente/shared/storage/localStorage/helpers';
|
||||
import {
|
||||
setJustSignedUp,
|
||||
setLocalReferralSource,
|
||||
} from '@ente/shared/storage/localStorage/helpers';
|
||||
import { SESSION_KEYS } from '@ente/shared/storage/sessionStorage';
|
||||
import { PAGES } from '@ente/accounts/constants/pages';
|
||||
import {
|
||||
|
@ -19,8 +22,11 @@ import {
|
|||
Checkbox,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
Link,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import FormPaperTitle from '@ente/shared/components/Form/FormPaper/Title';
|
||||
|
@ -34,11 +40,13 @@ import ShowHidePassword from '@ente/shared/components/Form/ShowHidePassword';
|
|||
import { APPS } from '@ente/shared/apps/constants';
|
||||
import { NextRouter } from 'next/router';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import InfoOutlined from '@mui/icons-material/InfoOutlined';
|
||||
|
||||
interface FormValues {
|
||||
email: string;
|
||||
passphrase: string;
|
||||
confirm: string;
|
||||
referral: string;
|
||||
}
|
||||
|
||||
interface SignUpProps {
|
||||
|
@ -63,7 +71,7 @@ export default function SignUp({ router, appName, login }: SignUpProps) {
|
|||
};
|
||||
|
||||
const registerUser = async (
|
||||
{ email, passphrase, confirm }: FormValues,
|
||||
{ email, passphrase, confirm, referral }: FormValues,
|
||||
{ setFieldError }: FormikHelpers<FormValues>
|
||||
) => {
|
||||
try {
|
||||
|
@ -74,6 +82,7 @@ export default function SignUp({ router, appName, login }: SignUpProps) {
|
|||
setLoading(true);
|
||||
try {
|
||||
setData(LS_KEYS.USER, { email });
|
||||
setLocalReferralSource(referral);
|
||||
await sendOtt(appName, email);
|
||||
} catch (e) {
|
||||
setFieldError('confirm', `${t('UNKNOWN_ERROR')} ${e.message}`);
|
||||
|
@ -115,6 +124,7 @@ export default function SignUp({ router, appName, login }: SignUpProps) {
|
|||
email: '',
|
||||
passphrase: '',
|
||||
confirm: '',
|
||||
referral: '',
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
email: Yup.string()
|
||||
|
@ -192,12 +202,47 @@ export default function SignUp({ router, appName, login }: SignUpProps) {
|
|||
<PasswordStrengthHint
|
||||
password={values.passphrase}
|
||||
/>
|
||||
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography
|
||||
textAlign={'left'}
|
||||
color="text.secondary"
|
||||
mt={'24px'}>
|
||||
{t('REFERRAL_CODE_HINT')}
|
||||
</Typography>
|
||||
<TextField
|
||||
hiddenLabel
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Tooltip
|
||||
title={t('REFERRAL_INFO')}>
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
color="secondary"
|
||||
edge={'end'}>
|
||||
<InfoOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
fullWidth
|
||||
name="referral"
|
||||
type="text"
|
||||
value={values.referral}
|
||||
onChange={handleChange('referral')}
|
||||
error={Boolean(errors.referral)}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
<FormGroup sx={{ width: '100%' }}>
|
||||
<FormControlLabel
|
||||
sx={{
|
||||
color: 'text.muted',
|
||||
ml: 0,
|
||||
mt: 2,
|
||||
mb: 0,
|
||||
}}
|
||||
control={
|
||||
<Checkbox
|
||||
|
@ -234,7 +279,7 @@ export default function SignUp({ router, appName, login }: SignUpProps) {
|
|||
/>
|
||||
</FormGroup>
|
||||
</VerticallyCentered>
|
||||
<Box my={4}>
|
||||
<Box mb={4}>
|
||||
<SubmitButton
|
||||
sx={{ my: 0 }}
|
||||
buttonText={t('CREATE_ACCOUNT')}
|
||||
|
|
|
@ -73,7 +73,11 @@ export default function Credentials({
|
|||
setUser(user);
|
||||
let key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
if (!key && isElectron()) {
|
||||
key = await ElectronAPIs.getEncryptionKey();
|
||||
try {
|
||||
key = await ElectronAPIs.getEncryptionKey();
|
||||
} catch (e) {
|
||||
logError(e, 'getEncryptionKey failed');
|
||||
}
|
||||
if (key) {
|
||||
await saveKeyInSessionStore(
|
||||
SESSION_KEYS.ENCRYPTION_KEY,
|
||||
|
|
|
@ -7,7 +7,10 @@ import { verifyOtt, sendOtt, putAttributes } from '../api/user';
|
|||
import { logoutUser } from '../services/user';
|
||||
import { configureSRP } from '../services/srp';
|
||||
import { clearFiles } from '@ente/shared/storage/localForage/helpers';
|
||||
import { setIsFirstLogin } from '@ente/shared/storage/localStorage/helpers';
|
||||
import {
|
||||
getLocalReferralSource,
|
||||
setIsFirstLogin,
|
||||
} from '@ente/shared/storage/localStorage/helpers';
|
||||
import { clearKeys } from '@ente/shared/storage/sessionStorage';
|
||||
import { PAGES } from '../constants/pages';
|
||||
import { KeyAttributes, User } from '@ente/shared/user/types';
|
||||
|
@ -58,7 +61,8 @@ export default function VerifyPage({ appContext, router, appName }: PageProps) {
|
|||
setFieldError
|
||||
) => {
|
||||
try {
|
||||
const resp = await verifyOtt(email, ott);
|
||||
const referralSource = getLocalReferralSource();
|
||||
const resp = await verifyOtt(email, ott, referralSource);
|
||||
const {
|
||||
keyAttributes,
|
||||
encryptedToken,
|
||||
|
|
|
@ -15,8 +15,6 @@ export enum PHOTOS_PAGES {
|
|||
SHARED_ALBUMS = '/shared-albums',
|
||||
// ML_DEBUG = '/ml-debug',
|
||||
DEDUPLICATE = '/deduplicate',
|
||||
// AUTH page is used to show (auth)enticator codes
|
||||
AUTH = '/auth',
|
||||
}
|
||||
|
||||
export enum AUTH_PAGES {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { setRecoveryKey } from '@ente/accounts/api/user';
|
|||
import { logError } from '@ente/shared/sentry';
|
||||
import isElectron from 'is-electron';
|
||||
import ElectronAPIs from '../electron';
|
||||
import { addLogLine } from '../logging';
|
||||
|
||||
const LOGIN_SUB_KEY_LENGTH = 32;
|
||||
const LOGIN_SUB_KEY_ID = 1;
|
||||
|
@ -104,7 +105,7 @@ export const saveKeyInSessionStore = async (
|
|||
key
|
||||
);
|
||||
setKey(keyType, sessionKeyAttributes);
|
||||
console.log('fromDesktop', fromDesktop);
|
||||
addLogLine('fromDesktop', fromDesktop);
|
||||
if (
|
||||
isElectron() &&
|
||||
!fromDesktop &&
|
||||
|
|
84
packages/shared/electron/service.ts
Normal file
84
packages/shared/electron/service.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import * as Comlink from 'comlink';
|
||||
import { LimitedCache } from '@ente/shared/storage/cacheStorage/types';
|
||||
import {
|
||||
ProxiedWorkerLimitedCache,
|
||||
WorkerSafeElectronClient,
|
||||
} from './worker/client';
|
||||
import { wrap } from 'comlink';
|
||||
import { deserializeToResponse, serializeResponse } from './worker/utils/proxy';
|
||||
import { runningInWorker } from '@ente/shared/platform';
|
||||
import { ElectronAPIsType } from './types';
|
||||
|
||||
export interface LimitedElectronAPIs
|
||||
extends Pick<
|
||||
ElectronAPIsType,
|
||||
| 'openDiskCache'
|
||||
| 'deleteDiskCache'
|
||||
| 'getSentryUserID'
|
||||
| 'convertToJPEG'
|
||||
> {}
|
||||
|
||||
class WorkerSafeElectronServiceImpl implements LimitedElectronAPIs {
|
||||
proxiedElectron:
|
||||
| Comlink.Remote<WorkerSafeElectronClient>
|
||||
| WorkerSafeElectronClient;
|
||||
ready: Promise<any>;
|
||||
|
||||
constructor() {
|
||||
this.ready = this.init();
|
||||
}
|
||||
private async init() {
|
||||
if (runningInWorker()) {
|
||||
const workerSafeElectronClient =
|
||||
wrap<typeof WorkerSafeElectronClient>(self);
|
||||
|
||||
this.proxiedElectron = await new workerSafeElectronClient();
|
||||
} else {
|
||||
this.proxiedElectron = new WorkerSafeElectronClient();
|
||||
}
|
||||
}
|
||||
async openDiskCache(cacheName: string) {
|
||||
await this.ready;
|
||||
const cache = await this.proxiedElectron.openDiskCache(cacheName);
|
||||
return {
|
||||
match: transformMatch(cache.match.bind(cache)),
|
||||
put: transformPut(cache.put.bind(cache)),
|
||||
delete: cache.delete.bind(cache),
|
||||
};
|
||||
}
|
||||
|
||||
async deleteDiskCache(cacheName: string) {
|
||||
await this.ready;
|
||||
return await this.proxiedElectron.deleteDiskCache(cacheName);
|
||||
}
|
||||
|
||||
async getSentryUserID() {
|
||||
await this.ready;
|
||||
return this.proxiedElectron.getSentryUserID();
|
||||
}
|
||||
async convertToJPEG(
|
||||
inputFileData: Uint8Array,
|
||||
filename: string
|
||||
): Promise<Uint8Array> {
|
||||
await this.ready;
|
||||
return this.proxiedElectron.convertToJPEG(inputFileData, filename);
|
||||
}
|
||||
}
|
||||
|
||||
export const WorkerSafeElectronService = new WorkerSafeElectronServiceImpl();
|
||||
|
||||
function transformMatch(
|
||||
fn: ProxiedWorkerLimitedCache['match']
|
||||
): LimitedCache['match'] {
|
||||
return async (key: string) => {
|
||||
return deserializeToResponse(await fn(key));
|
||||
};
|
||||
}
|
||||
|
||||
function transformPut(
|
||||
fn: ProxiedWorkerLimitedCache['put']
|
||||
): LimitedCache['put'] {
|
||||
return async (key: string, data: Response) => {
|
||||
fn(key, await serializeResponse(data));
|
||||
};
|
||||
}
|
61
packages/shared/electron/worker/client.ts
Normal file
61
packages/shared/electron/worker/client.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import * as Comlink from 'comlink';
|
||||
import { LimitedCache } from '@ente/shared/storage/cacheStorage/types';
|
||||
import { serializeResponse, deserializeToResponse } from './utils/proxy';
|
||||
import ElectronAPIs from '@ente/shared/electron';
|
||||
|
||||
export interface ProxiedLimitedElectronAPIs {
|
||||
openDiskCache: (cacheName: string) => Promise<ProxiedWorkerLimitedCache>;
|
||||
deleteDiskCache: (cacheName: string) => Promise<boolean>;
|
||||
getSentryUserID: () => Promise<string>;
|
||||
convertToJPEG: (
|
||||
inputFileData: Uint8Array,
|
||||
filename: string
|
||||
) => Promise<Uint8Array>;
|
||||
}
|
||||
export interface ProxiedWorkerLimitedCache {
|
||||
match: (key: string) => Promise<ArrayBuffer>;
|
||||
put: (key: string, data: ArrayBuffer) => Promise<void>;
|
||||
delete: (key: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export class WorkerSafeElectronClient implements ProxiedLimitedElectronAPIs {
|
||||
async openDiskCache(cacheName: string) {
|
||||
const cache = await ElectronAPIs.openDiskCache(cacheName);
|
||||
return Comlink.proxy({
|
||||
match: Comlink.proxy(transformMatch(cache.match.bind(cache))),
|
||||
put: Comlink.proxy(transformPut(cache.put.bind(cache))),
|
||||
delete: Comlink.proxy(cache.delete.bind(cache)),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteDiskCache(cacheName: string) {
|
||||
return await ElectronAPIs.deleteDiskCache(cacheName);
|
||||
}
|
||||
|
||||
async getSentryUserID() {
|
||||
return await ElectronAPIs.getSentryUserID();
|
||||
}
|
||||
|
||||
async convertToJPEG(
|
||||
inputFileData: Uint8Array,
|
||||
filename: string
|
||||
): Promise<Uint8Array> {
|
||||
return await ElectronAPIs.convertToJPEG(inputFileData, filename);
|
||||
}
|
||||
}
|
||||
|
||||
function transformMatch(
|
||||
fn: LimitedCache['match']
|
||||
): ProxiedWorkerLimitedCache['match'] {
|
||||
return async (key: string) => {
|
||||
return serializeResponse(await fn(key));
|
||||
};
|
||||
}
|
||||
|
||||
function transformPut(
|
||||
fn: LimitedCache['put']
|
||||
): ProxiedWorkerLimitedCache['put'] {
|
||||
return async (key: string, data: ArrayBuffer) => {
|
||||
fn(key, deserializeToResponse(data));
|
||||
};
|
||||
}
|
|
@ -86,6 +86,8 @@ export const CustomError = {
|
|||
ServerError: 'server error',
|
||||
FILE_NOT_FOUND: 'file not found',
|
||||
UNSUPPORTED_PLATFORM: 'Unsupported platform',
|
||||
MODEL_DOWNLOAD_PENDING:
|
||||
'Model download pending, skipping clip search request',
|
||||
};
|
||||
|
||||
export function handleUploadError(error: any): Error {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import ElectronAPIs from '@ente/shared/electron';
|
||||
import { WorkerSafeElectronService } from '@ente/shared/electron/service';
|
||||
import {
|
||||
getLocalSentryUserID,
|
||||
setLocalSentryUserID,
|
||||
|
@ -12,7 +12,7 @@ import { HttpStatusCode } from 'axios';
|
|||
|
||||
export async function getSentryUserID() {
|
||||
if (isElectron()) {
|
||||
return await ElectronAPIs.getSentryUserID();
|
||||
return await WorkerSafeElectronService.getSentryUserID();
|
||||
} else {
|
||||
let anonymizeUserID = getLocalSentryUserID();
|
||||
if (!anonymizeUserID) {
|
||||
|
|
|
@ -1,28 +1,17 @@
|
|||
import { LimitedCacheStorage } from './types';
|
||||
import { runningInElectron, runningInWorker } from '@ente/shared/platform';
|
||||
import { WorkerElectronCacheStorageService } from './workerElectron/service';
|
||||
import ElectronAPIs from '@ente/shared/electron';
|
||||
|
||||
import { runningInElectron } from '@ente/shared/platform';
|
||||
import { WorkerSafeElectronService } from '@ente/shared/electron/service';
|
||||
class cacheStorageFactory {
|
||||
workerElectronCacheStorageServiceInstance: WorkerElectronCacheStorageService;
|
||||
getCacheStorage(): LimitedCacheStorage {
|
||||
if (runningInElectron()) {
|
||||
if (runningInWorker()) {
|
||||
if (!this.workerElectronCacheStorageServiceInstance) {
|
||||
this.workerElectronCacheStorageServiceInstance =
|
||||
new WorkerElectronCacheStorageService();
|
||||
}
|
||||
return this.workerElectronCacheStorageServiceInstance;
|
||||
} else {
|
||||
return {
|
||||
open(cacheName) {
|
||||
return ElectronAPIs.openDiskCache(cacheName);
|
||||
},
|
||||
delete(cacheName) {
|
||||
return ElectronAPIs.deleteDiskCache(cacheName);
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
open(cacheName) {
|
||||
return WorkerSafeElectronService.openDiskCache(cacheName);
|
||||
},
|
||||
delete(cacheName) {
|
||||
return WorkerSafeElectronService.deleteDiskCache(cacheName);
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return transformBrowserCacheStorageToLimitedCacheStorage(caches);
|
||||
}
|
||||
|
|
|
@ -8,13 +8,3 @@ export interface LimitedCache {
|
|||
put: (key: string, data: Response) => Promise<void>;
|
||||
delete: (key: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface ProxiedLimitedCacheStorage {
|
||||
open: (cacheName: string) => Promise<ProxiedWorkerLimitedCache>;
|
||||
delete: (cacheName: string) => Promise<boolean>;
|
||||
}
|
||||
export interface ProxiedWorkerLimitedCache {
|
||||
match: (key: string) => Promise<ArrayBuffer>;
|
||||
put: (key: string, data: ArrayBuffer) => Promise<void>;
|
||||
delete: (key: string) => Promise<boolean>;
|
||||
}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
import * as Comlink from 'comlink';
|
||||
import {
|
||||
LimitedCache,
|
||||
ProxiedLimitedCacheStorage,
|
||||
ProxiedWorkerLimitedCache,
|
||||
} from '@ente/shared/storage/cacheStorage/types';
|
||||
import { serializeResponse, deserializeToResponse } from './utils/proxy';
|
||||
import ElectronAPIs from '@ente/shared/electron';
|
||||
|
||||
export class WorkerElectronCacheStorageClient
|
||||
implements ProxiedLimitedCacheStorage
|
||||
{
|
||||
async open(cacheName: string) {
|
||||
const cache = await ElectronAPIs.openDiskCache(cacheName);
|
||||
return Comlink.proxy({
|
||||
match: Comlink.proxy(transformMatch(cache.match.bind(cache))),
|
||||
put: Comlink.proxy(transformPut(cache.put.bind(cache))),
|
||||
delete: Comlink.proxy(cache.delete.bind(cache)),
|
||||
});
|
||||
}
|
||||
|
||||
async delete(cacheName: string) {
|
||||
return await ElectronAPIs.deleteDiskCache(cacheName);
|
||||
}
|
||||
}
|
||||
|
||||
function transformMatch(
|
||||
fn: LimitedCache['match']
|
||||
): ProxiedWorkerLimitedCache['match'] {
|
||||
return async (key: string) => {
|
||||
return serializeResponse(await fn(key));
|
||||
};
|
||||
}
|
||||
|
||||
function transformPut(
|
||||
fn: LimitedCache['put']
|
||||
): ProxiedWorkerLimitedCache['put'] {
|
||||
return async (key: string, data: ArrayBuffer) => {
|
||||
fn(key, deserializeToResponse(data));
|
||||
};
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
import * as Comlink from 'comlink';
|
||||
import {
|
||||
LimitedCache,
|
||||
LimitedCacheStorage,
|
||||
ProxiedWorkerLimitedCache,
|
||||
} from '../types';
|
||||
import { WorkerElectronCacheStorageClient } from './client';
|
||||
import { wrap } from 'comlink';
|
||||
import { deserializeToResponse, serializeResponse } from './utils/proxy';
|
||||
|
||||
export class WorkerElectronCacheStorageService implements LimitedCacheStorage {
|
||||
proxiedElectronCacheService: Comlink.Remote<WorkerElectronCacheStorageClient>;
|
||||
ready: Promise<any>;
|
||||
|
||||
constructor() {
|
||||
this.ready = this.init();
|
||||
}
|
||||
async init() {
|
||||
const electronCacheStorageProxy =
|
||||
wrap<typeof WorkerElectronCacheStorageClient>(self);
|
||||
|
||||
this.proxiedElectronCacheService =
|
||||
await new electronCacheStorageProxy();
|
||||
}
|
||||
async open(cacheName: string) {
|
||||
await this.ready;
|
||||
const cache = await this.proxiedElectronCacheService.open(cacheName);
|
||||
return {
|
||||
match: transformMatch(cache.match.bind(cache)),
|
||||
put: transformPut(cache.put.bind(cache)),
|
||||
delete: cache.delete.bind(cache),
|
||||
};
|
||||
}
|
||||
|
||||
async delete(cacheName: string) {
|
||||
await this.ready;
|
||||
return await this.proxiedElectronCacheService.delete(cacheName);
|
||||
}
|
||||
}
|
||||
|
||||
function transformMatch(
|
||||
fn: ProxiedWorkerLimitedCache['match']
|
||||
): LimitedCache['match'] {
|
||||
return async (key: string) => {
|
||||
return deserializeToResponse(await fn(key));
|
||||
};
|
||||
}
|
||||
|
||||
function transformPut(
|
||||
fn: ProxiedWorkerLimitedCache['put']
|
||||
): LimitedCache['put'] {
|
||||
return async (key: string, data: Response) => {
|
||||
fn(key, await serializeResponse(data));
|
||||
};
|
||||
}
|
|
@ -57,3 +57,11 @@ export function getLocalSentryUserID() {
|
|||
export function setLocalSentryUserID(id: string) {
|
||||
setData(LS_KEYS.AnonymizedUserID, { id });
|
||||
}
|
||||
|
||||
export function getLocalReferralSource() {
|
||||
return getData(LS_KEYS.REFERRAL_SOURCE)?.source;
|
||||
}
|
||||
|
||||
export function setLocalReferralSource(source: string) {
|
||||
setData(LS_KEYS.REFERRAL_SOURCE, { source });
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export enum LS_KEYS {
|
|||
SRP_ATTRIBUTES = 'srpAttributes',
|
||||
OPT_OUT_OF_CRASH_REPORTS = 'optOutOfCrashReports',
|
||||
CF_PROXY_DISABLED = 'cfProxyDisabled',
|
||||
REFERRAL_SOURCE = 'referralSource',
|
||||
}
|
||||
|
||||
export const setData = (key: LS_KEYS, value: object) => {
|
||||
|
|
|
@ -14,6 +14,8 @@ interface DateComponent<T = number> {
|
|||
second: T;
|
||||
}
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
export function getUnixTimeInMicroSecondsWithDelta(delta: TimeDelta): number {
|
||||
let currentDate = new Date();
|
||||
if (delta?.hours) {
|
||||
|
@ -112,7 +114,8 @@ function getDateComponentsFromSymbolJoinedString(
|
|||
}
|
||||
|
||||
function validateAndGetDateFromComponents(
|
||||
dateComponent: DateComponent<number>
|
||||
dateComponent: DateComponent<number>,
|
||||
options = { minYear: 1990, maxYear: currentYear + 1 }
|
||||
) {
|
||||
let date = getDateFromComponents(dateComponent);
|
||||
if (hasTimeValues(dateComponent) && !isTimePartValid(date, dateComponent)) {
|
||||
|
@ -123,6 +126,12 @@ function validateAndGetDateFromComponents(
|
|||
if (!isDatePartValid(date, dateComponent)) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
date.getFullYear() < options.minYear ||
|
||||
date.getFullYear() > options.maxYear
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { expose, Remote, wrap } from 'comlink';
|
||||
import { WorkerElectronCacheStorageClient } from '@ente/shared/storage/cacheStorage/workerElectron/client';
|
||||
import { WorkerSafeElectronClient } from '@ente/shared/electron/worker/client';
|
||||
import { addLocalLog } from '@ente/shared/logging';
|
||||
|
||||
export class ComlinkWorker<T extends new () => InstanceType<T>> {
|
||||
|
@ -17,7 +17,7 @@ export class ComlinkWorker<T extends new () => InstanceType<T>> {
|
|||
addLocalLog(() => `Initiated ${this.name}`);
|
||||
const comlink = wrap<T>(this.worker);
|
||||
this.remote = new comlink() as Promise<Remote<InstanceType<T>>>;
|
||||
expose(WorkerElectronCacheStorageClient, this.worker);
|
||||
expose(WorkerSafeElectronClient, this.worker);
|
||||
}
|
||||
|
||||
public getName() {
|
||||
|
|
62
yarn.lock
62
yarn.lock
|
@ -744,11 +744,6 @@
|
|||
dependencies:
|
||||
base-x "^3.0.6"
|
||||
|
||||
"@types/debounce-promise@^3.1.3":
|
||||
version "3.1.4"
|
||||
resolved "https://registry.npmjs.org/@types/debounce-promise/-/debounce-promise-3.1.4.tgz"
|
||||
integrity sha512-9SEVY3nsz+uMN2DwDocftB5TAgZe7D0cOzxxRhpotWs6T4QFqRaTXpXbOSzbk31/7iYcfCkJJPwWGzTxyuGhCg==
|
||||
|
||||
"@types/estree@*", "@types/estree@^1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz"
|
||||
|
@ -1622,10 +1617,10 @@ dayjs@^1.10.0:
|
|||
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz"
|
||||
integrity sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==
|
||||
|
||||
debounce-promise@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz"
|
||||
integrity sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==
|
||||
debounce@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/debounce/-/debounce-2.0.0.tgz#b2f914518a1481466f4edaee0b063e4d473ad549"
|
||||
integrity sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==
|
||||
|
||||
debug@4, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
|
||||
version "4.3.4"
|
||||
|
@ -2523,21 +2518,21 @@ hdbscan@0.0.1-alpha.5:
|
|||
dependencies:
|
||||
kd-tree-javascript "^1.0.3"
|
||||
|
||||
heic-convert@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.npmjs.org/heic-convert/-/heic-convert-1.2.4.tgz"
|
||||
integrity sha512-klJHyv+BqbgKiCQvCqI9IKIvweCcohDuDl0Jphearj8+16+v8eff2piVevHqq4dW9TK0r1onTR6PKHP1I4hdbA==
|
||||
heic-convert@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/heic-convert/-/heic-convert-2.0.0.tgz#8250d56247310eb1121ef791a4b9ecf2cc0cc2a0"
|
||||
integrity sha512-Kk2Ue5D7sAhyCmnHPLXQ4tzYH1qINmgppgqFn66x+DmClyL6sdmqGbHJ9cETy0Pxz5Ixz5w5JWJuIv9QqC1oKg==
|
||||
dependencies:
|
||||
heic-decode "^1.1.2"
|
||||
jpeg-js "^0.4.1"
|
||||
pngjs "^3.4.0"
|
||||
heic-decode "^2.0.0"
|
||||
jpeg-js "^0.4.4"
|
||||
pngjs "^6.0.0"
|
||||
|
||||
heic-decode@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/heic-decode/-/heic-decode-1.1.2.tgz"
|
||||
integrity sha512-UF8teegxvzQPdSTcx5frIUhitNDliz/9Pui0JFdIqVRE00spVE33DcCYtZqaLNyd4y5RP/QQWZFIc1YWVKKm2A==
|
||||
heic-decode@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/heic-decode/-/heic-decode-2.0.0.tgz#77ab96ee1a255f0a9952d0e88d584bca30577114"
|
||||
integrity sha512-NU+zsiDvdL+EebyTjrEqjkO2XYI7FgLhQzsbmO8dnnYce3S0PBSDm/ZyI4KpcGPXYEdb5W72vp/AQFuc4F8ASg==
|
||||
dependencies:
|
||||
libheif-js "^1.10.0"
|
||||
libheif-js "^1.17.1"
|
||||
|
||||
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
|
@ -2904,9 +2899,9 @@ isexe@^2.0.0:
|
|||
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
|
||||
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
|
||||
|
||||
jpeg-js@^0.4.1:
|
||||
jpeg-js@^0.4.4:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz"
|
||||
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
|
||||
integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==
|
||||
|
||||
js-sdsl@^4.1.4:
|
||||
|
@ -3006,10 +3001,10 @@ levn@^0.4.1:
|
|||
prelude-ls "^1.2.1"
|
||||
type-check "~0.4.0"
|
||||
|
||||
libheif-js@^1.10.0:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.npmjs.org/libheif-js/-/libheif-js-1.12.0.tgz"
|
||||
integrity sha512-hDs6xQ7028VOwAFwEtM0Q+B2x2NW69Jb2MhQFUbk3rUrHzz4qo5mqS8VrqNgYnSc8TiUGnR691LnO4uIfEE23w==
|
||||
libheif-js@^1.17.1:
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/libheif-js/-/libheif-js-1.17.1.tgz#7772cc5a31098df0354f0fadb49a939030765acd"
|
||||
integrity sha512-g9wBm/CasGZMjmH3B2sD9+AO7Y5+79F0oPS+sdAulSxQeYeCeiTIP+lDqvlPofD+y76wvfVtotKZ8AuvZQnWgg==
|
||||
|
||||
libsodium-wrappers@^0.7.8:
|
||||
version "0.7.9"
|
||||
|
@ -3484,6 +3479,11 @@ otpauth@^9.0.2:
|
|||
dependencies:
|
||||
jssha "~3.3.0"
|
||||
|
||||
p-debounce@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-debounce/-/p-debounce-4.0.0.tgz#348e3f44489baa9435cc7d807f17b3bb2fb16b24"
|
||||
integrity sha512-4Ispi9I9qYGO4lueiLDhe4q4iK5ERK8reLsuzH6BPaXn53EGaua8H66PXIFGrW897hwjXp+pVLrm/DLxN0RF0A==
|
||||
|
||||
p-limit@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz"
|
||||
|
@ -3609,10 +3609,10 @@ piexifjs@^1.0.6:
|
|||
resolved "https://registry.npmjs.org/piexifjs/-/piexifjs-1.0.6.tgz"
|
||||
integrity sha512-0wVyH0cKohzBQ5Gi2V1BuxYpxWfxF3cSqfFXfPIpl5tl9XLS5z4ogqhUCD20AbHi0h9aJkqXNJnkVev6gwh2ag==
|
||||
|
||||
pngjs@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz"
|
||||
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
|
||||
pngjs@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
|
||||
integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==
|
||||
|
||||
postcss@8.4.14:
|
||||
version "8.4.14"
|
||||
|
|
Loading…
Reference in a new issue