diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java index 7b53f4effe7..e2c0528d312 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java @@ -5349,6 +5349,31 @@ private JavaScriptRuntimeEnvironment createRuntimeEnvironment(final WindowExt wi ); } + /// Remembers the application's light/dark choice for the loading screen of the NEXT launch: + /// `light` or `dark`, or null/anything else to follow the browser again. The page reads the + /// key in `` before any Java runs (`JavascriptBundleWriter.splashTheme` in the + /// ByteCodeTranslator, which also names the key); a build whose `javascript.splash.appearance` + /// is `light` or `dark` ignores it. + private static void setSplashAppearance(String value) { + // The cast stays outside the try: a CHECKCAST is unchecked on ParparVM, so a handler + // around it would never run (scripts/check-cast-semantics.sh). + WindowExt win = (WindowExt) Window.current(); + try { + JSOImplementations.JSStorage storage = win.getLocalStorage(); + if (storage == null) { + return; + } + if ("light".equals(value) || "dark".equals(value)) { + storage.setItem("cn1.splashAppearance", value); + } else { + storage.removeItem("cn1.splashAppearance"); + } + } catch (RuntimeException e) { + // Storage disabled (private mode, a sandboxed frame): the splash follows the browser. + Log.e(e); + } + } + @JSBody(params={"msg"}, script="window.onbeforeunload=function(){return msg;}") private native static void setBeforeUnloadMessage(String msg); @@ -5357,6 +5382,9 @@ private JavaScriptRuntimeEnvironment createRuntimeEnvironment(final WindowExt wi @Override public void setPlatformHint(String key, String value) { + if ("platformHint.javascript.splashAppearance".equalsIgnoreCase(key)) { + setSplashAppearance(value); + } if ("platformHint.javascript.beforeUnloadMessage".equalsIgnoreCase(key)) { if (value == null) { removeBeforeUnload(); diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java index 0147e4d636e..adaeba3d29d 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java @@ -66,7 +66,18 @@ public interface JSFontMetrics extends JSObject { JSObject getBounds(); } + /// The page's `window.localStorage`, for the few values the page itself reads before any + /// Java runs (the loading screen's remembered appearance). + public interface JSStorage extends JSObject { + void setItem(String key, String value); + + void removeItem(String key); + } + public interface WindowExt extends JSObject { + + @JSProperty + abstract JSStorage getLocalStorage(); @JSProperty diff --git a/Ports/JavaScriptPort/src/main/webapp/port.js b/Ports/JavaScriptPort/src/main/webapp/port.js index 182606143d2..9d15a732616 100644 --- a/Ports/JavaScriptPort/src/main/webapp/port.js +++ b/Ports/JavaScriptPort/src/main/webapp/port.js @@ -4983,6 +4983,8 @@ const html5HideSplashWorkerSymbol = "cn1_com_codename1_impl_html5_HTML5Implement if (typeof document !== "undefined" && typeof globalThis !== "undefined" && typeof globalThis.jQuery === "function") { try { + // Same as the host handler: the page drops the splash background once started. + document.documentElement.setAttribute("data-cn1-started", ""); globalThis.jQuery("div#cn1-splash").fadeOut(100, function() { globalThis.jQuery(this).remove(); }); diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/WorkingWithJavascriptSplashAppearanceSnippet.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/WorkingWithJavascriptSplashAppearanceSnippet.java new file mode 100644 index 00000000000..796d3cc2478 --- /dev/null +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/WorkingWithJavascriptSplashAppearanceSnippet.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +package com.codenameone.developerguide.snippets.generated; + +import com.codename1.ui.CN; + +class WorkingWithJavascriptSplashAppearanceSnippet { + void snippet() { + // tag::working-with-javascript-splash-appearance[] + CN.setProperty("platformHint.javascript.splashAppearance", "dark"); // or "light" + CN.setProperty("platformHint.javascript.splashAppearance", null); // follow the browser again + // end::working-with-javascript-splash-appearance[] + } +} diff --git a/docs/demos/javascript/src/main/snippets/developer-guide/working-with-javascript.html b/docs/demos/javascript/src/main/snippets/developer-guide/working-with-javascript.html index 093262deb4f..551391157dd 100644 --- a/docs/demos/javascript/src/main/snippets/developer-guide/working-with-javascript.html +++ b/docs/demos/javascript/src/main/snippets/developer-guide/working-with-javascript.html @@ -1,14 +1,5 @@ // Generated from docs/developer-guide source blocks. Edit the guide snippets here, not inline. -// tag::working-with-javascript-html-001[] -
- - - -

