fix(plex): a Restart does not replay on a reload or a Forward onto the player
fromStart rode in history.state._sub, so reloading the player entry restarted the item from 0 again and the progress checkpoint overwrote its resume point. A sub-view config can now say what the history entry keeps of a view.
This commit is contained in:
@@ -76,3 +76,18 @@ describe("PLEX_SUB_URL decode guards", () => {
|
||||
expect(U.decode("")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("PLEX_SUB_URL entry", () => {
|
||||
// E28 review E1: the Restart click must not replay on a reload / Forward onto the player entry.
|
||||
it("keeps the player without its one-shot fromStart, and every other view whole", () => {
|
||||
const entry = U.entry!;
|
||||
expect(entry({ kind: "player", id: "7", queue: ["7", "8"], fromStart: true })).toEqual({
|
||||
kind: "player",
|
||||
id: "7",
|
||||
queue: ["7", "8"],
|
||||
fromStart: undefined,
|
||||
});
|
||||
const info = { kind: "info", id: "7" } as const;
|
||||
expect(entry(info)).toBe(info);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,6 +49,10 @@ export const PLEX_SUB_URL: SubviewUrl<Sub> = {
|
||||
return null; // discover (root) + the ephemeral player are not URL-mirrored
|
||||
}
|
||||
},
|
||||
// `fromStart` is the Restart CLICK, not a property of the entry: kept in history.state it restarted
|
||||
// the item from 0 again on every reload or Forward onto the player, and the progress checkpoint
|
||||
// then overwrote the resume point it had skipped (E28 review E1).
|
||||
entry: (v) => (v.kind === "player" && v.fromStart ? { ...v, fromStart: undefined } : v),
|
||||
decode: (raw) => {
|
||||
const i = raw.indexOf(":");
|
||||
const kind = i === -1 ? raw : raw.slice(0, i);
|
||||
|
||||
@@ -138,3 +138,35 @@ describe("useHistorySubview popstate", () => {
|
||||
expect(result.current.view).toBe("root");
|
||||
});
|
||||
});
|
||||
|
||||
// E28 review E1: a one-shot flag the view is OPENED with (the Plex player's `fromStart`, a card's
|
||||
// Restart) rode in history.state._sub, so a reload or a Forward onto that entry restarted from 0
|
||||
// again — and the player's checkpoint then overwrote the resume point. `entry` says what the
|
||||
// history entry keeps of a view; the view itself still opens with the flag.
|
||||
describe("useHistorySubview entry", () => {
|
||||
type V = { kind: string; once?: boolean };
|
||||
const withEntry: SubviewUrl<V> = {
|
||||
key: "v",
|
||||
encode: () => null,
|
||||
decode: () => null,
|
||||
entry: (v) => ({ kind: v.kind }),
|
||||
};
|
||||
|
||||
it("opens the view with the flag, but the history entry keeps it without", () => {
|
||||
const { result } = renderHook(() => useHistorySubview<V>({ kind: "root" }, withEntry));
|
||||
act(() => result.current.open({ kind: "player", once: true }));
|
||||
expect(result.current.view).toEqual({ kind: "player", once: true });
|
||||
expect(window.history.state._sub).toEqual({ kind: "player" });
|
||||
// A reload of that entry reads what the entry kept.
|
||||
const { result: reloaded } = renderHook(() =>
|
||||
useHistorySubview<V>({ kind: "root" }, withEntry),
|
||||
);
|
||||
expect(reloaded.current.view).toEqual({ kind: "player" });
|
||||
});
|
||||
|
||||
it("keeps it out of a replaced entry too", () => {
|
||||
const { result } = renderHook(() => useHistorySubview<V>({ kind: "root" }, withEntry));
|
||||
act(() => result.current.open({ kind: "player", once: true }, { replace: true }));
|
||||
expect(window.history.state._sub).toEqual({ kind: "player" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,6 +106,9 @@ export interface SubviewUrl<T> {
|
||||
encode: (view: T) => string | null;
|
||||
/** Parse a param value back to a sub-view, or null if it isn't a valid one. */
|
||||
decode: (raw: string) => T | null;
|
||||
/** What the history ENTRY keeps of a view, when that is less than the view opens with: a one-shot
|
||||
* flag (the Plex player's `fromStart`) must not replay on a reload or a Forward onto the entry. */
|
||||
entry?: (view: T) => T;
|
||||
}
|
||||
|
||||
export function useHistorySubview<T>(
|
||||
@@ -152,19 +155,20 @@ export function useHistorySubview<T>(
|
||||
const open = useCallback(
|
||||
(next: T, opts?: { replace?: boolean }) => {
|
||||
setView(next);
|
||||
const kept = url?.entry ? url.entry(next) : next;
|
||||
const href = url ? urlWithParams({ [url.key]: url.encode(next) }) : "";
|
||||
if (opts?.replace) {
|
||||
// Same ENTRY, so it keeps its sfScroll id: PageScroller keys offsets per (entry, view), and
|
||||
// the view key has changed, so the sub-view still opens at the top without minting an id
|
||||
// that would orphan the one this entry already carries.
|
||||
window.history.replaceState({ ...window.history.state, _sub: next }, "", href);
|
||||
window.history.replaceState({ ...window.history.state, _sub: kept }, "", href);
|
||||
notifyNav();
|
||||
return;
|
||||
}
|
||||
// A fresh sfScroll so the opened sub-view is its own scroll entry (starts at the top, and Back
|
||||
// restores the view we came from) — then notify PageScroller, which this push doesn't re-render.
|
||||
window.history.pushState(
|
||||
{ ...rememberNavPosition(), _sub: next, sfScroll: nextNavId() },
|
||||
{ ...rememberNavPosition(), _sub: kept, sfScroll: nextNavId() },
|
||||
"",
|
||||
href,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user