Merge branch 'main' into refactor-download-manager

This commit is contained in:
Abhinav 2023-12-07 09:22:10 +05:30
commit 2765df7547
39 changed files with 642 additions and 332 deletions

View file

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

View file

@ -38,7 +38,7 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generating encryption keys...",
"PASSPHRASE_HINT": "Password",
"CONFIRM_PASSPHRASE": "Confirm password",
"REFERRAL_CODE_HINT":"How did you hear abut Ente? (optional)",
"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!",
@ -425,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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -38,8 +38,8 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Encryptiecodes worden gegenereerd...",
"PASSPHRASE_HINT": "Wachtwoord",
"CONFIRM_PASSPHRASE": "Wachtwoord bevestigen",
"REFERRAL_CODE_HINT": "",
"REFERRAL_INFO": "",
"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.",
@ -159,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",
@ -174,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",
@ -425,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",

View file

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

View file

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

View file

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

View file

@ -425,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": "是的,停止上传",

View file

@ -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 { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { useRouter } from 'next/router';
@ -18,6 +17,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;
@ -35,7 +38,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: (
@ -54,6 +62,8 @@ interface Props {
}
const PhotoFrame = ({
page,
duplicates,
files,
syncWithRemote,
favItemIds,
@ -569,16 +579,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}

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

View file

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

View file

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

View file

@ -1100,6 +1100,7 @@ export default function Gallery() {
<GalleryEmptyState openUploader={openUploader} />
) : (
<PhotoFrame
page={PAGES.GALLERY}
files={filteredData}
syncWithRemote={syncWithRemote}
favItemIds={favItemIds}

View file

@ -470,6 +470,7 @@ export default function PublicCollectionGallery() {
openUploader={openUploader}
/>
<PhotoFrame
page={PAGES.SHARED_ALBUMS}
files={publicFiles}
syncWithRemote={syncWithRemote}
setSelected={() => null}

View file

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

View file

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

View 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']);
@ -302,7 +303,9 @@ async function getClipSuggestion(searchPhrase: string): Promise<Suggestion> {
label: searchPhrase,
};
} catch (e) {
logError(e, 'getClipSuggestion failed');
if (!e.message?.includes(CustomError.MODEL_DOWNLOAD_PENDING)) {
logError(e, 'getClipSuggestion failed');
}
return null;
}
}

View file

@ -1,7 +1,4 @@
export type DeduplicateContextType = {
clubSameTimeFilesOnly: boolean;
setClubSameTimeFilesOnly: (clubSameTimeFilesOnly: boolean) => void;
fileSizeMap: Map<number, number>;
isOnDeduplicatePage: boolean;
collectionNameMap: Map<number, string>;
};

View file

@ -140,11 +140,14 @@ 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
);
}
}

View file

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

View file

@ -1,7 +1,7 @@
import { APP_ENV } from './constants';
export const getAppEnv = () =>
process.env.NEXT_PUBLIC_APP_ENV ?? APP_ENV.DEVELOPMENT;
process.env.NEXT_PUBLIC_APP_ENV ?? APP_ENV.PRODUCTION;
export const isDisableSentryFlagSet = () => {
return process.env.NEXT_PUBLIC_DISABLE_SENTRY === 'true';

View file

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

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

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

View file

@ -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',
DOWNLOAD_MANAGER_NOT_READY: 'Download manager not initialized',
};

View file

@ -9,7 +9,7 @@ module.exports = {
};
module.exports.getAppEnv = () => {
return process.env.NEXT_PUBLIC_APP_ENV ?? ENV_DEVELOPMENT;
return process.env.NEXT_PUBLIC_APP_ENV ?? ENV_PRODUCTION;
};
module.exports.isDisableSentryFlagSet = () => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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() {