Deep-review of the quickfix batch surfaced two data/preference-loss bugs plus smaller items; all fixed with tests. - watch_sync _pull_apply: don't let the Plex-authoritative pull overwrite a DIRTY local row (a change whose immediate push failed) — that stranded the value forever since _repush_dirty only retries dirty rows. Leave it dirty for re-push. - PlexProvider / ChannelsProvider: a partial shared link opened in a fresh tab persisted mount-time DEFAULTS over the account's stored scope/sort/filters (base read before the account was pinned). Persist/keep only URL-present dims; re-hydrate the omitted ones from storage. - playback resume write-back: guard against out-of-order delivery (a superseded older push now drops) and evict the throttle map so it can't grow unbounded. - PlexPlayer: announce mid-playback buffering to assistive tech (role=status + sr-only label); new plex.player.buffering (HU+EN). - columnSort: hoist the shared key:dir sort codec used by both URL-state modules. - Tests: dirty-guard + flag-settle + per-key throttle scoping + superseded-push (backend); hold-repeat/suppress-click/unmount + partial-link no-clobber + refetch-doesn't-clobber-undo (frontend). Backend pytest 475, FE vitest 519.
68 lines
3.3 KiB
TypeScript
68 lines
3.3 KiB
TypeScript
// Serialize the Plex-browse content state (library scope + watch-state + sort + metadata filters) to/
|
|
// from the address bar so a Plex-browse view is bookmarkable/shareable — the S4 analogue of the feed's
|
|
// urlState. Keys are NAMESPACED (`pscope`/`psort`/`pshow`/`pf`) rather than reusing the feed's bare
|
|
// `scope`/`sort`/`show`: FeedFiltersProvider actively STRIPS its own KEYS from the URL whenever the app
|
|
// is off the feed page, so a shared key would be deleted out from under the Plex page. The namespaced
|
|
// keys are Plex-unique (like `?v=`/`?pl`), so no other provider ever reads or clears them. Only non-
|
|
// default values are emitted, so URLs stay clean.
|
|
import { parsePlexFilters, plexFiltersToParams, type PlexFilters } from "./api";
|
|
import { decodeSortParam, encodeSortParam, type SortState } from "./columnSort";
|
|
import { PLEX_SORT_FALLBACK } from "./plexSort";
|
|
|
|
// The content state S4 mirrors. `sort` is the RAW provider SortState (scope-healed only at the point of
|
|
// use), so the user's real preference round-trips through the URL.
|
|
export interface PlexUrlState {
|
|
scope: string;
|
|
sort: SortState;
|
|
show: string;
|
|
filters: PlexFilters;
|
|
}
|
|
|
|
const KEYS = ["pscope", "psort", "pshow", "pf"] as const;
|
|
const SCOPES = ["movie", "show", "both"];
|
|
const SHOWS = ["all", "unwatched", "in_progress", "watched"];
|
|
|
|
// Sort ⇄ `key:dir` (e.g. `year:desc`) via the shared codec in columnSort; the Plex default (`title:asc`)
|
|
// emits nothing.
|
|
function encodeSort(s: SortState): string | null {
|
|
if (!s || (s.key === PLEX_SORT_FALLBACK?.key && s.dir === PLEX_SORT_FALLBACK?.dir)) return null;
|
|
return encodeSortParam(s);
|
|
}
|
|
|
|
/** The address-bar delta for the Plex content state: each namespaced key mapped to its emitted value,
|
|
* or null to delete it (so a control back at its default clears the key). Pass `null` to clear ALL Plex
|
|
* keys — used when the app is off the Plex page (localStorage still holds the state; it re-mirrors on
|
|
* return). Feed the result into urlWithParams(). */
|
|
export function plexParamUpdates(state: PlexUrlState | null): Record<string, string | null> {
|
|
if (!state) return { pscope: null, psort: null, pshow: null, pf: null };
|
|
const pf = plexFiltersToParams(state.filters).toString();
|
|
return {
|
|
pscope: state.scope && state.scope !== "both" ? state.scope : null,
|
|
psort: encodeSort(state.sort),
|
|
pshow: state.show && state.show !== "all" ? state.show : null,
|
|
pf: pf || null,
|
|
};
|
|
}
|
|
|
|
/** True if the query string carries any Plex content key we recognize (a shared Plex-browse link). */
|
|
export function hasPlexParams(params: URLSearchParams): boolean {
|
|
return KEYS.some((k) => params.has(k));
|
|
}
|
|
|
|
/** Merge the Plex content keys from the URL over a base state (the localStorage-seeded defaults); an
|
|
* absent or invalid key keeps the base value, so a partial link (only `?psort=`) is honoured. */
|
|
export function plexParamsToState(params: URLSearchParams, base: PlexUrlState): PlexUrlState {
|
|
const scope = params.get("pscope");
|
|
const show = params.get("pshow");
|
|
const psort = params.get("psort");
|
|
const sort = psort != null ? (decodeSortParam(psort) ?? base.sort) : base.sort;
|
|
return {
|
|
scope: scope && SCOPES.includes(scope) ? scope : base.scope,
|
|
sort,
|
|
show: show && SHOWS.includes(show) ? show : base.show,
|
|
filters: params.has("pf")
|
|
? parsePlexFilters(new URLSearchParams(params.get("pf") ?? ""))
|
|
: base.filters,
|
|
};
|
|
}
|