Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[HTML5] Add PWA support to the editor page. #46796

Merged
merged 2 commits into from
Mar 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions misc/dist/html/editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@
<html xmlns='http://www.w3.org/1999/xhtml' lang='' xml:lang=''>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, user-scalable=no' />
<meta name='viewport' content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no' />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="application-name" content="Godot" />
<meta name="apple-mobile-web-app-title" content="Godot" />
<meta name="theme-color" content="#478cbf" />
<meta name="msapplication-navbutton-color" content="#478cbf" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="msapplication-starturl" content="/latest" />
<link id='-gd-engine-icon' rel='icon' type='image/png' href='favicon.png' />
<link rel="apple-touch-icon" type="image/png" href="favicon.png" />
<link rel="manifest" href="manifest.json" />
<title>Godot Engine Web Editor (@GODOT_VERSION@)</title>
<style>
*:focus {
Expand Down Expand Up @@ -250,7 +260,13 @@
<div id='status-notice' class='godot' style='display: none;'></div>
</div>
</div>

<script>
window.addEventListener("load", () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("service.worker.js");
}
});
</script>
<script src='godot.tools.js'></script>
<script>//<![CDATA[

Expand Down
18 changes: 18 additions & 0 deletions misc/dist/html/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "Godot Engine",
"short_name": "Godot",
"description": "Multi-platform 2D and 3D game engine with a feature-rich editor",
"lang": "en",
"start_url": "/godot.tools.html",
"display": "standalone",
"orientation": "landscape",
"theme_color": "#478cbf",
"icons": [
{
"src": "favicon.png",
"sizes": "256x256",
"type": "image/png"
}
],
"background_color": "#333b4f"
}
42 changes: 42 additions & 0 deletions misc/dist/html/offline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>You are offline</title>
<style>
html {
background-color: #333b4f;
color: #e0e0e0;
}

body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
margin: 2rem;
}

p {
margin-block: 1rem;
}

button {
display: block;
padding: 1rem 2rem;
margin: 3rem auto 0;
}
</style>
</head>
<body>
<h1>You are offline</h1>
<p>This application requires an Internet connection to run for the first time.</p>
<p>Press the button below to try reloading:</p>
<button type="button">Reload</button>

<script>
document.querySelector("button").addEventListener("click", () => {
window.location.reload();
});
</script>
</body>
</html>
84 changes: 84 additions & 0 deletions misc/dist/html/service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// This service worker is required to expose an exported Godot project as a
// Progressive Web App. It provides an offline fallback page telling the user
// that they need an Internet conneciton to run the project if desired.
// Incrementing CACHE_VERSION will kick off the install event and force
// previously cached resources to be updated from the network.
const CACHE_VERSION = "@GODOT_VERSION@";
const CACHE_NAME = "@GODOT_NAME@-cache";
const OFFLINE_URL = "offline.html";
// Files that will be cached on load.
const CACHED_FILES = [
"godot.tools.html",
"offline.html",
"godot.tools.js",
"godot.tools.worker.js",
"godot.tools.audio.worklet.js",
"logo.svg",
"favicon.png",
];

// Files that we might not want the user to preload, and will only be cached on first load.
const CACHABLE_FILES = [
"godot.tools.wasm",
];
const FULL_CACHE = CACHED_FILES.concat(CACHABLE_FILES);

self.addEventListener("install", (event) => {
event.waitUntil(async function () {
const cache = await caches.open(CACHE_NAME);
// Clear old cache (including optionals).
await Promise.all(FULL_CACHE.map(path => cache.delete(path)));
// Insert new one.
const done = await cache.addAll(CACHED_FILES);
return done;
}());
});

self.addEventListener("activate", (event) => {
event.waitUntil(async function () {
if ("navigationPreload" in self.registration) {
await self.registration.navigationPreload.enable();
}
}());
// Tell the active service worker to take control of the page immediately.
self.clients.claim();
});

self.addEventListener("fetch", (event) => {
const isNavigate = event.request.mode === "navigate";
const url = event.request.url || "";
const referrer = event.request.referrer || "";
const base = referrer.slice(0, referrer.lastIndexOf("/") + 1);
const local = url.startsWith(base) ? url.replace(base, "") : "";
const isCachable = FULL_CACHE.some(v => v === local) || (base === referrer && base.endsWith(CACHED_FILES[0]));
if (isNavigate || isCachable) {
event.respondWith(async function () {
try {
// Use the preloaded response, if it's there
let request = event.request.clone();
let response = await event.preloadResponse;
if (!response) {
// Or, go over network.
response = await fetch(event.request);
}
if (isCachable) {
// Update the cache
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
} catch (error) {
const cache = await caches.open(CACHE_NAME);
if (event.request.mode === "navigate") {
// Check if we have full cache.
const cached = await Promise.all(FULL_CACHE.map(name => cache.match(name)));
const missing = cached.some(v => v === undefined);
const cachedResponse = missing ? await caches.match(OFFLINE_URL) : await caches.match(CACHED_FILES[0]);
return cachedResponse;
}
const cachedResponse = await caches.match(event.request);
return cachedResponse;
}
}());
}
});
40 changes: 3 additions & 37 deletions platform/javascript/SCsub
Original file line number Diff line number Diff line change
Expand Up @@ -86,40 +86,6 @@ wrap_list = [
]
js_wrapped = env.Textfile("#bin/godot", [env.File(f) for f in wrap_list], TEXTFILESUFFIX="${PROGSUFFIX}.wrapped.js")

zip_dir = env.Dir("#bin/.javascript_zip")
binary_name = "godot.tools" if env["tools"] else "godot"
out_files = [
zip_dir.File(binary_name + ".js"),
zip_dir.File(binary_name + ".wasm"),
zip_dir.File(binary_name + ".html"),
zip_dir.File(binary_name + ".audio.worklet.js"),
]
html_file = "#misc/dist/html/full-size.html"
if env["tools"]:
subst_dict = {"@GODOT_VERSION@": env.GetBuildVersion()}
html_file = env.Substfile(
target="#bin/godot${PROGSUFFIX}.html", source="#misc/dist/html/editor.html", SUBST_DICT=subst_dict
)

in_files = [js_wrapped, build[1], html_file, "#platform/javascript/js/libs/audio.worklet.js"]
if env["gdnative_enabled"]:
in_files.append(build[2]) # Runtime
out_files.append(zip_dir.File(binary_name + ".side.wasm"))
elif env["threads_enabled"]:
in_files.append(build[2]) # Worker
out_files.append(zip_dir.File(binary_name + ".worker.js"))

if env["tools"]:
in_files.append("#misc/dist/html/logo.svg")
out_files.append(zip_dir.File("logo.svg"))
in_files.append("#icon.png")
out_files.append(zip_dir.File("favicon.png"))

zip_files = env.InstallAs(out_files, in_files)
env.Zip(
"#bin/godot",
zip_files,
ZIPROOT=zip_dir,
ZIPSUFFIX="${PROGSUFFIX}${ZIPSUFFIX}",
ZIPCOMSTR="Archiving $SOURCES as $TARGET",
)
# Extra will be the thread worker, or the GDNative side, or None
extra = build[2] if len(build) > 2 else None
env.CreateTemplateZip(js_wrapped, build[1], extra)
8 changes: 4 additions & 4 deletions platform/javascript/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
add_js_libraries,
add_js_pre,
add_js_externs,
get_build_version,
create_template_zip,
)
from methods import get_compiler_version
from SCons.Util import WhereIs
Expand Down Expand Up @@ -147,12 +147,12 @@ def configure(env):
env.AddMethod(add_js_pre, "AddJSPre")
env.AddMethod(add_js_externs, "AddJSExterns")

# Add method for getting build version string.
env.AddMethod(get_build_version, "GetBuildVersion")

# Add method that joins/compiles our Engine files.
env.AddMethod(create_engine_file, "CreateEngineFile")

