Merge develop

This commit is contained in:
Pieter Vander Vennet 2023-03-15 14:29:53 +01:00
commit e67d740769
719 changed files with 30508 additions and 15682 deletions

View file

@ -9,9 +9,10 @@ import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import { Utils } from "../../Utils"
import { MapillaryLink } from "./MapillaryLink"
import TranslatorsPanel from "./TranslatorsPanel"
import { OpenIdEditor, OpenJosm } from "./CopyrightPanel"
import Toggle from "../Input/Toggle"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import { DefaultGuiState } from "../DefaultGuiState"
export class BackToThemeOverview extends Toggle {
constructor(
@ -40,7 +41,6 @@ export class ActionButtons extends Combine {
readonly currentBounds: Store<BBox>
readonly locationControl: Store<Loc>
readonly osmConnection: OsmConnection
readonly isTranslator: Store<boolean>
readonly featureSwitchMoreQuests: Store<boolean>
}) {
const imgSize = "h-6 w-6"
@ -77,7 +77,14 @@ export class ActionButtons extends Combine {
new OpenIdEditor(state, iconStyle),
new MapillaryLink(state, iconStyle),
new OpenJosm(state, iconStyle).SetClass("hidden-on-mobile"),
new TranslatorsPanel(state, iconStyle),
new SubtleButton(
Svg.translate_ui().SetStyle(iconStyle),
Translations.t.translations.activateButton
).onClick(() => {
ScrollableFullScreen.collapse()
DefaultGuiState.state.userInfoIsOpened.setData(true)
DefaultGuiState.state.userInfoFocusedQuestion.setData("translation-mode")
}),
])
this.SetClass("block w-full link-no-underline")
}

View file

