Merge feature/svelte into develop

This commit is contained in:
Pieter Vander Vennet 2023-03-08 19:02:41 +01:00
commit 868d476891
120 changed files with 5168 additions and 10657 deletions

54
UI/AllTagsPanel.svelte Normal file
View file

@ -0,0 +1,54 @@
<script lang="ts">
import ToSvelte from "./Base/ToSvelte.svelte"
import Table from "./Base/Table"
import { UIEventSource } from "../Logic/UIEventSource"
//Svelte props
export let tags: UIEventSource<any>
export let state: any
const calculatedTags = [].concat(
// SimpleMetaTagger.lazyTags,
...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? [])
)
const allTags = tags.map((tags) => {
const parts = []
for (const key in tags) {
if (!tags.hasOwnProperty(key)) {
continue
}
let v = tags[key]
if (v === "") {
v = "<b>empty string</b>"
}
parts.push([key, v ?? "<b>undefined</b>"])
}
for (const key of calculatedTags) {
const value = tags[key]
if (value === undefined) {
continue
}
let type = ""
if (typeof value !== "string") {
type = " <i>" + typeof value + "</i>"
}
parts.push(["<i>" + key + "</i>", value])
}
return parts
})
const tagsTable = new Table(["Key", "Value"], $allTags).SetClass("zebra-table")
</script>
<section>
<ToSvelte construct={tagsTable} />
</section>
<style lang="scss">
section {
@apply border border-solid border-black rounded-2xl p-4 block;
}
</style>

View file

@ -1,47 +0,0 @@
import { VariableUiElement } from "./Base/VariableUIElement"
import { UIEventSource } from "../Logic/UIEventSource"
import Table from "./Base/Table"
export class AllTagsPanel extends VariableUiElement {
constructor(tags: UIEventSource<any>, state?) {
const calculatedTags = [].concat(
// SimpleMetaTagger.lazyTags,
...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ??
[])
)
super(
tags.map((tags) => {
const parts = []
for (const key in tags) {
if (!tags.hasOwnProperty(key)) {
continue
}
let v = tags[key]
if (v === "") {
v = "<b>empty string</b>"
}
parts.push([key, v ?? "<b>undefined</b>"])
}
for (const key of calculatedTags) {
const value = tags[key]
if (value === undefined) {
continue
}
let type = ""
if (typeof value !== "string") {
type = " <i>" + typeof value + "</i>"
}
parts.push(["<i>" + key + "</i>", value])
}
return new Table(["key", "value"], parts)
.SetStyle(
"border: 1px solid black; border-radius: 1em;padding:1em;display:block;"
)
.SetClass("zebra-table")
})
)
}
}

View file

