diff --git a/README.md b/README.md index 9024002..5bf6a04 100644 --- a/README.md +++ b/README.md @@ -344,6 +344,9 @@ Then put it behind your existing reverse proxy like everything else action — resolved server-side (one atomic operation, not one request per track) the same way launching a playlist already expands into everything it contains — see docs/architecture.md +- [x] Installable as a PWA (manifest + icons, standalone display, + no service worker — this app has nothing worth caching offline) + — see docs/architecture.md - [ ] Try a full party end-to-end with two real accounts (still the main gap) - [ ] Try casting to a real Chromecast (only the SDK's own bootstrap is confirmed here — no real device/session) diff --git a/client/build.mjs b/client/build.mjs index 0290eef..55fb725 100644 --- a/client/build.mjs +++ b/client/build.mjs @@ -11,7 +11,7 @@ // npm run watch -> rebuilds on save, for local dev (see README's // Running locally section) import * as esbuild from 'esbuild'; -import { copyFileSync, mkdirSync } from 'fs'; +import { copyFileSync, cpSync, mkdirSync } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -20,6 +20,11 @@ const outdir = path.join(here, 'dist'); mkdirSync(outdir, { recursive: true }); copyFileSync(path.join(here, 'src', 'index.html'), path.join(outdir, 'index.html')); +// PWA manifest + icons (manifest.webmanifest, icons/*) - see +// docs/architecture.md's "Progressive Web App" section. Copied wholesale +// rather than named file-by-file like index.html above, so adding another +// icon later needs no build.mjs change. +cpSync(path.join(here, 'src', 'public'), outdir, { recursive: true }); const buildOptions = { entryPoints: [path.join(here, 'src', 'app.ts')], diff --git a/client/src/index.html b/client/src/index.html index e57676f..ecb359c 100644 --- a/client/src/index.html +++ b/client/src/index.html @@ -4,6 +4,24 @@ Watch Party + + + + + + + + + + + + diff --git a/client/src/public/icons/apple-touch-icon.png b/client/src/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..c8262c7 Binary files /dev/null and b/client/src/public/icons/apple-touch-icon.png differ diff --git a/client/src/public/icons/favicon.png b/client/src/public/icons/favicon.png new file mode 100644 index 0000000..bcc545f Binary files /dev/null and b/client/src/public/icons/favicon.png differ diff --git a/client/src/public/icons/icon-192.png b/client/src/public/icons/icon-192.png new file mode 100644 index 0000000..e533df5 Binary files /dev/null and b/client/src/public/icons/icon-192.png differ diff --git a/client/src/public/icons/icon-512-maskable.png b/client/src/public/icons/icon-512-maskable.png new file mode 100644 index 0000000..b20de6e Binary files /dev/null and b/client/src/public/icons/icon-512-maskable.png differ diff --git a/client/src/public/icons/icon-512.png b/client/src/public/icons/icon-512.png new file mode 100644 index 0000000..983626f Binary files /dev/null and b/client/src/public/icons/icon-512.png differ diff --git a/client/src/public/manifest.webmanifest b/client/src/public/manifest.webmanifest new file mode 100644 index 0000000..f6a2eaa --- /dev/null +++ b/client/src/public/manifest.webmanifest @@ -0,0 +1,16 @@ +{ + "name": "Watch Party", + "short_name": "Watch Party", + "description": "Regardez et écoutez en même temps que les autres utilisateurs de votre serveur Emby.", + "lang": "fr", + "start_url": "/", + "display": "standalone", + "background_color": "#151216", + "theme_color": "#221d24", + "categories": ["entertainment", "multimedia"], + "icons": [ + { "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "icons/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/docs/architecture.md b/docs/architecture.md index 96bc008..4722557 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -3419,6 +3419,87 @@ tokens) rather than eyeballed. **Not verified**: an actual large discography queuing correctly end to end against a real server - no Emby login reachable from this sandbox. +## Progressive Web App + +Asked for directly, as a follow-up to Picture-in-picture above: since +that feature's automatic-on-backgrounding variant turned out not to be +achievable (browsers require a *fresh* user gesture for +`requestPictureInPicture()` - confirmed empirically, not just from +docs, by calling it from a real `visibilitychange` listener in a real +Chromium tab and getting `NotAllowedError` even with +`navigator.userActivation.hasBeenActive === true` from an earlier real +click), a PWA install was asked for instead. **Worth being explicit +about, since it'd be easy to assume otherwise**: installing this app +does not change that outcome - the transient-activation requirement is +part of the Picture-in-Picture spec itself, not something tied to +being loaded in a browser tab versus an installed, `display: standalone` +window. The one Chrome feature that skips that requirement +(media-session-handler-based automatic PiP) stays desktop-only and +getUserMedia-gated regardless of install state, and nothing above +touches that gate. So the manual `#pipBtn` toggle remains the way to +enter PiP either way - a PWA install is its own, independently useful +feature here (an icon on the home screen, no browser chrome/URL bar +around the player, an OS-level app-switcher entry), not a workaround +for the previous section's limitation. + +Three pieces, all standard and Chromium/Android-focused since that's +this app's stated mobile target (see Picture-in-picture above): + +- **`client/src/public/manifest.webmanifest`** - `name`/`short_name`, + `display: "standalone"`, and `background_color`/`theme_color` copied + by hand from `base.css`'s `--bg`/`--bg-header` (a manifest can't read + CSS custom properties, so these two are duplicated on purpose - if + that palette ever changes, update both places). `start_url: "/"`, + `lang: "fr"` matching `index.html`'s own ``. No + `service worker` - confirmed against MDN's own installability + requirements that Chrome does not require one for the install + prompt, and this app has no real use for one: every page is either + live party/socket state or a proxied Emby catalog, and caching either + offline would mean showing stale library data or a stale party that's + already gone, not a working offline mode - so skipping it avoids a + whole stale-cache-invalidation problem this app doesn't need to + solve. +- **Icons** - hand-authored SVG (this project has no existing + logo/brand asset), rasterized to PNG at the sizes each consumer + needs (192/512 for the manifest, 180 for `apple-touch-icon`, 32 for + the browser-tab favicon) via a `` in a real browser rather + than an image-editing tool, since that's what was available here. + Two source SVGs, not one: `icon.svg` fills the canvas (used for the + manifest's `"any"` purpose, the favicon, and `apple-touch-icon` - + none of those get OS-mask-cropped) and `icon-maskable.svg` scales + the same design down (~75%) so its farthest content point sits + inside Android's adaptive-icon safe zone (a circle of ~30.5% of the + icon's radius from center) - checked by computing the actual + farthest-point distance for both versions, not just eyeballed, + since a maskable icon that ignores that zone gets silently clipped + by circular-mask launchers. +- **`index.html`** - ``, a `theme-color` meta + (Chrome/Android chrome), and the separate iOS-only tags Safari needs + instead of reading the manifest for the equivalent behavior + (`apple-touch-icon` link, `apple-mobile-web-app-capable`, + `apple-mobile-web-app-status-bar-style`, `apple-mobile-web-app-title`). + +`build.mjs` gained one `cpSync(src/public, dist, { recursive: true })` +call alongside its existing `index.html` copy, rather than naming the +manifest and every icon individually - adding another icon later needs +no build.mjs change. `express.static` (already serving all of +`client/dist` unconditionally) picks up `manifest.webmanifest` and +`icons/*` with no server-side change at all. + +Verified: full `typecheck`/`build` (client only). Confirmed directly +against a real running instance of this server, not just assumed: +`manifest.webmanifest` and all three manifest icons resolve `200` with +the correct `application/manifest+json`/`image/png` content types +(express's static middleware infers both from the file extension +correctly on its own), the `apple-touch-icon` link resolves `200`, the +generated PNGs are genuinely the declared dimensions (32/180/192/512), +and the page's own console shows no manifest-related warnings after +loading it. **Not verified**: an actual "Add to Home Screen"/install +prompt, or the installed app's standalone window, on a real device - +no device reachable from this sandbox, same standing gap as every +other browser-native integration in this project (casting, PiP, the +intro-skip buttons). + ## Known limitations - Playback always goes through `hls.js` against whatever Emby's