# Add method for creating the final zip file
env.AddMethod(create_template_zip, "CreateTemplateZip")

# Closure compiler extern and support for ecmascript specs (const, let, etc).
env["ENV"]["EMCC_CLOSURE_ARGS"] = "--language_in ECMASCRIPT6"

Expand Down
61 changes: 60 additions & 1 deletion platform/javascript/emscripten_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def run_closure_compiler(target, source, env, for_signature):
return " ".join(cmd)


def get_build_version(env):
def get_build_version():
import version

name = "custom_build"
Expand All @@ -30,6 +30,65 @@ def create_engine_file(env, target, source, externs):
return env.Textfile(target, [env.File(s) for s in source])


def create_template_zip(env, js, wasm, extra):
binary_name = "godot.tools" if env["tools"] else "godot"
zip_dir = env.Dir("#bin/.javascript_zip")
in_files = [
js,
wasm,
"#platform/javascript/js/libs/audio.worklet.js",
]
out_files = [
zip_dir.File(binary_name + ".js"),
zip_dir.File(binary_name + ".wasm"),
zip_dir.File(binary_name + ".audio.worklet.js"),
]
# GDNative/Threads specific
if env["gdnative_enabled"]:
in_files.append(extra) # Runtime
out_files.append(zip_dir.File(binary_name + ".side.wasm"))
elif env["threads_enabled"]:
in_files.append(extra) # Worker
out_files.append(zip_dir.File(binary_name + ".worker.js"))

service_worker = "#misc/dist/html/service-worker.js"
if env["tools"]:
# HTML
html = "#misc/dist/html/editor.html"
subst_dict = {"@GODOT_VERSION@": get_build_version(), "@GODOT_NAME@": "GodotEngine"}
html = env.Substfile(target="#bin/godot${PROGSUFFIX}.html", source=html, SUBST_DICT=subst_dict)
in_files.append(html)
out_files.append(zip_dir.File(binary_name + ".html"))
# And logo/favicon
in_files.append("#misc/dist/html/logo.svg")
out_files.append(zip_dir.File("logo.svg"))
in_files.append("#icon.png")
out_files.append(zip_dir.File("favicon.png"))
# PWA
service_worker = env.Substfile(
target="#bin/godot${PROGSUFFIX}.service.worker.js", source=service_worker, SUBST_DICT=subst_dict
)
in_files.append(service_worker)
out_files.append(zip_dir.File("service.worker.js"))
in_files.append("#misc/dist/html/manifest.json")
out_files.append(zip_dir.File("manifest.json"))
in_files.append("#misc/dist/html/offline.html")
out_files.append(zip_dir.File("offline.html"))
else:
# HTML
in_files.append("#misc/dist/html/full-size.html")
out_files.append(zip_dir.File(binary_name + ".html"))

zip_files = env.InstallAs(out_files, in_files)
env.Zip(
"#bin/godot",
zip_files,
ZIPROOT=zip_dir,
ZIPSUFFIX="${PROGSUFFIX}${ZIPSUFFIX}",
ZIPCOMSTR="Archiving $SOURCES as $TARGET",
)


def add_js_libraries(env, libraries):
env.Append(JS_LIBS=env.File(libraries))

Expand Down
5 changes: 4 additions & 1 deletion platform/javascript/js/libs/library_godot_audio.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ const GodotAudioWorklet = {

close: function () {
return new Promise(function (resolve, reject) {
if (GodotAudioWorklet.promise === null) {
return;
}
GodotAudioWorklet.promise.then(function () {
GodotAudioWorklet.worklet.port.postMessage({
'cmd': 'stop',
Expand All @@ -247,7 +250,7 @@ const GodotAudioWorklet = {
GodotAudioWorklet.worklet = null;
GodotAudioWorklet.promise = null;
resolve();
});
}).catch(function (err) { /* aborted? */ });
Faless marked this conversation as resolved.
Show resolved Hide resolved
});
},
},
Expand Down