move deduplicate logic to seperate page

This commit is contained in:
Abhinav 2022-04-05 14:26:42 +05:30
parent 3210474a2f
commit 6622016df4
16 changed files with 318 additions and 252 deletions

View file

@ -1,20 +0,0 @@
import React from 'react';
import { IconButton } from './Container';
import LeftArrow from './icons/LeftArrow';
export default function BackButton({ setIsDeduplicating }) {
return (
<IconButton
style={{
position: 'absolute',
top: '1em',
left: '1em',
zIndex: 10,
}}
onClick={() => {
setIsDeduplicating(false);
}}>
<LeftArrow />
</IconButton>
);
}

View file

@ -1,43 +0,0 @@
import { GalleryContext } from 'pages/gallery';
import React, { useContext } from 'react';
import styled from 'styled-components';
import constants from 'utils/strings/constants';
const Wrapper = styled.div`
position: fixed;
display: flex;
align-items: center;
justify-content: center;
top: 0;
z-index: 1002;
min-height: 64px;
right: 64px;
`;
export default function ClubDuplicateFilesByTime() {
const galleryContext = useContext(GalleryContext);
return (
<Wrapper>
<input
type="checkbox"
style={{
width: '1em',
height: '1em',
}}
value={galleryContext.clubSameTimeFilesOnly ? 'true' : 'false'}
onChange={() => {
galleryContext.setClubSameTimeFilesOnly(
!galleryContext.clubSameTimeFilesOnly
);
}}></input>
<div
style={{
marginLeft: '0.5em',
fontSize: '16px',
marginRight: '0.8em',
}}>
{constants.CLUB_BY_CAPTURE_TIME}
</div>
</Wrapper>
);
}

View file

