diff --git a/.gitignore b/.gitignore index 745e5c7..de9ad49 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -fonts/rajdhani-fontfacekit/ \ No newline at end of file +fonts/rajdhani-fontfacekit/ +ChatRequest.json +chat-request-database.path \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 9f3c10c..3e47283 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,16 +1,21 @@ # AGENTS.md -Static OBS/browser overlay that reads Beat Saber Plus Song Overlay events over WebSocket (`ws://localhost:2947/socket`). +OBS/browser overlay that reads Beat Saber Plus Song Overlay events over WebSocket (`ws://localhost:2947/socket`). Serve the folder with **`deno task serve`** (see [`serve.ts`](serve.ts)) so the request-queue JSON loads from the same origin; configure the BS+ database path via `CHAT_REQUEST_DATABASE` or `chat-request-database.path`. + +## Preference: HTTP only, no `file://` + +Do **not** add or maintain code paths for opening the overlay as **`file://`**. The client assumes **`http:` / `https:`** for fetching JSON (cache-busted `fetch`). Do not reintroduce XHR/`file:` fallbacks or docs that suggest local file URLs—one supported way: serve with Deno (or another HTTP server) per [`README.md`](README.md). ## Files of interest - [`index.html`](index.html) — Page shell: markup for map info, time bar, score, settings dialog; pulls `index.css` and `index.js`. - [`index.js`](index.js) — Connects to BS+ WebSocket, parses JSON events (`gameState`, `mapInfo`, `pause`, `resume`, `score`), updates DOM; optional BeatSaver API fetch for custom maps; reads/writes settings from URL hash. - [`index.css`](index.css) — Layout, theming, visibility toggles driven by body classes and CSS variables from settings. +- [`serve.ts`](serve.ts) — Deno static server + optional mapping of `ChatRequest.json` / `database.json` to the real BS+ `Database.json` path. - [`types.d.ts`](types.d.ts) — JSDoc typedefs for BS+ payloads and events (editor/IDE hints only). - [`jsconfig.json`](jsconfig.json) — JS project roots/path so editors resolve modules and `types.d.ts`. - [`images/`](images/) — Cover fallback (`unknown.svg`), characteristic icons under `images/characteristic/` (filenames match BS+ characteristic strings). -- [`README.md`](README.md) — User-facing usage (hosted URL, OBS, local `file://`, BS+ module). +- [`README.md`](README.md) — User-facing usage (Deno, OBS URL, BS+ module). - [`dprint.json`](dprint.json) — Formatter config for the repo. - [`.editorconfig`](.editorconfig) — Basic indent/charset rules for editors. diff --git a/README.md b/README.md index bc51edc..9504ba8 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,25 @@ Requires [BeatSaberPlus](https://github.com/hardcpp/BeatSaberPlus)  +## Setup + +Install [Deno](https://docs.deno.com/runtime/getting_started/installation/) and serve the overlay over HTTP (see below). + +The server must know where Beat Saber Plus stores **`ChatRequest/Database.json`**. It then serves that file as `ChatRequest.json` and `database.json` (same data) over HTTP—no symlink. + +**Easiest:** copy `chat-request-database.path.example` to **`chat-request-database.path`** in this repo and put **one line**: the absolute path to `Database.json` (gitignored, so it stays on your machine). + +**Or** set the environment variable (overrides the path file): + +```powershell +$env:CHAT_REQUEST_DATABASE = "C:\Users\pleb\BSManager\BSInstances\1.40.8\UserData\BeatSaberPlus\ChatRequest\Database.json" +deno task serve +``` + +Then open **`http://127.0.0.1:8080/index.html`** (use the same host and port the terminal prints). Set `PORT` if needed. In OBS, use that URL for the browser source. + +If neither the path file nor `CHAT_REQUEST_DATABASE` is set, the overlay only finds a queue if you place a `ChatRequest.json` copy in the repo folder. + ### Usage -Clone the repo to use the overlay locally. - -Note: in OBS browser source, use URL `file:///C:/path-to-overlay.../index.html` instead of "Local file" so that URL parameters work. +Clone the repo and run `deno task serve` as above. diff --git a/chat-request-database.path.example b/chat-request-database.path.example new file mode 100644 index 0000000..d2e67ab --- /dev/null +++ b/chat-request-database.path.example @@ -0,0 +1 @@ +C:\path\to\UserData\BeatSaberPlus\ChatRequest\Database.json diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..0de2691 --- /dev/null +++ b/deno.json @@ -0,0 +1,6 @@ +{ + "tasks": { + "serve": "deno run --allow-net --allow-read --allow-env serve.ts", + "dev": "deno run --watch --allow-net --allow-read --allow-env serve.ts" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..a119795 --- /dev/null +++ b/deno.lock @@ -0,0 +1,66 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/cli@^1.0.28": "1.0.28", + "jsr:@std/encoding@^1.0.10": "1.0.10", + "jsr:@std/fmt@^1.0.9": "1.0.9", + "jsr:@std/fs@^1.0.23": "1.0.23", + "jsr:@std/html@^1.0.5": "1.0.5", + "jsr:@std/http@*": "1.0.25", + "jsr:@std/internal@^1.0.12": "1.0.12", + "jsr:@std/media-types@^1.1.0": "1.1.0", + "jsr:@std/net@^1.0.6": "1.0.6", + "jsr:@std/path@*": "1.1.4", + "jsr:@std/path@^1.1.4": "1.1.4", + "jsr:@std/streams@^1.0.17": "1.0.17" + }, + "jsr": { + "@std/cli@1.0.28": { + "integrity": "74ef9b976db59ca6b23a5283469c9072be6276853807a83ec6c7ce412135c70a" + }, + "@std/encoding@1.0.10": { + "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" + }, + "@std/fmt@1.0.9": { + "integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0" + }, + "@std/fs@1.0.23": { + "integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37" + }, + "@std/html@1.0.5": { + "integrity": "4e2d693f474cae8c16a920fa5e15a3b72267b94b84667f11a50c6dd1cb18d35e" + }, + "@std/http@1.0.25": { + "integrity": "577b4252290af1097132812b339fffdd55fb0f4aeb98ff11bdbf67998aa17193", + "dependencies": [ + "jsr:@std/cli", + "jsr:@std/encoding", + "jsr:@std/fmt", + "jsr:@std/fs", + "jsr:@std/html", + "jsr:@std/media-types", + "jsr:@std/net", + "jsr:@std/path@^1.1.4", + "jsr:@std/streams" + ] + }, + "@std/internal@1.0.12": { + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" + }, + "@std/media-types@1.1.0": { + "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" + }, + "@std/net@1.0.6": { + "integrity": "110735f93e95bb9feb95790a8b1d1bf69ec0dc74f3f97a00a76ea5efea25500c" + }, + "@std/path@1.1.4": { + "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/streams@1.0.17": { + "integrity": "7859f3d9deed83cf4b41f19223d4a67661b3d3819e9fc117698f493bf5992140" + } + } +} diff --git a/docs/ADR.md b/docs/ADR.md index f5430fd..f200615 100644 --- a/docs/ADR.md +++ b/docs/ADR.md @@ -2,15 +2,14 @@ ## Static web app with typed JavaScript -The overlay loads inside OBS from a URL or `file://` path, with no install step for streamers. +The overlay is plain HTML/CSS/JS with no bundler: run it locally with **`deno task serve`** or host the files on any static server. OBS uses an **http(s) URL** to the page. Consequences: Ease of use -- Adding or updating the overlay is **copy or point OBS at `index.html`** (hosted or local); nothing to build before use. -- We avoid a toolchain that would complicate “drop this folder in” or quick Netlify-style hosting. +- Adding or updating the overlay is **copy files or point OBS at a URL**; nothing to build before use beyond optional Deno for local serving and the chat-request database path. - Type safety is **optional IDE assistance**, not enforced at publish time (there is no `tsc` in CI by default). -This static, dependency-free shape exists **because** it stays easy to wire up as an OBS Browser Source while keeping the codebase maintainable through JSDoc and `types.d.ts`. +This dependency-free client shape stays easy to wire as an OBS Browser Source while keeping the codebase maintainable through JSDoc and `types.d.ts`. ## BeatSaberPlus diff --git a/docs/testing.md b/docs/testing.md index dbe2937..e1a35be 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,39 +1,37 @@ # Testing in a browser -The overlay is a static page. You can exercise the UI and wiring without OBS by opening it in any Chromium-based browser (Chrome, Edge) or Firefox. +Run **`deno task serve`** from the repo (see [README](../README.md)), then open the overlay in Chromium (Chrome, Edge) or Firefox. ## Open the page -Use a **`file://` URL** (same idea as OBS): absolute path to `index.html`, forward slashes. This clone: +Use the URL the server prints, for example: -`file:///C:/Users/example/ops/BeatSaber-Overlay/index.html?scale=1.5` +`http://127.0.0.1:8080/index.html?scale=1.5` -Paste that into the address bar, or drag `index.html` into a browser window. +Settings live in the **URL fragment** (after `#`). Put query parameters **before** the hash if you use both: `index.html?debug=1#…` -Settings are stored in the **URL fragment** (after `#`). Using a full `file://` URL (not only picking “Open file” in a way that strips the hash) keeps hash-based settings working, same idea as in the [README](../README.md). +## Preview the song overlay (no Beat Saber) -## Debug mode (no Beat Saber) +**Click anywhere** on the page (outside the settings dialog) to toggle **preview** mode. The song overlay appears with the built-in placeholder labels so you can check layout, toggles, scale, and fade without a game connection. -Without the game, the overlay normally hides outside **Playing** (and during BeatSaver loading). Add a **query parameter** so it stays visible for layout or WebSocket checks: +## Request list simulation -- Enable: `?debug=1` -- Example: `file:///C:/Users/example/ops/BeatSaber-Overlay/index.html?debug=1` +Enable **Debug** in the settings dialog (or add **`?debug=1`** to the URL). The song requests list then uses the **`history`** array from the JSON instead of **`queue`**, so you can see how entries look with the same shape as real data (`key`, `rqn`, `npr`, etc.). The header title is unchanged. -Put `?debug` **before** the `#` hash if you use both: `index.html?debug=1#…` +The Deno server exposes Beat Saber Plus data as **`ChatRequest.json`** and **`database.json`** (same file). With debug, the page tries **`ChatRequest.json`** first, then **`database.json`**. To load a different filename, add **`?requests=yourfile.json`**. ## What you should see -- The overlay layout with placeholder labels until live data arrives. - **Developer tools → Console:** log lines such as `Connecting to ws://localhost:2947/socket` on load. If [Beat Saber Plus](https://github.com/hardcpp/BeatSaberPlus) is **not** running with the Song Overlay module listening on that port, the socket will close and the script **retries every 10 seconds**—that is expected. - **With Beat Saber running** and BS+ Song Overlay enabled: you should see `Connection open.` when the WebSocket succeeds, and map info, time, and score update while you play. ## Quick UI checks without the game -- Click the page (outside the settings dialog) to toggle the **preview** / settings visibility. +- **Click** the page (outside the settings dialog) to toggle **preview** and open the settings strip. - Change checkboxes and values in the dialog; the **URL hash** should update and layout should reflect toggles and scale. ## Live data path -End-to-end testing needs the same pieces as streaming: **Beat Saber**, **Beat Saber Plus** with the **Song Overlay** module active, so `ws://localhost:2947/socket` accepts connections. The browser page only connects to that local WebSocket; it does not start the server. +End-to-end testing needs the same pieces as streaming: **Beat Saber**, **Beat Saber Plus** with the **Song Overlay** module active, so `ws://localhost:2947/socket` accepts connections. The browser page only connects to that local WebSocket; it does not start the game server. For custom maps, the overlay may request **BeatSaver** over HTTPS; use **Network** in devtools if those requests fail (offline, blocked, or API errors). diff --git a/index.css b/index.css index 0df1aff..422bc0c 100644 --- a/index.css +++ b/index.css @@ -36,15 +36,106 @@ body { background: rgba(0, 0, 0, 0); color: white; filter: drop-shadow(0 0 0.1rem black) drop-shadow(0 0 0.1rem black); +} + +#overlayStack { + position: relative; + flex: 1 1 auto; + min-height: 0; + width: 100%; + display: flex; + flex-direction: column; +} + +#songOverlay { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; + min-height: 0; + opacity: 1; transition-property: opacity; transition-timing-function: ease-out; transition-duration: calc(var(--fade, 300) * 1ms); } -/* Production: hide when not Playing (or loading map meta). Debug: ?debug=1 (see index.html). */ -html:not(.debug) body.loading, -html:not(.debug) body:not([data-game-state="Playing"], .preview) { +#requestOverlay { + position: absolute; + left: 0; + right: 0; + top: 0; + max-height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + gap: 0.45rem; + white-space: normal; + opacity: 1; + transition-property: opacity; + transition-timing-function: ease-out; + transition-duration: calc(var(--fade, 300) * 1ms); +} + +/* Song panel: hide when not Playing (or loading map meta). Use body.preview to show placeholders (click). */ +body.loading #songOverlay, +body:not([data-game-state="Playing"], .preview) #songOverlay { opacity: 0; + pointer-events: none; +} + +/* Request queue: show when idle (Menu); hide while Playing, preview, or loading map meta */ +body[data-game-state="Playing"] #requestOverlay, +body.preview #requestOverlay, +body.loading #requestOverlay { + opacity: 0; + pointer-events: none; +} + +#requestHeader { + font-size: 2.2rem; + font-weight: 700; + line-height: 1.1; +} + +#requestList { + margin: 0; + padding-left: 2.2rem; + font-size: 1.5rem; + line-height: 1.25; +} + +#requestList:empty { + display: none; +} + +#requestList .request-item + .request-item { + margin-top: 0.35rem; +} + +#requestEmpty { + font-size: 1.45rem; + opacity: 0.88; +} + +#requestOverlay.has-items #requestEmpty { + display: none; +} + +.request-item { + display: list-item; +} + +.request-title { + font-weight: 700; +} + +.request-meta { + opacity: 0.92; + font-size: 1.45rem; +} + +.request-meta::before { + content: "· "; } span { @@ -212,7 +303,7 @@ body:not(.bsr) #bsrKey { display: none; } -body.right > .row { +body.right #songOverlay > .row { flex-direction: row-reverse; } diff --git a/index.html b/index.html index 1d7f4a6..88ce702 100644 --- a/index.html +++ b/index.html @@ -4,12 +4,10 @@