Browse Source

Added selectAll checkbox to select all files on a day (#674)

## Description

fixes https://github.com/ente-io/ente/issues/535

This PR will add checkbox to all select files on a day, this will also
handle manual selection of files and select all checkbox on a day.
Vishnu Mohandas 1 year ago
parent
commit
a857a86608

+ 10 - 46
web/apps/photos/src/components/PhotoFrame.tsx

@@ -17,12 +17,16 @@ import DownloadManager, {
     LivePhotoSourceURL,
     SourceURLs,
 } from "services/download";
+import {
+    handleSelectCreator,
+    updateFileMsrcProps,
+    updateFileSrcProps,
+} from "utils/photoFrame";
 import { EnteFile } from "types/file";
 import {
     SelectedState,
     SetFilesDownloadProgressAttributesCreator,
 } from "types/gallery";
-import { updateFileMsrcProps, updateFileSrcProps } from "utils/photoFrame";
 import { PhotoList } from "./PhotoList";
 import { DedupePhotoList } from "./PhotoList/dedupe";
 import PreviewCard from "./pages/gallery/PreviewCard";
@@ -227,52 +231,12 @@ const PhotoFrame = ({
         setIsPhotoSwipeOpen?.(true);
     };
 
-    const handleSelect =
-        (id: number, isOwnFile: boolean, index?: number) =>
-        (checked: boolean) => {
-            if (typeof index !== "undefined") {
-                if (checked) {
-                    setRangeStart(index);
-                } else {
-                    setRangeStart(undefined);
-                }
-            }
-            setSelected((selected) => {
-                if (selected.collectionID !== activeCollectionID) {
-                    selected = { ownCount: 0, count: 0, collectionID: 0 };
-                }
-
-                const handleCounterChange = (count: number) => {
-                    if (selected[id] === checked) {
-                        return count;
-                    }
-                    if (checked) {
-                        return count + 1;
-                    } else {
-                        return count - 1;
-                    }
-                };
+    const handleSelect = handleSelectCreator(
+        setSelected,
+        activeCollectionID,
+        setRangeStart
+    );
 
-                const handleAllCounterChange = () => {
-                    if (isOwnFile) {
-                        return {
-                            ownCount: handleCounterChange(selected.ownCount),
-                            count: handleCounterChange(selected.count),
-                        };
-                    } else {
-                        return {
-                            count: handleCounterChange(selected.count),
-                        };
-                    }
-                };
-                return {
-                    ...selected,
-                    [id]: checked,
-                    collectionID: activeCollectionID,
-                    ...handleAllCounterChange(),
-                };
-            });
-        };
     const onHoverOver = (index: number) => () => {
         setCurrentHover(index);
     };

+ 84 - 11
web/apps/photos/src/components/PhotoList/index.tsx

@@ -1,8 +1,7 @@
 import { FlexWrapper } from "@ente/shared/components/Container";
 import { ENTE_WEBSITE_LINK } from "@ente/shared/constants/urls";
-import { formatDate } from "@ente/shared/time/format";
 import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
-import { Box, Link, Typography, styled } from "@mui/material";
+import { Box, Link, Typography, Checkbox, styled } from "@mui/material";
 import {
     DATE_CONTAINER_HEIGHT,
     GAP_BTW_TILES,
@@ -23,8 +22,10 @@ import {
     ListChildComponentProps,
     areEqual,
 } from "react-window";
-import { EnteFile } from "types/file";
 import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
+import { formatDate, getDate, isSameDay } from "@ente/shared/time/format";
+import { handleSelectCreator } from "utils/photoFrame";
+import { EnteFile } from "types/file";
 
 const A_DAY = 24 * 60 * 60 * 1000;
 const FOOTER_HEIGHT = 90;
@@ -185,6 +186,10 @@ const NothingContainer = styled(ListItemContainer)`
     justify-content: center;
 `;
 
+const SelectAllCheckBoxContainer = styled(Checkbox)<{ margin: number }>`
+    margin-left: ${(props) => props.margin}px;
+`;
+
 interface Props {
     height: number;
     width: number;
@@ -265,6 +270,8 @@ export function PhotoList({
     const shouldRefresh = useRef(false);
     const listRef = useRef(null);
 
+    const [checkedDates, setCheckedDates] = useState({});
+
     const fittableColumns = getFractionFittableColumns(width);
     let columns = Math.floor(fittableColumns);
 
@@ -473,14 +480,6 @@ export function PhotoList({
         });
     };
 
-    const isSameDay = (first, second) => {
-        return (
-            first.getFullYear() === second.getFullYear() &&
-            first.getMonth() === second.getMonth() &&
-            first.getDate() === second.getDate()
-        );
-    };
-
     const getPhotoListHeader = (photoListHeader) => {
         return {
             ...photoListHeader,
@@ -722,6 +721,62 @@ export function PhotoList({
         }
     };
 
+    useEffect(() => {
+        const notSelectedFiles = displayFiles?.filter(
+            (item) => !galleryContext.selectedFile[item.id]
+        );
+        const unselectedDates = [
+            ...new Set(notSelectedFiles?.map((item) => getDate(item))), // to get file's date which were manually unselected
+        ];
+
+        const localSelectedFiles = displayFiles.filter(
+            // to get files which were manually selected
+            (item) => !unselectedDates.includes(getDate(item))
+        );
+
+        const localSelectedDates = [
+            ...new Set(localSelectedFiles?.map((item) => getDate(item))),
+        ]; // to get file's date which were manually selected
+
+        unselectedDates.forEach((date) => {
+            setCheckedDates((prev) => ({
+                ...prev,
+                [date]: false,
+            })); // To uncheck select all checkbox if any of the file on the date is unselected
+        });
+
+        localSelectedDates.map((date) => {
+            setCheckedDates((prev) => ({
+                ...prev,
+                [date]: true,
+            }));
+            // To check select all checkbox if all of the files on the date is selected manually
+        });
+    }, [galleryContext.selectedFile]);
+
+    const handleSelect = handleSelectCreator(
+        galleryContext.setSelectedFiles,
+        activeCollectionID
+    );
+
+    const onChangeSelectAllCheckBox = (date: string) => {
+        const dates = { ...checkedDates, [date]: !checkedDates[date] };
+        const isDateSelected = !checkedDates[date];
+
+        setCheckedDates(dates);
+
+        const filesOnADay = displayFiles?.filter(
+            (item) => getDate(item) === date
+        ); // all files on a checked/unchecked day
+
+        filesOnADay.forEach((file) => {
+            handleSelect(
+                file.id,
+                file.ownerID === galleryContext?.user?.id
+            )(isDateSelected);
+        });
+    };
+
     const renderListItem = (
         listItem: TimeStampListItem,
         isScrolling: boolean,
@@ -733,6 +788,15 @@ export function PhotoList({
                         .map((item) => [
                             <DateContainer key={item.date} span={item.span}>
                                 {item.date}
+                                <SelectAllCheckBoxContainer
+                                    key={item.date}
+                                    name={item.date}
+                                    checked={checkedDates[item.date]}
+                                    onChange={() =>
+                                        onChangeSelectAllCheckBox(item.date)
+                                    }
+                                    margin={columns}
+                                />
                             </DateContainer>,
                             <div key={`${item.date}-gap`} />,
                         ])
@@ -740,6 +804,15 @@ export function PhotoList({
                 ) : (
                     <DateContainer span={columns}>
                         {listItem.date}
+                        <SelectAllCheckBoxContainer
+                            key={listItem.date}
+                            name={listItem.date}
+                            checked={checkedDates[listItem.date]}
+                            onChange={() =>
+                                onChangeSelectAllCheckBox(listItem.date)
+                            }
+                            margin={columns}
+                        />
                     </DateContainer>
                 );
             case ITEM_TYPE.SIZE_AND_COUNT:

+ 5 - 2
web/apps/photos/src/pages/gallery/index.tsx

@@ -163,6 +163,8 @@ const defaultGalleryContext: GalleryContextType = {
     emailList: null,
     openHiddenSection: () => null,
     isClipSearchResult: null,
+    selectedFile: null,
+    setSelectedFiles: () => null,
 };
 
 export const GalleryContext = createContext<GalleryContextType>(
@@ -1013,8 +1015,9 @@ export default function Gallery() {
                 emailList,
                 openHiddenSection,
                 isClipSearchResult,
-            }}
-        >
+                selectedFile: selected,
+                setSelectedFiles: setSelected,
+            }}>
             <FullScreenDropZone
                 getDragAndDropRootProps={getDragAndDropRootProps}
             >

+ 5 - 0
web/apps/photos/src/types/gallery/index.ts

@@ -11,6 +11,9 @@ export type SelectedState = {
     count: number;
     collectionID: number;
 };
+export type SetSelectedState = React.Dispatch<
+    React.SetStateAction<SelectedState>
+>;
 export type SetFiles = React.Dispatch<React.SetStateAction<EnteFile[]>>;
 export type SetCollections = React.Dispatch<React.SetStateAction<Collection[]>>;
 export type SetLoading = React.Dispatch<React.SetStateAction<boolean>>;
@@ -54,6 +57,8 @@ export type GalleryContextType = {
     emailList: string[];
     openHiddenSection: (callback?: () => void) => void;
     isClipSearchResult: boolean;
+    setSelectedFiles: (value) => void;
+    selectedFile: SelectedState;
 };
 
 export enum CollectionSelectorIntent {

+ 55 - 2
web/apps/photos/src/utils/photoFrame/index.ts

@@ -1,7 +1,8 @@
-import { logError } from "@ente/shared/sentry";
 import { FILE_TYPE } from "constants/file";
-import { LivePhotoSourceURL, SourceURLs } from "services/download";
 import { EnteFile } from "types/file";
+import { logError } from "@ente/shared/sentry";
+import { LivePhotoSourceURL, SourceURLs } from "services/download";
+import { SetSelectedState } from "types/gallery";
 
 const WAIT_FOR_VIDEO_PLAYBACK = 1 * 1000;
 
@@ -129,3 +130,55 @@ export async function updateFileSrcProps(
         file.src = url as string;
     }
 }
+
+export const handleSelectCreator =
+    (
+        setSelected: SetSelectedState,
+        activeCollectionID: number,
+        setRangeStart?
+    ) =>
+    (id: number, isOwnFile: boolean, index?: number) =>
+    (checked: boolean) => {
+        if (typeof index !== 'undefined') {
+            if (checked) {
+                setRangeStart(index);
+            } else {
+                setRangeStart(undefined);
+            }
+        }
+        setSelected((selected) => {
+            if (selected.collectionID !== activeCollectionID) {
+                selected = { ownCount: 0, count: 0, collectionID: 0 };
+            }
+
+            const handleCounterChange = (count: number) => {
+                if (selected[id] === checked) {
+                    return count;
+                }
+                if (checked) {
+                    return count + 1;
+                } else {
+                    return count - 1;
+                }
+            };
+
+            const handleAllCounterChange = () => {
+                if (isOwnFile) {
+                    return {
+                        ownCount: handleCounterChange(selected.ownCount),
+                        count: handleCounterChange(selected.count),
+                    };
+                } else {
+                    return {
+                        count: handleCounterChange(selected.count),
+                    };
+                }
+            };
+            return {
+                ...selected,
+                [id]: checked,
+                collectionID: activeCollectionID,
+                ...handleAllCounterChange(),
+            };
+        });
+    };

+ 21 - 0
web/packages/shared/time/format.ts

@@ -1,5 +1,7 @@
 import i18n, { t } from "i18next";
 
+const A_DAY = 24 * 60 * 60 * 1000;
+
 const dateTimeFullFormatter1 = new Intl.DateTimeFormat(i18n.language, {
     weekday: "short",
     month: "short",
@@ -76,3 +78,22 @@ export function formatDateRelative(date: number) {
                 u as Intl.RelativeTimeFormatUnit,
             );
 }
+
+export const isSameDay = (first, second) => {
+    return (
+        first.getFullYear() === second.getFullYear() &&
+        first.getMonth() === second.getMonth() &&
+        first.getDate() === second.getDate()
+    );
+};
+
+export const getDate = (item) => {
+    const currentDate = item.metadata.creationTime / 1000;
+    const date = isSameDay(new Date(currentDate), new Date())
+        ? t('TODAY')
+        : isSameDay(new Date(currentDate), new Date(Date.now() - A_DAY))
+        ? t('YESTERDAY')
+        : formatDate(currentDate);
+
+    return date;
+};