diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a2cda4..4eb177f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ edit the release notes and re-run the generator. Upgrading, pinning a version and rolling back: [docs/self-hosting.md](docs/self-hosting.md#upgrading). +## 0.106.1 — 2026-10-14 + +### Fixed + +- Plex player: clicking outside the Settings panel closes the panel again instead of closing the player. + ## 0.106.0 — 2026-10-13 ### New diff --git a/VERSION b/VERSION index 75602ab1..a2a3eca1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.106.0 +0.106.1 diff --git a/frontend/src/content/release-notes/en.json b/frontend/src/content/release-notes/en.json index 050abbda..fd256495 100644 --- a/frontend/src/content/release-notes/en.json +++ b/frontend/src/content/release-notes/en.json @@ -1,4 +1,12 @@ [ + { + "version": "0.106.1", + "date": "2026-10-14", + "new": [], + "fixed": [ + "Plex player: clicking outside the Settings panel closes the panel again instead of closing the player." + ] + }, { "version": "0.106.0", "date": "2026-10-13", diff --git a/frontend/src/content/release-notes/hu.json b/frontend/src/content/release-notes/hu.json index 1d12e8bb..94950c04 100644 --- a/frontend/src/content/release-notes/hu.json +++ b/frontend/src/content/release-notes/hu.json @@ -1,4 +1,12 @@ [ + { + "version": "0.106.1", + "date": "2026-10-14", + "new": [], + "fixed": [ + "Plex lejátszó: a Beállítások panelen kívül kattintva ismét csak a panel záródik be, nem a lejátszó." + ] + }, { "version": "0.106.0", "date": "2026-10-13", diff --git a/frontend/src/lib/history.dom.test.ts b/frontend/src/lib/history.dom.test.ts index 9a5ab3af..87dd6ed2 100644 --- a/frontend/src/lib/history.dom.test.ts +++ b/frontend/src/lib/history.dom.test.ts @@ -236,6 +236,53 @@ describe("closeOverlaysThenNavigate", () => { } }); + // The Plex player's ⋯ → Settings: the menu closes (its entry goes back) and the panel opens a task + // later, before that back() has landed. A push made then is the entry the landing back() takes, so + // the books said one entry stood that did not — and the panel's own close stepped back over the + // player's entry, closing the player on a click outside the panel. + it("an overlay opened while a back() is in flight pushes its entry only once that back() lands", async () => { + const landing: (() => void)[] = []; + const back = vi.spyOn(window.history, "back").mockImplementation(() => { + landing.push(() => + window.dispatchEvent(new PopStateEvent("popstate", { state: window.history.state })), + ); + }); + const land = () => act(() => landing.splice(0).forEach((l) => l())); + const push = vi.spyOn(window.history, "pushState"); + let panel: { unmount: () => void } | undefined; + try { + const menu = mountLayer(() => {}); + await settle(); + menu.unmount(); + await settle(); + expect(back).toHaveBeenCalledTimes(1); + + push.mockClear(); + panel = mountLayer(() => {}); + await tick(); + expect(push).not.toHaveBeenCalled(); // it would be undone by the landing back() + + land(); + await settle(); + expect(push).toHaveBeenCalledTimes(1); + + // The books are square: closing the panel reclaims its own entry, one back() and no more. + back.mockClear(); + panel.unmount(); + panel = undefined; + await settle(); + expect(back).toHaveBeenCalledTimes(1); + } finally { + // Square the module-global books for the next test, however far this one got. + panel?.unmount(); + await settle(); + land(); + await settle(); + back.mockRestore(); + push.mockRestore(); + } + }); + // No such overlay exists today, but a future "discard your changes?" guard must not have the page // switched out from under it: dropping the command is the honest outcome. it("drops the command when an overlay does not actually close", async () => { diff --git a/frontend/src/lib/history.ts b/frontend/src/lib/history.ts index 35ff60d7..7ece3984 100644 --- a/frontend/src/lib/history.ts +++ b/frontend/src/lib/history.ts @@ -261,10 +261,14 @@ let listenerInstalled = false; const syncWaiters = new Set<() => void>(); // Push/pop history entries until their count matches the live overlay count. Runs once per tick. +// A push waits while a back() of ours is still in flight: it would land on the entry just pushed and +// take it, leaving the books one entry above the history — and the next close then stepped back over +// the entry BELOW, the player's own (the Plex player's ⋯ → Settings, then a click outside the panel, +// closed the player). The popstate that lands the last back() reconciles again. function reconcileHistory(): void { reconcileScheduled = false; const want = overlayStack.length; - while (historyDepth < want) { + while (historyDepth < want && pendingProgrammatic === 0) { historyDepth++; window.history.pushState({ ...window.history.state, _ov: nextOverlayToken() }, ""); } @@ -294,6 +298,8 @@ function ensureOverlayListener(): void { window.addEventListener("popstate", () => { if (pendingProgrammatic > 0) { pendingProgrammatic--; // our own back(): consume and ignore + // The last one landed: an overlay that opened while it was in flight still owes its entry. + if (pendingProgrammatic === 0 && historyDepth < overlayStack.length) scheduleReconcile(); return; } // Genuine user Back: the browser already dropped one overlay entry, so close the topmost