@ -3,7 +3,6 @@ import Loc from "../../Models/Loc"
import BaseLayer from "../../Models/BaseLayer"
import { UIEventSource } from "../../Logic/UIEventSource"
import { BBox } from "../../Logic/BBox"
import { deprecate } from "util"
export interface MinimapOptions {
background?: UIEventSource<BaseLayer>

View file

@ -23,7 +23,7 @@ import StrayClickHandler from "../../Logic/Actors/StrayClickHandler"
* The stray-click-hanlders adds a marker to the map if no feature was clicked.
* Shows the given uiToShow-element in the messagebox
*/
export class StrayClickHandlerImplementation {
class StrayClickHandlerImplementation {
private _lastMarker
constructor(
@ -91,6 +91,7 @@ export class StrayClickHandlerImplementation {
})
}
}
export default class MinimapImplementation extends BaseUIElement implements MinimapObj {
private static _nextId = 0
public readonly leafletMap: UIEventSource<Map>

View file

@ -0,0 +1,82 @@
<script lang="ts">
import { onMount } from "svelte";
import { Store } from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
import Img from "./Img";
import Translations from "../i18n/Translations";
import { ImmutableStore } from "../../Logic/UIEventSource.js";
export let imageUrl: string | BaseUIElement = undefined;
export let message: string | BaseUIElement = undefined;
export let options: {
url?: string | Store<string>
newTab?: boolean
imgSize?: string
extraClasses?: string
} = {};
// Website to open when clicked
let href : Store<string> = undefined
if(options?.url){
href = typeof options?.url == "string" ? new ImmutableStore(options.url) : options.url;
}
let imgElem: HTMLElement;
let msgElem: HTMLElement;
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11");
onMount(() => {
// Image
if (imgElem && imageUrl) {
let img: BaseUIElement;
if ((imageUrl ?? "") === "") {
img = undefined;
} else if (typeof imageUrl !== "string") {
img = imageUrl?.SetClass(imgClasses);
}
if (img) imgElem.replaceWith(img.ConstructElement());
}
// Message
if (msgElem && message) {
let msg = Translations.W(message)?.SetClass("block text-ellipsis no-images flex-shrink");
msgElem.replaceWith(msg.ConstructElement());
}
});
</script>
<svelte:element
class={(options.extraClasses??"") + 'flex hover:shadow-xl transition-[color,background-color,box-shadow] hover:bg-unsubtle'}
href={$href}
target={options?.newTab ? "_blank" : ""}
this={href === undefined ? "span" : "a"}
>
<slot name="image">
{#if imageUrl !== undefined}
{#if typeof imageUrl === "string"}
<Img src={imageUrl} class={imgClasses+ " bg-red border border-black"}></Img>
{:else }
<template bind:this={imgElem} />
{/if}
{/if}
</slot>
<slot name="message">
<template bind:this={msgElem} />
</slot>
</svelte:element>
<style lang="scss">
span,
a {
@apply flex p-3 my-2 py-4 rounded-lg shrink-0;
@apply items-center w-full no-underline;
@apply bg-subtle text-black;
:global(span) {
@apply block text-ellipsis;
}
}
</style>

View file

@ -1,13 +1,11 @@
import Translations from "../i18n/Translations"
import Combine from "./Combine"
import BaseUIElement from "../BaseUIElement"
import Link from "./Link"
import Img from "./Img"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { UIElement } from "../UIElement"
import { VariableUiElement } from "./VariableUIElement"
import Lazy from "./Lazy"
import Loading from "./Loading"
import SubtleButtonSvelte from "./SubtleButton.svelte"
import SvelteUIElement from "./SvelteUIElement"
export class SubtleButton extends UIElement {
private readonly imageUrl: string | BaseUIElement
@ -15,7 +13,7 @@ export class SubtleButton extends UIElement {
private readonly options: {
url?: string | Store<string>
newTab?: boolean
imgSize?: string
imgSize?: string,
extraClasses?: string
}
@ -25,9 +23,9 @@ export class SubtleButton extends UIElement {
options: {
url?: string | Store<string>
newTab?: boolean
imgSize?: "h-11 w-11" | string
imgSize?: "h-11 w-11" | string,
extraClasses?: string
} = undefined
} = {}
) {
super()
this.imageUrl = imageUrl
@ -36,30 +34,11 @@ export class SubtleButton extends UIElement {
}
protected InnerRender(): string | BaseUIElement {
const classes =
"block flex p-3 my-2 bg-subtle rounded-lg hover:shadow-xl hover:bg-unsubtle transition-colors transition-shadow link-no-underline " +
(this?.options?.extraClasses ?? "")
const message = Translations.W(this.message)?.SetClass(
"block text-ellipsis no-images flex-shrink"
)
let img
const imgClasses =
"block justify-center flex-none mr-4 " + (this.options?.imgSize ?? "h-11 w-11")
if ((this.imageUrl ?? "") === "") {
img = undefined
} else if (typeof this.imageUrl === "string") {
img = new Img(this.imageUrl)?.SetClass(imgClasses)
} else {
img = this.imageUrl?.SetClass(imgClasses)
}
const button = new Combine([img, message]).SetClass("flex items-center group w-full")
if (this.options?.url == undefined) {
this.SetClass(classes)
return button
}
return new Link(button, this.options.url, this.options.newTab ?? false).SetClass(classes)
return new SvelteUIElement(SubtleButtonSvelte, {
imageUrl: this?.imageUrl ?? undefined,
message: this?.message ?? "",
options: this?.options ?? {},
})
}
public OnClickWithLoading(

View file

@ -0,0 +1,37 @@
import BaseUIElement from "../BaseUIElement"
import { SvelteComponentTyped } from "svelte"
/**
* The SvelteUIComponent serves as a translating class which which wraps a SvelteElement into the BaseUIElement framework.
*/
export default class SvelteUIElement<
Props extends Record<string, any> = any,
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> extends BaseUIElement {
private readonly _svelteComponent: {
new (args: {
target: HTMLElement
props: Props
events?: Events
slots?: Slots
}): SvelteComponentTyped<Props, Events, Slots>
}
private readonly _props: Props
constructor(svelteElement, props: Props) {
super()
this._svelteComponent = svelteElement
this._props = props
}
protected InnerConstructElement(): HTMLElement {
const el = document.createElement("div")
new this._svelteComponent({
target: el,
props: this._props,
})
return el
}
}

18
UI/Base/ToSvelte.svelte Normal file
View file

@ -0,0 +1,18 @@
<script lang="ts">
import BaseUIElement from "../BaseUIElement.js"
import { onMount } from "svelte"
export let construct: BaseUIElement | (() => BaseUIElement)
let elem: HTMLElement
onMount(() => {
let html =
typeof construct === "function"
? construct().ConstructElement()
: construct.ConstructElement()
elem.replaceWith(html)
})
</script>
<span bind:this={elem} />

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

@ -123,7 +123,9 @@ export default class CopyrightPanel extends Combine {
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

@ -0,0 +1,40 @@
<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={true} 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

@ -1,35 +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 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 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
private static readonly officialThemes: LayoutInformation[] = themeOverview
constructor(
state: UserRelatedState & {
@ -39,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,
@ -106,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,
@ -238,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.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
@ -364,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") {
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

@ -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

@ -0,0 +1,83 @@
<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}
<CustomGeneratorButton userDetails={state.osmConnection.userDetails} />
<ProfessionalServicesButton />
{/if}
{#each filteredThemes as theme}
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
<ThemeButton {theme} {isCustom} userDetails={state.osmConnection.userDetails} {state} />
{/if}
{/each}
</div>
{:else }
<div>
{#if ($search === undefined || $search === "") && !isCustom}
<CustomGeneratorButton userDetails={state.osmConnection.userDetails} />
<ProfessionalServicesButton />
{/if}
{#each filteredThemes as theme}
{#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

@ -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

@ -34,6 +34,8 @@ import { GeoLocationState } from "../Logic/State/GeoLocationState"
import Hotkeys from "./Base/Hotkeys"
import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers"
import CopyrightPanel from "./BigComponents/CopyrightPanel"
import SvelteUIElement from "./Base/SvelteUIElement"
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
/**
* The default MapComplete GUI initializer
@ -237,6 +239,20 @@ export default class DefaultGUI {
const welcomeMessageMapControl = Toggle.If(state.featureSwitchWelcomeMessage, () =>
self.InitWelcomeMessage()
)
const communityIndex = Toggle.If(state.featureSwitchCommunityIndex, () => {
const communityIndexControl = new MapControlButton(Svg.community_svg())
const communityIndex = new ScrollableFullScreen(
() => Translations.t.communityIndex.title,
() => new SvelteUIElement(CommunityIndexView, { ...state }),
"community_index"
)
communityIndexControl.onClick(() => {
communityIndex.Activate()
})
return communityIndexControl
})
const testingBadge = Toggle.If(state.featureSwitchIsTesting, () =>
new FixedUiElement("TESTING").SetClass("alert m-2 border-2 border-black")
)
@ -253,6 +269,7 @@ export default class DefaultGUI {
welcomeMessageMapControl,
userInfoMapControl,
copyright,
communityIndex,
extraLink,
testingBadge,
])

View file

@ -23,17 +23,22 @@ import { FlowStep } from "./FlowStep"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import Title from "../Base/Title"
import CheckBoxes from "../Input/Checkboxes"
import { AllTagsPanel } from "../AllTagsPanel"
import AllTagsPanel from "../AllTagsPanel.svelte"
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
import { Feature, Point } from "geojson"
import DivContainer from "../Base/DivContainer"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import SvelteUIElement from "../Base/SvelteUIElement"
class PreviewPanel extends ScrollableFullScreen {
constructor(tags: UIEventSource<any>) {
super(
(_) => new FixedUiElement("Element to import"),
(_) => new Combine(["The tags are:", new AllTagsPanel(tags)]).SetClass("flex flex-col"),
(_) =>
new Combine([
"The tags are:",
new SvelteUIElement(AllTagsPanel, { tags }),
]).SetClass("flex flex-col"),
"element"
)
}

View file

@ -20,7 +20,7 @@ import { CloseNoteButton } from "./Popup/CloseNoteButton"
import { NearbyImageVis } from "./Popup/NearbyImageVis"
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
import { Stores, UIEventSource } from "../Logic/UIEventSource"
import { AllTagsPanel } from "./AllTagsPanel"
import AllTagsPanel from "./AllTagsPanel.svelte"
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"
import { ImageCarousel } from "./Image/ImageCarousel"
import { ImageUploadFlow } from "./Image/ImageUploadFlow"
@ -53,6 +53,7 @@ import AutoApplyButton from "./Popup/AutoApplyButton"
import { LanguageElement } from "./Popup/LanguageElement"
import FeatureReviews from "../Logic/Web/MangroveReviews"
import Maproulette from "../Logic/Maproulette"
import SvelteUIElement from "./Base/SvelteUIElement"
export default class SpecialVisualizations {
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
@ -160,7 +161,8 @@ export default class SpecialVisualizations {
funcName: "all_tags",
docs: "Prints all key-value pairs of the object - used for debugging",
args: [],
constr: (state, tags: UIEventSource<any>) => new AllTagsPanel(tags, state),
constr: (state, tags: UIEventSource<any>) =>
new SvelteUIElement(AllTagsPanel, { tags, state }),
},
{
funcName: "image_carousel",

View file

@ -2,6 +2,7 @@ import Locale from "./Locale"
import { Utils } from "../../Utils"
import BaseUIElement from "../BaseUIElement"
import LinkToWeblate from "../Base/LinkToWeblate"
import { SvelteComponent } from "svelte"
export class Translation extends BaseUIElement {
public static forcedLanguage = undefined