Merge branch 'gallery-refactor' into hidden-support

This commit is contained in:
Abhinav 2023-05-24 13:57:47 +05:30
commit ff66abce74
30 changed files with 399 additions and 165 deletions

View file

@ -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": {

View file

@ -490,6 +490,7 @@
"PUBLIC_COLLECT_SUBTEXT": "",
"STOP_EXPORT": "",
"EXPORT_PROGRESS": "",
"MIGRATING_EXPORT": "",
"EXPORT_NOTIFICATION": {
"START": "",
"IN_PROGRESS": "",

View file

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

View file

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

View file

@ -490,6 +490,7 @@
"PUBLIC_COLLECT_SUBTEXT": "",
"STOP_EXPORT": "",
"EXPORT_PROGRESS": "",
"MIGRATING_EXPORT": "",
"EXPORT_NOTIFICATION": {
"START": "",
"IN_PROGRESS": "",

View file

@ -490,6 +490,7 @@
"PUBLIC_COLLECT_SUBTEXT": "",
"STOP_EXPORT": "",
"EXPORT_PROGRESS": "",
"MIGRATING_EXPORT": "",
"EXPORT_NOTIFICATION": {
"START": "",
"IN_PROGRESS": "",

View file

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

View file

@ -490,6 +490,7 @@
"PUBLIC_COLLECT_SUBTEXT": "",
"STOP_EXPORT": "",
"EXPORT_PROGRESS": "",
"MIGRATING_EXPORT": "",
"EXPORT_NOTIFICATION": {
"START": "",
"IN_PROGRESS": "",

View file

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

View file

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

View file

@ -490,6 +490,7 @@
"PUBLIC_COLLECT_SUBTEXT": "",
"STOP_EXPORT": "",
"EXPORT_PROGRESS": "",
"MIGRATING_EXPORT": "",
"EXPORT_NOTIFICATION": {
"START": "",
"IN_PROGRESS": "",

View file

@ -490,6 +490,7 @@
"PUBLIC_COLLECT_SUBTEXT": "",
"STOP_EXPORT": "",
"EXPORT_PROGRESS": "",
"MIGRATING_EXPORT": "",
"EXPORT_NOTIFICATION": {
"START": "",
"IN_PROGRESS": "",

View file

@ -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": "导出已在进行中",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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'}

View file

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

View file

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

View file

@ -5,5 +5,6 @@ export const ENTE_TRASH_FOLDER = 'Trash';
export enum ExportStage {
INIT = 0,
INPROGRESS = 1,
MIGRATION = 2,
FINISHED = 3,
}

View file

@ -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';

View file

@ -61,6 +61,6 @@ export default function useComponentScroll({
onFarLeft,
onFarRight,
scrollComponent,
componentRef: componentRef,
componentRef,
};
}

View file

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

View file

@ -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;
}

View file

@ -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]));
}

View file

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

View file

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