Styling tweaks to the dashboar
This commit is contained in:
parent
812563ddc5
commit
89278f6d74
5 changed files with 177 additions and 146 deletions
|
@ -20,17 +20,40 @@ import {UIEventSource} from "../Logic/UIEventSource";
|
|||
import LanguagePicker from "./LanguagePicker";
|
||||
import Lazy from "./Base/Lazy";
|
||||
import TagRenderingAnswer from "./Popup/TagRenderingAnswer";
|
||||
import Hash from "../Logic/Web/Hash";
|
||||
import FilterView from "./BigComponents/FilterView";
|
||||
import {FilterState} from "../Models/FilteredLayer";
|
||||
|
||||
|
||||
export default class DashboardGui {
|
||||
private readonly guiState: DefaultGuiState;
|
||||
private readonly state: FeaturePipelineState;
|
||||
private readonly currentView: UIEventSource<string | BaseUIElement> = new UIEventSource<string | BaseUIElement>("No selection")
|
||||
|
||||
|
||||
constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
|
||||
this.state = state;
|
||||
this.guiState = guiState;
|
||||
}
|
||||
|
||||
private viewSelector(shown: BaseUIElement, fullview: BaseUIElement, hash?: string): BaseUIElement {
|
||||
const currentView = this.currentView
|
||||
shown.SetClass("pl-1 pr-1 rounded-md")
|
||||
shown.onClick(() => {
|
||||
currentView.setData(fullview)
|
||||
})
|
||||
Hash.hash.addCallbackAndRunD(h => {
|
||||
if (h === hash) {
|
||||
currentView.setData(fullview)
|
||||
}
|
||||
})
|
||||
currentView.addCallbackAndRunD(cv => {
|
||||
if (cv == fullview) {
|
||||
shown.SetClass("bg-unsubtle")
|
||||
Hash.hash.setData(hash)
|
||||
} else {
|
||||
shown.RemoveClass("bg-unsubtle")
|
||||
}
|
||||
})
|
||||
return shown;
|
||||
}
|
||||
|
||||
private singleElementCache: Record<string, BaseUIElement> = {}
|
||||
|
@ -41,27 +64,16 @@ export default class DashboardGui {
|
|||
}
|
||||
const tags = this.state.allElements.getEventSourceById(element.properties.id)
|
||||
const title = new Combine([new Title(new TagRenderingAnswer(tags, layer.title, this.state), 4),
|
||||
Math.floor(distance) + "m away"
|
||||
]).SetClass("flex");
|
||||
// FeatureInfoBox.GenerateTitleBar(tags, layer, this.state)
|
||||
distance < 900 ? Math.floor(distance)+"m away":
|
||||
Utils.Round(distance / 1000) + "km away"
|
||||
]).SetClass("flex justify-between");
|
||||
|
||||
const currentView = this.currentView
|
||||
const info = new Lazy(() => new Combine([
|
||||
FeatureInfoBox.GenerateTitleBar(tags, layer, this.state),
|
||||
FeatureInfoBox.GenerateContent(tags, layer, this.state)]).SetStyle("overflox-x: hidden"));
|
||||
title.onClick(() => {
|
||||
currentView.setData(info)
|
||||
})
|
||||
|
||||
currentView.addCallbackAndRunD(cv => {
|
||||
if (cv == info) {
|
||||
title.SetClass("bg-blue-300")
|
||||
} else {
|
||||
title.RemoveClass("bg-blue-300")
|
||||
}
|
||||
})
|
||||
|
||||
return title;
|
||||
return this.viewSelector(title, info);
|
||||
}
|
||||
|
||||
private mainElementsView(elements: { element: OsmFeature, layer: LayerConfig, distance: number }[]): BaseUIElement {
|
||||
|
@ -75,6 +87,58 @@ export default class DashboardGui {
|
|||
return new Combine(elements.map(e => self.singleElementView(e.element, e.layer, e.distance)))
|
||||
}
|
||||
|
||||
private visibleElements(map: MinimapObj & BaseUIElement, layers: Record<string, LayerConfig>): { distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[]{
|
||||
const bbox= map.bounds.data
|
||||
if (bbox === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const location = map.location.data;
|
||||
const loc: [number, number] = [location.lon, location.lat]
|
||||
|
||||
const elementsWithMeta: { features: OsmFeature[], layer: string }[] = this.state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox)
|
||||
|
||||
let elements: { distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[] = []
|
||||
let seenElements = new Set<string>()
|
||||
for (const elementsWithMetaElement of elementsWithMeta) {
|
||||
const layer = layers[elementsWithMetaElement.layer]
|
||||
const filtered = this.state.filteredLayers.data.find(fl => fl.layerDef == layer);
|
||||
for (const element of elementsWithMetaElement.features) {
|
||||
console.log("Inspecting ", element.properties.id)
|
||||
if(!filtered.isDisplayed.data){
|
||||
continue
|
||||
}
|
||||
if (seenElements.has(element.properties.id)) {
|
||||
continue
|
||||
}
|
||||
seenElements.add(element.properties.id)
|
||||
if (!bbox.overlapsWith(BBox.get(element))) {
|
||||
continue
|
||||
}
|
||||
if (layer?.isShown?.GetRenderValue(element)?.Subs(element.properties)?.txt === "no") {
|
||||
continue
|
||||
}
|
||||
const activeFilters : FilterState[] = Array.from(filtered.appliedFilters.data.values());
|
||||
if(activeFilters.some(filter => !filter?.currentFilter?.matchesProperties(element.properties))){
|
||||
continue
|
||||
}
|
||||
const center = GeoOperations.centerpointCoordinates(element);
|
||||
elements.push({
|
||||
element,
|
||||
center,
|
||||
layer: layers[elementsWithMetaElement.layer],
|
||||
distance: GeoOperations.distanceBetween(loc, center)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
elements.sort((e0, e1) => e0.distance - e1.distance)
|
||||
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
public setup(): void {
|
||||
|
||||
const state = this.state;
|
||||
|
@ -95,75 +159,51 @@ export default class DashboardGui {
|
|||
layers[layer.id] = layer;
|
||||
}
|
||||
|
||||
|
||||
const elementsInview = map.bounds.map(bbox => {
|
||||
if (bbox === undefined) {
|
||||
return undefined
|
||||
const self = this;
|
||||
const elementsInview = new UIEventSource([]);
|
||||
function update(){
|
||||
elementsInview.setData( self.visibleElements(map, layers))
|
||||
}
|
||||
|
||||
map.bounds.addCallbackAndRun(update)
|
||||
state.featurePipeline.newDataLoadedSignal.addCallback(update);
|
||||
state.filteredLayers.addCallbackAndRun(fls => {
|
||||
for (const fl of fls) {
|
||||
fl.isDisplayed.addCallback(update)
|
||||
fl.appliedFilters.addCallback(update)
|
||||
}
|
||||
const location = map.location.data;
|
||||
const loc: [number, number] = [location.lon, location.lat]
|
||||
|
||||
const elementsWithMeta: { features: OsmFeature[], layer: string }[] = state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox)
|
||||
|
||||
let elements: { distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[] = []
|
||||
let seenElements = new Set<string>()
|
||||
for (const elementsWithMetaElement of elementsWithMeta) {
|
||||
for (const element of elementsWithMetaElement.features) {
|
||||
if (!bbox.overlapsWith(BBox.get(element))) {
|
||||
continue
|
||||
}
|
||||
if (seenElements.has(element.properties.id)) {
|
||||
continue
|
||||
}
|
||||
seenElements.add(element.properties.id)
|
||||
const center = GeoOperations.centerpointCoordinates(element);
|
||||
elements.push({
|
||||
element,
|
||||
center,
|
||||
layer: layers[elementsWithMetaElement.layer],
|
||||
distance: GeoOperations.distanceBetween(loc, center)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
elements.sort((e0, e1) => e0.distance - e1.distance)
|
||||
|
||||
|
||||
return elements;
|
||||
}, [this.state.featurePipeline.newDataLoadedSignal]);
|
||||
})
|
||||
|
||||
const welcome = new Combine([state.layoutToUse.description, state.layoutToUse.descriptionTail])
|
||||
const self = this;
|
||||
self.currentView.setData(welcome)
|
||||
new Combine([
|
||||
|
||||
new Combine([map.SetClass("w-full h-64"),
|
||||
new Title(state.layoutToUse.title, 2).onClick(() => {
|
||||
self.currentView.setData(welcome)
|
||||
}),
|
||||
new LanguagePicker(Object.keys(state.layoutToUse.title)),
|
||||
new Combine([
|
||||
this.viewSelector(new Title(state.layoutToUse.title, 2), welcome),
|
||||
map.SetClass("w-full h-64 shrink-0 rounded-lg"),
|
||||
new SearchAndGo(state),
|
||||
new Title(
|
||||
new VariableUiElement(elementsInview.map(elements => "There are " + elements?.length + " elements in view")))
|
||||
.onClick(() => self.currentView.setData("Statistics")),
|
||||
new VariableUiElement(elementsInview.map(elements => this.mainElementsView(elements)))])
|
||||
.SetClass("w-1/2 m-4"),
|
||||
new VariableUiElement(this.currentView).SetClass("w-1/2 h-full overflow-y-auto m-4")
|
||||
this.viewSelector(new Title(
|
||||
new VariableUiElement(elementsInview.map(elements => "There are " + elements?.length + " elements in view"))), new FixedUiElement("Stats")),
|
||||
|
||||
this.viewSelector(new FixedUiElement("Filter"),
|
||||
new Lazy(() => {
|
||||
return new FilterView(state.filteredLayers, state.overlayToggles)
|
||||
})
|
||||
),
|
||||
|
||||
new VariableUiElement(elementsInview.map(elements => this.mainElementsView(elements).SetClass("block mx-2")))
|
||||
.SetClass("block shrink-2 overflow-x-scroll h-full border-2 border-subtle rounded-lg"),
|
||||
new LanguagePicker(Object.keys(state.layoutToUse.title)).SetClass("mt-2")
|
||||
])
|
||||
.SetClass("w-1/2 m-4 flex flex-col"),
|
||||
new VariableUiElement(this.currentView).SetClass("w-1/2 overflow-y-auto m-4 ml-0 p-2 border-2 border-subtle rounded-xl m-y-8")
|
||||
]).SetClass("flex h-full")
|
||||
.AttachTo("leafletDiv")
|
||||
|
||||
}
|
||||
|
||||
private SetupElement() {
|
||||
const t = new Title("Elements in view", 3)
|
||||
|
||||
}
|
||||
|
||||
private SetupMap(): MinimapObj & BaseUIElement {
|
||||
const state = this.state;
|
||||
const guiState = this.guiState;
|
||||
|
||||
new ShowDataLayer({
|
||||
leafletMap: state.leafletMap,
|
||||
|
|
|
@ -140,7 +140,7 @@
|
|||
},
|
||||
"render": {
|
||||
"en": "There are {capacity:disabled} disabled parking spots",
|
||||
"nl": "Er zijn capacity:disabled} parkeerplaatsen voor gehandicapten"
|
||||
"nl": "Er zijn {capacity:disabled} parkeerplaatsen voor gehandicapten"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
{
|
||||
"id": "mapcomplete-changes",
|
||||
"title": {
|
||||
"en": "Changes made with MapComplete",
|
||||
"nl": "Wijzigingen gemaakt met MapComplete",
|
||||
"de": "Mit MapComplete vorgenommene Änderungen"
|
||||
"en": "Changes made with MapComplete"
|
||||
},
|
||||
"shortDescription": {
|
||||
"en": "Shows changes made by MapComplete",
|
||||
"nl": "Toont wijzigingen gemaakt met MapComplete",
|
||||
"de": "Zeigt die mit MapComplete vorgenommenen Änderungen"
|
||||
"en": "Shows changes made by MapComplete"
|
||||
},
|
||||
"description": {
|
||||
"en": "This maps shows all the changes made with MapComplete",
|
||||
"nl": "Deze kaart toont alle wijzigingen die met MapComplete werden gemaakt",
|
||||
"de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen"
|
||||
"en": "This maps shows all the changes made with MapComplete"
|
||||
},
|
||||
"maintainer": "",
|
||||
"icon": "./assets/svg/logo.svg",
|
||||
|
@ -28,8 +22,7 @@
|
|||
{
|
||||
"id": "mapcomplete-changes",
|
||||
"name": {
|
||||
"en": "Changeset centers",
|
||||
"de": "Zentrum der Änderungssätze"
|
||||
"en": "Changeset centers"
|
||||
},
|
||||
"minzoom": 0,
|
||||
"source": {
|
||||
|
@ -43,47 +36,35 @@
|
|||
],
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Changeset for {theme}",
|
||||
"nl": "Wijzigingset voor {theme}",
|
||||
"de": "Änderungssatz für {theme}"
|
||||
"en": "Changeset for {theme}"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"en": "Shows all MapComplete changes",
|
||||
"nl": "Toont alle wijzigingen met MapComplete",
|
||||
"de": "Zeigt alle MapComplete Änderungen"
|
||||
"en": "Shows all MapComplete changes"
|
||||
},
|
||||
"tagRenderings": [
|
||||
{
|
||||
"id": "render_id",
|
||||
"render": {
|
||||
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
|
||||
"nl": "Wijzigingset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
|
||||
"de": "Änderungssatz <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
|
||||
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "contributor",
|
||||
"render": {
|
||||
"en": "Change made by <a href='https://openstreetmap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}</a>",
|
||||
"nl": "Wijziging gemaakt door <a href='https://openstreetmap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}</a>",
|
||||
"de": "Geändert von <a href='https://openstreetmap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}</a>"
|
||||
"en": "Change made by <a href='https://openstreetmap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "theme",
|
||||
"render": {
|
||||
"en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
|
||||
"nl": "Wijziging met thema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
|
||||
"de": "Änderung mit Thema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>"
|
||||
"en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "theme~http.*",
|
||||
"then": {
|
||||
"en": "Change with <b>unofficial</b> theme <a href='https://mapcomplete.osm.be/theme.html?userlayout={theme}'>{theme}</a>",
|
||||
"nl": "Wijziging met <b>officieus</b> thema <a href='https://mapcomplete.osm.be/theme.html?userlayout={theme}'>{theme}</a>",
|
||||
"de": "Änderung mit <b>inoffiziellem</b> Thema <a href='https://mapcomplete.osm.be/theme.html?userlayout={theme}'>{theme}</a>"
|
||||
"en": "Change with <b>unofficial</b> theme <a href='https://mapcomplete.osm.be/theme.html?userlayout={theme}'>{theme}</a>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -379,9 +360,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "Themename contains {search}",
|
||||
"nl": "Themanaam bevat {search}",
|
||||
"de": "Themenname enthält {search}"
|
||||
"en": "Themename contains {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -397,9 +376,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "Made by contributor {search}",
|
||||
"nl": "Gemaakt door bijdrager {search}",
|
||||
"de": "Erstellt von {search}"
|
||||
"en": "Made by contributor {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -415,9 +392,7 @@
|
|||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "<b>Not</b> made by contributor {search}",
|
||||
"nl": "<b>Niet</b> gemaakt door bijdrager {search}",
|
||||
"de": "<b>Nicht</b> erstellt von {search}"
|
||||
"en": "<b>Not</b> made by contributor {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -432,9 +407,7 @@
|
|||
{
|
||||
"id": "link_to_more",
|
||||
"render": {
|
||||
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>",
|
||||
"nl": "Meer statistieken kunnen <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>hier</a> gevonden worden",
|
||||
"de": "Weitere Statistiken finden Sie <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>hier</a>"
|
||||
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -843,6 +843,11 @@ video {
|
|||
margin: 1px;
|
||||
}
|
||||
|
||||
.mx-2 {
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.my-2 {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
@ -866,6 +871,14 @@ video {
|
|||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.ml-0 {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
@ -890,10 +903,6 @@ video {
|
|||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
@ -1046,6 +1055,10 @@ video {
|
|||
height: 2rem;
|
||||
}
|
||||
|
||||
.h-64 {
|
||||
height: 16rem;
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -1090,10 +1103,6 @@ video {
|
|||
height: 0px;
|
||||
}
|
||||
|
||||
.h-64 {
|
||||
height: 16rem;
|
||||
}
|
||||
|
||||
.h-3 {
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
@ -1138,6 +1147,10 @@ video {
|
|||
width: 6rem;
|
||||
}
|
||||
|
||||
.w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.w-6 {
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
@ -1171,10 +1184,6 @@ video {
|
|||
width: min-content;
|
||||
}
|
||||
|
||||
.w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.w-max {
|
||||
width: -webkit-max-content;
|
||||
width: max-content;
|
||||
|
@ -1356,6 +1365,10 @@ video {
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.overflow-x-scroll {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -1395,14 +1408,18 @@ video {
|
|||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 0.75rem;
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.rounded-sm {
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
@ -1524,14 +1541,14 @@ video {
|
|||
padding: 1rem;
|
||||
}
|
||||
|
||||
.p-1 {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.p-1 {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.p-0 {
|
||||
padding: 0px;
|
||||
}
|
||||
|
@ -1554,6 +1571,14 @@ video {
|
|||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.pl-1 {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
.pr-1 {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.pb-12 {
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
@ -1578,14 +1603,6 @@ video {
|
|||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.pl-1 {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
.pr-1 {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.pt-2 {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
@ -1693,10 +1710,6 @@ video {
|
|||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
@ -1849,6 +1862,11 @@ video {
|
|||
color: var(--subtle-detail-color-contrast);
|
||||
}
|
||||
|
||||
.bg-unsubtle {
|
||||
background-color: var(--unsubtle-detail-color);
|
||||
color: var(--unsubtle-detail-color-contrast);
|
||||
}
|
||||
|
||||
:root {
|
||||
/* The main colour scheme of mapcomplete is configured here.
|
||||
* For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these.
|
||||
|
@ -2477,7 +2495,7 @@ input {
|
|||
|
||||
.mapping-icon-small-height {
|
||||
/* A mapping icon type */
|
||||
height: 2rem;
|
||||
height: 1.5rem;
|
||||
margin-right: 0.5rem;
|
||||
width: unset;
|
||||
}
|
||||
|
|
|
@ -6493,4 +6493,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue