feat: [] init
This commit is contained in:
parent
15113b4bbb
commit
5088bd113c
|
|
@ -142,6 +142,7 @@
|
|||
"js-sha256": "^0.9.0",
|
||||
"jshint": "^2.13.4",
|
||||
"klona": "^2.0.5",
|
||||
"leaflet": "^1.9.4",
|
||||
"libphonenumber-js": "^1.9.44",
|
||||
"linkedom": "^0.14.20",
|
||||
"localforage": "^1.7.3",
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
"react-helmet": "^5.2.1",
|
||||
"react-hook-form": "^7.28.0",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-leaflet": "3.2.5",
|
||||
"react-media-recorder": "^1.6.1",
|
||||
"react-modal": "^3.15.1",
|
||||
"react-page-visibility": "^7.0.0",
|
||||
|
|
|
|||
158
app/client/src/widgets/OpenStreetMapWidget/component/index.tsx
Normal file
158
app/client/src/widgets/OpenStreetMapWidget/component/index.tsx
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import React, { useEffect } from "react";
|
||||
import type { WidgetProps } from "widgets/BaseWidget";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import L from "leaflet";
|
||||
import { FullScreen, useFullScreenHandle } from "react-full-screen";
|
||||
import { Icon } from "@blueprintjs/core";
|
||||
|
||||
// Контейнер для карты с относительным позиционированием
|
||||
const MapWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
// Кнопка для переключения полноэкранного режима
|
||||
const FullscreenButton = styled.button`
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
`;
|
||||
|
||||
// 🧩 фикс, чтобы маркеры отображались (иначе пустые иконки)
|
||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"),
|
||||
iconUrl: require("leaflet/dist/images/marker-icon.png"),
|
||||
shadowUrl: require("leaflet/dist/images/marker-shadow.png"),
|
||||
});
|
||||
|
||||
// Компонент для обновления размера карты при изменении размера контейнера
|
||||
function MapResizer() {
|
||||
const map = useMap();
|
||||
|
||||
useEffect(() => {
|
||||
// Обновляем размер карты при монтировании
|
||||
const timer = setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [map]);
|
||||
|
||||
// Обновляем размер при изменении размера окна
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
map.invalidateSize();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, [map]);
|
||||
|
||||
// Обновляем размер при переключении полноэкранного режима
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
// Небольшая задержка, чтобы контейнер успел изменить размер
|
||||
setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||
document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
|
||||
document.addEventListener("mozfullscreenchange", handleFullscreenChange);
|
||||
document.addEventListener("MSFullscreenChange", handleFullscreenChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
||||
document.removeEventListener("webkitfullscreenchange", handleFullscreenChange);
|
||||
document.removeEventListener("mozfullscreenchange", handleFullscreenChange);
|
||||
document.removeEventListener("MSFullscreenChange", handleFullscreenChange);
|
||||
};
|
||||
}, [map]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function OpenStreetMapComponent(props: OpenStreetMapComponentProps) {
|
||||
// Получаем значения из props или используем значения по умолчанию
|
||||
const centerLat = props.centerLat ?? 55.751244;
|
||||
const centerLng = props.centerLng ?? 37.618423;
|
||||
const zoom = props.zoom ?? 7;
|
||||
const markerLat = props.markerLat ?? centerLat;
|
||||
const markerLng = props.markerLng ?? centerLng;
|
||||
const markerText = props.markerText ?? "Привет! Это Москва 🏙️";
|
||||
|
||||
const center: [number, number] = [centerLat, centerLng];
|
||||
const markerPosition: [number, number] = [markerLat, markerLng];
|
||||
|
||||
// Хук для управления полноэкранным режимом
|
||||
const fullScreenHandle = useFullScreenHandle();
|
||||
|
||||
return (
|
||||
<MapWrapper>
|
||||
<FullScreen handle={fullScreenHandle}>
|
||||
<MapContainer
|
||||
center={center}
|
||||
zoom={zoom}
|
||||
style={{ height: "500px", width: "500px" }}
|
||||
>
|
||||
<MapResizer />
|
||||
<TileLayer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
attribution='© <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors'
|
||||
/>
|
||||
<Marker position={markerPosition}>
|
||||
<Popup>{markerText}</Popup>
|
||||
</Marker>
|
||||
</MapContainer>
|
||||
</FullScreen>
|
||||
|
||||
{/* Кнопка для переключения полноэкранного режима */}
|
||||
<FullscreenButton
|
||||
onClick={fullScreenHandle.active ? fullScreenHandle.exit : fullScreenHandle.enter}
|
||||
title={fullScreenHandle.active ? "Выйти из полноэкранного режима" : "Развернуть на весь экран"}
|
||||
>
|
||||
<Icon
|
||||
icon={fullScreenHandle.active ? "minimize" : "maximize"}
|
||||
iconSize={16}
|
||||
/>
|
||||
</FullscreenButton>
|
||||
</MapWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export interface OpenStreetMapComponentProps extends WidgetProps {
|
||||
centerLat?: number;
|
||||
centerLng?: number;
|
||||
zoom?: number;
|
||||
markerLat?: number;
|
||||
markerLng?: number;
|
||||
markerText?: string;
|
||||
}
|
||||
|
||||
export default OpenStreetMapComponent;
|
||||
9
app/client/src/widgets/OpenStreetMapWidget/constants.ts
Normal file
9
app/client/src/widgets/OpenStreetMapWidget/constants.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// This file contains common constants which can be used across the widget configuration file (index.ts), widget and component folders.
|
||||
export const OPENSTREETMAP_WIDGET_CONSTANT = "";
|
||||
|
||||
export enum OverflowTypes {
|
||||
SCROLL = "SCROLL",
|
||||
TRUNCATE = "TRUNCATE",
|
||||
NONE = "NONE",
|
||||
}
|
||||
|
||||
2
app/client/src/widgets/OpenStreetMapWidget/icon.svg
Normal file
2
app/client/src/widgets/OpenStreetMapWidget/icon.svg
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" role="img"><title>OpenStreetMap icon</title><path d="M2.672 23.969c-.352-.089-.534-.234-1.471-1.168C.085 21.688.014 21.579.018 20.999c0-.645-.196-.414 3.368-3.986 3.6-3.608 3.415-3.451 4.064-3.449.302 0 .378.016.62.14l.277.14 1.744-1.744-.218-.343c-.425-.662-.825-1.629-1.006-2.429a7.657 7.657 0 0 1 1.479-6.44c2.49-3.12 6.959-3.812 10.26-1.588 1.812 1.218 2.99 3.099 3.328 5.314.07.467.07 1.579 0 2.074a7.554 7.554 0 0 1-2.205 4.402 6.712 6.712 0 0 1-1.943 1.401c-.959.483-1.775.71-2.881.803-1.573.131-3.32-.305-4.656-1.163l-.343-.218-1.744 1.744.14.28c.125.241.14.316.14.617.003.651.156.467-3.426 4.049-2.761 2.756-3.186 3.164-3.398 3.261-.271.125-.69.171-.945.106zM17.485 13.95a6.425 6.425 0 0 0 4.603-3.51c1.391-2.899.455-6.306-2.227-8.108-.638-.43-1.529-.794-2.367-.962-.581-.117-1.809-.104-2.414.025a6.593 6.593 0 0 0-2.452 1.064c-.444.315-1.177 1.048-1.487 1.487a6.384 6.384 0 0 0 .38 7.907 6.406 6.406 0 0 0 3.901 2.136c.509.078 1.542.058 2.065-.037zm-3.738 7.376a80.97 80.97 0 0 1-2.196-.651c-.025-.028 1.207-4.396 1.257-4.449.023-.026 4.242 1.152 4.414 1.236.062.026-.003.288-.525 2.102a398.513 398.513 0 0 0-.635 2.236c-.025.087-.069.156-.097.156-.028-.003-1.028-.287-2.219-.631zm2.912.524c0-.053 1.227-4.333 1.246-4.347.047-.034 4.324-1.23 4.341-1.211.019.019-1.199 4.337-1.23 4.36-.02.019-4.126 1.191-4.259 1.218-.054.011-.098 0-.098-.019zm-7.105-1.911c.846-.852 1.599-1.627 1.674-1.728.171-.218.405-.732.472-1.015.026-.118.053-.352.058-.522l.011-.307.182-.051c.103-.028.193-.044.202-.034.023.025-1.207 4.321-1.246 4.36-.02.016-.677.213-1.464.436l-1.425.405 1.537-1.542zm8.289-3.06a1.371 1.371 0 0 1-.059-.187l-.044-.156.156-.028c1.339-.227 2.776-.856 3.908-1.713.16-.125.252-.171.265-.134.054.165.272.95.265.959-.034.034-4.48 1.282-4.492 1.261zm-15.083-1.3c-.05-.039-1.179-3.866-1.264-4.29-.016-.084.146-.044 2.174.536 2.121.604 2.192.629 2.222.74.028.098.011.129-.125.223-.084.059-.769.724-1.523 1.479a63.877 63.877 0 0 1-1.39 1.367c-.016 0-.056-.025-.093-.054zm.821-4.378c-1.188-.343-2.164-.623-2.167-.626-.016-.012 1.261-4.433 1.285-4.46.022-.022 4.422 1.211 4.469 1.252.009.009-.269 1.017-.618 2.239-.576 2.02-.643 2.224-.723 2.22-.05-.003-1.059-.285-2.247-.626zm2.959.538c.012-.031.212-.723.444-1.534l.42-1.476.056.321c.093.556.265 1.188.464 1.741.106.296.187.539.181.545-.008.006-.332.101-.719.212-.389.109-.741.21-.786.224-.058.016-.075.006-.059-.034zM4.905 6.112c-1.187-.339-2.167-.635-2.18-.654-.04-.062-1.246-4.321-1.23-4.338.026-.025 4.31 1.204 4.351 1.246.047.051 1.28 4.379 1.246 4.376L4.91 6.113zm2.148-1.713l-.519-1.806-.078-.28 1.693-.483c.934-.265 1.724-.495 1.76-.508.034-.016-.083.14-.26.336A8.729 8.729 0 0 0 7.69 5.23a4.348 4.348 0 0 0-.132.561c0 .293-.115-.025-.505-1.39z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
3
app/client/src/widgets/OpenStreetMapWidget/index.ts
Normal file
3
app/client/src/widgets/OpenStreetMapWidget/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Widget from "./widget";
|
||||
|
||||
export default Widget;
|
||||
203
app/client/src/widgets/OpenStreetMapWidget/widget/index.tsx
Normal file
203
app/client/src/widgets/OpenStreetMapWidget/widget/index.tsx
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import React from "react";
|
||||
|
||||
import type { DerivedPropertiesMap } from "WidgetProvider/factory/types";
|
||||
|
||||
import BaseWidget from "widgets/BaseWidget";
|
||||
import type { WidgetProps, WidgetState } from "widgets/BaseWidget";
|
||||
|
||||
import OpenStreetMapComponent from "../component";
|
||||
import { ValidationTypes } from "constants/WidgetValidation";
|
||||
|
||||
import IconSVG from "../icon.svg";
|
||||
import { WIDGET_TAGS } from "constants/WidgetConstants";
|
||||
|
||||
class OpenStreetMapWidget extends BaseWidget<OpenStreetMapWidgetProps, WidgetState> {
|
||||
static type = "OPENSTREETMAP_WIDGET";
|
||||
|
||||
//Метаданные
|
||||
static getConfig() {
|
||||
return {
|
||||
name: "OpenStreetMap",
|
||||
iconSVG: IconSVG,
|
||||
needsMeta: false,
|
||||
isCanvas: false,
|
||||
tags: [WIDGET_TAGS.CONTENT],
|
||||
searchTags: ["map", "openstreet", "open"],
|
||||
};
|
||||
}
|
||||
|
||||
static getFeatures() {
|
||||
return {
|
||||
dynamicHeight: {
|
||||
sectionIndex: 0,
|
||||
active: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
//Значения по умолчанию
|
||||
static getDefaults() {
|
||||
return {
|
||||
widgetName: "OpenStreetMap",
|
||||
rows: 10,
|
||||
columns: 10,
|
||||
version: 1,
|
||||
// Значения по умолчанию для карты
|
||||
centerLat: 55.751244,
|
||||
centerLng: 37.618423,
|
||||
zoom: 7,
|
||||
markerLat: 55.751244,
|
||||
markerLng: 37.618423,
|
||||
markerText: "Привет! Это Москва 🏙️",
|
||||
};
|
||||
}
|
||||
|
||||
static getPropertyPaneContentConfig() {
|
||||
return [
|
||||
{
|
||||
sectionName: "General",
|
||||
children: [
|
||||
{
|
||||
propertyName: "centerLat",
|
||||
helpText: "Широта центра карты",
|
||||
label: "Широта центра",
|
||||
controlType: "INPUT_TEXT",
|
||||
placeholderText: "55.751244",
|
||||
defaultValue: 55.751244,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: {
|
||||
type: ValidationTypes.NUMBER,
|
||||
params: {
|
||||
min: -90,
|
||||
max: 90,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
propertyName: "centerLng",
|
||||
helpText: "Долгота центра карты",
|
||||
label: "Долгота центра",
|
||||
controlType: "INPUT_TEXT",
|
||||
placeholderText: "37.618423",
|
||||
defaultValue: 37.618423,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: {
|
||||
type: ValidationTypes.NUMBER,
|
||||
params: {
|
||||
min: -180,
|
||||
max: 180,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
propertyName: "zoom",
|
||||
helpText: "Уровень масштабирования карты (1-18)",
|
||||
label: "Масштаб",
|
||||
controlType: "INPUT_TEXT",
|
||||
placeholderText: "7",
|
||||
defaultValue: 7,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: {
|
||||
type: ValidationTypes.NUMBER,
|
||||
params: {
|
||||
min: 1,
|
||||
max: 18,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
propertyName: "markerLat",
|
||||
helpText: "Широта маркера",
|
||||
label: "Широта маркера",
|
||||
controlType: "INPUT_TEXT",
|
||||
placeholderText: "55.751244",
|
||||
defaultValue: 55.751244,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: {
|
||||
type: ValidationTypes.NUMBER,
|
||||
params: {
|
||||
min: -90,
|
||||
max: 90,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
propertyName: "markerLng",
|
||||
helpText: "Долгота маркера",
|
||||
label: "Долгота маркера",
|
||||
controlType: "INPUT_TEXT",
|
||||
placeholderText: "37.618423",
|
||||
defaultValue: 37.618423,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: {
|
||||
type: ValidationTypes.NUMBER,
|
||||
params: {
|
||||
min: -180,
|
||||
max: 180,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
propertyName: "markerText",
|
||||
helpText: "Текст во всплывающем окне маркера",
|
||||
label: "Текст маркера",
|
||||
controlType: "INPUT_TEXT",
|
||||
placeholderText: "Привет! Это Москва 🏙️",
|
||||
defaultValue: "Привет! Это Москва 🏙️",
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: {
|
||||
type: ValidationTypes.TEXT,
|
||||
},
|
||||
},
|
||||
{
|
||||
propertyName: "isVisible",
|
||||
helpText: "Управляет видимостью виджета",
|
||||
label: "Видимый",
|
||||
controlType: "SWITCH",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: { type: ValidationTypes.BOOLEAN },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
static getPropertyPaneStyleConfig() {
|
||||
return [];
|
||||
}
|
||||
|
||||
static getDerivedPropertiesMap(): DerivedPropertiesMap {
|
||||
return {};
|
||||
}
|
||||
|
||||
static getDefaultPropertiesMap(): Record<string, string> {
|
||||
return {};
|
||||
}
|
||||
|
||||
static getMetaPropertiesMap(): Record<string, any> {
|
||||
return {};
|
||||
}
|
||||
|
||||
//Рендерит компонент
|
||||
getWidgetView() {
|
||||
return <OpenStreetMapComponent {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
export interface OpenStreetMapWidgetProps extends WidgetProps {
|
||||
centerLat?: number;
|
||||
centerLng?: number;
|
||||
zoom?: number;
|
||||
markerLat?: number;
|
||||
markerLng?: number;
|
||||
markerText?: string;
|
||||
}
|
||||
|
||||
export default OpenStreetMapWidget;
|
||||
|
|
@ -368,6 +368,10 @@ const WidgetLoaders = new Map<string, () => Promise<typeof BaseWidget>>([
|
|||
"EXTERNAL_WIDGET",
|
||||
async () => import("./ExternalWidget").then((m) => m.default),
|
||||
],
|
||||
[
|
||||
"OPENSTREETMAP_WIDGET",
|
||||
async () => import("./OpenStreetMapWidget").then((m) => m.default),
|
||||
],
|
||||
|
||||
// Deprecated Widgets
|
||||
[
|
||||
|
|
|
|||
|
|
@ -7436,6 +7436,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-leaflet/core@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "@react-leaflet/core@npm:1.1.1"
|
||||
peerDependencies:
|
||||
leaflet: ^1.7.1
|
||||
react: ^17.0.1
|
||||
react-dom: ^17.0.1
|
||||
checksum: 2fc4a80e5524f9437ac6cef0f95e63388f2df6ecc5107fef85fd097eb2455436e796d41a4c43cdcbb983a4132403646823ba1dc0d953e866eb80808cbfaf0232
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-spectrum/utils@npm:^3.11.10, @react-spectrum/utils@npm:^3.9.0":
|
||||
version: 3.11.10
|
||||
resolution: "@react-spectrum/utils@npm:3.11.10"
|
||||
|
|
@ -13837,6 +13848,7 @@ __metadata:
|
|||
json5: ^2.2.3
|
||||
klona: ^2.0.5
|
||||
knip: ^5.30.2
|
||||
leaflet: ^1.9.4
|
||||
libphonenumber-js: ^1.9.44
|
||||
linkedom: ^0.14.20
|
||||
lint-staged: ^14.0.1
|
||||
|
|
@ -13900,6 +13912,7 @@ __metadata:
|
|||
react-hook-form: ^7.28.0
|
||||
react-is: ^16.12.0
|
||||
react-json-view: ^1.21.3
|
||||
react-leaflet: 3.2.5
|
||||
react-media-recorder: ^1.6.1
|
||||
react-modal: ^3.15.1
|
||||
react-page-visibility: ^7.0.0
|
||||
|
|
@ -24138,6 +24151,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"leaflet@npm:^1.9.4":
|
||||
version: 1.9.4
|
||||
resolution: "leaflet@npm:1.9.4"
|
||||
checksum: bfc79f17a247b37b92d84b3c78702501603392d6589fde606de4a825d11f1609d90225388834f2e0709dac327e52dcd4b4b9cc9fd3d590060c5b1e53b84fa6c6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"leven@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "leven@npm:3.1.0"
|
||||
|
|
@ -29956,6 +29976,19 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-leaflet@npm:3.2.5":
|
||||
version: 3.2.5
|
||||
resolution: "react-leaflet@npm:3.2.5"
|
||||
dependencies:
|
||||
"@react-leaflet/core": ^1.1.1
|
||||
peerDependencies:
|
||||
leaflet: ^1.7.1
|
||||
react: ^17.0.1
|
||||
react-dom: ^17.0.1
|
||||
checksum: 503b7cee8acc12e0e2c5e7675432e7ef5742463e3e5420282ce60a9efd306430caefae3cb282c976e3df7665f19aef5b49cdce44a034979b4ea3ee968c2621d2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-lifecycles-compat@npm:^3.0.0, react-lifecycles-compat@npm:^3.0.4":
|
||||
version: 3.0.4
|
||||
resolution: "react-lifecycles-compat@npm:3.0.4"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user