diff --git a/Models/Constants.ts b/Models/Constants.ts index 19ede120f..bc267d395 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -1,7 +1,7 @@ import { Utils } from "../Utils" export default class Constants { - public static vNumber = "0.25.5" + public static vNumber = "0.25.6" public static ImgurApiKey = "7070e7167f0a25a" public static readonly mapillary_client_token_v4 = diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index 36e4fa34c..6c63525d8 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -6,7 +6,8 @@ import Constants from "../Constants" import TilesourceConfig from "./TilesourceConfig" import { ExtractImages } from "./Conversion/FixImages" import ExtraLinkConfig from "./ExtraLinkConfig" - +import { Utils } from "../../Utils" +import * as used_languages from "../../assets/generated/used_languages.json" export default class LayoutConfig { public static readonly defaultSocialImage = "assets/SocialImage.png" public readonly id: string @@ -235,6 +236,54 @@ export default class LayoutConfig { return this.layers.some((l) => l.isLeftRightSensitive()) } + public missingTranslations(): { + completeness: Map + untranslated: Map + total: number + } { + const layout = this + let total = 0 + const completeness = new Map() + const untranslated = new Map() + + Utils.WalkObject( + layout, + (o) => { + const translation = (o) + if (translation.translations["*"] !== undefined) { + return + } + if (translation.context === undefined || translation.context.indexOf(":") < 0) { + // no source given - lets ignore + return + } + + total++ + used_languages.languages.forEach((ln) => { + const trans = translation.translations + if (trans["*"] !== undefined) { + return + } + if (trans[ln] === undefined) { + if (!untranslated.has(ln)) { + untranslated.set(ln, []) + } + untranslated.get(ln).push(translation.context) + } else { + completeness.set(ln, 1 + (completeness.get(ln) ?? 0)) + } + }) + }, + (o) => { + if (o === undefined || o === null) { + return false + } + return o instanceof Translation + } + ) + + return { completeness, untranslated, total } + } public getMatchingLayer(tags: any): LayerConfig | undefined { if (tags === undefined) { return undefined diff --git a/UI/BigComponents/TranslatorsPanel.ts b/UI/BigComponents/TranslatorsPanel.ts index 0124c10b1..2be3f939a 100644 --- a/UI/BigComponents/TranslatorsPanel.ts +++ b/UI/BigComponents/TranslatorsPanel.ts @@ -15,15 +15,13 @@ import { Store } from "../../Logic/UIEventSource" import { SubtleButton } from "../Base/SubtleButton" import Svg from "../../Svg" import * as native_languages from "../../assets/language_native.json" -import * as used_languages from "../../assets/generated/used_languages.json" import BaseUIElement from "../BaseUIElement" class TranslatorsPanelContent extends Combine { constructor(layout: LayoutConfig, isTranslator: Store) { const t = Translations.t.translations - const { completeness, untranslated, total } = - TranslatorsPanel.MissingTranslationsFor(layout) + const { completeness, untranslated, total } = layout.missingTranslations() const seed = t.completeness for (const ln of Array.from(completeness.keys())) { @@ -147,52 +145,4 @@ export default class TranslatorsPanel extends Toggle { ) this.SetClass("hidden-on-mobile") } - - public static MissingTranslationsFor(layout: LayoutConfig): { - completeness: Map - untranslated: Map - total: number - } { - let total = 0 - const completeness = new Map() - const untranslated = new Map() - - Utils.WalkObject( - layout, - (o) => { - const translation = (o) - if (translation.translations["*"] !== undefined) { - return - } - if (translation.context === undefined || translation.context.indexOf(":") < 0) { - // no source given - lets ignore - return - } - - total++ - used_languages.languages.forEach((ln) => { - const trans = translation.translations - if (trans["*"] !== undefined) { - return - } - if (trans[ln] === undefined) { - if (!untranslated.has(ln)) { - untranslated.set(ln, []) - } - untranslated.get(ln).push(translation.context) - } else { - completeness.set(ln, 1 + (completeness.get(ln) ?? 0)) - } - }) - }, - (o) => { - if (o === undefined || o === null) { - return false - } - return o instanceof Translation - } - ) - - return { completeness, untranslated, total } - } } diff --git a/UI/BigComponents/UserInformation.ts b/UI/BigComponents/UserInformation.ts index 66aca2699..fa3a5da3f 100644 --- a/UI/BigComponents/UserInformation.ts +++ b/UI/BigComponents/UserInformation.ts @@ -22,7 +22,7 @@ import { TagUtils } from "../../Logic/Tags/TagUtils" import * as usersettings from "../../assets/generated/layers/usersettings.json" import { LoginToggle } from "../Popup/LoginButton" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" - +import * as translators from "../../assets/translators.json" export class ImportViewerLinks extends VariableUiElement { constructor(osmConnection: OsmConnection) { super( @@ -44,24 +44,27 @@ export class ImportViewerLinks extends VariableUiElement { } class SingleUserSettingsPanel extends EditableTagRendering { - constructor(config: TagRenderingConfig, osmConnection: OsmConnection) { + constructor( + config: TagRenderingConfig, + osmConnection: OsmConnection, + amendedPrefs: UIEventSource, + userInfoFocusedQuestion?: UIEventSource + ) { const editMode = new UIEventSource(false) + // Isolate the preferences. THey'll be updated explicitely later on anyway super( - osmConnection.preferencesHandler.preferences, + amendedPrefs, config, [], { osmConnection }, { + answerElementClasses: "p-2", editMode, createSaveButton: (store) => - new SaveButton( - osmConnection.preferencesHandler.preferences, - osmConnection - ).onClick(() => { - const prefs = osmConnection.preferencesHandler.preferences + new SaveButton(amendedPrefs, osmConnection).onClick(() => { const selection = TagUtils.FlattenMultiAnswer( - TagUtils.FlattenAnd(store.data, prefs.data) - ).asChange(prefs.data) + TagUtils.FlattenAnd(store.data, amendedPrefs.data) + ).asChange(amendedPrefs.data) for (const kv of selection) { osmConnection.GetPreference(kv.k, "", "").setData(kv.v) } @@ -70,6 +73,16 @@ class SingleUserSettingsPanel extends EditableTagRendering { }), } ) + const self = this + this.SetClass("rounded-xl") + userInfoFocusedQuestion.addCallbackAndRun((selected) => { + if (config.id !== selected) { + console.log("Removing the glowingshadow...") + self.RemoveClass("glowing-shadow") + } else { + self.SetClass("glowing-shadow") + } + }) } } @@ -78,11 +91,35 @@ class UserInformationMainPanel extends VariableUiElement { osmConnection: OsmConnection, locationControl: UIEventSource, layout: LayoutConfig, - isOpened: UIEventSource + isOpened: UIEventSource, + userInfoFocusedQuestion?: UIEventSource ) { const t = Translations.t.userinfo const imgSize = "h-6 w-6" const ud = osmConnection.userDetails + + const amendedPrefs = new UIEventSource({}) + osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => { + for (const k in newPrefs) { + amendedPrefs.data[k] = newPrefs[k] + } + amendedPrefs.ping() + }) + osmConnection.userDetails.addCallback((userDetails) => { + for (const k in userDetails) { + amendedPrefs.data["_" + k] = "" + userDetails[k] + } + const simplifiedName = userDetails.name.toLowerCase().replace(/\s+/g, "") + const isTranslator = translators.contributors.some( + (c: { contributor: string; commits: number }) => { + const replaced = c.contributor.toLowerCase().replace(/\s+/g, "") + return replaced === simplifiedName + } + ) + amendedPrefs.data["_is_translator"] = "" + isTranslator + amendedPrefs.ping() + }) + super( ud.map((ud) => { let img: BaseUIElement = Svg.person_ui().SetClass("block") @@ -138,7 +175,12 @@ class UserInformationMainPanel extends VariableUiElement { const usersettingsConfig = new LayerConfig(usersettings, "userinformationpanel") const questions = usersettingsConfig.tagRenderings.map((c) => - new SingleUserSettingsPanel(c, osmConnection).SetClass("block my-4") + new SingleUserSettingsPanel( + c, + osmConnection, + amendedPrefs, + userInfoFocusedQuestion + ).SetClass("block my-4") ) return new Combine([ @@ -187,6 +229,7 @@ export default class UserInformationPanel extends ScrollableFullScreen { }, options?: { isOpened?: UIEventSource + userInfoFocusedQuestion?: UIEventSource } ) { const isOpened = options?.isOpened ?? new UIEventSource(false) @@ -207,7 +250,8 @@ export default class UserInformationPanel extends ScrollableFullScreen { state.osmConnection, state.locationControl, state.layoutToUse, - isOpened + isOpened, + options?.userInfoFocusedQuestion ), Translations.t.general.getStartedLogin, state diff --git a/UI/DefaultGUI.ts b/UI/DefaultGUI.ts index 079090e4e..d368d0cab 100644 --- a/UI/DefaultGUI.ts +++ b/UI/DefaultGUI.ts @@ -205,9 +205,9 @@ export default class DefaultGUI { const self = this const userInfoMapControl = Toggle.If(state.featureSwitchUserbadge, () => { - console.log("Guistate is", guiState) new UserInformationPanel(state, { isOpened: guiState.userInfoIsOpened, + userInfoFocusedQuestion: guiState.userInfoFocusedQuestion, }) const mapControl = new MapControlButton( diff --git a/UI/DefaultGuiState.ts b/UI/DefaultGuiState.ts index cb780b1c4..e56870e1a 100644 --- a/UI/DefaultGuiState.ts +++ b/UI/DefaultGuiState.ts @@ -19,6 +19,9 @@ export class DefaultGuiState { false ) public readonly userInfoIsOpened: UIEventSource = new UIEventSource(false) + public readonly userInfoFocusedQuestion: UIEventSource = new UIEventSource( + undefined + ) public readonly welcomeMessageOpenedTab: UIEventSource constructor() { @@ -38,6 +41,14 @@ export class DefaultGuiState { userinfo: this.userInfoIsOpened, } + const self = this + this.userInfoIsOpened.addCallback((isOpen) => { + if (!isOpen) { + console.log("Resetting focused question") + self.userInfoFocusedQuestion.setData(undefined) + } + }) + sources[Hash.hash.data?.toLowerCase()]?.setData(true) if (Hash.hash.data === "" || Hash.hash.data === undefined) { diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index 6121de2f1..75fdb05c1 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -17,6 +17,8 @@ import { Changes } from "../../Logic/Osm/Changes" import Loading from "../Base/Loading" import { LoginToggle } from "../Popup/LoginButton" import Constants from "../../Models/Constants" +import { DefaultGuiState } from "../DefaultGuiState" +import ScrollableFullScreen from "../Base/ScrollableFullScreen" export class ImageUploadFlow extends Toggle { private static readonly uploadCountsPerId = new Map>() @@ -80,6 +82,10 @@ export class ImageUploadFlow extends Toggle { ]).SetClass( "p-2 border-4 border-detail rounded-full font-bold h-full align-middle w-full flex justify-center" ) + const licenseStore = state?.osmConnection?.GetPreference( + Constants.OsmPreferenceKeyPicturesLicense, + "CC0" + ) const fileSelector = new FileSelectorButton(label) fileSelector.GetValue().addCallback((filelist) => { @@ -101,12 +107,7 @@ export class ImageUploadFlow extends Toggle { } } - console.log("Received images from the user, starting upload") - const license = - state?.osmConnection?.GetPreference( - Constants.OsmPreferenceKeyPicturesLicense, - "CC0" - )?.data ?? "CC0" + const license = licenseStore?.data ?? "CC0" const tags = tagsSource.data @@ -174,7 +175,21 @@ export class ImageUploadFlow extends Toggle { ), fileSelector, - Translations.t.image.respectPrivacy.Clone().SetStyle("font-size:small;"), + new Combine([ + Translations.t.image.respectPrivacy, + new VariableUiElement( + licenseStore.map((license) => + Translations.t.image.currentLicense.Subs({ license }) + ) + ) + .onClick(() => { + console.log("Opening the license settings... ") + ScrollableFullScreen.collapse() + DefaultGuiState.state.userInfoIsOpened.setData(true) + DefaultGuiState.state.userInfoFocusedQuestion.setData("picture-license") + }) + .SetClass("underline"), + ]).SetStyle("font-size:small;"), ]).SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center") super( diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index 6a86b9fab..6675721b1 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -21,6 +21,8 @@ export default class EditableTagRendering extends Toggle { options: { editMode?: UIEventSource innerElementClasses?: string + /* Classes applied _only_ on the rendered element, not on the question*/ + answerElementClasses?: string /* Default will apply the tags to the relevant object, only use in special cases */ createSaveButton?: (src: Store) => BaseUIElement } @@ -43,7 +45,10 @@ export default class EditableTagRendering extends Toggle { configuration, units, editMode, - { saveButtonConstructor: options?.createSaveButton } + { + saveButtonConstructor: options?.createSaveButton, + answerElementClasses: options?.answerElementClasses, + } ) rendering.SetClass(options.innerElementClasses) if (state?.featureSwitchIsDebugging?.data || state?.featureSwitchIsTesting?.data) { @@ -73,6 +78,7 @@ export default class EditableTagRendering extends Toggle { editMode: UIEventSource, options?: { saveButtonConstructor?: (src: Store) => BaseUIElement + answerElementClasses?: string } ): BaseUIElement { const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration, state) @@ -107,7 +113,7 @@ export default class EditableTagRendering extends Toggle { onlyIfPartiallyHidden: true, }) }), - ]).SetClass("flex justify-between w-full") + ]).SetClass("flex justify-between w-full " + (options?.answerElementClasses ?? "")) rendering = new Toggle(question, answerWithEditButton, editMode) } return rendering diff --git a/UI/i18n/Translation.ts b/UI/i18n/Translation.ts index adf641941..01b19304e 100644 --- a/UI/i18n/Translation.ts +++ b/UI/i18n/Translation.ts @@ -155,14 +155,13 @@ export class Translation extends BaseUIElement { return el } - const linkToWeblate = new LinkToWeblate(self.context, self.translations) - const wrapper = document.createElement("span") wrapper.appendChild(el) Locale.showLinkToWeblate.addCallbackAndRun((doShow) => { if (!doShow) { return } + const linkToWeblate = new LinkToWeblate(self.context, self.translations) wrapper.appendChild(linkToWeblate.ConstructElement()) return true }) diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index 3f8765194..243deefff 100644 --- a/assets/layers/usersettings/usersettings.json +++ b/assets/layers/usersettings/usersettings.json @@ -18,29 +18,45 @@ { "if": "mapcomplete-pictures-license=", "then": { - "en": "No license has been chosen yet" + "en": "Pictures you take will be licensed with CC0 and added to the public domain. This means that everyone can use your pictures for any purpose. This is the default choice." }, "hideInAnswer": true }, { "if": "mapcomplete-pictures-license=CC0", "then": { - "en": "Pictures you take will be licensed with CC0 and added to the public domain. This means that everyone can use your pictures for any purpose." + "en": "Pictures you take will be licensed with CC0 and added to the public domain. This means that everyone can use your pictures for any purpose." } }, { "if": "mapcomplete-pictures-license=CC-BY 4.0", "then": { - "en": "Pictures you take will be licensed with CC-BY 4.0 which requires everyone using your picture that they have to attribute you" + "en": "Pictures you take will be licensed with CC-BY 4.0 which requires everyone using your picture that they have to attribute you" } }, { "if": "mapcomplete-pictures-license=CC-BY-SA 4.0", "then": { - "en": "Pictures you take will be licensed with CC-BY-SA 4.0 which means that everyone using your picture must attribute you and that derivatives of your picture must be reshared with the same license." + "en": "Pictures you take will be licensed with CC-BY-SA 4.0 which means that everyone using your picture must attribute you and that derivatives of your picture must be reshared with the same license." } } ] + }, + { + "id": "translation-thanks", + "condition": "_is_translator=true", + "mappings": [{ + "if": "_is_translator=true", + "then": { + "en": "You have contributed to translating MapComplete! That's awesome!" + }, + "icon": "party" + }] + }, + { + "id": "debug", + "condition": "_name=Pieter Vander Vennet", + "render": "{all_tags()}" } ], "mapRendering": null diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json index bb175ae37..5771f2c70 100644 --- a/assets/tagRenderings/questions.json +++ b/assets/tagRenderings/questions.json @@ -1493,7 +1493,7 @@ } ] }, - "induction-loop": { + "induction-loop": { "description": "An accessibility feature: induction loops are for hard-hearing persons which have an FM-receiver.", "question": { "en": "Does this place have an audio induction loop for people with reduced hearing?", @@ -1766,4 +1766,4 @@ "ca": "El nom de la xarxa és {internet_access:ssid}" } } -} +} \ No newline at end of file diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index bd41185db..827800061 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -2505,6 +2505,21 @@ input { /* The checkbox that toggles a single layer */ } +.glowing-shadow { + -webkit-animation: glowing 1s ease-in-out infinite alternate; + animation: glowing 1s ease-in-out infinite alternate; +} + +@-webkit-keyframes glowing { + from { + box-shadow: 0 0 20px 10px #eaaf2588, inset 0 0 0px 1px #eaaf25; + } + + to { + box-shadow: 0 0 20px 20px #eaaf2588, inset 0 0 5px 1px #eaaf25; + } +} + .mapping-icon-small-height { /* A mapping icon type */ height: 1.5rem; diff --git a/css/tagrendering.css b/css/tagrendering.css index fa8106f58..417c4c62f 100644 --- a/css/tagrendering.css +++ b/css/tagrendering.css @@ -10,7 +10,7 @@ background-color: var(--subtle-detail-color); color: var(--subtle-detail-color-contrast); padding: 1em; - border-radius: 1em; + border-radius: 0.75rem; font-size: larger !important; overflow-wrap: initial; } diff --git a/index.css b/index.css index 234fcf37b..57918e7db 100644 --- a/index.css +++ b/index.css @@ -637,7 +637,19 @@ input { /* The checkbox that toggles a single layer */ } - +.glowing-shadow { + -webkit-animation: glowing 1s ease-in-out infinite alternate; + -moz-animation: glowing 1s ease-in-out infinite alternate; + animation: glowing 1s ease-in-out infinite alternate; +} +@-webkit-keyframes glowing { + from { + box-shadow: 0 0 20px 10px #eaaf2588, inset 0 0 0px 1px #eaaf25; + } + to { + box-shadow: 0 0 20px 20px #eaaf2588, inset 0 0 5px 1px #eaaf25; + } +} .mapping-icon-small-height { /* A mapping icon type */ diff --git a/langs/en.json b/langs/en.json index e0c75a721..171a8b11e 100644 --- a/langs/en.json +++ b/langs/en.json @@ -364,6 +364,7 @@ }, "image": { "addPicture": "Add picture", + "currentLicense": "Your images will be published under {license}", "doDelete": "Remove image", "dontDelete": "Cancel", "isDeleted": "Deleted", diff --git a/langs/shared-questions/en.json b/langs/shared-questions/en.json index 31b7273ca..25168f6f5 100644 --- a/langs/shared-questions/en.json +++ b/langs/shared-questions/en.json @@ -200,23 +200,6 @@ "phone": { "question": "What is the phone number of {title()}?" }, - "picture-license": { - "mappings": { - "0": { - "then": "No license has been chosen yet" - }, - "1": { - "then": "Pictures you take will be licensed with CC0 and added to the public domain. This means that everyone can use your pictures for any purpose." - }, - "2": { - "then": "Pictures you take will be licensed with CC-BY 4.0 which requires everyone using your picture that they have to attribute you" - }, - "3": { - "then": "Pictures you take will be licensed with CC-BY-SA 4.0 which means that everyone using your picture must attribute you and that derivatives of your picture must be reshared with the same license." - } - }, - "question": "Under what license do you want to publish your pictures?" - }, "service:electricity": { "mappings": { "0": {