feat(web): fix mobile scaling regressions and ship installable PWA
Mobile fixes (all in the embedded SPA): - Move the mobile media queries to the bottom of the stylesheet — the cinematheque design layer was appended after them, so its base rules (header padding, video heights) silently overrode the phone layout by source order. A comment now pins the ordering invariant. - The design layer's uiDrop entry animation (fill-mode:both) permanently overrode the sidebar's translateX(-100%), leaving the drawer stuck open over the content on phones; the mobile block now disables it. - Lower the single-column breakpoint 380px -> 350px: it was catching 360/375px phones and blowing every card up to full width. - viewport-fit=cover so the env(safe-area-inset-*) paddings actually resolve on iPhones; notch-safe padding on header/content/modals. - 16px inputs on small screens (kills iOS Safari's focus auto-zoom), same for the login page's password field. - Mobile rules for the design-layer components that had none: full-height command-palette sheet, clamped stats numerals, coarse-pointer touch targets for the custom player (taller scrub track, always-visible thumb, no volume slider). - CSP font-src now allows data: — the embedded base64 woff2 fonts were being blocked and silently fell back to system faces. PWA: - manifest.webmanifest, minimal service worker, and a new Catacomb arch icon set (SVG source + rendered PNGs incl. maskable + apple-touch), all include_str!/include_bytes!-embedded like the HTML. - sw.js only intercepts GET navigations to "/" (network-first, cached offline fallback) and the static assets; /api, /ws, /files, /music-files, /feed are never touched, and it's served no-store so binary upgrades keep propagating. - Routes + auth_middleware allowlist so the browser can fetch the statics pre-login; theme-color meta tracks the active theme's panel. - tests/api.rs covers the new endpoints incl. the ungated-when-password invariant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
52c561b47b
commit
891b1e0d3c
11 changed files with 435 additions and 70 deletions
76
src/web.rs
76
src/web.rs
|
|
@ -1160,7 +1160,7 @@ async fn security_headers(req: Request, next: Next) -> Response {
|
|||
img-src 'self' data: blob: https:; \
|
||||
media-src 'self' blob:; \
|
||||
connect-src 'self'; \
|
||||
font-src 'self'; \
|
||||
font-src 'self' data:; \
|
||||
object-src 'none'; \
|
||||
base-uri 'self'; \
|
||||
frame-ancestors 'none'";
|
||||
|
|
@ -1187,6 +1187,19 @@ async fn auth_middleware(
|
|||
if path == "/api/login" {
|
||||
return next.run(req).await;
|
||||
}
|
||||
// PWA static assets. Browsers fetch the manifest/icons during install
|
||||
// (not always with credentials) and must reach the service worker script
|
||||
// to register it from the login page's origin. All of these are
|
||||
// compile-time constants with nothing user- or library-specific, so
|
||||
// serving them pre-auth leaks nothing.
|
||||
if req.method().as_str() == "GET"
|
||||
&& (path == "/manifest.webmanifest"
|
||||
|| path == "/sw.js"
|
||||
|| path == "/apple-touch-icon.png"
|
||||
|| path.starts_with("/icons/"))
|
||||
{
|
||||
return next.run(req).await;
|
||||
}
|
||||
if is_authed(&state, req.headers()) {
|
||||
return next.run(req).await;
|
||||
}
|
||||
|
|
@ -3303,6 +3316,10 @@ async fn serve(config: Config, shutdown_rx: std::sync::mpsc::Receiver<()>) {
|
|||
|
||||
let app = Router::new()
|
||||
.route("/", get(get_index))
|
||||
.route("/manifest.webmanifest", get(get_manifest))
|
||||
.route("/sw.js", get(get_sw))
|
||||
.route("/icons/:name", get(get_icon))
|
||||
.route("/apple-touch-icon.png", get(get_apple_touch_icon))
|
||||
.route("/api/library", get(get_library))
|
||||
.route("/api/progress", get(get_progress))
|
||||
.route("/ws/progress", get(ws_progress))
|
||||
|
|
@ -3439,3 +3456,60 @@ const LOGIN_HTML: &str = include_str!("web_ui/login.html");
|
|||
/// Main library UI. Single-page app with embedded styles and JS — see the
|
||||
/// architecture notes at the top of `web_ui/index.html`.
|
||||
const HTML_UI: &str = include_str!("web_ui/index.html");
|
||||
|
||||
/// PWA assets, embedded like the HTML. The icon PNGs are rendered from
|
||||
/// `web_ui/icon.svg` — see the comment there for the regen commands.
|
||||
const MANIFEST: &str = include_str!("web_ui/manifest.webmanifest");
|
||||
const SW_JS: &str = include_str!("web_ui/sw.js");
|
||||
const ICON_192: &[u8] = include_bytes!("web_ui/icon-192.png");
|
||||
const ICON_512: &[u8] = include_bytes!("web_ui/icon-512.png");
|
||||
const APPLE_TOUCH_ICON: &[u8] = include_bytes!("web_ui/apple-touch-icon.png");
|
||||
|
||||
async fn get_manifest() -> impl IntoResponse {
|
||||
(
|
||||
[
|
||||
(header::CONTENT_TYPE, "application/manifest+json"),
|
||||
(header::CACHE_CONTROL, "public, max-age=3600"),
|
||||
],
|
||||
MANIFEST,
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_sw() -> impl IntoResponse {
|
||||
// no-store, same reasoning as the HTML: binary upgrades change the
|
||||
// embedded worker without changing the URL, and a stale SW would pin
|
||||
// stale behavior across upgrades.
|
||||
(
|
||||
[
|
||||
(header::CONTENT_TYPE, "text/javascript; charset=utf-8"),
|
||||
(header::CACHE_CONTROL, "no-store"),
|
||||
],
|
||||
SW_JS,
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_icon(Path(name): Path<String>) -> Response {
|
||||
let bytes: &'static [u8] = match name.as_str() {
|
||||
"icon-192.png" => ICON_192,
|
||||
"icon-512.png" => ICON_512,
|
||||
_ => return StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
(
|
||||
[
|
||||
(header::CONTENT_TYPE, "image/png"),
|
||||
(header::CACHE_CONTROL, "public, max-age=86400"),
|
||||
],
|
||||
bytes,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn get_apple_touch_icon() -> impl IntoResponse {
|
||||
(
|
||||
[
|
||||
(header::CONTENT_TYPE, "image/png"),
|
||||
(header::CACHE_CONTROL, "public, max-age=86400"),
|
||||
],
|
||||
APPLE_TOUCH_ICON,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue