forked from MapComplete/MapComplete
More layout tweaks and fixes
This commit is contained in:
parent
b678efffd1
commit
2019e6c34c
11 changed files with 161 additions and 99 deletions
|
@ -99,7 +99,8 @@
|
||||||
"key": "ref:velopark",
|
"key": "ref:velopark",
|
||||||
"useProxy": "no",
|
"useProxy": "no",
|
||||||
"host": "https://data.velopark.be",
|
"host": "https://data.velopark.be",
|
||||||
"mode": "readonly"
|
"mode": "readonly",
|
||||||
|
"collapsed": "no"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -485,7 +486,7 @@
|
||||||
"path": "./assets/themes/velopark/velopark.svg",
|
"path": "./assets/themes/velopark/velopark.svg",
|
||||||
"class": "medium"
|
"class": "medium"
|
||||||
},
|
},
|
||||||
"classes": "flex w-full ",
|
"classes": "flex flex-col m-2",
|
||||||
"render": {
|
"render": {
|
||||||
"special": {
|
"special": {
|
||||||
"type": "link",
|
"type": "link",
|
||||||
|
@ -511,7 +512,8 @@
|
||||||
"type": "linked_data_from_website",
|
"type": "linked_data_from_website",
|
||||||
"key": "ref:velopark",
|
"key": "ref:velopark",
|
||||||
"useProxy": "no",
|
"useProxy": "no",
|
||||||
"host": "https://data.velopark.be"
|
"host": "https://data.velopark.be",
|
||||||
|
"collapsed": "no"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,7 @@
|
||||||
"currentInOsmIs": "At the moment, OpenStreetMap has the following value recorded:",
|
"currentInOsmIs": "At the moment, OpenStreetMap has the following value recorded:",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
"error": "Could not load linked data from the website",
|
"error": "Could not load linked data from the website",
|
||||||
|
"lastModified": "External data has been last modified on {date}",
|
||||||
"loadedFrom": "The following data is loaded from <a href={url}>{source}</a> using the embedded JSON-LD",
|
"loadedFrom": "The following data is loaded from <a href={url}>{source}</a> using the embedded JSON-LD",
|
||||||
"missing": {
|
"missing": {
|
||||||
"intro": "OpenStreetMap has no information about the following attributes",
|
"intro": "OpenStreetMap has no information about the following attributes",
|
||||||
|
|
|
@ -515,7 +515,6 @@ class LayerOverviewUtils extends Script {
|
||||||
// At the same time, an index of available layers is built.
|
// At the same time, an index of available layers is built.
|
||||||
console.log("------------- VALIDATING THE BUILTIN QUESTIONS ---------------")
|
console.log("------------- VALIDATING THE BUILTIN QUESTIONS ---------------")
|
||||||
const sharedTagRenderings = this.getSharedTagRenderings(doesImageExist)
|
const sharedTagRenderings = this.getSharedTagRenderings(doesImageExist)
|
||||||
console.log("Shared questions are:", Array.from(sharedTagRenderings.keys()).join(", "))
|
|
||||||
console.log(" ---------- VALIDATING BUILTIN LAYERS ---------")
|
console.log(" ---------- VALIDATING BUILTIN LAYERS ---------")
|
||||||
const state: DesugaringContext = {
|
const state: DesugaringContext = {
|
||||||
tagRenderings: LayerOverviewUtils.asDict(sharedTagRenderings),
|
tagRenderings: LayerOverviewUtils.asDict(sharedTagRenderings),
|
||||||
|
|
|
@ -419,7 +419,7 @@ export default class MetaTagging {
|
||||||
"Static MetataggingObject for theme is not set; using `new Function` (aka `eval`) to get calculated tags. This might trip up the CSP"
|
"Static MetataggingObject for theme is not set; using `new Function` (aka `eval`) to get calculated tags. This might trip up the CSP"
|
||||||
)
|
)
|
||||||
|
|
||||||
const calculatedTags: [string, string, boolean][] = layer.calculatedTags
|
const calculatedTags: [string, string, boolean][] = layer?.calculatedTags ?? []
|
||||||
if (calculatedTags === undefined || calculatedTags.length === 0) {
|
if (calculatedTags === undefined || calculatedTags.length === 0) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
|
@ -748,7 +748,11 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static escapeStr(v: string): string {
|
private static escapeStr(v: string, context: ConversionContext): string {
|
||||||
|
if(typeof v !== "string"){
|
||||||
|
context.err("Detected a non-string value where one expected a string: "+v)
|
||||||
|
return RewriteSpecial.escapeStr(""+v, context)
|
||||||
|
}
|
||||||
return v
|
return v
|
||||||
.replace(/,/g, "&COMMA")
|
.replace(/,/g, "&COMMA")
|
||||||
.replace(/\{/g, "&LBRACE")
|
.replace(/\{/g, "&LBRACE")
|
||||||
|
@ -930,7 +934,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
||||||
|
|
||||||
if (foundLanguages.size === 0) {
|
if (foundLanguages.size === 0) {
|
||||||
const args = argNamesList
|
const args = argNamesList
|
||||||
.map((nm) => RewriteSpecial.escapeStr(special[nm] ?? ""))
|
.map((nm) => RewriteSpecial.escapeStr(special[nm] ?? "", context))
|
||||||
.join(",")
|
.join(",")
|
||||||
return {
|
return {
|
||||||
"*": `{${type}(${args})${clss}}`
|
"*": `{${type}(${args})${clss}}`
|
||||||
|
@ -949,7 +953,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof v === "string") {
|
if (typeof v === "string") {
|
||||||
args.push(RewriteSpecial.escapeStr(v))
|
args.push(RewriteSpecial.escapeStr(v, context))
|
||||||
} else if (typeof v === "object") {
|
} else if (typeof v === "object") {
|
||||||
args.push(JSON.stringify(v))
|
args.push(JSON.stringify(v))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="interactive flex w-full justify-between py-1 px-2">
|
<div class:interactive={!readonly} class="flex w-full justify-between py-1 px-2">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
{#if renderingExternal}
|
{#if renderingExternal}
|
||||||
|
|
80
src/UI/Comparison/ComparisonState.ts
Normal file
80
src/UI/Comparison/ComparisonState.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
|
import { OsmTags } from "../../Models/OsmFeature"
|
||||||
|
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
|
|
||||||
|
export class ComparisonState {
|
||||||
|
public readonly hasDifferencesAtStart: boolean
|
||||||
|
public readonly different: Store<string[]>
|
||||||
|
public readonly missing: Store<string[]>
|
||||||
|
public readonly unknownImages: Store<string[]>
|
||||||
|
public readonly propertyKeysExternal: string[]
|
||||||
|
public readonly knownImages: Store<Set<string>>
|
||||||
|
|
||||||
|
constructor(tags: UIEventSource<OsmTags>, externalProperties: Record<string, string>) {
|
||||||
|
|
||||||
|
externalProperties = { ...externalProperties }
|
||||||
|
delete externalProperties["@context"]
|
||||||
|
|
||||||
|
let externalKeys: string[] = Object.keys(externalProperties).sort()
|
||||||
|
|
||||||
|
const imageKeyRegex = /image|image:[0-9]+/
|
||||||
|
|
||||||
|
this.knownImages = tags.map(
|
||||||
|
(osmProperties) =>
|
||||||
|
new Set(
|
||||||
|
Object.keys(osmProperties)
|
||||||
|
.filter((k) => k.match(imageKeyRegex))
|
||||||
|
.map((k) => osmProperties[k])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
this.unknownImages = this.knownImages.map((images) =>
|
||||||
|
externalKeys
|
||||||
|
.filter((k) => k.match(imageKeyRegex))
|
||||||
|
.map((k) => externalProperties[k])
|
||||||
|
.filter((i) => !images.has(i))
|
||||||
|
)
|
||||||
|
|
||||||
|
this.propertyKeysExternal = externalKeys.filter((k) => k.match(imageKeyRegex) === null)
|
||||||
|
let propertyKeysExternal = this.propertyKeysExternal
|
||||||
|
this.missing = tags.map((osmProperties) =>
|
||||||
|
propertyKeysExternal.filter((k) => {
|
||||||
|
if (k.startsWith("_")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return osmProperties[k] === undefined && typeof externalProperties[k] === "string"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// let same = propertyKeysExternal.filter((key) => osmProperties[key] === externalProperties[key])
|
||||||
|
this.different = tags.map((osmProperties) =>
|
||||||
|
propertyKeysExternal.filter((key) => {
|
||||||
|
if (key.startsWith("_")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (osmProperties[key] === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof externalProperties[key] !== "string") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (osmProperties[key] === externalProperties[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "website") {
|
||||||
|
const osmCanon = new URL(osmProperties[key]).toString()
|
||||||
|
const externalCanon = new URL(externalProperties[key]).toString()
|
||||||
|
if (osmCanon === externalCanon) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
this.hasDifferencesAtStart =
|
||||||
|
this. different.data.length + this.missing.data.length + this.unknownImages.data.length > 0
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LinkableImage from "../Image/LinkableImage.svelte"
|
import LinkableImage from "../Image/LinkableImage.svelte"
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import type { OsmTags } from "../../Models/OsmFeature"
|
import type { OsmTags } from "../../Models/OsmFeature"
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import type { Feature } from "geojson"
|
import type { Feature } from "geojson"
|
||||||
|
@ -14,10 +14,10 @@
|
||||||
import AttributedImage from "../Image/AttributedImage.svelte"
|
import AttributedImage from "../Image/AttributedImage.svelte"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
|
import { ComparisonState } from "./ComparisonState"
|
||||||
|
|
||||||
export let externalProperties: Record<string, string>
|
export let externalProperties: Record<string, string>
|
||||||
delete externalProperties["@context"]
|
delete externalProperties["@context"]
|
||||||
console.log("External properties are", externalProperties)
|
|
||||||
export let sourceUrl: string
|
export let sourceUrl: string
|
||||||
|
|
||||||
export let tags: UIEventSource<OsmTags>
|
export let tags: UIEventSource<OsmTags>
|
||||||
|
@ -27,66 +27,14 @@
|
||||||
|
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
|
|
||||||
|
export let comparisonState : ComparisonState
|
||||||
|
let missing = comparisonState.missing
|
||||||
|
let unknownImages = comparisonState.unknownImages
|
||||||
|
let knownImages = comparisonState.knownImages
|
||||||
|
let different =comparisonState.different
|
||||||
|
|
||||||
const t = Translations.t.external
|
const t = Translations.t.external
|
||||||
|
|
||||||
let externalKeys: string[] = Object.keys(externalProperties).sort()
|
|
||||||
|
|
||||||
const imageKeyRegex = /image|image:[0-9]+/
|
|
||||||
let knownImages: Store<Set<string>> = tags.map(
|
|
||||||
(osmProperties) =>
|
|
||||||
new Set(
|
|
||||||
Object.keys(osmProperties)
|
|
||||||
.filter((k) => k.match(imageKeyRegex))
|
|
||||||
.map((k) => osmProperties[k])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
let unknownImages: Store<string[]> = knownImages.map((images) =>
|
|
||||||
externalKeys
|
|
||||||
.filter((k) => k.match(imageKeyRegex))
|
|
||||||
.map((k) => externalProperties[k])
|
|
||||||
.filter((i) => !images.has(i))
|
|
||||||
)
|
|
||||||
|
|
||||||
let propertyKeysExternal = externalKeys.filter((k) => k.match(imageKeyRegex) === null)
|
|
||||||
let missing: Store<string[]> = tags.map((osmProperties) =>
|
|
||||||
propertyKeysExternal.filter((k) => {
|
|
||||||
if (k.startsWith("_")) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return osmProperties[k] === undefined && typeof externalProperties[k] === "string"
|
|
||||||
})
|
|
||||||
)
|
|
||||||
// let same = propertyKeysExternal.filter((key) => osmProperties[key] === externalProperties[key])
|
|
||||||
let different: Store<string[]> = tags.map((osmProperties) =>
|
|
||||||
propertyKeysExternal.filter((key) => {
|
|
||||||
if (key.startsWith("_")) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (osmProperties[key] === undefined) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (typeof externalProperties[key] !== "string") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (osmProperties[key] === externalProperties[key]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === "website") {
|
|
||||||
const osmCanon = new URL(osmProperties[key]).toString()
|
|
||||||
const externalCanon = new URL(externalProperties[key]).toString()
|
|
||||||
if (osmCanon === externalCanon) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
let hasDifferencesAtStart =
|
|
||||||
different.data.length + missing.data.length + unknownImages.data.length > 0
|
|
||||||
|
|
||||||
let currentStep: "init" | "applying_all" | "all_applied" = "init"
|
let currentStep: "init" | "applying_all" | "all_applied" = "init"
|
||||||
let applyAllHovered = false
|
let applyAllHovered = false
|
||||||
|
|
||||||
|
@ -102,19 +50,13 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0}
|
|
||||||
<Tr cls="subtle" t={t.noDataLoaded} />
|
{#if $unknownImages.length === 0 && $missing.length === 0 && $different.length === 0}
|
||||||
{:else if !hasDifferencesAtStart}
|
|
||||||
<span class="subtle text-sm">
|
|
||||||
<Tr t={t.allIncluded.Subs({ source: sourceUrl })} />
|
|
||||||
</span>
|
|
||||||
{:else if $unknownImages.length === 0 && $missing.length === 0 && $different.length === 0}
|
|
||||||
<div class="thanks m-0 flex items-center gap-x-2 px-2">
|
<div class="thanks m-0 flex items-center gap-x-2 px-2">
|
||||||
<Party class="h-8 w-8 shrink-0" />
|
<Party class="h-8 w-8 shrink-0" />
|
||||||
<Tr t={t.allIncluded.Subs({ source: sourceUrl })} />
|
<Tr t={t.allIncluded.Subs({ source: sourceUrl })} />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="low-interaction p-1">
|
|
||||||
{#if !readonly}
|
{#if !readonly}
|
||||||
<Tr t={t.loadedFrom.Subs({ url: sourceUrl, source: sourceUrl })} />
|
<Tr t={t.loadedFrom.Subs({ url: sourceUrl, source: sourceUrl })} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -218,9 +160,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if externalProperties["_last_edit_timestamp"] !== undefined}
|
{#if externalProperties["_last_edit_timestamp"] !== undefined}
|
||||||
<span class="subtle text-sm">
|
<span class="subtle text-sm flex flex-end justify-end mt-2 mr-4">
|
||||||
External data has been last modified on {externalProperties["_last_edit_timestamp"]}
|
<Tr t={t.lastModified.Subs({date: new Date(externalProperties["_last_edit_timestamp"]).toLocaleString() })}/>
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
||||||
import GlobeAlt from "@babeard/svelte-heroicons/mini/GlobeAlt"
|
import GlobeAlt from "@babeard/svelte-heroicons/mini/GlobeAlt"
|
||||||
|
import { ComparisonState } from "./ComparisonState"
|
||||||
|
|
||||||
export let externalData: Store<
|
export let externalData: Store<
|
||||||
| { success: { content: Record<string, string> } }
|
| { success: { content: Record<string, string> } }
|
||||||
|
@ -28,7 +29,20 @@
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let sourceUrl: Store<string>
|
export let sourceUrl: Store<string>
|
||||||
|
|
||||||
export let collapsed : boolean
|
export let collapsed: boolean
|
||||||
|
const t = Translations.t.external
|
||||||
|
|
||||||
|
let comparisonState: Store<ComparisonState | undefined> = externalData.mapD(external => {
|
||||||
|
if (external["success"]) {
|
||||||
|
return new ComparisonState(tags, external["success"])
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
let unknownImages = comparisonState.bindD(ct => ct.unknownImages)
|
||||||
|
let knownImages = comparisonState.bindD(ct => ct.knownImages)
|
||||||
|
let propertyKeysExternal = comparisonState.mapD(ct => ct.propertyKeysExternal)
|
||||||
|
let hasDifferencesAtStart = comparisonState.mapD(ct => ct.hasDifferencesAtStart)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !$sourceUrl}
|
{#if !$sourceUrl}
|
||||||
|
@ -39,20 +53,27 @@
|
||||||
<div class="subtle italic low-interaction p-2 px-4 rounded">
|
<div class="subtle italic low-interaction p-2 px-4 rounded">
|
||||||
<Tr t={Translations.t.external.error} />
|
<Tr t={Translations.t.external.error} />
|
||||||
</div>
|
</div>
|
||||||
{:else if $externalData["success"] !== undefined}
|
{:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0}
|
||||||
<AccordionSingle>
|
<Tr cls="subtle" t={t.noDataLoaded} />
|
||||||
|
{:else if !$hasDifferencesAtStart}
|
||||||
|
<span class="subtle text-sm">
|
||||||
|
<Tr t={t.allIncluded.Subs({ source: $sourceUrl })} />
|
||||||
|
</span>
|
||||||
|
{:else if $comparisonState !== undefined}
|
||||||
|
<AccordionSingle expanded={!collapsed}>
|
||||||
<span slot="header" class="flex">
|
<span slot="header" class="flex">
|
||||||
<GlobeAlt class="w-6 h-6"/>
|
<GlobeAlt class="w-6 h-6" />
|
||||||
<Tr t={Translations.t.external.title}/>
|
<Tr t={Translations.t.external.title} />
|
||||||
</span>
|
</span>
|
||||||
<ComparisonTable
|
<ComparisonTable
|
||||||
externalProperties={$externalData["success"]}
|
externalProperties={$externalData["success"]}
|
||||||
{state}
|
{state}
|
||||||
{feature}
|
{feature}
|
||||||
{layer}
|
{layer}
|
||||||
{tags}
|
{tags}
|
||||||
{readonly}
|
{readonly}
|
||||||
sourceUrl={$sourceUrl}
|
sourceUrl={$sourceUrl}
|
||||||
/>
|
comparisonState={$comparisonState}
|
||||||
|
/>
|
||||||
</AccordionSingle>
|
</AccordionSingle>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -97,6 +97,15 @@ class SingleBackgroundHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private tryEnableSafe(): boolean{
|
||||||
|
try {
|
||||||
|
return this.tryEnable()
|
||||||
|
}catch (e) {
|
||||||
|
console.log("Error: could not enable due to error", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns 'false' if should be attempted again
|
* Returns 'false' if should be attempted again
|
||||||
* @private
|
* @private
|
||||||
|
|
|
@ -134,11 +134,16 @@ export class MoveWizardState {
|
||||||
// This is a new point. Check if it was snapped to an existing way due to the '_referencing_ways'-tag
|
// This is a new point. Check if it was snapped to an existing way due to the '_referencing_ways'-tag
|
||||||
const store = this._state.featureProperties.getStore(id)
|
const store = this._state.featureProperties.getStore(id)
|
||||||
store?.addCallbackAndRunD((tags) => {
|
store?.addCallbackAndRunD((tags) => {
|
||||||
if (tags._referencing_ways !== undefined && tags._referencing_ways !== "[]") {
|
try{
|
||||||
console.log("Got referencing ways according to the tags")
|
|
||||||
this.moveDisallowedReason.setData(t.partOfAWay)
|
if (tags._referencing_ways !== undefined && tags._referencing_ways !== "[]") {
|
||||||
return true
|
console.log("Got referencing ways according to the tags")
|
||||||
}
|
this.moveDisallowedReason.setData(t.partOfAWay)
|
||||||
|
return true // unregister
|
||||||
|
}
|
||||||
|
}catch (e) {
|
||||||
|
console.error("Could not get '_referencing_ways'-attribute of tags due to", e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue