Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-08-29 23:21:17 +02:00
commit d1e7eba2db
19 changed files with 554 additions and 448 deletions

View file

@ -175,13 +175,23 @@
"cs": "Jaké je telefonní číslo {title()}?" "cs": "Jaké je telefonní číslo {title()}?"
}, },
"render": { "render": {
"*": "<a href='tel:{phone}'>{phone}</a>" "special": {
"type": "link",
"href": "tel:{phone}",
"text": "{phone}"
}
}, },
"icon": "./assets/layers/questions/phone.svg", "icon": "./assets/layers/questions/phone.svg",
"mappings": [ "mappings": [
{ {
"if": "contact:phone~*", "if": "contact:phone~*",
"then": "<a href='tel:{contact:phone}'>{contact:phone}</a>", "then": {
"special": {
"type": "link",
"href": "tel:{contact:phone}",
"text": "{contact:phone}"
}
},
"hideInAnswer": true, "hideInAnswer": true,
"icon": "./assets/layers/questions/phone.svg" "icon": "./assets/layers/questions/phone.svg"
} }

View file

@ -231,6 +231,7 @@
}, },
{ {
"if": "access=customers", "if": "access=customers",
"icon": "key",
"then": { "then": {
"en": "Only access to customers", "en": "Only access to customers",
"de": "Der Zugang ist nur für Kunden", "de": "Der Zugang ist nur für Kunden",
@ -245,6 +246,7 @@
}, },
{ {
"if": "access=no", "if": "access=no",
"icon": "lock",
"alsoShowIf": "access=private", "alsoShowIf": "access=private",
"then": { "then": {
"en": "Not accessible", "en": "Not accessible",
@ -261,6 +263,7 @@
}, },
{ {
"if": "access=key", "if": "access=key",
"icon": "key",
"then": { "then": {
"en": "Accessible, but one has to ask a key to enter", "en": "Accessible, but one has to ask a key to enter",
"de": "Der Zugang ist möglich, aber man muss nach einen Schlüssel fragen", "de": "Der Zugang ist möglich, aber man muss nach einen Schlüssel fragen",

View file

@ -91,7 +91,7 @@
}, },
"freeform": { "freeform": {
"key": "inscription", "key": "inscription",
"type": "string", "type": "text",
"placeholder": { "placeholder": {
"en": "Text on the sign", "en": "Text on the sign",
"de": "Text auf dem Schild", "de": "Text auf dem Schild",

View file

@ -193,6 +193,7 @@
}, },
"josmNotOpened": "JOSM could not be reached. Make sure it is opened and remote control is enabled", "josmNotOpened": "JOSM could not be reached. Make sure it is opened and remote control is enabled",
"josmOpened": "JOSM is opened", "josmOpened": "JOSM is opened",
"madeBy": "Mady by <b>{author}</b>",
"mapContributionsBy": "The current visible data has edits made by {contributors}", "mapContributionsBy": "The current visible data has edits made by {contributors}",
"mapContributionsByAndHidden": "The current visible data has edits made by {contributors} and {hiddenCount} more contributors", "mapContributionsByAndHidden": "The current visible data has edits made by {contributors} and {hiddenCount} more contributors",
"mapDataByOsm": "Map data: OpenStreetMap", "mapDataByOsm": "Map data: OpenStreetMap",

View file

@ -2077,6 +2077,12 @@ video {
margin-bottom: calc(0px * var(--tw-space-y-reverse)); margin-bottom: calc(0px * var(--tw-space-y-reverse));
} }
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-2 > :not([hidden]) ~ :not([hidden]) { .space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0; --tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse)); margin-right: calc(0.5rem * var(--tw-space-x-reverse));
@ -2101,12 +2107,6 @@ video {
margin-left: calc(-1px * calc(1 - var(--tw-space-x-reverse))); margin-left: calc(-1px * calc(1 - var(--tw-space-x-reverse)));
} }
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-y-1 > :not([hidden]) ~ :not([hidden]) { .space-y-1 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0; --tw-space-y-reverse: 0;
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
@ -2435,6 +2435,10 @@ video {
border-top-right-radius: 0.25rem; border-top-right-radius: 0.25rem;
} }
.rounded-bl-full {
border-bottom-left-radius: 9999px;
}
.rounded-tl { .rounded-tl {
border-top-left-radius: 0.25rem; border-top-left-radius: 0.25rem;
} }
@ -3458,6 +3462,14 @@ video {
padding-top: 0px; padding-top: 0px;
} }
.pl-3 {
padding-left: 0.75rem;
}
.pb-3 {
padding-bottom: 0.75rem;
}
.pl-4 { .pl-4 {
padding-left: 1rem; padding-left: 1rem;
} }
@ -3466,14 +3478,6 @@ video {
padding-right: 1rem; padding-right: 1rem;
} }
.pl-3 {
padding-left: 0.75rem;
}
.pr-3 {
padding-right: 0.75rem;
}
.pl-1 { .pl-1 {
padding-left: 0.25rem; padding-left: 0.25rem;
} }
@ -4908,6 +4912,10 @@ a:hover {
background-color: #f2f2f2; background-color: #f2f2f2;
} }
.no-bold b {
font-weight: normal;
}
/************************* MISC ELEMENTS *************************/ /************************* MISC ELEMENTS *************************/
.selected svg:not(.noselect *) path.selectable { .selected svg:not(.noselect *) path.selectable {

View file

@ -6,6 +6,7 @@ import Script from "./Script"
const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"] const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"]
const ignoreTerms = ["searchTerms"] const ignoreTerms = ["searchTerms"]
class TranslationPart { class TranslationPart {
contents: Map<string, TranslationPart | string> = new Map<string, TranslationPart | string>() contents: Map<string, TranslationPart | string> = new Map<string, TranslationPart | string>()
@ -14,7 +15,8 @@ class TranslationPart {
const rootTranslation = new TranslationPart() const rootTranslation = new TranslationPart()
for (const file of files) { for (const file of files) {
const content = JSON.parse(readFileSync(file, { encoding: "utf8" })) const content = JSON.parse(readFileSync(file, { encoding: "utf8" }))
rootTranslation.addTranslation(file.substr(0, file.length - ".json".length), content) const language = file.substr(0, file.length - ".json".length)
rootTranslation.addTranslation(language, content)
} }
return rootTranslation return rootTranslation
} }
@ -46,18 +48,14 @@ class TranslationPart {
return return
} }
for (const translationsKey in translations) { for (const translationsKey in translations) {
if (!translations.hasOwnProperty(translationsKey)) {
continue
}
const v = translations[translationsKey] const v = translations[translationsKey]
if (typeof v != "string") { if (typeof v != "string") {
console.error( console.error(
`Non-string object at ${context} in translation while trying to add the translation ` + `Non-string object at ${context} in translation while trying to add the translation ` +
JSON.stringify(v) + JSON.stringify(v) +
` to '` + ` to '` +
translationsKey + translationsKey +
"'. The offending object which _should_ be a translation is: ", "'. The offending object which _should_ be a translation is: ",
v, v,
"\n\nThe current object is (only showing en):", "\n\nThe current object is (only showing en):",
this.toJson(), this.toJson(),
@ -96,17 +94,14 @@ class TranslationPart {
if (noTranslate !== undefined) { if (noTranslate !== undefined) {
console.log( console.log(
"Ignoring some translations for " + "Ignoring some translations for " +
context + context +
": " + ": " +
dontTranslateKeys.join(", ") dontTranslateKeys.join(", ")
) )
} }
} }
for (let key in object) { for (let key in object) {
if (!object.hasOwnProperty(key)) {
continue
}
if (ignoreTerms.indexOf(key) >= 0) { if (ignoreTerms.indexOf(key) >= 0) {
continue continue
} }
@ -155,13 +150,13 @@ class TranslationPart {
this.contents.set(key, new TranslationPart()) this.contents.set(key, new TranslationPart())
} }
;(this.contents.get(key) as TranslationPart).recursiveAdd(v, context + "." + key) (this.contents.get(key) as TranslationPart).recursiveAdd(v, context + "." + key)
} }
} }
knownLanguages(): string[] { knownLanguages(): string[] {
const languages = [] const languages = []
for (let key of Array.from(this.contents.keys())) { for (const key of Array.from(this.contents.keys())) {
const value = this.contents.get(key) const value = this.contents.get(key)
if (typeof value === "string") { if (typeof value === "string") {
@ -180,20 +175,20 @@ class TranslationPart {
const parts = [] const parts = []
let keys = Array.from(this.contents.keys()) let keys = Array.from(this.contents.keys())
keys = keys.sort() keys = keys.sort()
for (let key of keys) { for (const key of keys) {
let value = this.contents.get(key) let value = this.contents.get(key)
if (typeof value === "string") { if (typeof value === "string") {
value = value.replace(/"/g, '\\"').replace(/\n/g, "\\n") value = value.replace(/"/g, "\\\"").replace(/\n/g, "\\n")
if (neededLanguage === undefined) { if (neededLanguage === undefined) {
parts.push(`\"${key}\": \"${value}\"`) parts.push(`"${key}": "${value}"`)
} else if (key === neededLanguage) { } else if (key === neededLanguage) {
return `"${value}"` return `"${value}"`
} }
} else { } else {
const sub = (value as TranslationPart).toJson(neededLanguage) const sub = (value as TranslationPart).toJson(neededLanguage)
if (sub !== "") { if (sub !== "") {
parts.push(`\"${key}\": ${sub}`) parts.push(`"${key}": ${sub}`)
} }
} }
} }
@ -234,7 +229,7 @@ class TranslationPart {
} else if (!isLeaf) { } else if (!isLeaf) {
errors.push({ errors.push({
error: "Mixed node: non-leaf node has translation strings", error: "Mixed node: non-leaf node has translation strings",
path: path, path: path
}) })
} }
@ -285,7 +280,7 @@ class TranslationPart {
value + value +
"\n" + "\n" +
fixLink, fixLink,
path: path, path: path
}) })
} }
return return
@ -297,7 +292,7 @@ class TranslationPart {
error: `The translation for ${key} does not have the required subpart ${part} (in ${usedByLanguage}). error: `The translation for ${key} does not have the required subpart ${part} (in ${usedByLanguage}).
\tThe full translation is ${value} \tThe full translation is ${value}
\t${fixLink}`, \t${fixLink}`,
path: path, path: path
}) })
} }
} }
@ -334,24 +329,6 @@ class TranslationPart {
} }
} }
/**
* Checks that the given object only contains string-values
* @param tr
*/
function isTranslation(tr: any): boolean {
if (tr["#"] === "no-translations") {
return false
}
if (tr["special"]) {
return false
}
for (const key in tr) {
if (typeof tr[key] !== "string") {
return false
}
}
return true
}
/** /**
* Converts a translation object into something that can be added to the 'generated translations'. * Converts a translation object into something that can be added to the 'generated translations'.
@ -361,9 +338,10 @@ function isTranslation(tr: any): boolean {
function transformTranslation( function transformTranslation(
obj: any, obj: any,
path: string[] = [], path: string[] = [],
languageWhitelist: string[] = undefined languageWhitelist: string[] = undefined,
shortNotation = false
) { ) {
if (isTranslation(obj)) { if (GenerateTranslations.isTranslation(obj)) {
return `new Translation( ${JSON.stringify(obj)} )` return `new Translation( ${JSON.stringify(obj)} )`
} }
@ -380,7 +358,7 @@ function transformTranslation(
} }
let value = obj[key] let value = obj[key]
if (isTranslation(value)) { if (GenerateTranslations.isTranslation(value)) {
if (languageWhitelist !== undefined) { if (languageWhitelist !== undefined) {
const nv = {} const nv = {}
for (const ln of languageWhitelist) { for (const ln of languageWhitelist) {
@ -395,7 +373,7 @@ function transformTranslation(
)}.${key}\n\tThe translations in other languages are ${JSON.stringify(value)}` )}.${key}\n\tThe translations in other languages are ${JSON.stringify(value)}`
} }
const subParts: string[] = value["en"].match(/{[^}]*}/g) const subParts: string[] = value["en"].match(/{[^}]*}/g)
let expr = `return new Translation(${JSON.stringify(value)}, "core:${path.join( let expr = `new Translation(${JSON.stringify(value)}, "core:${path.join(
"." "."
)}.${key}")` )}.${key}")`
if (subParts !== null) { if (subParts !== null) {
@ -409,12 +387,16 @@ function transformTranslation(
"." "."
)}: A subpart contains invalid characters: ${subParts.join(", ")}` )}: A subpart contains invalid characters: ${subParts.join(", ")}`
} }
expr = `return new TypedTranslation<{ ${types.join(", ")} }>(${JSON.stringify( expr = `new TypedTranslation<{ ${types.join(", ")} }>(${JSON.stringify(
value value
)}, "core:${path.join(".")}.${key}")` )}, "core:${path.join(".")}.${key}")`
} }
if (shortNotation) {
values.push(`${spaces} ${key}: ${expr}`)
values.push(`${spaces}get ${key}() { ${expr} }`) } else {
values.push(`${spaces}get ${key}() { return ${expr} }`)
}
} else { } else {
values.push( values.push(
spaces + key + ": " + transformTranslation(value, [...path, key], languageWhitelist) spaces + key + ": " + transformTranslation(value, [...path, key], languageWhitelist)
@ -469,54 +451,11 @@ function formatFile(path) {
writeFileSync(path, JSON.stringify(contents, null, " ") + (endsWithNewline ? "\n" : "")) writeFileSync(path, JSON.stringify(contents, null, " ") + (endsWithNewline ? "\n" : ""))
} }
/**
* Generates the big compiledTranslations file
*/
function genTranslations() {
if (!fs.existsSync("./src/assets/generated/")) {
fs.mkdirSync("./src/assets/generated/")
}
const translations = JSON.parse(
fs.readFileSync("./src/assets/generated/translations.json", "utf-8")
)
const transformed = transformTranslation(translations)
let module = `import {Translation, TypedTranslation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n`
module += " public static t = " + transformed
module += "\n }"
fs.writeFileSync("./src/assets/generated/CompiledTranslations.ts", module)
}
/** /**
* Reads 'lang/*.json', writes them into to 'assets/generated/translations.json'. * Reads 'lang/*.json', writes them into to 'assets/generated/translations.json'.
* This is only for the core translations * This is only for the core translations
*/ */
function compileTranslationsFromWeblate() {
const translations = ScriptUtils.readDirRecSync("./langs", 1).filter(
(path) => path.indexOf(".json") > 0
)
const allTranslations = new TranslationPart()
allTranslations.validateStrict()
for (const translationFile of translations) {
try {
const contents = JSON.parse(readFileSync(translationFile, "utf-8"))
let language = translationFile.substring(translationFile.lastIndexOf("/") + 1)
language = language.substring(0, language.length - 5)
allTranslations.add(language, contents)
} catch (e) {
throw "Could not read file " + translationFile + " due to " + e
}
}
writeFileSync(
"./src/assets/generated/translations.json",
JSON.stringify(JSON.parse(allTranslations.toJson()), null, " ")
)
}
/** /**
* Get all the strings out of the layers; writes them onto the weblate paths * Get all the strings out of the layers; writes them onto the weblate paths
@ -608,7 +547,7 @@ function MergeTranslation(source: any, target: any, language: string, context: s
if (targetV[language] !== undefined && targetV[language] !== sourceV) { if (targetV[language] !== undefined && targetV[language] !== sourceV) {
was = " (overwritten " + targetV[language] + ")" was = " (overwritten " + targetV[language] + ")"
} }
console.log(" + ", context + "." + language, "-->", sourceV, was) // console.log(" + ", context + "." + language, "-->", sourceV, was)
continue continue
} }
if (typeof sourceV === "object") { if (typeof sourceV === "object") {
@ -697,7 +636,7 @@ function removeNonEnglishTranslations(object: any) {
leaf["en"] = en leaf["en"] = en
}, },
(possibleLeaf) => (possibleLeaf) =>
possibleLeaf !== null && typeof possibleLeaf === "object" && isTranslation(possibleLeaf) possibleLeaf !== null && typeof possibleLeaf === "object" && GenerateTranslations.isTranslation(possibleLeaf)
) )
} }
@ -732,6 +671,25 @@ class GenerateTranslations extends Script {
super("Syncs translations from/to the theme and layer files") super("Syncs translations from/to the theme and layer files")
} }
/**
* Checks that the given object only contains string-values
* @param tr
*/
static isTranslation(tr: Record<string, string | object>): boolean {
if (tr["#"] === "no-translations") {
return false
}
if (tr["special"]) {
return false
}
for (const key in tr) {
if (typeof tr[key] !== "string") {
return false
}
}
return true
}
/** /**
* OUtputs the 'used_languages.json'-file * OUtputs the 'used_languages.json'-file
*/ */
@ -754,22 +712,74 @@ class GenerateTranslations extends Script {
} }
} }
/**
* Generates the big compiledTranslations file based on 'translations.json'
*/
genTranslations(englishOnly?: boolean) {
if (!fs.existsSync("./src/assets/generated/")) {
fs.mkdirSync("./src/assets/generated/")
}
const translations = JSON.parse(
fs.readFileSync("./src/assets/generated/translations.json", "utf-8")
)
const transformed = transformTranslation(translations, undefined, englishOnly ? ["en"] : undefined, englishOnly)
let module = `import {Translation, TypedTranslation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n`
module += " public static t = " + transformed
module += "\n }"
fs.writeFileSync("./src/assets/generated/CompiledTranslations.ts", module)
}
compileTranslationsFromWeblate(englishOnly: boolean) {
const translations = ScriptUtils.readDirRecSync("./langs", 1).filter(
(path) => path.indexOf(".json") > 0
)
const allTranslations = new TranslationPart()
allTranslations.validateStrict()
for (const translationFile of translations) {
try {
const contents = JSON.parse(readFileSync(translationFile, "utf-8"))
let language = translationFile.substring(translationFile.lastIndexOf("/") + 1)
language = language.substring(0, language.length - 5)
if (englishOnly && language !== "en") {
continue
}
allTranslations.add(language, contents)
} catch (e) {
throw "Could not read file " + translationFile + " due to " + e
}
}
writeFileSync(
"./src/assets/generated/translations.json",
JSON.stringify(JSON.parse(allTranslations.toJson()), null, " ")
)
}
async main(args: string[]): Promise<void> { async main(args: string[]): Promise<void> {
if (!existsSync("./langs/themes")) { if (!existsSync("./langs/themes")) {
mkdirSync("./langs/themes") mkdirSync("./langs/themes")
} }
const themeOverwritesWeblate = args[0] === "--ignore-weblate" const themeOverwritesWeblate = args[0] === "--ignore-weblate"
const englishOnly = args[0] === "--english-only" const englishOnly = args[0] === "--english-only"
if (englishOnly) {
console.log("ENGLISH ONLY")
}
if (!themeOverwritesWeblate) { if (!themeOverwritesWeblate) {
mergeLayerTranslations() mergeLayerTranslations(englishOnly)
mergeThemeTranslations() mergeThemeTranslations(englishOnly)
compileTranslationsFromWeblate() this.compileTranslationsFromWeblate(englishOnly)
} else { } else {
console.log("Ignore weblate") console.log("Ignore weblate")
} }
this.detectUsedLanguages() this.detectUsedLanguages()
genTranslations() this.genTranslations(englishOnly)
{ {
const allTranslationFiles = ScriptUtils.readDirRecSync("langs").filter((path) => const allTranslationFiles = ScriptUtils.readDirRecSync("langs").filter((path) =>
path.endsWith(".json") path.endsWith(".json")

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { ImmutableStore, Store } from "../../Logic/UIEventSource" import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import Icon from "../Map/Icon.svelte" import Icon from "../Map/Icon.svelte"
import { Utils } from "../../Utils"
export let text: Store<string> export let text: Store<string>
export let href: Store<string> export let href: Store<string>
@ -13,7 +14,7 @@
</script> </script>
<a <a
href={$href} href={Utils.prepareHref($href)}
aria-label={$ariaLabel} aria-label={$ariaLabel}
title={$ariaLabel} title={$ariaLabel}
target={$newTab ? "_blank" : undefined} target={$newTab ? "_blank" : undefined}

View file

@ -1,6 +1,10 @@
<script lang="ts"> <script lang="ts">
import { Utils } from "../../Utils"
export let text: string export let text: string
export let href: string export let href: string
export let classnames: string = undefined export let classnames: string = undefined
export let download: string = undefined export let download: string = undefined
export let ariaLabel: string = undefined export let ariaLabel: string = undefined
@ -9,7 +13,7 @@
</script> </script>
<a <a
{href} href={Utils.prepareHref(href)}
aria-label={ariaLabel} aria-label={ariaLabel}
title={ariaLabel} title={ariaLabel}
target={newTab ? "_blank" : undefined} target={newTab ? "_blank" : undefined}

View file

@ -15,6 +15,7 @@
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" import { ComparisonState } from "./ComparisonState"
import LoginToggle from "../Base/LoginToggle.svelte"
export let externalData: Store< export let externalData: Store<
| { success: { content: Record<string, string> } } | { success: { content: Record<string, string> } }
@ -45,35 +46,38 @@
let enableLogin = state.featureSwitches.featureSwitchEnableLogin let enableLogin = state.featureSwitches.featureSwitchEnableLogin
</script> </script>
{#if !$sourceUrl || !$enableLogin} <LoginToggle {state} silentFail>
<!-- empty block -->
{:else if $externalData === undefined} {#if !$sourceUrl || !$enableLogin}
<Loading /> <!-- empty block -->
{:else if $externalData["error"] !== undefined} {:else if $externalData === undefined}
<div class="subtle low-interaction rounded p-2 px-4 italic"> <Loading />
<Tr t={Translations.t.external.error} /> {:else if $externalData["error"] !== undefined}
</div> <div class="subtle low-interaction rounded p-2 px-4 italic">
{:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0} <Tr t={Translations.t.external.error} />
<Tr cls="subtle" t={t.noDataLoaded} /> </div>
{:else if !$hasDifferencesAtStart} {:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0}
<Tr cls="subtle" t={t.noDataLoaded} />
{:else if !$hasDifferencesAtStart}
<span class="subtle text-sm"> <span class="subtle text-sm">
<Tr t={t.allIncluded.Subs({ source: $sourceUrl })} /> <Tr t={t.allIncluded.Subs({ source: $sourceUrl })} />
</span> </span>
{:else if $comparisonState !== undefined} {:else if $comparisonState !== undefined}
<AccordionSingle expanded={!collapsed}> <AccordionSingle expanded={!collapsed}>
<span slot="header" class="flex"> <span slot="header" class="flex">
<GlobeAlt class="h-6 w-6" /> <GlobeAlt class="h-6 w-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} comparisonState={$comparisonState}
/> />
</AccordionSingle> </AccordionSingle>
{/if} {/if}
</LoginToggle>

View file

@ -6,6 +6,7 @@
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider" import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
import { Mapillary } from "../../Logic/ImageProviders/Mapillary" import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import { MagnifyingGlassPlusIcon } from "@babeard/svelte-heroicons/outline"
export let image: Partial<ProvidedImage> export let image: Partial<ProvidedImage>
let fallbackImage: string = undefined let fallbackImage: string = undefined
@ -16,25 +17,37 @@
let imgEl: HTMLImageElement let imgEl: HTMLImageElement
export let imgClass: string = undefined export let imgClass: string = undefined
export let previewedImage: UIEventSource<ProvidedImage> = undefined export let previewedImage: UIEventSource<ProvidedImage> = undefined
export let attributionFormat: "minimal" | "medium" | "large" = "medium"
let canZoom = previewedImage !== undefined // We check if there is a SOURCE, not if there is data in it!
let loaded = false
</script> </script>
<div class="relative shrink-0"> <div class="relative shrink-0">
<img <div class="relative w-fit">
bind:this={imgEl} <img
class={imgClass ?? ""} bind:this={imgEl}
class:cursor-pointer={previewedImage !== undefined} on:load={() => loaded = true}
on:click={() => { class={imgClass ?? ""}
class:cursor-zoom-in={previewedImage !== undefined}
on:click={() => {
previewedImage?.setData(image) previewedImage?.setData(image)
}} }}
on:error={() => { on:error={() => {
if (fallbackImage) { if (fallbackImage) {
imgEl.src = fallbackImage imgEl.src = fallbackImage
} }
}} }}
src={image.url} src={image.url}
/> />
{#if canZoom && loaded}
<div class="absolute right-0 top-0 bg-black-transparent rounded-bl-full">
<MagnifyingGlassPlusIcon class="w-8 h-8 pl-3 pb-3 cursor-zoom-in" color="white" />
</div>
{/if}
</div>
<div class="absolute bottom-0 left-0"> <div class="absolute bottom-0 left-0">
<ImageAttribution {image} /> <ImageAttribution {image} {attributionFormat} />
</div> </div>
</div> </div>

View file

@ -4,11 +4,15 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import ToSvelte from "../Base/ToSvelte.svelte" import ToSvelte from "../Base/ToSvelte.svelte"
import { EyeIcon } from "@rgossiaux/svelte-heroicons/solid" import { EyeIcon } from "@rgossiaux/svelte-heroicons/solid"
import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations"
/** /**
* A small element showing the attribution of a single image * A small element showing the attribution of a single image
*/ */
export let image: Partial<ProvidedImage> & { id: string; url: string } export let image: Partial<ProvidedImage> & { id: string; url: string }
export let attributionFormat: "minimal" | "medium" | "large" = "medium"
let license: Store<LicenseInfo> = UIEventSource.FromPromise( let license: Store<LicenseInfo> = UIEventSource.FromPromise(
image.provider?.DownloadAttribution(image) image.provider?.DownloadAttribution(image)
) )
@ -16,50 +20,59 @@
</script> </script>
{#if $license !== undefined} {#if $license !== undefined}
<div class="no-images flex items-center rounded-lg bg-black p-0.5 pl-3 pr-3 text-sm text-white"> <div class="no-images flex items-center rounded-lg bg-black-transparent p-0.5 px-3 text-sm text-white">
{#if icon !== undefined} {#if icon !== undefined}
<div class="mr-2 h-6 w-6"> <div class="mr-2 h-6 w-6">
<ToSvelte construct={icon} /> <ToSvelte construct={icon} />
</div> </div>
{/if} {/if}
<div class="flex flex-col"> <div class="flex gap-x-2" class:flex-col={attributionFormat !== "minimal"}>
{#if $license.title} {#if attributionFormat !== "minimal" }
{#if $license.informationLocation} {#if $license.title}
<a href={$license.informationLocation.href} target="_blank" rel="noopener nofollower"> {#if $license.informationLocation}
{$license.title} <a href={$license.informationLocation.href} target="_blank" rel="noopener nofollower">
</a> {$license.title}
{:else} </a>
$license.title {:else}
$license.title
{/if}
{/if} {/if}
{/if} {/if}
{#if $license.artist} {#if $license.artist}
<div class="font-bold"> {#if attributionFormat === "large"}
{@html $license.artist} <Tr t={Translations.t.general.attribution.madeBy.Subs({author: $license.artist})} />
</div> {:else}
<div class="font-bold">
{@html $license.artist}
</div>
{/if}
{/if} {/if}
<div class="flex w-full justify-between gap-x-1">
{#if $license.license !== undefined || $license.licenseShortName !== undefined}
<div>
{$license?.license ?? $license?.licenseShortName}
</div>
{/if}
{#if $license.views}
<div class="flex justify-around self-center">
<EyeIcon class="h-4 w-4 pr-1" />
{$license.views}
</div>
{/if}
</div>
{#if $license.date} {#if $license.date}
<div> <div>
{$license.date.toLocaleDateString()} {$license.date.toLocaleDateString()}
</div> </div>
{/if} {/if}
{#if attributionFormat !== "minimal"}
<div class="flex w-full justify-between gap-x-1">
{#if ($license.license !== undefined || $license.licenseShortName !== undefined)}
<div>
{$license?.license ?? $license?.licenseShortName}
</div>
{/if}
{#if $license.views}
<div class="flex justify-around self-center text-xs">
<EyeIcon class="h-4 w-4 pr-1" />
{$license.views}
</div>
{/if}
</div>
{/if}
</div> </div>
</div> </div>
{/if} {/if}

View file

@ -38,9 +38,9 @@
class="pointer-events-none absolute bottom-0 left-0 flex w-full flex-wrap items-end justify-between" class="pointer-events-none absolute bottom-0 left-0 flex w-full flex-wrap items-end justify-between"
> >
<div <div
class="pointer-events-auto m-1 w-fit opacity-50 transition-colors duration-200 hover:opacity-100" class="pointer-events-auto m-1 w-fit transition-colors duration-200"
> >
<ImageAttribution {image} /> <ImageAttribution {image} attributionFormat="large"/>
</div> </div>
<button <button

View file

@ -14,6 +14,8 @@
import AttributedImage from "./AttributedImage.svelte" import AttributedImage from "./AttributedImage.svelte"
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte" import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
import LoginToggle from "../Base/LoginToggle.svelte" import LoginToggle from "../Base/LoginToggle.svelte"
import ImagePreview from "./ImagePreview.svelte"
import FloatOver from "../Base/FloatOver.svelte"
export let tags: UIEventSource<OsmTags> export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
@ -31,7 +33,7 @@
key: undefined, key: undefined,
provider: AllImageProviders.byName(image.provider), provider: AllImageProviders.byName(image.provider),
date: new Date(image.date), date: new Date(image.date),
id: Object.values(image.osmTags)[0], id: Object.values(image.osmTags)[0]
} }
async function applyLink(isLinked: boolean) { async function applyLink(isLinked: boolean) {
@ -42,7 +44,7 @@
if (isLinked) { if (isLinked) {
const action = new LinkImageAction(currentTags.id, key, url, tags, { const action = new LinkImageAction(currentTags.id, key, url, tags, {
theme: tags.data._orig_theme ?? state.layout.id, theme: tags.data._orig_theme ?? state.layout.id,
changeType: "link-image", changeType: "link-image"
}) })
await state.changes.applyAction(action) await state.changes.applyAction(action)
} else { } else {
@ -51,24 +53,26 @@
if (v === url) { if (v === url) {
const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, { const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, {
theme: tags.data._orig_theme ?? state.layout.id, theme: tags.data._orig_theme ?? state.layout.id,
changeType: "remove-image", changeType: "remove-image"
}) })
state.changes.applyAction(action) state.changes.applyAction(action)
} }
} }
} }
} }
isLinked.addCallback((isLinked) => applyLink(isLinked)) isLinked.addCallback((isLinked) => applyLink(isLinked))
</script> </script>
<div class="flex w-fit shrink-0 flex-col"> <div class="flex w-fit shrink-0 flex-col rounded-lg overflow-hidden" class:border-interactive={$isLinked}
<div class="cursor-zoom-in" on:click={() => state.previewedImage.setData(providedImage)}> style="border-width: 2px">
<AttributedImage <AttributedImage
image={providedImage} image={providedImage}
imgClass="max-h-64 w-auto" imgClass="max-h-64 w-auto"
previewedImage={state.previewedImage} previewedImage={state.previewedImage}
/> attributionFormat="minimal"
</div> />
<LoginToggle {state} silentFail={true}> <LoginToggle {state} silentFail={true}>
{#if linkable} {#if linkable}
<label> <label>

View file

@ -60,7 +60,7 @@
<Tr t={Translations.t.image.nearby.noNearbyImages} cls="alert" /> <Tr t={Translations.t.image.nearby.noNearbyImages} cls="alert" />
{/if} {/if}
{:else} {:else}
<div class="flex w-full space-x-1 overflow-x-auto" style="scroll-snap-type: x proximity"> <div class="flex w-full space-x-4 overflow-x-auto" style="scroll-snap-type: x proximity">
{#each $result as image (image.pictureUrl)} {#each $result as image (image.pictureUrl)}
<span class="w-fit shrink-0" style="scroll-snap-align: start"> <span class="w-fit shrink-0" style="scroll-snap-align: start">
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} /> <LinkableImage {tags} {image} {state} {feature} {layer} {linkable} />

View file

@ -352,6 +352,7 @@
{/if} {/if}
</legend> </legend>
<!-- Search menu -->
{#if config.mappings?.length >= 8 || hideMappingsUnlessSearchedFor} {#if config.mappings?.length >= 8 || hideMappingsUnlessSearchedFor}
<div class="sticky flex w-full" aria-hidden="true"> <div class="sticky flex w-full" aria-hidden="true">
<Search class="h-6 w-6" /> <Search class="h-6 w-6" />
@ -369,6 +370,7 @@
{/if} {/if}
{/if} {/if}
<!-- Actual options-->
{#if config?.freeform?.key && !(config?.mappings?.filter((m) => m.hideInAnswer != true)?.length > 0)} {#if config?.freeform?.key && !(config?.mappings?.filter((m) => m.hideInAnswer != true)?.length > 0)}
<!-- There are no options to choose from, simply show the input element: fill out the text field --> <!-- There are no options to choose from, simply show the input element: fill out the text field -->
<FreeformInput <FreeformInput
@ -384,7 +386,7 @@
/> />
{:else if config.mappings !== undefined && !config.multiAnswer} {:else if config.mappings !== undefined && !config.multiAnswer}
<!-- Simple radiobuttons as mapping --> <!-- Simple radiobuttons as mapping -->
<div class="flex flex-col"> <div class="flex flex-col no-bold">
{#each config.mappings as mapping, i (mapping.then)} {#each config.mappings as mapping, i (mapping.then)}
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices--> <!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices-->
<TagRenderingMappingInput <TagRenderingMappingInput
@ -432,7 +434,7 @@
</div> </div>
{:else if config.mappings !== undefined && config.multiAnswer} {:else if config.mappings !== undefined && config.multiAnswer}
<!-- Multiple answers can be chosen: checkboxes --> <!-- Multiple answers can be chosen: checkboxes -->
<div class="flex flex-col"> <div class="flex flex-col no-bold">
{#each config.mappings as mapping, i (mapping.then)} {#each config.mappings as mapping, i (mapping.then)}
<TagRenderingMappingInput <TagRenderingMappingInput
{mapping} {mapping}
@ -475,6 +477,8 @@
{/if} {/if}
</div> </div>
{/if} {/if}
<!-- Save and cancel buttons, in a logintoggle -->
<LoginToggle {state}> <LoginToggle {state}>
<Loading slot="loading" /> <Loading slot="loading" />
<SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}> <SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}>

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,6 @@ import { Translation, TypedTranslation } from "./Translation"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import CompiledTranslations from "../../assets/generated/CompiledTranslations" import CompiledTranslations from "../../assets/generated/CompiledTranslations"
import LanguageUtils from "../../Utils/LanguageUtils" import LanguageUtils from "../../Utils/LanguageUtils"
import { ClickableToggle } from "../Input/Toggle"
import { Store } from "../../Logic/UIEventSource" import { Store } from "../../Logic/UIEventSource"
import Locale from "./Locale" import Locale from "./Locale"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"

View file

@ -960,11 +960,15 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
if (!result["error"]) { if (!result["error"]) {
return result return result
} }
console.log(result) const error = result.error
if(result["error"]?.statuscode === 410){ if (error.statuscode === 410) {
// Gone permanently is not recoverable // Gone permanently is not recoverable
return result return result
} }
if (error.statuscode === 429 || error.statuscode === 509) {
// rate limited
return result
}
console.log( console.log(
`Request to ${url} failed, Trying again in a moment. Attempt ${ `Request to ${url} failed, Trying again in a moment. Attempt ${
i + 1 i + 1
@ -1774,7 +1778,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
static NoNullInplace<T>(items: T[]): T[] {
public static NoNullInplace<T>(items: T[]): T[] {
for (let i = items.length - 1; i >= 0; i--) { for (let i = items.length - 1; i >= 0; i--) {
if (items[i] === null || items[i] === undefined) { if (items[i] === null || items[i] === undefined) {
items.splice(i, 1) items.splice(i, 1)
@ -1783,6 +1788,27 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return items return items
} }
/**
* Removes or rewrites some characters in links, as some blink/chromium based browsers are picky about them
*
* Utils.prepareHref("tel:+32 123 456") // => "tel:+32123456"
* Utils.prepareHref("https://osm.org/user/User Name") // => "https://osm.org/user/User%20Name"
*/
static prepareHref(href: string): string {
if (href.startsWith("tel:")) {
// Telephone numbers are not allowed to contain spaces in chromium-based browsers
href = "tel:" + href.replaceAll(/[^+0-9]/g, "")
}
/* Chromium based browsers eat the spaces */
href = href.replaceAll(
/ /g,
"%20"
)
return href
}
private static emojiRegex = /[\p{Extended_Pictographic}🛰]/u private static emojiRegex = /[\p{Extended_Pictographic}🛰]/u
/** /**
@ -1793,7 +1819,15 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
* Utils.isEmoji("🍕") // => true * Utils.isEmoji("🍕") // => true
*/ */
public static isEmoji(string: string) { public static isEmoji(string: string) {
return Utils.emojiRegex.test(string) || return Utils.emojiRegex.test(string) || Utils.isEmojiFlag(string)
/[🇦-🇿]{2}/u.test(string) // flags, see https://stackoverflow.com/questions/53360006/detect-with-regex-if-emoji-is-country-flag }
/**
* Utils.isEmoji("🍕") // => false
* Utils.isEmojiFlag("🇧🇪") // => true
*/
public static isEmojiFlag(string: string) {
return /[🇦-🇿]{2}/u.test(string) // flags, see https://stackoverflow.com/questions/53360006/detect-with-regex-if-emoji-is-country-flag
} }
} }

View file

@ -568,6 +568,10 @@ a:hover {
background-color: #f2f2f2; background-color: #f2f2f2;
} }
.no-bold b {
font-weight: normal;
}
/************************* MISC ELEMENTS *************************/ /************************* MISC ELEMENTS *************************/
.selected svg:not(.noselect *) path.selectable { .selected svg:not(.noselect *) path.selectable {