forked from MapComplete/MapComplete
Port user settings
This commit is contained in:
parent
97aaa8e941
commit
8085079eff
30 changed files with 566 additions and 497 deletions
|
@ -2,10 +2,17 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
|||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
import { MangroveIdentity } from "../Web/MangroveReviews"
|
||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import { FeatureSource } from "../FeatureSource/FeatureSource"
|
||||
import { Feature } from "geojson"
|
||||
import { Utils } from "../../Utils"
|
||||
import translators from "../../assets/translators.json"
|
||||
import codeContributors from "../../assets/contributors.json"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import usersettings from "../../assets/generated/layers/usersettings.json"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import LinkToWeblate from "../../UI/Base/LinkToWeblate"
|
||||
|
||||
/**
|
||||
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
|
||||
|
@ -30,16 +37,30 @@ export default class UserRelatedState {
|
|||
* The number of seconds that the GPS-locations are stored in memory.
|
||||
* Time in seconds
|
||||
*/
|
||||
public gpsLocationHistoryRetentionTime = new UIEventSource(
|
||||
public readonly gpsLocationHistoryRetentionTime = new UIEventSource(
|
||||
7 * 24 * 60 * 60,
|
||||
"gps_location_retention"
|
||||
)
|
||||
|
||||
constructor(osmConnection: OsmConnection, availableLanguages?: string[]) {
|
||||
/**
|
||||
* Preferences as tags exposes many preferences and state properties as record.
|
||||
* This is used to bridge the internal state with the usersettings.json layerconfig file
|
||||
*/
|
||||
public readonly preferencesAsTags: UIEventSource<Record<string, string>>
|
||||
public static readonly usersettingsConfig = new LayerConfig(
|
||||
<LayerConfigJson>usersettings,
|
||||
"userinformationpanel"
|
||||
)
|
||||
|
||||
constructor(
|
||||
osmConnection: OsmConnection,
|
||||
availableLanguages?: string[],
|
||||
layout?: LayoutConfig
|
||||
) {
|
||||
this.osmConnection = osmConnection
|
||||
{
|
||||
const translationMode: UIEventSource<undefined | "true" | "false" | "mobile" | string> =
|
||||
this.osmConnection.GetPreference("translation-mode")
|
||||
this.osmConnection.GetPreference("translation-mode", "false")
|
||||
translationMode.addCallbackAndRunD((mode) => {
|
||||
mode = mode.toLowerCase()
|
||||
if (mode === "true" || mode === "yes") {
|
||||
|
@ -73,6 +94,8 @@ export default class UserRelatedState {
|
|||
this.installedUserThemes = this.InitInstalledUserThemes()
|
||||
|
||||
this.homeLocation = this.initHomeLocation()
|
||||
|
||||
this.preferencesAsTags = this.initAmendedPrefs(layout)
|
||||
}
|
||||
|
||||
public GetUnofficialTheme(id: string):
|
||||
|
@ -211,4 +234,127 @@ export default class UserRelatedState {
|
|||
})
|
||||
return new StaticFeatureSource(feature)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the 'amended preferences'.
|
||||
* This is inherently a dirty and chaotic method, as it shoves many properties into this EventSourcd
|
||||
* */
|
||||
private initAmendedPrefs(layout?: LayoutConfig): UIEventSource<Record<string, string>> {
|
||||
const amendedPrefs = new UIEventSource<Record<string, string>>({
|
||||
_theme: layout?.id,
|
||||
_backend: this.osmConnection.Backend(),
|
||||
})
|
||||
|
||||
const osmConnection = this.osmConnection
|
||||
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
|
||||
for (const k in newPrefs) {
|
||||
amendedPrefs.data[k] = newPrefs[k]
|
||||
}
|
||||
amendedPrefs.ping()
|
||||
})
|
||||
const usersettingsConfig = UserRelatedState.usersettingsConfig
|
||||
const translationMode = osmConnection.GetPreference("translation-mode")
|
||||
Locale.language.mapD(
|
||||
(language) => {
|
||||
amendedPrefs.data["_language"] = language
|
||||
const trmode = translationMode.data
|
||||
if ((trmode === "true" || trmode === "mobile") && layout !== undefined) {
|
||||
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]
|
||||
}
|
||||
|
||||
for (const [name, code, _] of usersettingsConfig.calculatedTags) {
|
||||
try {
|
||||
let result = new Function("feat", "return " + code + ";")({
|
||||
properties: amendedPrefs.data,
|
||||
})
|
||||
if (result !== undefined && result !== "" && result !== null) {
|
||||
if (typeof result !== "string") {
|
||||
result = JSON.stringify(result)
|
||||
}
|
||||
amendedPrefs.data[name] = result
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Calculating a tag for userprofile-settings failed for variable",
|
||||
name,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const simplifiedName = userDetails.name.toLowerCase().replace(/\s+/g, "")
|
||||
const isTranslator = translators.contributors.find(
|
||||
(c: { contributor: string; commits: number }) => {
|
||||
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
|
||||
return replaced === simplifiedName
|
||||
}
|
||||
)
|
||||
if (isTranslator) {
|
||||
amendedPrefs.data["_translation_contributions"] = "" + isTranslator.commits
|
||||
}
|
||||
const isCodeContributor = codeContributors.contributors.find(
|
||||
(c: { contributor: string; commits: number }) => {
|
||||
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
|
||||
return replaced === simplifiedName
|
||||
}
|
||||
)
|
||||
if (isCodeContributor) {
|
||||
amendedPrefs.data["_code_contributions"] = "" + isCodeContributor.commits
|
||||
}
|
||||
amendedPrefs.ping()
|
||||
})
|
||||
|
||||
amendedPrefs.addCallbackD((tags) => {
|
||||
for (const key in tags) {
|
||||
if (key.startsWith("_")) {
|
||||
continue
|
||||
}
|
||||
this.osmConnection.GetPreference(key, undefined, { prefix: "" }).setData(tags[key])
|
||||
}
|
||||
})
|
||||
|
||||
return amendedPrefs
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,7 +123,6 @@ export default class FilteredLayer {
|
|||
} else {
|
||||
properties = fieldstate
|
||||
}
|
||||
console.log("Building tagsspec with properties", properties)
|
||||
const missingKeys = option.fields
|
||||
.map((f) => f.name)
|
||||
.filter((key) => properties[key] === undefined)
|
||||
|
@ -182,7 +181,6 @@ export default class FilteredLayer {
|
|||
// We calculate the fields
|
||||
const fieldProperties = FilteredLayer.stringToFieldProperties(<string>state.data)
|
||||
const asTags = FilteredLayer.fieldsToTags(filter.options[0], fieldProperties)
|
||||
console.log("Current field properties:", state.data, fieldProperties, asTags)
|
||||
if (asTags) {
|
||||
needed.push(asTags)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ export class MenuState {
|
|||
public readonly highlightedLayerInFilters: UIEventSource<string> = new UIEventSource<string>(
|
||||
undefined
|
||||
)
|
||||
public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
constructor() {
|
||||
this.themeViewTabIndex = new UIEventSource(0)
|
||||
this.themeViewTab = this.themeViewTabIndex.sync(
|
||||
|
@ -57,6 +58,12 @@ export class MenuState {
|
|||
}
|
||||
}
|
||||
|
||||
public openUsersettings(highlightTagRendering?: string) {
|
||||
this.menuIsOpened.setData(true)
|
||||
this.menuViewTab.setData("settings")
|
||||
this.highlightedUserSetting.setData(highlightTagRendering)
|
||||
}
|
||||
|
||||
public closeAll() {
|
||||
this.menuIsOpened.setData(false)
|
||||
this.themeIsOpened.setData(false)
|
||||
|
|
|
@ -805,7 +805,13 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
const param = special[arg.name]
|
||||
if (param === undefined) {
|
||||
errors.push(
|
||||
`At ${context}: Obligated parameter '${arg.name}' in special rendering of type ${vis.funcName} not found.\n${arg.doc}`
|
||||
`At ${context}: Obligated parameter '${
|
||||
arg.name
|
||||
}' in special rendering of type ${
|
||||
vis.funcName
|
||||
} not found.\n The full special rendering specification is: '${JSON.stringify(
|
||||
input
|
||||
)}'\n ${arg.name}: ${arg.doc}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
),
|
||||
osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data,
|
||||
})
|
||||
this.userRelatedState = new UserRelatedState(this.osmConnection, layout?.language)
|
||||
this.userRelatedState = new UserRelatedState(this.osmConnection, layout?.language, layout)
|
||||
this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element")
|
||||
this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer")
|
||||
this.geolocation = new GeoLocationHandler(
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
const offlineModes: Partial<Record<OsmServiceState, Translation>> = {
|
||||
offline: t.loginFailedOfflineMode,
|
||||
unreachable: t.loginFailedUnreachableMode,
|
||||
unknown: t.loginFailedUnreachableMode,
|
||||
readonly: t.loginFailedReadonlyMode
|
||||
};
|
||||
const apiState = state.osmConnection.apiIsOnline;
|
||||
|
|
|
@ -7,15 +7,17 @@
|
|||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
export let selectedElement: Feature;
|
||||
export let state: SpecialVisualizationState;
|
||||
export let layer: LayerConfig;
|
||||
export let selectedElement: Feature;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let highlightedRendering: UIEventSource<string> = undefined;
|
||||
|
||||
|
||||
let _tags: Record<string, string>;
|
||||
onDestroy(tags.addCallbackAndRun(tags => {
|
||||
_tags = tags;
|
||||
}));
|
||||
export let state: SpecialVisualizationState;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
@ -40,7 +42,7 @@
|
|||
{#each layer.tagRenderings as config (config.id)}
|
||||
{#if config.condition === undefined || config.condition.matchesProperties(_tags)}
|
||||
{#if config.IsKnown(_tags)}
|
||||
<TagRenderingEditable {tags} {config} {state} {selectedElement} {layer}></TagRenderingEditable>
|
||||
<TagRenderingEditable {tags} {config} {state} {selectedElement} {layer} {highlightedRendering}></TagRenderingEditable>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Combine from "../Base/Combine"
|
||||
|
@ -8,25 +7,13 @@ import { VariableUiElement } from "../Base/VariableUIElement"
|
|||
import Img from "../Base/Img"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import Link from "../Base/Link"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Showdown from "showdown"
|
||||
import LanguagePicker from "../LanguagePicker"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import Constants from "../../Models/Constants"
|
||||
import EditableTagRendering from "../Popup/EditableTagRendering"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import { SaveButton } from "../Popup/SaveButton"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import usersettings from "../../assets/generated/layers/usersettings.json"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
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) {
|
||||
|
@ -48,51 +35,6 @@ export class ImportViewerLinks extends VariableUiElement {
|
|||
}
|
||||
}
|
||||
|
||||
class SingleUserSettingsPanel extends EditableTagRendering {
|
||||
constructor(
|
||||
config: TagRenderingConfig,
|
||||
osmConnection: OsmConnection,
|
||||
amendedPrefs: UIEventSource<any>,
|
||||
userInfoFocusedQuestion?: UIEventSource<string>
|
||||
) {
|
||||
const editMode = new UIEventSource(false)
|
||||
// Isolate the preferences. They'll be updated explicitely later on anyway
|
||||
super(
|
||||
amendedPrefs,
|
||||
config,
|
||||
[],
|
||||
{ osmConnection },
|
||||
{
|
||||
answerElementClasses: "p-2",
|
||||
editMode,
|
||||
createSaveButton: (store) =>
|
||||
new SaveButton(amendedPrefs, osmConnection).onClick(() => {
|
||||
const selection = TagUtils.FlattenMultiAnswer(
|
||||
TagUtils.FlattenAnd(store.data, amendedPrefs.data)
|
||||
).asChange(amendedPrefs.data)
|
||||
for (const kv of selection) {
|
||||
if (kv.k.startsWith("_")) {
|
||||
continue
|
||||
}
|
||||
osmConnection.GetPreference(kv.k, "", { prefix: "" }).setData(kv.v)
|
||||
}
|
||||
|
||||
editMode.setData(false)
|
||||
}),
|
||||
}
|
||||
)
|
||||
const self = this
|
||||
this.SetClass("rounded-xl")
|
||||
userInfoFocusedQuestion.addCallbackAndRun((selected) => {
|
||||
if (config.id !== selected) {
|
||||
self.RemoveClass("glowing-shadow")
|
||||
} else {
|
||||
self.SetClass("glowing-shadow")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class UserInformationMainPanel extends VariableUiElement {
|
||||
private readonly settings: UIEventSource<Record<string, BaseUIElement>>
|
||||
private readonly userInfoFocusedQuestion?: UIEventSource<string>
|
||||
|
@ -104,210 +46,9 @@ class UserInformationMainPanel extends VariableUiElement {
|
|||
isOpened: UIEventSource<boolean>,
|
||||
userInfoFocusedQuestion?: UIEventSource<string>
|
||||
) {
|
||||
const t = Translations.t.userinfo
|
||||
const imgSize = "h-6 w-6"
|
||||
const ud = osmConnection.userDetails
|
||||
const settings = new UIEventSource<Record<string, BaseUIElement>>({})
|
||||
const usersettingsConfig = new LayerConfig(usersettings, "userinformationpanel")
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
for (const [name, code, _] of usersettingsConfig.calculatedTags) {
|
||||
try {
|
||||
let result = new Function("feat", "return " + code + ";")({
|
||||
properties: amendedPrefs.data,
|
||||
})
|
||||
if (result !== undefined && result !== "" && result !== null) {
|
||||
if (typeof result !== "string") {
|
||||
result = JSON.stringify(result)
|
||||
}
|
||||
amendedPrefs.data[name] = result
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Calculating a tag for userprofile-settings failed for variable",
|
||||
name,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const simplifiedName = userDetails.name.toLowerCase().replace(/\s+/g, "")
|
||||
const isTranslator = translators.contributors.find(
|
||||
(c: { contributor: string; commits: number }) => {
|
||||
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
|
||||
return replaced === simplifiedName
|
||||
}
|
||||
)
|
||||
if (isTranslator) {
|
||||
amendedPrefs.data["_translation_contributions"] = "" + isTranslator.commits
|
||||
}
|
||||
const isCodeContributor = codeContributors.contributors.find(
|
||||
(c: { contributor: string; commits: number }) => {
|
||||
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
|
||||
return replaced === simplifiedName
|
||||
}
|
||||
)
|
||||
if (isCodeContributor) {
|
||||
amendedPrefs.data["_code_contributions"] = "" + isCodeContributor.commits
|
||||
}
|
||||
amendedPrefs.ping()
|
||||
})
|
||||
|
||||
super(
|
||||
ud.map((ud) => {
|
||||
let img: BaseUIElement = Svg.person_ui().SetClass("block")
|
||||
if (ud.img !== undefined) {
|
||||
img = new Img(ud.img)
|
||||
}
|
||||
img.SetClass("rounded-full h-12 w-12 m-4")
|
||||
|
||||
let description: BaseUIElement = undefined
|
||||
const editLink = osmConnection.Backend() + "/profile/edit"
|
||||
if (ud.description) {
|
||||
const editButton = new Link(
|
||||
Svg.pencil_svg().SetClass("h-4 w-4"),
|
||||
editLink,
|
||||
true
|
||||
).SetClass(
|
||||
"absolute block bg-subtle rounded-full p-2 bottom-2 right-2 w-min self-end"
|
||||
)
|
||||
|
||||
const htmlString = new Showdown.Converter()
|
||||
.makeHtml(ud.description)
|
||||
.replace(/>/g, ">")
|
||||
.replace(/</g, "<")
|
||||
description = new Combine([
|
||||
new FixedUiElement(htmlString).SetClass("link-underline"),
|
||||
editButton,
|
||||
]).SetClass("relative w-full m-2")
|
||||
} else {
|
||||
description = new Combine([
|
||||
t.noDescription,
|
||||
new SubtleButton(Svg.pencil_svg(), t.noDescriptionCallToAction, {
|
||||
imgSize,
|
||||
url: editLink,
|
||||
newTab: true,
|
||||
}),
|
||||
]).SetClass("w-full m-2")
|
||||
}
|
||||
|
||||
let panToHome: BaseUIElement
|
||||
if (ud.home) {
|
||||
panToHome = new SubtleButton(Svg.home_svg(), t.moveToHome, {
|
||||
imgSize,
|
||||
}).onClick(() => {
|
||||
const home = ud?.home
|
||||
if (home === undefined) {
|
||||
return
|
||||
}
|
||||
locationControl.setData({ ...home, zoom: 16 })
|
||||
isOpened.setData(false)
|
||||
})
|
||||
}
|
||||
|
||||
const settingElements = []
|
||||
for (const c of usersettingsConfig.tagRenderings) {
|
||||
const settingsPanel = new SingleUserSettingsPanel(
|
||||
c,
|
||||
osmConnection,
|
||||
amendedPrefs,
|
||||
userInfoFocusedQuestion
|
||||
).SetClass("block my-4")
|
||||
settings.data[c.id] = settingsPanel
|
||||
settingElements.push(settingsPanel)
|
||||
}
|
||||
settings.ping()
|
||||
|
||||
return new Combine([
|
||||
new Combine([img, description]).SetClass("flex border border-black rounded-md"),
|
||||
new LanguagePicker(
|
||||
layout.language,
|
||||
Translations.t.general.pickLanguage.Clone()
|
||||
),
|
||||
...settingElements,
|
||||
new SubtleButton(
|
||||
Svg.envelope_svg(),
|
||||
new Combine([
|
||||
t.gotoInbox,
|
||||
ud.unreadMessages == 0
|
||||
? undefined
|
||||
: t.newMessages.SetClass("alert block"),
|
||||
]),
|
||||
{ imgSize, url: `${ud.backend}/messages/inbox`, newTab: true }
|
||||
),
|
||||
new SubtleButton(Svg.gear_svg(), t.gotoSettings, {
|
||||
imgSize,
|
||||
url: `${ud.backend}/user/${encodeURIComponent(ud.name)}/account`,
|
||||
newTab: true,
|
||||
}),
|
||||
panToHome,
|
||||
new ImportViewerLinks(osmConnection),
|
||||
new SubtleButton(Svg.logout_svg(), Translations.t.general.logout, {
|
||||
imgSize,
|
||||
}).onClick(() => {
|
||||
osmConnection.LogOut()
|
||||
}),
|
||||
])
|
||||
})
|
||||
)
|
||||
this.SetClass("flex flex-col")
|
||||
super()
|
||||
this.settings = settings
|
||||
this.userInfoFocusedQuestion = userInfoFocusedQuestion
|
||||
const self = this
|
||||
|
@ -325,50 +66,3 @@ class UserInformationMainPanel extends VariableUiElement {
|
|||
this.settings.data[focusedId]?.ScrollIntoView()
|
||||
}
|
||||
}
|
||||
|
||||
export default class UserInformationPanel extends ScrollableFullScreen {
|
||||
private readonly userPanel: UserInformationMainPanel
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
readonly layoutToUse: LayoutConfig
|
||||
readonly osmConnection: OsmConnection
|
||||
readonly locationControl: UIEventSource<Loc>
|
||||
readonly featureSwitchUserbadge: Store<boolean>
|
||||
},
|
||||
options?: {
|
||||
isOpened?: UIEventSource<boolean>
|
||||
userInfoFocusedQuestion?: UIEventSource<string>
|
||||
}
|
||||
) {
|
||||
const isOpened = options?.isOpened ?? new UIEventSource<boolean>(false)
|
||||
const userPanel = new UserInformationMainPanel(
|
||||
state.osmConnection,
|
||||
state.locationControl,
|
||||
state.layoutToUse,
|
||||
isOpened,
|
||||
options?.userInfoFocusedQuestion
|
||||
)
|
||||
super(
|
||||
() => {
|
||||
return new VariableUiElement(
|
||||
state.osmConnection.userDetails.map((ud) => {
|
||||
if (ud.loggedIn === false) {
|
||||
return Translations.t.userinfo.titleNotLoggedIn
|
||||
}
|
||||
return Translations.t.userinfo.welcome.Subs(ud)
|
||||
})
|
||||
)
|
||||
},
|
||||
() => new LoginToggle(userPanel, Translations.t.general.getStartedLogin, state),
|
||||
"userinfo",
|
||||
isOpened
|
||||
)
|
||||
this.userPanel = userPanel
|
||||
}
|
||||
|
||||
Activate() {
|
||||
super.Activate()
|
||||
this.userPanel?.focusOnSelectedQuestion()
|
||||
}
|
||||
}
|
||||
|
|
48
UI/BigComponents/UserProfile.svelte
Normal file
48
UI/BigComponents/UserProfile.svelte
Normal file
|
@ -0,0 +1,48 @@
|
|||
<script lang="ts">
|
||||
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { PencilAltIcon, UserCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { onDestroy } from "svelte";
|
||||
import Showdown from "showdown";
|
||||
import FromHtml from "../Base/FromHtml.svelte";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations.js";
|
||||
|
||||
/**
|
||||
* This panel shows information about the logged-in user, showing account name, profile pick, description and an edit-button
|
||||
*/
|
||||
export let osmConnection: OsmConnection;
|
||||
let userdetails: UIEventSource<UserDetails> = osmConnection.userDetails;
|
||||
let description: string;
|
||||
onDestroy(userdetails.addCallbackAndRunD(userdetails => {
|
||||
description = new Showdown.Converter()
|
||||
.makeHtml(userdetails.description)
|
||||
?.replace(/>/g, ">")
|
||||
?.replace(/</g, "<");
|
||||
|
||||
}));
|
||||
</script>
|
||||
|
||||
<div class="flex border border-gray-300 border-dashed m-1 p-1 rounded-md link-underline">
|
||||
{#if $userdetails.img}
|
||||
<img src={$userdetails.img} class="rounded-full w-12 h-12 m-4">
|
||||
{:else}
|
||||
<UserCircleIcon class="w-12 h-12" />
|
||||
{/if}
|
||||
<div class="flex flex-col relative">
|
||||
<h3>{$userdetails.name}</h3>
|
||||
{#if description}
|
||||
<FromHtml src={description} />
|
||||
<a href={osmConnection.Backend() + "/profile/edit"} target="_blank">
|
||||
<PencilAltIcon class="p-2 w-6 h-6 subtle-background rounded-full absolute right-1 bottom-1" />
|
||||
</a>
|
||||
{:else}
|
||||
<Tr t={Translations.t. userinfo.noDescription} />
|
||||
<a href={osmConnection.Backend() + "/profile/edit"} target="_blank" class="flex subtle-background items-center">
|
||||
<PencilAltIcon slot="image" class="p-2 w-8 h-8" />
|
||||
<Tr slot="message" t={Translations.t.userinfo.noDescriptionCallToAction} />
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -158,30 +158,6 @@ export default class DefaultGUI {
|
|||
|
||||
const self = this
|
||||
|
||||
const userInfoMapControl = Toggle.If(state.featureSwitchUserbadge, () => {
|
||||
new UserInformationPanel(state, {
|
||||
isOpened: guiState.userInfoIsOpened,
|
||||
userInfoFocusedQuestion: guiState.userInfoFocusedQuestion,
|
||||
})
|
||||
|
||||
const mapControl = new MapControlButton(
|
||||
new VariableUiElement(
|
||||
state.osmConnection.userDetails.map((ud) => {
|
||||
if (ud?.img === undefined) {
|
||||
return Svg.person_ui().SetClass("mt-1 block")
|
||||
}
|
||||
return new Img(ud?.img)
|
||||
})
|
||||
).SetClass("block rounded-full overflow-hidden"),
|
||||
{
|
||||
dontStyle: true,
|
||||
}
|
||||
).onClick(() => {
|
||||
self.guiState.userInfoIsOpened.setData(true)
|
||||
})
|
||||
|
||||
return new LoginToggle(mapControl, Translations.t.general.loginWithOpenStreetMap, state)
|
||||
})
|
||||
const extraLink = Toggle.If(
|
||||
state.featureSwitchExtraLinkEnabled,
|
||||
() => new ExtraLinkButton(state, state.layoutToUse.extraLink)
|
||||
|
@ -200,7 +176,7 @@ export default class DefaultGUI {
|
|||
const copyright = new MapControlButton(Svg.copyright_svg()).onClick(() =>
|
||||
guiState.copyrightViewIsOpened.setData(true)
|
||||
)
|
||||
new Combine([welcomeMessageMapControl, userInfoMapControl, copyright, extraLink])
|
||||
new Combine([welcomeMessageMapControl, copyright, extraLink])
|
||||
.SetClass("flex flex-col")
|
||||
.AttachTo("top-left")
|
||||
|
||||
|
|
|
@ -15,32 +15,51 @@
|
|||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let selectedElement: Feature;
|
||||
export let state: SpecialVisualizationState;
|
||||
export let layer: LayerConfig
|
||||
export let layer: LayerConfig;
|
||||
|
||||
export let showQuestionIfUnknown : boolean= false
|
||||
let editMode = false
|
||||
export let highlightedRendering: UIEventSource<string> = undefined;
|
||||
export let showQuestionIfUnknown: boolean = false;
|
||||
let editMode = false;
|
||||
onDestroy(tags.addCallbackAndRunD(tags => {
|
||||
editMode = showQuestionIfUnknown && !config.IsKnown(tags)
|
||||
|
||||
}))
|
||||
editMode = showQuestionIfUnknown && !config.IsKnown(tags);
|
||||
|
||||
}));
|
||||
|
||||
let htmlElem: HTMLElement;
|
||||
if (highlightedRendering) {
|
||||
onDestroy(highlightedRendering.addCallbackAndRun(highlighted => {
|
||||
console.log("Highlighted rendering is", highlighted)
|
||||
if(htmlElem === undefined){
|
||||
return
|
||||
}
|
||||
if (config.id === highlighted) {
|
||||
htmlElem.classList.add("glowing-shadow");
|
||||
} else {
|
||||
htmlElem.classList.remove("glowing-shadow");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{#if config.question}
|
||||
{#if editMode}
|
||||
<TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer} >
|
||||
<button slot="cancel" on:click={() => {editMode = false}}>
|
||||
<Tr t={Translations.t.general.cancel}/>
|
||||
</button>
|
||||
</TagRenderingQuestion>
|
||||
{:else}
|
||||
<div class="flex justify-between">
|
||||
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
|
||||
<button on:click={() => {editMode = true}} class="w-6 h-6 rounded-full subtle-background p-1">
|
||||
<PencilAltIcon></PencilAltIcon>
|
||||
</button>
|
||||
</div>
|
||||
<div bind:this={htmlElem}>
|
||||
{#if config.question}
|
||||
{#if editMode}
|
||||
<TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer}>
|
||||
<button slot="cancel" on:click={() => {editMode = false}}>
|
||||
<Tr t={Translations.t.general.cancel} />
|
||||
</button>
|
||||
</TagRenderingQuestion>
|
||||
{:else}
|
||||
<div class="flex justify-between">
|
||||
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
|
||||
<button on:click={() => {editMode = true}} class="shrink-0 w-6 h-6 rounded-full subtle-background p-1">
|
||||
<PencilAltIcon></PencilAltIcon>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else }
|
||||
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
|
||||
{/if}
|
||||
{:else }
|
||||
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter";
|
||||
import FreeformInput from "./FreeformInput.svelte";
|
||||
import Translations from "../../i18n/Translations.js";
|
||||
import FromHtml from "../../Base/FromHtml.svelte";
|
||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
|
@ -63,6 +62,24 @@
|
|||
}>();
|
||||
|
||||
function onSave() {
|
||||
|
||||
if (layer.source === null) {
|
||||
/**
|
||||
* This is a special, priviliged layer.
|
||||
* We simply apply the tags onto the records
|
||||
*/
|
||||
const kv = selectedTags.asChange(tags.data);
|
||||
for (const { k, v } of kv) {
|
||||
if (v === undefined) {
|
||||
delete tags.data[k];
|
||||
} else {
|
||||
tags.data[k] = v;
|
||||
}
|
||||
}
|
||||
tags.ping();
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch("saved", { config, applied: selectedTags });
|
||||
const change = new ChangeTagAction(
|
||||
tags.data.id,
|
||||
|
@ -76,6 +93,7 @@
|
|||
freeformInput.setData(undefined);
|
||||
selectedMapping = 0;
|
||||
selectedTags = undefined;
|
||||
|
||||
change.CreateChangeDescriptions().then(changes =>
|
||||
state.changes.applyChanges(changes)
|
||||
).catch(console.error);
|
||||
|
@ -93,12 +111,14 @@
|
|||
</span>
|
||||
<span class="alert">{config.id}</span>
|
||||
</div>
|
||||
<SpecialTranslation slot="else" t={config.question} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
|
||||
<SpecialTranslation slot="else" t={config.question} {tags} {state} {layer}
|
||||
feature={selectedElement}></SpecialTranslation>
|
||||
</If>
|
||||
|
||||
{#if config.questionhint}
|
||||
<div class="subtle">
|
||||
<SpecialTranslation t={config.questionhint} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
|
||||
<SpecialTranslation t={config.questionhint} {tags} {state} {layer}
|
||||
feature={selectedElement}></SpecialTranslation>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -152,7 +172,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<TagHint osmConnection={state.osmConnection} tags={selectedTags}></TagHint>
|
||||
<TagHint osmConnection={state.osmConnection} tags={selectedTags}></TagHint>
|
||||
<div>
|
||||
<!-- TagRenderingQuestion-buttons -->
|
||||
<slot name="cancel"></slot>
|
||||
|
@ -163,7 +183,7 @@
|
|||
{:else }
|
||||
<div class="w-6 h-6">
|
||||
<!-- Invalid value; show an inactive button or something like that-->
|
||||
<ExclamationIcon></ExclamationIcon>
|
||||
<ExclamationIcon></ExclamationIcon>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -56,16 +56,27 @@ import Maproulette from "../Logic/Maproulette"
|
|||
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
||||
import QuestionViz from "./Popup/QuestionViz"
|
||||
import SimpleAddUI from "./BigComponents/SimpleAddUI"
|
||||
import { Feature } from "geojson"
|
||||
import { GeoOperations } from "../Logic/GeoOperations"
|
||||
import CreateNewNote from "./Popup/CreateNewNote.svelte"
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"
|
||||
import UserProfile from "./BigComponents/UserProfile.svelte"
|
||||
import LanguagePicker from "./LanguagePicker"
|
||||
import Link from "./Base/Link"
|
||||
|
||||
export default class SpecialVisualizations {
|
||||
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
|
||||
|
||||
static undoEncoding(str: string) {
|
||||
return str
|
||||
.trim()
|
||||
.replace(/&LPARENS/g, "(")
|
||||
.replace(/&RPARENS/g, ")")
|
||||
.replace(/&LBRACE/g, "{")
|
||||
.replace(/&RBRACE/g, "}")
|
||||
.replace(/&COMMA/g, ",")
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* For a given string, returns a specification what parts are fixed and what parts are special renderings.
|
||||
|
@ -115,15 +126,7 @@ export default class SpecialVisualizations {
|
|||
)
|
||||
const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "")
|
||||
if (argument.length > 0) {
|
||||
const realArgs = argument.split(",").map((str) =>
|
||||
str
|
||||
.trim()
|
||||
.replace(/&LPARENS/g, "(")
|
||||
.replace(/&RPARENS/g, ")")
|
||||
.replace(/&LBRACE/g, "{")
|
||||
.replace(/&RBRACE/g, "}")
|
||||
.replace(/&COMMA/g, ",")
|
||||
)
|
||||
const realArgs = argument.split(",").map((str) => this.undoEncoding(str))
|
||||
for (let i = 0; i < realArgs.length; i++) {
|
||||
if (args.length <= i) {
|
||||
args.push(realArgs[i])
|
||||
|
@ -273,6 +276,39 @@ export default class SpecialVisualizations {
|
|||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "user_profile",
|
||||
args: [],
|
||||
docs: "A component showing information about the currently logged in user (username, profile description, profile picture + link to edit them). Mostly meant to be used in the 'user-settings'",
|
||||
constr(state: SpecialVisualizationState): BaseUIElement {
|
||||
return new SvelteUIElement(UserProfile, {
|
||||
osmConnection: state.osmConnection,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "language_picker",
|
||||
args: [],
|
||||
docs: "A component to set the language of the user interface",
|
||||
constr(state: SpecialVisualizationState): BaseUIElement {
|
||||
return new LanguagePicker(
|
||||
state.layout.language,
|
||||
Translations.t.general.pickLanguage.Clone()
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "logout",
|
||||
args: [],
|
||||
docs: "Shows a button where the user can log out",
|
||||
constr(state: SpecialVisualizationState): BaseUIElement {
|
||||
return new SubtleButton(Svg.logout_ui(), Translations.t.general.logout, {
|
||||
imgSize: "w-6 h-6",
|
||||
}).onClick(() => {
|
||||
state.osmConnection.LogOut()
|
||||
})
|
||||
},
|
||||
},
|
||||
new HistogramViz(),
|
||||
new StealViz(),
|
||||
new MinimapViz(),
|
||||
|
@ -717,7 +753,7 @@ export default class SpecialVisualizations {
|
|||
docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'",
|
||||
example:
|
||||
"`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.",
|
||||
constr: (state, tagsSource, args, feature) =>
|
||||
constr: (state, tagsSource) =>
|
||||
new VariableUiElement(
|
||||
tagsSource.map((tags) => {
|
||||
const layer = state.layout.getMatchingLayer(tags)
|
||||
|
@ -933,6 +969,40 @@ export default class SpecialVisualizations {
|
|||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "link",
|
||||
docs: "Construct a link. By using the 'special' visualisation notation, translation should be easier",
|
||||
args: [
|
||||
{
|
||||
name: "text",
|
||||
doc: "Text to be shown",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "href",
|
||||
doc: "The URL to link to",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature
|
||||
): BaseUIElement {
|
||||
const [text, href] = args
|
||||
return new VariableUiElement(
|
||||
tagSource.map(
|
||||
(tags) =>
|
||||
new Link(
|
||||
Utils.SubstituteKeys(text, tags),
|
||||
Utils.SubstituteKeys(href, tags),
|
||||
true
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "multi",
|
||||
docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering",
|
||||
|
|
|
@ -87,7 +87,7 @@ export class SubstitutedTranslation extends VariableUiElement {
|
|||
tagsSource.data.id
|
||||
)
|
||||
return viz.func
|
||||
.constr(state, tagsSource, proto.args, feature, undefined)
|
||||
.constr(state, tagsSource, proto.args.map(t => SpecialVisualizations.undoEncoding(t)), feature, undefined)
|
||||
?.SetStyle(proto.style)
|
||||
} catch (e) {
|
||||
console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e)
|
||||
|
|
|
@ -2,13 +2,12 @@
|
|||
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte";
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
|
||||
import MapControlButton from "./Base/MapControlButton.svelte";
|
||||
import ToSvelte from "./Base/ToSvelte.svelte";
|
||||
import Svg from "../Svg";
|
||||
import If from "./Base/If.svelte";
|
||||
import { GeolocationControl } from "./BigComponents/GeolocationControl.js";
|
||||
import { GeolocationControl } from "./BigComponents/GeolocationControl";
|
||||
import type { Feature } from "geojson";
|
||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
|
@ -17,19 +16,21 @@
|
|||
import ThemeViewState from "../Models/ThemeViewState";
|
||||
import type { MapProperties } from "../Models/MapProperties";
|
||||
import Geosearch from "./BigComponents/Geosearch.svelte";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui";
|
||||
import Translations from "./i18n/Translations";
|
||||
import { CogIcon, MenuIcon, EyeIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { CogIcon, EyeIcon, MenuIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import Tr from "./Base/Tr.svelte";
|
||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
|
||||
import FloatOver from "./Base/FloatOver.svelte";
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.js";
|
||||
import { Utils } from "../Utils.js";
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy";
|
||||
import { Utils } from "../Utils";
|
||||
import Constants from "../Models/Constants";
|
||||
import TabbedGroup from "./Base/TabbedGroup.svelte";
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState";
|
||||
import LoginToggle from "./Base/LoginToggle.svelte";
|
||||
import LoginButton from "./Base/LoginButton.svelte";
|
||||
|
||||
export let layout: LayoutConfig;
|
||||
const state = new ThemeViewState(layout);
|
||||
export let state: ThemeViewState;
|
||||
let layout = state.layout;
|
||||
|
||||
let selectedElementTags: Store<UIEventSource<Record<string, string>>> =
|
||||
state.selectedElement.mapD((f) => {
|
||||
|
@ -43,6 +44,7 @@
|
|||
let mapproperties: MapProperties = state.mapProperties;
|
||||
let featureSwitches: FeatureSwitchState = state.featureSwitches;
|
||||
let availableLayers = state.availableLayers;
|
||||
let userdetails = state.osmConnection.userDetails;
|
||||
</script>
|
||||
|
||||
|
||||
|
@ -97,7 +99,7 @@
|
|||
{#if $selectedElement !== undefined && $selectedLayer !== undefined}
|
||||
<FloatOver on:close={() => {selectedElement.setData(undefined)}}>
|
||||
<SelectedElementView layer={$selectedLayer} selectedElement={$selectedElement}
|
||||
tags={$selectedElementTags} state={state}></SelectedElementView>
|
||||
tags={$selectedElementTags} state={state} />
|
||||
</FloatOver>
|
||||
|
||||
{/if}
|
||||
|
@ -106,7 +108,7 @@
|
|||
<!-- Theme page -->
|
||||
<FloatOver on:close={() => state.guistate.themeIsOpened.setData(false)}>
|
||||
<TabbedGroup tab={state.guistate.themeViewTabIndex}>
|
||||
<Tr slot="title0" t={layout.title} />
|
||||
<Tr slot="title0" t={layout.title} />
|
||||
|
||||
<div slot="content0">
|
||||
|
||||
|
@ -130,17 +132,18 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div slot="title1" class="flex">
|
||||
|
||||
<div class="flex" slot="title1">
|
||||
<If condition={state.featureSwitches.featureSwitchFilter}>
|
||||
<img class="w-4 h-4" src="./assets/svg/filter.svg">
|
||||
<Tr t={Translations.t.general.menu.filter} />
|
||||
<img class="w-4 h-4" src="./assets/svg/filter.svg">
|
||||
<Tr t={Translations.t.general.menu.filter} />
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<div slot="content1" class="flex flex-col">
|
||||
<div class="flex flex-col" slot="content1">
|
||||
{#each layout.layers as layer}
|
||||
<Filterview zoomlevel={state.mapProperties.zoom} filteredLayer={state.layerState.filteredLayers.get(layer.id)} highlightedLayer={state.guistate.highlightedLayerInFilters}></Filterview>
|
||||
<Filterview zoomlevel={state.mapProperties.zoom} filteredLayer={state.layerState.filteredLayers.get(layer.id)}
|
||||
highlightedLayer={state.guistate.highlightedLayerInFilters}></Filterview>
|
||||
{/each}
|
||||
<If condition={state.featureSwitches.featureSwitchBackgroundSelection}>
|
||||
<RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker>
|
||||
|
@ -154,50 +157,56 @@
|
|||
<If condition={state.guistate.menuIsOpened}>
|
||||
<!-- Menu page -->
|
||||
<FloatOver on:close={() => state.guistate.menuIsOpened.setData(false)}>
|
||||
<TabGroup on:change={(e) => {state.guistate.menuViewTabIndex.setData(e.detail)} }>
|
||||
<TabList>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div class="flex">
|
||||
<Tr t={Translations.t.general.aboutMapcompleteTitle}></Tr>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div class="flex">
|
||||
<CogIcon class="w-6 h-6"/>
|
||||
Settings
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div class="flex">
|
||||
<img class="w-6" src="./assets/svg/community.svg">
|
||||
Get in touch with others
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div class="flex">
|
||||
<EyeIcon class="w-6"/>
|
||||
<Tr t={Translations.t.privacy.title}></Tr>
|
||||
</div>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels >
|
||||
<TabPanel class="flex flex-col">
|
||||
<Tr t={Translations.t.general.aboutMapcomplete.Subs({
|
||||
<TabbedGroup tab={state.guistate.menuViewTabIndex}>
|
||||
<div class="flex" slot="title0">
|
||||
<Tr t={Translations.t.general.aboutMapcompleteTitle}></Tr>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col" slot="content0">
|
||||
<Tr t={Translations.t.general.aboutMapcomplete.Subs({
|
||||
osmcha_link: Utils.OsmChaLinkFor(7),
|
||||
})}></Tr>
|
||||
|
||||
{Constants.vNumber}
|
||||
</TabPanel>
|
||||
<TabPanel>User settings</TabPanel>
|
||||
<TabPanel>
|
||||
<CommunityIndexView location={state.mapProperties.location}></CommunityIndexView>
|
||||
{Constants.vNumber}
|
||||
</div>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ToSvelte construct={() => new PrivacyPolicy()}></ToSvelte>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
<If condition={state.osmConnection.isLoggedIn} slot="title1">
|
||||
<div class="flex">
|
||||
<CogIcon class="w-6 h-6" />
|
||||
<Tr t={UserRelatedState.usersettingsConfig.title.GetRenderValue({})} />
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<div slot="content1">
|
||||
<!-- All shown components are set by 'usersettings.json', which happily uses some special visualisations created specifically for it -->
|
||||
<LoginToggle {state}>
|
||||
<div slot="not-logged-in">
|
||||
<Tr class="alert" t={Translations.t.userinfo.notLoggedIn}/>
|
||||
<LoginButton osmConnection={state.osmConnection}></LoginButton>
|
||||
</div>
|
||||
<SelectedElementView
|
||||
layer={UserRelatedState.usersettingsConfig}
|
||||
selectedElement={({
|
||||
type:"Feature",properties: {}, geometry: {type:"Point", coordinates: [0,0]}
|
||||
})} {state}
|
||||
tags={state.userRelatedState.preferencesAsTags}
|
||||
highlightedRendering={state.guistate.highlightedUserSetting}
|
||||
/>
|
||||
</LoginToggle>
|
||||
</div>
|
||||
|
||||
<div class="flex" slot="title2">
|
||||
<img class="w-6" src="./assets/svg/community.svg">
|
||||
Get in touch with others
|
||||
</div>
|
||||
<CommunityIndexView location={state.mapProperties.location} slot="content2"></CommunityIndexView>
|
||||
|
||||
<div class="flex" slot="title3">
|
||||
<EyeIcon class="w-6" />
|
||||
<Tr t={Translations.t.privacy.title}></Tr>
|
||||
</div>
|
||||
<ToSvelte construct={() => new PrivacyPolicy()} slot="content3"></ToSvelte>
|
||||
</TabbedGroup>
|
||||
</FloatOver>
|
||||
</If>
|
||||
|
||||
|
|
|
@ -5,7 +5,12 @@
|
|||
"de": "Eine spezielle Ebene, die nicht für die Darstellung auf einer Karte gedacht ist, sondern für die Festlegung von Benutzereinstellungen verwendet wird",
|
||||
"nl": "Een speciale lag die niet getoond wordt op de kaart, maar die de instellingen van de gebruiker weergeeft"
|
||||
},
|
||||
"title": null,
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Settings",
|
||||
"nl": "Instellingen"
|
||||
}
|
||||
},
|
||||
"source": "special",
|
||||
"calculatedTags": [
|
||||
"_mastodon_candidate_md=feat.properties._description.match(/\\[[^\\]]*\\]\\((.*(mastodon|en.osm.town).*)\\).*/)?.at(1)",
|
||||
|
@ -15,6 +20,66 @@
|
|||
"_mastodon_candidate=feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a"
|
||||
],
|
||||
"tagRenderings": [
|
||||
{
|
||||
"id": "profile",
|
||||
"render": {
|
||||
"*": "{user_profile()}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "language_picker",
|
||||
"render": {
|
||||
"*": "{language_picker()}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "inbox",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "_unreadMessages=0",
|
||||
"then": {
|
||||
"special": {
|
||||
"type": "link",
|
||||
"href": "{_backend}/messages/inbox",
|
||||
"text": {
|
||||
"en": "Open your inbox",
|
||||
"nl": "Ga naar je inbox"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "_unreadMessages>0",
|
||||
"then": {
|
||||
"special": {
|
||||
"type": "link",
|
||||
"text": {
|
||||
"en": "<b class='alert'>You have {_unreadMessages}</b><br/>Open your inbox"
|
||||
},
|
||||
"href": "{_backend}/messages/inbox"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "settings-link",
|
||||
"render": {
|
||||
"special": {
|
||||
"type": "link",
|
||||
"text": {
|
||||
"en": "Open your settings on OpenStreetMap.org"
|
||||
},
|
||||
"href": "{_backend}/account/edit"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "logout",
|
||||
"render": {
|
||||
"*": "{logout()}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "picture-license",
|
||||
"description": "This question is not meant to be placed on an OpenStreetMap-element; however it is used in the user information panel to ask which license the user wants",
|
||||
|
@ -328,4 +393,4 @@
|
|||
}
|
||||
],
|
||||
"mapRendering": null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -859,10 +859,6 @@ video {
|
|||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
@ -931,6 +927,10 @@ video {
|
|||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.ml-3 {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
@ -1063,14 +1063,14 @@ video {
|
|||
height: 2.75rem;
|
||||
}
|
||||
|
||||
.h-96 {
|
||||
height: 24rem;
|
||||
}
|
||||
|
||||
.h-64 {
|
||||
height: 16rem;
|
||||
}
|
||||
|
||||
.h-96 {
|
||||
height: 24rem;
|
||||
}
|
||||
|
||||
.h-0 {
|
||||
height: 0px;
|
||||
}
|
||||
|
|
|
@ -594,9 +594,7 @@
|
|||
},
|
||||
"userinfo": {
|
||||
"gotoInbox": "Obre la teva safata d'entrada",
|
||||
"gotoSettings": "Aneu a la vostra configuració a OpenStreetMap.org",
|
||||
"moveToHome": "Mou el mapa a la vostra ubicació de casa",
|
||||
"welcome": "Benvingut/da {name}"
|
||||
"gotoSettings": "Aneu a la vostra configuració a OpenStreetMap.org"
|
||||
},
|
||||
"validation": {
|
||||
"color": {
|
||||
|
|
|
@ -265,10 +265,7 @@
|
|||
"userinfo": {
|
||||
"gotoInbox": "Otevřít poštu",
|
||||
"gotoSettings": "Přejít do vašich nastavení na OpenStreetMap.org",
|
||||
"moveToHome": "Přesunout mapu na vaší domovskou polohu",
|
||||
"noDescription": "Na svém profilu zatím nemáte popis",
|
||||
"noDescriptionCallToAction": "Přidat popis profilu",
|
||||
"titleNotLoggedIn": "Vítejte",
|
||||
"welcome": "Vítejte, {name}"
|
||||
"noDescriptionCallToAction": "Přidat popis profilu"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -934,12 +934,8 @@
|
|||
"userinfo": {
|
||||
"gotoInbox": "Posteingang öffnen",
|
||||
"gotoSettings": "Einstellungen auf OpenStreetMap.org öffnen",
|
||||
"moveToHome": "Verschieben Sie die Karte an Ihren Heimatstandort",
|
||||
"newMessages": "Sie haben neue Nachrichten",
|
||||
"noDescription": "Sie haben noch keine Profilbeschreibung",
|
||||
"noDescriptionCallToAction": "Profilbeschreibung hinzufügen",
|
||||
"titleNotLoggedIn": "Willkommen",
|
||||
"welcome": "Willkommen {name}"
|
||||
"noDescriptionCallToAction": "Profilbeschreibung hinzufügen"
|
||||
},
|
||||
"validation": {
|
||||
"color": {
|
||||
|
|
|
@ -958,12 +958,9 @@
|
|||
"userinfo": {
|
||||
"gotoInbox": "Open your inbox",
|
||||
"gotoSettings": "Go to your settings on OpenStreetMap.org",
|
||||
"moveToHome": "Move the map to your home location",
|
||||
"newMessages": "you have new messages",
|
||||
"noDescription": "You don't have a description on your profile yet",
|
||||
"noDescriptionCallToAction": "Add a profile description",
|
||||
"titleNotLoggedIn": "Welcome",
|
||||
"welcome": "Welcome {name}"
|
||||
"notLoggedIn": "You have logged out"
|
||||
},
|
||||
"validation": {
|
||||
"color": {
|
||||
|
|
|
@ -713,9 +713,7 @@
|
|||
"missing": "{count} cadenas sin traducir",
|
||||
"notImmediate": "Las traducciones no se actualizan directamente. Habitualmente esto lleva unos días"
|
||||
},
|
||||
"userinfo": {
|
||||
"welcome": "Bienvenido {name}"
|
||||
},
|
||||
"userinfo": {},
|
||||
"validation": {
|
||||
"color": {
|
||||
"description": "Un color o código hexadecimal"
|
||||
|
|
|
@ -488,9 +488,7 @@
|
|||
},
|
||||
"userinfo": {
|
||||
"gotoInbox": "Ouvrir sa boite de réception",
|
||||
"gotoSettings": "Paramètres sur OpenStreetMap.org",
|
||||
"moveToHome": "Déplacez la carte vers votre emplacement",
|
||||
"welcome": "Bienvenue {name}"
|
||||
"gotoSettings": "Paramètres sur OpenStreetMap.org"
|
||||
},
|
||||
"validation": {
|
||||
"email": {
|
||||
|
|
|
@ -8540,6 +8540,24 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"inbox": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": {
|
||||
"special": {
|
||||
"text": "Open your inbox"
|
||||
}
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"then": {
|
||||
"special": {
|
||||
"text": "<b class='alert'>You have {_unreadMessages}</b><br/>Open your inbox"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"picture-license": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
|
@ -8617,6 +8635,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": "Settings"
|
||||
}
|
||||
},
|
||||
"veterinary": {
|
||||
|
|
|
@ -8265,6 +8265,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"inbox": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": {
|
||||
"special": {
|
||||
"text": "Ga naar je inbox"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"picture-license": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
|
@ -8314,6 +8325,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": "Instellingen"
|
||||
}
|
||||
},
|
||||
"veterinary": {
|
||||
|
|
|
@ -692,11 +692,8 @@
|
|||
"userinfo": {
|
||||
"gotoInbox": "Åpne innboksen din",
|
||||
"gotoSettings": "Gå til innstillingene dine på OpenStreetMap.org",
|
||||
"newMessages": "du har nye meldinger",
|
||||
"noDescription": "Du har ikke noen profilbeskrivelse enda",
|
||||
"noDescriptionCallToAction": "Legg til profilbeskrivelse",
|
||||
"titleNotLoggedIn": "Velkommen",
|
||||
"welcome": "Velkommen {name}"
|
||||
"noDescriptionCallToAction": "Legg til profilbeskrivelse"
|
||||
},
|
||||
"validation": {
|
||||
"color": {
|
||||
|
|
|
@ -938,12 +938,8 @@
|
|||
"userinfo": {
|
||||
"gotoInbox": "Open je inbox",
|
||||
"gotoSettings": "Ga naar je instellingen op OpenStreetMap.org",
|
||||
"moveToHome": "Beweeg de kaart naar je thuislocatie",
|
||||
"newMessages": "je hebt nieuwe berichten",
|
||||
"noDescription": "Je hebt nog geen beschrijving op je profiel",
|
||||
"noDescriptionCallToAction": "Voeg een profielbeschrijving toe",
|
||||
"titleNotLoggedIn": "Welkom",
|
||||
"welcome": "Welkom {name}"
|
||||
"noDescriptionCallToAction": "Voeg een profielbeschrijving toe"
|
||||
},
|
||||
"validation": {
|
||||
"color": {
|
||||
|
|
|
@ -192,11 +192,8 @@
|
|||
"userinfo": {
|
||||
"gotoInbox": "Otwórz swoją skrzynkę odbiorczą",
|
||||
"gotoSettings": "Przejdź do swoich ustawień na OpenStreetMap.org",
|
||||
"moveToHome": "Przesuń mapę do Twojej lokalizacji domowej",
|
||||
"newMessages": "masz nowe wiadomości",
|
||||
"noDescription": "Nie masz jeszcze opisu w swoim profilu",
|
||||
"noDescriptionCallToAction": "Dodaj opis profilu",
|
||||
"welcome": "Witaj {name}"
|
||||
"noDescriptionCallToAction": "Dodaj opis profilu"
|
||||
},
|
||||
"validation": {
|
||||
"color": {
|
||||
|
|
|
@ -216,10 +216,7 @@
|
|||
"translations": {
|
||||
"activateButton": "Помогите перевести MapComplete"
|
||||
},
|
||||
"userinfo": {
|
||||
"titleNotLoggedIn": "Добро пожаловать",
|
||||
"welcome": "Добро пожаловать, {name}"
|
||||
},
|
||||
"userinfo": {},
|
||||
"validation": {
|
||||
"nat": {
|
||||
"notANumber": "Введите число"
|
||||
|
|
16
test.ts
16
test.ts
|
@ -7,17 +7,24 @@ import ThemeViewState from "./Models/ThemeViewState"
|
|||
import Combine from "./UI/Base/Combine"
|
||||
import SpecialVisualizations from "./UI/SpecialVisualizations"
|
||||
import AddNewPoint from "./UI/Popup/AddNewPoint/AddNewPoint.svelte"
|
||||
import UserProfile from "./UI/BigComponents/UserProfile.svelte"
|
||||
|
||||
async function main() {
|
||||
new FixedUiElement("").AttachTo("extradiv")
|
||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||
const main = new SvelteUIElement(ThemeViewGUI, { layout })
|
||||
const state = new ThemeViewState(layout)
|
||||
|
||||
const main = new SvelteUIElement(ThemeViewGUI, { state })
|
||||
state.guistate.menuIsOpened.setData(true)
|
||||
state.guistate.menuViewTab.setData("settings")
|
||||
main.AttachTo("maindiv")
|
||||
}
|
||||
|
||||
async function testspecial() {
|
||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||
const state = new ThemeViewState(layout)
|
||||
|
||||
state.guistate.openUsersettings("picture-license")
|
||||
const all = SpecialVisualizations.specialVisualizations.map((s) =>
|
||||
SpecialVisualizations.renderExampleOfSpecial(state, s)
|
||||
)
|
||||
|
@ -27,12 +34,7 @@ async function testspecial() {
|
|||
async function test() {
|
||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||
const state = new ThemeViewState(layout)
|
||||
state.featureSwitches.featureSwitchIsTesting.setData(true)
|
||||
new SvelteUIElement(AddNewPoint, {
|
||||
state,
|
||||
coordinate: { lon: 3.22001, lat: 51.21576 },
|
||||
}).AttachTo("maindiv")
|
||||
//*/
|
||||
new SvelteUIElement(UserProfile, { osmConnection: state.osmConnection }).AttachTo("maindiv")
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
Loading…
Reference in a new issue