[web] File list cleanup and refactoring - Part 2 (#6559)
This commit is contained in:
@@ -227,33 +227,24 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
const [_items, setItems] = useState<FileListItem[]>([]);
|
||||
const items = useDeferredValue(_items);
|
||||
|
||||
const listRef = useRef<VariableSizeList | null>(null);
|
||||
|
||||
const [rangeStartIndex, setRangeStartIndex] = useState<number | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [hoverIndex, setHoverIndex] = useState<number | undefined>(undefined);
|
||||
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
|
||||
// Timeline date strings for which all photos have been selected.
|
||||
//
|
||||
// See: [Note: Timeline date string]
|
||||
const [checkedTimelineDateStrings, setCheckedTimelineDateStrings] =
|
||||
useState(new Set());
|
||||
useState(new Set<string>());
|
||||
|
||||
const [rangeStart, setRangeStart] = useState<number | null>(null);
|
||||
const [currentHover, setCurrentHover] = useState<number | null>(null);
|
||||
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
|
||||
const listRef = useRef<VariableSizeList | null>(null);
|
||||
|
||||
const layoutParams = useMemo(
|
||||
() => computeThumbnailGridLayoutParams(width),
|
||||
[width],
|
||||
);
|
||||
|
||||
const {
|
||||
// containerWidth,
|
||||
// isSmallerLayout,
|
||||
// paddingInline,
|
||||
columns,
|
||||
// itemWidth,
|
||||
// itemHeight,
|
||||
// gap,
|
||||
} = layoutParams;
|
||||
|
||||
useEffect(() => {
|
||||
// Since width and height are dependencies, there might be too many
|
||||
// updates to the list during a resize. The list computation too, while
|
||||
@@ -326,11 +317,9 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSmallerLayout) {
|
||||
items = mergeTimeStampList(items, columns);
|
||||
}
|
||||
if (!isSmallerLayout) items = mergeRowsWherePossible(items, columns);
|
||||
|
||||
if (annotatedFiles.length == 0) {
|
||||
if (!annotatedFiles.length) {
|
||||
items.push({
|
||||
height: height - 48,
|
||||
tag: "span",
|
||||
@@ -409,33 +398,40 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
});
|
||||
}, [annotatedFiles, selected]);
|
||||
|
||||
const handleSelectMulti = handleSelectCreatorMulti(
|
||||
setSelected,
|
||||
mode,
|
||||
user?.id,
|
||||
activeCollectionID,
|
||||
activePersonID,
|
||||
const handleSelectMulti = useMemo(
|
||||
() =>
|
||||
handleSelectCreatorMulti(
|
||||
setSelected,
|
||||
mode,
|
||||
user?.id,
|
||||
activeCollectionID,
|
||||
activePersonID,
|
||||
),
|
||||
[setSelected, mode, user?.id, activeCollectionID, activePersonID],
|
||||
);
|
||||
|
||||
const onChangeSelectAllCheckBox = (date: string) => {
|
||||
const next = new Set(checkedTimelineDateStrings);
|
||||
let isDateSelected: boolean;
|
||||
if (!next.has(date)) {
|
||||
next.add(date);
|
||||
isDateSelected = true;
|
||||
} else {
|
||||
next.delete(date);
|
||||
isDateSelected = false;
|
||||
}
|
||||
setCheckedTimelineDateStrings(next);
|
||||
const onChangeSelectAllCheckBox = useCallback(
|
||||
(date: string) => {
|
||||
const next = new Set(checkedTimelineDateStrings);
|
||||
let isDateSelected: boolean;
|
||||
if (!next.has(date)) {
|
||||
next.add(date);
|
||||
isDateSelected = true;
|
||||
} else {
|
||||
next.delete(date);
|
||||
isDateSelected = false;
|
||||
}
|
||||
setCheckedTimelineDateStrings(next);
|
||||
|
||||
// All files on a checked/unchecked day.
|
||||
const filesOnADay = annotatedFiles.filter(
|
||||
(af) => af.timelineDateString === date,
|
||||
);
|
||||
// All files on a checked/unchecked day.
|
||||
const filesOnADay = annotatedFiles.filter(
|
||||
(af) => af.timelineDateString === date,
|
||||
);
|
||||
|
||||
handleSelectMulti(filesOnADay.map((af) => af.file))(isDateSelected);
|
||||
};
|
||||
handleSelectMulti(filesOnADay.map((af) => af.file))(isDateSelected);
|
||||
},
|
||||
[annotatedFiles, checkedTimelineDateStrings, handleSelectMulti],
|
||||
);
|
||||
|
||||
const handleSelect = useMemo(
|
||||
() =>
|
||||
@@ -445,37 +441,36 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
user?.id,
|
||||
activeCollectionID,
|
||||
activePersonID,
|
||||
setRangeStart,
|
||||
setRangeStartIndex,
|
||||
),
|
||||
[setSelected, mode, user?.id, activeCollectionID, activePersonID],
|
||||
);
|
||||
|
||||
const onHoverOver = (index: number) => () => {
|
||||
setCurrentHover(index);
|
||||
};
|
||||
const handleRangeSelect = useCallback(
|
||||
(index: number) => {
|
||||
if (rangeStartIndex === undefined || rangeStartIndex == index)
|
||||
return;
|
||||
|
||||
const handleRangeSelect = (index: number) => () => {
|
||||
if (typeof rangeStart != "undefined" && rangeStart !== index) {
|
||||
const direction =
|
||||
(index - rangeStart!) / Math.abs(index - rangeStart!);
|
||||
const direction = index > rangeStartIndex ? 1 : -1;
|
||||
let checked = true;
|
||||
for (
|
||||
let i = rangeStart!;
|
||||
let i = rangeStartIndex;
|
||||
(index - i) * direction >= 0;
|
||||
i += direction
|
||||
) {
|
||||
checked = checked && !!selected[annotatedFiles[i]!.file.id];
|
||||
}
|
||||
for (
|
||||
let i = rangeStart!;
|
||||
let i = rangeStartIndex;
|
||||
(index - i) * direction > 0;
|
||||
i += direction
|
||||
) {
|
||||
handleSelect(annotatedFiles[i]!.file)(!checked);
|
||||
}
|
||||
handleSelect(annotatedFiles[index]!.file, index)(!checked);
|
||||
}
|
||||
};
|
||||
},
|
||||
[annotatedFiles, selected, rangeStartIndex, handleSelect],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -500,124 +495,146 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected.count === 0) {
|
||||
setRangeStart(null);
|
||||
}
|
||||
if (selected.count == 0) setRangeStartIndex(undefined);
|
||||
}, [selected]);
|
||||
|
||||
const getThumbnail = (
|
||||
{ file }: FileListAnnotatedFile,
|
||||
index: number,
|
||||
isScrolling: boolean,
|
||||
) => (
|
||||
<FileThumbnail
|
||||
key={`tile-${file.id}-selected-${selected[file.id] ?? false}`}
|
||||
{...{ user, emailByUserID }}
|
||||
file={file}
|
||||
onClick={() => onItemClick(index)}
|
||||
selectable={selectable!}
|
||||
onSelect={handleSelect(file, index)}
|
||||
selected={
|
||||
(!mode
|
||||
? selected.collectionID === activeCollectionID
|
||||
: mode == selected.context?.mode &&
|
||||
(selected.context.mode == "people"
|
||||
? selected.context.personID == activePersonID
|
||||
: selected.context.collectionID ==
|
||||
activeCollectionID)) && !!selected[file.id]
|
||||
}
|
||||
selectOnClick={selected.count > 0}
|
||||
onHover={onHoverOver(index)}
|
||||
onRangeSelect={handleRangeSelect(index)}
|
||||
isRangeSelectActive={isShiftKeyPressed && selected.count > 0}
|
||||
isInsSelectRange={
|
||||
(index >= rangeStart! && index <= currentHover!) ||
|
||||
(index >= currentHover! && index <= rangeStart!)
|
||||
}
|
||||
activeCollectionID={activeCollectionID}
|
||||
showPlaceholder={isScrolling}
|
||||
isFav={favoriteFileIDs?.has(file.id)}
|
||||
/>
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const renderListItem = (
|
||||
listItem: FileListItem,
|
||||
isScrolling: boolean | undefined,
|
||||
) => {
|
||||
const haveSelection = selected.count > 0;
|
||||
switch (listItem.tag) {
|
||||
case "date":
|
||||
return listItem.dates ? (
|
||||
listItem.dates
|
||||
.map((item) => [
|
||||
<DateListItem key={item.date} span={item.span}>
|
||||
{haveSelection && (
|
||||
<Checkbox
|
||||
key={item.date}
|
||||
name={item.date}
|
||||
checked={checkedTimelineDateStrings.has(
|
||||
item.date,
|
||||
)}
|
||||
onChange={() =>
|
||||
onChangeSelectAllCheckBox(item.date)
|
||||
}
|
||||
size="small"
|
||||
sx={{ pl: 0 }}
|
||||
/>
|
||||
)}
|
||||
{item.date}
|
||||
</DateListItem>,
|
||||
<div key={`${item.date}-gap`} />,
|
||||
])
|
||||
.flat()
|
||||
) : (
|
||||
<DateListItem span={columns}>
|
||||
{haveSelection && (
|
||||
<Checkbox
|
||||
key={listItem.date}
|
||||
name={listItem.date!}
|
||||
checked={checkedTimelineDateStrings.has(
|
||||
listItem.date,
|
||||
)}
|
||||
onChange={() =>
|
||||
onChangeSelectAllCheckBox(listItem.date!)
|
||||
const renderListItem = useCallback(
|
||||
(
|
||||
listItem: FileListItem,
|
||||
layoutParams: ThumbnailGridLayoutParams,
|
||||
isScrolling: boolean,
|
||||
) => {
|
||||
const haveSelection = selected.count > 0;
|
||||
switch (listItem.tag) {
|
||||
case "date":
|
||||
return listItem.dates ? (
|
||||
listItem.dates
|
||||
.map((item) => [
|
||||
<DateListItem key={item.date} span={item.span}>
|
||||
{haveSelection && (
|
||||
<Checkbox
|
||||
key={item.date}
|
||||
name={item.date}
|
||||
checked={checkedTimelineDateStrings.has(
|
||||
item.date,
|
||||
)}
|
||||
onChange={() =>
|
||||
onChangeSelectAllCheckBox(
|
||||
item.date,
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
sx={{ pl: 0 }}
|
||||
/>
|
||||
)}
|
||||
{item.date}
|
||||
</DateListItem>,
|
||||
<div key={`${item.date}-gap`} />,
|
||||
])
|
||||
.flat()
|
||||
) : (
|
||||
<DateListItem span={layoutParams.columns}>
|
||||
{haveSelection && (
|
||||
<Checkbox
|
||||
key={listItem.date}
|
||||
name={listItem.date!}
|
||||
checked={checkedTimelineDateStrings.has(
|
||||
listItem.date!,
|
||||
)}
|
||||
onChange={() =>
|
||||
onChangeSelectAllCheckBox(
|
||||
listItem.date!,
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
sx={{ pl: 0 }}
|
||||
/>
|
||||
)}
|
||||
{listItem.date}
|
||||
</DateListItem>
|
||||
);
|
||||
case "file": {
|
||||
const ret = listItem.items!.map(({ file }, i) => {
|
||||
const index = listItem.itemStartIndex! + i;
|
||||
return (
|
||||
<FileThumbnail
|
||||
key={`tile-${file.id}-selected-${selected[file.id] ?? false}`}
|
||||
{...{ user, emailByUserID }}
|
||||
file={file}
|
||||
selectable={selectable!}
|
||||
selected={
|
||||
(!mode
|
||||
? selected.collectionID ===
|
||||
activeCollectionID
|
||||
: mode == selected.context?.mode &&
|
||||
(selected.context.mode == "people"
|
||||
? selected.context.personID ==
|
||||
activePersonID
|
||||
: selected.context.collectionID ==
|
||||
activeCollectionID)) &&
|
||||
!!selected[file.id]
|
||||
}
|
||||
size="small"
|
||||
sx={{ pl: 0 }}
|
||||
selectOnClick={selected.count > 0}
|
||||
isRangeSelectActive={
|
||||
isShiftKeyPressed && selected.count > 0
|
||||
}
|
||||
isInSelectRange={
|
||||
rangeStartIndex !== undefined &&
|
||||
hoverIndex !== undefined &&
|
||||
((index >= rangeStartIndex &&
|
||||
index <= hoverIndex) ||
|
||||
(index >= hoverIndex &&
|
||||
index <= rangeStartIndex))
|
||||
}
|
||||
activeCollectionID={activeCollectionID}
|
||||
showPlaceholder={isScrolling}
|
||||
isFav={!!favoriteFileIDs?.has(file.id)}
|
||||
onClick={() => onItemClick(index)}
|
||||
onSelect={handleSelect(file, index)}
|
||||
onHover={() => setHoverIndex(index)}
|
||||
onRangeSelect={() => handleRangeSelect(index)}
|
||||
/>
|
||||
)}
|
||||
{listItem.date}
|
||||
</DateListItem>
|
||||
);
|
||||
case "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]!.file.id}-gap-${i}`}
|
||||
/>,
|
||||
);
|
||||
sum += 1;
|
||||
});
|
||||
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]!.file.id}-gap-${i}`}
|
||||
/>,
|
||||
);
|
||||
sum += 1;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
return ret;
|
||||
default:
|
||||
return listItem.component;
|
||||
}
|
||||
default:
|
||||
return listItem.component;
|
||||
}
|
||||
};
|
||||
},
|
||||
[
|
||||
activeCollectionID,
|
||||
activePersonID,
|
||||
checkedTimelineDateStrings,
|
||||
emailByUserID,
|
||||
favoriteFileIDs,
|
||||
handleRangeSelect,
|
||||
handleSelect,
|
||||
hoverIndex,
|
||||
isShiftKeyPressed,
|
||||
mode,
|
||||
onChangeSelectAllCheckBox,
|
||||
onItemClick,
|
||||
rangeStartIndex,
|
||||
selectable,
|
||||
selected,
|
||||
user,
|
||||
],
|
||||
);
|
||||
|
||||
const itemData = useMemo(
|
||||
() => ({ items, layoutParams, renderListItem }),
|
||||
@@ -681,9 +698,9 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks and merge multiple dates into a single row.
|
||||
* Merge multiple dates into a single row.
|
||||
*/
|
||||
const mergeTimeStampList = (
|
||||
const mergeRowsWherePossible = (
|
||||
items: FileListItem[],
|
||||
columns: number,
|
||||
): FileListItem[] => {
|
||||
@@ -814,9 +831,10 @@ interface FileListItemData {
|
||||
items: FileListItem[];
|
||||
layoutParams: ThumbnailGridLayoutParams;
|
||||
renderListItem: (
|
||||
timeStampListItem: FileListItem,
|
||||
isScrolling?: boolean,
|
||||
) => React.JSX.Element;
|
||||
listItem: FileListItem,
|
||||
layoutParams: ThumbnailGridLayoutParams,
|
||||
isScrolling: boolean,
|
||||
) => React.ReactNode;
|
||||
}
|
||||
|
||||
const FileListRow = memo(
|
||||
@@ -850,7 +868,7 @@ const FileListRow = memo(
|
||||
},
|
||||
]}
|
||||
>
|
||||
{renderListItem(item, isScrolling)}
|
||||
{renderListItem(item, layoutParams, !!isScrolling)}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
@@ -859,36 +877,36 @@ const FileListRow = memo(
|
||||
|
||||
type FileThumbnailProps = {
|
||||
file: EnteFile;
|
||||
onClick: () => void;
|
||||
selectable: boolean;
|
||||
selected: boolean;
|
||||
isRangeSelectActive: boolean;
|
||||
selectOnClick: boolean;
|
||||
isInSelectRange: boolean;
|
||||
activeCollectionID: number;
|
||||
showPlaceholder: boolean;
|
||||
isFav: boolean;
|
||||
onClick: () => void;
|
||||
onSelect: (checked: boolean) => void;
|
||||
onHover: () => void;
|
||||
onRangeSelect: () => void;
|
||||
isRangeSelectActive: boolean;
|
||||
selectOnClick: boolean;
|
||||
isInsSelectRange: boolean;
|
||||
activeCollectionID: number;
|
||||
showPlaceholder: boolean;
|
||||
isFav: boolean | undefined;
|
||||
} & Pick<FileListProps, "user" | "emailByUserID">;
|
||||
|
||||
const FileThumbnail: React.FC<FileThumbnailProps> = ({
|
||||
file,
|
||||
user,
|
||||
onClick,
|
||||
selectable,
|
||||
selected,
|
||||
onSelect,
|
||||
selectOnClick,
|
||||
onHover,
|
||||
onRangeSelect,
|
||||
isRangeSelectActive,
|
||||
isInsSelectRange,
|
||||
isInSelectRange,
|
||||
isFav,
|
||||
emailByUserID,
|
||||
activeCollectionID,
|
||||
showPlaceholder,
|
||||
onClick,
|
||||
onSelect,
|
||||
onHover,
|
||||
onRangeSelect,
|
||||
}) => {
|
||||
const [imageURL, setImageURL] = useState<string | undefined>(undefined);
|
||||
const [isLongPressing, setIsLongPressing] = useState(false);
|
||||
@@ -972,7 +990,7 @@ const FileThumbnail: React.FC<FileThumbnailProps> = ({
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
onChange={handleSelect}
|
||||
$active={isRangeSelectActive && isInsSelectRange}
|
||||
$active={isRangeSelectActive && isInSelectRange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
@@ -1008,9 +1026,7 @@ const FileThumbnail: React.FC<FileThumbnailProps> = ({
|
||||
className="preview-card-hover-overlay"
|
||||
checked={selected}
|
||||
/>
|
||||
{isRangeSelectActive && isInsSelectRange && (
|
||||
<InSelectRangeOverlay />
|
||||
)}
|
||||
{isRangeSelectActive && isInSelectRange && <InSelectRangeOverlay />}
|
||||
|
||||
{deleteBy && (
|
||||
<TileBottomTextOverlay>
|
||||
|
||||
@@ -11,18 +11,15 @@ export const handleSelectCreator =
|
||||
userID: number | undefined,
|
||||
activeCollectionID: number,
|
||||
activePersonID: string | undefined,
|
||||
// @ts-expect-error Need to add types
|
||||
setRangeStart?,
|
||||
setRangeStartIndex: (index: number | undefined) => void,
|
||||
) =>
|
||||
({ id, ownerID }: { id: number; ownerID: number }, index?: number) =>
|
||||
(checked: boolean) => {
|
||||
if (typeof index != "undefined") {
|
||||
if (checked) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
setRangeStart(index);
|
||||
setRangeStartIndex(index);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
setRangeStart(undefined);
|
||||
setRangeStartIndex(undefined);
|
||||
}
|
||||
}
|
||||
setSelected((_selected) => {
|
||||
|
||||
Reference in New Issue
Block a user