forked from MapComplete/MapComplete
Merge branch 'develop' into feature/eslint
This commit is contained in:
commit
ce897d28df
1450 changed files with 20081 additions and 16531 deletions
|
|
@ -35,7 +35,7 @@
|
|||
src={`https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/community_index/${resource.type}.svg`}
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<a href={resource.resolved.url} target="_blank" rel="noreferrer nofollow" class="font-bold">
|
||||
<a href={resource.resolved.url} target="_blank" rel="noreferrer nofollow noopener" class="font-bold">
|
||||
{resource.resolved.name ?? resource.resolved.url}
|
||||
</a>
|
||||
{resource.resolved?.description}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export default class CopyrightPanel extends Combine {
|
|||
let bgAttr: BaseUIElement | string = undefined
|
||||
if (attrText && attrUrl) {
|
||||
bgAttr =
|
||||
"<a href='" + attrUrl + "' target='_blank'>" + attrText + "</a>"
|
||||
"<a href='" + attrUrl + "' target='_blank' rel='noopener'>" + attrText + "</a>"
|
||||
} else if (attrUrl) {
|
||||
bgAttr = attrUrl
|
||||
} else {
|
||||
|
|
|
|||
121
src/UI/BigComponents/ShareScreen.svelte
Normal file
121
src/UI/BigComponents/ShareScreen.svelte
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<script lang="ts">/**
|
||||
* A screen showing:
|
||||
* - A link to share the current view
|
||||
* - Some query parameters that can be enabled/disabled
|
||||
* - The code to embed MC as IFrame
|
||||
*/
|
||||
|
||||
import ThemeViewState from "../../Models/ThemeViewState";
|
||||
import { QueryParameters } from "../../Logic/Web/QueryParameters";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import { Utils } from "../../Utils";
|
||||
import Svg from "../../Svg";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import { DocumentDuplicateIcon } from "@rgossiaux/svelte-heroicons/outline";
|
||||
|
||||
export let state: ThemeViewState;
|
||||
const tr = Translations.t.general.sharescreen;
|
||||
|
||||
let url = window.location;
|
||||
let linkToShare: string = undefined;
|
||||
/**
|
||||
* In some cases (local deploys, custom themes), we need to set the URL to `/theme.html?layout=xyz` instead of `/xyz?...`
|
||||
*/
|
||||
let needsThemeRedirect = url.port !== "" || url.hostname.match(/^[0-9]/) || !state.layout.official;
|
||||
let layoutId = state.layout.id;
|
||||
let baseLink = url.protocol + "//" + url.host + "/" + (needsThemeRedirect ? "theme.html?layout=" + layoutId + "&" : layoutId + "?");
|
||||
|
||||
let showWelcomeMessage = true;
|
||||
let enableLogin = true;
|
||||
$: {
|
||||
const layout = state.layout;
|
||||
let excluded = Utils.NoNull([
|
||||
showWelcomeMessage ? undefined : "fs-welcome-message",
|
||||
enableLogin ? undefined : "fs-enable-login"
|
||||
]);
|
||||
linkToShare = baseLink + QueryParameters.GetParts(new Set(excluded))
|
||||
.concat(excluded.map(k => k + "=" + false))
|
||||
.join("&");
|
||||
if (layout.definitionRaw !== undefined) {
|
||||
linkToShare += "&userlayout=" + (layout.definedAtUrl ?? layout.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function shareCurrentLink() {
|
||||
await navigator.share({
|
||||
title: Translations.W(state.layout.title)?.ConstructElement().textContent ?? "MapComplete",
|
||||
text: Translations.W(state.layout.description)?.ConstructElement().textContent ?? "",
|
||||
url: linkToShare
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
let isCopied = false;
|
||||
|
||||
async function copyCurrentLink() {
|
||||
await navigator.clipboard.writeText(linkToShare);
|
||||
isCopied = true;
|
||||
await Utils.waitFor(5000);
|
||||
isCopied = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
<Tr t={tr.intro} />
|
||||
<div class="flex">
|
||||
{#if typeof navigator?.share === "function"}
|
||||
<button class="w-8 h-8 p-1 shrink-0" on:click={shareCurrentLink}>
|
||||
<ToSvelte construct={Svg.share_svg()} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if navigator.clipboard !== undefined}
|
||||
<button class="w-8 h-8 p-1 shrink-0 no-image-background" on:click={copyCurrentLink}>
|
||||
<DocumentDuplicateIcon />
|
||||
</button>
|
||||
{/if}
|
||||
<div class="literal-code" on:click={e => Utils.selectTextIn(e.target)}>
|
||||
{linkToShare}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
|
||||
{#if isCopied}
|
||||
<Tr t={tr.copiedToClipboard} cls="thanks m-2" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<Tr t={ tr.embedIntro} />
|
||||
|
||||
|
||||
<div class="flex flex-col my-1 link-underline">
|
||||
|
||||
<label>
|
||||
<input bind:checked={showWelcomeMessage} type="checkbox" />
|
||||
<Tr t={tr.fsWelcomeMessage} />
|
||||
</label>
|
||||
|
||||
|
||||
<label>
|
||||
<input bind:checked={enableLogin} type="checkbox" />
|
||||
<Tr t={tr.fsUserbadge} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="literal-code m-1">
|
||||
<span class="literal-code iframe-code-block"> <br />
|
||||
<iframe src="${url}" <br />
|
||||
allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" <br />
|
||||
title="${state.layout.title?.txt ?? "MapComplete" } with MapComplete"> <br />
|
||||
</iframe> <br />
|
||||
</span>
|
||||
</div>
|
||||
<Tr t={tr.documentation} cls="link-underline"/>
|
||||
</div>
|
||||
|
|
@ -1,257 +0,0 @@
|
|||
/* eslint-disable prefer-const */
|
||||
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<boolean>[] = []
|
||||
const optionParts: Store<string>[] = []
|
||||
|
||||
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<boolean>
|
||||
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<string, string> } | 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 `<span class='literal-code iframe-code-block'>
|
||||
<iframe src="${url}" allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" title="${
|
||||
layout.title?.txt ?? "MapComplete"
|
||||
} with MapComplete"></iframe>
|
||||
</span>`
|
||||
})
|
||||
)
|
||||
|
||||
const linkStatus = new UIEventSource<string | Translation>("")
|
||||
const link = new VariableUiElement(
|
||||
url.map(
|
||||
(url) =>
|
||||
`<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%">`
|
||||
)
|
||||
).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")
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import SubtleLink from "../Base/SubtleLink.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
export let theme: LayoutInformation
|
||||
export let isCustom: boolean = false
|
||||
|
|
|
|||
|
|
@ -45,9 +45,7 @@
|
|||
<Tr t={layout.description} />
|
||||
<Tr t={Translations.t.general.welcomeExplanation.general} />
|
||||
{#if layout.layers.some((l) => l.presets?.length > 0)}
|
||||
<If condition={state.featureSwitches.featureSwitchAddNew}>
|
||||
<Tr t={Translations.t.general.welcomeExplanation.addNew} />
|
||||
</If>
|
||||
{/if}
|
||||
|
||||
<Tr t={layout.descriptionTail} />
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
<div class="gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each filteredThemes as theme (theme.id)}
|
||||
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
|
||||
<!-- TODO: doesn't work if first theme is hidden -->
|
||||
<!-- TODO: doesn't work if first theme is hidden -->
|
||||
{#if theme === firstTheme && !isCustom && $search !== "" && $search !== undefined}
|
||||
<ThemeButton
|
||||
{theme}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
<a
|
||||
href={osmConnection.Backend() + "/profile/edit"}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="link-no-underline flex items-center self-end"
|
||||
>
|
||||
<PencilAltIcon slot="image" class="h-8 w-8 p-2" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue