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` | ||||
|  | @ -59,7 +78,7 @@ 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], | ||||
|                 } | ||||
|  | @ -116,7 +137,7 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P | |||
|         return path | ||||
|     } | ||||
| 
 | ||||
| async function createManifest( | ||||
|     async createManifest( | ||||
|         layout: LayoutConfig, | ||||
|         alreadyWritten: string[] | ||||
|     ): Promise<{ | ||||
|  | @ -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, | ||||
|  | @ -198,7 +219,7 @@ async function createManifest( | |||
|         } | ||||
|     } | ||||
| 
 | ||||
| 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") { | ||||
|  | @ -209,13 +230,9 @@ function asLangSpan(t: Translation, tag = "span"): string { | |||
|         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,11 +277,11 @@ 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: { | ||||
|  | @ -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]) | ||||
|  | @ -373,10 +391,9 @@ async function generateCsp( | |||
|         ].join("\n") | ||||
|     } | ||||
| 
 | ||||
| async function createLandingPage( | ||||
|     async createLandingPage( | ||||
|         layout: LayoutConfig, | ||||
|         layoutJson: LayoutConfigJson, | ||||
|     manifest, | ||||
|         whiteIcons, | ||||
|         alreadyWritten | ||||
|     ) { | ||||
|  | @ -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", | ||||
|  | @ -590,8 +614,6 @@ 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…
	
	Add table
		Add a link
		
	
		Reference in a new issue