forked from MapComplete/MapComplete
Fix: fix #1817, some more improvements to the loading screen
This commit is contained in:
parent
e36e594b89
commit
6394ee8e68
2 changed files with 545 additions and 523 deletions
|
@ -17,19 +17,38 @@ import * as eli_global from "../src/assets/global-raster-layers.json"
|
|||
import ValidationUtils from "../src/Models/ThemeConfig/Conversion/ValidationUtils"
|
||||
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import { QuestionableTagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import Script from "./Script"
|
||||
import crypto from "crypto"
|
||||
|
||||
const sharp = require("sharp")
|
||||
const template = readFileSync("theme.html", "utf8")
|
||||
let codeTemplate = readFileSync("src/index_theme.ts.template", "utf8")
|
||||
|
||||
function enc(str: string): string {
|
||||
class GenerateLayouts extends Script {
|
||||
private readonly template = readFileSync("theme.html", "utf8")
|
||||
private readonly codeTemplate = readFileSync("src/index_theme.ts.template", "utf8")
|
||||
private readonly removeOtherLanguages = readFileSync("src/UI/RemoveOtherLanguages.ts", "utf8")
|
||||
.split("\n")
|
||||
.slice(1)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s !== "")
|
||||
.join("\n")
|
||||
private readonly removeOtherLanguagesHash =
|
||||
"sha256-" + crypto.createHash("sha256").update(this.removeOtherLanguages).digest("base64")
|
||||
private previousSrc: Set<string> = new Set<string>()
|
||||
private eliUrlsCached: string[]
|
||||
private date = new Date().toISOString()
|
||||
|
||||
constructor() {
|
||||
super("Generates an '<theme>.html' and 'index_<theme>.ts' for every theme")
|
||||
}
|
||||
|
||||
enc(str: string): string {
|
||||
return encodeURIComponent(str.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
async function createIcon(iconPath: string, size: number, alreadyWritten: string[]) {
|
||||
async createIcon(iconPath: string, size: number, alreadyWritten: string[]) {
|
||||
let name = iconPath.split(".").slice(0, -1).join(".") // drop svg suffix
|
||||
if (name.startsWith("./")) {
|
||||
name = name.substr(2)
|
||||
name = name.substring(2)
|
||||
}
|
||||
|
||||
const newname = `assets/generated/images/${name.replace(/\//g, "_")}${size}.png`
|
||||
|
@ -57,9 +76,9 @@ async function createIcon(iconPath: string, size: number, alreadyWritten: string
|
|||
}
|
||||
|
||||
return newname
|
||||
}
|
||||
}
|
||||
|
||||
async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): Promise<string> {
|
||||
async createSocialImage(layout: LayoutConfig, template: "" | "Wide"): Promise<string> {
|
||||
if (!layout.icon.endsWith(".svg")) {
|
||||
console.warn(
|
||||
"Not creating a social image for " +
|
||||
|
@ -96,7 +115,9 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P
|
|||
return {
|
||||
$: {
|
||||
id: "icon",
|
||||
transform: `translate(${cx - r},${cy - r}) scale(${(r * 2) / Number(width)}) `,
|
||||
transform: `translate(${cx - r},${cy - r}) scale(${
|
||||
(r * 2) / Number(width)
|
||||
}) `,
|
||||
},
|
||||
g: [svg],
|
||||
}
|
||||
|
@ -114,15 +135,15 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P
|
|||
writeFileSync(path, xml)
|
||||
console.log("Created social image at ", path)
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
async function createManifest(
|
||||
async createManifest(
|
||||
layout: LayoutConfig,
|
||||
alreadyWritten: string[]
|
||||
): Promise<{
|
||||
): Promise<{
|
||||
manifest: any
|
||||
whiteIcons: string[]
|
||||
}> {
|
||||
}> {
|
||||
Translation.forcedLanguage = "en"
|
||||
const icons = []
|
||||
|
||||
|
@ -153,8 +174,8 @@ async function createManifest(
|
|||
|
||||
const sizes = [72, 96, 120, 128, 144, 152, 180, 192, 384, 512]
|
||||
for (const size of sizes) {
|
||||
const name = await createIcon(path, size, alreadyWritten)
|
||||
const whiteIcon = await createIcon(whiteBackgroundPath, size, alreadyWritten)
|
||||
const name = await this.createIcon(path, size, alreadyWritten)
|
||||
const whiteIcon = await this.createIcon(whiteBackgroundPath, size, alreadyWritten)
|
||||
whiteIcons.push(whiteIcon)
|
||||
icons.push({
|
||||
src: name,
|
||||
|
@ -196,9 +217,9 @@ async function createManifest(
|
|||
manifest,
|
||||
whiteIcons,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function asLangSpan(t: Translation, tag = "span"): string {
|
||||
asLangSpan(t: Translation, tag = "span"): string {
|
||||
const values: string[] = []
|
||||
for (const lang in t.translations) {
|
||||
if (lang === "_context") {
|
||||
|
@ -207,15 +228,11 @@ function asLangSpan(t: Translation, tag = "span"): string {
|
|||
values.push(`<${tag} lang="${lang}">${t.translations[lang]}</${tag}>`)
|
||||
}
|
||||
return values.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
let previousSrc: Set<string> = new Set<string>()
|
||||
|
||||
let eliUrlsCached: string[]
|
||||
|
||||
async function eliUrls(): Promise<string[]> {
|
||||
if (eliUrlsCached) {
|
||||
return eliUrlsCached
|
||||
async eliUrls(): Promise<string[]> {
|
||||
if (this.eliUrlsCached) {
|
||||
return this.eliUrlsCached
|
||||
}
|
||||
const urls: string[] = []
|
||||
const regex = /{switch:([^}]+)}/
|
||||
|
@ -260,17 +277,17 @@ async function eliUrls(): Promise<string[]> {
|
|||
urls.push(styleSpec["glyphs"])
|
||||
}
|
||||
}
|
||||
eliUrlsCached = urls
|
||||
this.eliUrlsCached = urls
|
||||
return Utils.NoNull(urls).sort()
|
||||
}
|
||||
}
|
||||
|
||||
async function generateCsp(
|
||||
async generateCsp(
|
||||
layout: LayoutConfig,
|
||||
layoutJson: LayoutConfigJson,
|
||||
options: {
|
||||
scriptSrcs: string[]
|
||||
}
|
||||
): Promise<string> {
|
||||
): Promise<string> {
|
||||
const apiUrls: string[] = [
|
||||
...Constants.defaultOverpassUrls,
|
||||
Constants.countryCoderEndpoint,
|
||||
|
@ -279,7 +296,7 @@ async function generateCsp(
|
|||
"https://api.openstreetmap.org",
|
||||
"https://pietervdvn.goatcounter.com",
|
||||
"https://cache.mapcomplete.org",
|
||||
].concat(...(await eliUrls()))
|
||||
].concat(...(await this.eliUrls()))
|
||||
|
||||
SpecialVisualizations.specialVisualizations.forEach((sv) => {
|
||||
if (typeof sv.needsUrls === "function") {
|
||||
|
@ -339,17 +356,16 @@ async function generateCsp(
|
|||
|
||||
const connectSrc = Array.from(hosts).sort()
|
||||
|
||||
const newSrcs = connectSrc.filter((newItem) => !previousSrc.has(newItem))
|
||||
const newSrcs = connectSrc.filter((newItem) => !this.previousSrc.has(newItem))
|
||||
|
||||
console.log(
|
||||
"Got",
|
||||
hosts.size,
|
||||
"connect-src items for theme",
|
||||
layout.id,
|
||||
"(extra sources: ",
|
||||
newSrcs.join(" ") + ")"
|
||||
newSrcs.length > 0 ? "(extra sources: " + newSrcs.join(" ") + ")" : ""
|
||||
)
|
||||
previousSrc = hosts
|
||||
this.previousSrc = hosts
|
||||
|
||||
const csp: Record<string, string> = {
|
||||
"default-src": "'self'",
|
||||
|
@ -359,9 +375,11 @@ async function generateCsp(
|
|||
"report-to": "https://report.mapcomplete.org/csp",
|
||||
"worker-src": "'self' blob:", // Vite somehow loads the worker via a 'blob'
|
||||
"style-src": "'self' 'unsafe-inline'", // unsafe-inline is needed to change the default background pin colours
|
||||
"script-src": ["'self'", "https://gc.zgo.at/count.js", ...(options?.scriptSrcs ?? [])].join(
|
||||
" "
|
||||
),
|
||||
"script-src": [
|
||||
"'self'",
|
||||
"https://gc.zgo.at/count.js",
|
||||
...(options?.scriptSrcs?.map((s) => "'" + s + "'") ?? []),
|
||||
].join(" "),
|
||||
}
|
||||
const content = Object.keys(csp)
|
||||
.map((k) => k + " " + csp[k])
|
||||
|
@ -371,15 +389,14 @@ async function generateCsp(
|
|||
`<meta http-equiv ="Report-To" content='{"group":"csp-endpoint", "max_age": 86400,"endpoints": [\{"url": "https://report.mapcomplete.org/csp"}], "include_subdomains": true}'>`,
|
||||
`<meta http-equiv="Content-Security-Policy" content="${content}">`,
|
||||
].join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
async function createLandingPage(
|
||||
async createLandingPage(
|
||||
layout: LayoutConfig,
|
||||
layoutJson: LayoutConfigJson,
|
||||
manifest,
|
||||
whiteIcons,
|
||||
alreadyWritten
|
||||
) {
|
||||
) {
|
||||
Locale.language.setData(layout.language[0])
|
||||
const targetLanguage = layout.language[0]
|
||||
const ogTitle = Translations.T(layout.title).textFor(targetLanguage).replace(/"/g, '\\"')
|
||||
|
@ -391,16 +408,16 @@ async function createLandingPage(
|
|||
let ogImage = layout.socialImage
|
||||
let twitterImage = ogImage
|
||||
if (ogImage === LayoutConfig.defaultSocialImage && layout.official) {
|
||||
ogImage = (await createSocialImage(layout, "")) ?? layout.socialImage
|
||||
twitterImage = (await createSocialImage(layout, "Wide")) ?? layout.socialImage
|
||||
ogImage = (await this.createSocialImage(layout, "")) ?? layout.socialImage
|
||||
twitterImage = (await this.createSocialImage(layout, "Wide")) ?? layout.socialImage
|
||||
}
|
||||
if (twitterImage.endsWith(".svg")) {
|
||||
// svgs are badly supported as social image, we use a generated svg instead
|
||||
twitterImage = await createIcon(twitterImage, 512, alreadyWritten)
|
||||
twitterImage = await this.createIcon(twitterImage, 512, alreadyWritten)
|
||||
}
|
||||
|
||||
if (ogImage.endsWith(".svg")) {
|
||||
ogImage = await createIcon(ogImage, 512, alreadyWritten)
|
||||
ogImage = await this.createIcon(ogImage, 512, alreadyWritten)
|
||||
}
|
||||
|
||||
let customCss = ""
|
||||
|
@ -409,7 +426,7 @@ async function createLandingPage(
|
|||
const cssContent = readFileSync(layout.customCss)
|
||||
customCss = "<style>" + cssContent + "</style>"
|
||||
} catch (e) {
|
||||
customCss = `<link rel='stylesheet' href="${layout.customCss}"/>`
|
||||
customCss = `<link rel="stylesheet" href="${layout.customCss}"/>`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -442,7 +459,7 @@ async function createLandingPage(
|
|||
|
||||
let themeSpecific = [
|
||||
`<title>${ogTitle}</title>`,
|
||||
`<link rel="manifest" href="${enc(layout.id)}.webmanifest">`,
|
||||
`<link rel="manifest" href="${this.enc(layout.id)}.webmanifest">`,
|
||||
og,
|
||||
customCss,
|
||||
`<link rel="icon" href="${icon}" sizes="any" type="image/svg+xml">`,
|
||||
|
@ -450,9 +467,10 @@ async function createLandingPage(
|
|||
].join("\n")
|
||||
|
||||
const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title })
|
||||
const templateLines = template.split("\n")
|
||||
let output = template
|
||||
.replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1"))
|
||||
// const templateLines: string[] = this.template.split("\n").slice(1) // Slice to remove the 'export {}'-line
|
||||
|
||||
return this.template
|
||||
.replace("Loading MapComplete, hang on...", this.asLangSpan(loadingText, "h1"))
|
||||
.replace(
|
||||
"Made with OpenStreetMap",
|
||||
Translations.t.general.poweredByOsm.textFor(targetLanguage)
|
||||
|
@ -460,29 +478,31 @@ async function createLandingPage(
|
|||
.replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific)
|
||||
.replace(
|
||||
/<!-- CSP -->/,
|
||||
await generateCsp(layout, layoutJson, {
|
||||
scriptSrcs: [],
|
||||
await this.generateCsp(layout, layoutJson, {
|
||||
scriptSrcs: [this.removeOtherLanguagesHash],
|
||||
})
|
||||
)
|
||||
.replace(
|
||||
/<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s,
|
||||
asLangSpan(layout.shortDescription)
|
||||
this.asLangSpan(layout.shortDescription)
|
||||
)
|
||||
.replace(
|
||||
/<!-- IMAGE-START -->.*<!-- IMAGE-END -->/s,
|
||||
"<img class='p-8 h-32 w-32 self-start' src='" + icon + "' />"
|
||||
"<img class='p-4 h-32 w-32 self-start' src='" + icon + "' />"
|
||||
)
|
||||
|
||||
.replace(
|
||||
/.*\/src\/index\.ts.*/,
|
||||
`<script type="module" src="./index_${layout.id}.ts"></script>`
|
||||
)
|
||||
.replace("Version", Constants.vNumber)
|
||||
|
||||
return output
|
||||
}
|
||||
.replace(
|
||||
/\n.*RemoveOtherLanguages.*\n/i,
|
||||
"\n<script>" + this.removeOtherLanguages + "</script>\n"
|
||||
)
|
||||
.replace("Version", `${Constants.vNumber} <div class='text-xs'>${this.date}</div>`)
|
||||
}
|
||||
|
||||
async function createIndexFor(theme: LayoutConfig) {
|
||||
async createIndexFor(theme: LayoutConfig) {
|
||||
const filename = "index_" + theme.id + ".ts"
|
||||
|
||||
const imports = [
|
||||
|
@ -490,7 +510,9 @@ async function createIndexFor(theme: LayoutConfig) {
|
|||
`import { ThemeMetaTagging } from "./src/assets/generated/metatagging/${theme.id}"`,
|
||||
]
|
||||
for (const layerName of Constants.added_by_default) {
|
||||
imports.push(`import ${layerName} from "./src/assets/generated/layers/${layerName}.json"`)
|
||||
imports.push(
|
||||
`import ${layerName} from "./src/assets/generated/layers/${layerName}.json"`
|
||||
)
|
||||
}
|
||||
writeFileSync(filename, imports.join("\n") + "\n")
|
||||
|
||||
|
@ -500,22 +522,25 @@ async function createIndexFor(theme: LayoutConfig) {
|
|||
addLayers.push(` layout.layers.push(<any> ${layerName})`)
|
||||
}
|
||||
|
||||
codeTemplate = codeTemplate.replace(" // LAYOUT.ADD_LAYERS", addLayers.join("\n"))
|
||||
let codeTemplate = this.codeTemplate.replace(
|
||||
" // LAYOUT.ADD_LAYERS",
|
||||
addLayers.join("\n")
|
||||
)
|
||||
|
||||
appendFileSync(filename, codeTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
function createDir(path) {
|
||||
createDir(path) {
|
||||
if (!existsSync(path)) {
|
||||
mkdirSync(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
async main(): Promise<void> {
|
||||
const alreadyWritten = []
|
||||
createDir("./public/assets/")
|
||||
createDir("./public/assets/generated")
|
||||
createDir("./public/assets/generated/images")
|
||||
this.createDir("./public/assets/")
|
||||
this.createDir("./public/assets/generated")
|
||||
this.createDir("./public/assets/generated/images")
|
||||
|
||||
const blacklist = [
|
||||
"",
|
||||
|
@ -554,25 +579,24 @@ async function main(): Promise<void> {
|
|||
console.log("Could not write manifest for ", layoutName, " because ", err)
|
||||
}
|
||||
}
|
||||
const { manifest, whiteIcons } = await createManifest(layout, alreadyWritten)
|
||||
const { manifest, whiteIcons } = await this.createManifest(layout, alreadyWritten)
|
||||
const manif = JSON.stringify(manifest, undefined, 2)
|
||||
const manifestLocation = encodeURIComponent(layout.id.toLowerCase()) + ".webmanifest"
|
||||
writeFile("public/" + manifestLocation, manif, err)
|
||||
|
||||
// Create a landing page for the given theme
|
||||
const landing = await createLandingPage(
|
||||
const landing = await this.createLandingPage(
|
||||
layout,
|
||||
layoutConfigJson,
|
||||
manifest,
|
||||
whiteIcons,
|
||||
alreadyWritten
|
||||
)
|
||||
|
||||
writeFile(enc(layout.id) + ".html", landing, err)
|
||||
await createIndexFor(layout)
|
||||
writeFile(this.enc(layout.id) + ".html", landing, err)
|
||||
await this.createIndexFor(layout)
|
||||
}
|
||||
|
||||
const { manifest } = await createManifest(
|
||||
const { manifest } = await this.createManifest(
|
||||
new LayoutConfig({
|
||||
icon: "./assets/svg/mapcomplete_logo.svg",
|
||||
id: "index",
|
||||
|
@ -589,9 +613,7 @@ async function main(): Promise<void> {
|
|||
|
||||
const manif = JSON.stringify(manifest, undefined, 2)
|
||||
writeFileSync("public/index.webmanifest", manif)
|
||||
}
|
||||
}
|
||||
|
||||
ScriptUtils.fixUtils()
|
||||
main().then(() => {
|
||||
console.log("All done!")
|
||||
})
|
||||
new GenerateLayouts().run()
|
||||
|
|
|
@ -59,12 +59,12 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-start w-full">
|
||||
<div class="flex justify-between items-end w-full">
|
||||
|
||||
<!-- IMAGE-START -->
|
||||
<img aria-hidden="true" class="p-8 h-32 w-32 self-start" src="./assets/svg/add.svg">
|
||||
<img aria-hidden="true" class="p-4 h-32 w-32 self-start" src="./assets/svg/add.svg">
|
||||
<!-- IMAGE-END -->
|
||||
<div class="h-min subtle">
|
||||
<div class="h-min subtle flex flex-col items-end">
|
||||
Version
|
||||
</div>
|
||||
|
||||
|
|
Loading…
Reference in a new issue