forked from MapComplete/MapComplete
		
	Merge develop
This commit is contained in:
		
						commit
						d1e7eba2db
					
				
					 19 changed files with 554 additions and 448 deletions
				
			
		|  | @ -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" | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -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", | ||||||
|  |  | ||||||
|  | @ -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", | ||||||
|  |  | ||||||
|  | @ -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", | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
|  | @ -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") | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -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} /> | ||||||
|  |  | ||||||
|  | @ -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
											
										
									
								
							|  | @ -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" | ||||||
|  |  | ||||||
							
								
								
									
										44
									
								
								src/Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										44
									
								
								src/Utils.ts
									
										
									
									
									
								
							|  | @ -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 | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue