Add a download button, improve share functionality for custom themes

This commit is contained in:
pietervdvn 2022-04-18 02:39:30 +02:00
parent 303ccfa322
commit 6ad64e3f70
10 changed files with 64 additions and 40 deletions

View file

@ -14,7 +14,6 @@ import {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
import SharedTagRenderings from "../Customizations/SharedTagRenderings"; import SharedTagRenderings from "../Customizations/SharedTagRenderings";
import * as known_layers from "../assets/generated/known_layers.json" 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 {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme";
import * as licenses from "../assets/generated/license_info.json" import * as licenses from "../assets/generated/license_info.json"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
@ -43,10 +42,6 @@ export default class DetermineLayout {
} }
let layoutId: string = undefined let layoutId: string = undefined
if (location.href.indexOf("buurtnatuur.be") >= 0) {
layoutId = "buurtnatuur"
}
const path = window.location.pathname.split("/").slice(-1)[0]; const path = window.location.pathname.split("/").slice(-1)[0];
if (path !== "theme.html" && path !== "") { if (path !== "theme.html" && path !== "") {
@ -72,7 +67,7 @@ export default class DetermineLayout {
public static LoadLayoutFromHash( public static LoadLayoutFromHash(
userLayoutParam: UIEventSource<string> userLayoutParam: UIEventSource<string>
): (LayoutConfig & {definition: LayoutConfigJson}) | null { ): LayoutConfig | null {
let hash = location.hash.substr(1); let hash = location.hash.substr(1);
let json: any; let json: any;
@ -113,9 +108,7 @@ export default class DetermineLayout {
const layoutToUse = DetermineLayout.prepCustomTheme(json) const layoutToUse = DetermineLayout.prepCustomTheme(json)
userLayoutParam.setData(layoutToUse.id); userLayoutParam.setData(layoutToUse.id);
const config = new LayoutConfig(layoutToUse, false); return layoutToUse
config["definition"] = json
return <any> config
} catch (e) { } catch (e) {
console.error(e) console.error(e)
if (hash === undefined || hash.length < 10) { if (hash === undefined || hash.length < 10) {
@ -144,7 +137,7 @@ export default class DetermineLayout {
.AttachTo("centermessage"); .AttachTo("centermessage");
} }
private static prepCustomTheme(json: any): LayoutConfigJson { private static prepCustomTheme(json: any, sourceUrl?: string): LayoutConfig {
if(json.layers === undefined && json.tagRenderings !== undefined){ if(json.layers === undefined && json.tagRenderings !== undefined){
const iconTr = json.mapRendering.map(mr => mr.icon).find(icon => icon !== 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<string, LayerConfigJson>() const knownLayersDict = new Map<string, LayerConfigJson>()
for (const key in known_layers.layers) { for (const key in known_layers.layers) {
const layer = known_layers.layers[key] const layer = known_layers.layers[key]
@ -172,10 +164,17 @@ export default class DetermineLayout {
sharedLayers: knownLayersDict sharedLayers: knownLayersDict
} }
json = new FixLegacyTheme().convertStrict(json, "While loading a dynamic theme") 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 FixImages(DetermineLayout._knownImages).convertStrict(json, "While fixing the images")
json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme") json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme")
console.log("The layoutconfig is ", json) 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<LayoutConfig | null> { private static async LoadRemoteTheme(link: string): Promise<LayoutConfig | null> {
@ -190,8 +189,7 @@ export default class DetermineLayout {
try { try {
parsed.id = link; parsed.id = link;
console.log("Loaded remote link:", link) console.log("Loaded remote link:", link)
const layoutToUse = DetermineLayout.prepCustomTheme(parsed) return DetermineLayout.prepCustomTheme(parsed, link)
return new LayoutConfig(layoutToUse, false)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
DetermineLayout.ShowErrorOnCustomTheme( DetermineLayout.ShowErrorOnCustomTheme(

View file

@ -2,7 +2,7 @@ import {Utils} from "../Utils";
export default class Constants { export default class Constants {
public static vNumber = "0.18.0"; public static vNumber = "0.18.1";
public static ImgurApiKey = '7070e7167f0a25a' public static ImgurApiKey = '7070e7167f0a25a'
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"

View file

@ -68,7 +68,7 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
const warnings = [] const warnings = []
const information = [] const information = []
const theme = new LayoutConfig(json, true, "test") const theme = new LayoutConfig(json, true)
{ {
// Legacy format checks // Legacy format checks

View file

@ -56,9 +56,17 @@ export default class LayoutConfig {
public readonly usedImages: string[] public readonly usedImages: string[]
public readonly extraLink?: ExtraLinkConfig 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.official = official;
this.id = json.id; this.id = json.id;
this.definedAtUrl = options?.definedAtUrl
this.definitionRaw = options?.definitionRaw
if (official) { if (official) {
if (json.id.toLowerCase() !== json.id) { if (json.id.toLowerCase() !== json.id) {
throw "The id of a theme should be lowercase: " + 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 throw "The id of a theme should match [a-z0-9-_]*: " + json.id
} }
} }
if(context === undefined){ const context = this.id
context = this.id
}else{
context = context + "." + this.id;
}
this.maintainer = json.maintainer; this.maintainer = json.maintainer;
this.credits = json.credits; this.credits = json.credits;
this.version = json.version; this.version = json.version;

View file

@ -14,6 +14,8 @@ import BaseLayer from "../../Models/BaseLayer";
import FilteredLayer from "../../Models/FilteredLayer"; import FilteredLayer from "../../Models/FilteredLayer";
import {InputElement} from "../Input/InputElement"; import {InputElement} from "../Input/InputElement";
import CheckBoxes, {CheckBox} from "../Input/Checkboxes"; import CheckBoxes, {CheckBox} from "../Input/Checkboxes";
import {SubtleButton} from "../Base/SubtleButton";
import LZString from "lz-string";
export default class ShareScreen extends Combine { export default class ShareScreen extends Combine {
@ -24,14 +26,6 @@ export default class ShareScreen extends Combine {
const optionCheckboxes: InputElement<boolean>[] = [] const optionCheckboxes: InputElement<boolean>[] = []
const optionParts: (UIEventSource<string>)[] = []; const optionParts: (UIEventSource<string>)[] = [];
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) const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true)
optionCheckboxes.push(includeLocation); optionCheckboxes.push(includeLocation);
@ -49,6 +43,7 @@ export default class ShareScreen extends Combine {
} else { } else {
return null; return null;
} }
}, [currentLocation])); }, [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 options = new Combine(optionCheckboxes).SetClass("flex flex-col")
const url = (currentLocation ?? new UIEventSource(undefined)).map(() => { const url = (currentLocation ?? new UIEventSource(undefined)).map(() => {
@ -126,13 +124,21 @@ export default class ShareScreen extends Combine {
const host = window.location.host; const host = window.location.host;
let path = window.location.pathname; let path = window.location.pathname;
path = path.substr(0, path.lastIndexOf("/")); 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))); const parts = Utils.NoEmpty(Utils.NoNull(optionParts.map((eventSource) => eventSource.data)));
if (parts.length === 0) { if (parts.length === 0) {
return literalText; return literalText + hash;
} }
return literalText + "?" + parts.join("&"); return literalText + "?" + parts.join("&") + hash;
}, optionParts); }, optionParts);
@ -185,12 +191,26 @@ 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([ super([
tr.intro.Clone(), tr.intro,
link, link,
new VariableUiElement(linkStatus), new VariableUiElement(linkStatus),
tr.addToHomeScreen.Clone(), downloadThemeConfig,
tr.embedIntro.Clone(), tr.addToHomeScreen,
tr.embedIntro,
options, options,
iframeCode, iframeCode,
]) ])

View file

@ -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 * Triggers a 'download file' popup which will download the contents
*/ */
public static offerContentsAsDownloadableFile(contents: string | Blob, fileName: string = "download.txt", 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"); const element = document.createElement("a");
let file; let file;
if (typeof (contents) === "string") { if (typeof (contents) === "string") {

View file

@ -203,6 +203,8 @@
"sharescreen": { "sharescreen": {
"addToHomeScreen": "<h3>Add to your home screen</h3>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.", "addToHomeScreen": "<h3>Add to your home screen</h3>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", "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", "editThemeDescription": "Add or change questions to this map theme",
"editThisTheme": "Edit this theme", "editThisTheme": "Edit this theme",
"embedIntro": "<h3>Embed on your website</h3>Please, embed this map into your website. <br/>We encourage you to do it - you don't even have to ask permission. <br/> It is free, and always will be. The more people are using this, the more valuable it becomes.", "embedIntro": "<h3>Embed on your website</h3>Please, embed this map into your website. <br/>We encourage you to do it - you don't even have to ask permission. <br/> It is free, and always will be. The more people are using this, the more valuable it becomes.",

View file

@ -303,7 +303,7 @@ async function main(): Promise<void> {
if (theme !== undefined && layoutConfigJson.id !== theme) { if (theme !== undefined && layoutConfigJson.id !== theme) {
continue continue
} }
const layout = new LayoutConfig(layoutConfigJson, true, "generating layouts") const layout = new LayoutConfig(layoutConfigJson, true)
const layoutName = layout.id const layoutName = layout.id
if (blacklist.indexOf(layoutName.toLowerCase()) >= 0) { if (blacklist.indexOf(layoutName.toLowerCase()) >= 0) {
console.log(`Skipping a layout with name${layoutName}, it is on the blacklist`); console.log(`Skipping a layout with name${layoutName}, it is on the blacklist`);

View file

@ -47,7 +47,7 @@ Utils.injectJsonDownloadForTests(
) )
it("should download the latest version", () => { it("should download the latest version", () => {
const state = new UserRelatedState(new LayoutConfig(<any> bookcaseJson, true, "tests")) const state = new UserRelatedState(new LayoutConfig(<any> bookcaseJson, true))
const feature = { const feature = {
"type": "Feature", "type": "Feature",
"id": "node/5568693115", "id": "node/5568693115",

View file

@ -145,7 +145,7 @@ describe("FixLegacyTheme", () => {
<any> walking_node_theme, <any> walking_node_theme,
"While testing") "While testing")
expect(fixed.errors, "Could not fix the legacy theme").empty 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 expect(theme).not.undefined
}) })