Merge branch 'main' into fix-double-uncategorized-collection
This commit is contained in:
commit
d3dd86c085
25 changed files with 772 additions and 96 deletions
|
@ -624,5 +624,8 @@
|
|||
"FASTER_UPLOAD_DESCRIPTION": "",
|
||||
"MAGIC_SEARCH_STATUS": "",
|
||||
"INDEXED_ITEMS": "",
|
||||
"CACHE_DIRECTORY": ""
|
||||
"CACHE_DIRECTORY": "",
|
||||
"FREEHAND": "",
|
||||
"APPLY_CROP": "",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
|
||||
}
|
||||
|
|
|
@ -624,5 +624,8 @@
|
|||
"FASTER_UPLOAD_DESCRIPTION": "Route uploads through nearby servers",
|
||||
"MAGIC_SEARCH_STATUS": "Magic Search Status",
|
||||
"INDEXED_ITEMS": "Indexed items",
|
||||
"CACHE_DIRECTORY": "Cache folder"
|
||||
"CACHE_DIRECTORY": "Cache folder",
|
||||
"FREEHAND": "Freehand",
|
||||
"APPLY_CROP": "Apply Crop",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving."
|
||||
}
|
||||
|
|
|
@ -624,5 +624,8 @@
|
|||
"FASTER_UPLOAD_DESCRIPTION": "",
|
||||
"MAGIC_SEARCH_STATUS": "",
|
||||
"INDEXED_ITEMS": "",
|
||||
"CACHE_DIRECTORY": ""
|
||||
"CACHE_DIRECTORY": "",
|
||||
"FREEHAND": "",
|
||||
"APPLY_CROP": "",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
|
||||
}
|
||||
|
|
|
@ -624,5 +624,8 @@
|
|||
"FASTER_UPLOAD_DESCRIPTION": "",
|
||||
"MAGIC_SEARCH_STATUS": "",
|
||||
"INDEXED_ITEMS": "",
|
||||
"CACHE_DIRECTORY": ""
|
||||
"CACHE_DIRECTORY": "",
|
||||
"FREEHAND": "",
|
||||
"APPLY_CROP": "",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
|
||||
}
|
||||
|
|
|
@ -624,5 +624,8 @@
|
|||
"FASTER_UPLOAD_DESCRIPTION": "",
|
||||
"MAGIC_SEARCH_STATUS": "",
|
||||
"INDEXED_ITEMS": "",
|
||||
"CACHE_DIRECTORY": ""
|
||||
"CACHE_DIRECTORY": "",
|
||||
"FREEHAND": "",
|
||||
"APPLY_CROP": "",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
|
||||
}
|
||||
|
|
|
@ -624,5 +624,8 @@
|
|||
"FASTER_UPLOAD_DESCRIPTION": "Router les chargements vers les serveurs à proximité",
|
||||
"MAGIC_SEARCH_STATUS": "",
|
||||
"INDEXED_ITEMS": "Éléments indexés",
|
||||
"CACHE_DIRECTORY": ""
|
||||
"CACHE_DIRECTORY": "",
|
||||
"FREEHAND": "",
|
||||
"APPLY_CROP": "",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
|
||||
}
|
||||
|
|
|
@ -624,5 +624,8 @@
|
|||
"FASTER_UPLOAD_DESCRIPTION": "",
|
||||
"MAGIC_SEARCH_STATUS": "",
|
||||
"INDEXED_ITEMS": "",
|
||||
"CACHE_DIRECTORY": ""
|
||||
"CACHE_DIRECTORY": "",
|
||||
"FREEHAND": "",
|
||||
"APPLY_CROP": "",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
|
||||
}
|
||||
|
|
|
@ -85,9 +85,9 @@
|
|||
"ZOOM_IN_OUT": "In/uitzoomen",
|
||||
"PREVIOUS": "Vorige (←)",
|
||||
"NEXT": "Volgende (→)",
|
||||
"TITLE_PHOTOS": "",
|
||||
"TITLE_ALBUMS": "",
|
||||
"TITLE_AUTH": "",
|
||||
"TITLE_PHOTOS": "Ente Foto's",
|
||||
"TITLE_ALBUMS": "Ente Foto's",
|
||||
"TITLE_AUTH": "Ente Auth",
|
||||
"UPLOAD_FIRST_PHOTO": "Je eerste foto uploaden",
|
||||
"IMPORT_YOUR_FOLDERS": "Importeer uw mappen",
|
||||
"UPLOAD_DROPZONE_MESSAGE": "Sleep om een back-up van je bestanden te maken",
|
||||
|
@ -622,7 +622,10 @@
|
|||
"PHOTO_EDITOR": "Fotobewerker",
|
||||
"FASTER_UPLOAD": "Snellere uploads",
|
||||
"FASTER_UPLOAD_DESCRIPTION": "Uploaden door nabije servers",
|
||||
"MAGIC_SEARCH_STATUS": "",
|
||||
"MAGIC_SEARCH_STATUS": "Magische Zoekfunctie Status",
|
||||
"INDEXED_ITEMS": "Geïndexeerde bestanden",
|
||||
"CACHE_DIRECTORY": "Cache map"
|
||||
"CACHE_DIRECTORY": "Cache map",
|
||||
"FREEHAND": "",
|
||||
"APPLY_CROP": "",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
|
||||
}
|
||||
|
|
|
@ -624,5 +624,8 @@
|
|||
"FASTER_UPLOAD_DESCRIPTION": "",
|
||||
"MAGIC_SEARCH_STATUS": "",
|
||||
"INDEXED_ITEMS": "",
|
||||
"CACHE_DIRECTORY": ""
|
||||
"CACHE_DIRECTORY": "",
|
||||
"FREEHAND": "",
|
||||
"APPLY_CROP": "",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
|
||||
}
|
||||
|
|
|
@ -624,5 +624,8 @@
|
|||
"FASTER_UPLOAD_DESCRIPTION": "",
|
||||
"MAGIC_SEARCH_STATUS": "",
|
||||
"INDEXED_ITEMS": "",
|
||||
"CACHE_DIRECTORY": ""
|
||||
"CACHE_DIRECTORY": "",
|
||||
"FREEHAND": "",
|
||||
"APPLY_CROP": "",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
|
||||
}
|
||||
|
|
|
@ -624,5 +624,8 @@
|
|||
"FASTER_UPLOAD_DESCRIPTION": "",
|
||||
"MAGIC_SEARCH_STATUS": "",
|
||||
"INDEXED_ITEMS": "",
|
||||
"CACHE_DIRECTORY": ""
|
||||
"CACHE_DIRECTORY": "",
|
||||
"FREEHAND": "",
|
||||
"APPLY_CROP": "",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
|
||||
}
|
||||
|
|
|
@ -624,5 +624,8 @@
|
|||
"FASTER_UPLOAD_DESCRIPTION": "通过附近的服务器路由上传",
|
||||
"MAGIC_SEARCH_STATUS": "魔法搜索状态",
|
||||
"INDEXED_ITEMS": "索引项目",
|
||||
"CACHE_DIRECTORY": "缓存文件夹"
|
||||
"CACHE_DIRECTORY": "缓存文件夹",
|
||||
"FREEHAND": "",
|
||||
"APPLY_CROP": "",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
|
||||
}
|
||||
|
|
|
@ -140,8 +140,8 @@ function CollectionSelector({
|
|||
? t('UNHIDE_TO_COLLECTION')
|
||||
: t('SELECT_COLLECTION')}
|
||||
</DialogTitleWithCloseButton>
|
||||
<DialogContent>
|
||||
<FlexWrapper flexWrap="wrap" gap={0.5}>
|
||||
<DialogContent sx={{ '&&&': { padding: 0 } }}>
|
||||
<FlexWrapper flexWrap="wrap" gap={'4px'} padding={'16px'}>
|
||||
<AddCollectionButton
|
||||
showNextModal={attributes.showNextModal}
|
||||
/>
|
||||
|
|
|
@ -12,7 +12,6 @@ import PhotoViewer from 'components/PhotoViewer';
|
|||
import { TRASH_SECTION } from 'constants/collection';
|
||||
import { updateFileMsrcProps, updateFileSrcProps } from 'utils/photoFrame';
|
||||
import { SelectedState } from 'types/gallery';
|
||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import { useRouter } from 'next/router';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import { addLogLine } from '@ente/shared/logging';
|
||||
|
@ -89,9 +88,6 @@ const PhotoFrame = ({
|
|||
[k: number]: boolean;
|
||||
}>({});
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
PublicCollectionGalleryContext
|
||||
);
|
||||
const [rangeStart, setRangeStart] = useState(null);
|
||||
const [currentHover, setCurrentHover] = useState(null);
|
||||
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
|
||||
|
@ -315,9 +311,7 @@ const PhotoFrame = ({
|
|||
file={item}
|
||||
updateURL={updateURL(index)}
|
||||
onClick={onThumbnailClick(index)}
|
||||
selectable={
|
||||
!publicCollectionGalleryContext?.accessedThroughSharedURL
|
||||
}
|
||||
selectable={enableDownload}
|
||||
onSelect={handleSelect(
|
||||
item.id,
|
||||
item.ownerID === galleryContext.user?.id,
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
|
||||
import { MenuItemGroup } from 'components/Menu/MenuItemGroup';
|
||||
import MenuSectionTitle from 'components/Menu/MenuSectionTitle';
|
||||
import { useContext } from 'react';
|
||||
import { ImageEditorOverlayContext } from './';
|
||||
import { CropBoxProps } from './';
|
||||
import type { MutableRefObject } from 'react';
|
||||
import { t } from 'i18next';
|
||||
import CropIcon from '@mui/icons-material/Crop';
|
||||
|
||||
interface IProps {
|
||||
previewScale: number;
|
||||
cropBoxProps: CropBoxProps;
|
||||
cropBoxRef: MutableRefObject<HTMLDivElement>;
|
||||
resetCropBox: () => void;
|
||||
}
|
||||
|
||||
export const cropRegionOfCanvas = (
|
||||
canvas: HTMLCanvasElement,
|
||||
topLeftX: number,
|
||||
topLeftY: number,
|
||||
bottomRightX: number,
|
||||
bottomRightY: number,
|
||||
scale: number = 1
|
||||
) => {
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context || !canvas) return;
|
||||
context.imageSmoothingEnabled = false;
|
||||
|
||||
const width = (bottomRightX - topLeftX) * scale;
|
||||
const height = (bottomRightY - topLeftY) * scale;
|
||||
|
||||
const img = new Image();
|
||||
img.src = canvas.toDataURL();
|
||||
img.onload = () => {
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
context.drawImage(
|
||||
img,
|
||||
topLeftX,
|
||||
topLeftY,
|
||||
width,
|
||||
height,
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const getCropRegionArgs = (
|
||||
cropBoxEle: HTMLDivElement,
|
||||
canvasEle: HTMLCanvasElement
|
||||
) => {
|
||||
// get the bounding rectangle of the crop box
|
||||
const cropBoxRect = cropBoxEle.getBoundingClientRect();
|
||||
// Get the bounding rectangle of the canvas
|
||||
const canvasRect = canvasEle.getBoundingClientRect();
|
||||
|
||||
// calculate the scale of the canvas display relative to its actual dimensions
|
||||
const displayScale = canvasEle.width / canvasRect.width;
|
||||
|
||||
// calculate the coordinates of the crop box relative to the canvas and adjust for any scrolling by adding scroll offsets
|
||||
const x1 =
|
||||
(cropBoxRect.left - canvasRect.left + window.scrollX) * displayScale;
|
||||
const y1 =
|
||||
(cropBoxRect.top - canvasRect.top + window.scrollY) * displayScale;
|
||||
const x2 = x1 + cropBoxRect.width * displayScale;
|
||||
const y2 = y1 + cropBoxRect.height * displayScale;
|
||||
|
||||
return {
|
||||
x1,
|
||||
x2,
|
||||
y1,
|
||||
y2,
|
||||
};
|
||||
};
|
||||
|
||||
const CropMenu = (props: IProps) => {
|
||||
const {
|
||||
canvasRef,
|
||||
originalSizeCanvasRef,
|
||||
canvasLoading,
|
||||
setCanvasLoading,
|
||||
setTransformationPerformed,
|
||||
setCurrentTab,
|
||||
} = useContext(ImageEditorOverlayContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuSectionTitle title={t('FREEHAND')} />
|
||||
<MenuItemGroup
|
||||
style={{
|
||||
marginBottom: '0.5rem',
|
||||
}}>
|
||||
<EnteMenuItem
|
||||
disabled={canvasLoading}
|
||||
startIcon={<CropIcon />}
|
||||
onClick={() => {
|
||||
if (!props.cropBoxRef.current || !canvasRef.current)
|
||||
return;
|
||||
|
||||
const { x1, x2, y1, y2 } = getCropRegionArgs(
|
||||
props.cropBoxRef.current,
|
||||
canvasRef.current
|
||||
);
|
||||
setCanvasLoading(true);
|
||||
setTransformationPerformed(true);
|
||||
cropRegionOfCanvas(canvasRef.current, x1, y1, x2, y2);
|
||||
cropRegionOfCanvas(
|
||||
originalSizeCanvasRef.current,
|
||||
x1 / props.previewScale,
|
||||
y1 / props.previewScale,
|
||||
x2 / props.previewScale,
|
||||
y2 / props.previewScale
|
||||
);
|
||||
props.resetCropBox();
|
||||
setCanvasLoading(false);
|
||||
|
||||
setCurrentTab('transform');
|
||||
}}
|
||||
label={t('APPLY_CROP')}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CropMenu;
|
|
@ -0,0 +1,118 @@
|
|||
import { CropBoxProps } from './';
|
||||
import type { Ref, Dispatch, SetStateAction, CSSProperties } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
const handleStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
height: '10px',
|
||||
width: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid black',
|
||||
};
|
||||
|
||||
const seHandleStyle: CSSProperties = {
|
||||
...handleStyle,
|
||||
right: '-5px',
|
||||
bottom: '-5px',
|
||||
cursor: 'se-resize',
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
cropBox: CropBoxProps;
|
||||
setIsDragging: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const FreehandCropRegion = forwardRef(
|
||||
(props: IProps, ref: Ref<HTMLDivElement>) => {
|
||||
return (
|
||||
<>
|
||||
{/* Top overlay */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: props.cropBox.y + 'px', // height up to the top of the crop box
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
pointerEvents: 'none',
|
||||
}}></div>
|
||||
|
||||
{/* Bottom overlay */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: `calc(100% - ${
|
||||
props.cropBox.y + props.cropBox.height
|
||||
}px)`, // height from the bottom of the crop box to the bottom of the canvas
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
pointerEvents: 'none',
|
||||
}}></div>
|
||||
|
||||
{/* Left overlay */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: props.cropBox.y + 'px',
|
||||
left: 0,
|
||||
width: props.cropBox.x + 'px', // width up to the left side of the crop box
|
||||
height: props.cropBox.height + 'px', // same height as the crop box
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
pointerEvents: 'none',
|
||||
}}></div>
|
||||
|
||||
{/* Right overlay */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: props.cropBox.y + 'px',
|
||||
right: 0,
|
||||
width: `calc(100% - ${
|
||||
props.cropBox.x + props.cropBox.width
|
||||
}px)`, // width from the right side of the crop box to the right side of the canvas
|
||||
height: props.cropBox.height + 'px', // same height as the crop box
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
pointerEvents: 'none',
|
||||
}}></div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
position: 'absolute',
|
||||
left: props.cropBox.x + 'px',
|
||||
top: props.cropBox.y + 'px',
|
||||
width: props.cropBox.width + 'px',
|
||||
height: props.cropBox.height + 'px',
|
||||
border: '1px solid white',
|
||||
gridTemplateColumns: '1fr 1fr 1fr',
|
||||
gridTemplateRows: '1fr 1fr 1fr',
|
||||
gap: '0px',
|
||||
zIndex: 30, // make sure the crop box is above the overlays
|
||||
}}
|
||||
ref={ref}>
|
||||
{Array.from({ length: 9 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
border: '1px solid white',
|
||||
boxSizing: 'border-box',
|
||||
pointerEvents: 'none',
|
||||
}}></div>
|
||||
))}
|
||||
|
||||
<div
|
||||
style={seHandleStyle}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
props.setIsDragging(true);
|
||||
}}></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default FreehandCropRegion;
|
|
@ -89,6 +89,7 @@ const TransformMenu = () => {
|
|||
);
|
||||
};
|
||||
};
|
||||
|
||||
const flipCanvas = (
|
||||
canvas: HTMLCanvasElement,
|
||||
direction: 'vertical' | 'horizontal'
|
||||
|
|
|
@ -29,6 +29,7 @@ import mime from 'mime-types';
|
|||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { HorizontalFlex } from '@ente/shared/components/Container';
|
||||
import TransformMenu from './TransformMenu';
|
||||
import CropMenu from './CropMenu';
|
||||
import ColoursMenu from './ColoursMenu';
|
||||
import { FileWithCollection } from 'types/upload';
|
||||
import uploadManager from 'services/upload/uploadManager';
|
||||
|
@ -44,6 +45,12 @@ import { getEditorCloseConfirmationMessage } from 'utils/ui';
|
|||
import { logError } from '@ente/shared/sentry';
|
||||
import { getFileType } from 'services/typeDetectionService';
|
||||
import { downloadUsingAnchor } from '@ente/shared/utils';
|
||||
import { CORNER_THRESHOLD, FILTER_DEFAULT_VALUES } from 'constants/photoEditor';
|
||||
import FreehandCropRegion from './FreehandCropRegion';
|
||||
import EnteButton from '@ente/shared/components/EnteButton';
|
||||
import { CenteredFlex } from '@ente/shared/components/Container';
|
||||
import CropIcon from '@mui/icons-material/Crop';
|
||||
import { cropRegionOfCanvas, getCropRegionArgs } from './CropMenu';
|
||||
|
||||
interface IProps {
|
||||
file: EnteFile;
|
||||
|
@ -59,16 +66,11 @@ export const ImageEditorOverlayContext = createContext(
|
|||
setTransformationPerformed: Dispatch<SetStateAction<boolean>>;
|
||||
setCanvasLoading: Dispatch<SetStateAction<boolean>>;
|
||||
canvasLoading: boolean;
|
||||
setCurrentTab: Dispatch<SetStateAction<OperationTab>>;
|
||||
}
|
||||
);
|
||||
|
||||
const filterDefaultValues = {
|
||||
brightness: 100,
|
||||
contrast: 100,
|
||||
blur: 0,
|
||||
saturation: 100,
|
||||
invert: false,
|
||||
};
|
||||
type OperationTab = 'crop' | 'transform' | 'colours';
|
||||
|
||||
const getEditedFileName = (fileName: string) => {
|
||||
const fileNameParts = fileName.split('.');
|
||||
|
@ -77,6 +79,13 @@ const getEditedFileName = (fileName: string) => {
|
|||
return editedFileName;
|
||||
};
|
||||
|
||||
export interface CropBoxProps {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const ImageEditorOverlay = (props: IProps) => {
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
|
@ -88,19 +97,17 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
|
||||
const [currentRotationAngle, setCurrentRotationAngle] = useState(0);
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<'transform' | 'colours'>(
|
||||
'transform'
|
||||
);
|
||||
const [currentTab, setCurrentTab] = useState<OperationTab>('transform');
|
||||
|
||||
const [brightness, setBrightness] = useState(
|
||||
filterDefaultValues.brightness
|
||||
FILTER_DEFAULT_VALUES.brightness
|
||||
);
|
||||
const [contrast, setContrast] = useState(filterDefaultValues.contrast);
|
||||
const [blur, setBlur] = useState(filterDefaultValues.blur);
|
||||
const [contrast, setContrast] = useState(FILTER_DEFAULT_VALUES.contrast);
|
||||
const [blur, setBlur] = useState(FILTER_DEFAULT_VALUES.blur);
|
||||
const [saturation, setSaturation] = useState(
|
||||
filterDefaultValues.saturation
|
||||
FILTER_DEFAULT_VALUES.saturation
|
||||
);
|
||||
const [invert, setInvert] = useState(filterDefaultValues.invert);
|
||||
const [invert, setInvert] = useState(FILTER_DEFAULT_VALUES.invert);
|
||||
|
||||
const [transformationPerformed, setTransformationPerformed] =
|
||||
useState(false);
|
||||
|
@ -110,6 +117,149 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
|
||||
const [showControlsDrawer, setShowControlsDrawer] = useState(true);
|
||||
|
||||
const [previewCanvasScale, setPreviewCanvasScale] = useState(0);
|
||||
|
||||
const [cropBox, setCropBox] = useState<CropBoxProps>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
|
||||
const [startX, setStartX] = useState(0);
|
||||
const [startY, setStartY] = useState(0);
|
||||
|
||||
const [beforeGrowthHeight, setBeforeGrowthHeight] = useState(0);
|
||||
const [beforeGrowthWidth, setBeforeGrowthWidth] = useState(0);
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isGrowing, setIsGrowing] = useState(false);
|
||||
|
||||
const cropBoxRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const getCanvasBoundsOffsets = () => {
|
||||
const canvasBounds = {
|
||||
height: canvasRef.current.height,
|
||||
width: canvasRef.current.width,
|
||||
};
|
||||
const parentBounds = parentRef.current.getBoundingClientRect();
|
||||
|
||||
// calculate the offset created by centering the canvas in its parent
|
||||
const offsetX = (parentBounds.width - canvasBounds.width) / 2;
|
||||
const offsetY = (parentBounds.height - canvasBounds.height) / 2;
|
||||
|
||||
return {
|
||||
offsetY,
|
||||
offsetX,
|
||||
canvasBounds,
|
||||
parentBounds,
|
||||
};
|
||||
};
|
||||
|
||||
const handleDragStart = (e) => {
|
||||
if (currentTab !== 'crop') return;
|
||||
|
||||
const rect = cropBoxRef.current.getBoundingClientRect();
|
||||
const offsetX = e.pageX - rect.left - rect.width / 2;
|
||||
const offsetY = e.pageY - rect.top - rect.height / 2;
|
||||
|
||||
// check if the cursor is near the corners of the box
|
||||
const isNearLeftOrRightEdge =
|
||||
e.pageX < rect.left + CORNER_THRESHOLD ||
|
||||
e.pageX > rect.right - CORNER_THRESHOLD;
|
||||
const isNearTopOrBottomEdge =
|
||||
e.pageY < rect.top + CORNER_THRESHOLD ||
|
||||
e.pageY > rect.bottom - CORNER_THRESHOLD;
|
||||
|
||||
if (isNearLeftOrRightEdge && isNearTopOrBottomEdge) {
|
||||
// cursor is near a corner, do not initiate dragging
|
||||
setIsGrowing(true);
|
||||
setStartX(e.pageX);
|
||||
setStartY(e.pageY);
|
||||
setBeforeGrowthWidth(cropBox.width);
|
||||
setBeforeGrowthHeight(cropBox.height);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDragging(true);
|
||||
setStartX(e.pageX - offsetX);
|
||||
setStartY(e.pageY - offsetY);
|
||||
};
|
||||
|
||||
const handleDrag = (e) => {
|
||||
if (!isDragging && !isGrowing) return;
|
||||
|
||||
// d- variables are the delta change between start and now
|
||||
const dx = e.pageX - startX;
|
||||
const dy = e.pageY - startY;
|
||||
|
||||
const { offsetX, offsetY, canvasBounds } = getCanvasBoundsOffsets();
|
||||
|
||||
if (isGrowing) {
|
||||
setCropBox((prev) => {
|
||||
const newWidth = Math.min(
|
||||
beforeGrowthWidth + dx,
|
||||
canvasBounds.width - prev.x + offsetX
|
||||
);
|
||||
const newHeight = Math.min(
|
||||
beforeGrowthHeight + dy,
|
||||
canvasBounds.height - prev.y + offsetY
|
||||
);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
setCropBox((prev) => {
|
||||
let newX = prev.x + dx;
|
||||
let newY = prev.y + dy;
|
||||
|
||||
// constrain the new position to the canvas boundaries, accounting for the offset
|
||||
newX = Math.max(
|
||||
offsetX,
|
||||
Math.min(newX, offsetX + canvasBounds.width - prev.width)
|
||||
);
|
||||
newY = Math.max(
|
||||
offsetY,
|
||||
Math.min(newY, offsetY + canvasBounds.height - prev.height)
|
||||
);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
x: newX,
|
||||
y: newY,
|
||||
};
|
||||
});
|
||||
setStartX(e.pageX);
|
||||
setStartY(e.pageY);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setStartX(0);
|
||||
setStartY(0);
|
||||
|
||||
setIsGrowing(false);
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const resetCropBox = () => {
|
||||
setCropBox((prev) => {
|
||||
const { offsetX, offsetY, canvasBounds } = getCanvasBoundsOffsets();
|
||||
|
||||
return {
|
||||
...prev,
|
||||
x: offsetX,
|
||||
y: offsetY,
|
||||
height: canvasBounds.height,
|
||||
width: canvasBounds.width,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) {
|
||||
return;
|
||||
|
@ -117,17 +267,23 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
try {
|
||||
applyFilters([canvasRef.current, originalSizeCanvasRef.current]);
|
||||
setColoursAdjusted(
|
||||
brightness !== filterDefaultValues.brightness ||
|
||||
contrast !== filterDefaultValues.contrast ||
|
||||
blur !== filterDefaultValues.blur ||
|
||||
saturation !== filterDefaultValues.saturation ||
|
||||
invert !== filterDefaultValues.invert
|
||||
brightness !== FILTER_DEFAULT_VALUES.brightness ||
|
||||
contrast !== FILTER_DEFAULT_VALUES.contrast ||
|
||||
blur !== FILTER_DEFAULT_VALUES.blur ||
|
||||
saturation !== FILTER_DEFAULT_VALUES.saturation ||
|
||||
invert !== FILTER_DEFAULT_VALUES.invert
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'Error applying filters');
|
||||
}
|
||||
}, [brightness, contrast, blur, saturation, invert, canvasRef, fileURL]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTab !== 'crop') return;
|
||||
resetCropBox();
|
||||
setShowControlsDrawer(false);
|
||||
}, [currentTab]);
|
||||
|
||||
const applyFilters = async (canvases: HTMLCanvasElement[]) => {
|
||||
try {
|
||||
for (const canvas of canvases) {
|
||||
|
@ -203,6 +359,7 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
}
|
||||
|
||||
setCanvasLoading(true);
|
||||
|
||||
resetFilters();
|
||||
setCurrentRotationAngle(0);
|
||||
|
||||
|
@ -226,6 +383,7 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
parentRef.current.clientWidth / img.width,
|
||||
parentRef.current.clientHeight / img.height
|
||||
);
|
||||
setPreviewCanvasScale(scale);
|
||||
|
||||
const width = img.width * scale;
|
||||
const height = img.height * scale;
|
||||
|
@ -246,6 +404,13 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
setColoursAdjusted(false);
|
||||
|
||||
setCanvasLoading(false);
|
||||
|
||||
resetCropBox();
|
||||
setStartX(0);
|
||||
setStartY(0);
|
||||
setIsDragging(false);
|
||||
setIsGrowing(false);
|
||||
|
||||
resolve(true);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
|
@ -387,35 +552,97 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
boxSizing={'border-box'}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center">
|
||||
justifyContent="center"
|
||||
position="relative"
|
||||
onMouseUp={handleDragEnd}
|
||||
onMouseMove={isDragging ? handleDrag : null}
|
||||
onMouseDown={handleDragStart}>
|
||||
<Box
|
||||
height="90%"
|
||||
width="100%"
|
||||
ref={parentRef}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center">
|
||||
{(fileURL === null || canvasLoading) && (
|
||||
<CircularProgress />
|
||||
)}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}>
|
||||
<Box
|
||||
height="90%"
|
||||
width="100%"
|
||||
ref={parentRef}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
position="relative">
|
||||
{(fileURL === null || canvasLoading) && (
|
||||
<CircularProgress />
|
||||
)}
|
||||
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
display:
|
||||
fileURL === null || canvasLoading
|
||||
? 'none'
|
||||
: 'block',
|
||||
position: 'absolute',
|
||||
}}
|
||||
/>
|
||||
<canvas
|
||||
ref={originalSizeCanvasRef}
|
||||
style={{
|
||||
display: 'none',
|
||||
}}
|
||||
/>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
display:
|
||||
fileURL === null || canvasLoading
|
||||
? 'none'
|
||||
: 'block',
|
||||
position: 'absolute',
|
||||
}}
|
||||
/>
|
||||
<canvas
|
||||
ref={originalSizeCanvasRef}
|
||||
style={{
|
||||
display: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
{currentTab === 'crop' && (
|
||||
<FreehandCropRegion
|
||||
cropBox={cropBox}
|
||||
ref={cropBoxRef}
|
||||
setIsDragging={setIsDragging}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{currentTab === 'crop' && (
|
||||
<CenteredFlex marginTop="1rem">
|
||||
<EnteButton
|
||||
color="accent"
|
||||
startIcon={<CropIcon />}
|
||||
onClick={() => {
|
||||
if (
|
||||
!cropBoxRef.current ||
|
||||
!canvasRef.current
|
||||
)
|
||||
return;
|
||||
|
||||
const { x1, x2, y1, y2 } =
|
||||
getCropRegionArgs(
|
||||
cropBoxRef.current,
|
||||
canvasRef.current
|
||||
);
|
||||
setCanvasLoading(true);
|
||||
setTransformationPerformed(true);
|
||||
cropRegionOfCanvas(
|
||||
canvasRef.current,
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2
|
||||
);
|
||||
cropRegionOfCanvas(
|
||||
originalSizeCanvasRef.current,
|
||||
x1 / previewCanvasScale,
|
||||
y1 / previewCanvasScale,
|
||||
x2 / previewCanvasScale,
|
||||
y2 / previewCanvasScale
|
||||
);
|
||||
resetCropBox();
|
||||
setCanvasLoading(false);
|
||||
|
||||
setCurrentTab('transform');
|
||||
}}>
|
||||
{t('APPLY_CROP')}
|
||||
</EnteButton>
|
||||
</CenteredFlex>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -441,6 +668,7 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
onChange={(_, value) => {
|
||||
setCurrentTab(value);
|
||||
}}>
|
||||
<Tab label={t('CROP')} value="crop" />
|
||||
<Tab label={t('TRANSFORM')} value="transform" />
|
||||
<Tab
|
||||
label={t('COLORS')}
|
||||
|
@ -463,18 +691,25 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
label={t('RESTORE_ORIGINAL')}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
{currentTab === 'transform' && (
|
||||
<ImageEditorOverlayContext.Provider
|
||||
value={{
|
||||
originalSizeCanvasRef,
|
||||
canvasRef,
|
||||
setCanvasLoading,
|
||||
canvasLoading,
|
||||
setTransformationPerformed,
|
||||
}}>
|
||||
<TransformMenu />
|
||||
</ImageEditorOverlayContext.Provider>
|
||||
)}
|
||||
<ImageEditorOverlayContext.Provider
|
||||
value={{
|
||||
originalSizeCanvasRef,
|
||||
canvasRef,
|
||||
setCanvasLoading,
|
||||
canvasLoading,
|
||||
setTransformationPerformed,
|
||||
setCurrentTab,
|
||||
}}>
|
||||
{currentTab === 'crop' && (
|
||||
<CropMenu
|
||||
previewScale={previewCanvasScale}
|
||||
cropBoxProps={cropBox}
|
||||
cropBoxRef={cropBoxRef}
|
||||
resetCropBox={resetCropBox}
|
||||
/>
|
||||
)}
|
||||
{currentTab === 'transform' && <TransformMenu />}
|
||||
</ImageEditorOverlayContext.Provider>
|
||||
{currentTab === 'colours' && (
|
||||
<ColoursMenu
|
||||
brightness={brightness}
|
||||
|
@ -495,14 +730,25 @@ const ImageEditorOverlay = (props: IProps) => {
|
|||
startIcon={<DownloadIcon />}
|
||||
onClick={downloadEditedPhoto}
|
||||
label={t('DOWNLOAD_EDITED')}
|
||||
disabled={
|
||||
!transformationPerformed && !coloursAdjusted
|
||||
}
|
||||
/>
|
||||
<MenuItemDivider />
|
||||
<EnteMenuItem
|
||||
startIcon={<CloudUploadIcon />}
|
||||
onClick={saveCopyToEnte}
|
||||
label={t('SAVE_A_COPY_TO_ENTE')}
|
||||
disabled={
|
||||
!transformationPerformed && !coloursAdjusted
|
||||
}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
{!transformationPerformed && !coloursAdjusted && (
|
||||
<MenuSectionTitle
|
||||
title={t('PHOTO_EDIT_REQUIRED_TO_SAVE')}
|
||||
/>
|
||||
)}
|
||||
</EnteDrawer>
|
||||
</Backdrop>
|
||||
</>
|
||||
|
|
|
@ -192,6 +192,12 @@ function PhotoViewer(props: Iprops) {
|
|||
case 'L':
|
||||
onFavClick(photoSwipe?.currItem as EnteFile);
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
handleArrowClick(event, 'left');
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
handleArrowClick(event, 'right');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -352,6 +358,7 @@ function PhotoViewer(props: Iprops) {
|
|||
maxSpreadZoom: 5,
|
||||
index: currentIndex,
|
||||
showHideOpacity: true,
|
||||
arrowKeys: false,
|
||||
getDoubleTapZoom(isMouseClick, item) {
|
||||
if (isMouseClick) {
|
||||
return 2.5;
|
||||
|
@ -505,6 +512,24 @@ function PhotoViewer(props: Iprops) {
|
|||
appContext.setDialogMessage(getTrashFileMessage(() => trashFile(file)));
|
||||
};
|
||||
|
||||
const handleArrowClick = (
|
||||
e: KeyboardEvent,
|
||||
direction: 'left' | 'right'
|
||||
) => {
|
||||
// ignore arrow clicks if the user is typing in a text field
|
||||
if (
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (direction === 'left') {
|
||||
photoSwipe.prev();
|
||||
} else {
|
||||
photoSwipe.next();
|
||||
}
|
||||
};
|
||||
|
||||
const updateItems = (items: EnteFile[]) => {
|
||||
try {
|
||||
if (photoSwipe) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import UploadProgressContext from 'contexts/uploadProgress';
|
|||
import { t } from 'i18next';
|
||||
|
||||
import { UPLOAD_STAGES } from 'constants/upload';
|
||||
import { CaptionedText } from 'components/CaptionedText';
|
||||
|
||||
export const InProgressSection = () => {
|
||||
const { inProgressUploads, hasLivePhotos, uploadFileNames, uploadStage } =
|
||||
|
@ -44,9 +45,14 @@ export const InProgressSection = () => {
|
|||
return (
|
||||
<UploadProgressSection>
|
||||
<UploadProgressSectionTitle expandIcon={<ExpandMoreIcon />}>
|
||||
{uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA
|
||||
? t('INPROGRESS_METADATA_EXTRACTION')
|
||||
: t('INPROGRESS_UPLOADS')}
|
||||
<CaptionedText
|
||||
mainText={
|
||||
uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA
|
||||
? t('INPROGRESS_METADATA_EXTRACTION')
|
||||
: t('INPROGRESS_UPLOADS')
|
||||
}
|
||||
subText={String(inProgressUploads?.length ?? 0)}
|
||||
/>
|
||||
</UploadProgressSectionTitle>
|
||||
<UploadProgressSectionContent>
|
||||
{hasLivePhotos && (
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import ItemList from 'components/ItemList';
|
||||
import { Typography } from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import { ResultItemContainer } from './styledComponents';
|
||||
import { UPLOAD_RESULT } from 'constants/upload';
|
||||
|
@ -11,6 +10,7 @@ import {
|
|||
UploadProgressSectionTitle,
|
||||
} from './section';
|
||||
import UploadProgressContext from 'contexts/uploadProgress';
|
||||
import { CaptionedText } from 'components/CaptionedText';
|
||||
|
||||
export interface ResultSectionProps {
|
||||
uploadResult: UPLOAD_RESULT;
|
||||
|
@ -46,7 +46,10 @@ export const ResultSection = (props: ResultSectionProps) => {
|
|||
return (
|
||||
<UploadProgressSection>
|
||||
<UploadProgressSectionTitle expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography> {props.sectionTitle}</Typography>
|
||||
<CaptionedText
|
||||
mainText={props.sectionTitle}
|
||||
subText={String(fileList?.length ?? 0)}
|
||||
/>
|
||||
</UploadProgressSectionTitle>
|
||||
<UploadProgressSectionContent>
|
||||
{props.sectionInfo && (
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { styled } from '@mui/material';
|
||||
import { Tooltip, styled } from '@mui/material';
|
||||
import PlayCircleOutlineOutlinedIcon from '@mui/icons-material/PlayCircleOutlineOutlined';
|
||||
import DownloadManager from 'services/download';
|
||||
import useLongPress from '@ente/shared/hooks/useLongPress';
|
||||
|
@ -298,7 +298,7 @@ export default function PreviewCard(props: IProps) {
|
|||
}
|
||||
};
|
||||
|
||||
return (
|
||||
const renderFn = () => (
|
||||
<Cont
|
||||
key={`thumb-${file.id}}`}
|
||||
onClick={handleClick}
|
||||
|
@ -360,4 +360,22 @@ export default function PreviewCard(props: IProps) {
|
|||
)}
|
||||
</Cont>
|
||||
);
|
||||
|
||||
if (deduplicateContext.isOnDeduplicatePage) {
|
||||
return (
|
||||
<Tooltip
|
||||
placement="bottom-start"
|
||||
enterDelay={300}
|
||||
enterNextDelay={100}
|
||||
title={`${
|
||||
file.metadata.title
|
||||
} - ${deduplicateContext.collectionNameMap.get(
|
||||
file.collectionID
|
||||
)}`}>
|
||||
{renderFn()}
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return renderFn();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { useContext } from 'react';
|
||||
import { FluidContainer } from '@ente/shared/components/Container';
|
||||
import { SelectionBar } from '@ente/shared/components/Navbar/SelectionBar';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { Box, IconButton, Stack, Tooltip } from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import { t } from 'i18next';
|
||||
import { formatNumber } from 'utils/number/format';
|
||||
|
||||
interface Props {
|
||||
count: number;
|
||||
ownCount: number;
|
||||
clearSelection: () => void;
|
||||
downloadFilesHelper: () => void;
|
||||
}
|
||||
|
||||
const SelectedFileOptions = ({
|
||||
downloadFilesHelper,
|
||||
count,
|
||||
ownCount,
|
||||
clearSelection,
|
||||
}: Props) => {
|
||||
const { isMobile } = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<SelectionBar isMobile={isMobile}>
|
||||
<FluidContainer>
|
||||
<IconButton onClick={clearSelection}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Box ml={1.5}>
|
||||
{formatNumber(count)} {t('SELECTED')}{' '}
|
||||
{ownCount !== count &&
|
||||
`(${formatNumber(ownCount)} ${t('YOURS')})`}
|
||||
</Box>
|
||||
</FluidContainer>
|
||||
<Stack spacing={2} direction="row" mr={2}>
|
||||
<Tooltip title={t('DOWNLOAD')}>
|
||||
<IconButton onClick={downloadFilesHelper}>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</SelectionBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectedFileOptions;
|
10
apps/photos/src/constants/photoEditor.ts
Normal file
10
apps/photos/src/constants/photoEditor.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export const FILTER_DEFAULT_VALUES = {
|
||||
brightness: 100,
|
||||
contrast: 100,
|
||||
blur: 0,
|
||||
saturation: 100,
|
||||
invert: false,
|
||||
};
|
||||
|
||||
// CORNER_THRESHOLD defines the threshold near the corners of the crop box in which dragging is assumed as not the intention
|
||||
export const CORNER_THRESHOLD = 20;
|
|
@ -16,7 +16,13 @@ import {
|
|||
} from 'services/publicCollectionService';
|
||||
import { Collection } from 'types/collection';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { downloadFile, mergeMetadata, sortFiles } from 'utils/file';
|
||||
import {
|
||||
downloadFile,
|
||||
downloadFiles,
|
||||
getSelectedFiles,
|
||||
mergeMetadata,
|
||||
sortFiles,
|
||||
} from 'utils/file';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import { CustomError, parseSharingErrorCodes } from '@ente/shared/error';
|
||||
|
@ -52,7 +58,7 @@ import UploadButton from 'components/Upload/UploadButton';
|
|||
import bs58 from 'bs58';
|
||||
import AddPhotoAlternateOutlined from '@mui/icons-material/AddPhotoAlternateOutlined';
|
||||
import ComlinkCryptoWorker from '@ente/shared/crypto';
|
||||
import { UploadTypeSelectorIntent } from 'types/gallery';
|
||||
import { SelectedState, UploadTypeSelectorIntent } from 'types/gallery';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||
import OverflowMenu from '@ente/shared/components/OverflowMenu/menu';
|
||||
|
@ -60,6 +66,7 @@ import { OverflowMenuOption } from '@ente/shared/components/OverflowMenu/option'
|
|||
import { ENTE_WEBSITE_LINK } from '@ente/shared/constants/urls';
|
||||
import { APPS } from '@ente/shared/apps/constants';
|
||||
import downloadManager from 'services/download';
|
||||
import SelectedFileOptions from 'components/pages/sharedAlbum/SelectedFileOptions';
|
||||
|
||||
export default function PublicCollectionGallery() {
|
||||
const token = useRef<string>(null);
|
||||
|
@ -87,6 +94,12 @@ export default function PublicCollectionGallery() {
|
|||
const [blockingLoad, setBlockingLoad] = useState(false);
|
||||
const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false);
|
||||
|
||||
const [selected, setSelected] = useState<SelectedState>({
|
||||
ownCount: 0,
|
||||
count: 0,
|
||||
collectionID: 0,
|
||||
});
|
||||
|
||||
const {
|
||||
getRootProps: getDragAndDropRootProps,
|
||||
getInputProps: getDragAndDropInputProps,
|
||||
|
@ -441,6 +454,22 @@ export default function PublicCollectionGallery() {
|
|||
}
|
||||
}
|
||||
|
||||
const downloadFilesHelper = async () => {
|
||||
try {
|
||||
const selectedFiles = getSelectedFiles(selected, publicFiles);
|
||||
await downloadFiles(selectedFiles);
|
||||
} catch (e) {
|
||||
logError(e, 'failed to download selected files');
|
||||
}
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
if (!selected?.count) {
|
||||
return;
|
||||
}
|
||||
setSelected({ ownCount: 0, count: 0, collectionID: 0 });
|
||||
};
|
||||
|
||||
return (
|
||||
<PublicCollectionGalleryContext.Provider
|
||||
value={{
|
||||
|
@ -468,8 +497,8 @@ export default function PublicCollectionGallery() {
|
|||
page={PAGES.SHARED_ALBUMS}
|
||||
files={publicFiles}
|
||||
syncWithRemote={syncWithRemote}
|
||||
setSelected={() => null}
|
||||
selected={{ count: 0, collectionID: null, ownCount: 0 }}
|
||||
setSelected={setSelected}
|
||||
selected={selected}
|
||||
activeCollectionID={ALL_SECTION}
|
||||
enableDownload={downloadEnabled}
|
||||
fileToCollectionsMap={null}
|
||||
|
@ -498,6 +527,14 @@ export default function PublicCollectionGallery() {
|
|||
UploadTypeSelectorIntent.collectPhotos
|
||||
}
|
||||
/>
|
||||
{selected.count > 0 && (
|
||||
<SelectedFileOptions
|
||||
downloadFilesHelper={downloadFilesHelper}
|
||||
clearSelection={clearSelection}
|
||||
count={selected.count}
|
||||
ownCount={selected.ownCount}
|
||||
/>
|
||||
)}
|
||||
</FullScreenDropZone>
|
||||
</PublicCollectionGalleryContext.Provider>
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue