[web] File viewer code cleanup (#5294)
Cleaning pending leftovers from old viewer.
This commit is contained in:
@@ -1,15 +1,30 @@
|
||||
import { assertionFailed } from "@/base/assert";
|
||||
import { Overlay } from "@/base/components/containers";
|
||||
import { isSameDay } from "@/base/date";
|
||||
import { EnteFile } from "@/media/file";
|
||||
import { formattedDateRelative } from "@/base/i18n-date";
|
||||
import { downloadManager } from "@/gallery/services/download";
|
||||
import { EnteFile, enteFileDeletionDate } from "@/media/file";
|
||||
import { FileType } from "@/media/file-type";
|
||||
import {
|
||||
GAP_BTW_TILES,
|
||||
IMAGE_CONTAINER_MAX_HEIGHT,
|
||||
IMAGE_CONTAINER_MAX_WIDTH,
|
||||
MIN_COLUMNS,
|
||||
} from "@/new/photos/components/FileList";
|
||||
import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer";
|
||||
import {
|
||||
LoadingThumbnail,
|
||||
StaticThumbnail,
|
||||
} from "@/new/photos/components/PlaceholderThumbnails";
|
||||
import { TileBottomTextOverlay } from "@/new/photos/components/Tiles";
|
||||
import { TRASH_SECTION } from "@/new/photos/services/collection";
|
||||
import { FlexWrapper } from "@ente/shared/components/Container";
|
||||
import useLongPress from "@ente/shared/hooks/useLongPress";
|
||||
import AlbumOutlinedIcon from "@mui/icons-material/AlbumOutlined";
|
||||
import FavoriteRoundedIcon from "@mui/icons-material/FavoriteRounded";
|
||||
import PlayCircleOutlineOutlinedIcon from "@mui/icons-material/PlayCircleOutlineOutlined";
|
||||
import { Box, Checkbox, Link, Typography, styled } from "@mui/material";
|
||||
import type { PhotoFrameProps } from "components/PhotoFrame";
|
||||
import Avatar from "components/pages/gallery/Avatar";
|
||||
import { t } from "i18next";
|
||||
import memoize from "memoize-one";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
@@ -20,12 +35,13 @@ import {
|
||||
ListChildComponentProps,
|
||||
areEqual,
|
||||
} from "react-window";
|
||||
import { SelectedState } from "types/gallery";
|
||||
import { shouldShowAvatar } from "utils/file";
|
||||
import {
|
||||
handleSelectCreator,
|
||||
handleSelectCreatorMulti,
|
||||
} from "utils/photoFrame";
|
||||
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
|
||||
import PreviewCard from "./pages/gallery/PreviewCard";
|
||||
|
||||
export const DATE_CONTAINER_HEIGHT = 48;
|
||||
export const SPACE_BTW_DATES = 44;
|
||||
@@ -59,154 +75,58 @@ export interface TimeStampListItem {
|
||||
fileCount?: number;
|
||||
}
|
||||
|
||||
const ListItem = styled("div")`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const getTemplateColumns = (
|
||||
columns: number,
|
||||
shrinkRatio: number,
|
||||
groups?: number[],
|
||||
): string => {
|
||||
if (groups) {
|
||||
// need to confirm why this was there
|
||||
// const sum = groups.reduce((acc, item) => acc + item, 0);
|
||||
// if (sum < columns) {
|
||||
// groups[groups.length - 1] += columns - sum;
|
||||
// }
|
||||
return groups
|
||||
.map(
|
||||
(x) =>
|
||||
`repeat(${x}, ${IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio}px)`,
|
||||
)
|
||||
.join(` ${SPACE_BTW_DATES}px `);
|
||||
} else {
|
||||
return `repeat(${columns},${
|
||||
IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio
|
||||
}px)`;
|
||||
}
|
||||
};
|
||||
|
||||
function getFractionFittableColumns(width: number): number {
|
||||
return (
|
||||
(width - 2 * getGapFromScreenEdge(width) + GAP_BTW_TILES) /
|
||||
(IMAGE_CONTAINER_MAX_WIDTH + GAP_BTW_TILES)
|
||||
);
|
||||
}
|
||||
|
||||
function getGapFromScreenEdge(width: number) {
|
||||
if (width > MIN_COLUMNS * IMAGE_CONTAINER_MAX_WIDTH) {
|
||||
return 24;
|
||||
} else {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
function getShrinkRatio(width: number, columns: number) {
|
||||
return (
|
||||
(width -
|
||||
2 * getGapFromScreenEdge(width) -
|
||||
(columns - 1) * GAP_BTW_TILES) /
|
||||
(columns * IMAGE_CONTAINER_MAX_WIDTH)
|
||||
);
|
||||
}
|
||||
|
||||
const ListContainer = styled(Box, {
|
||||
shouldForwardProp: (propName) => propName != "gridTemplateColumns",
|
||||
})<{ gridTemplateColumns: string }>`
|
||||
display: grid;
|
||||
grid-template-columns: ${(props) => props.gridTemplateColumns};
|
||||
grid-column-gap: ${GAP_BTW_TILES}px;
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ListItemContainer = styled(FlexWrapper)<{ span: number }>`
|
||||
grid-column: span ${(props) => props.span};
|
||||
`;
|
||||
|
||||
const DateContainer = styled(ListItemContainer)(
|
||||
({ theme }) => `
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: ${DATE_CONTAINER_HEIGHT}px;
|
||||
color: ${theme.vars.palette.text.muted};
|
||||
`,
|
||||
);
|
||||
|
||||
const FooterContainer = styled(ListItemContainer)`
|
||||
margin-bottom: 0.75rem;
|
||||
@media (max-width: 540px) {
|
||||
font-size: 12px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
margin-top: calc(2rem + 20px);
|
||||
`;
|
||||
|
||||
const AlbumFooterContainer = styled(ListItemContainer, {
|
||||
shouldForwardProp: (propName) => propName != "hasReferral",
|
||||
})<{ hasReferral: boolean }>`
|
||||
margin-top: 48px;
|
||||
margin-bottom: ${({ hasReferral }) => (!hasReferral ? `10px` : "0px")};
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const FullStretchContainer = styled("div")(
|
||||
({ theme }) => `
|
||||
margin: 0 -24px;
|
||||
width: calc(100% + 46px);
|
||||
left: -24px;
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) {
|
||||
margin: 0 -4px;
|
||||
width: calc(100% + 6px);
|
||||
left: -4px;
|
||||
}
|
||||
background-color: ${theme.vars.palette.accent.main};
|
||||
`,
|
||||
);
|
||||
|
||||
const NothingContainer = styled(ListItemContainer)`
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export interface FileListAnnotatedFile {
|
||||
file: EnteFile;
|
||||
/**
|
||||
* The date string using with the associated {@link file} should be shown in
|
||||
* the timeline.
|
||||
*
|
||||
* This for used for grouping files: all files which have the same
|
||||
* {@link timelineDateString} are grouped together into a section titled
|
||||
* with that {@link timelineDateString}.
|
||||
* [Note: Timeline date string]
|
||||
*
|
||||
* The timeline date string is a formatted date string under which a
|
||||
* particular file should be grouped in the gallery listing. e.g. "Today",
|
||||
* "Yesterday", "Fri, 21 Feb" etc.
|
||||
*
|
||||
* All files which have the same timelineDateString will be grouped under a
|
||||
* single section in the gallery listing, prefixed by the timelineDateString
|
||||
* itself, and a checkbox to select all files on that date.
|
||||
*/
|
||||
timelineDateString: string;
|
||||
}
|
||||
|
||||
type FileListProps = Pick<
|
||||
PhotoFrameProps,
|
||||
| "mode"
|
||||
| "modePlus"
|
||||
| "selectable"
|
||||
| "selected"
|
||||
| "setSelected"
|
||||
| "favoriteFileIDs"
|
||||
> & {
|
||||
export interface FileListProps {
|
||||
/** The height we should occupy (needed since the list is virtualized). */
|
||||
height: number;
|
||||
/** The width we should occupy.*/
|
||||
width: number;
|
||||
/**
|
||||
* The files to show, annotated with cached precomputed properties that are
|
||||
* frequently needed by the {@link FileList}.
|
||||
*/
|
||||
annotatedFiles: FileListAnnotatedFile[];
|
||||
showAppDownloadBanner: boolean;
|
||||
mode?: GalleryBarMode;
|
||||
/**
|
||||
* This is an experimental prop, to see if we can merge the separate
|
||||
* "isInSearchMode" state kept by the gallery to be instead provided as a
|
||||
* another mode in which the gallery operates.
|
||||
*/
|
||||
modePlus?: GalleryBarMode | "search";
|
||||
showAppDownloadBanner?: boolean;
|
||||
selectable?: boolean;
|
||||
setSelected: (
|
||||
selected: SelectedState | ((selected: SelectedState) => SelectedState),
|
||||
) => void;
|
||||
selected: SelectedState;
|
||||
/** This will be set if mode is not "people". */
|
||||
activeCollectionID: number;
|
||||
activePersonID?: string;
|
||||
/** This will be set if mode is "people". */
|
||||
activePersonID?: string | undefined;
|
||||
/**
|
||||
* File IDs of all the files that the user has marked as a favorite.
|
||||
*
|
||||
* Not set in the context of the shared albums app.
|
||||
*/
|
||||
favoriteFileIDs?: Set<number>;
|
||||
/**
|
||||
* Called when the user activates the thumbnail at the given {@link index}.
|
||||
*
|
||||
@@ -214,55 +134,11 @@ type FileListProps = Pick<
|
||||
* {@link annotatedFiles}.
|
||||
*/
|
||||
onItemClick: (index: number) => void;
|
||||
};
|
||||
|
||||
interface ItemData {
|
||||
timeStampList: TimeStampListItem[];
|
||||
columns: number;
|
||||
shrinkRatio: number;
|
||||
renderListItem: (
|
||||
timeStampListItem: TimeStampListItem,
|
||||
isScrolling?: boolean,
|
||||
) => React.JSX.Element;
|
||||
}
|
||||
|
||||
const createItemData = memoize(
|
||||
(
|
||||
timeStampList: TimeStampListItem[],
|
||||
columns: number,
|
||||
shrinkRatio: number,
|
||||
renderListItem: (
|
||||
timeStampListItem: TimeStampListItem,
|
||||
isScrolling?: boolean,
|
||||
) => React.JSX.Element,
|
||||
): ItemData => ({ timeStampList, columns, shrinkRatio, renderListItem }),
|
||||
);
|
||||
|
||||
const PhotoListRow = React.memo(
|
||||
({
|
||||
index,
|
||||
style,
|
||||
isScrolling,
|
||||
data,
|
||||
}: ListChildComponentProps<ItemData>) => {
|
||||
const { timeStampList, columns, shrinkRatio, renderListItem } = data;
|
||||
return (
|
||||
<ListItem style={style}>
|
||||
<ListContainer
|
||||
gridTemplateColumns={getTemplateColumns(
|
||||
columns,
|
||||
shrinkRatio,
|
||||
timeStampList[index].groups,
|
||||
)}
|
||||
>
|
||||
{renderListItem(timeStampList[index], isScrolling)}
|
||||
</ListContainer>
|
||||
</ListItem>
|
||||
);
|
||||
},
|
||||
areEqual,
|
||||
);
|
||||
|
||||
/**
|
||||
* A virtualized list of files, each represented by their thumbnail.
|
||||
*/
|
||||
export const FileList: React.FC<FileListProps> = ({
|
||||
height,
|
||||
width,
|
||||
@@ -545,7 +421,7 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
} else {
|
||||
footerHeight = FOOTER_HEIGHT;
|
||||
}
|
||||
const photoFrameHeight = (() => {
|
||||
const fileListHeight = (() => {
|
||||
let sum = 0;
|
||||
const getCurrentItemSize = getItemSize(timeStampList);
|
||||
for (let i = 0; i < timeStampList.length; i++) {
|
||||
@@ -559,7 +435,7 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
return {
|
||||
itemType: ITEM_TYPE.OTHER,
|
||||
item: <></>,
|
||||
height: Math.max(height - photoFrameHeight - footerHeight, 0),
|
||||
height: Math.max(height - fileListHeight - footerHeight, 0),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -886,7 +762,7 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
index: number,
|
||||
isScrolling: boolean,
|
||||
) => (
|
||||
<PreviewCard
|
||||
<FileThumbnail
|
||||
key={`tile-${file.id}-selected-${selected[file.id] ?? false}`}
|
||||
file={file}
|
||||
onClick={() => onItemClick(index)}
|
||||
@@ -1039,3 +915,472 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
const ListItem = styled("div")`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const getTemplateColumns = (
|
||||
columns: number,
|
||||
shrinkRatio: number,
|
||||
groups?: number[],
|
||||
): string => {
|
||||
if (groups) {
|
||||
// need to confirm why this was there
|
||||
// const sum = groups.reduce((acc, item) => acc + item, 0);
|
||||
// if (sum < columns) {
|
||||
// groups[groups.length - 1] += columns - sum;
|
||||
// }
|
||||
return groups
|
||||
.map(
|
||||
(x) =>
|
||||
`repeat(${x}, ${IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio}px)`,
|
||||
)
|
||||
.join(` ${SPACE_BTW_DATES}px `);
|
||||
} else {
|
||||
return `repeat(${columns},${
|
||||
IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio
|
||||
}px)`;
|
||||
}
|
||||
};
|
||||
|
||||
function getFractionFittableColumns(width: number): number {
|
||||
return (
|
||||
(width - 2 * getGapFromScreenEdge(width) + GAP_BTW_TILES) /
|
||||
(IMAGE_CONTAINER_MAX_WIDTH + GAP_BTW_TILES)
|
||||
);
|
||||
}
|
||||
|
||||
function getGapFromScreenEdge(width: number) {
|
||||
if (width > MIN_COLUMNS * IMAGE_CONTAINER_MAX_WIDTH) {
|
||||
return 24;
|
||||
} else {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
function getShrinkRatio(width: number, columns: number) {
|
||||
return (
|
||||
(width -
|
||||
2 * getGapFromScreenEdge(width) -
|
||||
(columns - 1) * GAP_BTW_TILES) /
|
||||
(columns * IMAGE_CONTAINER_MAX_WIDTH)
|
||||
);
|
||||
}
|
||||
|
||||
const ListContainer = styled(Box, {
|
||||
shouldForwardProp: (propName) => propName != "gridTemplateColumns",
|
||||
})<{ gridTemplateColumns: string }>`
|
||||
display: grid;
|
||||
grid-template-columns: ${(props) => props.gridTemplateColumns};
|
||||
grid-column-gap: ${GAP_BTW_TILES}px;
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ListItemContainer = styled(FlexWrapper)<{ span: number }>`
|
||||
grid-column: span ${(props) => props.span};
|
||||
`;
|
||||
|
||||
const DateContainer = styled(ListItemContainer)(
|
||||
({ theme }) => `
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: ${DATE_CONTAINER_HEIGHT}px;
|
||||
color: ${theme.vars.palette.text.muted};
|
||||
`,
|
||||
);
|
||||
|
||||
const FooterContainer = styled(ListItemContainer)`
|
||||
margin-bottom: 0.75rem;
|
||||
@media (max-width: 540px) {
|
||||
font-size: 12px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
margin-top: calc(2rem + 20px);
|
||||
`;
|
||||
|
||||
const AlbumFooterContainer = styled(ListItemContainer, {
|
||||
shouldForwardProp: (propName) => propName != "hasReferral",
|
||||
})<{ hasReferral: boolean }>`
|
||||
margin-top: 48px;
|
||||
margin-bottom: ${({ hasReferral }) => (!hasReferral ? `10px` : "0px")};
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const FullStretchContainer = styled("div")(
|
||||
({ theme }) => `
|
||||
margin: 0 -24px;
|
||||
width: calc(100% + 46px);
|
||||
left: -24px;
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) {
|
||||
margin: 0 -4px;
|
||||
width: calc(100% + 6px);
|
||||
left: -4px;
|
||||
}
|
||||
background-color: ${theme.vars.palette.accent.main};
|
||||
`,
|
||||
);
|
||||
|
||||
const NothingContainer = styled(ListItemContainer)`
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
interface ItemData {
|
||||
timeStampList: TimeStampListItem[];
|
||||
columns: number;
|
||||
shrinkRatio: number;
|
||||
renderListItem: (
|
||||
timeStampListItem: TimeStampListItem,
|
||||
isScrolling?: boolean,
|
||||
) => React.JSX.Element;
|
||||
}
|
||||
|
||||
const createItemData = memoize(
|
||||
(
|
||||
timeStampList: TimeStampListItem[],
|
||||
columns: number,
|
||||
shrinkRatio: number,
|
||||
renderListItem: (
|
||||
timeStampListItem: TimeStampListItem,
|
||||
isScrolling?: boolean,
|
||||
) => React.JSX.Element,
|
||||
): ItemData => ({ timeStampList, columns, shrinkRatio, renderListItem }),
|
||||
);
|
||||
|
||||
const PhotoListRow = React.memo(
|
||||
({
|
||||
index,
|
||||
style,
|
||||
isScrolling,
|
||||
data,
|
||||
}: ListChildComponentProps<ItemData>) => {
|
||||
const { timeStampList, columns, shrinkRatio, renderListItem } = data;
|
||||
return (
|
||||
<ListItem style={style}>
|
||||
<ListContainer
|
||||
gridTemplateColumns={getTemplateColumns(
|
||||
columns,
|
||||
shrinkRatio,
|
||||
timeStampList[index].groups,
|
||||
)}
|
||||
>
|
||||
{renderListItem(timeStampList[index], isScrolling)}
|
||||
</ListContainer>
|
||||
</ListItem>
|
||||
);
|
||||
},
|
||||
areEqual,
|
||||
);
|
||||
|
||||
interface FileThumbnailProps {
|
||||
file: EnteFile;
|
||||
onClick: () => void;
|
||||
selectable: boolean;
|
||||
selected: boolean;
|
||||
onSelect: (checked: boolean) => void;
|
||||
onHover: () => void;
|
||||
onRangeSelect: () => void;
|
||||
isRangeSelectActive: boolean;
|
||||
selectOnClick: boolean;
|
||||
isInsSelectRange: boolean;
|
||||
activeCollectionID: number;
|
||||
showPlaceholder: boolean;
|
||||
isFav: boolean;
|
||||
}
|
||||
|
||||
const FileThumbnail: React.FC<FileThumbnailProps> = ({
|
||||
file,
|
||||
onClick,
|
||||
selectable,
|
||||
selected,
|
||||
onSelect,
|
||||
selectOnClick,
|
||||
onHover,
|
||||
onRangeSelect,
|
||||
isRangeSelectActive,
|
||||
isInsSelectRange,
|
||||
isFav,
|
||||
activeCollectionID,
|
||||
showPlaceholder,
|
||||
}) => {
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
||||
const [imageURL, setImageURL] = useState<string | undefined>(undefined);
|
||||
|
||||
const longPress = useLongPress(() => onSelect(!selected), 500);
|
||||
|
||||
useEffect(() => {
|
||||
let didCancel = false;
|
||||
|
||||
void downloadManager
|
||||
.renderableThumbnailURL(file, showPlaceholder)
|
||||
.then((url) => !didCancel && setImageURL(url));
|
||||
|
||||
return () => {
|
||||
didCancel = true;
|
||||
};
|
||||
}, [file, showPlaceholder]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (selectOnClick) {
|
||||
if (isRangeSelectActive) {
|
||||
onRangeSelect();
|
||||
} else {
|
||||
onSelect(!selected);
|
||||
}
|
||||
} else if (imageURL) {
|
||||
onClick?.();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
if (isRangeSelectActive) {
|
||||
onRangeSelect?.();
|
||||
} else {
|
||||
onSelect(e.target.checked);
|
||||
}
|
||||
};
|
||||
|
||||
const handleHover = () => {
|
||||
if (isRangeSelectActive) {
|
||||
onHover();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FileThumbnail_
|
||||
key={`thumb-${file.id}}`}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleHover}
|
||||
disabled={!imageURL}
|
||||
{...(selectable ? longPress : {})}
|
||||
>
|
||||
{selectable && (
|
||||
<Check
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
onChange={handleSelect}
|
||||
$active={isRangeSelectActive && isInsSelectRange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
{file.metadata.hasStaticThumbnail ? (
|
||||
<StaticThumbnail fileType={file.metadata.fileType} />
|
||||
) : imageURL ? (
|
||||
<img src={imageURL} />
|
||||
) : (
|
||||
<LoadingThumbnail />
|
||||
)}
|
||||
{file.metadata.fileType === FileType.livePhoto ? (
|
||||
<FileTypeIndicatorOverlay>
|
||||
<AlbumOutlinedIcon />
|
||||
</FileTypeIndicatorOverlay>
|
||||
) : (
|
||||
file.metadata.fileType === FileType.video && (
|
||||
<FileTypeIndicatorOverlay>
|
||||
<PlayCircleOutlineOutlinedIcon />
|
||||
</FileTypeIndicatorOverlay>
|
||||
)
|
||||
)}
|
||||
{selected && <SelectedOverlay />}
|
||||
{shouldShowAvatar(file, galleryContext.user) && (
|
||||
<AvatarOverlay>
|
||||
<Avatar file={file} />
|
||||
</AvatarOverlay>
|
||||
)}
|
||||
{isFav && (
|
||||
<FavoriteOverlay>
|
||||
<FavoriteRoundedIcon />
|
||||
</FavoriteOverlay>
|
||||
)}
|
||||
|
||||
<HoverOverlay
|
||||
className="preview-card-hover-overlay"
|
||||
checked={selected}
|
||||
/>
|
||||
{isRangeSelectActive && isInsSelectRange && (
|
||||
<InSelectRangeOverlay />
|
||||
)}
|
||||
|
||||
{activeCollectionID === TRASH_SECTION && file.isTrashed && (
|
||||
<TileBottomTextOverlay>
|
||||
<Typography variant="small">
|
||||
{formattedDateRelative(enteFileDeletionDate(file))}
|
||||
</Typography>
|
||||
</TileBottomTextOverlay>
|
||||
)}
|
||||
</FileThumbnail_>
|
||||
);
|
||||
};
|
||||
|
||||
const FileThumbnail_ = styled("div")<{ disabled: boolean }>`
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin-bottom: ${GAP_BTW_TILES}px;
|
||||
min-width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
|
||||
user-select: none;
|
||||
& > img {
|
||||
object-fit: cover;
|
||||
max-width: 100%;
|
||||
min-height: 100%;
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
input[type="checkbox"] {
|
||||
visibility: visible;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.preview-card-hover-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
const Check = styled("input")<{ $active: boolean }>(
|
||||
({ theme, $active }) => `
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
/* Increase z-index in stacking order to capture clicks */
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
@media (pointer: coarse) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
background-color: #ddd;
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
vertical-align: bottom;
|
||||
margin: 6px 6px;
|
||||
transition: background-color 0.3s ease;
|
||||
pointer-events: inherit;
|
||||
|
||||
}
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 11px;
|
||||
border-right: 2px solid #333;
|
||||
border-bottom: 2px solid #333;
|
||||
transition: transform 0.3s ease;
|
||||
pointer-events: inherit;
|
||||
transform: translate(-18px, 9px) rotate(45deg);
|
||||
}
|
||||
|
||||
/* checkmark background (filled circle) */
|
||||
&:checked::before {
|
||||
content: "";
|
||||
background-color: ${theme.vars.palette.accent.main};
|
||||
border-color: ${theme.vars.palette.accent.main};
|
||||
color: white;
|
||||
}
|
||||
/* checkmark foreground (tick) */
|
||||
&:checked::after {
|
||||
content: "";
|
||||
border-right: 2px solid #ddd;
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
visibility: hidden;
|
||||
${$active && "visibility: visible; opacity: 0.5;"};
|
||||
&:checked {
|
||||
visibility: visible;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const HoverOverlay = styled("div")<{ checked: boolean }>`
|
||||
opacity: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
outline: none;
|
||||
height: 40%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
${(props) =>
|
||||
!props.checked &&
|
||||
"background:linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0))"};
|
||||
`;
|
||||
|
||||
/**
|
||||
* An overlay showing the avatars of the person who shared the item, at the top
|
||||
* right.
|
||||
*/
|
||||
const AvatarOverlay = styled(Overlay)`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
/**
|
||||
* An overlay showing the favorite icon at bottom left.
|
||||
*/
|
||||
const FavoriteOverlay = styled(Overlay)`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
padding: 5px;
|
||||
color: white;
|
||||
opacity: 0.6;
|
||||
`;
|
||||
|
||||
/**
|
||||
* An overlay with a gradient, showing the file type indicator (e.g. live photo,
|
||||
* video) at the bottom right.
|
||||
*/
|
||||
const FileTypeIndicatorOverlay = styled(Overlay)`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
padding: 5px;
|
||||
color: white;
|
||||
background: linear-gradient(
|
||||
315deg,
|
||||
rgba(0 0 0 / 0.14) 0%,
|
||||
rgba(0 0 0 / 0.05) 30%,
|
||||
transparent 50%
|
||||
);
|
||||
`;
|
||||
|
||||
const InSelectRangeOverlay = styled(Overlay)(
|
||||
({ theme }) => `
|
||||
outline: none;
|
||||
background: ${theme.vars.palette.accent.main};
|
||||
opacity: 0.14;
|
||||
`,
|
||||
);
|
||||
|
||||
const SelectedOverlay = styled(Overlay)(
|
||||
({ theme }) => `
|
||||
border: 2px solid ${theme.vars.palette.accent.main};
|
||||
border-radius: 4px;
|
||||
`,
|
||||
);
|
||||
|
||||
@@ -1,107 +1,35 @@
|
||||
import { isSameDay } from "@/base/date";
|
||||
import { formattedDate } from "@/base/i18n-date";
|
||||
import type { FileInfoProps } from "@/gallery/components/FileInfo";
|
||||
import { FileViewer } from "@/gallery/components/viewer/FileViewer";
|
||||
import { type RenderableSourceURLs } from "@/gallery/services/download";
|
||||
import {
|
||||
FileViewer,
|
||||
type FileViewerProps,
|
||||
} from "@/gallery/components/viewer/FileViewer";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { EnteFile } from "@/media/file";
|
||||
import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer";
|
||||
import { moveToTrash, TRASH_SECTION } from "@/new/photos/services/collection";
|
||||
import { styled } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
import { useCallback, useContext, useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import AutoSizer from "react-virtualized-auto-sizer";
|
||||
import {
|
||||
addToFavorites,
|
||||
removeFromFavorites,
|
||||
} from "services/collectionService";
|
||||
import uploadManager from "services/upload/uploadManager";
|
||||
import {
|
||||
SelectedState,
|
||||
SetFilesDownloadProgressAttributesCreator,
|
||||
} from "types/gallery";
|
||||
import { SetFilesDownloadProgressAttributesCreator } from "types/gallery";
|
||||
import { downloadSingleFile } from "utils/file";
|
||||
import { FileList, type FileListAnnotatedFile } from "./FileList";
|
||||
import {
|
||||
FileList,
|
||||
type FileListAnnotatedFile,
|
||||
type FileListProps,
|
||||
} from "./FileList";
|
||||
|
||||
const Container = styled("div")`
|
||||
display: block;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
.pswp-thumbnail {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* An {@link EnteFile} augmented with various in-memory state used for
|
||||
* displaying it in the photo viewer.
|
||||
*/
|
||||
export type DisplayFile = EnteFile & {
|
||||
src?: string;
|
||||
srcURLs?: RenderableSourceURLs;
|
||||
export type FileListWithViewerProps = {
|
||||
/**
|
||||
* An object URL corresponding to the image portion, if any, associated with
|
||||
* the {@link DisplayFile}.
|
||||
*
|
||||
* - For images, this will be the object URL of the renderable image itself.
|
||||
* - For live photos, this will be the object URL of the image portion of
|
||||
* the live photo.
|
||||
* - For videos, this will not be defined.
|
||||
* The list of files to show.
|
||||
*/
|
||||
associatedImageURL?: string | undefined;
|
||||
msrc?: string;
|
||||
html?: string;
|
||||
w?: number;
|
||||
h?: number;
|
||||
title?: string;
|
||||
isSourceLoaded?: boolean;
|
||||
conversionFailed?: boolean;
|
||||
canForceConvert?: boolean;
|
||||
/**
|
||||
* [Note: Timeline date string]
|
||||
*
|
||||
* The timeline date string is a formatted date string under which a
|
||||
* particular file should be grouped in the gallery listing. e.g. "Today",
|
||||
* "Yesterday", "Fri, 21 Feb" etc.
|
||||
*
|
||||
* All files which have the same timelineDateString will be grouped under a
|
||||
* single section in the gallery listing, prefixed by the timelineDateString
|
||||
* itself, and a checkbox to select all files on that date.
|
||||
*/
|
||||
timelineDateString?: string;
|
||||
};
|
||||
|
||||
export type PhotoFrameProps = Pick<
|
||||
FileInfoProps,
|
||||
| "fileCollectionIDs"
|
||||
| "allCollectionsNameByID"
|
||||
| "onSelectCollection"
|
||||
| "onSelectPerson"
|
||||
> & {
|
||||
mode?: GalleryBarMode;
|
||||
/**
|
||||
* This is an experimental prop, to see if we can merge the separate
|
||||
* "isInSearchMode" state kept by the gallery to be instead provided as a
|
||||
* another mode in which the gallery operates.
|
||||
*/
|
||||
modePlus?: GalleryBarMode | "search";
|
||||
files: EnteFile[];
|
||||
selectable?: boolean;
|
||||
setSelected: (
|
||||
selected: SelectedState | ((selected: SelectedState) => SelectedState),
|
||||
) => void;
|
||||
selected: SelectedState;
|
||||
/**
|
||||
* File IDs of all the files that the user has marked as a favorite.
|
||||
*
|
||||
* Not set in the context of the shared albums app.
|
||||
*/
|
||||
favoriteFileIDs?: Set<number>;
|
||||
enableDownload?: boolean;
|
||||
/**
|
||||
* Called when the component wants to update the in-memory, unsynced,
|
||||
* favorite status of a file.
|
||||
@@ -125,51 +53,68 @@ export type PhotoFrameProps = Pick<
|
||||
* Not set in the context of the shared albums app.
|
||||
*/
|
||||
onMarkTempDeleted?: (files: EnteFile[]) => void;
|
||||
/** This will be set if mode is not "people". */
|
||||
activeCollectionID: number;
|
||||
/** This will be set if mode is "people". */
|
||||
activePersonID?: string | undefined;
|
||||
enableDownload?: boolean;
|
||||
showAppDownloadBanner?: boolean;
|
||||
isInIncomingSharedCollection?: boolean;
|
||||
isInHiddenSection?: boolean;
|
||||
setFilesDownloadProgressAttributesCreator?: SetFilesDownloadProgressAttributesCreator;
|
||||
/**
|
||||
* Called when the visibility of the file viewer dialog changes.
|
||||
*/
|
||||
onSetOpenFileViewer?: (open: boolean) => void;
|
||||
/**
|
||||
* Called when an action in the file viewer requires us to sync with remote.
|
||||
*/
|
||||
onSyncWithRemote: () => Promise<void>;
|
||||
};
|
||||
} & Pick<
|
||||
FileListProps,
|
||||
| "mode"
|
||||
| "modePlus"
|
||||
| "showAppDownloadBanner"
|
||||
| "selectable"
|
||||
| "selected"
|
||||
| "setSelected"
|
||||
| "activeCollectionID"
|
||||
| "activePersonID"
|
||||
| "favoriteFileIDs"
|
||||
> &
|
||||
Pick<
|
||||
FileViewerProps,
|
||||
| "user"
|
||||
| "isInIncomingSharedCollection"
|
||||
| "isInHiddenSection"
|
||||
| "fileCollectionIDs"
|
||||
| "allCollectionsNameByID"
|
||||
| "onSelectCollection"
|
||||
| "onSelectPerson"
|
||||
>;
|
||||
|
||||
/**
|
||||
* TODO: Rename me to FileListWithViewer (or Gallery?)
|
||||
* A list of files (represented by their thumbnails), along with a file viewer
|
||||
* that opens on activating the thumbnail (and also allows the user to navigate
|
||||
* through this list of files).
|
||||
*/
|
||||
const PhotoFrame = ({
|
||||
export const FileListWithViewer: React.FC<FileListWithViewerProps> = ({
|
||||
mode,
|
||||
modePlus,
|
||||
user,
|
||||
files,
|
||||
enableDownload,
|
||||
showAppDownloadBanner,
|
||||
selectable,
|
||||
selected,
|
||||
setSelected,
|
||||
favoriteFileIDs,
|
||||
onMarkUnsyncedFavoriteUpdate,
|
||||
onMarkTempDeleted,
|
||||
activeCollectionID,
|
||||
activePersonID,
|
||||
enableDownload,
|
||||
fileCollectionIDs,
|
||||
allCollectionsNameByID,
|
||||
showAppDownloadBanner,
|
||||
favoriteFileIDs,
|
||||
isInIncomingSharedCollection,
|
||||
isInHiddenSection,
|
||||
fileCollectionIDs,
|
||||
allCollectionsNameByID,
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
onMarkUnsyncedFavoriteUpdate,
|
||||
onMarkTempDeleted,
|
||||
onSetOpenFileViewer,
|
||||
onSyncWithRemote,
|
||||
onSelectCollection,
|
||||
onSelectPerson,
|
||||
}: PhotoFrameProps) => {
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
||||
}) => {
|
||||
const [openFileViewer, setOpenFileViewer] = useState(false);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
@@ -245,18 +190,18 @@ const PhotoFrame = ({
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<FileList
|
||||
{...{ width, height, annotatedFiles }}
|
||||
{...{
|
||||
mode,
|
||||
modePlus,
|
||||
showAppDownloadBanner,
|
||||
selectable,
|
||||
selected,
|
||||
setSelected,
|
||||
activeCollectionID,
|
||||
activePersonID,
|
||||
showAppDownloadBanner,
|
||||
favoriteFileIDs,
|
||||
}}
|
||||
{...{ width, height, annotatedFiles }}
|
||||
onItemClick={handleThumbnailClick}
|
||||
/>
|
||||
)}
|
||||
@@ -264,31 +209,34 @@ const PhotoFrame = ({
|
||||
<FileViewer
|
||||
open={openFileViewer}
|
||||
onClose={handleCloseFileViewer}
|
||||
user={galleryContext.user ?? undefined}
|
||||
files={files}
|
||||
initialIndex={currentIndex}
|
||||
disableDownload={!enableDownload}
|
||||
isInIncomingSharedCollection={isInIncomingSharedCollection}
|
||||
isInTrashSection={activeCollectionID === TRASH_SECTION}
|
||||
isInHiddenSection={isInHiddenSection}
|
||||
onTriggerSyncWithRemote={handleTriggerSyncWithRemote}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
onDownload={handleDownload}
|
||||
onDelete={handleDelete}
|
||||
onSaveEditedImageCopy={handleSaveEditedImageCopy}
|
||||
{...{
|
||||
user,
|
||||
files,
|
||||
isInHiddenSection,
|
||||
isInIncomingSharedCollection,
|
||||
favoriteFileIDs,
|
||||
fileCollectionIDs,
|
||||
allCollectionsNameByID,
|
||||
onSelectCollection,
|
||||
onSelectPerson,
|
||||
}}
|
||||
onTriggerSyncWithRemote={handleTriggerSyncWithRemote}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
onDownload={handleDownload}
|
||||
onDelete={handleDelete}
|
||||
onSaveEditedImageCopy={handleSaveEditedImageCopy}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhotoFrame;
|
||||
const Container = styled("div")`
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
/**
|
||||
* See: [Note: Timeline date string]
|
||||
@@ -1,349 +0,0 @@
|
||||
import { Overlay } from "@/base/components/containers";
|
||||
import { formattedDateRelative } from "@/base/i18n-date";
|
||||
import log from "@/base/log";
|
||||
import { downloadManager } from "@/gallery/services/download";
|
||||
import { enteFileDeletionDate } from "@/media/file";
|
||||
import { FileType } from "@/media/file-type";
|
||||
import { GAP_BTW_TILES } from "@/new/photos/components/FileList";
|
||||
import {
|
||||
LoadingThumbnail,
|
||||
StaticThumbnail,
|
||||
} from "@/new/photos/components/PlaceholderThumbnails";
|
||||
import { TileBottomTextOverlay } from "@/new/photos/components/Tiles";
|
||||
import { TRASH_SECTION } from "@/new/photos/services/collection";
|
||||
import useLongPress from "@ente/shared/hooks/useLongPress";
|
||||
import AlbumOutlinedIcon from "@mui/icons-material/AlbumOutlined";
|
||||
import FavoriteRoundedIcon from "@mui/icons-material/FavoriteRounded";
|
||||
import PlayCircleOutlineOutlinedIcon from "@mui/icons-material/PlayCircleOutlineOutlined";
|
||||
import { styled, Typography } from "@mui/material";
|
||||
import type { DisplayFile } from "components/PhotoFrame";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { shouldShowAvatar } from "utils/file";
|
||||
import Avatar from "./Avatar";
|
||||
|
||||
interface PreviewCardProps {
|
||||
file: DisplayFile;
|
||||
onClick: () => void;
|
||||
selectable: boolean;
|
||||
selected: boolean;
|
||||
onSelect: (checked: boolean) => void;
|
||||
onHover: () => void;
|
||||
onRangeSelect: () => void;
|
||||
isRangeSelectActive: boolean;
|
||||
selectOnClick: boolean;
|
||||
isInsSelectRange: boolean;
|
||||
activeCollectionID: number;
|
||||
showPlaceholder: boolean;
|
||||
isFav: boolean;
|
||||
}
|
||||
|
||||
const Check = styled("input")<{ $active: boolean }>(
|
||||
({ theme, $active }) => `
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
/* Increase z-index in stacking order to capture clicks */
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
@media (pointer: coarse) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
background-color: #ddd;
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
vertical-align: bottom;
|
||||
margin: 6px 6px;
|
||||
transition: background-color 0.3s ease;
|
||||
pointer-events: inherit;
|
||||
|
||||
}
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 11px;
|
||||
border-right: 2px solid #333;
|
||||
border-bottom: 2px solid #333;
|
||||
transition: transform 0.3s ease;
|
||||
pointer-events: inherit;
|
||||
transform: translate(-18px, 9px) rotate(45deg);
|
||||
}
|
||||
|
||||
/* checkmark background (filled circle) */
|
||||
&:checked::before {
|
||||
content: "";
|
||||
background-color: ${theme.vars.palette.accent.main};
|
||||
border-color: ${theme.vars.palette.accent.main};
|
||||
color: white;
|
||||
}
|
||||
/* checkmark foreground (tick) */
|
||||
&:checked::after {
|
||||
content: "";
|
||||
border-right: 2px solid #ddd;
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
visibility: hidden;
|
||||
${$active && "visibility: visible; opacity: 0.5;"};
|
||||
&:checked {
|
||||
visibility: visible;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const HoverOverlay = styled("div")<{ checked: boolean }>`
|
||||
opacity: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
outline: none;
|
||||
height: 40%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
${(props) =>
|
||||
!props.checked &&
|
||||
"background:linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0))"};
|
||||
`;
|
||||
|
||||
/**
|
||||
* An overlay showing the avatars of the person who shared the item, at the top
|
||||
* right.
|
||||
*/
|
||||
const AvatarOverlay = styled(Overlay)`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
/**
|
||||
* An overlay showing the favorite icon at bottom left.
|
||||
*/
|
||||
const FavoriteOverlay = styled(Overlay)`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
padding: 5px;
|
||||
color: white;
|
||||
opacity: 0.6;
|
||||
`;
|
||||
|
||||
/**
|
||||
* An overlay with a gradient, showing the file type indicator (e.g. live photo,
|
||||
* video) at the bottom right.
|
||||
*/
|
||||
const FileTypeIndicatorOverlay = styled(Overlay)`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
padding: 5px;
|
||||
color: white;
|
||||
background: linear-gradient(
|
||||
315deg,
|
||||
rgba(0 0 0 / 0.14) 0%,
|
||||
rgba(0 0 0 / 0.05) 30%,
|
||||
transparent 50%
|
||||
);
|
||||
`;
|
||||
|
||||
const InSelectRangeOverlay = styled(Overlay)(
|
||||
({ theme }) => `
|
||||
outline: none;
|
||||
background: ${theme.vars.palette.accent.main};
|
||||
opacity: 0.14;
|
||||
`,
|
||||
);
|
||||
|
||||
const SelectedOverlay = styled(Overlay)(
|
||||
({ theme }) => `
|
||||
border: 2px solid ${theme.vars.palette.accent.main};
|
||||
border-radius: 4px;
|
||||
`,
|
||||
);
|
||||
|
||||
const Cont = styled("div")<{ disabled: boolean }>`
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin-bottom: ${GAP_BTW_TILES}px;
|
||||
min-width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
|
||||
user-select: none;
|
||||
& > img {
|
||||
object-fit: cover;
|
||||
max-width: 100%;
|
||||
min-height: 100%;
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
input[type="checkbox"] {
|
||||
visibility: visible;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.preview-card-hover-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
export default function PreviewCard({
|
||||
file,
|
||||
onClick,
|
||||
selectable,
|
||||
selected,
|
||||
onSelect,
|
||||
selectOnClick,
|
||||
onHover,
|
||||
onRangeSelect,
|
||||
isRangeSelectActive,
|
||||
isInsSelectRange,
|
||||
isFav,
|
||||
activeCollectionID,
|
||||
showPlaceholder,
|
||||
}: PreviewCardProps) {
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
||||
const longPressCallback = () => {
|
||||
onSelect(!selected);
|
||||
};
|
||||
|
||||
const longPress = useLongPress(longPressCallback, 500);
|
||||
|
||||
const [imgSrc, setImgSrc] = useState<string>(file.msrc);
|
||||
|
||||
const isMounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
try {
|
||||
if (file.msrc) {
|
||||
return;
|
||||
}
|
||||
const url: string =
|
||||
await downloadManager.renderableThumbnailURL(
|
||||
file,
|
||||
showPlaceholder,
|
||||
);
|
||||
|
||||
if (!isMounted.current || !url) {
|
||||
return;
|
||||
}
|
||||
setImgSrc(url);
|
||||
} catch (e) {
|
||||
log.error("preview card useEffect failed", e);
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
main();
|
||||
}, [showPlaceholder]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (selectOnClick) {
|
||||
if (isRangeSelectActive) {
|
||||
onRangeSelect();
|
||||
} else {
|
||||
onSelect(!selected);
|
||||
}
|
||||
} else if (file?.msrc || imgSrc) {
|
||||
onClick?.();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
if (isRangeSelectActive) {
|
||||
onRangeSelect?.();
|
||||
} else {
|
||||
onSelect(e.target.checked);
|
||||
}
|
||||
};
|
||||
|
||||
const handleHover = () => {
|
||||
if (isRangeSelectActive) {
|
||||
onHover();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Cont
|
||||
key={`thumb-${file.id}}`}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleHover}
|
||||
disabled={!file?.msrc && !imgSrc}
|
||||
{...(selectable ? longPress : {})}
|
||||
>
|
||||
{selectable && (
|
||||
<Check
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
onChange={handleSelect}
|
||||
$active={isRangeSelectActive && isInsSelectRange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
{file.metadata.hasStaticThumbnail ? (
|
||||
<StaticThumbnail fileType={file.metadata.fileType} />
|
||||
) : imgSrc ? (
|
||||
<img src={imgSrc} />
|
||||
) : (
|
||||
<LoadingThumbnail />
|
||||
)}
|
||||
{file.metadata.fileType === FileType.livePhoto ? (
|
||||
<FileTypeIndicatorOverlay>
|
||||
<AlbumOutlinedIcon />
|
||||
</FileTypeIndicatorOverlay>
|
||||
) : (
|
||||
file.metadata.fileType === FileType.video && (
|
||||
<FileTypeIndicatorOverlay>
|
||||
<PlayCircleOutlineOutlinedIcon />
|
||||
</FileTypeIndicatorOverlay>
|
||||
)
|
||||
)}
|
||||
{selected && <SelectedOverlay />}
|
||||
{shouldShowAvatar(file, galleryContext.user) && (
|
||||
<AvatarOverlay>
|
||||
<Avatar file={file} />
|
||||
</AvatarOverlay>
|
||||
)}
|
||||
{isFav && (
|
||||
<FavoriteOverlay>
|
||||
<FavoriteRoundedIcon />
|
||||
</FavoriteOverlay>
|
||||
)}
|
||||
|
||||
<HoverOverlay
|
||||
className="preview-card-hover-overlay"
|
||||
checked={selected}
|
||||
/>
|
||||
{isRangeSelectActive && isInsSelectRange && (
|
||||
<InSelectRangeOverlay />
|
||||
)}
|
||||
|
||||
{activeCollectionID === TRASH_SECTION && file.isTrashed && (
|
||||
<TileBottomTextOverlay>
|
||||
<Typography variant="small">
|
||||
{formattedDateRelative(enteFileDeletionDate(file))}
|
||||
</Typography>
|
||||
</TileBottomTextOverlay>
|
||||
)}
|
||||
</Cont>
|
||||
);
|
||||
}
|
||||
@@ -96,13 +96,13 @@ import CollectionNamer, {
|
||||
import { GalleryBarAndListHeader } from "components/Collections/GalleryBarAndListHeader";
|
||||
import { Export } from "components/Export";
|
||||
import { ITEM_TYPE, TimeStampListItem } from "components/FileList";
|
||||
import { FileListWithViewer } from "components/FileListWithViewer";
|
||||
import {
|
||||
FilesDownloadProgress,
|
||||
FilesDownloadProgressAttributes,
|
||||
} from "components/FilesDownloadProgress";
|
||||
import { FixCreationTime } from "components/FixCreationTime";
|
||||
import GalleryEmptyState from "components/GalleryEmptyState";
|
||||
import PhotoFrame from "components/PhotoFrame";
|
||||
import { Sidebar } from "components/Sidebar";
|
||||
import { Upload, type UploadTypeSelectorIntent } from "components/Upload";
|
||||
import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions";
|
||||
@@ -1047,21 +1047,22 @@ const Page: React.FC = () => {
|
||||
!state.view.activePerson ? (
|
||||
<PeopleEmptyState />
|
||||
) : (
|
||||
<PhotoFrame
|
||||
<FileListWithViewer
|
||||
mode={barMode}
|
||||
modePlus={isInSearchMode ? "search" : barMode}
|
||||
user={user}
|
||||
files={filteredFiles}
|
||||
setSelected={setSelected}
|
||||
selected={selected}
|
||||
favoriteFileIDs={state.favoriteFileIDs}
|
||||
activeCollectionID={activeCollectionID}
|
||||
activePersonID={activePerson?.id}
|
||||
enableDownload={true}
|
||||
fileCollectionIDs={state.fileCollectionIDs}
|
||||
allCollectionsNameByID={state.allCollectionsNameByID}
|
||||
showAppDownloadBanner={
|
||||
files.length < 30 && !isInSearchMode
|
||||
}
|
||||
selectable={true}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
activeCollectionID={activeCollectionID}
|
||||
activePersonID={activePerson?.id}
|
||||
fileCollectionIDs={state.fileCollectionIDs}
|
||||
allCollectionsNameByID={state.allCollectionsNameByID}
|
||||
isInIncomingSharedCollection={
|
||||
collectionSummaries.get(activeCollectionID)?.type ==
|
||||
"incomingShareCollaborator" ||
|
||||
@@ -1069,10 +1070,10 @@ const Page: React.FC = () => {
|
||||
"incomingShareViewer"
|
||||
}
|
||||
isInHiddenSection={barMode == "hidden-albums"}
|
||||
favoriteFileIDs={state.favoriteFileIDs}
|
||||
setFilesDownloadProgressAttributesCreator={
|
||||
setFilesDownloadProgressAttributesCreator
|
||||
}
|
||||
selectable={true}
|
||||
onMarkUnsyncedFavoriteUpdate={
|
||||
handleMarkUnsyncedFavoriteUpdate
|
||||
}
|
||||
|
||||
@@ -52,11 +52,11 @@ import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
||||
import { Box, Button, IconButton, Stack, styled, Tooltip } from "@mui/material";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { ITEM_TYPE, TimeStampListItem } from "components/FileList";
|
||||
import { FileListWithViewer } from "components/FileListWithViewer";
|
||||
import {
|
||||
FilesDownloadProgress,
|
||||
FilesDownloadProgressAttributes,
|
||||
} from "components/FilesDownloadProgress";
|
||||
import PhotoFrame from "components/PhotoFrame";
|
||||
import { Upload } from "components/Upload";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -509,19 +509,19 @@ export default function PublicCollectionGallery() {
|
||||
)}
|
||||
</NavbarBase>
|
||||
|
||||
<PhotoFrame
|
||||
<FileListWithViewer
|
||||
files={publicFiles}
|
||||
onSyncWithRemote={handleSyncWithRemote}
|
||||
setSelected={setSelected}
|
||||
selected={selected}
|
||||
activeCollectionID={ALL_SECTION}
|
||||
enableDownload={downloadEnabled}
|
||||
selectable={downloadEnabled}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
activeCollectionID={ALL_SECTION}
|
||||
fileCollectionIDs={undefined}
|
||||
allCollectionsNameByID={undefined}
|
||||
setFilesDownloadProgressAttributesCreator={
|
||||
setFilesDownloadProgressAttributesCreator
|
||||
}
|
||||
selectable={downloadEnabled}
|
||||
onSyncWithRemote={handleSyncWithRemote}
|
||||
/>
|
||||
{blockingLoad && <TranslucentLoadingOverlay />}
|
||||
<Upload
|
||||
|
||||
@@ -32,7 +32,6 @@ import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
||||
import FullscreenExitOutlinedIcon from "@mui/icons-material/FullscreenExitOutlined";
|
||||
import FullscreenOutlinedIcon from "@mui/icons-material/FullscreenOutlined";
|
||||
import {
|
||||
Box,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
@@ -1025,7 +1024,7 @@ const Shortcuts: React.FC<ShortcutsProps> = ({
|
||||
<DialogTitle>{t("shortcuts")}</DialogTitle>
|
||||
<DialogCloseIconButton {...{ onClose }} />
|
||||
</SpacedRow>
|
||||
<ShortcutsContent sx={{ "&&": { pt: 2, pb: 5, px: 5 } }}>
|
||||
<ShortcutsContent>
|
||||
<Shortcut action={t("close")} shortcut={ut("Esc")} />
|
||||
<Shortcut
|
||||
action={formattedListJoin([t("previous"), t("next")])}
|
||||
@@ -1074,10 +1073,17 @@ const Shortcuts: React.FC<ShortcutsProps> = ({
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const ShortcutsContent = styled(DialogContent)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
const ShortcutsContent: React.FC<React.PropsWithChildren> = ({ children }) => (
|
||||
<DialogContent sx={{ "&&": { pt: 1, pb: 5, px: 5 } }}>
|
||||
<ShortcutsTable>
|
||||
<tbody>{children}</tbody>
|
||||
</ShortcutsTable>
|
||||
</DialogContent>
|
||||
);
|
||||
|
||||
const ShortcutsTable = styled("table")`
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 14px;
|
||||
`;
|
||||
|
||||
interface ShortcutProps {
|
||||
@@ -1086,12 +1092,18 @@ interface ShortcutProps {
|
||||
}
|
||||
|
||||
const Shortcut: React.FC<ShortcutProps> = ({ action, shortcut }) => (
|
||||
<Box sx={{ display: "flex", gap: 2 }}>
|
||||
<Typography sx={{ color: "text.muted", minWidth: "min(20ch, 40svw)" }}>
|
||||
<tr>
|
||||
<Typography
|
||||
component="td"
|
||||
sx={{ color: "text.muted", width: "min(20ch, 40svw)" }}
|
||||
>
|
||||
{action}
|
||||
</Typography>
|
||||
<Typography sx={{ fontWeight: "medium" }}>{shortcut}</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography component="td" sx={{ fontWeight: "medium" }}>
|
||||
{shortcut}
|
||||
</Typography>
|
||||
</tr>
|
||||
);
|
||||
|
||||
const fileIsEditableImage = (file: EnteFile) => {
|
||||
|
||||
@@ -3,4 +3,4 @@ export const IMAGE_CONTAINER_MAX_HEIGHT = 180;
|
||||
export const IMAGE_CONTAINER_MAX_WIDTH = 180;
|
||||
export const MIN_COLUMNS = 4;
|
||||
|
||||
// TODO: Move the PhotoList component here.
|
||||
// TODO: Move the FileList component here.
|
||||
|
||||
Reference in New Issue
Block a user