More
This commit is contained in:
parent
40cd4caeec
commit
1d7c5705b2
@ -18,6 +18,7 @@ import { getNewZoom } from "../scene/zoom";
|
|||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics";
|
import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics";
|
||||||
import colors from "../colors";
|
import colors from "../colors";
|
||||||
|
import { GRID_SIZE } from "../constants";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
@ -64,7 +65,7 @@ export const actionClearCanvas = register({
|
|||||||
elementLocked: appState.elementLocked,
|
elementLocked: appState.elementLocked,
|
||||||
exportBackground: appState.exportBackground,
|
exportBackground: appState.exportBackground,
|
||||||
exportEmbedScene: appState.exportEmbedScene,
|
exportEmbedScene: appState.exportEmbedScene,
|
||||||
gridSize: appState.gridSize,
|
gridSize: appState.gridSize || GRID_SIZE,
|
||||||
shouldAddWatermark: appState.shouldAddWatermark,
|
shouldAddWatermark: appState.shouldAddWatermark,
|
||||||
showStats: appState.showStats,
|
showStats: appState.showStats,
|
||||||
},
|
},
|
||||||
|
@ -1,181 +1,167 @@
|
|||||||
|
import { Point, simplify } from "points-on-curve";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import rough from "roughjs/bin/rough";
|
|
||||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import { simplify, Point } from "points-on-curve";
|
import rough from "roughjs/bin/rough";
|
||||||
|
|
||||||
import {
|
|
||||||
newElement,
|
|
||||||
newTextElement,
|
|
||||||
duplicateElement,
|
|
||||||
isInvisiblySmallElement,
|
|
||||||
isTextElement,
|
|
||||||
textWysiwyg,
|
|
||||||
getCommonBounds,
|
|
||||||
getCursorForResizingElement,
|
|
||||||
getPerfectElementSize,
|
|
||||||
getNormalizedDimensions,
|
|
||||||
newLinearElement,
|
|
||||||
transformElements,
|
|
||||||
getElementWithTransformHandleType,
|
|
||||||
getResizeOffsetXY,
|
|
||||||
getResizeArrowDirection,
|
|
||||||
getTransformHandleTypeFromCoords,
|
|
||||||
isNonDeletedElement,
|
|
||||||
updateTextElement,
|
|
||||||
dragSelectedElements,
|
|
||||||
getDragOffsetXY,
|
|
||||||
dragNewElement,
|
|
||||||
hitTest,
|
|
||||||
isHittingElementBoundingBoxWithoutHittingElement,
|
|
||||||
getNonDeletedElements,
|
|
||||||
} from "../element";
|
|
||||||
import {
|
|
||||||
getElementsWithinSelection,
|
|
||||||
isOverScrollBars,
|
|
||||||
getElementsAtPosition,
|
|
||||||
getElementContainingPosition,
|
|
||||||
getNormalizedZoom,
|
|
||||||
getSelectedElements,
|
|
||||||
isSomeElementSelected,
|
|
||||||
calculateScrollCenter,
|
|
||||||
} from "../scene";
|
|
||||||
import { loadFromBlob, exportCanvas } from "../data";
|
|
||||||
|
|
||||||
import { renderScene } from "../renderer";
|
|
||||||
import {
|
|
||||||
AppState,
|
|
||||||
GestureEvent,
|
|
||||||
Gesture,
|
|
||||||
ExcalidrawProps,
|
|
||||||
SceneData,
|
|
||||||
} from "../types";
|
|
||||||
import {
|
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawTextElement,
|
|
||||||
NonDeleted,
|
|
||||||
ExcalidrawGenericElement,
|
|
||||||
ExcalidrawLinearElement,
|
|
||||||
ExcalidrawBindableElement,
|
|
||||||
} from "../element/types";
|
|
||||||
|
|
||||||
import { distance2d, isPathALoop, getGridPoint } from "../math";
|
|
||||||
|
|
||||||
import {
|
|
||||||
isWritableElement,
|
|
||||||
isInputLike,
|
|
||||||
isToolIcon,
|
|
||||||
debounce,
|
|
||||||
distance,
|
|
||||||
resetCursor,
|
|
||||||
viewportCoordsToSceneCoords,
|
|
||||||
sceneCoordsToViewportCoords,
|
|
||||||
setCursorForShape,
|
|
||||||
tupleToCoors,
|
|
||||||
ResolvablePromise,
|
|
||||||
resolvablePromise,
|
|
||||||
withBatchedUpdates,
|
|
||||||
} from "../utils";
|
|
||||||
import {
|
|
||||||
KEYS,
|
|
||||||
isArrowKey,
|
|
||||||
getResizeCenterPointKey,
|
|
||||||
getResizeWithSidesSameLengthKey,
|
|
||||||
getRotateWithDiscreteAngleKey,
|
|
||||||
CODES,
|
|
||||||
} from "../keys";
|
|
||||||
|
|
||||||
import { findShapeByKey } from "../shapes";
|
|
||||||
import { createHistory, SceneHistory } from "../history";
|
|
||||||
|
|
||||||
import ContextMenu from "./ContextMenu";
|
|
||||||
|
|
||||||
import { ActionManager } from "../actions/manager";
|
|
||||||
import "../actions";
|
import "../actions";
|
||||||
|
import { actionDeleteSelected, actionFinalize } from "../actions";
|
||||||
|
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
||||||
|
import { ActionManager } from "../actions/manager";
|
||||||
import { actions } from "../actions/register";
|
import { actions } from "../actions/register";
|
||||||
|
|
||||||
import { ActionResult } from "../actions/types";
|
import { ActionResult } from "../actions/types";
|
||||||
import { getDefaultAppState } from "../appState";
|
|
||||||
import { t, getLanguage } from "../i18n";
|
|
||||||
|
|
||||||
import {
|
|
||||||
copyToClipboard,
|
|
||||||
parseClipboard,
|
|
||||||
probablySupportsClipboardBlob,
|
|
||||||
probablySupportsClipboardWriteText,
|
|
||||||
} from "../clipboard";
|
|
||||||
import { normalizeScroll } from "../scene";
|
|
||||||
import { getCenter, getDistance } from "../gesture";
|
|
||||||
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
|
|
||||||
|
|
||||||
import {
|
|
||||||
CURSOR_TYPE,
|
|
||||||
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
|
|
||||||
ELEMENT_TRANSLATE_AMOUNT,
|
|
||||||
POINTER_BUTTON,
|
|
||||||
DRAGGING_THRESHOLD,
|
|
||||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
|
||||||
LINE_CONFIRM_THRESHOLD,
|
|
||||||
EVENT,
|
|
||||||
ENV,
|
|
||||||
CANVAS_ONLY_ACTIONS,
|
|
||||||
DEFAULT_VERTICAL_ALIGN,
|
|
||||||
GRID_SIZE,
|
|
||||||
MIME_TYPES,
|
|
||||||
TAP_TWICE_TIMEOUT,
|
|
||||||
TOUCH_CTX_MENU_TIMEOUT,
|
|
||||||
APP_NAME,
|
|
||||||
} from "../constants";
|
|
||||||
|
|
||||||
import LayerUI from "./LayerUI";
|
|
||||||
import { ScrollBars, SceneState } from "../scene/types";
|
|
||||||
import { mutateElement } from "../element/mutateElement";
|
|
||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
|
||||||
import {
|
|
||||||
isLinearElement,
|
|
||||||
isLinearElementType,
|
|
||||||
isBindingElement,
|
|
||||||
isBindingElementType,
|
|
||||||
} from "../element/typeChecks";
|
|
||||||
import { actionFinalize, actionDeleteSelected } from "../actions";
|
|
||||||
|
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
|
||||||
import {
|
|
||||||
getSelectedGroupIds,
|
|
||||||
isSelectedViaGroup,
|
|
||||||
selectGroupsForSelectedElements,
|
|
||||||
isElementInGroup,
|
|
||||||
getSelectedGroupIdForElement,
|
|
||||||
getElementsInGroup,
|
|
||||||
editGroupForSelectedElement,
|
|
||||||
} from "../groups";
|
|
||||||
import { Library } from "../data/library";
|
|
||||||
import Scene from "../scene/Scene";
|
|
||||||
import {
|
|
||||||
getHoveredElementForBinding,
|
|
||||||
maybeBindLinearElement,
|
|
||||||
getEligibleElementsForBinding,
|
|
||||||
bindOrUnbindSelectedElements,
|
|
||||||
unbindLinearElements,
|
|
||||||
fixBindingsAfterDuplication,
|
|
||||||
fixBindingsAfterDeletion,
|
|
||||||
isLinearElementSimpleAndAlreadyBound,
|
|
||||||
isBindingEnabled,
|
|
||||||
updateBoundElements,
|
|
||||||
shouldEnableBindingForPointerEvent,
|
|
||||||
} from "../element/binding";
|
|
||||||
import { MaybeTransformHandleType } from "../element/transformHandles";
|
|
||||||
import { deepCopyElement } from "../element/newElement";
|
|
||||||
import { renderSpreadsheet } from "../charts";
|
|
||||||
import { isValidLibrary } from "../data/json";
|
|
||||||
import { getNewZoom } from "../scene/zoom";
|
|
||||||
import { restore } from "../data/restore";
|
|
||||||
import {
|
import {
|
||||||
EVENT_DIALOG,
|
EVENT_DIALOG,
|
||||||
EVENT_LIBRARY,
|
EVENT_LIBRARY,
|
||||||
EVENT_SHAPE,
|
EVENT_SHAPE,
|
||||||
trackEvent,
|
trackEvent,
|
||||||
} from "../analytics";
|
} from "../analytics";
|
||||||
|
import { getDefaultAppState } from "../appState";
|
||||||
|
import { renderSpreadsheet } from "../charts";
|
||||||
|
import {
|
||||||
|
copyToClipboard,
|
||||||
|
parseClipboard,
|
||||||
|
probablySupportsClipboardBlob,
|
||||||
|
probablySupportsClipboardWriteText,
|
||||||
|
} from "../clipboard";
|
||||||
|
import {
|
||||||
|
APP_NAME,
|
||||||
|
CANVAS_ONLY_ACTIONS,
|
||||||
|
CURSOR_TYPE,
|
||||||
|
DEFAULT_VERTICAL_ALIGN,
|
||||||
|
DRAGGING_THRESHOLD,
|
||||||
|
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
|
||||||
|
ELEMENT_TRANSLATE_AMOUNT,
|
||||||
|
ENV,
|
||||||
|
EVENT,
|
||||||
|
LINE_CONFIRM_THRESHOLD,
|
||||||
|
MIME_TYPES,
|
||||||
|
POINTER_BUTTON,
|
||||||
|
TAP_TWICE_TIMEOUT,
|
||||||
|
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||||
|
TOUCH_CTX_MENU_TIMEOUT,
|
||||||
|
} from "../constants";
|
||||||
|
import { exportCanvas, loadFromBlob } from "../data";
|
||||||
|
import { isValidLibrary } from "../data/json";
|
||||||
|
import { Library } from "../data/library";
|
||||||
|
import { restore } from "../data/restore";
|
||||||
|
import {
|
||||||
|
dragNewElement,
|
||||||
|
dragSelectedElements,
|
||||||
|
duplicateElement,
|
||||||
|
getCommonBounds,
|
||||||
|
getCursorForResizingElement,
|
||||||
|
getDragOffsetXY,
|
||||||
|
getElementWithTransformHandleType,
|
||||||
|
getNonDeletedElements,
|
||||||
|
getNormalizedDimensions,
|
||||||
|
getPerfectElementSize,
|
||||||
|
getResizeArrowDirection,
|
||||||
|
getResizeOffsetXY,
|
||||||
|
getTransformHandleTypeFromCoords,
|
||||||
|
hitTest,
|
||||||
|
isHittingElementBoundingBoxWithoutHittingElement,
|
||||||
|
isInvisiblySmallElement,
|
||||||
|
isNonDeletedElement,
|
||||||
|
isTextElement,
|
||||||
|
newElement,
|
||||||
|
newLinearElement,
|
||||||
|
newTextElement,
|
||||||
|
textWysiwyg,
|
||||||
|
transformElements,
|
||||||
|
updateTextElement,
|
||||||
|
} from "../element";
|
||||||
|
import {
|
||||||
|
bindOrUnbindSelectedElements,
|
||||||
|
fixBindingsAfterDeletion,
|
||||||
|
fixBindingsAfterDuplication,
|
||||||
|
getEligibleElementsForBinding,
|
||||||
|
getHoveredElementForBinding,
|
||||||
|
isBindingEnabled,
|
||||||
|
isLinearElementSimpleAndAlreadyBound,
|
||||||
|
maybeBindLinearElement,
|
||||||
|
shouldEnableBindingForPointerEvent,
|
||||||
|
unbindLinearElements,
|
||||||
|
updateBoundElements,
|
||||||
|
} from "../element/binding";
|
||||||
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import { mutateElement } from "../element/mutateElement";
|
||||||
|
import { deepCopyElement } from "../element/newElement";
|
||||||
|
import { MaybeTransformHandleType } from "../element/transformHandles";
|
||||||
|
import {
|
||||||
|
isBindingElement,
|
||||||
|
isBindingElementType,
|
||||||
|
isLinearElement,
|
||||||
|
isLinearElementType,
|
||||||
|
} from "../element/typeChecks";
|
||||||
|
import {
|
||||||
|
ExcalidrawBindableElement,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawGenericElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
NonDeleted,
|
||||||
|
} from "../element/types";
|
||||||
|
import { getCenter, getDistance } from "../gesture";
|
||||||
|
import {
|
||||||
|
editGroupForSelectedElement,
|
||||||
|
getElementsInGroup,
|
||||||
|
getSelectedGroupIdForElement,
|
||||||
|
getSelectedGroupIds,
|
||||||
|
isElementInGroup,
|
||||||
|
isSelectedViaGroup,
|
||||||
|
selectGroupsForSelectedElements,
|
||||||
|
} from "../groups";
|
||||||
|
import { createHistory, SceneHistory } from "../history";
|
||||||
|
import { getLanguage, t } from "../i18n";
|
||||||
|
import {
|
||||||
|
CODES,
|
||||||
|
getResizeCenterPointKey,
|
||||||
|
getResizeWithSidesSameLengthKey,
|
||||||
|
getRotateWithDiscreteAngleKey,
|
||||||
|
isArrowKey,
|
||||||
|
KEYS,
|
||||||
|
} from "../keys";
|
||||||
|
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
||||||
|
import { renderScene } from "../renderer";
|
||||||
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||||
|
import {
|
||||||
|
calculateScrollCenter,
|
||||||
|
getElementContainingPosition,
|
||||||
|
getElementsAtPosition,
|
||||||
|
getElementsWithinSelection,
|
||||||
|
getNormalizedZoom,
|
||||||
|
getSelectedElements,
|
||||||
|
isOverScrollBars,
|
||||||
|
isSomeElementSelected,
|
||||||
|
normalizeScroll,
|
||||||
|
} from "../scene";
|
||||||
|
import Scene from "../scene/Scene";
|
||||||
|
import { SceneState, ScrollBars } from "../scene/types";
|
||||||
|
import { getNewZoom } from "../scene/zoom";
|
||||||
|
import { findShapeByKey } from "../shapes";
|
||||||
|
import {
|
||||||
|
AppState,
|
||||||
|
ExcalidrawProps,
|
||||||
|
Gesture,
|
||||||
|
GestureEvent,
|
||||||
|
SceneData,
|
||||||
|
} from "../types";
|
||||||
|
import {
|
||||||
|
debounce,
|
||||||
|
distance,
|
||||||
|
isInputLike,
|
||||||
|
isToolIcon,
|
||||||
|
isWritableElement,
|
||||||
|
resetCursor,
|
||||||
|
ResolvablePromise,
|
||||||
|
resolvablePromise,
|
||||||
|
sceneCoordsToViewportCoords,
|
||||||
|
setCursorForShape,
|
||||||
|
tupleToCoors,
|
||||||
|
viewportCoordsToSceneCoords,
|
||||||
|
withBatchedUpdates,
|
||||||
|
} from "../utils";
|
||||||
|
import ContextMenu from "./ContextMenu";
|
||||||
|
import LayerUI from "./LayerUI";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
|
|
||||||
const { history } = createHistory();
|
const { history } = createHistory();
|
||||||
@ -3662,7 +3648,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
CANVAS_ONLY_ACTIONS.includes(action.name),
|
CANVAS_ONLY_ACTIONS.includes(action.name),
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
checked: this.state.gridSize !== null,
|
checked: this.state.showGrid,
|
||||||
shortcutName: "gridMode",
|
shortcutName: "gridMode",
|
||||||
label: t("labels.gridMode"),
|
label: t("labels.gridMode"),
|
||||||
action: this.toggleGridMode,
|
action: this.toggleGridMode,
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
import React, { CSSProperties, useEffect, useState } from "react";
|
|
||||||
import classes from "./SlidableInput.module.css";
|
|
||||||
import { throttle } from "./utils/throttle";
|
|
||||||
|
|
||||||
interface SlidableInputProps {
|
|
||||||
value: number;
|
|
||||||
prefix?: string;
|
|
||||||
suffix?: string;
|
|
||||||
minValue?: number;
|
|
||||||
maxValue?: number;
|
|
||||||
style?: CSSProperties;
|
|
||||||
onChange?: (value: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SlidableInput: React.FC<SlidableInputProps> = ({
|
|
||||||
value,
|
|
||||||
style,
|
|
||||||
prefix,
|
|
||||||
suffix,
|
|
||||||
onChange,
|
|
||||||
minValue,
|
|
||||||
maxValue,
|
|
||||||
}) => {
|
|
||||||
const [isLocked, setIsLocked] = useState<boolean>(true);
|
|
||||||
const previousX = React.useRef(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onMouseMoveHandler = throttle((event: MouseEvent) => {
|
|
||||||
if (isLocked) return;
|
|
||||||
|
|
||||||
const nextX = event.screenX;
|
|
||||||
if (nextX === previousX.current) return;
|
|
||||||
const nextValue = value + (nextX > previousX.current ? 1 : -1);
|
|
||||||
|
|
||||||
onChange &&
|
|
||||||
nextValue <= (maxValue || Infinity) &&
|
|
||||||
nextValue >= (typeof minValue === "number" ? minValue : -Infinity) &&
|
|
||||||
onChange(nextValue);
|
|
||||||
|
|
||||||
previousX.current = nextX;
|
|
||||||
}, 250) as EventListenerOrEventListenerObject;
|
|
||||||
|
|
||||||
window.addEventListener("mousemove", onMouseMoveHandler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("mousemove", onMouseMoveHandler);
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [isLocked, value]);
|
|
||||||
|
|
||||||
const onMouseDown = () => setIsLocked(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onMouseUp = () => setIsLocked(true);
|
|
||||||
window.addEventListener("mouseup", onMouseUp);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("mouseup", onMouseUp);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className={classes.input} style={style} onMouseDown={onMouseDown}>
|
|
||||||
{prefix}
|
|
||||||
{value}
|
|
||||||
{suffix}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
@ -12,7 +12,6 @@ import { AppState } from "../types";
|
|||||||
import { debounce, nFormatter } from "../utils";
|
import { debounce, nFormatter } from "../utils";
|
||||||
import { close } from "./icons";
|
import { close } from "./icons";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { SlidableInput } from "./SlidableInput";
|
|
||||||
import "./Stats.scss";
|
import "./Stats.scss";
|
||||||
|
|
||||||
type StorageSizes = { scene: number; total: number };
|
type StorageSizes = { scene: number; total: number };
|
||||||
@ -158,19 +157,23 @@ export const Stats = (props: {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
{props.appState.showGrid && (
|
||||||
<tr>
|
<>
|
||||||
<th colSpan={2}>{"Misc"}</th>
|
<tr>
|
||||||
</tr>
|
<th colSpan={2}>{"Misc"}</th>
|
||||||
<tr>
|
</tr>
|
||||||
<td>{"Grid size"}</td>
|
<tr
|
||||||
<td>
|
onClick={() => {
|
||||||
<SlidableInput
|
// TODO: better way to update the gridSize and re-render the scene
|
||||||
value={props.appState.gridSize || 8}
|
props.appState.gridSize =
|
||||||
minValue={8}
|
((props.appState.gridSize - 5) % 50) + 10;
|
||||||
/>
|
}}
|
||||||
</td>
|
>
|
||||||
</tr>
|
<td>{"Grid size"}</td>
|
||||||
|
<td>{props.appState.gridSize}</td>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</Island>
|
</Island>
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
export const throttle = (func: Function, limit: number): Function => {
|
|
||||||
let inThrottle: boolean;
|
|
||||||
|
|
||||||
return function (this: any): any {
|
|
||||||
const args = arguments;
|
|
||||||
const context = this;
|
|
||||||
|
|
||||||
if (!inThrottle) {
|
|
||||||
inThrottle = true;
|
|
||||||
func.apply(context, args);
|
|
||||||
setTimeout(() => (inThrottle = false), limit);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
@ -233,7 +233,7 @@ export const renderScene = (
|
|||||||
context.scale(sceneState.zoom.value, sceneState.zoom.value);
|
context.scale(sceneState.zoom.value, sceneState.zoom.value);
|
||||||
|
|
||||||
// Grid
|
// Grid
|
||||||
if (renderGrid && appState.gridSize) {
|
if (renderGrid && appState.showGrid) {
|
||||||
strokeGrid(
|
strokeGrid(
|
||||||
context,
|
context,
|
||||||
appState.gridSize,
|
appState.gridSize,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user