feat(plex): move the Discover/Browse toggle into the header
The Discover ⟷ Browse switch now sits in the header next to the library-scope pill (All / Movies / Shows), instead of on the chip row / atop the Discover surface. It's driven through a small one-way PlexViewBridge: PlexBrowse still OWNS the sub-view (useHistorySubview) and publishes its top-level view + opener to the bridge; the always-mounted Header reads it to render PlexViewToggle. A global sub-view provider was deliberately avoided — PlexBrowse's hook remounts on every plex entry and re-seeds from history (a page switch clears ?v + drops _sub), so each entry starts at the Discover root; lifting it out would let the view go stale across navigation. The toggle hides on deeper views (show/info/person/…) and on non-plex pages (bridge cleared on unmount). sortView is just SortControl now; the grid toolbar no longer carries the toggle. Verified: toggle in the header, switches discover⟷grid, hides on info + on Feed, and Feed→Plex resets to Discover (no stale view); no console errors.
This commit is contained in:
@@ -5,6 +5,7 @@ import type { Me } from "../lib/api";
|
||||
import SearchBar from "./SearchBar";
|
||||
import PlexSearchBox from "./PlexSearchBox";
|
||||
import PlexScopeToggle from "./PlexScopeToggle";
|
||||
import PlexViewToggle from "./PlexViewToggle";
|
||||
import FeedScopeToggle from "./FeedScopeToggle";
|
||||
import SyncStatus from "./SyncStatus";
|
||||
import { useNavigation } from "./NavigationProvider";
|
||||
@@ -103,8 +104,10 @@ const Header = forwardRef<
|
||||
so the primary "what am I browsing" control is always in reach (mirrors Plex's scope toggle). */}
|
||||
{page === "feed" && <FeedScopeToggle />}
|
||||
{/* Library scope (movie/show/both) sits right of the search box on Plex — relocated here from
|
||||
the filter sidebar (S3) so the primary "what am I browsing" control is always in reach. */}
|
||||
the filter sidebar (S3) so the primary "what am I browsing" control is always in reach. The
|
||||
Discover ⟷ Browse toggle sits right beside it (driven via the PlexViewBridge). */}
|
||||
{page === "plex" && me.plex_enabled && <PlexScopeToggle />}
|
||||
{page === "plex" && me.plex_enabled && <PlexViewToggle />}
|
||||
{/* Per-user sync status — it used to cost the nav rail a whole block. It sits with the other
|
||||
pills rather than pinned to the far right: on a wide window that just strands it. Only on
|
||||
the two modules it says anything about: the feed those subscriptions fill, and the manager
|
||||
|
||||
@@ -25,7 +25,6 @@ import { useHistorySubview } from "../lib/history";
|
||||
import { PLEX_SUB_URL, type Sub } from "./plexSubUrl";
|
||||
import { toggleWatchState } from "../lib/plexWatch";
|
||||
import SortControl from "./SortControl";
|
||||
import SegmentedToggle from "./SegmentedToggle";
|
||||
import FilterSelect from "./FilterSelect";
|
||||
import PlexDiscover from "./PlexDiscover";
|
||||
import { PageToolbar } from "./PageShell";
|
||||
@@ -34,6 +33,7 @@ import ItemCount from "./ItemCount";
|
||||
import { StateMessage } from "./QueryState";
|
||||
import { usePlexActiveFilters, usePlexClearFilters } from "./usePlexActiveFilters";
|
||||
import { usePlex, usePlexActions } from "./PlexProvider";
|
||||
import { usePublishPlexView } from "./PlexViewBridge";
|
||||
import { EpisodeCard, PlexPosterCard } from "./plexCards";
|
||||
import { PlexInfoView, PlexPersonView, PlexSeasonView, PlexShowView } from "./plexViews";
|
||||
|
||||
@@ -82,6 +82,20 @@ export default function PlexBrowse() {
|
||||
// or an added filter is a browse intent → flip to the grid (below); an explicit toggle returns.
|
||||
const sub = useHistorySubview<Sub>({ kind: "discover" }, PLEX_SUB_URL);
|
||||
|
||||
// Publish the top-level view (discover vs grid) + opener to the header's Discover/Browse toggle via
|
||||
// the bridge; clear it on unmount so the toggle disappears when you leave the plex page. Keyed on the
|
||||
// view KIND (open is behaviourally stable — it takes the next view as an arg, closing over nothing).
|
||||
const publishView = usePublishPlexView();
|
||||
useLayoutEffect(() => {
|
||||
publishView({
|
||||
isDiscover: sub.view.kind === "discover",
|
||||
toggleable: sub.view.kind === "discover" || sub.view.kind === "grid",
|
||||
open: sub.open,
|
||||
});
|
||||
return () => publishView(null);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sub.view.kind]);
|
||||
|
||||
// Sidebar asked to open a playlist → push it as a subview (browser Back returns to the grid), then
|
||||
// clear the signal so it doesn't re-open.
|
||||
useEffect(() => {
|
||||
@@ -366,25 +380,10 @@ export default function PlexBrowse() {
|
||||
);
|
||||
}
|
||||
|
||||
// Discover ⟷ Browse switch, shown on both surfaces so either can return to the other.
|
||||
const viewToggle = (
|
||||
<SegmentedToggle
|
||||
size="md"
|
||||
ariaLabel={t("plex.view.label")}
|
||||
options={[
|
||||
{ value: "discover", label: t("plex.discover.tab") },
|
||||
{ value: "grid", label: t("plex.view.browse") },
|
||||
]}
|
||||
value={sub.view.kind === "discover" ? "discover" : "grid"}
|
||||
onChange={(v) => sub.open(v === "discover" ? { kind: "discover" } : { kind: "grid" })}
|
||||
/>
|
||||
);
|
||||
|
||||
// Sort + the Discover/Browse toggle are tightly bound to the grid below, so on the grid view they
|
||||
// live pinned to the right of the applied-filter chip row (directly above the cards), not on their
|
||||
// own toolbar line — the same idiom as the Feed's `sortView`.
|
||||
// Sort is tightly bound to the grid below, so on the grid view it lives pinned to the right of the
|
||||
// applied-filter chip row (directly above the cards), not on its own toolbar line — the same idiom as
|
||||
// the Feed's `sortView`. The Discover/Browse toggle moved up to the header (next to the scope pill).
|
||||
const sortView = (
|
||||
<>
|
||||
<SortControl
|
||||
options={sortOptions}
|
||||
sort={sortState}
|
||||
@@ -393,15 +392,11 @@ export default function PlexBrowse() {
|
||||
ascLabel={t("plex.filter.dir.asc")}
|
||||
descLabel={t("plex.filter.dir.desc")}
|
||||
/>
|
||||
<span className="mx-0.5 h-5 w-px bg-border" aria-hidden="true" />
|
||||
{viewToggle}
|
||||
</>
|
||||
);
|
||||
|
||||
if (sub.view.kind === "discover") {
|
||||
return (
|
||||
<div className="p-4 max-w-[1600px] mx-auto">
|
||||
<div className="mb-4 flex items-center">{viewToggle}</div>
|
||||
<PlexDiscover
|
||||
scope={scope}
|
||||
onOpenCard={onCard}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createContext, useContext, useState, type ReactNode } from "react";
|
||||
import { type Sub } from "./plexSubUrl";
|
||||
|
||||
// A one-way bridge so the always-mounted Header can host the Plex Discover ⟷ Browse toggle even though
|
||||
// the sub-view state lives in (and is owned by) the plex-only PlexBrowse. PlexBrowse PUBLISHES its
|
||||
// current top-level view + opener here (on mount and on view change) and clears it on unmount; the
|
||||
// Header READS it to render the toggle beside the library-scope pill.
|
||||
//
|
||||
// Why a bridge and not a global sub-view provider: PlexBrowse's `useHistorySubview` REMOUNTS every time
|
||||
// you enter the plex page, re-seeding the view from history (a page switch clears ?v + drops _sub, so
|
||||
// each plex entry starts at the Discover root). Lifting that hook to an always-mounted provider would
|
||||
// lose the remount and let the view go stale across navigation. Keeping it in PlexBrowse preserves the
|
||||
// reseed; the bridge only mirrors the small slice the header needs.
|
||||
export type PlexViewControls = {
|
||||
isDiscover: boolean; // the Discover landing vs the flat Browse grid
|
||||
toggleable: boolean; // only discover|grid host the toggle; deeper views (show/info/person/…) hide it
|
||||
open: (v: Sub) => void;
|
||||
};
|
||||
|
||||
const ReadCtx = createContext<PlexViewControls | null>(null);
|
||||
const PublishCtx = createContext<(c: PlexViewControls | null) => void>(() => {});
|
||||
|
||||
export function PlexViewBridge({ children }: { children: ReactNode }) {
|
||||
const [controls, setControls] = useState<PlexViewControls | null>(null);
|
||||
return (
|
||||
<PublishCtx.Provider value={setControls}>
|
||||
<ReadCtx.Provider value={controls}>{children}</ReadCtx.Provider>
|
||||
</PublishCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const usePlexViewControls = () => useContext(ReadCtx);
|
||||
export const usePublishPlexView = () => useContext(PublishCtx);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SegmentedToggle from "./SegmentedToggle";
|
||||
import { usePlexViewControls } from "./PlexViewBridge";
|
||||
|
||||
// The Discover ⟷ Browse switch, hosted in the header next to the library-scope pill (All/Movies/Shows).
|
||||
// It reflects + drives PlexBrowse's top-level sub-view through the PlexViewBridge, and hides itself on
|
||||
// the deeper views (a show/info/person page has no Discover/Browse choice).
|
||||
export default function PlexViewToggle() {
|
||||
const { t } = useTranslation();
|
||||
const ctl = usePlexViewControls();
|
||||
if (!ctl || !ctl.toggleable) return null;
|
||||
return (
|
||||
<SegmentedToggle
|
||||
size="md"
|
||||
ariaLabel={t("plex.view.label")}
|
||||
options={[
|
||||
{ value: "discover", label: t("plex.discover.tab") },
|
||||
{ value: "grid", label: t("plex.view.browse") },
|
||||
]}
|
||||
value={ctl.isDiscover ? "discover" : "grid"}
|
||||
onChange={(v) => ctl.open(v === "discover" ? { kind: "discover" } : { kind: "grid" })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { FeedFiltersProvider } from "./components/FeedFiltersProvider";
|
||||
import { SavedViewsProvider } from "./components/SavedViewsProvider";
|
||||
import { PlaylistsProvider } from "./components/PlaylistsProvider";
|
||||
import { PlexProvider } from "./components/PlexProvider";
|
||||
import { PlexViewBridge } from "./components/PlexViewBridge";
|
||||
import { ChannelsProvider } from "./components/ChannelsProvider";
|
||||
import { FeedViewProvider } from "./components/FeedViewProvider";
|
||||
import { PrefsProvider } from "./components/PrefsProvider";
|
||||
@@ -61,6 +62,7 @@ const root =
|
||||
<SavedViewsProvider>
|
||||
<PlaylistsProvider>
|
||||
<PlexProvider>
|
||||
<PlexViewBridge>
|
||||
<ChannelsProvider>
|
||||
<FeedViewProvider>
|
||||
<PrefsProvider>
|
||||
@@ -70,6 +72,7 @@ const root =
|
||||
</PrefsProvider>
|
||||
</FeedViewProvider>
|
||||
</ChannelsProvider>
|
||||
</PlexViewBridge>
|
||||
</PlexProvider>
|
||||
</PlaylistsProvider>
|
||||
</SavedViewsProvider>
|
||||
|
||||
Reference in New Issue
Block a user