fix(ui): keep one Back arrow on detail pages (#1627)

Movie and series detail pages showed two arrow_back controls while the
inline player was open: the shell's sticky arrow (added in #1576 so Back
survives scrolling) meant "Close player" in watch state, while the
now-playing bar carried a second arrow that meant route-level Back — the
same icon with two meanings, next to a "Close player" button that
duplicated the first.

The shell's sticky arrow is now route-level Back in browse and watch
alike, and the bar carries no arrow of its own. Closing the player is
the bar's "Close player" button and Escape, which still unwinds one
level (close, then back). Hosts without a browse Back target (M3U,
downloads) render no arrow in either state.

Unit specs for the shell and the inline player cover the new contract;
the Electron and web E2E helpers that pressed Back from watch are
updated, and the M3U flow closes the player through the bar's button.
Docs, the mirrored CLAUDE.md/AGENTS.md paragraph and a release note
follow the change.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 authored and GitHub committed 2026-09-19 10:44:17 +02:00
1 parent 5273e5fb55
commit c76a901e8d
22 files changed
+127 -107

No files matched your search

+14 -12
View File
@@ -162,18 +162,20 @@ Contracts:
player subtree, so shell state changes cannot recreate the `<video>`.
- **External MPV/VLC sessions do not flip the layout to watch** — browse
layout stays, and the primary CTA keeps its "Stop <player>" behavior.
- The shell's sticky control and Escape close inline playback through
`closePlayerRequested`; hosts wire it to `closeInlinePlayer()`. From browse,
they emit the host-owned `backClicked` instead (unless `backAvailable=false`).
Escape respects fullscreen, menus/dialogs, editable fields and hidden/inert
surfaces, and consumes a handled key so one press performs only one action.
See [Portal Detail Navigation](./portal-detail-navigation.md).
- The now-playing bar separates two exits: the back arrow emits
`backClicked`, which hosts wire to their route-level `goBack()` (straight
back to the list — everything browse offers is also visible in watch, so
a two-step unwind would be ceremony); the "Close player" button and
Escape emit `closed`/`closePlayerRequested` and return to browse without
navigating.
- The shell's sticky arrow emits the host-owned `backClicked` in browse and
watch alike (unless `backAvailable=false`, when it is not rendered at all):
hosts wire it to their route-level `goBack()`, straight back to the list —
everything browse offers is also visible in watch, so a two-step unwind
would be ceremony. Escape alone unwinds one level: in watch it emits
`closePlayerRequested`, which hosts wire to `closeInlinePlayer()`; in browse
it emits `backClicked`. Escape respects fullscreen, menus/dialogs, editable
fields and hidden/inert surfaces, and consumes a handled key so one press
performs only one action. See
[Portal Detail Navigation](./portal-detail-navigation.md).
- The now-playing bar has one exit of its own: the "Close player" button
emits `closed` and returns to browse without navigating. It carries no back
arrow — a second arrow beside the sticky one, with a different meaning,
was the duplicate this contract removes.
- Entering watch scrolls the shell to the top; leaving keeps the scroll
position.
+17 -11
View File
@@ -30,10 +30,14 @@ The button has an opaque app-themed surface, visible keyboard focus, an Escape
shortcut hint via native `title` and Electron `no-drag` hit testing. The hint
does not create an overlay that could consume the first Escape press.
The sticky control and Escape unwind one level: watch emits
`closePlayerRequested`, browse emits `backClicked`. Hosts retain their existing
route/inline/collection return behavior. The now-playing bar's separate route
Back action still returns directly to the list. Browse Escape requires focus
The sticky control is route-level Back in both states: it emits `backClicked`
whether or not inline playback is active, so the arrow keeps one meaning and
the list is one click away while watching. Only Escape unwinds one level: watch
emits `closePlayerRequested`, browse emits `backClicked`. Hosts retain their
existing route/inline/collection return behavior. The now-playing bar carries
no second arrow; its "Close player" button is the pointer counterpart of the
watch Escape (two arrows with different meanings, and two controls for one
action, were the pre-#1576 confusion this replaces). Browse Escape requires focus
inside this shell; watch keeps the existing global close shortcut, including
M3U playback started from its sidebar. Handled events, key repeats/modifiers,
editable fields, inert/hidden shells, fullscreen, dialogs and menus are ignored.
@@ -41,13 +45,15 @@ Escape bubbles through the shell before Material's body-level tooltip dispatcher
so focused detail actions return with one press even while their tooltip is open.
The document listener remains the outside-shell watch fallback; `defaultPrevented`
prevents duplicate actions and preserves descendant handlers' priority.
After closing a player, lost focus moves to the sticky control (or the shell
when there is no browse Back), without scrolling or stealing existing focus.
After Escape closes a player, lost focus moves to the sticky control (or the
shell when there is no browse Back), without scrolling or stealing existing
focus.
Hosts without browse navigation set `backAvailable=false`: M3U uses its channel
sidebar, and collection bootstrap placeholders have no return handler. They
have no browse button or browse Escape action; M3U watch still offers Close
player. Loading/error shells with a return handler keep Back available.
render no sticky arrow in either state and have no browse Escape action; their
watch exits are the bar's Close player button and Escape. Loading/error shells
with a return handler keep Back available.
## Summary
@@ -55,9 +61,9 @@ player. Loading/error shells with a return handler keep Back available.
- Stalker uses an inline/store-state detail model.
- Detail pages themselves are two-state (browse ↔ watch) inside
`PortalDetailShellComponent`; entering/leaving watch is a layout state,
not a navigation. Route-level back semantics are unchanged; the
sticky watch control closes the inline player, while the now-playing bar
retains its separate direct return to the list. See
not a navigation. Route-level back semantics are unchanged; the one
sticky arrow returns to the list from either state, while Escape and the
now-playing bar's Close button close the inline player. See
[Embedded Inline Playback](./embedded-inline-playback.md).
- Favorites and recently viewed collections now use collection-owned inline detail
for non-live Xtream and Stalker items.