...Loading...

-
-// end::working-with-javascript-html-001[] - // tag::working-with-javascript-html-002[] // end::working-with-javascript-html-002[] diff --git a/docs/developer-guide/Working-With-Javascript.asciidoc b/docs/developer-guide/Working-With-Javascript.asciidoc index a489cc6c3d1..e15f8b75041 100644 --- a/docs/developer-guide/Working-With-Javascript.asciidoc +++ b/docs/developer-guide/Working-With-Javascript.asciidoc @@ -262,15 +262,44 @@ This tells Apache to proxy all requests for '/app' to the domain https://www.mye [id="splash_screen", reftext="Customizing the Splash Screen"] === Customizing the splash screen -Since your application may include many resource files, videos, etc., the build-server will generate a splash screen for your app to display while it's loading. This basically shows a progress indicator with your app's icon. +While the application downloads and starts, the page shows a loading screen: the app icon, an animated progress bar and a "Loading" label. It stays up until the first form is shown. -You can customize this splash screen by modifying the HTML source inside the *cn1-splash* `div` tag of your app's index.html file: +Build hints set its look, so the settings survive every build instead of being edited into each generated bundle: -[source,html] +[cols="1,1,3",options="header"] +|=== +|Build hint |Default |Meaning + +|`javascript.splash.appearance` +|`auto` once any dark color is set, otherwise `light` +|`auto` follows the browser's light/dark preference. `light` or `dark` always uses that set of colors, for an app with a single appearance. An app that sets none of these hints keeps a light loading screen, so a light app never opens dark. + +|`javascript.splash.background`, `javascript.splash.textColor`, `javascript.splash.accentColor` +|`#ffffff`, `#666666`, `#0072ff` +|The page background, the label and the progress bar in the light appearance. + +|`javascript.splash.darkBackground`, `javascript.splash.darkTextColor`, `javascript.splash.darkAccentColor` +|`#121212`, `#b3b3b3`, `#4da3ff` +|The same three in the dark appearance. + +|`javascript.splash.logo` +|The app icon +|A different image, as a path relative to the bundle's `index.html`, or `none` for no image. +|=== + +Colors are hex values (`#rgb`, `#rrggbb`, with optional alpha) or CSS color names. The page applies them before it paints anything, and keeps the background until the first form is shown, so a dark app no longer opens on a white page. + +An app that lets the user choose its own appearance can have the next launch's loading screen match that choice, even when it differs from the browser's preference: + +[source,java] ---- -include::../demos/javascript/src/main/snippets/developer-guide/working-with-javascript.html[tag=working-with-javascript-html-001,indent=0] +include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/WorkingWithJavascriptSplashAppearanceSnippet.java[tag=working-with-javascript-splash-appearance,indent=0] ---- +The choice is kept in the browser's local storage and applies only in `auto` mode. + +For anything the hints don't cover, the markup is the `cn1-splash` `div` in the generated `index.html`; edit it there if you host the bundle yourself. + [id="debugging", reftext="Debugging in Chrome"] === Debugging diff --git a/maven/build-engine/src/main/java/com/codename1/builders/JavaScriptBuildHints.java b/maven/build-engine/src/main/java/com/codename1/builders/JavaScriptBuildHints.java index 746b00ba83f..c84810dc559 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/JavaScriptBuildHints.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/JavaScriptBuildHints.java @@ -89,6 +89,19 @@ public final class JavaScriptBuildHints { "javascript.native.theme", "javascript.desktopTheme" }; + /// The `javascript.splash.*` hints that style the loading screen, each passed to the + /// translator as `codename1.` (see `JavascriptBundleWriter.SplashSettings`, which + /// validates them). + static final String[] SPLASH_HINTS = { + "javascript.splash.appearance", "javascript.splash.background", + "javascript.splash.textColor", "javascript.splash.accentColor", + "javascript.splash.darkBackground", "javascript.splash.darkTextColor", + "javascript.splash.darkAccentColor", "javascript.splash.logo" + }; + + /// The 192px icon `JavaScriptPwa` writes into every bundle. + static final String SPLASH_DEFAULT_LOGO = "_icons/icon-192.png"; + private JavaScriptBuildHints() { } @@ -126,6 +139,20 @@ static List translatorOptions(BuildRequest request) { if (darkReader != null) { out.add("-Dcodename1.javascript.darkreaderLock=" + !"false".equalsIgnoreCase(darkReader.trim())); } + for (String hint : SPLASH_HINTS) { + String value = request.getArg(hint, null); + if (value != null && value.trim().length() > 0) { + out.add("-Dcodename1." + hint + "=" + value.trim()); + } + } + // The loading screen shows the application's icon unless the developer named another + // logo (or none). JavaScriptPwa writes the icon to this path after the translator runs, + // from the same request.getIcon(); without an icon it writes a placeholder square, which + // identifies nothing, so then there is no logo. + String logo = request.getArg("javascript.splash.logo", null); + if ((logo == null || logo.trim().length() == 0) && request.getIcon() != null) { + out.add("-Dcodename1.javascript.splash.logo=" + SPLASH_DEFAULT_LOGO); + } return out; } diff --git a/maven/build-engine/src/test/java/com/codename1/builders/JavaScriptBuildHintsTest.java b/maven/build-engine/src/test/java/com/codename1/builders/JavaScriptBuildHintsTest.java index 9f079da8cb1..b7d25274ba5 100644 --- a/maven/build-engine/src/test/java/com/codename1/builders/JavaScriptBuildHintsTest.java +++ b/maven/build-engine/src/test/java/com/codename1/builders/JavaScriptBuildHintsTest.java @@ -107,6 +107,29 @@ void translatorGetsTheTitleAndOnlyTheOptOutsThatWereSet() { assertTrue(opts.contains("-Dcodename1.javascript.darkreaderLock=false"), opts.toString()); } + @Test + void splashHintsReachTheTranslatorAndTheIconIsTheDefaultLogo() { + BuildRequest r = request("javascript.splash.appearance", " dark ", + "javascript.splash.darkBackground", "#1e1e1e"); + List opts = JavaScriptBuildHints.translatorOptions(r); + assertTrue(opts.contains("-Dcodename1.javascript.splash.appearance=dark"), opts.toString()); + assertTrue(opts.contains("-Dcodename1.javascript.splash.darkBackground=#1e1e1e"), opts.toString()); + // No icon: the PWA step writes a placeholder square, which is not worth showing. + assertFalse(opts.toString().contains("splash.logo"), opts.toString()); + + r.setIcon(new byte[] {1}); + opts = JavaScriptBuildHints.translatorOptions(r); + assertTrue(opts.contains("-Dcodename1.javascript.splash.logo=" + JavaScriptBuildHints.SPLASH_DEFAULT_LOGO), + opts.toString()); + + r = request("javascript.splash.logo", "none"); + r.setIcon(new byte[] {1}); + opts = JavaScriptBuildHints.translatorOptions(r); + assertTrue(opts.contains("-Dcodename1.javascript.splash.logo=none"), opts.toString()); + assertFalse(opts.contains("-Dcodename1.javascript.splash.logo=" + JavaScriptBuildHints.SPLASH_DEFAULT_LOGO), + opts.toString()); + } + @Test void noHintsShipsOnlyTheLegacyPair() { assertEquals(set("iOS7Theme", "android_holo_light"), ship(request())); diff --git a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java index 1a44124a328..3ff9998de98 100644 --- a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java +++ b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java @@ -111,6 +111,79 @@ static void register(List h) { + "lock in its default Dynamic mode only; its Filter, Filter+ and Static modes " + "ignore it. Set to `false` to omit the tag.")); + h.add(new Hint("javascript.splash.appearance") + .group(HintGroup.JAVASCRIPT) + .values("SplashAppearance", "auto", "light", "dark") + .platform("javascript") + .doc("Which colors the loading screen uses. Defaults to `auto` when any " + + "`javascript.splash.dark*` color is set and to `light` otherwise, so an app " + + "that never asked for a dark loading screen doesn't get one. `auto` follows the " + + "browser's light/dark preference, or the choice the app remembered with " + + "`Display.setProperty(\"platformHint.javascript.splashAppearance\", \"dark\")`. " + + "`light` and `dark` always use that set, for apps with one appearance.")); + + h.add(new Hint("javascript.splash.background") + .group(HintGroup.JAVASCRIPT) + .type(HintType.STRING) + .def("#ffffff") + .platform("javascript") + .doc("The loading screen's background in light appearance, as a hex color (`#rgb`, `#rrggbb`, " + + "with optional alpha) or a CSS color name. Applied from the first frame the " + + "page paints until the first form shows.")); + + h.add(new Hint("javascript.splash.textColor") + .group(HintGroup.JAVASCRIPT) + .type(HintType.STRING) + .def("#666666") + .platform("javascript") + .doc("The loading screen's text color in light appearance, as a hex color (`#rgb`, `#rrggbb`, " + + "with optional alpha) or a CSS color name. Applied from the first frame the " + + "page paints until the first form shows.")); + + h.add(new Hint("javascript.splash.accentColor") + .group(HintGroup.JAVASCRIPT) + .type(HintType.STRING) + .def("#0072ff") + .platform("javascript") + .doc("The loading screen's progress bar color in light appearance, as a hex color (`#rgb`, `#rrggbb`, " + + "with optional alpha) or a CSS color name. Applied from the first frame the " + + "page paints until the first form shows.")); + + h.add(new Hint("javascript.splash.darkBackground") + .group(HintGroup.JAVASCRIPT) + .type(HintType.STRING) + .def("#121212") + .platform("javascript") + .doc("The loading screen's background in dark appearance, as a hex color (`#rgb`, `#rrggbb`, " + + "with optional alpha) or a CSS color name. Applied from the first frame the " + + "page paints until the first form shows.")); + + h.add(new Hint("javascript.splash.darkTextColor") + .group(HintGroup.JAVASCRIPT) + .type(HintType.STRING) + .def("#b3b3b3") + .platform("javascript") + .doc("The loading screen's text color in dark appearance, as a hex color (`#rgb`, `#rrggbb`, " + + "with optional alpha) or a CSS color name. Applied from the first frame the " + + "page paints until the first form shows.")); + + h.add(new Hint("javascript.splash.darkAccentColor") + .group(HintGroup.JAVASCRIPT) + .type(HintType.STRING) + .def("#4da3ff") + .platform("javascript") + .doc("The loading screen's progress bar color in dark appearance, as a hex color (`#rgb`, `#rrggbb`, " + + "with optional alpha) or a CSS color name. Applied from the first frame the " + + "page paints until the first form shows.")); + + h.add(new Hint("javascript.splash.logo") + .group(HintGroup.JAVASCRIPT) + .type(HintType.STRING) + .platform("javascript") + .doc("The image the loading screen shows above the progress bar, as a path relative to " + + "the bundle's `index.html` (letters, digits, `/`, `.`, `-` and `_`). Defaults to the " + + "app icon; `none` shows no image.")); + h.add(new Hint("javascript.desktopTheme") .group(HintGroup.JAVASCRIPT) .type(HintType.STRING) diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java index e7e196e75a5..66607c21c39 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java @@ -1693,6 +1693,7 @@ private static void writeIndex(File outputDirectory) throws IOException { System.getProperty(PAGE_TITLE_PROPERTY), Boolean.parseBoolean(Util.systemProperty(ALLOW_TRANSLATION_PROPERTY, "false")), !"false".equalsIgnoreCase(Util.systemProperty(DARKREADER_LOCK_PROPERTY, "true"))); + index = applySplashSettings(index, SplashSettings.fromSystemProperties()); Util.writeUtf8(new File(outputDirectory, "index.html"), index); } @@ -1741,6 +1742,206 @@ static String applyPageSettings(String index, String title, boolean allowTransla .replace("__APP_TITLE__", escapeHtml(pageTitle)); } + /// Prefix of the translator properties the `javascript.splash.*` build hints become. + static final String SPLASH_PROPERTY_PREFIX = "codename1.javascript.splash."; + + /// Local storage key an application writes, through the + /// `platformHint.javascript.splashAppearance` property, to have the next launch's loading + /// screen follow its own light/dark choice instead of the browser's. `HTML5Implementation` + /// writes it; the inline script [#splashTheme(SplashSettings)] emits reads it before the + /// first paint. + static final String SPLASH_APPEARANCE_STORAGE_KEY = "cn1.splashAppearance"; + + /// The loading screen's look: the logo and the colours for each appearance. Every value is + /// validated here, because each one is written verbatim into the page's CSS or markup. + static final class SplashSettings { + /// `auto` (follow the browser, or the app's remembered choice), `light` or `dark`. + final String appearance; + final String background; + final String text; + final String accent; + final String darkBackground; + final String darkText; + final String darkAccent; + /// A URL relative to the bundle root, or null for no logo. + final String logo; + + SplashSettings(String appearance, String background, String text, String accent, + String darkBackground, String darkText, String darkAccent, String logo) { + // With no appearance and no dark colour named, the splash stays light: an existing + // light app must not gain a dark loading screen ahead of its white first form just + // because the browser prefers dark. Naming any dark colour is what says the app has + // a dark appearance, and makes `auto` the default. + boolean darkNamed = notBlank(darkBackground) || notBlank(darkText) || notBlank(darkAccent); + this.appearance = appearance(appearance, darkNamed ? "auto" : "light"); + this.background = color("background", background, "#ffffff"); + this.text = color("textColor", text, "#666666"); + this.accent = color("accentColor", accent, "#0072ff"); + this.darkBackground = color("darkBackground", darkBackground, "#121212"); + this.darkText = color("darkTextColor", darkText, "#b3b3b3"); + this.darkAccent = color("darkAccentColor", darkAccent, "#4da3ff"); + this.logo = logo(logo); + } + + static SplashSettings defaults() { + return new SplashSettings(null, null, null, null, null, null, null, null); + } + + static SplashSettings fromSystemProperties() { + return new SplashSettings(prop("appearance"), prop("background"), prop("textColor"), + prop("accentColor"), prop("darkBackground"), prop("darkTextColor"), + prop("darkAccentColor"), prop("logo")); + } + + private static String prop(String name) { + return System.getProperty(SPLASH_PROPERTY_PREFIX + name); + } + + private static boolean notBlank(String v) { + return v != null && v.trim().length() > 0; + } + + private static String appearance(String value, String def) { + if (value == null || value.trim().length() == 0) { + return def; + } + String v = value.trim(); + if (v.equalsIgnoreCase("auto") || v.equalsIgnoreCase("light") || v.equalsIgnoreCase("dark")) { + return asciiLower(v); + } + throw new IllegalArgumentException("javascript.splash.appearance must be auto, light or dark, not '" + + value + "'"); + } + + /// A hex colour (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`) or a CSS colour keyword. Nothing + /// else: `rgb(...)` and friends would need a parser to be sure they cannot close the + /// declaration, and the hex forms express every colour they can. + private static String color(String hint, String value, String def) { + if (value == null || value.trim().length() == 0) { + return def; + } + String v = value.trim(); + boolean ok; + if (v.charAt(0) == '#') { + int n = v.length() - 1; + ok = n == 3 || n == 4 || n == 6 || n == 8; + for (int i = 1; ok && i < v.length(); i++) { + char c = v.charAt(i); + ok = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } + } else { + // A name has to be a real one: a misspelt name, or a keyword such as `inherit`, + // makes the variable invalid where it is used, and the page falls back to a + // transparent background with no error anywhere. + ok = CSS_COLOR_NAMES.indexOf("," + asciiLower(v) + ",") >= 0; + } + if (!ok) { + throw new IllegalArgumentException("javascript.splash." + hint + + " must be a hex colour such as #1e1e1e or a CSS named colour, not '" + value + "'"); + } + return v; + } + + /// The CSS named colours (CSS Color Module Level 4), plus `transparent`. + private static final String CSS_COLOR_NAMES = ",transparent,aliceblue,antiquewhite,aqua," + + "aquamarine,azure,beige,bisque,black,blanchedalmond,blue,blueviolet,brown,burlywood," + + "cadetblue,chartreuse,chocolate,coral,cornflowerblue,cornsilk,crimson,cyan,darkblue," + + "darkcyan,darkgoldenrod,darkgray,darkgreen,darkgrey,darkkhaki,darkmagenta," + + "darkolivegreen,darkorange,darkorchid,darkred,darksalmon,darkseagreen,darkslateblue," + + "darkslategray,darkslategrey,darkturquoise,darkviolet,deeppink,deepskyblue,dimgray," + + "dimgrey,dodgerblue,firebrick,floralwhite,forestgreen,fuchsia,gainsboro,ghostwhite," + + "gold,goldenrod,gray,green,greenyellow,grey,honeydew,hotpink,indianred,indigo,ivory," + + "khaki,lavender,lavenderblush,lawngreen,lemonchiffon,lightblue,lightcoral,lightcyan," + + "lightgoldenrodyellow,lightgray,lightgreen,lightgrey,lightpink,lightsalmon," + + "lightseagreen,lightskyblue,lightslategray,lightslategrey,lightsteelblue,lightyellow," + + "lime,limegreen,linen,magenta,maroon,mediumaquamarine,mediumblue,mediumorchid," + + "mediumpurple,mediumseagreen,mediumslateblue,mediumspringgreen,mediumturquoise," + + "mediumvioletred,midnightblue,mintcream,mistyrose,moccasin,navajowhite,navy,oldlace," + + "olive,olivedrab,orange,orangered,orchid,palegoldenrod,palegreen,paleturquoise," + + "palevioletred,papayawhip,peachpuff,peru,pink,plum,powderblue,purple,rebeccapurple," + + "red,rosybrown,royalblue,saddlebrown,salmon,sandybrown,seagreen,seashell,sienna," + + "silver,skyblue,slateblue,slategray,slategrey,snow,springgreen,steelblue,tan,teal," + + "thistle,tomato,turquoise,violet,wheat,white,whitesmoke,yellow,yellowgreen,"; + + /// `none` (or nothing) for no logo; otherwise a path inside the bundle. The file is not + /// checked: the translator runs before the bundle is complete (the default icon is + /// written by the builder afterwards). A missing one costs nothing visible, since the + /// `` carries `alt=""` and browsers draw nothing for an empty-alt image that fails. Restricted to + /// plain path characters so it can go into a `src` attribute as it is, and never `..` + /// or a scheme, so it cannot point the page at another origin. + private static String logo(String value) { + if (value == null || value.trim().length() == 0 || value.trim().equalsIgnoreCase("none")) { + return null; + } + String v = value.trim(); + boolean ok = v.charAt(0) != '/' && v.indexOf("..") < 0; + for (int i = 0; ok && i < v.length(); i++) { + char c = v.charAt(i); + ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') + || c == '/' || c == '.' || c == '-' || c == '_'; + } + if (!ok) { + throw new IllegalArgumentException("javascript.splash.logo must be a path inside the web " + + "bundle (letters, digits, '/', '.', '-', '_'), not '" + value + "'"); + } + return v; + } + } + + private static String asciiLower(String s) { + StringBuilder out = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + out.append(c >= 'A' && c <= 'Z' ? (char) (c + ('a' - 'A')) : c); + } + return out.toString(); + } + + /// Fills the loading screen's placeholders: its colours and its logo. + /// + /// This adds an inline `"); + } else { + out.append(""); + } + return out.toString(); + } + + private static String vars(String background, String text, String accent) { + return "--cn1-splash-background:" + background + ";--cn1-splash-text:" + text + + ";--cn1-splash-accent:" + accent + ";"; + } + private static String escapeHtml(String text) { StringBuilder out = new StringBuilder(text.length()); for (int i = 0; i < text.length(); i++) { diff --git a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js index 6036e185a70..a018945aea1 100644 --- a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js +++ b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js @@ -4118,6 +4118,12 @@ w.parent.postMessage({ type: 'cn1-app-ready' }, '*'); } } catch (_pmErr) { /* cross-origin parent -- ignore */ } + // The page background is the splash colour only while the splash is up; from the + // first form on, the page is transparent again as it always was, so an embed and any + // area the canvas leaves uncovered show what they showed before (index.html). + if (doc.documentElement) { + doc.documentElement.setAttribute('data-cn1-started', ''); + } var splash = doc.getElementById('cn1-splash'); if (!splash) { return null; diff --git a/vm/ByteCodeTranslator/src/javascript/index.html b/vm/ByteCodeTranslator/src/javascript/index.html index 2a4b0587370..9f275120358 100644 --- a/vm/ByteCodeTranslator/src/javascript/index.html +++ b/vm/ByteCodeTranslator/src/javascript/index.html @@ -5,26 +5,61 @@ __APP_TITLE__ +