@ -9,6 +9,7 @@ import { VariableUiElement } from "../Base/VariableUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { BBox } from "../../Logic/BBox"
import { Utils } from "../../Utils"
import Translations from "../i18n/Translations"
/**
* The bottom right attribution panel in the leaflet map
@ -58,7 +59,13 @@ export default class Attribution extends Combine {
true
)
let editWithJosm = new VariableUiElement(
const mapDataByOsm = new Link(
Translations.t.general.attribution.mapDataByOsm,
"https://openstreetmap.org/copyright",
true
)
const editWithJosm = new VariableUiElement(
userDetails.map(
(userDetails) => {
if (userDetails.csCount < Constants.userJourney.tagsVisibleAndWikiLinked) {
@ -79,7 +86,7 @@ export default class Attribution extends Combine {
[location, currentBounds]
)
)
super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary])
super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary, mapDataByOsm])
this.SetClass("flex")
}
}

View file

@ -108,7 +108,10 @@ class SingleLayerSelectionButton extends Toggle {
// Is the previous layer still valid? If so, we don't bother to switch
if (
previousLayer.data.feature === null ||
GeoOperations.inside(locationControl.data, previousLayer.data.feature)
GeoOperations.inside(
[locationControl.data.lon, locationControl.data.lat],
previousLayer.data.feature
)
) {
return
}

View file

@ -0,0 +1,45 @@
<script lang="ts">
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Tiles } from "../../Models/TileRange"
import { Utils } from "../../Utils"
import global_community from "../../assets/community_index_global_resources.json"
import ContactLink from "./ContactLink.svelte"
import { GeoOperations } from "../../Logic/GeoOperations"
import Translations from "../i18n/Translations"
import ToSvelte from "../Base/ToSvelte.svelte"
import type { Feature, Geometry, GeometryCollection } from "@turf/turf"
export let locationControl: Store<{ lat: number; lon: number }>
const tileToFetch: Store<string> = locationControl.mapD((l) => {
const t = Tiles.embedded_tile(l.lat, l.lon, 6)
return `https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/community_index/tile_${t.z}_${t.x}_${t.y}.geojson`
})
const t = Translations.t.communityIndex
const resources = new UIEventSource<
Feature<Geometry | GeometryCollection, { resources; nameEn: string }>[]
>([])
tileToFetch.addCallbackAndRun(async (url) => {
const data = await Utils.downloadJsonCached(url, 24 * 60 * 60)
if (data === undefined) {
return
}
resources.setData(data.features)
})
const filteredResources = resources.map(
(features) =>
features.filter((f) => {
return GeoOperations.inside([locationControl.data.lon, locationControl.data.lat], f)
}),
[locationControl]
)
</script>
<div>
<ToSvelte construct={t.intro} />
{#each $filteredResources as feature}
<ContactLink country={feature.properties} />
{/each}
<ContactLink country={{ resources: global_community, nameEn: "Global resources" }} />
</div>

View file

@ -0,0 +1,50 @@
<script lang="ts">
// A contact link indicates how a mapper can contact their local community
// The _properties_ of a community feature
import Locale from "../i18n/Locale.js"
import Translations from "../i18n/Translations"
import ToSvelte from "../Base/ToSvelte.svelte"
import * as native from "../../assets/language_native.json"
import { TypedTranslation } from "../i18n/Translation"
const availableTranslationTyped: TypedTranslation<{ native: string }> =
Translations.t.communityIndex.available
const availableTranslation = availableTranslationTyped.OnEveryLanguage((s, ln) =>
s.replace("{native}", native[ln] ?? ln)
)
export let country: { resources; nameEn: string }
let resources: {
id: string
resolved: Record<string, string>
languageCodes: string[]
type: string
}[] = []
$: resources = Array.from(Object.values(country?.resources ?? {}))
const language = Locale.language
</script>
<div>
{#if country?.nameEn}
<h3>{country?.nameEn}</h3>
{/if}
{#each resources as resource}
<div class="flex link-underline items-center my-4">
<img
class="w-8 h-8 m-2"
src={`https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/community_index/${resource.type}.svg`}
/>
<div class="flex flex-col">
<a href={resource.resolved.url} target="_blank" rel="noreferrer nofollow" class="font-bold">
{resource.resolved.name ?? resource.resolved.url}
</a>
{resource.resolved?.description}
{#if resource.languageCodes?.indexOf($language) >= 0}
<span class="border-2 rounded-full border-lime-500 text-sm w-fit px-2">
<ToSvelte construct={() => availableTranslation.Clone()} />
</span>
{/if}
</div>
</div>
{/each}
</div>

View file

@ -2,13 +2,13 @@ import Combine from "../Base/Combine"
import Translations from "../i18n/Translations"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { FixedUiElement } from "../Base/FixedUiElement"
import * as licenses from "../../assets/generated/license_info.json"
import licenses from "../../assets/generated/license_info.json"
import SmallLicense from "../../Models/smallLicense"
import { Utils } from "../../Utils"
import Link from "../Base/Link"
import { VariableUiElement } from "../Base/VariableUIElement"
import * as contributors from "../../assets/contributors.json"
import * as translators from "../../assets/translators.json"
import contributors from "../../assets/contributors.json"
import translators from "../../assets/translators.json"
import BaseUIElement from "../BaseUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import Title from "../Base/Title"
@ -118,12 +118,13 @@ export default class CopyrightPanel extends Combine {
currentBounds: Store<BBox>
locationControl: UIEventSource<Loc>
osmConnection: OsmConnection
isTranslator: Store<boolean>
}) {
const t = Translations.t.general.attribution
const layoutToUse = state.layoutToUse
const iconAttributions = layoutToUse.usedImages.map(CopyrightPanel.IconAttribution)
const iconAttributions: BaseUIElement[] = layoutToUse.usedImages.map(
CopyrightPanel.IconAttribution
)
let maintainer: BaseUIElement = undefined
if (layoutToUse.credits !== undefined && layoutToUse.credits !== "") {

View file

@ -0,0 +1,38 @@
<script lang="ts">
import UserDetails from "../../Logic/Osm/OsmConnection"
import { UIEventSource } from "../../Logic/UIEventSource"
import Constants from "../../Models/Constants"
import Svg from "../../Svg"
import SubtleButton from "../Base/SubtleButton.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations"
export let userDetails: UIEventSource<UserDetails>
const t = Translations.t.general.morescreen
console.log($userDetails.csCount < 50)
</script>
<div>
{#if $userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock}
<SubtleButton
options={{
url: "https://github.com/pietervdvn/MapComplete/issues",
newTab: true,
}}
>
<span slot="message">{t.requestATheme.toString()}</span>
</SubtleButton>
{:else}
<SubtleButton
options={{
url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html",
}}
>
<span slot="image" class="h-11 w-11 mx-4 bg-red">
<ToSvelte construct={Svg.pencil_ui()} />
</span>
<span slot="message">{t.createYourOwnTheme.toString()}</span>
</SubtleButton>
{/if}
</div>

View file

@ -227,7 +227,7 @@ export class DownloadPanel extends Toggle {
bbox,
new Set(neededLayers)
)
outer: for (const tile of featureList) {
for (const tile of featureList) {
if (Constants.priviliged_layers.indexOf(tile.layer) >= 0) {
continue
}
@ -238,7 +238,7 @@ export class DownloadPanel extends Toggle {
}
const featureList = perLayer.get(tile.layer)
const filters = layer.appliedFilters.data
for (const feature of tile.features) {
perfeature: for (const feature of tile.features) {
if (!bbox.overlapsWith(BBox.get(feature))) {
continue
}
@ -250,7 +250,7 @@ export class DownloadPanel extends Toggle {
continue
}
if (!filter.currentFilter.matchesProperties(feature.properties)) {
continue outer
continue perfeature
}
}
}
@ -281,7 +281,7 @@ export class DownloadPanel extends Toggle {
delete feature.properties[key]
}
featureList.push(feature)
featureList.push(cleaned)
}
}

View file

@ -1,9 +1,9 @@
import Combine from "../Base/Combine"
import * as welcome_messages from "../../assets/welcome_message.json"
import welcome_messages from "../../assets/welcome_message.json"
import BaseUIElement from "../BaseUIElement"
import { FixedUiElement } from "../Base/FixedUiElement"
import MoreScreen from "./MoreScreen"
import * as themeOverview from "../../assets/generated/theme_overview.json"
import themeOverview from "../../assets/generated/theme_overview.json"
import Translations from "../i18n/Translations"
import Title from "../Base/Title"
@ -43,7 +43,7 @@ export default class FeaturedMessage extends Combine {
}[] = []
const themesById = new Map<string, { id: string; title: any; shortDescription: any }>()
for (const theme of themeOverview["default"]) {
for (const theme of themeOverview) {
themesById.set(theme.id, theme)
}
@ -88,9 +88,7 @@ export default class FeaturedMessage extends Combine {
const msg = new FixedUiElement(welcome_message.message).SetClass("link-underline font-lg")
els.push(new Combine([title, msg]).SetClass("m-4"))
if (welcome_message.featured_theme !== undefined) {
const theme = themeOverview["default"].filter(
(th) => th.id === welcome_message.featured_theme
)[0]
const theme = themeOverview.filter((th) => th.id === welcome_message.featured_theme)[0]
els.push(
MoreScreen.createLinkButton({}, theme)

View file

@ -279,12 +279,17 @@ export class LayerFilterPanel extends Combine {
const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6")
const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2 w-6")
const qp = QueryParameters.GetBooleanQueryParameter(
"filter-" + filterConfig.id,
false,
"Is filter '" + filterConfig.options[0].question.textFor("en") + " enabled?"
)
const toggle = new ClickableToggle(
new Combine([icon, option.question.Clone().SetClass("block")]).SetClass("flex"),
new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass(
"flex"
)
),
qp
)
.ToggleOnClick()
.SetClass("block m-1")
@ -315,6 +320,15 @@ export class LayerFilterPanel extends Combine {
state: i,
}))
let filterPicker: InputElement<number>
const value = QueryParameters.GetQueryParameter(
"filter-" + filterConfig.id,
"0",
"Value for filter " + filterConfig.id
).sync(
(str) => Number(str),
[],
(n) => "" + n
)
if (options.length <= 6) {
filterPicker = new RadioButton(
@ -323,6 +337,7 @@ export class LayerFilterPanel extends Combine {
new FixedInputElement(option.question.Clone().SetClass("block"), i)
),
{
value,
dontStyle: true,
}
)
@ -332,7 +347,8 @@ export class LayerFilterPanel extends Combine {
options.map((option, i) => ({
value: i,
shown: option.question.Clone(),
}))
})),
value
)
}

View file

@ -85,15 +85,6 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
tabs.push({ header: Svg.share_img, content: new ShareScreen(state) })
}
const copyright = {
header: Svg.copyright_svg(),
content: new Combine([
Translations.t.general.openStreetMapIntro.SetClass("link-underline"),
new CopyrightPanel(state),
]),
}
tabs.push(copyright)
const privacy = {
header: Svg.eye_svg(),
content: new PrivacyPolicy(),

View file

@ -6,6 +6,7 @@ import { BBox } from "../../Logic/BBox"
import Loc from "../../Models/Loc"
import Hotkeys from "../Base/Hotkeys"
import Translations from "../i18n/Translations"
import Constants from "../../Models/Constants"
/**
* Displays an icon depending on the state of the geolocation.
@ -20,6 +21,9 @@ export class GeolocationControl extends VariableUiElement {
}
) {
const lastClick = new UIEventSource<Date>(undefined)
lastClick.addCallbackD((date) => {
geolocationHandler.geolocationState.requestMoment.setData(date)
})
const lastClickWithinThreeSecs = lastClick.map((lastClick) => {
if (lastClick === undefined) {
return false
@ -27,9 +31,19 @@ export class GeolocationControl extends VariableUiElement {
const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000
return timeDiff <= 3
})
const geolocationState = geolocationHandler.geolocationState
const lastRequestWithinTimeout = geolocationHandler.geolocationState.requestMoment.map(
(date) => {
if (date === undefined) {
return false
}
const timeDiff = (new Date().getTime() - date.getTime()) / 1000
console.log("Timediff", timeDiff)
return timeDiff <= Constants.zoomToLocationTimeout
}
)
const geolocationState = geolocationHandler?.geolocationState
super(
geolocationState.permission.map(
geolocationState?.permission?.map(
(permission) => {
if (permission === "denied") {
return Svg.location_refused_svg()
@ -43,9 +57,10 @@ export class GeolocationControl extends VariableUiElement {
return Svg.location_empty_svg()
}
// Position not yet found, but permission is either requested or granted: we spin to indicate activity
const icon = !geolocationHandler.mapHasMoved.data
? Svg.location_svg()
: Svg.location_empty_svg()
const icon =
!geolocationHandler.mapHasMoved.data || lastRequestWithinTimeout.data
? Svg.location_svg()
: Svg.location_empty_svg()
return icon
.SetClass("cursor-wait")
.SetStyle("animation: spin 4s linear infinite;")
@ -65,6 +80,7 @@ export class GeolocationControl extends VariableUiElement {
geolocationState.isLocked,
geolocationHandler.mapHasMoved,
lastClickWithinThreeSecs,
lastRequestWithinTimeout,
]
)
)
@ -74,6 +90,8 @@ export class GeolocationControl extends VariableUiElement {
geolocationState.permission.data !== "granted" &&
geolocationState.currentGPSLocation.data === undefined
) {
lastClick.setData(new Date())
geolocationState.requestMoment.setData(new Date())
await geolocationState.requestPermission()
}
@ -85,6 +103,7 @@ export class GeolocationControl extends VariableUiElement {
if (geolocationState.currentGPSLocation.data === undefined) {
// No location is known yet, not much we can do
lastClick.setData(new Date())
return
}
@ -126,5 +145,12 @@ export class GeolocationControl extends VariableUiElement {
}
}, 500)
})
geolocationHandler.geolocationState.requestMoment.addCallbackAndRunD((_) => {
window.setTimeout(() => {
if (lastRequestWithinTimeout.data) {
geolocationHandler.geolocationState.requestMoment.ping()
}
}, 500)
})
}
}

View file

@ -0,0 +1,47 @@
<script lang="ts">
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { UIEventSource } from "../../Logic/UIEventSource"
import type Loc from "../../Models/Loc"
import * as themeOverview from "../../assets/generated/theme_overview.json"
import { Utils } from "../../Utils"
import ThemesList from "./ThemesList.svelte"
import Translations from "../i18n/Translations"
import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
export let search: UIEventSource<string>
export let state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> }
export let onMainScreen: boolean = true
const prefix = "mapcomplete-hidden-theme-"
const hiddenThemes: LayoutInformation[] = themeOverview["default"].filter(
(layout) => layout.hideFromOverview
)
const userPreferences = state.osmConnection.preferencesHandler.preferences
const t = Translations.t.general.morescreen
$: knownThemesId = Utils.NoNull(
Object.keys($userPreferences)
.filter((key) => key.startsWith(prefix))
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
)
$: knownThemes = hiddenThemes.filter((theme) => knownThemesId.includes(theme.id))
</script>
<ThemesList
{search}
{state}
{onMainScreen}
themes={knownThemes}
isCustom={false}
hideThemes={false}
>
<svelte:fragment slot="title">
<h3>{t.previouslyHiddenTitle.toString()}</h3>
<p>
{t.hiddenExplanation.Subs({
hidden_discovered: knownThemes.length.toString(),
total_hidden: hiddenThemes.length.toString(),
})}
</p>
</svelte:fragment>
</ThemesList>

View file

@ -105,24 +105,7 @@ export default class LeftControls extends Combine {
state.featureSwitchBackgroundSelection
)
// If the welcomeMessage is disabled, the copyright is hidden (as that is where the copyright is located
const copyright = new Toggle(
undefined,
new Lazy(() => {
new ScrollableFullScreen(
() => Translations.t.general.attribution.attributionTitle,
() => new CopyrightPanel(state),
"copyright",
guiState.copyrightViewIsOpened
)
return new MapControlButton(Svg.copyright_svg()).onClick(() =>
guiState.copyrightViewIsOpened.setData(true)
)
}),
state.featureSwitchWelcomeMessage
)
super([currentViewAction, filterButton, downloadButton, copyright, mapSwitch])
super([currentViewAction, filterButton, downloadButton, mapSwitch])
this.SetClass("flex flex-col")
}

View file

@ -1,36 +1,25 @@
import { VariableUiElement } from "../Base/VariableUIElement"
import Svg from "../../Svg"
import Combine from "../Base/Combine"
import { SubtleButton } from "../Base/SubtleButton"
import Translations from "../i18n/Translations"
import * as personal from "../../assets/themes/personal/personal.json"
import Constants from "../../Models/Constants"
import BaseUIElement from "../BaseUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { ImmutableStore, Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import LayoutConfig, { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import Loc from "../../Models/Loc"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import Toggle from "../Input/Toggle"
import { Utils } from "../../Utils"
import Title from "../Base/Title"
import * as themeOverview from "../../assets/generated/theme_overview.json"
import themeOverview from "../../assets/generated/theme_overview.json"
import { Translation } from "../i18n/Translation"
import { TextField } from "../Input/TextField"
import FilteredCombine from "../Base/FilteredCombine"
import Locale from "../i18n/Locale"
import SvelteUIElement from "../Base/SvelteUIElement"
import ThemesList from "./ThemesList.svelte"
import HiddenThemeList from "./HiddenThemeList.svelte"
import UnofficialThemeList from "./UnofficialThemeList.svelte"
export default class MoreScreen extends Combine {
private static readonly officialThemes: {
id: string
icon: string
title: any
shortDescription: any
definition?: any
mustHaveLanguage?: boolean
hideFromOverview: boolean
keywors?: any[]
}[] = themeOverview["default"]
private static readonly officialThemes: LayoutInformation[] = themeOverview
constructor(
state: UserRelatedState & {
@ -40,13 +29,6 @@ export default class MoreScreen extends Combine {
onMainScreen: boolean = false
) {
const tr = Translations.t.general.morescreen
let themeButtonStyle = ""
let themeListStyle = ""
if (onMainScreen) {
themeButtonStyle = "h-32 min-h-32 max-h-32 text-ellipsis overflow-hidden"
themeListStyle =
"md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4"
}
const search = new TextField({
placeholder: tr.searchForATheme,
@ -107,38 +89,26 @@ export default class MoreScreen extends Combine {
super([
new Combine([searchBar]).SetClass("flex justify-center"),
MoreScreen.createOfficialThemesList(
new SvelteUIElement(ThemesList, {
state,
themeButtonStyle,
themeListStyle,
search.GetValue()
),
MoreScreen.createPreviouslyVistedHiddenList(
onMainScreen,
search: search.GetValue(),
themes: MoreScreen.officialThemes,
}),
new SvelteUIElement(HiddenThemeList, {
state,
themeButtonStyle,
themeListStyle,
search.GetValue()
),
MoreScreen.createUnofficialThemeList(
themeButtonStyle,
onMainScreen,
search: search.GetValue(),
}),
new SvelteUIElement(UnofficialThemeList, {
state,
themeListStyle,
search.GetValue()
),
onMainScreen,
search: search.GetValue(),
}),
tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10"),
])
}
private static NothingFound(search: UIEventSource<string>): BaseUIElement {
const t = Translations.t.general.morescreen
return new Combine([
new Title(t.noMatchingThemes, 5).SetClass("w-max font-bold"),
new SubtleButton(Svg.search_disable_ui(), t.noSearch, { imgSize: "h-6" })
.SetClass("h-12 w-max")
.onClick(() => search.setData("")),
]).SetClass("flex flex-col items-center w-full")
}
private static createUrlFor(
layout: { id: string; definition?: string },
isCustom: boolean,
@ -239,102 +209,6 @@ export default class MoreScreen extends Combine {
new SubtleButton(undefined, t.button, { url: "./professional.html" }),
]).SetClass("flex flex-col border border-gray-300 p-2 rounded-lg")
}
private static createUnofficialThemeList(
buttonClass: string,
state: UserRelatedState,
themeListClasses: string,
search: UIEventSource<string>
): BaseUIElement {
var currentIds: Store<string[]> = state.installedUserThemes
var stableIds = Stores.ListStabilized<string>(currentIds)
return new VariableUiElement(
stableIds.map((ids) => {
const allThemes: { element: BaseUIElement; predicate?: (s: string) => boolean }[] =
[]
for (const id of ids) {
const themeInfo = state.GetUnofficialTheme(id)
if (themeInfo === undefined) {
continue
}
const link = MoreScreen.createLinkButton(state, themeInfo, true)
if (link !== undefined) {
allThemes.push({
element: link.SetClass(buttonClass),
predicate: (s) => id.toLowerCase().indexOf(s) >= 0,
})
}
}
if (allThemes.length <= 0) {
return undefined
}
return new Combine([
Translations.t.general.customThemeIntro,
new FilteredCombine(allThemes, search, {
innerClasses: themeListClasses,
onEmpty: MoreScreen.NothingFound(search),
}),
])
})
)
}
private static createPreviouslyVistedHiddenList(
state: UserRelatedState,
buttonClass: string,
themeListStyle: string,
search: UIEventSource<string>
): BaseUIElement {
const t = Translations.t.general.morescreen
const prefix = "mapcomplete-hidden-theme-"
const hiddenThemes = themeOverview["default"].filter((layout) => layout.hideFromOverview)
const hiddenTotal = hiddenThemes.length
return new Toggle(
new VariableUiElement(
state.osmConnection.preferencesHandler.preferences.map((allPreferences) => {
const knownThemes: Set<string> = new Set(
Utils.NoNull(
Object.keys(allPreferences)
.filter((key) => key.startsWith(prefix))
.map((key) =>
key.substring(prefix.length, key.length - "-enabled".length)
)
)
)
if (knownThemes.size === 0) {
return undefined
}
const knownThemeDescriptions = hiddenThemes
.filter((theme) => knownThemes.has(theme.id))
.map((theme) => ({
element: MoreScreen.createLinkButton(state, theme)?.SetClass(
buttonClass
),
predicate: MoreScreen.MatchesLayoutFunc(theme),
}))
const knownLayouts = new FilteredCombine(knownThemeDescriptions, search, {
innerClasses: themeListStyle,
onEmpty: MoreScreen.NothingFound(search),
})
return new Combine([
new Title(t.previouslyHiddenTitle),
t.hiddenExplanation.Subs({
hidden_discovered: "" + knownThemes.size,
total_hidden: "" + hiddenTotal,
}),
knownLayouts,
])
})
).SetClass("flex flex-col"),
undefined,
state.osmConnection.isLoggedIn
)
}
private static MatchesLayoutFunc(layout: {
id: string
@ -365,70 +239,4 @@ export default class MoreScreen extends Combine {
return false
}
}
private static createOfficialThemesList(
state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> },
buttonClass: string,
themeListStyle: string,
search: UIEventSource<string>
): BaseUIElement {
let buttons: { element: BaseUIElement; predicate?: (s: string) => boolean }[] =
MoreScreen.officialThemes.map((layout) => {
if (layout === undefined) {
console.trace("Layout is undefined")
return undefined
}
if (layout.hideFromOverview) {
return undefined
}
const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass)
if (layout.id === personal.id) {
const element = new VariableUiElement(
state.osmConnection.userDetails
.map((userdetails) => userdetails.csCount)
.map((csCount) => {
if (csCount < Constants.userJourney.personalLayoutUnlock) {
return undefined
} else {
return button
}
})
)
return { element }
}
return { element: button, predicate: MoreScreen.MatchesLayoutFunc(layout) }
})
const professional = MoreScreen.CreateProffessionalSerivesButton()
const customGeneratorLink = MoreScreen.createCustomGeneratorButton(state)
buttons.splice(0, 0, { element: customGeneratorLink }, { element: professional })
return new FilteredCombine(buttons, search, {
innerClasses: themeListStyle,
onEmpty: MoreScreen.NothingFound(search),
})
}
/*
* Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets
* */
private static createCustomGeneratorButton(state: {
osmConnection: OsmConnection
}): VariableUiElement {
const tr = Translations.t.general.morescreen
return new VariableUiElement(
state.osmConnection.userDetails.map((userDetails) => {
if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) {
return new SubtleButton(null, tr.requestATheme.Clone(), {
url: "https://github.com/pietervdvn/MapComplete/issues",
newTab: true,
})
}
return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme.Clone(), {
url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html",
newTab: false,
})
})
)
}
}