@ -1,7 +1,7 @@
import React from 'react';
import styled from 'styled-components';
import constants from 'utils/strings/constants';
import { IconWithMessage } from './pages/gallery/SelectedFileOptions';
import { IconWithMessage } from './pages/gallery/SelectedFileOptions/GalleryOptions';
const Wrapper = styled.button`
border: none;

View file

@ -2,7 +2,7 @@ import React, { useContext } from 'react';
import { Button } from 'react-bootstrap';
import styled from 'styled-components';
import constants from 'utils/strings/constants';
import { GalleryContext } from 'pages/gallery';
import { DeduplicateContext } from 'pages/deduplicate';
const Wrapper = styled.div`
display: flex;
@ -18,10 +18,10 @@ const Wrapper = styled.div`
`;
export default function EmptyScreen({ openFileUploader }) {
const galleryContext = useContext(GalleryContext);
const deduplicateContext = useContext(DeduplicateContext);
return (
<Wrapper>
{galleryContext.isDeduplicating ? (
{deduplicateContext.isOnDeduplicatePage ? (
<b
style={{
fontSize: '2em',

View file

@ -24,6 +24,7 @@ import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { useRouter } from 'next/router';
import EmptyScreen from './EmptyScreen';
import { AppContext } from 'pages/_app';
import { DeduplicateContext } from 'pages/deduplicate';
const Container = styled.div`
display: block;
@ -44,20 +45,20 @@ interface Props {
files: EnteFile[];
setFiles: SetFiles;
syncWithRemote: () => Promise<void>;
favItemIds: Set<number>;
favItemIds?: Set<number>;
setSelected: (
selected: SelectedState | ((selected: SelectedState) => SelectedState)
) => void;
selected: SelectedState;
isFirstLoad;
isFirstLoad?;
openFileUploader?;
isInSearchMode: boolean;
isInSearchMode?: boolean;
search?: Search;
setSearchStats?: setSearchStats;
deleted?: number[];
activeCollection: number;
isSharedCollection: boolean;
enableDownload: boolean;
isSharedCollection?: boolean;
enableDownload?: boolean;
}
type SourceURL = {
@ -88,6 +89,7 @@ const PhotoFrame = ({
const startTime = Date.now();
const galleryContext = useContext(GalleryContext);
const appContext = useContext(AppContext);
const deduplicateContext = useContext(DeduplicateContext);
const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext
);
@ -171,7 +173,7 @@ const PhotoFrame = ({
}),
}))
.filter((item) => {
if (deleted.includes(item.id)) {
if (deleted?.includes(item.id)) {
return false;
}
if (
@ -550,7 +552,7 @@ const PhotoFrame = ({
showAppDownloadBanner={
files.length < 30 &&
!isInSearchMode &&
!galleryContext.isDeduplicating
!deduplicateContext.isOnDeduplicatePage
}
resetFetching={resetFetching}
/>

View file

@ -15,8 +15,8 @@ import constants from 'utils/strings/constants';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { ENTE_WEBSITE_LINK } from 'constants/urls';
import { getVariantColor, ButtonVariant } from './pages/gallery/LinkButton';
import { GalleryContext } from 'pages/gallery';
import { convertBytesToHumanReadable } from 'utils/billing';
import { DeduplicateContext } from 'pages/deduplicate';
const A_DAY = 24 * 60 * 60 * 1000;
const NO_OF_PAGES = 2;
@ -159,7 +159,7 @@ export function PhotoList({
const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext
);
const galleryContext = useContext(GalleryContext);
const deduplicateContext = useContext(DeduplicateContext);
let columns = Math.floor(width / IMAGE_CONTAINER_MAX_WIDTH);
let listItemHeight = IMAGE_CONTAINER_MAX_HEIGHT;
@ -178,16 +178,13 @@ export function PhotoList({
useEffect(() => {
let timeStampList: TimeStampListItem[] = [];
if (galleryContext.isDeduplicating) {
if (deduplicateContext.isOnDeduplicatePage) {
skipMerge = true;
groupByFileSize(timeStampList);
} else {
groupByTime(timeStampList);
}
if (galleryContext.isDeduplicating) {
skipMerge = true;
}
if (!skipMerge) {
timeStampList = mergeTimeStampList(timeStampList, columns);
}
@ -221,16 +218,16 @@ export function PhotoList({
let index = 0;
while (index < filteredData.length) {
const file = filteredData[index];
const currentFileSize = galleryContext.fileSizeMap.get(file.id);
const currentFileSize = deduplicateContext.fileSizeMap.get(file.id);
const currentCreationTime = file.metadata.creationTime;
let lastFileIndex = index;
while (lastFileIndex < filteredData.length) {
if (
galleryContext.fileSizeMap.get(
deduplicateContext.fileSizeMap.get(
filteredData[lastFileIndex].id
) !== currentFileSize ||
(galleryContext.clubSameTimeFilesOnly &&
(deduplicateContext.clubSameTimeFilesOnly &&
filteredData[lastFileIndex].metadata.creationTime !==
currentCreationTime)
) {

View file

@ -290,7 +290,7 @@ export default function Sidebar(props: Props) {
<LinkButton
style={{ marginTop: '30px' }}
onClick={() => {
galleryContext.setIsDeduplicating(true);
router.push(PAGES.DEDUPLICATE);
}}>
{constants.DEDUPLICATE_FILES}
</LinkButton>

View file

@ -5,7 +5,7 @@ import { OverlayTrigger } from 'react-bootstrap';
import { COLLECTION_SORT_BY } from 'constants/collection';
import constants from 'utils/strings/constants';
import CollectionSortOptions from './CollectionSortOptions';
import { IconWithMessage } from './SelectedFileOptions';
import { IconWithMessage } from './SelectedFileOptions/GalleryOptions';
interface Props {
setCollectionSortBy: (sortBy: COLLECTION_SORT_BY) => void;

View file

@ -0,0 +1,99 @@
import { IconButton } from 'components/Container';
import {
IconWithMessage,
SelectionBar,
SelectionContainer,
} from './GalleryOptions';
import constants from 'utils/strings/constants';
import DeleteIcon from 'components/icons/DeleteIcon';
import React, { useContext } from 'react';
import styled from 'styled-components';
import { DeduplicateContext } from 'pages/deduplicate';
import CloseIcon from 'components/icons/CloseIcon';
import LeftArrow from 'components/icons/LeftArrow';
import { SetDialogMessage } from 'components/MessageDialog';
const VerticalLine = styled.div`
position: absolute;
width: 1px;
top: 0;
bottom: 0;
background: #303030;
`;
interface IProps {
deleteFileHelper: () => void;
setDialogMessage: SetDialogMessage;
close: () => void;
count: number;
}
export default function DeduplicateOptions({
setDialogMessage,
deleteFileHelper,
close,
count,
}: IProps) {
const deduplicateContext = useContext(DeduplicateContext);
const trashHandler = () =>
setDialogMessage({
title: constants.CONFIRM_DELETE,
content: constants.TRASH_MESSAGE,
staticBackdrop: true,
proceed: {
action: deleteFileHelper,
text: constants.MOVE_TO_TRASH,
variant: 'danger',
},
close: { text: constants.CANCEL },
});
return (
<SelectionBar>
<SelectionContainer>
<IconButton onClick={close}>
{deduplicateContext.isOnDeduplicatePage ? (
<LeftArrow />
) : (
<CloseIcon />
)}
</IconButton>
<div>
{count} {constants.SELECTED}
</div>
</SelectionContainer>
<input
type="checkbox"
style={{
width: '1em',
height: '1em',
}}
value={
deduplicateContext.clubSameTimeFilesOnly ? 'true' : 'false'
}
onChange={() => {
deduplicateContext.setClubSameTimeFilesOnly(
!deduplicateContext.clubSameTimeFilesOnly
);
}}></input>
<div
style={{
marginLeft: '0.5em',
fontSize: '16px',
marginRight: '0.8em',
}}>
{constants.CLUB_BY_CAPTURE_TIME}
</div>
<div>
<VerticalLine />
</div>
<IconWithMessage message={constants.DELETE}>
<IconButton onClick={trashHandler}>
<DeleteIcon />
</IconButton>
</IconWithMessage>
</SelectionBar>
);
}

View file

@ -1,29 +0,0 @@
import { IconButton } from 'components/Container';
import { IconWithMessage } from '.';
import constants from 'utils/strings/constants';
import DeleteIcon from 'components/icons/DeleteIcon';
import React from 'react';
import styled from 'styled-components';
const VerticalLine = styled.div`
position: absolute;
width: 1px;
top: 0;
bottom: 0;
background: #303030;
`;
export default function DeduplicatingOptions({ trashHandler }) {
return (
<>
<div>
<VerticalLine />
</div>
<IconWithMessage message={constants.DELETE}>
<IconButton onClick={trashHandler}>
<DeleteIcon />
</IconButton>
</IconWithMessage>
</>
);
}

View file

@ -1,5 +1,5 @@
import { SetDialogMessage } from 'components/MessageDialog';
import React, { useEffect, useState, useContext } from 'react';
import React, { useEffect, useState } from 'react';
import { SetCollectionSelectorAttributes } from '../CollectionSelector';
import styled from 'styled-components';
import Navbar from 'components/Navbar';
@ -17,7 +17,6 @@ import {
TRASH_SECTION,
} from 'constants/collection';
import UnArchive from 'components/icons/UnArchive';
import { OverlayTrigger } from 'react-bootstrap';
import { Collection } from 'types/collection';
import RemoveIcon from 'components/icons/RemoveIcon';
import RestoreIcon from 'components/icons/RestoreIcon';
@ -26,8 +25,7 @@ import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { FIX_CREATION_TIME_VISIBLE_TO_USER_IDS } from 'constants/user';
import DownloadIcon from 'components/icons/DownloadIcon';
import { User } from 'types/user';
import { GalleryContext } from 'pages/gallery';
import DeduplicatingOptions from './DeduplicatingOptions';
import { OverlayTrigger } from 'react-bootstrap';
interface Props {
addToCollectionHelper?: (collection: Collection) => void;
@ -48,7 +46,7 @@ interface Props {
isFavoriteCollection?: boolean;
}
const SelectionBar = styled(Navbar)`
export const SelectionBar = styled(Navbar)`
position: fixed;
top: 0;
color: #fff;
@ -57,7 +55,7 @@ const SelectionBar = styled(Navbar)`
padding: 0 16px;
`;
const SelectionContainer = styled.div`
export const SelectionContainer = styled.div`
flex: 1;
align-items: center;
display: flex;
@ -67,6 +65,7 @@ interface IconWithMessageProps {
children?: any;
message: string;
}
export const IconWithMessage = (props: IconWithMessageProps) => (
<OverlayTrigger
placement="bottom"
@ -94,7 +93,6 @@ const SelectedFileOptions = ({
isFavoriteCollection,
}: Props) => {
const [showFixCreationTime, setShowFixCreationTime] = useState(false);
const galleryContext = useContext(GalleryContext);
useEffect(() => {
const user: User = getData(LS_KEYS.USER);
@ -177,9 +175,7 @@ const SelectedFileOptions = ({
{count} {constants.SELECTED}
</div>
</SelectionContainer>
{galleryContext.isDeduplicating ? (
<DeduplicatingOptions trashHandler={trashHandler} />
) : activeCollection === TRASH_SECTION ? (
{activeCollection === TRASH_SECTION ? (
<>
<IconWithMessage message={constants.RESTORE}>
<IconButton onClick={restoreHandler}>

View file

@ -13,6 +13,7 @@ export enum PAGES {
VERIFY = '/verify',
ROOT = '/',
SHARED_ALBUMS = '/shared-albums',
DEDUPLICATE = '/deduplicate',
}
export const getAlbumSiteHost = () =>
process.env.NODE_ENV === 'production'

View file

@ -0,0 +1,142 @@
import constants from 'utils/strings/constants';
import PhotoFrame from 'components/PhotoFrame';
import { ALL_SECTION } from 'constants/collection';
import { AppContext } from 'pages/_app';
import React, { createContext, useContext, useEffect, useState } from 'react';
import {
getDuplicateFiles,
clubDuplicatesByTime,
} from 'services/deduplicationService';
import { trashFiles } from 'services/fileService';
import { EnteFile } from 'types/file';
import { SelectedState } from 'types/gallery';
import { ServerErrorCodes } from 'utils/error';
import { getSelectedFiles } from 'utils/file';
import {
DeduplicateContextType,
DefaultDeduplicateContext,
} from 'types/deduplicate';
import Router from 'next/router';
import DeduplicateOptions from 'components/pages/gallery/SelectedFileOptions/DeduplicateOptions';
export const DeduplicateContext = createContext<DeduplicateContextType>(
DefaultDeduplicateContext
);
export default function Deduplicate() {
const { setDialogMessage, startLoading, finishLoading, showNavBar } =
useContext(AppContext);
const [duplicateFiles, setDuplicateFiles] = useState<EnteFile[]>([]);
const [clubSameTimeFilesOnly, setClubSameTimeFilesOnly] = useState(false);
const [fileSizeMap, setFileSizeMap] = useState(new Map<number, number>());
const [selected, setSelected] = useState<SelectedState>({
count: 0,
collectionID: 0,
});
const closeDeduplication = function () {
Router.back();
setSelected({ count: 0, collectionID: 0 });
};
useEffect(() => {
showNavBar(true);
});
useEffect(() => {
syncWithRemote();
}, [clubSameTimeFilesOnly]);
const syncWithRemote = async () => {
startLoading();
let duplicates = await getDuplicateFiles();
if (clubSameTimeFilesOnly) {
duplicates = clubDuplicatesByTime(duplicates);
}
const currFileSizeMap = new Map<number, number>();
let allDuplicateFiles: EnteFile[] = [];
let toSelectFileIDs: number[] = [];
let count = 0;
for (const dupe of duplicates) {
allDuplicateFiles = allDuplicateFiles.concat(dupe.files);
// select all except first file
toSelectFileIDs = toSelectFileIDs.concat(
dupe.files.slice(1).map((f) => f.id)
);
count += dupe.files.length - 1;
for (const file of dupe.files) {
currFileSizeMap.set(file.id, dupe.size);
}
}
setDuplicateFiles(allDuplicateFiles);
setFileSizeMap(currFileSizeMap);
const selectedFiles = {
count: count,
collectionID: ALL_SECTION,
};
for (const fileID of toSelectFileIDs) {
selectedFiles[fileID] = true;
}
setSelected(selectedFiles);
finishLoading();
};
const deleteFileHelper = async () => {
try {
startLoading();
const selectedFiles = getSelectedFiles(selected, duplicateFiles);
await trashFiles(selectedFiles);
closeDeduplication();
} catch (e) {
switch (e.status?.toString()) {
case ServerErrorCodes.FORBIDDEN:
setDialogMessage({
title: constants.ERROR,
staticBackdrop: true,
close: { variant: 'danger' },
content: constants.NOT_FILE_OWNER,
});
}
setDialogMessage({
title: constants.ERROR,
staticBackdrop: true,
close: { variant: 'danger' },
content: constants.UNKNOWN_ERROR,
});
} finally {
await syncWithRemote();
finishLoading();
}
};
return (
<DeduplicateContext.Provider
value={{
...DefaultDeduplicateContext,
clubSameTimeFilesOnly,
setClubSameTimeFilesOnly,
fileSizeMap,
isOnDeduplicatePage: true,
}}>
<PhotoFrame
files={duplicateFiles}
setFiles={setDuplicateFiles}
syncWithRemote={syncWithRemote}
setSelected={setSelected}
selected={selected}
activeCollection={ALL_SECTION}
/>
<DeduplicateOptions
setDialogMessage={setDialogMessage}
deleteFileHelper={deleteFileHelper}
count={selected.count}
close={closeDeduplication}
/>
</DeduplicateContext.Provider>
);
}

View file

@ -51,7 +51,7 @@ import {
sortFilesIntoCollections,
} from 'utils/file';
import SearchBar from 'components/Search';
import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions';
import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions/GalleryOptions';
import CollectionSelector, {
CollectionSelectorAttributes,
} from 'components/pages/gallery/CollectionSelector';
@ -103,12 +103,6 @@ import {
import Collections from 'components/pages/gallery/Collections';
import { VISIBILITY_STATE } from 'constants/file';
import ToastNotification from 'components/ToastNotification';
import {
clubDuplicatesByTime,
getDuplicateFiles,
} from 'services/deduplicationService';
import ClubDuplicateFilesByTime from 'components/ClubDuplicateFilesByTime';
import BackButton from 'components/BackButton';
export const DeadCenter = styled.div`
flex: 1;
@ -134,11 +128,6 @@ const defaultGalleryContext: GalleryContextType = {
setNotificationAttributes: () => null,
setBlockingLoad: () => null,
clubSameTimeFilesOnly: false,
setClubSameTimeFilesOnly: null,
fileSizeMap: new Map<number, number>(),
isDeduplicating: false,
setIsDeduplicating: null,
};
export const GalleryContext = createContext<GalleryContextType>(
@ -205,11 +194,6 @@ export default function Gallery() {
const [notificationAttributes, setNotificationAttributes] =
useState<NotificationAttributes>(null);
const [isDeduplicating, setIsDeduplicating] = useState(false);
const [duplicateFiles, setDuplicateFiles] = useState<EnteFile[]>([]);
const [clubSameTimeFilesOnly, setClubSameTimeFilesOnly] = useState(false);
const [fileSizeMap, setFileSizeMap] = useState(new Map<number, number>());
const showPlanSelectorModal = () => setPlanModalView(true);
const clearNotificationAttributes = () => setNotificationAttributes(null);
@ -246,57 +230,6 @@ export default function Gallery() {
appContext.showNavBar(true);
}, []);
useEffect(() => {
const main = async () => {
if (isDeduplicating) {
startLoading();
let duplicates = await getDuplicateFiles();
if (clubSameTimeFilesOnly) {
duplicates = clubDuplicatesByTime(duplicates);
}
const currFileSizeMap = new Map<number, number>();
let allDuplicateFiles: EnteFile[] = [];
let toSelectFileIDs: number[] = [];
let count = 0;
for (const dupe of duplicates) {
allDuplicateFiles = allDuplicateFiles.concat(dupe.files);
// select all except first file
toSelectFileIDs = toSelectFileIDs.concat(
dupe.files.slice(1).map((f) => f.id)
);
count += dupe.files.length - 1;
for (const file of dupe.files) {
currFileSizeMap.set(file.id, dupe.size);
}
}
setDuplicateFiles(allDuplicateFiles);
setFileSizeMap(currFileSizeMap);
const selectedFiles = {
count: count,
collectionID: ALL_SECTION,
};
for (const fileID of toSelectFileIDs) {
selectedFiles[fileID] = true;
}
setSelected(selectedFiles);
setActiveCollection(ALL_SECTION);
finishLoading();
} else {
setDuplicateFiles([]);
setFileSizeMap(new Map<number, number>());
setClubSameTimeFilesOnly(false);
}
};
main();
}, [isDeduplicating, clubSameTimeFilesOnly]);
useEffect(
() => collectionSelectorAttributes && setCollectionSelectorView(true),
[collectionSelectorAttributes]
@ -619,11 +552,6 @@ export default function Gallery() {
setNotificationAttributes,
setBlockingLoad,
clubSameTimeFilesOnly,
setClubSameTimeFilesOnly,
fileSizeMap,
setIsDeduplicating: setIsDeduplicating,
isDeduplicating: isDeduplicating,
}}>
<FullScreenDropZone
getRootProps={getRootProps}
@ -657,35 +585,29 @@ export default function Gallery() {
setFiles={setFiles}
isFirstUpload={collectionsAndTheirLatestFile?.length === 0}
/>
{!isDeduplicating && (
<>
<SearchBar
isOpen={isInSearchMode}
setOpen={setIsInSearchMode}
isFirstFetch={isFirstFetch}
collections={collections}
files={getNonTrashedUniqueUserFiles(files)}
setActiveCollection={setActiveCollection}
setSearch={updateSearch}
searchStats={searchStats}
/>
<Collections
collections={collections}
collectionAndTheirLatestFile={
collectionsAndTheirLatestFile
}
isInSearchMode={isInSearchMode}
activeCollection={activeCollection}
setActiveCollection={setActiveCollection}
syncWithRemote={syncWithRemote}
setDialogMessage={setDialogMessage}
setCollectionNamerAttributes={
setCollectionNamerAttributes
}
collectionFilesCount={collectionFilesCount}
/>
</>
)}
<SearchBar
isOpen={isInSearchMode}
setOpen={setIsInSearchMode}
isFirstFetch={isFirstFetch}
collections={collections}
files={getNonTrashedUniqueUserFiles(files)}
setActiveCollection={setActiveCollection}
setSearch={updateSearch}
searchStats={searchStats}
/>
<Collections
collections={collections}
collectionAndTheirLatestFile={collectionsAndTheirLatestFile}
isInSearchMode={isInSearchMode}
activeCollection={activeCollection}
setActiveCollection={setActiveCollection}
syncWithRemote={syncWithRemote}
setDialogMessage={setDialogMessage}
setCollectionNamerAttributes={setCollectionNamerAttributes}
collectionFilesCount={collectionFilesCount}
/>
{blockingLoad && (
<LoadingOverlay>
<EnteSpinner />
@ -722,28 +644,19 @@ export default function Gallery() {
attributes={fixCreationTimeAttributes}
/>
{isDeduplicating ? (
<>
<BackButton setIsDeduplicating={setIsDeduplicating} />
<ClubDuplicateFilesByTime />
</>
) : (
<>
<UploadButton
isFirstFetch={isFirstFetch}
openFileUploader={openFileUploader}
/>
<Sidebar
collections={collections}
setDialogMessage={setDialogMessage}
setLoading={setBlockingLoad}
/>
</>
)}
<UploadButton
isFirstFetch={isFirstFetch}
openFileUploader={openFileUploader}
/>
<Sidebar
collections={collections}
setDialogMessage={setDialogMessage}
setLoading={setBlockingLoad}
/>
<PhotoFrame
files={isDeduplicating ? duplicateFiles : files}
setFiles={isDeduplicating ? setDuplicateFiles : setFiles}
files={files}
setFiles={setFiles}
syncWithRemote={syncWithRemote}
favItemIds={favItemIds}
setSelected={setSelected}

View file

@ -0,0 +1,13 @@
export type DeduplicateContextType = {
clubSameTimeFilesOnly: boolean;
setClubSameTimeFilesOnly: (clubSameTimeFilesOnly: boolean) => void;
fileSizeMap: Map<number, number>;
isOnDeduplicatePage: boolean;
};
export const DefaultDeduplicateContext = {
clubSameTimeFilesOnly: false,
setClubSameTimeFilesOnly: () => null,
fileSizeMap: new Map<number, number>(),
isOnDeduplicatePage: false,
};

View file

@ -31,11 +31,6 @@ export type GalleryContextType = {
setNotificationAttributes: (attributes: NotificationAttributes) => void;
setBlockingLoad: (value: boolean) => void;
clubSameTimeFilesOnly: boolean;
setClubSameTimeFilesOnly: (clubSameTimeFilesOnly: boolean) => void;
fileSizeMap: Map<number, number>;
isDeduplicating: boolean;
setIsDeduplicating: (value: boolean) => void;
};
export interface NotificationAttributes {