mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat: Add Aspect Ratio To Canvas Bounding Box (#3717)
This commit is contained in:
commit
10bb05b753
@ -528,7 +528,7 @@
|
|||||||
"hidePreview": "Hide Preview",
|
"hidePreview": "Hide Preview",
|
||||||
"showPreview": "Show Preview",
|
"showPreview": "Show Preview",
|
||||||
"controlNetControlMode": "Control Mode",
|
"controlNetControlMode": "Control Mode",
|
||||||
"clipSkip": "Clip Skip",
|
"clipSkip": "CLIP Skip",
|
||||||
"aspectRatio": "Ratio"
|
"aspectRatio": "Ratio"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
setIsMovingBoundingBox,
|
setIsMovingBoundingBox,
|
||||||
setIsTransformingBoundingBox,
|
setIsTransformingBoundingBox,
|
||||||
} from 'features/canvas/store/canvasSlice';
|
} from 'features/canvas/store/canvasSlice';
|
||||||
|
import { uiSelector } from 'features/ui/store/uiSelectors';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { GroupConfig } from 'konva/lib/Group';
|
import { GroupConfig } from 'konva/lib/Group';
|
||||||
import { KonvaEventObject } from 'konva/lib/Node';
|
import { KonvaEventObject } from 'konva/lib/Node';
|
||||||
@ -22,8 +23,8 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|||||||
import { Group, Rect, Transformer } from 'react-konva';
|
import { Group, Rect, Transformer } from 'react-konva';
|
||||||
|
|
||||||
const boundingBoxPreviewSelector = createSelector(
|
const boundingBoxPreviewSelector = createSelector(
|
||||||
canvasSelector,
|
[canvasSelector, uiSelector],
|
||||||
(canvas) => {
|
(canvas, ui) => {
|
||||||
const {
|
const {
|
||||||
boundingBoxCoordinates,
|
boundingBoxCoordinates,
|
||||||
boundingBoxDimensions,
|
boundingBoxDimensions,
|
||||||
@ -35,6 +36,8 @@ const boundingBoxPreviewSelector = createSelector(
|
|||||||
shouldSnapToGrid,
|
shouldSnapToGrid,
|
||||||
} = canvas;
|
} = canvas;
|
||||||
|
|
||||||
|
const { aspectRatio } = ui;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
boundingBoxCoordinates,
|
boundingBoxCoordinates,
|
||||||
boundingBoxDimensions,
|
boundingBoxDimensions,
|
||||||
@ -45,6 +48,7 @@ const boundingBoxPreviewSelector = createSelector(
|
|||||||
shouldSnapToGrid,
|
shouldSnapToGrid,
|
||||||
tool,
|
tool,
|
||||||
hitStrokeWidth: 20 / stageScale,
|
hitStrokeWidth: 20 / stageScale,
|
||||||
|
aspectRatio,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -70,6 +74,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
|||||||
shouldSnapToGrid,
|
shouldSnapToGrid,
|
||||||
tool,
|
tool,
|
||||||
hitStrokeWidth,
|
hitStrokeWidth,
|
||||||
|
aspectRatio,
|
||||||
} = useAppSelector(boundingBoxPreviewSelector);
|
} = useAppSelector(boundingBoxPreviewSelector);
|
||||||
|
|
||||||
const transformerRef = useRef<Konva.Transformer>(null);
|
const transformerRef = useRef<Konva.Transformer>(null);
|
||||||
@ -137,12 +142,22 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
|||||||
const x = Math.round(rect.x());
|
const x = Math.round(rect.x());
|
||||||
const y = Math.round(rect.y());
|
const y = Math.round(rect.y());
|
||||||
|
|
||||||
dispatch(
|
if (aspectRatio) {
|
||||||
setBoundingBoxDimensions({
|
const newHeight = roundToMultiple(width / aspectRatio, 64);
|
||||||
width,
|
dispatch(
|
||||||
height,
|
setBoundingBoxDimensions({
|
||||||
})
|
width: width,
|
||||||
);
|
height: newHeight,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dispatch(
|
||||||
|
setBoundingBoxDimensions({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
setBoundingBoxCoordinates({
|
setBoundingBoxCoordinates({
|
||||||
@ -154,7 +169,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
|||||||
// Reset the scale now that the coords/dimensions have been un-scaled
|
// Reset the scale now that the coords/dimensions have been un-scaled
|
||||||
rect.scaleX(1);
|
rect.scaleX(1);
|
||||||
rect.scaleY(1);
|
rect.scaleY(1);
|
||||||
}, [dispatch, shouldSnapToGrid]);
|
}, [dispatch, shouldSnapToGrid, aspectRatio]);
|
||||||
|
|
||||||
const anchorDragBoundFunc = useCallback(
|
const anchorDragBoundFunc = useCallback(
|
||||||
(
|
(
|
||||||
|
@ -7,7 +7,14 @@ import {
|
|||||||
import { IRect, Vector2d } from 'konva/lib/types';
|
import { IRect, Vector2d } from 'konva/lib/types';
|
||||||
import { clamp, cloneDeep } from 'lodash-es';
|
import { clamp, cloneDeep } from 'lodash-es';
|
||||||
//
|
//
|
||||||
|
import {
|
||||||
|
setActiveTab,
|
||||||
|
setAspectRatio,
|
||||||
|
setShouldUseCanvasBetaLayout,
|
||||||
|
} from 'features/ui/store/uiSlice';
|
||||||
import { RgbaColor } from 'react-colorful';
|
import { RgbaColor } from 'react-colorful';
|
||||||
|
import { sessionCanceled } from 'services/api/thunks/session';
|
||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
import calculateCoordinates from '../util/calculateCoordinates';
|
import calculateCoordinates from '../util/calculateCoordinates';
|
||||||
import calculateScale from '../util/calculateScale';
|
import calculateScale from '../util/calculateScale';
|
||||||
import { STAGE_PADDING_PERCENTAGE } from '../util/constants';
|
import { STAGE_PADDING_PERCENTAGE } from '../util/constants';
|
||||||
@ -28,13 +35,6 @@ import {
|
|||||||
isCanvasBaseImage,
|
isCanvasBaseImage,
|
||||||
isCanvasMaskLine,
|
isCanvasMaskLine,
|
||||||
} from './canvasTypes';
|
} from './canvasTypes';
|
||||||
import { ImageDTO } from 'services/api/types';
|
|
||||||
import { sessionCanceled } from 'services/api/thunks/session';
|
|
||||||
import {
|
|
||||||
setActiveTab,
|
|
||||||
setShouldUseCanvasBetaLayout,
|
|
||||||
} from 'features/ui/store/uiSlice';
|
|
||||||
import { imageUrlsReceived } from 'services/api/thunks/image';
|
|
||||||
|
|
||||||
export const initialLayerState: CanvasLayerState = {
|
export const initialLayerState: CanvasLayerState = {
|
||||||
objects: [],
|
objects: [],
|
||||||
@ -240,6 +240,16 @@ export const canvasSlice = createSlice({
|
|||||||
state.scaledBoundingBoxDimensions = scaledDimensions;
|
state.scaledBoundingBoxDimensions = scaledDimensions;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
flipBoundingBoxAxes: (state) => {
|
||||||
|
const [currWidth, currHeight] = [
|
||||||
|
state.boundingBoxDimensions.width,
|
||||||
|
state.boundingBoxDimensions.height,
|
||||||
|
];
|
||||||
|
state.boundingBoxDimensions = {
|
||||||
|
width: currHeight,
|
||||||
|
height: currWidth,
|
||||||
|
};
|
||||||
|
},
|
||||||
setBoundingBoxCoordinates: (state, action: PayloadAction<Vector2d>) => {
|
setBoundingBoxCoordinates: (state, action: PayloadAction<Vector2d>) => {
|
||||||
state.boundingBoxCoordinates = floorCoordinates(action.payload);
|
state.boundingBoxCoordinates = floorCoordinates(action.payload);
|
||||||
},
|
},
|
||||||
@ -864,6 +874,15 @@ export const canvasSlice = createSlice({
|
|||||||
builder.addCase(setActiveTab, (state, action) => {
|
builder.addCase(setActiveTab, (state, action) => {
|
||||||
state.doesCanvasNeedScaling = true;
|
state.doesCanvasNeedScaling = true;
|
||||||
});
|
});
|
||||||
|
builder.addCase(setAspectRatio, (state, action) => {
|
||||||
|
const ratio = action.payload;
|
||||||
|
if (ratio) {
|
||||||
|
state.boundingBoxDimensions.height = roundToMultiple(
|
||||||
|
state.boundingBoxDimensions.width / ratio,
|
||||||
|
64
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
|
// builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
|
||||||
// const { image_name, image_url, thumbnail_url } = action.payload;
|
// const { image_name, image_url, thumbnail_url } = action.payload;
|
||||||
@ -912,6 +931,7 @@ export const {
|
|||||||
setBoundingBoxDimensions,
|
setBoundingBoxDimensions,
|
||||||
setBoundingBoxPreviewFill,
|
setBoundingBoxPreviewFill,
|
||||||
setBoundingBoxScaleMethod,
|
setBoundingBoxScaleMethod,
|
||||||
|
flipBoundingBoxAxes,
|
||||||
setBrushColor,
|
setBrushColor,
|
||||||
setBrushSize,
|
setBrushSize,
|
||||||
setCanvasContainerDimensions,
|
setCanvasContainerDimensions,
|
||||||
|
@ -2,22 +2,26 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import IAISlider from 'common/components/IAISlider';
|
import IAISlider from 'common/components/IAISlider';
|
||||||
|
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
import {
|
import {
|
||||||
canvasSelector,
|
canvasSelector,
|
||||||
isStagingSelector,
|
isStagingSelector,
|
||||||
} from 'features/canvas/store/canvasSelectors';
|
} from 'features/canvas/store/canvasSelectors';
|
||||||
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
|
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
|
||||||
|
import { uiSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[canvasSelector, isStagingSelector],
|
[canvasSelector, isStagingSelector, uiSelector],
|
||||||
(canvas, isStaging) => {
|
(canvas, isStaging, ui) => {
|
||||||
const { boundingBoxDimensions } = canvas;
|
const { boundingBoxDimensions } = canvas;
|
||||||
|
const { aspectRatio } = ui;
|
||||||
return {
|
return {
|
||||||
boundingBoxDimensions,
|
boundingBoxDimensions,
|
||||||
isStaging,
|
isStaging,
|
||||||
|
aspectRatio,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
@ -25,7 +29,8 @@ const selector = createSelector(
|
|||||||
|
|
||||||
const ParamBoundingBoxWidth = () => {
|
const ParamBoundingBoxWidth = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { boundingBoxDimensions, isStaging } = useAppSelector(selector);
|
const { boundingBoxDimensions, isStaging, aspectRatio } =
|
||||||
|
useAppSelector(selector);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -36,6 +41,15 @@ const ParamBoundingBoxWidth = () => {
|
|||||||
height: Math.floor(v),
|
height: Math.floor(v),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (aspectRatio) {
|
||||||
|
const newWidth = roundToMultiple(v * aspectRatio, 64);
|
||||||
|
dispatch(
|
||||||
|
setBoundingBoxDimensions({
|
||||||
|
width: newWidth,
|
||||||
|
height: Math.floor(v),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResetHeight = () => {
|
const handleResetHeight = () => {
|
||||||
@ -45,6 +59,15 @@ const ParamBoundingBoxWidth = () => {
|
|||||||
height: Math.floor(512),
|
height: Math.floor(512),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (aspectRatio) {
|
||||||
|
const newWidth = roundToMultiple(512 * aspectRatio, 64);
|
||||||
|
dispatch(
|
||||||
|
setBoundingBoxDimensions({
|
||||||
|
width: newWidth,
|
||||||
|
height: Math.floor(512),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
import { Flex, Spacer, Text } from '@chakra-ui/react';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { flipBoundingBoxAxes } from 'features/canvas/store/canvasSlice';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { MdOutlineSwapVert } from 'react-icons/md';
|
||||||
|
import ParamAspectRatio from '../../Core/ParamAspectRatio';
|
||||||
|
import ParamBoundingBoxHeight from './ParamBoundingBoxHeight';
|
||||||
|
import ParamBoundingBoxWidth from './ParamBoundingBoxWidth';
|
||||||
|
|
||||||
|
export default function ParamBoundingBoxSize() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
gap: 2,
|
||||||
|
p: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
flexDirection: 'column',
|
||||||
|
w: 'full',
|
||||||
|
bg: 'base.150',
|
||||||
|
_dark: {
|
||||||
|
bg: 'base.750',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex alignItems="center" gap={2}>
|
||||||
|
<Text
|
||||||
|
sx={{
|
||||||
|
fontSize: 'sm',
|
||||||
|
width: 'full',
|
||||||
|
color: 'base.700',
|
||||||
|
_dark: {
|
||||||
|
color: 'base.300',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('parameters.aspectRatio')}
|
||||||
|
</Text>
|
||||||
|
<Spacer />
|
||||||
|
<ParamAspectRatio />
|
||||||
|
<IAIIconButton
|
||||||
|
tooltip={t('ui.swapSizes')}
|
||||||
|
aria-label={t('ui.swapSizes')}
|
||||||
|
size="sm"
|
||||||
|
icon={<MdOutlineSwapVert />}
|
||||||
|
fontSize={20}
|
||||||
|
onClick={() => dispatch(flipBoundingBoxAxes())}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<ParamBoundingBoxWidth />
|
||||||
|
<ParamBoundingBoxHeight />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
@ -2,22 +2,26 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import IAISlider from 'common/components/IAISlider';
|
import IAISlider from 'common/components/IAISlider';
|
||||||
|
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
import {
|
import {
|
||||||
canvasSelector,
|
canvasSelector,
|
||||||
isStagingSelector,
|
isStagingSelector,
|
||||||
} from 'features/canvas/store/canvasSelectors';
|
} from 'features/canvas/store/canvasSelectors';
|
||||||
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
|
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
|
||||||
|
import { uiSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[canvasSelector, isStagingSelector],
|
[canvasSelector, isStagingSelector, uiSelector],
|
||||||
(canvas, isStaging) => {
|
(canvas, isStaging, ui) => {
|
||||||
const { boundingBoxDimensions } = canvas;
|
const { boundingBoxDimensions } = canvas;
|
||||||
|
const { aspectRatio } = ui;
|
||||||
return {
|
return {
|
||||||
boundingBoxDimensions,
|
boundingBoxDimensions,
|
||||||
isStaging,
|
isStaging,
|
||||||
|
aspectRatio,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
@ -25,7 +29,8 @@ const selector = createSelector(
|
|||||||
|
|
||||||
const ParamBoundingBoxWidth = () => {
|
const ParamBoundingBoxWidth = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { boundingBoxDimensions, isStaging } = useAppSelector(selector);
|
const { boundingBoxDimensions, isStaging, aspectRatio } =
|
||||||
|
useAppSelector(selector);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -36,6 +41,15 @@ const ParamBoundingBoxWidth = () => {
|
|||||||
width: Math.floor(v),
|
width: Math.floor(v),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (aspectRatio) {
|
||||||
|
const newHeight = roundToMultiple(v / aspectRatio, 64);
|
||||||
|
dispatch(
|
||||||
|
setBoundingBoxDimensions({
|
||||||
|
width: Math.floor(v),
|
||||||
|
height: newHeight,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResetWidth = () => {
|
const handleResetWidth = () => {
|
||||||
@ -45,6 +59,15 @@ const ParamBoundingBoxWidth = () => {
|
|||||||
width: Math.floor(512),
|
width: Math.floor(512),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (aspectRatio) {
|
||||||
|
const newHeight = roundToMultiple(512 / aspectRatio, 64);
|
||||||
|
dispatch(
|
||||||
|
setBoundingBoxDimensions({
|
||||||
|
width: Math.floor(512),
|
||||||
|
height: newHeight,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -4,8 +4,7 @@ import { stateSelector } from 'app/store/store';
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import IAICollapse from 'common/components/IAICollapse';
|
import IAICollapse from 'common/components/IAICollapse';
|
||||||
import ParamBoundingBoxHeight from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight';
|
import ParamBoundingBoxSize from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxSize';
|
||||||
import ParamBoundingBoxWidth from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxWidth';
|
|
||||||
import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale';
|
import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale';
|
||||||
import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations';
|
import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations';
|
||||||
import ParamModelandVAEandScheduler from 'features/parameters/components/Parameters/Core/ParamModelandVAEandScheduler';
|
import ParamModelandVAEandScheduler from 'features/parameters/components/Parameters/Core/ParamModelandVAEandScheduler';
|
||||||
@ -51,8 +50,7 @@ const UnifiedCanvasCoreParameters = () => {
|
|||||||
<Box pt={2}>
|
<Box pt={2}>
|
||||||
<ParamSeedFull />
|
<ParamSeedFull />
|
||||||
</Box>
|
</Box>
|
||||||
<ParamBoundingBoxWidth />
|
<ParamBoundingBoxSize />
|
||||||
<ParamBoundingBoxHeight />
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -65,8 +63,7 @@ const UnifiedCanvasCoreParameters = () => {
|
|||||||
<Box pt={2}>
|
<Box pt={2}>
|
||||||
<ParamSeedFull />
|
<ParamSeedFull />
|
||||||
</Box>
|
</Box>
|
||||||
<ParamBoundingBoxWidth />
|
<ParamBoundingBoxSize />
|
||||||
<ParamBoundingBoxHeight />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ImageToImageStrength />
|
<ImageToImageStrength />
|
||||||
|
Loading…
Reference in New Issue
Block a user