diff --git a/src/Logic/State/FeatureSwitchState.ts b/src/Logic/State/FeatureSwitchState.ts index ebdb057c93..389c966a80 100644 --- a/src/Logic/State/FeatureSwitchState.ts +++ b/src/Logic/State/FeatureSwitchState.ts @@ -51,10 +51,9 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches { */ public readonly layoutToUse: LayoutConfig - public readonly featureSwitchUserbadge: UIEventSource + public readonly featureSwitchEnableLogin: UIEventSource public readonly featureSwitchSearch: UIEventSource public readonly featureSwitchBackgroundSelection: UIEventSource - public readonly featureSwitchAddNew: UIEventSource public readonly featureSwitchWelcomeMessage: UIEventSource public readonly featureSwitchCommunityIndex: UIEventSource public readonly featureSwitchExtraLinkEnabled: UIEventSource @@ -78,10 +77,10 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches { // Helper function to initialize feature switches - this.featureSwitchUserbadge = FeatureSwitchUtils.initSwitch( - "fs-userbadge", + this.featureSwitchEnableLogin = FeatureSwitchUtils.initSwitch( + "fs-enable-login", layoutToUse?.enableUserBadge ?? true, - "Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode." + "Disables/Enables logging in and thus disables editing all together. This effectively puts MapComplete into read-only mode." ) this.featureSwitchSearch = FeatureSwitchUtils.initSwitch( "fs-search", @@ -99,11 +98,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches { layoutToUse?.enableLayers ?? true, "Disables/Enables the filter view" ) - this.featureSwitchAddNew = FeatureSwitchUtils.initSwitch( - "fs-add-new", - layoutToUse?.enableAddNewPoints ?? true, - "Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)" - ) + this.featureSwitchWelcomeMessage = FeatureSwitchUtils.initSwitch( "fs-welcome-message", true, @@ -201,12 +196,6 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches { ) ) - this.featureSwitchUserbadge.addCallbackAndRun((userbadge) => { - if (!userbadge) { - this.featureSwitchAddNew.setData(false) - } - }) - this.backgroundLayerId = QueryParameters.GetQueryParameter( "background", layoutToUse?.defaultBackgroundId ?? "osm", diff --git a/src/Logic/Web/QueryParameters.ts b/src/Logic/Web/QueryParameters.ts index 208c8e8dd5..8d0dd9a1f4 100644 --- a/src/Logic/Web/QueryParameters.ts +++ b/src/Logic/Web/QueryParameters.ts @@ -4,14 +4,13 @@ import { UIEventSource } from "../UIEventSource" import Hash from "./Hash" import { Utils } from "../../Utils" -import doc = Mocha.reporters.doc export class QueryParameters { static defaults: Record = {} static documentation: Map = new Map() - private static order: string[] = ["layout", "test", "z", "lat", "lon"] protected static readonly _wasInitialized: Set = new Set() protected static readonly knownSources: Record> = {} + private static order: string[] = ["layout", "test", "z", "lat", "lon"] private static initialized = false public static GetQueryParameter( @@ -74,6 +73,7 @@ export class QueryParameters { this.init() return QueryParameters._wasInitialized.has(key) } + public static initializedParameters(): ReadonlyArray { return Array.from(QueryParameters._wasInitialized.keys()) } @@ -108,14 +108,12 @@ export class QueryParameters { } } - /** - * Set the query parameters of the page location - * @constructor - * @private - */ - private static Serialize() { - const parts = [] + public static GetParts(exclude?: Set) { + const parts: string[] = [] for (const key of QueryParameters.order) { + if (exclude?.has(key)) { + continue + } if (QueryParameters.knownSources[key]?.data === undefined) { continue } @@ -134,6 +132,16 @@ export class QueryParameters { encodeURIComponent(QueryParameters.knownSources[key].data) ) } + return parts + } + + /** + * Set the query parameters of the page location + * @constructor + * @private + */ + private static Serialize() { + const parts = QueryParameters.GetParts() if (!Utils.runningFromConsole) { // Don't pollute the history every time a parameter changes try { @@ -151,4 +159,8 @@ export class QueryParameters { QueryParameters._wasInitialized.clear() QueryParameters.order = [] } + + static GetDefaultFor(key: string): string { + return QueryParameters.defaults[key] + } } diff --git a/src/Logic/Web/ThemeViewStateHashActor.ts b/src/Logic/Web/ThemeViewStateHashActor.ts index a474b029bd..b10208f09e 100644 --- a/src/Logic/Web/ThemeViewStateHashActor.ts +++ b/src/Logic/Web/ThemeViewStateHashActor.ts @@ -1,9 +1,25 @@ import ThemeViewState from "../../Models/ThemeViewState" import Hash from "./Hash" +import { MenuState } from "../../Models/MenuState" export default class ThemeViewStateHashActor { private readonly _state: ThemeViewState + public static readonly documentation = [ + "The URL-hash can contain multiple values:", + "", + "- The id of the currently selected object, e.g. `node/1234`", + "- The currently opened menu view", + "- The base64-encoded JSON-file specifying a custom theme (only when loading)", + "", + "### Possible hashes to open a menu", + "", + "The possible hashes are:", + "", + MenuState._menuviewTabs.map((tab) => "`menu:" + tab + "`").join(","), + MenuState._themeviewTabs.map((tab) => "`theme-menu:" + tab + "`").join(","), + ] + /** * Converts the hash to the appropriate themeview state and, vice versa, sets the hash. * diff --git a/src/Models/MenuState.ts b/src/Models/MenuState.ts index 3b6aa600a0..63dda397cf 100644 --- a/src/Models/MenuState.ts +++ b/src/Models/MenuState.ts @@ -50,11 +50,15 @@ export class MenuState { ) public highlightedUserSetting: UIEventSource = new UIEventSource(undefined) - constructor(themeid: string = "") { + constructor(shouldOpenWelcomeMessage: boolean, themeid: string = "") { + // Note: this class is _not_ responsible to update the Hash, @see ThemeViewStateHashActor for this if (themeid) { themeid += "-" } - this.themeIsOpened = LocalStorageSource.GetParsed(themeid + "thememenuisopened", true) + this.themeIsOpened = LocalStorageSource.GetParsed( + themeid + "thememenuisopened", + shouldOpenWelcomeMessage + ) this.themeViewTabIndex = LocalStorageSource.GetParsed(themeid + "themeviewtabindex", 0) this.themeViewTab = this.themeViewTabIndex.sync( (i) => MenuState._themeviewTabs[i], diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 395c4a7c92..c9471bf5f0 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -114,15 +114,18 @@ export default class ThemeViewState implements SpecialVisualizationState { constructor(layout: LayoutConfig) { this.layout = layout - this.guistate = new MenuState(layout.id) + this.featureSwitches = new FeatureSwitchState(layout) + this.guistate = new MenuState( + this.featureSwitches.featureSwitchWelcomeMessage.data, + layout.id + ) this.map = new UIEventSource(undefined) const initial = new InitialMapPositioning(layout) this.mapProperties = new MapLibreAdaptor(this.map, initial) const geolocationState = new GeoLocationState() - this.featureSwitches = new FeatureSwitchState(layout) this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting - this.featureSwitchUserbadge = this.featureSwitches.featureSwitchUserbadge + this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin this.osmConnection = new OsmConnection({ dryRun: this.featureSwitches.featureSwitchIsTesting, @@ -469,7 +472,7 @@ export default class ThemeViewState implements SpecialVisualizationState { new ShowDataLayer(this.map, { features: new FilteringFeatureSource(last_click_layer, last_click), - doShowLayer: new ImmutableStore(true), + doShowLayer: this.featureSwitches.featureSwitchEnableLogin, layer: last_click_layer.layerDef, selectedElement: this.selectedElement, selectedLayer: this.selectedLayer, diff --git a/src/UI/BigComponents/ShareScreen.svelte b/src/UI/BigComponents/ShareScreen.svelte new file mode 100644 index 0000000000..f17c3553d7 --- /dev/null +++ b/src/UI/BigComponents/ShareScreen.svelte @@ -0,0 +1,121 @@ + + + +
+ + +
+ {#if typeof navigator?.share === "function"} + + {/if} + {#if navigator.clipboard !== undefined} + + {/if} +
Utils.selectTextIn(e.target)}> + {linkToShare} +
+
+ +
+ + {#if isCopied} + + {/if} +
+ + + + + + + +
+ <span class="literal-code iframe-code-block">
+ <iframe src="${url}"
+ allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px"
+ title="${state.layout.title?.txt ?? "MapComplete" } with MapComplete">
+ </iframe>
+ </span> +
+ +
diff --git a/src/UI/BigComponents/ShareScreen.ts b/src/UI/BigComponents/ShareScreen.ts deleted file mode 100644 index f1956c1979..0000000000 --- a/src/UI/BigComponents/ShareScreen.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { VariableUiElement } from "../Base/VariableUIElement" -import { Translation } from "../i18n/Translation" -import Svg from "../../Svg" -import Combine from "../Base/Combine" -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import { Utils } from "../../Utils" -import Translations from "../i18n/Translations" -import BaseUIElement from "../BaseUIElement" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import { InputElement } from "../Input/InputElement" -import { CheckBox } from "../Input/Checkboxes" -import { SubtleButton } from "../Base/SubtleButton" -import LZString from "lz-string" -import { SpecialVisualizationState } from "../SpecialVisualization" - -export class ShareScreen extends Combine { - constructor(state: SpecialVisualizationState) { - const layout = state?.layout - const tr = Translations.t.general.sharescreen - - const optionCheckboxes: InputElement[] = [] - const optionParts: Store[] = [] - - const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true) - optionCheckboxes.push(includeLocation) - - const currentLocation = state.mapProperties.location - const zoom = state.mapProperties.zoom - - optionParts.push( - includeLocation.GetValue().map( - (includeL) => { - if (currentLocation === undefined) { - return null - } - if (includeL) { - return [ - ["z", zoom.data], - ["lat", currentLocation.data?.lat], - ["lon", currentLocation.data?.lon], - ] - .filter((p) => p[1] !== undefined) - .map((p) => p[0] + "=" + p[1]) - .join("&") - } else { - return null - } - }, - [currentLocation, zoom] - ) - ) - - function fLayerToParam(flayer: { - isDisplayed: UIEventSource - layerDef: LayerConfig - }) { - if (flayer.isDisplayed.data) { - return null // Being displayed is the default - } - return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data - } - - const currentLayer: Store< - { id: string; name: string | Record } | undefined - > = state.mapProperties.rasterLayer.map((l) => l?.properties) - const currentBackground = new VariableUiElement( - currentLayer.map((layer) => { - return tr.fsIncludeCurrentBackgroundMap.Subs({ name: layer?.name ?? "" }) - }) - ) - const includeCurrentBackground = new CheckBox(currentBackground, true) - optionCheckboxes.push(includeCurrentBackground) - optionParts.push( - includeCurrentBackground.GetValue().map( - (includeBG) => { - if (includeBG) { - return "background=" + currentLayer.data?.id - } else { - return null - } - }, - [currentLayer] - ) - ) - - const includeLayerChoices = new CheckBox(tr.fsIncludeCurrentLayers, true) - optionCheckboxes.push(includeLayerChoices) - - optionParts.push( - includeLayerChoices.GetValue().map( - (includeLayerSelection) => { - if (includeLayerSelection) { - return Utils.NoNull( - Array.from(state.layerState.filteredLayers.values()).map(fLayerToParam) - ).join("&") - } else { - return null - } - }, - Array.from(state.layerState.filteredLayers.values()).map( - (flayer) => flayer.isDisplayed - ) - ) - ) - - const switches = [ - { urlName: "fs-userbadge", human: tr.fsUserbadge }, - { urlName: "fs-search", human: tr.fsSearch }, - { urlName: "fs-welcome-message", human: tr.fsWelcomeMessage }, - { urlName: "fs-layers", human: tr.fsLayers }, - { urlName: "fs-add-new", human: tr.fsAddNew }, - { urlName: "fs-geolocation", human: tr.fsGeolocation }, - ] - - for (const swtch of switches) { - const checkbox = new CheckBox(Translations.W(swtch.human)) - optionCheckboxes.push(checkbox) - optionParts.push( - checkbox.GetValue().map((isEn) => { - if (isEn) { - return null - } else { - return `${swtch.urlName}=false` - } - }) - ) - } - - if (layout.definitionRaw !== undefined) { - optionParts.push(new UIEventSource("userlayout=" + (layout.definedAtUrl ?? layout.id))) - } - - const options = new Combine(optionCheckboxes).SetClass("flex flex-col") - const url = (currentLocation ?? new UIEventSource(undefined)).map(() => { - const host = window.location.host - let path = window.location.pathname - path = path.substr(0, path.lastIndexOf("/")) - let id = layout.id.toLowerCase() - if (layout.definitionRaw !== undefined) { - id = "theme.html" - } - let literalText = `https://${host}${path}/${id}` - - let hash = "" - if (layout.definedAtUrl === undefined && layout.definitionRaw !== undefined) { - hash = "#" + LZString.compressToBase64(Utils.MinifyJSON(layout.definitionRaw)) - } - const parts = Utils.NoEmpty( - Utils.NoNull(optionParts.map((eventSource) => eventSource.data)) - ) - if (parts.length === 0) { - return literalText + hash - } - return literalText + "?" + parts.join("&") + hash - }, optionParts) - - const iframeCode = new VariableUiElement( - url.map((url) => { - return ` - <iframe src="${url}" allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" title="${ - layout.title?.txt ?? "MapComplete" - } with MapComplete"></iframe> - ` - }) - ) - - const linkStatus = new UIEventSource("") - const link = new VariableUiElement( - url.map( - (url) => - `` - ) - ).onClick(async () => { - const shareData = { - title: Translations.W(layout.title)?.ConstructElement().textContent ?? "", - text: Translations.W(layout.description)?.ConstructElement().textContent ?? "", - url: url.data, - } - - function rejected() { - const copyText = document.getElementById("code-link--copyable") - - // @ts-ignore - copyText.select() - // @ts-ignore - copyText.setSelectionRange(0, 99999) /*For mobile devices*/ - - document.execCommand("copy") - const copied = tr.copiedToClipboard.Clone() - copied.SetClass("thanks") - linkStatus.setData(copied) - } - - try { - navigator - .share(shareData) - .then(() => { - const thx = tr.thanksForSharing.Clone() - thx.SetClass("thanks") - linkStatus.setData(thx) - }, rejected) - .catch(rejected) - } catch (err) { - rejected() - } - }) - - let downloadThemeConfig: BaseUIElement = undefined - if (layout.definitionRaw !== undefined) { - const downloadThemeConfigAsJson = new SubtleButton( - Svg.download_svg(), - new Combine([tr.downloadCustomTheme, tr.downloadCustomThemeHelp.SetClass("subtle")]) - .onClick(() => { - Utils.offerContentsAsDownloadableFile( - layout.definitionRaw, - layout.id + ".mapcomplete-theme-definition.json", - { - mimetype: "application/json", - } - ) - }) - .SetClass("flex flex-col") - ) - let editThemeConfig: BaseUIElement = undefined - if (layout.definedAtUrl === undefined) { - const patchedDefinition = JSON.parse(layout.definitionRaw) - patchedDefinition["language"] = Object.keys(patchedDefinition.title) - editThemeConfig = new SubtleButton( - Svg.pencil_svg(), - "Edit this theme on the custom theme generator", - { - url: `https://pietervdvn.github.io/mc/legacy/070/customGenerator.html#${btoa( - JSON.stringify(patchedDefinition) - )}`, - } - ) - } - downloadThemeConfig = new Combine([ - downloadThemeConfigAsJson, - editThemeConfig, - ]).SetClass("flex flex-col") - } - - super([ - tr.intro, - link, - new VariableUiElement(linkStatus), - downloadThemeConfig, - tr.addToHomeScreen, - tr.embedIntro, - options, - iframeCode, - ]) - this.SetClass("flex flex-col link-underline") - } -} diff --git a/src/UI/BigComponents/ThemeIntroPanel.svelte b/src/UI/BigComponents/ThemeIntroPanel.svelte index 60b38a81e1..9e58063dfc 100644 --- a/src/UI/BigComponents/ThemeIntroPanel.svelte +++ b/src/UI/BigComponents/ThemeIntroPanel.svelte @@ -45,9 +45,7 @@ {#if layout.layers.some((l) => l.presets?.length > 0)} - - {/if} diff --git a/src/UI/QueryParameterDocumentation.ts b/src/UI/QueryParameterDocumentation.ts index 4c2255f4bc..55956ae54a 100644 --- a/src/UI/QueryParameterDocumentation.ts +++ b/src/UI/QueryParameterDocumentation.ts @@ -6,6 +6,7 @@ import Translations from "./i18n/Translations" import { QueryParameters } from "../Logic/Web/QueryParameters" import FeatureSwitchState from "../Logic/State/FeatureSwitchState" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor" export default class QueryParameterDocumentation { private static QueryParamDocsIntro = [ @@ -60,6 +61,7 @@ export default class QueryParameterDocumentation { public static GenerateQueryParameterDocs(): BaseUIElement { const docs: (string | BaseUIElement)[] = [ ...QueryParameterDocumentation.QueryParamDocsIntro, + ...ThemeViewStateHashActor.documentation, ] this.UrlParamDocs().forEach((value, key) => { const c = new Combine([ diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 88fe5c24ac..e69d73fd5b 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -1,57 +1,57 @@