diff --git a/source/_extensions/pwa_service.py b/source/_extensions/pwa_service.py new file mode 100644 index 0000000000..d61f2cf68b --- /dev/null +++ b/source/_extensions/pwa_service.py @@ -0,0 +1,125 @@ +import sphinx as Sphinx +from typing import Any, Dict, List +import os +from docutils import nodes +import json +import shutil +from urllib.parse import urljoin, urlparse, urlunparse +from sphinx.util import logging +from sphinx.util.console import green, red, yellow # pylint: disable=no-name-in-module + +manifest = { + "name": "", + "short_name": "", + "theme_color": "", + "background_color": "", + "display": "standalone", + "scope": "/", + "start_url": "/index.html", + "icons": [], +} + +logger = logging.getLogger(__name__) + +def get_files_to_cache(outDir: str, config: Dict[str, Any]): + files_to_cache = [] + for (dirpath, dirname, filenames) in os.walk(outDir): + dirpath = dirpath.split(outDir)[1] + + # skip adding sources to cache + if os.sep + "_sources" + os.sep in dirpath: + continue + + # add files to cache + for name in filenames: + if "sw.js" in name: + continue + + dirpath = dirpath.replace("\\", "/") + dirpath = dirpath.lstrip("/") + + # we have to use absolute urls in our cache resource, because fetch will return an absolute url + # this means that we cannot accurately cache resources that are in PRs because RTD does not give us + # the url + if config["html_baseurl"] is not None: + # readthedocs uses html_baseurl for sphinx > 1.8 + parse_result = urlparse(config["html_baseurl"]) + + # Grab root url from canonical url + url = parse_result.netloc + + # enables RTD multilanguage support + if os.getenv("READTHEDOCS"): + url = "https://" + url + "/" + os.getenv("READTHEDOCS_LANGUAGE") + "/" + os.getenv("READTHEDOCS_VERSION") + "/" + + if config["html_baseurl"] is None and not os.getenv("CI"): + logger.warning( + red(f"html_baseurl is not configured. This can be ignored if deployed in RTD environments.") + ) + url = "" + + if dirpath == "": + resource_url = urljoin( + url, name + ) + files_to_cache.append(resource_url) + else: + resource_url = url + dirpath + "/" + name + files_to_cache.append(resource_url) + + return files_to_cache + + +def build_finished(app: Sphinx, exception: Exception): + outDir = app.outdir + outDirStatic = outDir + os.sep + "_static" + os.sep + files_to_cache = get_files_to_cache(outDir, app.config) + + # dumps a json file with our cache + with open(outDirStatic + "cache.json", "w") as f: + json.dump(files_to_cache, f) + + # copies over our service worker + shutil.copyfile( + os.path.dirname(__file__) + os.sep + "pwa_service_files" + os.sep + "sw.js", + outDir + os.sep + "sw.js", + ) + + +def html_page_context( + app: Sphinx, + pagename: str, + templatename: str, + context: Dict[str, Any], + doctree: nodes.document, +) -> None: + if pagename == "index": + context[ + "metatags" + ] += '' + context[ + "metatags" + ] += f'' + + if app.config["pwa_apple_icon"] is not None: + context[ + "metatags" + ] += f'' + + +def setup(app: Sphinx) -> Dict[str, Any]: + app.add_config_value("pwa_name", "", "html") + app.add_config_value("pwa_short_name", "", "html") + app.add_config_value("pwa_theme_color", "", "html") + app.add_config_value("pwa_background_color", "", "html") + app.add_config_value("pwa_display", "standalone", "html") + app.add_config_value("pwa_icons", [], "html") + app.add_config_value("pwa_apple_icon", "", "html") + + app.connect("html-page-context", html_page_context) + app.connect("build-finished", build_finished) + + return { + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/source/_extensions/pwa_service_files/sw.js b/source/_extensions/pwa_service_files/sw.js new file mode 100644 index 0000000000..621fe4e5f1 --- /dev/null +++ b/source/_extensions/pwa_service_files/sw.js @@ -0,0 +1,121 @@ +"use strict"; +// extend this to update the service worker every push +// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Offline_Service_workers +let cacheName = 'js13kPWA-v1'; + +// todo test +self.addEventListener('install', function (e) { + e.waitUntil(async function() { + await fetch('_static/cache.json') + .then(response => response.json()) + .then(async (data) => { + for (let i = 0; i < data.length / 10; i++) { + const tofetch = data.slice(i * 10, i * 10 + 10); + await addKeys(tofetch) + } + }) + }()); +}); + +// opt for a cache first response, for quickest load times +// we'll still update the page assets in the background +self.addEventListener('fetch', function (event) { + event.respondWith(async function () { + let request_url = event.request.url; + + try { + await addKeys([request_url]) //put in format our addKeys function expects + } catch (error) { + console.error("Error downloading from remote:", error) + } + + let res = await getKey(request_url) + + console.log("Fetching:", event.request.url) + return res; + }()); +}); + +let dbPromise; + +async function getDB() { + if (dbPromise) { + return dbPromise; + } else { + let request = indexedDB.open("frc-docs", "1") + + dbPromise = new Promise((resolve, reject) => { + request.onsuccess = function (event) { + console.log("Successfully opened database!") + resolve(event.target.result) + } + + request.onerror = function (event) { + console.error("Error opening database for getKey():", request.error) + reject() + } + + request.onupgradeneeded = function (event) { + let db = event.target.result; + db.createObjectStore("urls", { keyPath: 'key' }) + } + }); + + return dbPromise; + } +} + +async function getKey(key) { + let db = await getDB() + console.log("Grabbing key", key) + return new Promise((resolve, reject) => { + try { + let transaction = db.transaction("urls").objectStore("urls"); + let request = transaction.get(key) + + request.onsuccess = function (event) { + let res = request.result; + console.log("Successfully retrieved result:", res) + resolve(new Response(res.value)); + } + + request.onerror = function (event) { + console.error("Error on retrieving blob:", key, request.error) + reject() + } + + } catch (ex) { + console.error(ex.message); + reject() + } + }) +} + +async function addKeys(datas) { + let db = await getDB() + return Promise.all( + datas.map(async (data) => { + let fetchedData = await fetch(data) + .then(x => x.blob()) + .catch((error) => { + console.error("Error fetching", data) + return new Promise((resolve, reject) => { + reject(); + }) + }) + let transaction = db.transaction("urls", "readwrite").objectStore("urls") + let request = transaction.put({key: data, value: fetchedData}) + + return new Promise((resolve, reject) => { + request.onsuccess = function() { + resolve() + } + request.onerror = function () { + console.log(request.error) + reject(request.error) + } + }); + }) + ); + // data is already a key/value object with url/data +} \ No newline at end of file diff --git a/source/_static/first-logo-256px.png b/source/_static/first-logo-256px.png new file mode 100644 index 0000000000..438488faf9 Binary files /dev/null and b/source/_static/first-logo-256px.png differ diff --git a/source/_static/first-logo-512px.png b/source/_static/first-logo-512px.png new file mode 100644 index 0000000000..19d0ca6304 Binary files /dev/null and b/source/_static/first-logo-512px.png differ diff --git a/source/_static/frcdocs.webmanifest b/source/_static/frcdocs.webmanifest new file mode 100644 index 0000000000..1d3e9622ec --- /dev/null +++ b/source/_static/frcdocs.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "FRC Docs", + "short_name": "FRC Docs", + "theme_color": "#003974", + "background_color": "#003974", + "display": "standalone", + "scope": "../", + "start_url": "../index.html", + "icons": [ + { + "src": "/_static/first-logo-256px.png", + "type": "image/png", + "sizes": "256x256" + }, + { + "src": "/_static/first-logo-512px.png", + "type": "image/png", + "sizes": "512x512" + } + ] +} diff --git a/source/_static/touch-icon.png b/source/_static/touch-icon.png new file mode 100644 index 0000000000..0b4b5c78eb Binary files /dev/null and b/source/_static/touch-icon.png differ diff --git a/source/conf.py b/source/conf.py index 61d01af306..04b4c8e57e 100644 --- a/source/conf.py +++ b/source/conf.py @@ -59,6 +59,7 @@ "_extensions.post_process", "_extensions.rtd_patch", "_extensions.localization", + "_extensions.pwa_service", ] extensions += local_extensions @@ -177,6 +178,9 @@ # Use MathJax3 for better page loading times mathjax_path = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" +# PWA Specific Settings +pwa_apple_icon = "_static/touch-icon.png" + # -- Options for HTML output ------------------------------------------------- @@ -198,7 +202,10 @@ # Specify canonical root # This tells search engines that this domain is preferred -html_baseurl = "https://docs.wpilib.org/en/stable/" +if os.getenv("TESTING"): + html_baseurl = "http://localhost:8000/" +else: + html_baseurl = "https://frc-docs--1704.org.readthedocs.build/en/1704/" html_theme_options = { "collapse_navigation": True,