added rendering for deduplication

This commit is contained in:
Rushikesh Tote 2022-03-23 13:06:24 +05:30
parent 6e5d38e772
commit 1285a2231d
8 changed files with 446 additions and 8 deletions

View file

@ -62,10 +62,10 @@ interface Props {
) => void;
selected: SelectedState;
isFirstLoad;
openFileUploader;
openFileUploader?;
isInSearchMode: boolean;
search: Search;
setSearchStats: setSearchStats;
search?: Search;
setSearchStats?: setSearchStats;
deleted?: number[];
activeCollection: number;
isSharedCollection: boolean;
@ -147,7 +147,7 @@ const PhotoFrame = ({
timeTaken: (Date.now() - startTime) / 1000,
});
}
if (search.fileIndex || search.fileIndex === 0) {
if (search?.fileIndex || search?.fileIndex === 0) {
const filteredDataIdx = filteredData.findIndex(
(data) => data.dataIndex === search.fileIndex
);
@ -186,7 +186,7 @@ const PhotoFrame = ({
return false;
}
if (
search.date &&
search?.date &&
!isSameDayAnyYear(search.date)(
new Date(item.metadata.creationTime / 1000)
)
@ -194,7 +194,7 @@ const PhotoFrame = ({
return false;
}
if (
search.location &&
search?.location &&
!isInsideBox(item.metadata, search.location)
) {
return false;

View file

@ -285,6 +285,13 @@ export default function Sidebar(props: Props) {
}}>
{constants.UPDATE_EMAIL}
</LinkButton>
<LinkButton
style={{ marginTop: '30px' }}
onClick={() => {
router.push(PAGES.DEDUPLICATE);
}}>
{constants.DEDUPLICATE_FILES}
</LinkButton>
<Divider />
<>
<FixLargeThumbnails

View file

@ -0,0 +1,24 @@
import React from 'react';
export default function LeftArrow(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={props.width}
height={props.height}
viewBox={props.viewBox}
fill="currentColor"
className="bi bi-arrow-left">
<path
fillRule="evenodd"
d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"
/>
</svg>
);
}
LeftArrow.defaultProps = {
height: 32,
width: 32,
viewBox: '0 0 16 16',
};

View file

@ -0,0 +1,105 @@
import { IconButton } from 'components/Container';
import CloseIcon from 'components/icons/CloseIcon';
import DeleteIcon from 'components/icons/DeleteIcon';
import { SetDialogMessage } from 'components/MessageDialog';
import Navbar from 'components/Navbar';
import React from 'react';
import { OverlayTrigger } from 'react-bootstrap';
import styled from 'styled-components';
import constants from 'utils/strings/constants';
interface Props {
setDialogMessage: SetDialogMessage;
deleteFileHelper: () => void;
count: number;
clearSelection: () => void;
clubByTime: boolean;
setClubByTime: (clubByTime: boolean) => void;
}
const SelectionBar = styled(Navbar)`
position: fixed;
top: 0;
color: #fff;
z-index: 1001;
width: 100%;
padding: 0 16px;
`;
const SelectionContainer = styled.div`
flex: 1;
align-items: center;
display: flex;
`;
interface IconWithMessageProps {
children?: any;
message: string;
}
export const IconWithMessage = (props: IconWithMessageProps) => (
<OverlayTrigger
placement="bottom"
overlay={<p style={{ zIndex: 1002 }}>{props.message}</p>}>
{props.children}
</OverlayTrigger>
);
const SelectedFileOptions = ({
setDialogMessage,
deleteFileHelper,
count,
clearSelection,
clubByTime,
setClubByTime,
}: Props) => {
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={clearSelection}>
<CloseIcon />
</IconButton>
<div>
{count} {constants.SELECTED}
</div>
</SelectionContainer>
<>
<input
type="checkbox"
style={{
width: '1em',
height: '1em',
}}
value={clubByTime ? 'true' : 'false'}
onChange={() => {
setClubByTime(!clubByTime);
}}></input>
<div
style={{
marginLeft: '0.5em',
}}>
{constants.CLUB_BY_CAPTURE_TIME}
</div>
<IconWithMessage message={constants.DELETE}>
<IconButton onClick={trashHandler}>
<DeleteIcon />
</IconButton>
</IconWithMessage>
</>
</SelectionBar>
);
};
export default SelectedFileOptions;

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,297 @@
import EnteSpinner from 'components/EnteSpinner';
import LeftArrow from 'components/icons/LeftArrow';
import { LoadingOverlay } from 'components/LoadingOverlay';
import MessageDialog, { MessageAttributes } from 'components/MessageDialog';
import SelectedDuplicatesOptions from 'components/pages/deduplicate/SelectedDuplicatesOptions';
import AlertBanner from 'components/pages/gallery/AlertBanner';
import PhotoFrame from 'components/PhotoFrame';
import ToastNotification from 'components/ToastNotification';
import { ALL_SECTION } from 'constants/collection';
import { PAGES } from 'constants/pages';
import { useRouter } from 'next/router';
import { defaultGalleryContext, GalleryContext } from 'pages/gallery';
import { AppContext } from 'pages/_app';
import React, { useContext, useEffect, useRef, useState } from 'react';
import LoadingBar from 'react-top-loading-bar';
import { syncCollections } from 'services/collectionService';
import deduplicationService from 'services/deduplicationService';
import { getLocalFiles, syncFiles, trashFiles } from 'services/fileService';
import { getLocalTrash, getTrashedFiles } from 'services/trashService';
import { isTokenValid, logoutUser } from 'services/userService';
import { EnteFile } from 'types/file';
import { NotificationAttributes, SelectedState } from 'types/gallery';
import { checkConnectivity } from 'utils/common';
import { CustomError, ServerErrorCodes } from 'utils/error';
import { getSelectedFiles, mergeMetadata, sortFiles } from 'utils/file';
import { isFirstLogin, setIsFirstLogin, setJustSignedUp } from 'utils/storage';
import { clearKeys, getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import constants from 'utils/strings/constants';
export default function Deduplicate() {
const router = useRouter();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [localFiles, setLocalFiles] = useState<EnteFile[]>(null);
const [duplicateFiles, setDuplicateFiles] = useState<EnteFile[]>([]);
const [bannerMessage, setBannerMessage] = useState<JSX.Element | string>(
null
);
const [isFirstLoad, setIsFirstLoad] = useState(false);
const [selected, setSelected] = useState<SelectedState>({
count: 0,
collectionID: 0,
});
const [dialogMessage, setDialogMessage] = useState<MessageAttributes>();
const [messageDialogView, setMessageDialogView] = useState(false);
const [blockingLoad, setBlockingLoad] = useState(false);
const loadingBar = useRef(null);
const isLoadingBarRunning = useRef(false);
const syncInProgress = useRef(true);
const resync = useRef(false);
const appContext = useContext(AppContext);
const [clubByTime, setClubByTime] = useState(false);
const [notificationAttributes, setNotificationAttributes] =
useState<NotificationAttributes>(null);
const closeMessageDialog = () => setMessageDialogView(false);
const clearNotificationAttributes = () => setNotificationAttributes(null);
useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if (!key) {
appContext.setRedirectURL(router.asPath);
router.push(PAGES.ROOT);
return;
}
const main = async () => {
setIsFirstLoad(isFirstLogin());
setIsFirstLogin(false);
const localFiles = mergeMetadata(await getLocalFiles());
const trash = await getLocalTrash();
const trashedFile = getTrashedFiles(trash);
setLocalFiles(sortFiles([...localFiles, ...trashedFile]));
await syncWithRemote(true);
setIsFirstLoad(false);
setJustSignedUp(false);
};
main();
appContext.showNavBar(true);
}, []);
useEffect(() => setMessageDialogView(true), [dialogMessage]);
const syncWithRemote = async (force = false, silent = false) => {
if (syncInProgress.current && !force) {
resync.current = true;
return;
}
syncInProgress.current = true;
try {
checkConnectivity();
if (!(await isTokenValid())) {
throw new Error(ServerErrorCodes.SESSION_EXPIRED);
}
!silent && startLoading();
const collections = await syncCollections();
await syncFiles(collections, setLocalFiles);
let duplicates = await deduplicationService.getDuplicateFiles();
if (clubByTime) {
duplicates = await deduplicationService.clubDuplicatesByTime(
duplicates
);
}
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;
}
setDuplicateFiles(allDuplicateFiles);
const selectedFiles = {
count: count,
collectionID: 0,
};
for (const fileID of toSelectFileIDs) {
selectedFiles[fileID] = true;
}
setSelected(selectedFiles);
} catch (e) {
switch (e.message) {
case ServerErrorCodes.SESSION_EXPIRED:
setBannerMessage(constants.SESSION_EXPIRED_MESSAGE);
setDialogMessage({
title: constants.SESSION_EXPIRED,
content: constants.SESSION_EXPIRED_MESSAGE,
staticBackdrop: true,
nonClosable: true,
proceed: {
text: constants.LOGIN,
action: logoutUser,
variant: 'success',
},
});
break;
case CustomError.KEY_MISSING:
clearKeys();
router.push(PAGES.CREDENTIALS);
break;
}
} finally {
!silent && finishLoading();
}
syncInProgress.current = false;
if (resync.current) {
resync.current = false;
syncWithRemote();
}
};
useEffect(() => {
startLoading();
const sync = async () => {
await syncWithRemote();
};
sync();
finishLoading();
}, [clubByTime]);
const clearSelection = function () {
setSelected({ count: 0, collectionID: 0 });
};
const startLoading = () => {
!isLoadingBarRunning.current && loadingBar.current?.continuousStart();
isLoadingBarRunning.current = true;
};
const finishLoading = () => {
isLoadingBarRunning.current && loadingBar.current?.complete();
isLoadingBarRunning.current = false;
};
if (!duplicateFiles) {
return <div />;
}
const deleteFileHelper = async () => {
startLoading();
try {
const selectedFiles = getSelectedFiles(selected, duplicateFiles);
await trashFiles(selectedFiles);
clearSelection();
} 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(false, true);
finishLoading();
}
};
return (
<GalleryContext.Provider
value={{
...defaultGalleryContext,
closeMessageDialog,
syncWithRemote,
setDialogMessage,
startLoading,
finishLoading,
setNotificationAttributes,
setBlockingLoad,
}}>
{blockingLoad && (
<LoadingOverlay>
<EnteSpinner />
</LoadingOverlay>
)}
<LoadingBar color="#51cd7c" ref={loadingBar} />
<AlertBanner bannerMessage={bannerMessage} />
<ToastNotification
attributes={notificationAttributes}
clearAttributes={clearNotificationAttributes}
/>
<MessageDialog
size="lg"
show={messageDialogView}
onHide={closeMessageDialog}
attributes={dialogMessage}
/>
{duplicateFiles.length > 0 ? (
<PhotoFrame
files={duplicateFiles}
setFiles={setDuplicateFiles}
syncWithRemote={syncWithRemote}
favItemIds={new Set()}
setSelected={setSelected}
selected={selected}
isFirstLoad={isFirstLoad}
isInSearchMode={false}
deleted={[]}
activeCollection={ALL_SECTION}
isSharedCollection={false}
enableDownload={true}
/>
) : (
<b
style={{
fontSize: '2em',
textAlign: 'center',
marginTop: '20%',
}}>
{constants.NO_DUPLICATES_FOUND}
</b>
)}
{selected.count > 0 ? (
<SelectedDuplicatesOptions
setDialogMessage={setDialogMessage}
deleteFileHelper={deleteFileHelper}
count={selected.count}
clearSelection={clearSelection}
clubByTime={clubByTime}
setClubByTime={setClubByTime}
/>
) : (
<div
style={{
position: 'absolute',
top: '1em',
left: '1em',
zIndex: 10,
}}
onClick={() => {
router.push(PAGES.GALLERY);
}}>
<LeftArrow />
</div>
)}
</GalleryContext.Provider>
);
}

View file

@ -121,7 +121,7 @@ const AlertContainer = styled.div`
text-align: center;
`;
const defaultGalleryContext: GalleryContextType = {
export const defaultGalleryContext: GalleryContextType = {
thumbs: new Map(),
files: new Map(),
showPlanSelectorModal: () => null,

View file

@ -684,10 +684,14 @@ const englishConstants = {
LIVE_PHOTO: 'this is a live photo',
LIVE: 'LIVE',
DISABLE_PASSWORD: 'disable password lock',
DISABLE_PASSWORD_MESSAGE: 'are you sure that you want to disable the password lock?',
DISABLE_PASSWORD_MESSAGE:
'are you sure that you want to disable the password lock?',
PASSWORD_LOCK: 'password lock',
LOCK: 'lock',
DOWNLOAD_UPLOAD_LOGS: 'debug logs',
DEDUPLICATE_FILES: 'deduplicate files',
NO_DUPLICATES_FOUND: "you've no duplicate files that can be cleared",
CLUB_BY_CAPTURE_TIME: 'club by capture time',
};
export default englishConstants;