fix(ui): review round 2 — reveal floor, failed next page, count states, row Play

- DataTable: the reveal floor holds only in the view it was made for (Channels
  never clears its follow, so every later sort mounted rows up to it)
- Feed + Plex browse: a failed next page stops the read-ahead and shows a row
  that retries, instead of re-requesting it once a round-trip
- the YouTube Load more click uses nextPageOnClick; a contract pins NEXT_PAGE
  to the read-aheads and the click sites to nextPageOnClick
- ItemCount: a failed count says so; a stale one says updating to a screen reader
- the narrow-picture Play lift applies only where a hover row is there to clear
- PlayerModal: one description ladder for the popover and the panel, both tested
- plexImage contract reads every img src, any attribute order
- Tabs comment names who persists the sub-tab
This commit is contained in:
2026-10-03 02:25:15 +02:00
parent c4c442bc5b
commit b40440b825
17 changed files with 360 additions and 63 deletions
+14 -2
View File
@@ -9,7 +9,7 @@ import {
} from "@tanstack/react-query";
import { User, X } from "lucide-react";
import { qk } from "../lib/queryKeys";
import { mayReadAhead, NEXT_PAGE } from "../lib/nextPage";
import { mayReadAhead, NEXT_PAGE, nextPageOnClick } from "../lib/nextPage";
import { healPlexField } from "../lib/plexSearch";
import { healPlexSort, plexSortOptions, PLEX_SORT_FALLBACK } from "../lib/plexSort";
import type { SortState } from "../lib/columnSort";
@@ -317,7 +317,10 @@ export default function PlexBrowse() {
// mount/unmount, so the observer always watches the live node; a plain ref would keep observing the
// detached old node after Back and never fire fetchNextPage again (feed stuck at the loaded pages).
const [sentinelEl, setSentinelEl] = useState<HTMLDivElement | null>(null);
const { hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } = browseQ;
const { isFetching, isFetchingNextPage, isFetchNextPageError, fetchNextPage } = browseQ;
// A failed next page stops the read-ahead (the row at the bottom retries): the observer is rebuilt
// on every settle and would re-request it once a round-trip while the sentinel is in view.
const hasNextPage = browseQ.hasNextPage && !isFetchNextPageError;
useEffect(() => {
if (!sentinelEl) return;
const io = new IntersectionObserver(
@@ -753,6 +756,15 @@ export default function PlexBrowse() {
{isFetchingNextPage && (
<p className="text-center text-xs text-muted pb-4">{t("plex.loading")}</p>
)}
{isFetchNextPageError && !isFetchingNextPage && (
<StateMessage
tone="error"
compact
onRetry={() => void fetchNextPage(nextPageOnClick(isFetchingNextPage))}
>
{t("common.loadMoreFailed")}
</StateMessage>
)}
</div>
);
}