Merge develop
This commit is contained in:
commit
e67d740769
719 changed files with 30508 additions and 15682 deletions
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
45
UI/BigComponents/CommunityIndexView.svelte
Normal file
45
UI/BigComponents/CommunityIndexView.svelte
Normal 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>
|
50
UI/BigComponents/ContactLink.svelte
Normal file
50
UI/BigComponents/ContactLink.svelte
Normal 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>
|
|
@ -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 !== "") {
|
||||
|
|
38
UI/BigComponents/CustomGeneratorButton.svelte
Normal file
38
UI/BigComponents/CustomGeneratorButton.svelte
Normal 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>
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
47
UI/BigComponents/HiddenThemeList.svelte
Normal file
47
UI/BigComponents/HiddenThemeList.svelte
Normal 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>
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
65
UI/BigComponents/NoThemeResultButton.svelte
Normal file
65
UI/BigComponents/NoThemeResultButton.svelte
Normal 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>
|
24
UI/BigComponents/ProfessionalServicesButton.svelte
Normal file
24
UI/BigComponents/ProfessionalServicesButton.svelte
Normal 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>
|
|
@ -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(() => {
|
||||
|
|
|
@ -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,
|
||||
|
|
110
UI/BigComponents/ThemeButton.svelte
Normal file
110
UI/BigComponents/ThemeButton.svelte
Normal 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>
|
|
@ -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
|
||||
|
|
82
UI/BigComponents/ThemesList.svelte
Normal file
82
UI/BigComponents/ThemesList.svelte
Normal 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>
|
|
@ -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")
|
||||
}
|
||||
}
|
35
UI/BigComponents/UnofficialThemeList.svelte
Normal file
35
UI/BigComponents/UnofficialThemeList.svelte
Normal 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>
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue