added rendering for deduplication
This commit is contained in:
parent
6e5d38e772
commit
1285a2231d
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
24
src/components/icons/LeftArrow.tsx
Normal file
24
src/components/icons/LeftArrow.tsx
Normal 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',
|
||||
};
|
105
src/components/pages/deduplicate/SelectedDuplicatesOptions.tsx
Normal file
105
src/components/pages/deduplicate/SelectedDuplicatesOptions.tsx
Normal 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;
|
|
@ -13,6 +13,7 @@ export enum PAGES {
|
|||
VERIFY = '/verify',
|
||||
ROOT = '/',
|
||||
SHARED_ALBUMS = '/shared-albums',
|
||||
DEDUPLICATE = '/deduplicate',
|
||||
}
|
||||
export const getAlbumSiteHost = () =>
|
||||
process.env.NODE_ENV === 'production'
|
||||
|
|
297
src/pages/deduplicate/index.tsx
Normal file
297
src/pages/deduplicate/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue