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();
|
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
|
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) => {
|
decode: (raw) => {
|
||||||
const i = raw.indexOf(":");
|
const i = raw.indexOf(":");
|
||||||
const kind = i === -1 ? raw : raw.slice(0, i);
|
const kind = i === -1 ? raw : raw.slice(0, i);
|
||||||
|
|||||||
@@ -138,3 +138,35 @@ describe("useHistorySubview popstate", () => {
|
|||||||
expect(result.current.view).toBe("root");
|
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;
|
encode: (view: T) => string | null;
|
||||||
/** Parse a param value back to a sub-view, or null if it isn't a valid one. */
|
/** Parse a param value back to a sub-view, or null if it isn't a valid one. */
|
||||||
decode: (raw: string) => T | null;
|
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>(
|
export function useHistorySubview<T>(
|
||||||
@@ -152,19 +155,20 @@ export function useHistorySubview<T>(
|
|||||||
const open = useCallback(
|
const open = useCallback(
|
||||||
(next: T, opts?: { replace?: boolean }) => {
|
(next: T, opts?: { replace?: boolean }) => {
|
||||||
setView(next);
|
setView(next);
|
||||||
|
const kept = url?.entry ? url.entry(next) : next;
|
||||||
const href = url ? urlWithParams({ [url.key]: url.encode(next) }) : "";
|
const href = url ? urlWithParams({ [url.key]: url.encode(next) }) : "";
|
||||||
if (opts?.replace) {
|
if (opts?.replace) {
|
||||||
// Same ENTRY, so it keeps its sfScroll id: PageScroller keys offsets per (entry, view), and
|
// 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
|
// 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.
|
// 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();
|
notifyNav();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// A fresh sfScroll so the opened sub-view is its own scroll entry (starts at the top, and Back
|
// 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.
|
// restores the view we came from) — then notify PageScroller, which this push doesn't re-render.
|
||||||
window.history.pushState(
|
window.history.pushState(
|
||||||
{ ...rememberNavPosition(), _sub: next, sfScroll: nextNavId() },
|
{ ...rememberNavPosition(), _sub: kept, sfScroll: nextNavId() },
|
||||||
"",
|
"",
|
||||||
href,
|
href,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user