From 6ad64e3f701e4877ffadfb83379c7f0e34d5588a Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 18 Apr 2022 02:39:30 +0200 Subject: [PATCH] Add a download button, improve share functionality for custom themes --- Logic/DetermineLayout.ts | 26 +++++----- Models/Constants.ts | 2 +- Models/ThemeConfig/Conversion/Validation.ts | 2 +- Models/ThemeConfig/LayoutConfig.ts | 16 ++++--- UI/BigComponents/ShareScreen.ts | 48 +++++++++++++------ Utils.ts | 2 +- langs/en.json | 2 + scripts/generateLayouts.ts | 2 +- test/Logic/Actors/Actors.spec.ts | 2 +- .../Conversion/FixLegacyTheme.spec.ts | 2 +- 10 files changed, 64 insertions(+), 40 deletions(-) diff --git a/Logic/DetermineLayout.ts b/Logic/DetermineLayout.ts index 734988583..b935481e8 100644 --- a/Logic/DetermineLayout.ts +++ b/Logic/DetermineLayout.ts @@ -14,7 +14,6 @@ import {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; import SharedTagRenderings from "../Customizations/SharedTagRenderings"; import * as known_layers from "../assets/generated/known_layers.json" -import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme"; import * as licenses from "../assets/generated/license_info.json" import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; @@ -43,10 +42,6 @@ export default class DetermineLayout { } let layoutId: string = undefined - if (location.href.indexOf("buurtnatuur.be") >= 0) { - layoutId = "buurtnatuur" - } - const path = window.location.pathname.split("/").slice(-1)[0]; if (path !== "theme.html" && path !== "") { @@ -72,7 +67,7 @@ export default class DetermineLayout { public static LoadLayoutFromHash( userLayoutParam: UIEventSource - ): (LayoutConfig & {definition: LayoutConfigJson}) | null { + ): LayoutConfig | null { let hash = location.hash.substr(1); let json: any; @@ -113,9 +108,7 @@ export default class DetermineLayout { const layoutToUse = DetermineLayout.prepCustomTheme(json) userLayoutParam.setData(layoutToUse.id); - const config = new LayoutConfig(layoutToUse, false); - config["definition"] = json - return config + return layoutToUse } catch (e) { console.error(e) if (hash === undefined || hash.length < 10) { @@ -144,7 +137,7 @@ export default class DetermineLayout { .AttachTo("centermessage"); } - private static prepCustomTheme(json: any): LayoutConfigJson { + private static prepCustomTheme(json: any, sourceUrl?: string): LayoutConfig { if(json.layers === undefined && json.tagRenderings !== undefined){ const iconTr = json.mapRendering.map(mr => mr.icon).find(icon => icon !== undefined) @@ -161,7 +154,6 @@ export default class DetermineLayout { } } - const knownLayersDict = new Map() for (const key in known_layers.layers) { const layer = known_layers.layers[key] @@ -172,10 +164,17 @@ export default class DetermineLayout { sharedLayers: knownLayersDict } json = new FixLegacyTheme().convertStrict(json, "While loading a dynamic theme") + const raw = json; + json = new FixImages(DetermineLayout._knownImages).convertStrict(json, "While fixing the images") json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme") console.log("The layoutconfig is ", json) - return json + + + return new LayoutConfig(json, false, { + definitionRaw: JSON.stringify(raw, null, " "), + definedAtUrl: sourceUrl + }) } private static async LoadRemoteTheme(link: string): Promise { @@ -190,8 +189,7 @@ export default class DetermineLayout { try { parsed.id = link; console.log("Loaded remote link:", link) - const layoutToUse = DetermineLayout.prepCustomTheme(parsed) - return new LayoutConfig(layoutToUse, false) + return DetermineLayout.prepCustomTheme(parsed, link) } catch (e) { console.error(e) DetermineLayout.ShowErrorOnCustomTheme( diff --git a/Models/Constants.ts b/Models/Constants.ts index 117f31cd0..181cb4143 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import {Utils} from "../Utils"; export default class Constants { - public static vNumber = "0.18.0"; + public static vNumber = "0.18.1"; public static ImgurApiKey = '7070e7167f0a25a' public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts index 4bdf9c415..912707877 100644 --- a/Models/ThemeConfig/Conversion/Validation.ts +++ b/Models/ThemeConfig/Conversion/Validation.ts @@ -68,7 +68,7 @@ class ValidateTheme extends DesugaringStep { const warnings = [] const information = [] - const theme = new LayoutConfig(json, true, "test") + const theme = new LayoutConfig(json, true) { // Legacy format checks diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index 402f28892..2d8598f6a 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -56,9 +56,17 @@ export default class LayoutConfig { public readonly usedImages: string[] public readonly extraLink?: ExtraLinkConfig - constructor(json: LayoutConfigJson, official = true, context?: string) { + public readonly definedAtUrl? : string; + public readonly definitionRaw?: string; + + constructor(json: LayoutConfigJson, official = true,options?: { + definedAtUrl?: string, + definitionRaw?: string + }) { this.official = official; this.id = json.id; + this.definedAtUrl = options?.definedAtUrl + this.definitionRaw = options?.definitionRaw if (official) { if (json.id.toLowerCase() !== json.id) { throw "The id of a theme should be lowercase: " + json.id @@ -67,11 +75,7 @@ export default class LayoutConfig { throw "The id of a theme should match [a-z0-9-_]*: " + json.id } } - if(context === undefined){ - context = this.id - }else{ - context = context + "." + this.id; - } + const context = this.id this.maintainer = json.maintainer; this.credits = json.credits; this.version = json.version; diff --git a/UI/BigComponents/ShareScreen.ts b/UI/BigComponents/ShareScreen.ts index a55062779..de96839bf 100644 --- a/UI/BigComponents/ShareScreen.ts +++ b/UI/BigComponents/ShareScreen.ts @@ -14,6 +14,8 @@ import BaseLayer from "../../Models/BaseLayer"; import FilteredLayer from "../../Models/FilteredLayer"; import {InputElement} from "../Input/InputElement"; import CheckBoxes, {CheckBox} from "../Input/Checkboxes"; +import {SubtleButton} from "../Base/SubtleButton"; +import LZString from "lz-string"; export default class ShareScreen extends Combine { @@ -24,14 +26,6 @@ export default class ShareScreen extends Combine { const optionCheckboxes: InputElement[] = [] const optionParts: (UIEventSource)[] = []; - function check() { - return Svg.checkmark_svg().SetStyle("width: 1.5em; display:inline-block;"); - } - - function nocheck() { - return Svg.no_checkmark_svg().SetStyle("width: 1.5em; display: inline-block;"); - } - const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true) optionCheckboxes.push(includeLocation); @@ -49,6 +43,7 @@ export default class ShareScreen extends Combine { } else { return null; } + }, [currentLocation])); @@ -119,6 +114,9 @@ export default class ShareScreen extends Combine { } + 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(() => { @@ -126,13 +124,21 @@ export default class ShareScreen extends Combine { const host = window.location.host; let path = window.location.pathname; path = path.substr(0, path.lastIndexOf("/")); - let literalText = `https://${host}${path}/${layout.id.toLowerCase()}` + 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; + return literalText + hash; } - return literalText + "?" + parts.join("&"); + return literalText + "?" + parts.join("&") + hash; }, optionParts); @@ -184,13 +190,27 @@ export default class ShareScreen extends Combine { }); + + let downloadThemeConfig: BaseUIElement = undefined; + if(layout.definitionRaw !== undefined){ + downloadThemeConfig = 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")) + } super([ - tr.intro.Clone(), + tr.intro, link, new VariableUiElement(linkStatus), - tr.addToHomeScreen.Clone(), - tr.embedIntro.Clone(), + downloadThemeConfig, + tr.addToHomeScreen, + tr.embedIntro, options, iframeCode, ]) diff --git a/Utils.ts b/Utils.ts index c8a7da1c9..b55b4d775 100644 --- a/Utils.ts +++ b/Utils.ts @@ -756,7 +756,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * Triggers a 'download file' popup which will download the contents */ public static offerContentsAsDownloadableFile(contents: string | Blob, fileName: string = "download.txt", - options?: { mimetype: string | "text/plain" | "text/csv" | "application/vnd.geo+json" | "{gpx=application/gpx+xml}" }) { + options?: { mimetype: string | "text/plain" | "text/csv" | "application/vnd.geo+json" | "{gpx=application/gpx+xml}" | "application/json" }) { const element = document.createElement("a"); let file; if (typeof (contents) === "string") { diff --git a/langs/en.json b/langs/en.json index 7d96b84c2..d96568f83 100644 --- a/langs/en.json +++ b/langs/en.json @@ -203,6 +203,8 @@ "sharescreen": { "addToHomeScreen": "

Add to your home screen

You can easily add this website to your smartphone home screen for a native feel. Click the 'Add to home screen' button in the URL bar to do this.", "copiedToClipboard": "Link copied to clipboard", + "downloadCustomTheme": "Download the configuration for this theme", + "downloadCustomThemeHelp": "An experienced contributor can use this file to improve your theme", "editThemeDescription": "Add or change questions to this map theme", "editThisTheme": "Edit this theme", "embedIntro": "

Embed on your website

Please, embed this map into your website.
We encourage you to do it - you don't even have to ask permission.
It is free, and always will be. The more people are using this, the more valuable it becomes.", diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 7711d7678..e3c311eda 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -303,7 +303,7 @@ async function main(): Promise { if (theme !== undefined && layoutConfigJson.id !== theme) { continue } - const layout = new LayoutConfig(layoutConfigJson, true, "generating layouts") + const layout = new LayoutConfig(layoutConfigJson, true) const layoutName = layout.id if (blacklist.indexOf(layoutName.toLowerCase()) >= 0) { console.log(`Skipping a layout with name${layoutName}, it is on the blacklist`); diff --git a/test/Logic/Actors/Actors.spec.ts b/test/Logic/Actors/Actors.spec.ts index 334647c56..2aed28b52 100644 --- a/test/Logic/Actors/Actors.spec.ts +++ b/test/Logic/Actors/Actors.spec.ts @@ -47,7 +47,7 @@ Utils.injectJsonDownloadForTests( ) it("should download the latest version", () => { - const state = new UserRelatedState(new LayoutConfig( bookcaseJson, true, "tests")) + const state = new UserRelatedState(new LayoutConfig( bookcaseJson, true)) const feature = { "type": "Feature", "id": "node/5568693115", diff --git a/test/Models/ThemeConfig/Conversion/FixLegacyTheme.spec.ts b/test/Models/ThemeConfig/Conversion/FixLegacyTheme.spec.ts index e61496bd2..0fbe04cca 100644 --- a/test/Models/ThemeConfig/Conversion/FixLegacyTheme.spec.ts +++ b/test/Models/ThemeConfig/Conversion/FixLegacyTheme.spec.ts @@ -145,7 +145,7 @@ describe("FixLegacyTheme", () => { walking_node_theme, "While testing") expect(fixed.errors, "Could not fix the legacy theme").empty - const theme = new LayoutConfig(fixed.result, false,"test") + const theme = new LayoutConfig(fixed.result, false) expect(theme).not.undefined })