Merge branch 'main' into refactor-download-manager
This commit is contained in:
commit
2765df7547
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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": "是的,停止上传",
|
||||
|
|
|
@ -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}
|
||||
|
|
365
apps/photos/src/components/PhotoList/dedupe.tsx
Normal file
365
apps/photos/src/components/PhotoList/dedupe.tsx
Normal file
|
@ -0,0 +1,365 @@
|
|||
import React, { useRef, useEffect, useState, useMemo } from 'react';
|
||||
import {
|
||||
VariableSizeList as List,
|
||||
ListChildComponentProps,
|
||||
areEqual,
|
||||
} from 'react-window';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { EnteFile } from 'types/file';
|
||||
import {
|
||||
IMAGE_CONTAINER_MAX_HEIGHT,
|
||||
MIN_COLUMNS,
|
||||
DATE_CONTAINER_HEIGHT,
|
||||
GAP_BTW_TILES,
|
||||
SPACE_BTW_DATES,
|
||||
SIZE_AND_COUNT_CONTAINER_HEIGHT,
|
||||
IMAGE_CONTAINER_MAX_WIDTH,
|
||||
} from 'constants/gallery';
|
||||
import { convertBytesToHumanReadable } from '@ente/shared/utils/size';
|
||||
import { FlexWrapper } from '@ente/shared/components/Container';
|
||||
import { t } from 'i18next';
|
||||
import memoize from 'memoize-one';
|
||||
import { Duplicate } from 'services/deduplicationService';
|
||||
|
||||
export enum ITEM_TYPE {
|
||||
TIME = 'TIME',
|
||||
FILE = 'FILE',
|
||||
SIZE_AND_COUNT = 'SIZE_AND_COUNT',
|
||||
HEADER = 'HEADER',
|
||||
FOOTER = 'FOOTER',
|
||||
MARKETING_FOOTER = 'MARKETING_FOOTER',
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
|
||||
export interface TimeStampListItem {
|
||||
itemType: ITEM_TYPE;
|
||||
items?: EnteFile[];
|
||||
itemStartIndex?: number;
|
||||
date?: string;
|
||||
dates?: {
|
||||
date: string;
|
||||
span: number;
|
||||
}[];
|
||||
groups?: number[];
|
||||
item?: any;
|
||||
id?: string;
|
||||
height?: number;
|
||||
fileSize?: number;
|
||||
fileCount?: number;
|
||||
}
|
||||
|
||||
const ListItem = styled('div')`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const getTemplateColumns = (
|
||||
columns: number,
|
||||
shrinkRatio: number,
|
||||
groups?: number[]
|
||||
): string => {
|
||||
if (groups) {
|
||||
// need to confirm why this was there
|
||||
// const sum = groups.reduce((acc, item) => acc + item, 0);
|
||||
// if (sum < columns) {
|
||||
// groups[groups.length - 1] += columns - sum;
|
||||
// }
|
||||
return groups
|
||||
.map(
|
||||
(x) =>
|
||||
`repeat(${x}, ${IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio}px)`
|
||||
)
|
||||
.join(` ${SPACE_BTW_DATES}px `);
|
||||
} else {
|
||||
return `repeat(${columns},${
|
||||
IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio
|
||||
}px)`;
|
||||
}
|
||||
};
|
||||
|
||||
function getFractionFittableColumns(width: number): number {
|
||||
return (
|
||||
(width - 2 * getGapFromScreenEdge(width) + GAP_BTW_TILES) /
|
||||
(IMAGE_CONTAINER_MAX_WIDTH + GAP_BTW_TILES)
|
||||
);
|
||||
}
|
||||
|
||||
function getGapFromScreenEdge(width: number) {
|
||||
if (width > MIN_COLUMNS * IMAGE_CONTAINER_MAX_WIDTH) {
|
||||
return 24;
|
||||
} else {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
function getShrinkRatio(width: number, columns: number) {
|
||||
return (
|
||||
(width -
|
||||
2 * getGapFromScreenEdge(width) -
|
||||
(columns - 1) * GAP_BTW_TILES) /
|
||||
(columns * IMAGE_CONTAINER_MAX_WIDTH)
|
||||
);
|
||||
}
|
||||
|
||||
const ListContainer = styled(Box)<{
|
||||
columns: number;
|
||||
shrinkRatio: number;
|
||||
groups?: number[];
|
||||
}>`
|
||||
display: grid;
|
||||
grid-template-columns: ${({ columns, shrinkRatio, groups }) =>
|
||||
getTemplateColumns(columns, shrinkRatio, groups)};
|
||||
grid-column-gap: ${GAP_BTW_TILES}px;
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
padding: 0 24px;
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ListItemContainer = styled(FlexWrapper)<{ span: number }>`
|
||||
grid-column: span ${(props) => props.span};
|
||||
`;
|
||||
|
||||
const DateContainer = styled(ListItemContainer)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: ${DATE_CONTAINER_HEIGHT}px;
|
||||
color: ${({ theme }) => theme.colors.text.muted};
|
||||
`;
|
||||
|
||||
const SizeAndCountContainer = styled(DateContainer)`
|
||||
margin-top: 1rem;
|
||||
height: ${SIZE_AND_COUNT_CONTAINER_HEIGHT}px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
height: number;
|
||||
width: number;
|
||||
duplicates: Duplicate[];
|
||||
showAppDownloadBanner: boolean;
|
||||
getThumbnail: (
|
||||
file: EnteFile,
|
||||
index: number,
|
||||
isScrolling?: boolean
|
||||
) => JSX.Element;
|
||||
activeCollectionID: number;
|
||||
}
|
||||
|
||||
interface ItemData {
|
||||
timeStampList: TimeStampListItem[];
|
||||
columns: number;
|
||||
shrinkRatio: number;
|
||||
renderListItem: (
|
||||
timeStampListItem: TimeStampListItem,
|
||||
isScrolling?: boolean
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
const createItemData = memoize(
|
||||
(
|
||||
timeStampList: TimeStampListItem[],
|
||||
columns: number,
|
||||
shrinkRatio: number,
|
||||
renderListItem: (
|
||||
timeStampListItem: TimeStampListItem,
|
||||
isScrolling?: boolean
|
||||
) => JSX.Element
|
||||
): ItemData => ({
|
||||
timeStampList,
|
||||
columns,
|
||||
shrinkRatio,
|
||||
renderListItem,
|
||||
})
|
||||
);
|
||||
const PhotoListRow = React.memo(
|
||||
({
|
||||
index,
|
||||
style,
|
||||
isScrolling,
|
||||
data,
|
||||
}: ListChildComponentProps<ItemData>) => {
|
||||
const { timeStampList, columns, shrinkRatio, renderListItem } = data;
|
||||
return (
|
||||
<ListItem style={style}>
|
||||
<ListContainer
|
||||
columns={columns}
|
||||
shrinkRatio={shrinkRatio}
|
||||
groups={timeStampList[index].groups}>
|
||||
{renderListItem(timeStampList[index], isScrolling)}
|
||||
</ListContainer>
|
||||
</ListItem>
|
||||
);
|
||||
},
|
||||
areEqual
|
||||
);
|
||||
|
||||
const getTimeStampListFromDuplicates = (duplicates: Duplicate[], columns) => {
|
||||
const timeStampList: TimeStampListItem[] = [];
|
||||
for (let index = 0; index < duplicates.length; index++) {
|
||||
const dupes = duplicates[index];
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.SIZE_AND_COUNT,
|
||||
fileSize: dupes.size,
|
||||
fileCount: dupes.files.length,
|
||||
});
|
||||
let lastIndex = 0;
|
||||
while (lastIndex < dupes.files.length) {
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.FILE,
|
||||
items: dupes.files.slice(lastIndex, lastIndex + columns),
|
||||
itemStartIndex: index,
|
||||
});
|
||||
lastIndex += columns;
|
||||
}
|
||||
}
|
||||
return timeStampList;
|
||||
};
|
||||
|
||||
export function DedupePhotoList({
|
||||
height,
|
||||
width,
|
||||
duplicates,
|
||||
getThumbnail,
|
||||
activeCollectionID,
|
||||
}: Props) {
|
||||
const [timeStampList, setTimeStampList] = useState<TimeStampListItem[]>([]);
|
||||
const refreshInProgress = useRef(false);
|
||||
const shouldRefresh = useRef(false);
|
||||
const listRef = useRef(null);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const fittableColumns = getFractionFittableColumns(width);
|
||||
let columns = Math.floor(fittableColumns);
|
||||
if (columns < MIN_COLUMNS) {
|
||||
columns = MIN_COLUMNS;
|
||||
}
|
||||
return columns;
|
||||
}, [width]);
|
||||
|
||||
const shrinkRatio = getShrinkRatio(width, columns);
|
||||
const listItemHeight =
|
||||
IMAGE_CONTAINER_MAX_HEIGHT * shrinkRatio + GAP_BTW_TILES;
|
||||
|
||||
const refreshList = () => {
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const main = () => {
|
||||
if (refreshInProgress.current) {
|
||||
shouldRefresh.current = true;
|
||||
return;
|
||||
}
|
||||
refreshInProgress.current = true;
|
||||
const timeStampList = getTimeStampListFromDuplicates(
|
||||
duplicates,
|
||||
columns
|
||||
);
|
||||
setTimeStampList(timeStampList);
|
||||
refreshInProgress.current = false;
|
||||
if (shouldRefresh.current) {
|
||||
shouldRefresh.current = false;
|
||||
setTimeout(main, 0);
|
||||
}
|
||||
};
|
||||
main();
|
||||
}, [columns, duplicates]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshList();
|
||||
}, [timeStampList]);
|
||||
|
||||
const getItemSize = (timeStampList) => (index) => {
|
||||
switch (timeStampList[index].itemType) {
|
||||
case ITEM_TYPE.TIME:
|
||||
return DATE_CONTAINER_HEIGHT;
|
||||
case ITEM_TYPE.SIZE_AND_COUNT:
|
||||
return SIZE_AND_COUNT_CONTAINER_HEIGHT;
|
||||
case ITEM_TYPE.FILE:
|
||||
return listItemHeight;
|
||||
default:
|
||||
return timeStampList[index].height;
|
||||
}
|
||||
};
|
||||
|
||||
const generateKey = (index) => {
|
||||
switch (timeStampList[index].itemType) {
|
||||
case ITEM_TYPE.FILE:
|
||||
return `${timeStampList[index].items[0].id}-${
|
||||
timeStampList[index].items.slice(-1)[0].id
|
||||
}`;
|
||||
default:
|
||||
return `${timeStampList[index].id}-${index}`;
|
||||
}
|
||||
};
|
||||
|
||||
const renderListItem = (
|
||||
listItem: TimeStampListItem,
|
||||
isScrolling: boolean
|
||||
) => {
|
||||
switch (listItem.itemType) {
|
||||
case ITEM_TYPE.SIZE_AND_COUNT:
|
||||
return (
|
||||
<SizeAndCountContainer span={columns}>
|
||||
{listItem.fileCount} {t('FILES')},{' '}
|
||||
{convertBytesToHumanReadable(listItem.fileSize || 0)}{' '}
|
||||
{t('EACH')}
|
||||
</SizeAndCountContainer>
|
||||
);
|
||||
case ITEM_TYPE.FILE: {
|
||||
const ret = listItem.items.map((item, idx) =>
|
||||
getThumbnail(
|
||||
item,
|
||||
listItem.itemStartIndex + idx,
|
||||
isScrolling
|
||||
)
|
||||
);
|
||||
if (listItem.groups) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < listItem.groups.length - 1; i++) {
|
||||
sum = sum + listItem.groups[i];
|
||||
ret.splice(
|
||||
sum,
|
||||
0,
|
||||
<div key={`${listItem.items[0].id}-gap-${i}`} />
|
||||
);
|
||||
sum += 1;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
default:
|
||||
return listItem.item;
|
||||
}
|
||||
};
|
||||
|
||||
if (!timeStampList?.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const itemData = createItemData(
|
||||
timeStampList,
|
||||
columns,
|
||||
shrinkRatio,
|
||||
renderListItem
|
||||
);
|
||||
|
||||
return (
|
||||
<List
|
||||
key={`${activeCollectionID}`}
|
||||
itemData={itemData}
|
||||
ref={listRef}
|
||||
itemSize={getItemSize(timeStampList)}
|
||||
height={height}
|
||||
width={width}
|
||||
itemCount={timeStampList.length}
|
||||
itemKey={generateKey}
|
||||
overscanCount={3}
|
||||
useIsScrolling>
|
||||
{PhotoListRow}
|
||||
</List>
|
||||
);
|
||||
}
|
|
@ -19,14 +19,12 @@ import {
|
|||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import { ENTE_WEBSITE_LINK } from '@ente/shared/constants/urls';
|
||||
import { convertBytesToHumanReadable } from '@ente/shared/utils/size';
|
||||
import { DeduplicateContext } from 'pages/deduplicate';
|
||||
import { FlexWrapper } from '@ente/shared/components/Container';
|
||||
import { Typography } from '@mui/material';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { formatDate } from '@ente/shared/time/format';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { t } from 'i18next';
|
||||
import { areFilesWithFileHashSame, hasFileHash } from 'utils/upload';
|
||||
import memoize from 'memoize-one';
|
||||
|
||||
const A_DAY = 24 * 60 * 60 * 1000;
|
||||
|
@ -261,7 +259,6 @@ export function PhotoList({
|
|||
const publicCollectionGalleryContext = useContext(
|
||||
PublicCollectionGalleryContext
|
||||
);
|
||||
const deduplicateContext = useContext(DeduplicateContext);
|
||||
|
||||
const [timeStampList, setTimeStampList] = useState<TimeStampListItem[]>([]);
|
||||
const refreshInProgress = useRef(false);
|
||||
|
@ -306,9 +303,6 @@ export function PhotoList({
|
|||
}
|
||||
if (galleryContext.isClipSearchResult) {
|
||||
noGrouping(timeStampList);
|
||||
} else if (deduplicateContext.isOnDeduplicatePage) {
|
||||
skipMerge = true;
|
||||
groupByFileSize(timeStampList);
|
||||
} else {
|
||||
groupByTime(timeStampList);
|
||||
}
|
||||
|
@ -345,9 +339,6 @@ export function PhotoList({
|
|||
width,
|
||||
height,
|
||||
displayFiles,
|
||||
deduplicateContext.isOnDeduplicatePage,
|
||||
deduplicateContext.fileSizeMap,
|
||||
deduplicateContext.clubSameTimeFilesOnly,
|
||||
galleryContext.photoListHeader,
|
||||
publicCollectionGalleryContext.photoListHeader,
|
||||
galleryContext.isClipSearchResult,
|
||||
|
@ -420,67 +411,6 @@ export function PhotoList({
|
|||
refreshList();
|
||||
}, [timeStampList]);
|
||||
|
||||
const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
|
||||
let index = 0;
|
||||
while (index < displayFiles.length) {
|
||||
const firstFile = displayFiles[index];
|
||||
const firstFileSize = deduplicateContext.fileSizeMap.get(
|
||||
firstFile.id
|
||||
);
|
||||
const firstFileCreationTime = firstFile.metadata.creationTime;
|
||||
let lastFileIndex = index;
|
||||
|
||||
while (lastFileIndex < displayFiles.length) {
|
||||
const lastFile = displayFiles[lastFileIndex];
|
||||
|
||||
const lastFileSize = deduplicateContext.fileSizeMap.get(
|
||||
lastFile.id
|
||||
);
|
||||
if (lastFileSize !== firstFileSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
const lastFileCreationTime = lastFile.metadata.creationTime;
|
||||
if (
|
||||
deduplicateContext.clubSameTimeFilesOnly &&
|
||||
lastFileCreationTime !== firstFileCreationTime
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
const eitherFileHasFileHash =
|
||||
hasFileHash(lastFile.metadata) ||
|
||||
hasFileHash(firstFile.metadata);
|
||||
if (
|
||||
eitherFileHasFileHash &&
|
||||
!areFilesWithFileHashSame(
|
||||
lastFile.metadata,
|
||||
firstFile.metadata
|
||||
)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
lastFileIndex++;
|
||||
}
|
||||
lastFileIndex--;
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.SIZE_AND_COUNT,
|
||||
fileSize: firstFileSize,
|
||||
fileCount: lastFileIndex - index + 1,
|
||||
});
|
||||
|
||||
while (index <= lastFileIndex) {
|
||||
const tileSize = Math.min(columns, lastFileIndex - index + 1);
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.FILE,
|
||||
items: displayFiles.slice(index, index + tileSize),
|
||||
itemStartIndex: index,
|
||||
});
|
||||
index += tileSize;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const groupByTime = (timeStampList: TimeStampListItem[]) => {
|
||||
let listItemIndex = 0;
|
||||
let currentDate;
|
|
@ -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}
|
||||
|
|
|
@ -1100,6 +1100,7 @@ export default function Gallery() {
|
|||
<GalleryEmptyState openUploader={openUploader} />
|
||||
) : (
|
||||
<PhotoFrame
|
||||
page={PAGES.GALLERY}
|
||||
files={filteredData}
|
||||
syncWithRemote={syncWithRemote}
|
||||
favItemIds={favItemIds}
|
||||
|
|
|
@ -470,6 +470,7 @@ export default function PublicCollectionGallery() {
|
|||
openUploader={openUploader}
|
||||
/>
|
||||
<PhotoFrame
|
||||
page={PAGES.SHARED_ALBUMS}
|
||||
files={publicFiles}
|
||||
syncWithRemote={syncWithRemote}
|
||||
setSelected={() => null}
|
||||
|
|
|
@ -16,12 +16,12 @@ interface DuplicatesResponse {
|
|||
}>;
|
||||
}
|
||||
|
||||
interface DuplicateFiles {
|
||||
export interface Duplicate {
|
||||
files: EnteFile[];
|
||||
size: number;
|
||||
}
|
||||
|
||||
export async function getDuplicateFiles(
|
||||
export async function getDuplicates(
|
||||
files: EnteFile[],
|
||||
collectionNameMap: Map<number, string>
|
||||
) {
|
||||
|
@ -33,7 +33,7 @@ export async function getDuplicateFiles(
|
|||
fileMap.set(file.id, file);
|
||||
}
|
||||
|
||||
let result: DuplicateFiles[] = [];
|
||||
let result: Duplicate[] = [];
|
||||
|
||||
for (const dupe of dupes) {
|
||||
let duplicateFiles: EnteFile[] = [];
|
||||
|
@ -64,8 +64,8 @@ export async function getDuplicateFiles(
|
|||
}
|
||||
}
|
||||
|
||||
function getDupesGroupedBySameFileHashes(dupe: DuplicateFiles) {
|
||||
const result: DuplicateFiles[] = [];
|
||||
function getDupesGroupedBySameFileHashes(dupe: Duplicate) {
|
||||
const result: Duplicate[] = [];
|
||||
|
||||
const fileWithHashes: EnteFile[] = [];
|
||||
const fileWithoutHashes: EnteFile[] = [];
|
||||
|
@ -95,8 +95,8 @@ function getDupesGroupedBySameFileHashes(dupe: DuplicateFiles) {
|
|||
return result;
|
||||
}
|
||||
|
||||
function groupDupesByFileHashes(dupe: DuplicateFiles) {
|
||||
const result: DuplicateFiles[] = [];
|
||||
function groupDupesByFileHashes(dupe: Duplicate) {
|
||||
const result: Duplicate[] = [];
|
||||
|
||||
const filesSortedByFileHash = dupe.files
|
||||
.map((file) => {
|
||||
|
@ -141,51 +141,6 @@ function groupDupesByFileHashes(dupe: DuplicateFiles) {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function clubDuplicatesByTime(dupes: DuplicateFiles[]) {
|
||||
const result: DuplicateFiles[] = [];
|
||||
for (const dupe of dupes) {
|
||||
let files: EnteFile[] = [];
|
||||
const creationTimeCounter = new Map<number, number>();
|
||||
|
||||
let mostFreqCreationTime = 0;
|
||||
let mostFreqCreationTimeCount = 0;
|
||||
for (const file of dupe.files) {
|
||||
const creationTime = file.metadata.creationTime;
|
||||
if (creationTimeCounter.has(creationTime)) {
|
||||
creationTimeCounter.set(
|
||||
creationTime,
|
||||
creationTimeCounter.get(creationTime) + 1
|
||||
);
|
||||
} else {
|
||||
creationTimeCounter.set(creationTime, 1);
|
||||
}
|
||||
if (
|
||||
creationTimeCounter.get(creationTime) >
|
||||
mostFreqCreationTimeCount
|
||||
) {
|
||||
mostFreqCreationTime = creationTime;
|
||||
mostFreqCreationTimeCount =
|
||||
creationTimeCounter.get(creationTime);
|
||||
}
|
||||
|
||||
files.push(file);
|
||||
}
|
||||
|
||||
files = files.filter((file) => {
|
||||
return file.metadata.creationTime === mostFreqCreationTime;
|
||||
});
|
||||
|
||||
if (files.length > 1) {
|
||||
result.push({
|
||||
files,
|
||||
size: dupe.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function fetchDuplicateFileIDs() {
|
||||
try {
|
||||
const response = await HTTPService.get(
|
||||
|
|
|
@ -4,16 +4,18 @@ import { logError } from '@ente/shared/sentry';
|
|||
import { ElectronFile } from 'types/upload';
|
||||
import { CustomError } from '@ente/shared/error';
|
||||
import { convertBytesToHumanReadable } from '@ente/shared/utils/size';
|
||||
import { WorkerSafeElectronService } from '@ente/shared/electron/service';
|
||||
|
||||
class ElectronImageProcessorService {
|
||||
async convertToJPEG(fileBlob: Blob, filename: string): Promise<Blob> {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const inputFileData = new Uint8Array(await fileBlob.arrayBuffer());
|
||||
const convertedFileData = await ElectronAPIs.convertToJPEG(
|
||||
inputFileData,
|
||||
filename
|
||||
);
|
||||
const convertedFileData =
|
||||
await WorkerSafeElectronService.convertToJPEG(
|
||||
inputFileData,
|
||||
filename
|
||||
);
|
||||
addLogLine(
|
||||
`originalFileSize:${convertBytesToHumanReadable(
|
||||
fileBlob?.size
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
export type DeduplicateContextType = {
|
||||
clubSameTimeFilesOnly: boolean;
|
||||
setClubSameTimeFilesOnly: (clubSameTimeFilesOnly: boolean) => void;
|
||||
fileSizeMap: Map<number, number>;
|
||||
isOnDeduplicatePage: boolean;
|
||||
collectionNameMap: Map<number, string>;
|
||||
};
|
||||
|
|
|
@ -140,11 +140,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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ✅');
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -8,6 +8,7 @@ import { setRecoveryKey } from '@ente/accounts/api/user';
|
|||
import { logError } from '@ente/shared/sentry';
|
||||
import isElectron from 'is-electron';
|
||||
import ElectronAPIs from '../electron';
|
||||
import { addLogLine } from '../logging';
|
||||
|
||||
const LOGIN_SUB_KEY_LENGTH = 32;
|
||||
const LOGIN_SUB_KEY_ID = 1;
|
||||
|
@ -104,7 +105,7 @@ export const saveKeyInSessionStore = async (
|
|||
key
|
||||
);
|
||||
setKey(keyType, sessionKeyAttributes);
|
||||
console.log('fromDesktop', fromDesktop);
|
||||
addLogLine('fromDesktop', fromDesktop);
|
||||
if (
|
||||
isElectron() &&
|
||||
!fromDesktop &&
|
||||
|
|
84
packages/shared/electron/service.ts
Normal file
84
packages/shared/electron/service.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import * as Comlink from 'comlink';
|
||||
import { LimitedCache } from '@ente/shared/storage/cacheStorage/types';
|
||||
import {
|
||||
ProxiedWorkerLimitedCache,
|
||||
WorkerSafeElectronClient,
|
||||
} from './worker/client';
|
||||
import { wrap } from 'comlink';
|
||||
import { deserializeToResponse, serializeResponse } from './worker/utils/proxy';
|
||||
import { runningInWorker } from '@ente/shared/platform';
|
||||
import { ElectronAPIsType } from './types';
|
||||
|
||||
export interface LimitedElectronAPIs
|
||||
extends Pick<
|
||||
ElectronAPIsType,
|
||||
| 'openDiskCache'
|
||||
| 'deleteDiskCache'
|
||||
| 'getSentryUserID'
|
||||
| 'convertToJPEG'
|
||||
> {}
|
||||
|
||||
class WorkerSafeElectronServiceImpl implements LimitedElectronAPIs {
|
||||
proxiedElectron:
|
||||
| Comlink.Remote<WorkerSafeElectronClient>
|
||||
| WorkerSafeElectronClient;
|
||||
ready: Promise<any>;
|
||||
|
||||
constructor() {
|
||||
this.ready = this.init();
|
||||
}
|
||||
private async init() {
|
||||
if (runningInWorker()) {
|
||||
const workerSafeElectronClient =
|
||||
wrap<typeof WorkerSafeElectronClient>(self);
|
||||
|
||||
this.proxiedElectron = await new workerSafeElectronClient();
|
||||
} else {
|
||||
this.proxiedElectron = new WorkerSafeElectronClient();
|
||||
}
|
||||
}
|
||||
async openDiskCache(cacheName: string) {
|
||||
await this.ready;
|
||||
const cache = await this.proxiedElectron.openDiskCache(cacheName);
|
||||
return {
|
||||
match: transformMatch(cache.match.bind(cache)),
|
||||
put: transformPut(cache.put.bind(cache)),
|
||||
delete: cache.delete.bind(cache),
|
||||
};
|
||||
}
|
||||
|
||||
async deleteDiskCache(cacheName: string) {
|
||||
await this.ready;
|
||||
return await this.proxiedElectron.deleteDiskCache(cacheName);
|
||||
}
|
||||
|
||||
async getSentryUserID() {
|
||||
await this.ready;
|
||||
return this.proxiedElectron.getSentryUserID();
|
||||
}
|
||||
async convertToJPEG(
|
||||
inputFileData: Uint8Array,
|
||||
filename: string
|
||||
): Promise<Uint8Array> {
|
||||
await this.ready;
|
||||
return this.proxiedElectron.convertToJPEG(inputFileData, filename);
|
||||
}
|
||||
}
|
||||
|
||||
export const WorkerSafeElectronService = new WorkerSafeElectronServiceImpl();
|
||||
|
||||
function transformMatch(
|
||||
fn: ProxiedWorkerLimitedCache['match']
|
||||
): LimitedCache['match'] {
|
||||
return async (key: string) => {
|
||||
return deserializeToResponse(await fn(key));
|
||||
};
|
||||
}
|
||||
|
||||
function transformPut(
|
||||
fn: ProxiedWorkerLimitedCache['put']
|
||||
): LimitedCache['put'] {
|
||||
return async (key: string, data: Response) => {
|
||||
fn(key, await serializeResponse(data));
|
||||
};
|
||||
}
|
61
packages/shared/electron/worker/client.ts
Normal file
61
packages/shared/electron/worker/client.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import * as Comlink from 'comlink';
|
||||
import { LimitedCache } from '@ente/shared/storage/cacheStorage/types';
|
||||
import { serializeResponse, deserializeToResponse } from './utils/proxy';
|
||||
import ElectronAPIs from '@ente/shared/electron';
|
||||
|
||||
export interface ProxiedLimitedElectronAPIs {
|
||||
openDiskCache: (cacheName: string) => Promise<ProxiedWorkerLimitedCache>;
|
||||
deleteDiskCache: (cacheName: string) => Promise<boolean>;
|
||||
getSentryUserID: () => Promise<string>;
|
||||
convertToJPEG: (
|
||||
inputFileData: Uint8Array,
|
||||
filename: string
|
||||
) => Promise<Uint8Array>;
|
||||
}
|
||||
export interface ProxiedWorkerLimitedCache {
|
||||
match: (key: string) => Promise<ArrayBuffer>;
|
||||
put: (key: string, data: ArrayBuffer) => Promise<void>;
|
||||
delete: (key: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export class WorkerSafeElectronClient implements ProxiedLimitedElectronAPIs {
|
||||
async openDiskCache(cacheName: string) {
|
||||
const cache = await ElectronAPIs.openDiskCache(cacheName);
|
||||
return Comlink.proxy({
|
||||
match: Comlink.proxy(transformMatch(cache.match.bind(cache))),
|
||||
put: Comlink.proxy(transformPut(cache.put.bind(cache))),
|
||||
delete: Comlink.proxy(cache.delete.bind(cache)),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteDiskCache(cacheName: string) {
|
||||
return await ElectronAPIs.deleteDiskCache(cacheName);
|
||||
}
|
||||
|
||||
async getSentryUserID() {
|
||||
return await ElectronAPIs.getSentryUserID();
|
||||
}
|
||||
|
||||
async convertToJPEG(
|
||||
inputFileData: Uint8Array,
|
||||
filename: string
|
||||
): Promise<Uint8Array> {
|
||||
return await ElectronAPIs.convertToJPEG(inputFileData, filename);
|
||||
}
|
||||
}
|
||||
|
||||
function transformMatch(
|
||||
fn: LimitedCache['match']
|
||||
): ProxiedWorkerLimitedCache['match'] {
|
||||
return async (key: string) => {
|
||||
return serializeResponse(await fn(key));
|
||||
};
|
||||
}
|
||||
|
||||
function transformPut(
|
||||
fn: LimitedCache['put']
|
||||
): ProxiedWorkerLimitedCache['put'] {
|
||||
return async (key: string, data: ArrayBuffer) => {
|
||||
fn(key, deserializeToResponse(data));
|
||||
};
|
||||
}
|
|
@ -86,6 +86,8 @@ export const CustomError = {
|
|||
ServerError: 'server error',
|
||||
FILE_NOT_FOUND: 'file not found',
|
||||
UNSUPPORTED_PLATFORM: 'Unsupported platform',
|
||||
MODEL_DOWNLOAD_PENDING:
|
||||
'Model download pending, skipping clip search request',
|
||||
DOWNLOAD_MANAGER_NOT_READY: 'Download manager not initialized',
|
||||
};
|
||||
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import ElectronAPIs from '@ente/shared/electron';
|
||||
import { WorkerSafeElectronService } from '@ente/shared/electron/service';
|
||||
import {
|
||||
getLocalSentryUserID,
|
||||
setLocalSentryUserID,
|
||||
|
@ -12,7 +12,7 @@ import { HttpStatusCode } from 'axios';
|
|||
|
||||
export async function getSentryUserID() {
|
||||
if (isElectron()) {
|
||||
return await ElectronAPIs.getSentryUserID();
|
||||
return await WorkerSafeElectronService.getSentryUserID();
|
||||
} else {
|
||||
let anonymizeUserID = getLocalSentryUserID();
|
||||
if (!anonymizeUserID) {
|
||||
|
|
|
@ -1,28 +1,17 @@
|
|||
import { LimitedCacheStorage } from './types';
|
||||
import { runningInElectron, runningInWorker } from '@ente/shared/platform';
|
||||
import { WorkerElectronCacheStorageService } from './workerElectron/service';
|
||||
import ElectronAPIs from '@ente/shared/electron';
|
||||
|
||||
import { runningInElectron } from '@ente/shared/platform';
|
||||
import { WorkerSafeElectronService } from '@ente/shared/electron/service';
|
||||
class cacheStorageFactory {
|
||||
workerElectronCacheStorageServiceInstance: WorkerElectronCacheStorageService;
|
||||
getCacheStorage(): LimitedCacheStorage {
|
||||
if (runningInElectron()) {
|
||||
if (runningInWorker()) {
|
||||
if (!this.workerElectronCacheStorageServiceInstance) {
|
||||
this.workerElectronCacheStorageServiceInstance =
|
||||
new WorkerElectronCacheStorageService();
|
||||
}
|
||||
return this.workerElectronCacheStorageServiceInstance;
|
||||
} else {
|
||||
return {
|
||||
open(cacheName) {
|
||||
return ElectronAPIs.openDiskCache(cacheName);
|
||||
},
|
||||
delete(cacheName) {
|
||||
return ElectronAPIs.deleteDiskCache(cacheName);
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
open(cacheName) {
|
||||
return WorkerSafeElectronService.openDiskCache(cacheName);
|
||||
},
|
||||
delete(cacheName) {
|
||||
return WorkerSafeElectronService.deleteDiskCache(cacheName);
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return transformBrowserCacheStorageToLimitedCacheStorage(caches);
|
||||
}
|
||||
|
|
|
@ -8,13 +8,3 @@ export interface LimitedCache {
|
|||
put: (key: string, data: Response) => Promise<void>;
|
||||
delete: (key: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface ProxiedLimitedCacheStorage {
|
||||
open: (cacheName: string) => Promise<ProxiedWorkerLimitedCache>;
|
||||
delete: (cacheName: string) => Promise<boolean>;
|
||||
}
|
||||
export interface ProxiedWorkerLimitedCache {
|
||||
match: (key: string) => Promise<ArrayBuffer>;
|
||||
put: (key: string, data: ArrayBuffer) => Promise<void>;
|
||||
delete: (key: string) => Promise<boolean>;
|
||||
}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
import * as Comlink from 'comlink';
|
||||
import {
|
||||
LimitedCache,
|
||||
ProxiedLimitedCacheStorage,
|
||||
ProxiedWorkerLimitedCache,
|
||||
} from '@ente/shared/storage/cacheStorage/types';
|
||||
import { serializeResponse, deserializeToResponse } from './utils/proxy';
|
||||
import ElectronAPIs from '@ente/shared/electron';
|
||||
|
||||
export class WorkerElectronCacheStorageClient
|
||||
implements ProxiedLimitedCacheStorage
|
||||
{
|
||||
async open(cacheName: string) {
|
||||
const cache = await ElectronAPIs.openDiskCache(cacheName);
|
||||
return Comlink.proxy({
|
||||
match: Comlink.proxy(transformMatch(cache.match.bind(cache))),
|
||||
put: Comlink.proxy(transformPut(cache.put.bind(cache))),
|
||||
delete: Comlink.proxy(cache.delete.bind(cache)),
|
||||
});
|
||||
}
|
||||
|
||||
async delete(cacheName: string) {
|
||||
return await ElectronAPIs.deleteDiskCache(cacheName);
|
||||
}
|
||||
}
|
||||
|
||||
function transformMatch(
|
||||
fn: LimitedCache['match']
|
||||
): ProxiedWorkerLimitedCache['match'] {
|
||||
return async (key: string) => {
|
||||
return serializeResponse(await fn(key));
|
||||
};
|
||||
}
|
||||
|
||||
function transformPut(
|
||||
fn: LimitedCache['put']
|
||||
): ProxiedWorkerLimitedCache['put'] {
|
||||
return async (key: string, data: ArrayBuffer) => {
|
||||
fn(key, deserializeToResponse(data));
|
||||
};
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
import * as Comlink from 'comlink';
|
||||
import {
|
||||
LimitedCache,
|
||||
LimitedCacheStorage,
|
||||
ProxiedWorkerLimitedCache,
|
||||
} from '../types';
|
||||
import { WorkerElectronCacheStorageClient } from './client';
|
||||
import { wrap } from 'comlink';
|
||||
import { deserializeToResponse, serializeResponse } from './utils/proxy';
|
||||
|
||||
export class WorkerElectronCacheStorageService implements LimitedCacheStorage {
|
||||
proxiedElectronCacheService: Comlink.Remote<WorkerElectronCacheStorageClient>;
|
||||
ready: Promise<any>;
|
||||
|
||||
constructor() {
|
||||
this.ready = this.init();
|
||||
}
|
||||
async init() {
|
||||
const electronCacheStorageProxy =
|
||||
wrap<typeof WorkerElectronCacheStorageClient>(self);
|
||||
|
||||
this.proxiedElectronCacheService =
|
||||
await new electronCacheStorageProxy();
|
||||
}
|
||||
async open(cacheName: string) {
|
||||
await this.ready;
|
||||
const cache = await this.proxiedElectronCacheService.open(cacheName);
|
||||
return {
|
||||
match: transformMatch(cache.match.bind(cache)),
|
||||
put: transformPut(cache.put.bind(cache)),
|
||||
delete: cache.delete.bind(cache),
|
||||
};
|
||||
}
|
||||
|
||||
async delete(cacheName: string) {
|
||||
await this.ready;
|
||||
return await this.proxiedElectronCacheService.delete(cacheName);
|
||||
}
|
||||
}
|
||||
|
||||
function transformMatch(
|
||||
fn: ProxiedWorkerLimitedCache['match']
|
||||
): LimitedCache['match'] {
|
||||
return async (key: string) => {
|
||||
return deserializeToResponse(await fn(key));
|
||||
};
|
||||
}
|
||||
|
||||
function transformPut(
|
||||
fn: ProxiedWorkerLimitedCache['put']
|
||||
): LimitedCache['put'] {
|
||||
return async (key: string, data: Response) => {
|
||||
fn(key, await serializeResponse(data));
|
||||
};
|
||||
}
|
|
@ -14,6 +14,8 @@ interface DateComponent<T = number> {
|
|||
second: T;
|
||||
}
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
export function getUnixTimeInMicroSecondsWithDelta(delta: TimeDelta): number {
|
||||
let currentDate = new Date();
|
||||
if (delta?.hours) {
|
||||
|
@ -112,7 +114,8 @@ function getDateComponentsFromSymbolJoinedString(
|
|||
}
|
||||
|
||||
function validateAndGetDateFromComponents(
|
||||
dateComponent: DateComponent<number>
|
||||
dateComponent: DateComponent<number>,
|
||||
options = { minYear: 1990, maxYear: currentYear + 1 }
|
||||
) {
|
||||
let date = getDateFromComponents(dateComponent);
|
||||
if (hasTimeValues(dateComponent) && !isTimePartValid(date, dateComponent)) {
|
||||
|
@ -123,6 +126,12 @@ function validateAndGetDateFromComponents(
|
|||
if (!isDatePartValid(date, dateComponent)) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
date.getFullYear() < options.minYear ||
|
||||
date.getFullYear() > options.maxYear
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { expose, Remote, wrap } from 'comlink';
|
||||
import { WorkerElectronCacheStorageClient } from '@ente/shared/storage/cacheStorage/workerElectron/client';
|
||||
import { WorkerSafeElectronClient } from '@ente/shared/electron/worker/client';
|
||||
import { addLocalLog } from '@ente/shared/logging';
|
||||
|
||||
export class ComlinkWorker<T extends new () => InstanceType<T>> {
|
||||
|
@ -17,7 +17,7 @@ export class ComlinkWorker<T extends new () => InstanceType<T>> {
|
|||
addLocalLog(() => `Initiated ${this.name}`);
|
||||
const comlink = wrap<T>(this.worker);
|
||||
this.remote = new comlink() as Promise<Remote<InstanceType<T>>>;
|
||||
expose(WorkerElectronCacheStorageClient, this.worker);
|
||||
expose(WorkerSafeElectronClient, this.worker);
|
||||
}
|
||||
|
||||
public getName() {
|
||||
|
|
Loading…
Reference in a new issue