View file

@ -0,0 +1,65 @@
<script lang="ts" context="module">
export interface Theme {
id: string
icon: string
title: any
shortDescription: any
definition?: any
mustHaveLanguage?: boolean
hideFromOverview: boolean
keywords?: any[]
}
</script>
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import Svg from "../../Svg"
import SubtleButton from "../Base/SubtleButton.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations"
export let search: UIEventSource<string>
const t = Translations.t.general.morescreen
</script>
<span>
<h5>{t.noMatchingThemes.toString()}</h5>
<button
on:click={() => {
search.setData("")
}}
>
<span>
<SubtleButton>
<span slot="image">
<ToSvelte construct={Svg.search_disable_ui()} />
</span>
<span slot="message">{t.noSearch.toString()}</span>
</SubtleButton>
</span>
</button>
</span>
<style lang="scss">
span {
@apply flex flex-col items-center w-full;
h5 {
@apply w-max font-bold;
}
// SubtleButton
button {
@apply h-12;
span {
@apply w-max;
:global(img) {
@apply h-6;
}
}
}
}
</style>

View file

@ -0,0 +1,24 @@
<script lang="ts">
import SubtleButton from "../Base/SubtleButton.svelte"
import Title from "../Base/Title"
import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations"
const t = Translations.t.professional.indexPage
</script>
<div>
<ToSvelte construct={new Title(t.hook, 4)} />
<span>
{t.hookMore.toString()}
</span>
<SubtleButton options={{ url: "./professional.html" }}>
<span slot="message">{t.button.toString()}</span>
</SubtleButton>
</div>
<style lang="scss">
div {
@apply flex flex-col border border-gray-300 p-2 rounded-lg;
}
</style>

