Merge branch 'gallery-refactor' into hidden-support
This commit is contained in:
commit
ff66abce74
|
@ -55,6 +55,7 @@
|
|||
"jszip": "3.8.0",
|
||||
"libsodium-wrappers": "^0.7.8",
|
||||
"localforage": "^1.9.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"ml-matrix": "^6.8.2",
|
||||
"next": "^13.1.2",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
|
@ -88,6 +89,8 @@
|
|||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ente/eslint-config": "*",
|
||||
"@ente/tsconfig": "*",
|
||||
"@next/bundle-analyzer": "^13.1.6",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/debounce-promise": "^3.1.3",
|
||||
|
@ -98,14 +101,13 @@
|
|||
"@types/react-collapse": "^5.0.1",
|
||||
"@types/react-datepicker": "^4.1.7",
|
||||
"@types/react-select": "^4.0.15",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.2",
|
||||
"@types/react-window-infinite-loader": "^1.0.3",
|
||||
"@types/wicg-file-system-access": "^2020.9.5",
|
||||
"@types/yup": "^0.29.7",
|
||||
"@types/zxcvbn": "^4.4.1",
|
||||
"eslint": "^8.28.0",
|
||||
"@ente/tsconfig": "*",
|
||||
"@ente/eslint-config": "*",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"standard": {
|
||||
|
|
|
@ -490,6 +490,7 @@
|
|||
"PUBLIC_COLLECT_SUBTEXT": "",
|
||||
"STOP_EXPORT": "",
|
||||
"EXPORT_PROGRESS": "",
|
||||
"MIGRATING_EXPORT": "",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "",
|
||||
"IN_PROGRESS": "",
|
||||
|
|
|
@ -491,6 +491,7 @@
|
|||
"PUBLIC_COLLECT_SUBTEXT": "Allow people with the link to also add photos to the shared album.",
|
||||
"STOP_EXPORT": "Stop",
|
||||
"EXPORT_PROGRESS": "<a>{{progress.success}} / {{progress.total}}</a> items synced",
|
||||
"MIGRATING_EXPORT": "Preparing...",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "Export started",
|
||||
"IN_PROGRESS": "Export already in progress",
|
||||
|
|
|
@ -490,6 +490,7 @@
|
|||
"PUBLIC_COLLECT_SUBTEXT": "Permitir a las personas con el enlace añadir fotos al álbum compartido.",
|
||||
"STOP_EXPORT": "Stop",
|
||||
"EXPORT_PROGRESS": "<a>{{progress.success}} / {{progress.total}}</a> archivos exportados",
|
||||
"MIGRATING_EXPORT": "",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "Exportar iniciando",
|
||||
"IN_PROGRESS": "Exportación ya en curso",
|
||||
|
|
|
@ -490,6 +490,7 @@
|
|||
"PUBLIC_COLLECT_SUBTEXT": "",
|
||||
"STOP_EXPORT": "",
|
||||
"EXPORT_PROGRESS": "",
|
||||
"MIGRATING_EXPORT": "",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "",
|
||||
"IN_PROGRESS": "",
|
||||
|
|
|
@ -490,6 +490,7 @@
|
|||
"PUBLIC_COLLECT_SUBTEXT": "",
|
||||
"STOP_EXPORT": "",
|
||||
"EXPORT_PROGRESS": "",
|
||||
"MIGRATING_EXPORT": "",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "",
|
||||
"IN_PROGRESS": "",
|
||||
|
|
|
@ -490,6 +490,7 @@
|
|||
"PUBLIC_COLLECT_SUBTEXT": "Autoriser les personnes ayant le lien d'ajouter des photos à l'album partagé.",
|
||||
"STOP_EXPORT": "Stop",
|
||||
"EXPORT_PROGRESS": "<a>{{progress.success}} / {{progress.total}}</a> fichiers exportés",
|
||||
"MIGRATING_EXPORT": "",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "L'export a démarré",
|
||||
"IN_PROGRESS": "Un export est déjà en cours",
|
||||
|
|
|
@ -490,6 +490,7 @@
|
|||
"PUBLIC_COLLECT_SUBTEXT": "",
|
||||
"STOP_EXPORT": "",
|
||||
"EXPORT_PROGRESS": "",
|
||||
"MIGRATING_EXPORT": "",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "",
|
||||
"IN_PROGRESS": "",
|
||||
|
|
|
@ -490,6 +490,7 @@
|
|||
"PUBLIC_COLLECT_SUBTEXT": "Sta toe dat mensen met de link ook foto's kunnen toevoegen aan het gedeelde album.",
|
||||
"STOP_EXPORT": "Stoppen",
|
||||
"EXPORT_PROGRESS": "<a>{{progress.success}} / {{progress.total}}</a> bestanden geëxporteerd",
|
||||
"MIGRATING_EXPORT": "",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "Exporteren begonnen",
|
||||
"IN_PROGRESS": "Exporteren is al bezig",
|
||||
|
|
|
@ -1,61 +1,61 @@
|
|||
{
|
||||
"HERO_SLIDE_1_TITLE": "",
|
||||
"HERO_SLIDE_1_TITLE": "<div>Backups privados</div><div>para as suas memórias</div>",
|
||||
"HERO_SLIDE_1": "",
|
||||
"HERO_SLIDE_2_TITLE": "",
|
||||
"HERO_SLIDE_2": "",
|
||||
"HERO_SLIDE_3_TITLE": "",
|
||||
"HERO_SLIDE_3": "",
|
||||
"LOGIN": "",
|
||||
"SIGN_UP": "",
|
||||
"NEW_USER": "",
|
||||
"EXISTING_USER": "",
|
||||
"ENTER_NAME": "",
|
||||
"PUBLIC_UPLOADER_NAME_MESSAGE": "",
|
||||
"ENTER_EMAIL": "",
|
||||
"EMAIL_ERROR": "",
|
||||
"REQUIRED": "",
|
||||
"EMAIL_SENT": "",
|
||||
"CHECK_INBOX": "",
|
||||
"ENTER_OTT": "",
|
||||
"RESEND_MAIL": "",
|
||||
"VERIFY": "",
|
||||
"UNKNOWN_ERROR": "",
|
||||
"INVALID_CODE": "",
|
||||
"EXPIRED_CODE": "",
|
||||
"SENDING": "",
|
||||
"SENT": "",
|
||||
"PASSWORD": "",
|
||||
"LINK_PASSWORD": "",
|
||||
"RETURN_PASSPHRASE_HINT": "",
|
||||
"SET_PASSPHRASE": "",
|
||||
"VERIFY_PASSPHRASE": "",
|
||||
"INCORRECT_PASSPHRASE": "",
|
||||
"HERO_SLIDE_3_TITLE": "<div>Disponível</div><div> em qualquer lugar</div>",
|
||||
"HERO_SLIDE_3": "Android, iOS, Web, Desktop",
|
||||
"LOGIN": "Entrar",
|
||||
"SIGN_UP": "Registar",
|
||||
"NEW_USER": "Novo no ente",
|
||||
"EXISTING_USER": "Utilizador existente",
|
||||
"ENTER_NAME": "Insira o nome",
|
||||
"PUBLIC_UPLOADER_NAME_MESSAGE": "Adicione um nome para que os seus amigos saibam a quem agradecer por estas ótimas fotos!",
|
||||
"ENTER_EMAIL": "Insira o endereço de email",
|
||||
"EMAIL_ERROR": "Inserir um endereço de email válido",
|
||||
"REQUIRED": "Obrigatório",
|
||||
"EMAIL_SENT": "Código de verificação enviado para <a>{{email}}</a>",
|
||||
"CHECK_INBOX": "Verifique a sua caixa de entrada (e spam) para concluir a verificação",
|
||||
"ENTER_OTT": "Código de verificação",
|
||||
"RESEND_MAIL": "Reenviar código",
|
||||
"VERIFY": "Verificar",
|
||||
"UNKNOWN_ERROR": "Ocorreu um erro. Tente novamente",
|
||||
"INVALID_CODE": "Código de verificação inválido",
|
||||
"EXPIRED_CODE": "O seu código de verificação expirou",
|
||||
"SENDING": "A enviar...",
|
||||
"SENT": "Enviado!",
|
||||
"PASSWORD": "Palavra-passe",
|
||||
"LINK_PASSWORD": "Introduza a palavra-passe para desbloquear o álbum",
|
||||
"RETURN_PASSPHRASE_HINT": "Palavra-passe",
|
||||
"SET_PASSPHRASE": "Definir palavra-passe",
|
||||
"VERIFY_PASSPHRASE": "Entrar",
|
||||
"INCORRECT_PASSPHRASE": "Palavra-passe incorreta",
|
||||
"ENTER_ENC_PASSPHRASE": "",
|
||||
"PASSPHRASE_DISCLAIMER": "",
|
||||
"WELCOME_TO_ENTE_HEADING": "",
|
||||
"WELCOME_TO_ENTE_HEADING": "Bem-vindo ao <a/>",
|
||||
"WELCOME_TO_ENTE_SUBHEADING": "",
|
||||
"WHERE_YOUR_BEST_PHOTOS_LIVE": "",
|
||||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "",
|
||||
"PASSPHRASE_HINT": "",
|
||||
"CONFIRM_PASSPHRASE": "",
|
||||
"PASSPHRASE_MATCH_ERROR": "",
|
||||
"CONSOLE_WARNING_STOP": "",
|
||||
"CONSOLE_WARNING_STOP": "PARAR!",
|
||||
"CONSOLE_WARNING_DESC": "",
|
||||
"CREATE_COLLECTION": "",
|
||||
"ENTER_ALBUM_NAME": "",
|
||||
"CLOSE_OPTION": "",
|
||||
"ENTER_FILE_NAME": "",
|
||||
"CLOSE": "",
|
||||
"NO": "",
|
||||
"CREATE_COLLECTION": "Novo álbum",
|
||||
"ENTER_ALBUM_NAME": "Nome do álbum",
|
||||
"CLOSE_OPTION": "Fechar (Esc)",
|
||||
"ENTER_FILE_NAME": "Nome do ficheiro",
|
||||
"CLOSE": "Fechar",
|
||||
"NO": "Não",
|
||||
"NOTHING_HERE": "",
|
||||
"UPLOAD": "",
|
||||
"IMPORT": "",
|
||||
"ADD_PHOTOS": "",
|
||||
"ADD_MORE_PHOTOS": "",
|
||||
"add_photos_one": "",
|
||||
"add_photos_other": "",
|
||||
"SELECT_PHOTOS": "",
|
||||
"FILE_UPLOAD": "",
|
||||
"IMPORT": "Importar",
|
||||
"ADD_PHOTOS": "Adicionar fotos",
|
||||
"ADD_MORE_PHOTOS": "Adicionar mais fotos",
|
||||
"add_photos_one": "Adicionar item",
|
||||
"add_photos_other": "Adicionar {{count, number}} itens",
|
||||
"SELECT_PHOTOS": "Selecionar fotos",
|
||||
"FILE_UPLOAD": "Enviar Ficheiro",
|
||||
"UPLOAD_STAGE_MESSAGE": {
|
||||
"0": "",
|
||||
"1": "",
|
||||
|
@ -70,9 +70,9 @@
|
|||
"STORAGE_QUOTA_EXCEEDED": "",
|
||||
"INITIAL_LOAD_DELAY_WARNING": "",
|
||||
"USER_DOES_NOT_EXIST": "",
|
||||
"NO_ACCOUNT": "",
|
||||
"ACCOUNT_EXISTS": "",
|
||||
"CREATE": "",
|
||||
"NO_ACCOUNT": "Não possui uma conta",
|
||||
"ACCOUNT_EXISTS": "Já possui uma conta",
|
||||
"CREATE": "Criar",
|
||||
"DOWNLOAD": "",
|
||||
"DOWNLOAD_OPTION": "",
|
||||
"DOWNLOAD_FAVORITES": "",
|
||||
|
@ -490,6 +490,7 @@
|
|||
"PUBLIC_COLLECT_SUBTEXT": "",
|
||||
"STOP_EXPORT": "",
|
||||
"EXPORT_PROGRESS": "",
|
||||
"MIGRATING_EXPORT": "",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "",
|
||||
"IN_PROGRESS": "",
|
||||
|
|
|
@ -490,6 +490,7 @@
|
|||
"PUBLIC_COLLECT_SUBTEXT": "",
|
||||
"STOP_EXPORT": "",
|
||||
"EXPORT_PROGRESS": "",
|
||||
"MIGRATING_EXPORT": "",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "",
|
||||
"IN_PROGRESS": "",
|
||||
|
|
|
@ -490,6 +490,7 @@
|
|||
"PUBLIC_COLLECT_SUBTEXT": "",
|
||||
"STOP_EXPORT": "",
|
||||
"EXPORT_PROGRESS": "",
|
||||
"MIGRATING_EXPORT": "",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "",
|
||||
"IN_PROGRESS": "",
|
||||
|
|
|
@ -290,7 +290,7 @@
|
|||
"UPLOAD_TO_COLLECTION": "上传至相册",
|
||||
"UNCATEGORIZED": "未分类的",
|
||||
"ARCHIVE": "存档",
|
||||
"FAVORITES": "",
|
||||
"FAVORITES": "收藏",
|
||||
"ARCHIVE_COLLECTION": "存档相册",
|
||||
"ARCHIVE_SECTION_NAME": "存档",
|
||||
"ALL_SECTION_NAME": "全部",
|
||||
|
@ -490,6 +490,7 @@
|
|||
"PUBLIC_COLLECT_SUBTEXT": "允许具有链接的人也将照片添加到共享相册。",
|
||||
"STOP_EXPORT": "停止",
|
||||
"EXPORT_PROGRESS": "<a>{{progress.success}} / {{progress.total}}</a> 个文件已导出",
|
||||
"MIGRATING_EXPORT": "",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "导出已开始",
|
||||
"IN_PROGRESS": "导出已在进行中",
|
||||
|
|
|
@ -8,17 +8,20 @@ import { t } from 'i18next';
|
|||
interface Iprops {
|
||||
collectionSummary: CollectionSummary;
|
||||
onCollectionClick: (collectionID: number) => void;
|
||||
isScrolling?: boolean;
|
||||
}
|
||||
|
||||
export default function AllCollectionCard({
|
||||
onCollectionClick,
|
||||
collectionSummary,
|
||||
isScrolling,
|
||||
}: Iprops) {
|
||||
return (
|
||||
<CollectionCard
|
||||
collectionTile={AllCollectionTile}
|
||||
latestFile={collectionSummary.latestFile}
|
||||
onClick={() => onCollectionClick(collectionSummary.id)}>
|
||||
onClick={() => onCollectionClick(collectionSummary.id)}
|
||||
isScrolling={isScrolling}>
|
||||
<AllCollectionTileText>
|
||||
<Typography>{collectionSummary.name}</Typography>
|
||||
<Typography variant="small" color="text.muted">
|
||||
|
|
|
@ -1,28 +1,135 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { DialogContent } from '@mui/material';
|
||||
import { FlexWrapper } from 'components/Container';
|
||||
import AllCollectionCard from './collectionCard';
|
||||
import { CollectionSummary } from 'types/collection';
|
||||
import {
|
||||
FixedSizeList as List,
|
||||
ListChildComponentProps,
|
||||
areEqual,
|
||||
} from 'react-window';
|
||||
import memoize from 'memoize-one';
|
||||
import useWindowSize from 'hooks/useWindowSize';
|
||||
import { AllCollectionMobileBreakpoint } from './dialog';
|
||||
|
||||
const MobileColumns = 2;
|
||||
const DesktopColumns = 3;
|
||||
|
||||
interface Iprops {
|
||||
collectionSummaries: CollectionSummary[];
|
||||
onCollectionClick: (id?: number) => void;
|
||||
}
|
||||
|
||||
interface ItemData {
|
||||
collectionRowList: CollectionSummary[][];
|
||||
onCollectionClick: (id?: number) => void;
|
||||
}
|
||||
|
||||
// This helper function memoizes incoming props,
|
||||
// To avoid causing unnecessary re-renders pure Row components.
|
||||
// This is only needed since we are passing multiple props with a wrapper object.
|
||||
// If we were only passing a single, stable value (e.g. items),
|
||||
// We could just pass the value directly.
|
||||
const createItemData = memoize((collectionRowList, onCollectionClick) => ({
|
||||
collectionRowList,
|
||||
onCollectionClick,
|
||||
}));
|
||||
|
||||
//If list items are expensive to render,
|
||||
// Consider using React.memo or shouldComponentUpdate to avoid unnecessary re-renders.
|
||||
// https://reactjs.org/docs/react-api.html#reactmemo
|
||||
// https://reactjs.org/docs/react-api.html#reactpurecomponent
|
||||
const AllCollectionRow = React.memo(
|
||||
({
|
||||
data,
|
||||
index,
|
||||
style,
|
||||
isScrolling,
|
||||
}: ListChildComponentProps<ItemData>) => {
|
||||
const { collectionRowList, onCollectionClick } = data;
|
||||
const collectionRow = collectionRowList[index];
|
||||
return (
|
||||
<div style={style}>
|
||||
<FlexWrapper gap={0.5}>
|
||||
{collectionRow.map((item: any) => (
|
||||
<AllCollectionCard
|
||||
isScrolling={isScrolling}
|
||||
onCollectionClick={onCollectionClick}
|
||||
collectionSummary={item}
|
||||
key={item.id}
|
||||
/>
|
||||
))}
|
||||
</FlexWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
areEqual
|
||||
);
|
||||
|
||||
export default function AllCollectionContent({
|
||||
collectionSummaries,
|
||||
onCollectionClick,
|
||||
}: Iprops) {
|
||||
const refreshInProgress = useRef(false);
|
||||
const shouldRefresh = useRef(false);
|
||||
|
||||
const [collectionRowList, setCollectionRowList] = useState([]);
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
|
||||
useEffect(() => {
|
||||
if (!windowSize.width || !collectionSummaries) {
|
||||
return;
|
||||
}
|
||||
const main = async () => {
|
||||
if (refreshInProgress.current) {
|
||||
shouldRefresh.current = true;
|
||||
return;
|
||||
}
|
||||
refreshInProgress.current = true;
|
||||
|
||||
const collectionRowList: CollectionSummary[][] = [];
|
||||
let index = 0;
|
||||
const columns =
|
||||
windowSize.width > AllCollectionMobileBreakpoint
|
||||
? DesktopColumns
|
||||
: MobileColumns;
|
||||
while (index < collectionSummaries.length) {
|
||||
const collectionRow: CollectionSummary[] = [];
|
||||
for (
|
||||
let i = 0;
|
||||
i < columns && index < collectionSummaries.length;
|
||||
i++
|
||||
) {
|
||||
collectionRow.push(collectionSummaries[index++]);
|
||||
}
|
||||
collectionRowList.push(collectionRow);
|
||||
}
|
||||
setCollectionRowList(collectionRowList);
|
||||
refreshInProgress.current = false;
|
||||
if (shouldRefresh.current) {
|
||||
shouldRefresh.current = false;
|
||||
setTimeout(main, 0);
|
||||
}
|
||||
};
|
||||
main();
|
||||
}, [collectionSummaries, windowSize]);
|
||||
|
||||
// Bundle additional data to list items using the "itemData" prop.
|
||||
// It will be accessible to item renderers as props.data.
|
||||
// Memoize this data to avoid bypassing shouldComponentUpdate().
|
||||
const itemData = createItemData(collectionRowList, onCollectionClick);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<FlexWrapper flexWrap="wrap" gap={0.5}>
|
||||
{collectionSummaries.map((collectionSummary) => (
|
||||
<AllCollectionCard
|
||||
onCollectionClick={onCollectionClick}
|
||||
collectionSummary={collectionSummary}
|
||||
key={collectionSummary.id}
|
||||
/>
|
||||
))}
|
||||
</FlexWrapper>
|
||||
<List
|
||||
height={windowSize.height ?? 0}
|
||||
width={'100%'}
|
||||
itemCount={collectionRowList.length}
|
||||
itemSize={154}
|
||||
itemData={itemData}>
|
||||
{AllCollectionRow}
|
||||
</List>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import { Dialog, Slide, styled } from '@mui/material';
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const AllCollectionMobileBreakpoint = 559;
|
||||
|
||||
export const AllCollectionDialog = styled(Dialog)<{
|
||||
position: 'flex-start' | 'center' | 'flex-end';
|
||||
}>(({ theme, position }) => ({
|
||||
|
@ -18,7 +20,7 @@ export const AllCollectionDialog = styled(Dialog)<{
|
|||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
[theme.breakpoints.down(559)]: {
|
||||
[theme.breakpoints.down(AllCollectionMobileBreakpoint)]: {
|
||||
'& .MuiPaper-root': {
|
||||
width: '324px',
|
||||
},
|
||||
|
|
|
@ -11,12 +11,14 @@ export default function CollectionCard(props: {
|
|||
latestFile: EnteFile;
|
||||
onClick: () => void;
|
||||
collectionTile: any;
|
||||
isScrolling?: boolean;
|
||||
}) {
|
||||
const {
|
||||
latestFile: file,
|
||||
onClick,
|
||||
children,
|
||||
collectionTile: CustomCollectionTile,
|
||||
isScrolling,
|
||||
} = props;
|
||||
|
||||
const [coverImageURL, setCoverImageURL] = useState(null);
|
||||
|
@ -27,13 +29,16 @@ export default function CollectionCard(props: {
|
|||
return;
|
||||
}
|
||||
if (!galleryContext.thumbs.has(file.id)) {
|
||||
if (isScrolling) {
|
||||
return;
|
||||
}
|
||||
const url = await downloadManager.getThumbnail(file);
|
||||
galleryContext.thumbs.set(file.id, url);
|
||||
}
|
||||
setCoverImageURL(galleryContext.thumbs.get(file.id));
|
||||
};
|
||||
main();
|
||||
}, [file]);
|
||||
}, [file, isScrolling]);
|
||||
|
||||
return (
|
||||
<CustomCollectionTile onClick={onClick}>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import { EnteFile } from 'types/file';
|
||||
import {
|
||||
ActiveIndicator,
|
||||
CollectionBarTile,
|
||||
|
@ -14,30 +13,33 @@ import Favorite from '@mui/icons-material/FavoriteRounded';
|
|||
import ArchiveIcon from '@mui/icons-material/Archive';
|
||||
import PeopleIcon from '@mui/icons-material/People';
|
||||
import LinkIcon from '@mui/icons-material/Link';
|
||||
import { CollectionSummary } from 'types/collection';
|
||||
|
||||
interface Iprops {
|
||||
active: boolean;
|
||||
latestFile: EnteFile;
|
||||
collectionName: string;
|
||||
collectionType: CollectionSummaryType;
|
||||
onClick: () => void;
|
||||
collectionSummary: CollectionSummary;
|
||||
activeCollection: number;
|
||||
onCollectionClick: (collectionID: number) => void;
|
||||
isScrolling?: boolean;
|
||||
}
|
||||
|
||||
const CollectionListBarCard = React.forwardRef((props: Iprops, ref: any) => {
|
||||
const { active, collectionName, collectionType, ...others } = props;
|
||||
const CollectionListBarCard = (props: Iprops) => {
|
||||
const { activeCollection, collectionSummary, onCollectionClick } = props;
|
||||
|
||||
return (
|
||||
<Box ref={ref}>
|
||||
<CollectionCard collectionTile={CollectionBarTile} {...others}>
|
||||
<CollectionCardText collectionName={collectionName} />
|
||||
<CollectionCardIcon collectionType={collectionType} />
|
||||
<Box>
|
||||
<CollectionCard
|
||||
collectionTile={CollectionBarTile}
|
||||
latestFile={collectionSummary.latestFile}
|
||||
onClick={() => {
|
||||
onCollectionClick(collectionSummary.id);
|
||||
}}>
|
||||
<CollectionCardText collectionName={collectionSummary.name} />
|
||||
<CollectionCardIcon collectionType={collectionSummary.type} />
|
||||
</CollectionCard>
|
||||
{active && <ActiveIndicator />}
|
||||
{activeCollection === collectionSummary.id && <ActiveIndicator />}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
export default CollectionListBarCard;
|
||||
};
|
||||
|
||||
function CollectionCardText({ collectionName }) {
|
||||
return (
|
||||
|
@ -70,3 +72,5 @@ function CollectionCardIcon({ collectionType }) {
|
|||
</CollectionBarTileIcon>
|
||||
);
|
||||
}
|
||||
|
||||
export default CollectionListBarCard;
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
import ScrollButton from 'components/Collections/CollectionListBar/ScrollButton';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { ALL_SECTION, COLLECTION_SORT_BY } from 'constants/collection';
|
||||
import { Box, IconButton, Typography } from '@mui/material';
|
||||
import {
|
||||
CollectionListBarWrapper,
|
||||
ScrollContainer,
|
||||
CollectionListWrapper,
|
||||
} from 'components/Collections/styledComponents';
|
||||
import CollectionListBarCard from 'components/Collections/CollectionListBar/CollectionCard';
|
||||
import useComponentScroll, { SCROLL_DIRECTION } from 'hooks/useComponentScroll';
|
||||
import useWindowSize from 'hooks/useWindowSize';
|
||||
import { IconButtonWithBG, SpaceBetweenFlex } from 'components/Container';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { CollectionSummary } from 'types/collection';
|
||||
import CollectionSort from '../AllCollections/CollectionSort';
|
||||
import { t } from 'i18next';
|
||||
import {
|
||||
FixedSizeList as List,
|
||||
ListChildComponentProps,
|
||||
areEqual,
|
||||
} from 'react-window';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import memoize from 'memoize-one';
|
||||
import useComponentScroll, { SCROLL_DIRECTION } from 'hooks/useComponentScroll';
|
||||
import useWindowSize from 'hooks/useWindowSize';
|
||||
import ScrollButton from './ScrollButton';
|
||||
|
||||
interface IProps {
|
||||
activeCollection?: number;
|
||||
|
@ -26,7 +32,50 @@ interface IProps {
|
|||
setCollectionSortBy: (v: COLLECTION_SORT_BY) => void;
|
||||
}
|
||||
|
||||
export default function CollectionListBar(props: IProps) {
|
||||
interface ItemData {
|
||||
collectionSummaries: CollectionSummary[];
|
||||
activeCollection?: number;
|
||||
onCollectionClick: (id?: number) => void;
|
||||
}
|
||||
|
||||
const CollectionListBarCardWidth = 94;
|
||||
|
||||
const createItemData = memoize(
|
||||
(collectionSummaries, activeCollection, onCollectionClick) => ({
|
||||
collectionSummaries,
|
||||
activeCollection,
|
||||
onCollectionClick,
|
||||
})
|
||||
);
|
||||
|
||||
const CollectionCardContainer = React.memo(
|
||||
({
|
||||
data,
|
||||
index,
|
||||
style,
|
||||
isScrolling,
|
||||
}: ListChildComponentProps<ItemData>) => {
|
||||
const { collectionSummaries, activeCollection, onCollectionClick } =
|
||||
data;
|
||||
|
||||
const collectionSummary = collectionSummaries[index];
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<CollectionListBarCard
|
||||
key={collectionSummary.id}
|
||||
activeCollection={activeCollection}
|
||||
isScrolling={isScrolling}
|
||||
collectionSummary={collectionSummary}
|
||||
onCollectionClick={onCollectionClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
areEqual
|
||||
);
|
||||
|
||||
const CollectionListBar = (props: IProps) => {
|
||||
const {
|
||||
activeCollection,
|
||||
setActiveCollection,
|
||||
|
@ -38,27 +87,38 @@ export default function CollectionListBar(props: IProps) {
|
|||
|
||||
const windowSize = useWindowSize();
|
||||
|
||||
const { componentRef, scrollComponent, onFarLeft, onFarRight } =
|
||||
useComponentScroll({
|
||||
dependencies: [windowSize, collectionSummaries],
|
||||
});
|
||||
const {
|
||||
componentRef: collectionListWrapperRef,
|
||||
scrollComponent,
|
||||
onFarLeft,
|
||||
onFarRight,
|
||||
} = useComponentScroll({
|
||||
dependencies: [windowSize, collectionSummaries],
|
||||
});
|
||||
|
||||
const collectionChipsRef = collectionSummaries.reduce(
|
||||
(refMap, collectionSummary) => {
|
||||
refMap[collectionSummary.id] = React.createRef();
|
||||
return refMap;
|
||||
},
|
||||
{}
|
||||
);
|
||||
const collectionListRef = React.useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
collectionChipsRef[activeCollection]?.current.scrollIntoView();
|
||||
if (!collectionListRef.current) {
|
||||
return;
|
||||
}
|
||||
// scroll the active collection into view
|
||||
const activeCollectionIndex = collectionSummaries.findIndex(
|
||||
(item) => item.id === activeCollection
|
||||
);
|
||||
collectionListRef.current.scrollToItem(activeCollectionIndex, 'smart');
|
||||
}, [activeCollection]);
|
||||
|
||||
const clickHandler = (collectionID?: number) => () => {
|
||||
const onCollectionClick = (collectionID?: number) => {
|
||||
setActiveCollection(collectionID ?? ALL_SECTION);
|
||||
};
|
||||
|
||||
const itemData = createItemData(
|
||||
collectionSummaries,
|
||||
activeCollection,
|
||||
onCollectionClick
|
||||
);
|
||||
|
||||
return (
|
||||
<CollectionListBarWrapper>
|
||||
<SpaceBetweenFlex mb={1}>
|
||||
|
@ -84,19 +144,22 @@ export default function CollectionListBar(props: IProps) {
|
|||
onClick={scrollComponent(SCROLL_DIRECTION.LEFT)}
|
||||
/>
|
||||
)}
|
||||
<ScrollContainer ref={componentRef}>
|
||||
{collectionSummaries.map((item) => (
|
||||
<CollectionListBarCard
|
||||
key={item.id}
|
||||
latestFile={item.latestFile}
|
||||
ref={collectionChipsRef[item.id]}
|
||||
active={activeCollection === item.id}
|
||||
onClick={clickHandler(item.id)}
|
||||
collectionType={item.type}
|
||||
collectionName={item.name}
|
||||
/>
|
||||
))}
|
||||
</ScrollContainer>
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<List
|
||||
ref={collectionListRef}
|
||||
outerRef={collectionListWrapperRef}
|
||||
itemData={itemData}
|
||||
layout="horizontal"
|
||||
width={width}
|
||||
height={110}
|
||||
itemCount={collectionSummaries.length}
|
||||
itemSize={CollectionListBarCardWidth}
|
||||
useIsScrolling>
|
||||
{CollectionCardContainer}
|
||||
</List>
|
||||
)}
|
||||
</AutoSizer>
|
||||
{!onFarRight && (
|
||||
<ScrollButton
|
||||
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
||||
|
@ -122,4 +185,6 @@ export default function CollectionListBar(props: IProps) {
|
|||
</Box>
|
||||
</CollectionListBarWrapper>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default CollectionListBar;
|
||||
|
|
|
@ -35,6 +35,8 @@ export default function ExportInProgress(props: Props) {
|
|||
<Box mb={1.5}>
|
||||
{isLoading ? (
|
||||
t('EXPORT_STARTING')
|
||||
) : props.exportStage === ExportStage.MIGRATION ? (
|
||||
t('MIGRATING_EXPORT')
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey={'EXPORT_PROGRESS'}
|
||||
|
|
|
@ -29,7 +29,6 @@ import { getExportDirectoryDoesNotExistMessage } from 'utils/ui';
|
|||
import { t } from 'i18next';
|
||||
import LinkButton from './pages/gallery/LinkButton';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { formatNumber } from 'utils/number/format';
|
||||
import { addLogLine } from 'utils/logging';
|
||||
|
||||
const ExportFolderPathContainer = styled(LinkButton)`
|
||||
|
@ -212,14 +211,6 @@ export default function ExportModal(props: Props) {
|
|||
continuousExport={continuousExport}
|
||||
toggleContinuousExport={toggleContinuousExport}
|
||||
/>
|
||||
<SpaceBetweenFlex minHeight={'48px'} pr={'16px'}>
|
||||
<Typography color="text.muted">
|
||||
{t('TOTAL_ITEMS')}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{formatNumber(fileExportStats.totalCount)}
|
||||
</Typography>
|
||||
</SpaceBetweenFlex>
|
||||
</DialogContent>
|
||||
<Divider />
|
||||
<ExportDynamicContent
|
||||
|
@ -331,6 +322,7 @@ const ExportDynamicContent = ({
|
|||
return <ExportInit startExport={startExport} />;
|
||||
|
||||
case ExportStage.INPROGRESS:
|
||||
case ExportStage.MIGRATION:
|
||||
return (
|
||||
<ExportInProgress
|
||||
exportStage={exportStage}
|
||||
|
|
|
@ -11,15 +11,20 @@ import { openLink } from 'utils/common';
|
|||
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
|
||||
import { Typography } from '@mui/material';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { REDIRECTS, getRedirectURL } from 'constants/redirects';
|
||||
import { DESKTOP_ROADMAP_URL, WEB_ROADMAP_URL } from 'constants/urls';
|
||||
|
||||
export default function HelpSection() {
|
||||
const { setDialogMessage } = useContext(AppContext);
|
||||
const { openExportModal } = useContext(GalleryContext);
|
||||
|
||||
async function openRoadmapURL() {
|
||||
const roadmapRedirectURL = getRedirectURL(REDIRECTS.ROADMAP);
|
||||
openLink(roadmapRedirectURL, true);
|
||||
async function openRoadmap() {
|
||||
let roadmapURL: string;
|
||||
if (isElectron()) {
|
||||
roadmapURL = DESKTOP_ROADMAP_URL;
|
||||
} else {
|
||||
roadmapURL = WEB_ROADMAP_URL;
|
||||
}
|
||||
openLink(roadmapURL, true);
|
||||
}
|
||||
|
||||
function handleExportOpen() {
|
||||
|
@ -33,7 +38,7 @@ export default function HelpSection() {
|
|||
return (
|
||||
<>
|
||||
<EnteMenuItem
|
||||
onClick={openRoadmapURL}
|
||||
onClick={openRoadmap}
|
||||
label={t('REQUEST_FEATURE')}
|
||||
variant="secondary"
|
||||
/>
|
||||
|
|
|
@ -5,5 +5,6 @@ export const ENTE_TRASH_FOLDER = 'Trash';
|
|||
export enum ExportStage {
|
||||
INIT = 0,
|
||||
INPROGRESS = 1,
|
||||
MIGRATION = 2,
|
||||
FINISHED = 3,
|
||||
}
|
||||
|
|
|
@ -12,3 +12,8 @@ export const APP_DOWNLOAD_URL = 'https://ente.io/download/desktop';
|
|||
export const FEEDBACK_EMAIL = 'feedback@ente.io';
|
||||
|
||||
export const DELETE_ACCOUNT_EMAIL = 'account-deletion@ente.io';
|
||||
|
||||
export const WEB_ROADMAP_URL = 'https://github.com/ente-io/photos-web/issues';
|
||||
|
||||
export const DESKTOP_ROADMAP_URL =
|
||||
'https://github.com/ente-io/photos-desktop/issues';
|
||||
|
|
|
@ -61,6 +61,6 @@ export default function useComponentScroll({
|
|||
onFarLeft,
|
||||
onFarRight,
|
||||
scrollComponent,
|
||||
componentRef: componentRef,
|
||||
componentRef,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -128,10 +128,14 @@ class ExportService {
|
|||
}
|
||||
}
|
||||
|
||||
async runMigration(exportDir: string, exportRecord: ExportRecord) {
|
||||
async runMigration(
|
||||
exportDir: string,
|
||||
exportRecord: ExportRecord,
|
||||
updateProgress: (progress: ExportProgress) => void
|
||||
) {
|
||||
try {
|
||||
addLogLine('running migration');
|
||||
await migrateExport(exportDir, exportRecord);
|
||||
await migrateExport(exportDir, exportRecord, updateProgress);
|
||||
addLogLine('migration completed');
|
||||
} catch (e) {
|
||||
logError(e, 'migration failed');
|
||||
|
@ -281,14 +285,24 @@ class ExportService {
|
|||
|
||||
async preExport(exportFolder: string) {
|
||||
this.verifyExportFolderExists(exportFolder);
|
||||
const exportRecord = await this.getExportRecord(exportFolder);
|
||||
await this.updateExportStage(ExportStage.MIGRATION);
|
||||
this.updateExportProgress({
|
||||
success: 0,
|
||||
failed: 0,
|
||||
total: 0,
|
||||
});
|
||||
await this.runMigration(
|
||||
exportFolder,
|
||||
exportRecord,
|
||||
this.updateExportProgress.bind(this)
|
||||
);
|
||||
await this.updateExportStage(ExportStage.INPROGRESS);
|
||||
this.updateExportProgress({
|
||||
success: 0,
|
||||
failed: 0,
|
||||
total: 0,
|
||||
});
|
||||
const exportRecord = await this.getExportRecord(exportFolder);
|
||||
await this.runMigration(exportFolder, exportRecord);
|
||||
}
|
||||
|
||||
async postExport() {
|
||||
|
@ -312,6 +326,7 @@ class ExportService {
|
|||
|
||||
async stopRunningExport() {
|
||||
try {
|
||||
addLogLine('user requested export cancellation');
|
||||
this.exportInProgress.exec();
|
||||
this.exportInProgress = null;
|
||||
this.reRunNeeded = false;
|
||||
|
@ -344,16 +359,15 @@ class ExportService {
|
|||
addLogLine('export started');
|
||||
await this.runExport(exportFolder, isCanceled);
|
||||
addLogLine('export completed');
|
||||
await this.postExport();
|
||||
this.exportInProgress = null;
|
||||
if (this.reRunNeeded) {
|
||||
this.reRunNeeded = false;
|
||||
addLogLine('re-running export');
|
||||
setTimeout(() => this.scheduleExport(), 0);
|
||||
}
|
||||
} finally {
|
||||
if (!isCanceled.status || !this.exportInProgress) {
|
||||
if (isCanceled.status) {
|
||||
addLogLine('export cancellation done');
|
||||
if (!this.exportInProgress) {
|
||||
await this.postExport();
|
||||
}
|
||||
} else {
|
||||
await this.postExport();
|
||||
addLogLine('resetting export in progress after completion');
|
||||
this.exportInProgress = null;
|
||||
if (this.reRunNeeded) {
|
||||
this.reRunNeeded = false;
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
FileExportNames,
|
||||
ExportRecordV0,
|
||||
CollectionExportNames,
|
||||
ExportProgress,
|
||||
} from 'types/export';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { User } from 'types/user';
|
||||
|
@ -38,10 +39,12 @@ import { FILE_TYPE } from 'constants/file';
|
|||
import { decodeLivePhoto } from 'services/livePhotoService';
|
||||
import downloadManager from 'services/downloadManager';
|
||||
import { retryAsyncFunction } from 'utils/network';
|
||||
import { sleep } from 'utils/common';
|
||||
|
||||
export async function migrateExport(
|
||||
exportDir: string,
|
||||
exportRecord: ExportRecordV1 | ExportRecordV2 | ExportRecord
|
||||
exportRecord: ExportRecordV1 | ExportRecordV2 | ExportRecord,
|
||||
updateProgress: (progress: ExportProgress) => void
|
||||
) {
|
||||
try {
|
||||
addLogLine(`current export version: ${exportRecord.version}`);
|
||||
|
@ -63,7 +66,11 @@ export async function migrateExport(
|
|||
}
|
||||
if (exportRecord.version === 2) {
|
||||
addLogLine('migrating export to version 3');
|
||||
await migrationV2ToV3(exportDir, exportRecord as ExportRecordV2);
|
||||
await migrationV2ToV3(
|
||||
exportDir,
|
||||
exportRecord as ExportRecordV2,
|
||||
updateProgress
|
||||
);
|
||||
exportRecord = await exportService.updateExportRecord(exportDir, {
|
||||
version: 3,
|
||||
});
|
||||
|
@ -115,7 +122,8 @@ async function migrationV1ToV2(
|
|||
|
||||
async function migrationV2ToV3(
|
||||
exportDir: string,
|
||||
exportRecord: ExportRecordV2
|
||||
exportRecord: ExportRecordV2,
|
||||
updateProgress: (progress: ExportProgress) => void
|
||||
) {
|
||||
if (!exportRecord?.exportedFiles) {
|
||||
return;
|
||||
|
@ -134,7 +142,8 @@ async function migrationV2ToV3(
|
|||
|
||||
const fileExportNames = await getFileExportNamesFromExportedFiles(
|
||||
exportRecord,
|
||||
getExportedFiles(personalFiles, exportRecord)
|
||||
getExportedFiles(personalFiles, exportRecord),
|
||||
updateProgress
|
||||
);
|
||||
|
||||
exportRecord.exportedCollectionPaths = undefined;
|
||||
|
@ -264,7 +273,8 @@ async function getCollectionExportNamesFromExportedCollectionPaths(
|
|||
*/
|
||||
async function getFileExportNamesFromExportedFiles(
|
||||
exportRecord: ExportRecordV2,
|
||||
exportedFiles: EnteFile[]
|
||||
exportedFiles: EnteFile[],
|
||||
updateProgress: (progress: ExportProgress) => void
|
||||
): Promise<FileExportNames> {
|
||||
if (!exportedFiles.length) {
|
||||
return;
|
||||
|
@ -278,7 +288,9 @@ async function getFileExportNamesFromExportedFiles(
|
|||
const exportedCollectionPaths = convertCollectionIDFolderPathObjectToMap(
|
||||
exportRecord.exportedCollectionPaths
|
||||
);
|
||||
let success = 0;
|
||||
for (const file of exportedFiles) {
|
||||
await sleep(0);
|
||||
const collectionPath = exportedCollectionPaths.get(file.collectionID);
|
||||
addLocalLog(
|
||||
() =>
|
||||
|
@ -323,6 +335,11 @@ async function getFileExportNamesFromExportedFiles(
|
|||
...exportedFileNames,
|
||||
[getExportRecordFileUID(file)]: fileExportName,
|
||||
};
|
||||
updateProgress({
|
||||
total: exportedFiles.length,
|
||||
success: success++,
|
||||
failed: 0,
|
||||
});
|
||||
}
|
||||
return exportedFileNames;
|
||||
}
|
||||
|
|
|
@ -49,7 +49,6 @@ class UploadManager {
|
|||
private remainingFiles: FileWithCollection[] = [];
|
||||
private failedFiles: FileWithCollection[];
|
||||
private existingFiles: EnteFile[];
|
||||
private userOwnedNonTrashedExistingFiles: EnteFile[];
|
||||
private setFiles: SetFiles;
|
||||
private collections: Map<number, Collection>;
|
||||
private uploadInProgress: boolean;
|
||||
|
@ -92,12 +91,8 @@ class UploadManager {
|
|||
this.existingFiles = await getLocalPublicFiles(
|
||||
getPublicCollectionUID(this.publicUploadProps.token)
|
||||
);
|
||||
this.userOwnedNonTrashedExistingFiles = this.existingFiles;
|
||||
} else {
|
||||
this.existingFiles = await getLocalFiles();
|
||||
this.userOwnedNonTrashedExistingFiles = getUserOwnedFiles(
|
||||
this.existingFiles
|
||||
);
|
||||
this.existingFiles = getUserOwnedFiles(await getLocalFiles());
|
||||
}
|
||||
this.collections = new Map(
|
||||
collections.map((collection) => [collection.id, collection])
|
||||
|
@ -287,7 +282,7 @@ class UploadManager {
|
|||
fileWithCollection = { ...fileWithCollection, collection };
|
||||
const { fileUploadResult, uploadedFile } = await uploader(
|
||||
worker,
|
||||
this.userOwnedNonTrashedExistingFiles,
|
||||
this.existingFiles,
|
||||
fileWithCollection,
|
||||
this.uploaderName
|
||||
);
|
||||
|
@ -408,13 +403,11 @@ class UploadManager {
|
|||
if (!decryptedFile) {
|
||||
throw Error("decrypted file can't be undefined");
|
||||
}
|
||||
this.userOwnedNonTrashedExistingFiles.push(decryptedFile);
|
||||
this.existingFiles.push(decryptedFile);
|
||||
this.updateUIFiles(decryptedFile);
|
||||
}
|
||||
|
||||
private updateUIFiles(decryptedFile: EnteFile) {
|
||||
this.existingFiles.push(decryptedFile);
|
||||
this.existingFiles = sortFiles(this.existingFiles);
|
||||
this.setFiles((files) => sortFiles([...files, decryptedFile]));
|
||||
}
|
||||
|
||||
|
|
|
@ -55,16 +55,16 @@ export const getUpdateReadyToInstallMessage = (
|
|||
icon: <AutoAwesomeOutlinedIcon />,
|
||||
title: t('UPDATE_AVAILABLE'),
|
||||
content: t('UPDATE_INSTALLABLE_MESSAGE'),
|
||||
proceed: {
|
||||
action: () => ElectronUpdateService.updateAndRestart(),
|
||||
text: t('INSTALL_NOW'),
|
||||
variant: 'accent',
|
||||
},
|
||||
close: {
|
||||
text: t('INSTALL_ON_NEXT_LAUNCH'),
|
||||
variant: 'secondary',
|
||||
action: () => ElectronUpdateService.updateAndRestart(),
|
||||
},
|
||||
proceed: {
|
||||
action: () =>
|
||||
ElectronUpdateService.muteUpdateNotification(updateInfo.version),
|
||||
text: t('INSTALL_NOW'),
|
||||
variant: 'accent',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
21
yarn.lock
21
yarn.lock
|
@ -979,6 +979,13 @@
|
|||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-virtualized-auto-sizer@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz#b3187dae1dfc4c15880c9cfc5b45f2719ea6ebd4"
|
||||
integrity sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-window-infinite-loader@^1.0.3":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmjs.org/@types/react-window-infinite-loader/-/react-window-infinite-loader-1.0.5.tgz"
|
||||
|
@ -3447,6 +3454,11 @@ md5.js@^1.3.4:
|
|||
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz"
|
||||
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
||||
|
||||
memoize-one@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
|
||||
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
|
||||
|
||||
merge-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz"
|
||||
|
@ -3606,14 +3618,7 @@ node-fetch@2.6.7, node-fetch@~2.6.1:
|
|||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-fetch@^2.6.7:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
|
||||
integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-fetch@^2.6.1:
|
||||
node-fetch@^2.6.1, node-fetch@^2.6.7:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
|
||||
integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==
|
||||
|
|
Loading…
Reference in a new issue