diff --git a/index.html b/index.html index 781dabc..436593e 100644 --- a/index.html +++ b/index.html @@ -4,8 +4,11 @@ + CircuitPython Code Editor + + diff --git a/js/common/pwa.js b/js/common/pwa.js new file mode 100644 index 0000000..22e9e6d --- /dev/null +++ b/js/common/pwa.js @@ -0,0 +1,11 @@ +export function registerPWA() { + if (!import.meta.env.PROD || !("serviceWorker" in navigator)) { + return; + } + + window.addEventListener("load", () => { + navigator.serviceWorker.register("/sw.js").catch((error) => { + console.warn("Unable to register service worker", error); + }); + }); +} diff --git a/js/script.js b/js/script.js index 64906cf..6aee6e3 100644 --- a/js/script.js +++ b/js/script.js @@ -29,6 +29,9 @@ import { CONNTYPE } from './constants.js'; import './layout.js'; // load for side effects only import { setupPlotterChart } from "./common/plotter.js"; import { mainContent, showSerial } from './layout.js'; +import { registerPWA } from "./common/pwa.js"; + +registerPWA(); // Instantiate workflows let workflows = {}; diff --git a/public/assets/images/pwa-icon-180.png b/public/assets/images/pwa-icon-180.png new file mode 100644 index 0000000..72edfe9 Binary files /dev/null and b/public/assets/images/pwa-icon-180.png differ diff --git a/public/assets/images/pwa-icon-192.png b/public/assets/images/pwa-icon-192.png new file mode 100644 index 0000000..14c610f Binary files /dev/null and b/public/assets/images/pwa-icon-192.png differ diff --git a/public/assets/images/pwa-icon-512.png b/public/assets/images/pwa-icon-512.png new file mode 100644 index 0000000..f4eeb51 Binary files /dev/null and b/public/assets/images/pwa-icon-512.png differ diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..2a16e69 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,29 @@ +{ + "name": "CircuitPython Code Editor", + "short_name": "CircuitPython", + "description": "Edit files and use the serial console on CircuitPython devices.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#673ab7", + "icons": [ + { + "src": "/assets/images/pwa-icon-180.png", + "sizes": "180x180", + "type": "image/png" + }, + { + "src": "/assets/images/pwa-icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/assets/images/pwa-icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..171ba9e --- /dev/null +++ b/public/sw.js @@ -0,0 +1,80 @@ +const CACHE_VERSION = "circuitpython-web-editor-v1"; +const APP_SHELL = [ + "/", + "/index.html", + "/manifest.webmanifest", + "/assets/index.css", + "/assets/checkmark.svg", + "/assets/js/device.js", + "/assets/js/index.js", + "/assets/fonts/fa-brands-400.woff2", + "/assets/fonts/fa-regular-400.woff2", + "/assets/fonts/fa-solid-900.woff2", + "/assets/fonts/fa-v4compatibility.woff2", + "/assets/images/favicon.ico", + "/assets/images/loading-blinka.gif", + "/assets/images/loading-blinka.webp", + "/assets/images/logo.png", + "/assets/images/logo@2x.png", + "/assets/images/logo@3x.png", + "/assets/images/pwa-icon-180.png", + "/assets/images/pwa-icon-192.png", + "/assets/images/pwa-icon-512.png" +]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(CACHE_VERSION).then((cache) => cache.addAll(APP_SHELL)), + ); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => Promise.all( + keys + .filter((key) => key !== CACHE_VERSION) + .map((key) => caches.delete(key)), + )), + ); +}); + +async function networkFirst(request) { + const cache = await caches.open(CACHE_VERSION); + try { + const response = await fetch(request); + cache.put(request, response.clone()); + return response; + } catch (error) { + const cached = await cache.match(request); + if (cached) { + return cached; + } + throw error; + } +} + +async function cacheFirst(request) { + const cache = await caches.open(CACHE_VERSION); + const cached = await cache.match(request); + if (cached) { + return cached; + } + + const response = await fetch(request); + cache.put(request, response.clone()); + return response; +} + +self.addEventListener("fetch", (event) => { + const requestUrl = new URL(event.request.url); + if (event.request.method !== "GET" || requestUrl.origin !== self.location.origin) { + return; + } + + if (event.request.mode === "navigate") { + event.respondWith(networkFirst(event.request)); + return; + } + + event.respondWith(cacheFirst(event.request)); +});