Make the server installable as a home-screen PWA
Adds a web manifest, hand-drawn cup+frame icons, and a presence-only service worker (no offline caching) so mobile browsers offer "Add to Home Screen" for the server UI.
This commit is contained in:
+8
-1
@@ -18,7 +18,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -60,6 +60,13 @@ def health() -> dict:
|
|||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/sw.js")
|
||||||
|
def service_worker() -> FileResponse:
|
||||||
|
# Served from / rather than /static/sw.js so its default scope is the
|
||||||
|
# whole app -- a SW can only ever control paths at or below its own URL.
|
||||||
|
return FileResponse("app/static/sw.js", media_type="application/javascript")
|
||||||
|
|
||||||
|
|
||||||
def _device_credential_redirect(request: Request, db, allow_legacy: bool) -> str | None:
|
def _device_credential_redirect(request: Request, db, allow_legacy: bool) -> str | None:
|
||||||
"""The on-frame manage QR points at the server root with the device's
|
"""The on-frame manage QR points at the server root with the device's
|
||||||
own credentials (new firmware: ?id=&token=; deployed firmware:
|
own credentials (new firmware: ?id=&token=; deployed firmware:
|
||||||
|
|||||||
@@ -58,6 +58,15 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// Registering this is what makes Chrome/Android offer the "Add to Home
|
||||||
|
// screen" install prompt -- a manifest link alone isn't enough. Served
|
||||||
|
// from /sw.js (not /static/sw.js) so its scope is the whole app.
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
window.addEventListener("load", function () {
|
||||||
|
navigator.serviceWorker.register("/sw.js");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Shared display names for widget_type, everywhere one shows up in the
|
// Shared display names for widget_type, everywhere one shows up in the
|
||||||
// UI (Layout canvas, Add-a-widget buttons, button-assignment dropdowns).
|
// UI (Layout canvas, Add-a-widget buttons, button-assignment dropdowns).
|
||||||
const WIDGET_LABELS = {
|
const WIDGET_LABELS = {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 501 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "ESPresso Frame",
|
||||||
|
"short_name": "ESPresso",
|
||||||
|
"description": "Manage your e-ink photo frames.",
|
||||||
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#f5f6f8",
|
||||||
|
"theme_color": "#2563eb",
|
||||||
|
"icons": [
|
||||||
|
{ "src": "/static/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||||
|
{ "src": "/static/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Presence-only service worker: satisfies the "installable" requirement
|
||||||
|
// (Chrome/Android in particular checks for a controlling SW with a fetch
|
||||||
|
// handler) without adding an offline cache -- every request just goes to
|
||||||
|
// the network as normal. Served from / (see app/main.py's /sw.js route)
|
||||||
|
// so its scope covers the whole app, not just /static/.
|
||||||
|
self.addEventListener("install", () => self.skipWaiting());
|
||||||
|
self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
|
||||||
|
self.addEventListener("fetch", (event) => event.respondWith(fetch(event.request)));
|
||||||
@@ -17,6 +17,14 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/static/theme.css">
|
<link rel="stylesheet" href="/static/theme.css">
|
||||||
|
<link rel="manifest" href="/static/manifest.json">
|
||||||
|
<link rel="icon" href="/static/icons/favicon.png">
|
||||||
|
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
|
||||||
|
<meta name="theme-color" content="#2563eb">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="ESPresso">
|
||||||
{% block extra_head %}{% endblock %}
|
{% block extra_head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -17,6 +17,14 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/static/theme.css">
|
<link rel="stylesheet" href="/static/theme.css">
|
||||||
|
<link rel="manifest" href="/static/manifest.json">
|
||||||
|
<link rel="icon" href="/static/icons/favicon.png">
|
||||||
|
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
|
||||||
|
<meta name="theme-color" content="#2563eb">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="ESPresso">
|
||||||
{% block extra_head %}{% endblock %}
|
{% block extra_head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user