refactor: component files export only components; lint fails on a warning

The 43 react-refresh/only-export-components warnings the gate let through meant an edit to a
provider (or AnchoredPopover, Toaster, plexDetailUi, …) could not be hot-swapped: Fast Refresh
reloads a module that also exports hooks or helpers.

- Each provider's contexts, the types its hooks return and the hooks themselves live in a sibling
  context module (navigationContext.ts, channelsContext.ts, …); the provider file keeps the
  component. loadAccountFilters moved next to the feed filters' defaults (feedFiltersInit.ts),
  following navigationInit/playlistsInit.
- Helpers that sat in component files have their own modules: welcomeMode, downloadTitle,
  plexPlayable, channelSync, ui/anchoredPopoverHooks, ui/pageToolbarSlot, ui/pagerContent,
  ui/toastLevels, ui/overlayOrder, ui/tableAlign, lib/plexDetail; the cells of the channel columns
  and the registry's page wrappers are components files of their own (channelCells, modulePages).
- usePersistedTab, an alias of useAccountPersistedState, is gone; its callers use the original.

npm run lint is now eslint --max-warnings 0, so a warning fails the gate like an error does.
This commit is contained in:
2026-09-17 02:59:09 +02:00
parent 569190480d
commit c3d88d9b20
158 changed files with 1600 additions and 1431 deletions
+11 -58
View File
@@ -1,13 +1,4 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
} from "react";
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { urlWithParams, type Page } from "../lib/urlState";
import { pageTitleKey } from "../lib/pageMeta";
@@ -20,57 +11,19 @@ import {
rememberNavPosition,
} from "../lib/history";
import { channelViewFrom, loadInitialPage, PAGE_KEY, type ChannelView } from "./navigationInit";
import {
NavigationActionsContext,
NavigationStateContext,
type NavigationActions,
type NavigationState,
} from "./navigationContext";
// Owns the app's navigation + browser-history state, lifted out of the App god component so pages
// consume it via a hook instead of prop-drilling `page`/`setPage`/`onOpenChannel` through the tree.
// Mirrors ConfirmProvider (context + default value + one-line hooks); composed in main.tsx inside
// ConfirmProvider, around <App/>.
//
// TWO contexts on purpose: a STABLE actions context (setPage/openChannel/… never change reference)
// and a CHANGING state context (page/channelView/ytSearch). Context consumers re-render whenever the
// value changes, so deep memo'd leaves (VideoCard, PlayerModal) subscribe to actions ONLY and never
// re-render on a page/channel switch — preserving the memo that keeps feed appends from re-rendering
// every existing card.
export interface NavigationState {
page: Page;
/** The open channel page (null = none). Overlays `page`; rides in history.state._chan. */
channelView: ChannelView | null;
/** Live YouTube search term (null = normal feed). A feed sub-view; rides in history.state._yt. */
ytSearch: string | null;
}
export interface NavigationActions {
/** Switch modules. Closes every open overlay first and pushes only once their history entries are
* back (`lib/history.closeOverlaysThenNavigate`), so over an open player the push lands a moment
* after the call; with nothing open it is synchronous. The requested page being already current
* (with no channel page over it) pushes nothing — but the overlays still close. */
setPage: (next: Page) => void;
openChannel: (id: string, name?: string) => void;
closeChannel: () => void;
enterYtSearch: (q: string) => void;
exitYtSearch: () => void;
}
const StateContext = createContext<NavigationState>({
page: "feed",
channelView: null,
ytSearch: null,
});
const ActionsContext = createContext<NavigationActions>({
setPage: () => {},
openChannel: () => {},
closeChannel: () => {},
enterYtSearch: () => {},
exitYtSearch: () => {},
});
export function useNavigation(): NavigationState {
return useContext(StateContext);
}
export function useNavigationActions(): NavigationActions {
return useContext(ActionsContext);
}
// The contexts, their value types and the hooks live in navigationContext.ts.
export function NavigationProvider({ children }: { children: ReactNode }) {
const { t, i18n } = useTranslation();
@@ -256,8 +209,8 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
);
return (
<ActionsContext.Provider value={actions}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</ActionsContext.Provider>
<NavigationActionsContext.Provider value={actions}>
<NavigationStateContext.Provider value={state}>{children}</NavigationStateContext.Provider>
</NavigationActionsContext.Provider>
);
}