fix(plex,channels): deep-review fixes — durable watch-sync + no shared-link clobber
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.
This commit is contained in:
@@ -131,15 +131,31 @@ function readPlexFilters(): PlexFilters {
|
||||
return EMPTY_PLEX_FILTERS;
|
||||
}
|
||||
}
|
||||
function readPlexUrlSeed(): PlexUrlState | null {
|
||||
// A shared-link seed = the URL-merged state PLUS which dimensions the URL actually carried. `present`
|
||||
// matters because the base below is read at MOUNT, before the account is pinned (getAccountRaw returns
|
||||
// null then), so the base for an OMITTED dimension is a hard default, NOT this account's stored value.
|
||||
// The provider must therefore persist/keep only the PRESENT dimensions on account-pin and re-read the
|
||||
// account's real stored value for the absent ones — otherwise a partial link silently resets them.
|
||||
type PlexSeedPresent = { scope: boolean; sort: boolean; show: boolean; filters: boolean };
|
||||
type PlexUrlSeed = { state: PlexUrlState; present: PlexSeedPresent };
|
||||
function readPlexUrlSeed(): PlexUrlSeed | null {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (!hasPlexParams(params)) return null;
|
||||
return plexParamsToState(params, {
|
||||
const state = plexParamsToState(params, {
|
||||
scope: readPlexScope(),
|
||||
sort: readPlexSort(),
|
||||
show: readPlexShow(),
|
||||
filters: readPlexFilters(),
|
||||
});
|
||||
return {
|
||||
state,
|
||||
present: {
|
||||
scope: params.has("pscope"),
|
||||
sort: params.has("psort"),
|
||||
show: params.has("pshow"),
|
||||
filters: params.has("pf"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function PlexProvider({ children }: { children: ReactNode }) {
|
||||
@@ -148,12 +164,12 @@ export function PlexProvider({ children }: { children: ReactNode }) {
|
||||
const [urlSeed] = useState(readPlexUrlSeed);
|
||||
// The former per-library picker is now a cross-library SCOPE: movie | show | both (unified library).
|
||||
// (Reuses the old LS.plexLibrary key; an old stored library-id value falls back to "both".)
|
||||
const [scope, setScopeState] = useState<string>(() => urlSeed?.scope ?? readPlexScope());
|
||||
const [show, setShowState] = useState<string>(() => urlSeed?.show ?? readPlexShow());
|
||||
const [sort, setSortState] = useState<SortState>(() => urlSeed?.sort ?? readPlexSort());
|
||||
const [scope, setScopeState] = useState<string>(() => urlSeed?.state.scope ?? readPlexScope());
|
||||
const [show, setShowState] = useState<string>(() => urlSeed?.state.show ?? readPlexShow());
|
||||
const [sort, setSortState] = useState<SortState>(() => urlSeed?.state.sort ?? readPlexSort());
|
||||
// The expanded Plex filters live as one JSON blob (the string-only account store serializes it).
|
||||
const [filtersRaw, setFiltersRaw] = useState<string>(() =>
|
||||
urlSeed ? JSON.stringify(urlSeed.filters) : readPlexFiltersRaw(),
|
||||
urlSeed ? JSON.stringify(urlSeed.state.filters) : readPlexFiltersRaw(),
|
||||
);
|
||||
const filters = useMemo<PlexFilters>(() => {
|
||||
try {
|
||||
@@ -180,10 +196,20 @@ export function PlexProvider({ children }: { children: ReactNode }) {
|
||||
// non-shared slices (q/field) from storage. Mirrors FeedFiltersProvider's initialHadUrlFilters branch.
|
||||
const markDirty = useAccountRehydrate(() => {
|
||||
if (urlSeed) {
|
||||
setAccountRaw(LS.plexLibrary, urlSeed.scope);
|
||||
setAccountRaw(LS.plexShow, urlSeed.show);
|
||||
setAccountRaw(LS.plexSort, JSON.stringify(urlSeed.sort));
|
||||
setAccountRaw(LS.plexFilters, JSON.stringify(urlSeed.filters));
|
||||
// Per dimension: a dimension the URL carried (present) is the shared view — adopt it and persist
|
||||
// it to this now-pinned account (so it wins and survives a later plain reload). A dimension the
|
||||
// URL OMITTED was seeded from a mount-time default (no account yet), so do NOT persist that
|
||||
// default over the account's real stored value — re-read the stored value instead, exactly like a
|
||||
// normal load. This is the fix for a partial shared link silently resetting the omitted slices.
|
||||
const { state, present } = urlSeed;
|
||||
if (present.scope) setAccountRaw(LS.plexLibrary, state.scope);
|
||||
else setScopeState(readPlexScope());
|
||||
if (present.show) setAccountRaw(LS.plexShow, state.show);
|
||||
else setShowState(readPlexShow());
|
||||
if (present.sort) setAccountRaw(LS.plexSort, JSON.stringify(state.sort));
|
||||
else setSortState(readPlexSort());
|
||||
if (present.filters) setAccountRaw(LS.plexFilters, JSON.stringify(state.filters));
|
||||
else setFiltersRaw(readPlexFiltersRaw());
|
||||
setQState(readPlexQ());
|
||||
setFieldState(readPlexField());
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user