Merge branch 'main' into fix-double-uncategorized-collection

This commit is contained in:
Abhinav Kumar 2024-01-23 09:40:41 +05:30 committed by GitHub
commit d3dd86c085
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 772 additions and 96 deletions

View file

@ -624,5 +624,8 @@
"FASTER_UPLOAD_DESCRIPTION": "",
"MAGIC_SEARCH_STATUS": "",
"INDEXED_ITEMS": "",
"CACHE_DIRECTORY": ""
"CACHE_DIRECTORY": "",
"FREEHAND": "",
"APPLY_CROP": "",
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
}

View file

@ -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."
}

View file

@ -624,5 +624,8 @@
"FASTER_UPLOAD_DESCRIPTION": "",
"MAGIC_SEARCH_STATUS": "",
"INDEXED_ITEMS": "",
"CACHE_DIRECTORY": ""
"CACHE_DIRECTORY": "",
"FREEHAND": "",
"APPLY_CROP": "",
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
}

View file

@ -624,5 +624,8 @@
"FASTER_UPLOAD_DESCRIPTION": "",
"MAGIC_SEARCH_STATUS": "",
"INDEXED_ITEMS": "",
"CACHE_DIRECTORY": ""
"CACHE_DIRECTORY": "",
"FREEHAND": "",
"APPLY_CROP": "",
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
}

View file

@ -624,5 +624,8 @@
"FASTER_UPLOAD_DESCRIPTION": "",
"MAGIC_SEARCH_STATUS": "",
"INDEXED_ITEMS": "",
"CACHE_DIRECTORY": ""
"CACHE_DIRECTORY": "",
"FREEHAND": "",
"APPLY_CROP": "",
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
}

View file

@ -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": ""
}

View file

@ -624,5 +624,8 @@
"FASTER_UPLOAD_DESCRIPTION": "",
"MAGIC_SEARCH_STATUS": "",
"INDEXED_ITEMS": "",
"CACHE_DIRECTORY": ""
"CACHE_DIRECTORY": "",
"FREEHAND": "",
"APPLY_CROP": "",
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
}

View file

@ -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": ""
}

View file

@ -624,5 +624,8 @@
"FASTER_UPLOAD_DESCRIPTION": "",
"MAGIC_SEARCH_STATUS": "",
"INDEXED_ITEMS": "",
"CACHE_DIRECTORY": ""
"CACHE_DIRECTORY": "",
"FREEHAND": "",
"APPLY_CROP": "",
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
}

View file

@ -624,5 +624,8 @@
"FASTER_UPLOAD_DESCRIPTION": "",
"MAGIC_SEARCH_STATUS": "",
"INDEXED_ITEMS": "",
"CACHE_DIRECTORY": ""
"CACHE_DIRECTORY": "",
"FREEHAND": "",
"APPLY_CROP": "",
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
}

View file

@ -624,5 +624,8 @@
"FASTER_UPLOAD_DESCRIPTION": "",
"MAGIC_SEARCH_STATUS": "",
"INDEXED_ITEMS": "",
"CACHE_DIRECTORY": ""
"CACHE_DIRECTORY": "",
"FREEHAND": "",
"APPLY_CROP": "",
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
}

View file

@ -624,5 +624,8 @@
"FASTER_UPLOAD_DESCRIPTION": "通过附近的服务器路由上传",
"MAGIC_SEARCH_STATUS": "魔法搜索状态",
"INDEXED_ITEMS": "索引项目",
"CACHE_DIRECTORY": "缓存文件夹"
"CACHE_DIRECTORY": "缓存文件夹",
"FREEHAND": "",
"APPLY_CROP": "",
"PHOTO_EDIT_REQUIRED_TO_SAVE": ""
}

View file

@ -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}
/>

View file

@ -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,

View file

@ -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;

View file

@ -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;

View file

@ -89,6 +89,7 @@ const TransformMenu = () => {
);
};
};
const flipCanvas = (
canvas: HTMLCanvasElement,
direction: 'vertical' | 'horizontal'

View file

@ -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>
</>

View file

@ -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) {

View file

@ -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 && (

View file

@ -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 && (

View file

@ -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();
}
}

View file

@ -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;

View 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;

View file

@ -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>
);