diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
index 7618accc..de2f13aa 100644
--- a/frontend/src/components/Header.tsx
+++ b/frontend/src/components/Header.tsx
@@ -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" && }
{/* 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 && }
+ {page === "plex" && me.plex_enabled && }
{/* 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
diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx
index 72e38d6b..878b9225 100644
--- a/frontend/src/components/PlexBrowse.tsx
+++ b/frontend/src/components/PlexBrowse.tsx
@@ -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({ 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,42 +380,23 @@ export default function PlexBrowse() {
);
}
- // Discover ⟷ Browse switch, shown on both surfaces so either can return to the other.
- const viewToggle = (
- 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 = (
- <>
- setSort(next ?? PLEX_SORT_FALLBACK)}
- label={t("plex.filter.sort")}
- ascLabel={t("plex.filter.dir.asc")}
- descLabel={t("plex.filter.dir.desc")}
- />
-
- {viewToggle}
- >
+ setSort(next ?? PLEX_SORT_FALLBACK)}
+ label={t("plex.filter.sort")}
+ ascLabel={t("plex.filter.dir.asc")}
+ descLabel={t("plex.filter.dir.desc")}
+ />
);
if (sub.view.kind === "discover") {
return (
-
{viewToggle}
void;
+};
+
+const ReadCtx = createContext(null);
+const PublishCtx = createContext<(c: PlexViewControls | null) => void>(() => {});
+
+export function PlexViewBridge({ children }: { children: ReactNode }) {
+ const [controls, setControls] = useState(null);
+ return (
+
+ {children}
+
+ );
+}
+
+export const usePlexViewControls = () => useContext(ReadCtx);
+export const usePublishPlexView = () => useContext(PublishCtx);
diff --git a/frontend/src/components/PlexViewToggle.tsx b/frontend/src/components/PlexViewToggle.tsx
new file mode 100644
index 00000000..a2ba5a7b
--- /dev/null
+++ b/frontend/src/components/PlexViewToggle.tsx
@@ -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 (
+ ctl.open(v === "discover" ? { kind: "discover" } : { kind: "grid" })}
+ />
+ );
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 0b39bfb1..00e3acd7 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -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,15 +62,17 @@ const root =
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+