Fix: fix #1817, some more improvements to the loading screen

This commit is contained in:
Pieter Vander Vennet 2024-03-11 01:17:33 +01:00
parent e36e594b89
commit 6394ee8e68
2 changed files with 545 additions and 523 deletions

View file

@ -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()

View file

@ -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>