View file

@ -13,12 +13,10 @@ export default class RightControls extends Combine {
state: MapState & { featurePipeline: FeaturePipeline },
geolocationHandler: GeoLocationHandler
) {
const geolocationButton = new Toggle(
const geolocationButton = Toggle.If(state.featureSwitchGeolocation, () =>
new MapControlButton(new GeolocationControl(geolocationHandler, state), {
dontStyle: true,
}).SetClass("p-1"),
undefined,
state.featureSwitchGeolocation
}).SetClass("p-1")
)
const plus = new MapControlButton(Svg.plus_svg()).onClick(() => {

View file

@ -1,7 +1,7 @@
/**
* Asks to add a feature at the last clicked location, at least if zoom is sufficient
*/
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import Svg from "../../Svg"
import { SubtleButton } from "../Base/SubtleButton"
import Combine from "../Base/Combine"
@ -101,6 +101,9 @@ export default class SimpleAddUI extends LoginToggle {
snapOntoWay?: OsmWay
): Promise<void> {
tags.push(new Tag(Tag.newlyCreated.key, new Date().toISOString()))
if (snapOntoWay) {
tags.push(new Tag("_referencing_ways", "way/" + snapOntoWay.id))
}
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
theme: state.layoutToUse?.id ?? "unkown",
changeType: "create",
@ -283,11 +286,16 @@ export default class SimpleAddUI extends LoginToggle {
const presets = layer.layerDef.presets
for (const preset of presets) {
const tags = TagUtils.KVtoProperties(preset.tags ?? [])
const isSnapping = preset.preciseInput.snapToLayers?.length > 0
let icon: () => BaseUIElement = () =>
layer.layerDef.mapRendering[0]
.GenerateLeafletStyle(new UIEventSource<any>(tags), false, {
noSize: true,
})
.GenerateLeafletStyle(
new ImmutableStore<any>(
isSnapping ? tags : { _referencing_ways: ["way/-1"], ...tags }
),
false,
{ noSize: true }
)
.html.SetClass("w-12 h-12 block relative")
const presetInfo: PresetInfo = {
layerToAddTo: layer,

View file

@ -0,0 +1,110 @@
<script lang="ts">
import SubtleButton from "../Base/SubtleButton.svelte"
import { Translation } from "../i18n/Translation"
import * as personal from "../../assets/themes/personal/personal.json"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Constants from "../../Models/Constants"
import type Loc from "../../Models/Loc"
import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
export let theme: LayoutInformation
export let isCustom: boolean = false
export let userDetails: UIEventSource<UserDetails>
export let state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> }
$: title = new Translation(
theme.title,
!isCustom && !theme.mustHaveLanguage ? "themes:" + theme.id + ".title" : undefined
).toString()
$: description = new Translation(theme.shortDescription).toString()
// TODO: Improve this function
function createUrl(
layout: { id: string; definition?: string },
isCustom: boolean,
state?: { locationControl?: UIEventSource<{ lat; lon; zoom }>; layoutToUse?: { id } }
): Store<string> {
if (layout === undefined) {
return undefined
}
if (layout.id === undefined) {
console.error("ID is undefined for layout", layout)
return undefined
}
if (layout.id === state?.layoutToUse?.id) {
return undefined
}
const currentLocation = state?.locationControl
let path = window.location.pathname
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
path = path.substr(0, path.lastIndexOf("/"))
// Path will now contain '/dir/dir', or empty string in case of nothing
if (path === "") {
path = "."
}
let linkPrefix = `${path}/${layout.id.toLowerCase()}.html?`
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
linkPrefix = `${path}/theme.html?layout=${layout.id}&`
}
if (isCustom) {
linkPrefix = `${path}/theme.html?userlayout=${layout.id}&`
}
let hash = ""
if (layout.definition !== undefined) {
hash = "#" + btoa(JSON.stringify(layout.definition))
}
return (
currentLocation?.map((currentLocation) => {
const params = [
["z", currentLocation?.zoom],
["lat", currentLocation?.lat],
["lon", currentLocation?.lon],
]
.filter((part) => part[1] !== undefined)
.map((part) => part[0] + "=" + part[1])
.join("&")
return `${linkPrefix}${params}${hash}`
}) ?? new ImmutableStore<string>(`${linkPrefix}`)
)
}
</script>
{#if theme.id !== personal.id || $userDetails.csCount > Constants.userJourney.personalLayoutUnlock}
<div>
<SubtleButton options={{ url: createUrl(theme, isCustom, state) }}>
<img slot="image" src={theme.icon} class="block h-11 w-11 bg-red mx-4" alt="" />
<span slot="message" class="message">
<span>
<span>{title}</span>
<span>{description}</span>
</span>
</span>
</SubtleButton>
</div>
{/if}
<style lang="scss">
div {
@apply h-32 min-h-[8rem] max-h-32 text-ellipsis overflow-hidden;
span.message {
@apply flex flex-col justify-center h-24;
& > span {
@apply flex flex-col overflow-hidden;
span:nth-child(2) {
@apply text-[#999];
}
}
}
}
</style>

View file

@ -5,15 +5,12 @@ import Toggle from "../Input/Toggle"
import { SubtleButton } from "../Base/SubtleButton"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { LoginToggle } from "../Popup/LoginButton"
import Svg from "../../Svg"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs"
import LoggedInUserIndicator from "../LoggedInUserIndicator"
import { ActionButtons } from "./ActionButtons"
import { BBox } from "../../Logic/BBox"
import Loc from "../../Models/Loc"
import UserSurveyPanel from "../UserSurveyPanel"
export default class ThemeIntroductionPanel extends Combine {
constructor(
@ -27,7 +24,6 @@ export default class ThemeIntroductionPanel extends Combine {
osmConnection: OsmConnection
currentBounds: Store<BBox>
locationControl: UIEventSource<Loc>
isTranslator: Store<boolean>
},
guistate?: { userInfoIsOpened: UIEventSource<boolean> }
) {
@ -70,7 +66,6 @@ export default class ThemeIntroductionPanel extends Combine {
const hasPresets = layout.layers.some((l) => l.presets?.length > 0)
super([
layout.description.Clone().SetClass("block mb-4"),
new UserSurveyPanel(),
new Combine([
t.welcomeExplanation.general,
hasPresets

View file

@ -0,0 +1,82 @@
<script lang="ts">
import NoThemeResultButton from "./NoThemeResultButton.svelte"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { UIEventSource } from "../../Logic/UIEventSource"
import type Loc from "../../Models/Loc"
import Locale from "../i18n/Locale"
import CustomGeneratorButton from "./CustomGeneratorButton.svelte"
import ProfessionalServicesButton from "./ProfessionalServicesButton.svelte"
import ThemeButton from "./ThemeButton.svelte"
import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
export let search: UIEventSource<string>
export let themes: LayoutInformation[]
export let state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> }
export let isCustom: boolean = false
export let onMainScreen: boolean = true
export let hideThemes: boolean = true
// Filter theme based on search value
$: filteredThemes = themes.filter((theme) => {
if ($search === undefined || $search === "") return true
const srch = $search.toLocaleLowerCase()
if (theme.id.toLowerCase().indexOf(srch) >= 0) {
return true
}
const entitiesToSearch = [theme.shortDescription, theme.title, ...(theme.keywords ?? [])]
for (const entity of entitiesToSearch) {
if (entity === undefined) {
continue
}
const term = entity["*"] ?? entity[Locale.language.data]
if (term?.toLowerCase()?.indexOf(search) >= 0) {
return true
}
}
return false
})
</script>
<section>
<slot name="title" />
{#if onMainScreen}
<div class="md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 gap-4">
{#if ($search === undefined || $search === "") && !isCustom && hideThemes}
<CustomGeneratorButton userDetails={state.osmConnection.userDetails} />
<ProfessionalServicesButton />
{/if}
{#each filteredThemes as theme (theme.id)}
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
<ThemeButton {theme} {isCustom} userDetails={state.osmConnection.userDetails} {state} />
{/if}
{/each}
</div>
{:else}
<div>
{#if ($search === undefined || $search === "") && !isCustom && hideThemes}
<CustomGeneratorButton userDetails={state.osmConnection.userDetails} />
<ProfessionalServicesButton />
{/if}
{#each filteredThemes as theme (theme.id)}
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
<ThemeButton {theme} {isCustom} userDetails={state.osmConnection.userDetails} {state} />
{/if}
{/each}
</div>
{/if}
{#if filteredThemes.length == 0}
<NoThemeResultButton {search} />
{/if}
</section>
<style lang="scss">
section {
@apply flex flex-col;
}
</style>

View file

@ -1,148 +0,0 @@
import Toggle from "../Input/Toggle"
import Lazy from "../Base/Lazy"
import { Utils } from "../../Utils"
import Translations from "../i18n/Translations"
import Combine from "../Base/Combine"
import Locale from "../i18n/Locale"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Translation } from "../i18n/Translation"
import { VariableUiElement } from "../Base/VariableUIElement"
import Link from "../Base/Link"
import LinkToWeblate from "../Base/LinkToWeblate"
import Toggleable from "../Base/Toggleable"
import Title from "../Base/Title"
import { Store } from "../../Logic/UIEventSource"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import * as native_languages from "../../assets/language_native.json"
import BaseUIElement from "../BaseUIElement"
class TranslatorsPanelContent extends Combine {
constructor(layout: LayoutConfig, isTranslator: Store<boolean>) {
const t = Translations.t.translations
const { completeness, untranslated, total } = layout.missingTranslations()
const seed = t.completeness
for (const ln of Array.from(completeness.keys())) {
if (ln === "*") {
continue
}
if (seed.translations[ln] === undefined) {
seed.translations[ln] = seed.translations["en"]
}
}
const completenessTr = {}
const completenessPercentage = {}
seed.SupportedLanguages().forEach((ln) => {
completenessTr[ln] = "" + (completeness.get(ln) ?? 0)
completenessPercentage[ln] =
"" + Math.round((100 * (completeness.get(ln) ?? 0)) / total)
})
function missingTranslationsFor(language: string): BaseUIElement[] {
// e.g. "themes:<themename>.layers.0.tagRenderings..., or "layers:<layername>.description
const missingKeys = Utils.NoNull(untranslated.get(language) ?? [])
.filter((ctx) => ctx.indexOf(":") >= 0)
.map((ctx) => ctx.replace(/note_import_[a-zA-Z0-9_]*/, "note_import"))
const hasMissingTheme = missingKeys.some((k) => k.startsWith("themes:"))
const missingLayers = Utils.Dedup(
missingKeys
.filter((k) => k.startsWith("layers:"))
.map((k) => k.slice("layers:".length).split(".")[0])
)
console.log(
"Getting untranslated string for",
language,
"raw:",
missingKeys,
"hasMissingTheme:",
hasMissingTheme,
"missingLayers:",
missingLayers
)
return Utils.NoNull([
hasMissingTheme
? new Link(
"themes:" + layout.id + ".* (zen mode)",
LinkToWeblate.hrefToWeblateZen(language, "themes", layout.id),
true
)
: undefined,
...missingLayers.map(
(id) =>
new Link(
"layer:" + id + ".* (zen mode)",
LinkToWeblate.hrefToWeblateZen(language, "layers", id),
true
)
),
...missingKeys.map(
(context) =>
new Link(context, LinkToWeblate.hrefToWeblate(language, context), true)
),
])
}
// "translationCompleteness": "Translations for {theme} in {language} are at {percentage}: {translated} out of {total}",
const translated = seed.Subs({
total,
theme: layout.title,
percentage: new Translation(completenessPercentage),
translated: new Translation(completenessTr),
language: seed.OnEveryLanguage((_, lng) => native_languages[lng] ?? lng),
})
super([
new Title(Translations.t.translations.activateButton),
new Toggle(t.isTranslator.SetClass("thanks block"), undefined, isTranslator),
t.help,
translated,
/*Disable button:*/
new SubtleButton(undefined, t.deactivate).onClick(() => {
Locale.showLinkToWeblate.setData(false)
}),
new VariableUiElement(
Locale.language.map((ln) => {
const missing = missingTranslationsFor(ln)
if (missing.length === 0) {
return undefined
}
let title = Translations.t.translations.allMissing
if (untranslated.get(ln) !== undefined) {
title = Translations.t.translations.missing.Subs({
count: untranslated.get(ln).length,
})
}
return new Toggleable(
new Title(title),
new Combine(missing).SetClass("flex flex-col")
)
})
),
])
}
}
export default class TranslatorsPanel extends Toggle {
constructor(
state: { layoutToUse: LayoutConfig; isTranslator: Store<boolean> },
iconStyle?: string
) {
const t = Translations.t.translations
super(
new Lazy(
() => new TranslatorsPanelContent(state.layoutToUse, state.isTranslator)
).SetClass("flex flex-col"),
new SubtleButton(Svg.translate_ui().SetStyle(iconStyle), t.activateButton).onClick(() =>
Locale.showLinkToWeblate.setData(true)
),
Locale.showLinkToWeblate
)
this.SetClass("hidden-on-mobile")
}
}

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import type Loc from "../../Models/Loc"
import { Utils } from "../../Utils"
import ThemesList from "./ThemesList.svelte"
import Translations from "../i18n/Translations"
import UserRelatedState from "../../Logic/State/UserRelatedState"
export let search: UIEventSource<string>
export let state: UserRelatedState & {
osmConnection: OsmConnection
locationControl?: UIEventSource<Loc>
}
export let onMainScreen: boolean = true
const t = Translations.t.general
const currentIds: Store<string[]> = state.installedUserThemes
const stableIds = Stores.ListStabilized<string>(currentIds)
$: customThemes = Utils.NoNull($stableIds.map((id) => state.GetUnofficialTheme(id)))
</script>
<ThemesList
{search}
{state}
{onMainScreen}
themes={customThemes}
isCustom={true}
hideThemes={false}
>
<svelte:fragment slot="title">
<!-- TODO: Change string to exclude html -->
{@html t.customThemeIntro.toString()}
</svelte:fragment>
</ThemesList>

View file

@ -19,11 +19,14 @@ import EditableTagRendering from "../Popup/EditableTagRendering"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import { SaveButton } from "../Popup/SaveButton"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import * as usersettings from "../../assets/generated/layers/usersettings.json"
import 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"
import * as codeContributors from "../../assets/contributors.json"
import translators from "../../assets/translators.json"
import codeContributors from "../../assets/contributors.json"
import Locale from "../i18n/Locale"
import { Utils } from "../../Utils"
import LinkToWeblate from "../Base/LinkToWeblate"
export class ImportViewerLinks extends VariableUiElement {
constructor(osmConnection: OsmConnection) {
@ -53,7 +56,7 @@ class SingleUserSettingsPanel extends EditableTagRendering {
userInfoFocusedQuestion?: UIEventSource<string>
) {
const editMode = new UIEventSource(false)
// Isolate the preferences. THey'll be updated explicitely later on anyway
// Isolate the preferences. They'll be updated explicitely later on anyway
super(
amendedPrefs,
config,
@ -68,7 +71,10 @@ class SingleUserSettingsPanel extends EditableTagRendering {
TagUtils.FlattenAnd(store.data, amendedPrefs.data)
).asChange(amendedPrefs.data)
for (const kv of selection) {
osmConnection.GetPreference(kv.k, "", "").setData(kv.v)
if (kv.k.startsWith("_")) {
continue
}
osmConnection.GetPreference(kv.k, "", { prefix: "" }).setData(kv.v)
}
editMode.setData(false)
@ -104,13 +110,59 @@ class UserInformationMainPanel extends VariableUiElement {
const settings = new UIEventSource<Record<string, BaseUIElement>>({})
const usersettingsConfig = new LayerConfig(usersettings, "userinformationpanel")
const amendedPrefs = new UIEventSource<any>({})
const amendedPrefs = new UIEventSource<any>({ _theme: layout?.id })
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
for (const k in newPrefs) {
amendedPrefs.data[k] = newPrefs[k]
}
amendedPrefs.ping()
})
const translationMode = osmConnection.GetPreference("translation-mode")
Locale.language.mapD(
(language) => {
amendedPrefs.data["_language"] = language
const trmode = translationMode.data
if (trmode === "true" || trmode === "mobile") {
const missing = layout.missingTranslations()
const total = missing.total
const untranslated = missing.untranslated.get(language) ?? []
const hasMissingTheme = untranslated.some((k) => k.startsWith("themes:"))
const missingLayers = Utils.Dedup(
untranslated
.filter((k) => k.startsWith("layers:"))
.map((k) => k.slice("layers:".length).split(".")[0])
)
const zenLinks: { link: string; id: string }[] = Utils.NoNull([
hasMissingTheme
? {
id: "theme:" + layout.id,
link: LinkToWeblate.hrefToWeblateZen(
language,
"themes",
layout.id
),
}
: undefined,
...missingLayers.map((id) => ({
id: "layer:" + id,
link: LinkToWeblate.hrefToWeblateZen(language, "layers", id),
})),
])
const untranslated_count = untranslated.length
amendedPrefs.data["_translation_total"] = "" + total
amendedPrefs.data["_translation_translated_count"] =
"" + (total - untranslated_count)
amendedPrefs.data["_translation_percentage"] =
"" + Math.floor((100 * (total - untranslated_count)) / total)
console.log("Setting zenLinks", zenLinks)
amendedPrefs.data["_translation_links"] = JSON.stringify(zenLinks)
}
amendedPrefs.ping()
},
[translationMode]
)
osmConnection.userDetails.addCallback((userDetails) => {
for (const k in userDetails) {
amendedPrefs.data["_" + k] = "" + userDetails[k]
@ -143,7 +195,7 @@ class UserInformationMainPanel extends VariableUiElement {
return replaced === simplifiedName
}
)
if(isTranslator){
if (isTranslator) {
amendedPrefs.data["_translation_contributions"] = "" + isTranslator.commits
}
const isCodeContributor = codeContributors.contributors.find(
@ -152,7 +204,7 @@ class UserInformationMainPanel extends VariableUiElement {
return replaced === simplifiedName
}
)
if(isCodeContributor){
if (isCodeContributor) {
amendedPrefs.data["_code_contributions"] = "" + isCodeContributor.commits
}
amendedPrefs.ping()