feat(plex): P1.5 — Plex as a nav module + integrated search + filter sidebar

Per user feedback: promote Plex from a hidden feed-Source option to a first-class
left-nav module, with its own filter sidebar and the shared top search box.

- Plex is now a nav-rail module (page='plex', gated on me.plex_enabled) — its own
  page → correct browser Back/Forward; removed the Feed Source-dropdown 'plex' hack.
- PlexSidebar.tsx: left filter column (library scope, watch-state for movies:
  all/unwatched/in_progress/watched, sort) — per-account persisted (App state).
- Header search box now also serves the Plex page (integrated search); the live
  YouTube-search escalation stays feed-only.
- PlexBrowse.tsx reworked: infinite scroll (IntersectionObserver, no 'Load more'),
  content-visibility for smoother long-list scroll, show drill-down rides history
  (useHistorySubview) so Back returns to the grid; dropped the stray 'YouTube feed'
  button on the show page.
- backend /browse gains a per-user watch-state filter (show=, movies only).
- plex i18n (navLabel + filter.*) en/hu/de.

Note: episode-title 'Episode N' on some shows (e.g. Westworld) is Plex-side
(unmatched show) — we mirror what Plex has; Match/Refresh in Plex + re-sync fixes it.
This commit is contained in:
npeter83 2026-07-05 03:29:20 +02:00
parent 62636319b1
commit 219a935121
14 changed files with 290 additions and 133 deletions

View file

@ -63,6 +63,7 @@ import { CURRENT_VERSION } from "./lib/releaseNotes";
// never reach non-admins. All are rendered inside a <Suspense> boundary below.
const Feed = lazy(() => import("./components/Feed"));
const PlexBrowse = lazy(() => import("./components/PlexBrowse"));
const PlexSidebar = lazy(() => import("./components/PlexSidebar"));
const ChannelPage = lazy(() => import("./components/ChannelPage"));
const Channels = lazy(() => import("./components/Channels"));
const Playlists = lazy(() => import("./components/Playlists"));
@ -242,6 +243,10 @@ export default function App() {
const [channelsViewRaw, setChannelsView] = useAccountPersistedState(LS.channelsView, "subscribed");
const channelsView: ChannelsView =
channelsViewRaw === "discovery" ? "discovery" : "subscribed";
// Plex module filters (its own left-sidebar filter section) — per-account persisted.
const [plexLib, setPlexLib] = useAccountPersistedState(LS.plexLibrary, "");
const [plexShowFilter, setPlexShowFilter] = useAccountPersistedState(LS.plexShow, "all");
const [plexSort, setPlexSort] = useAccountPersistedState(LS.plexSort, "added");
// Bumped to tell the channel manager to drop a stale column filter when we send the user
// there to see a specific set (the header's "without full history" link).
const [channelsFilterReset, setChannelsFilterReset] = useState(0);
@ -713,6 +718,20 @@ export default function App() {
onToggleCollapse={() => setFilterCollapsed(!filterCollapsed)}
/>
)}
{page === "plex" && meQuery.data!.plex_enabled && (
<Suspense fallback={null}>
<PlexSidebar
library={plexLib}
setLibrary={setPlexLib}
show={plexShowFilter}
setShow={setPlexShowFilter}
sort={plexSort}
setSort={setPlexSort}
collapsed={filterCollapsed}
onToggleCollapse={() => setFilterCollapsed(!filterCollapsed)}
/>
</Suspense>
)}
<div className="relative flex-1 min-w-0 flex flex-col">
{channelView ? (
<Suspense fallback={pageFallback}>
@ -782,8 +801,8 @@ export default function App() {
prefs={prefsCtl}
onOpenWizard={() => setWizardOpen(true)}
/>
) : meQuery.data!.plex_enabled && filters.librarySource === "plex" ? (
<PlexBrowse filters={filters} setFilters={setFilters} />
) : page === "plex" && meQuery.data!.plex_enabled ? (
<PlexBrowse q={filters.q} library={plexLib} show={plexShowFilter} sort={plexSort} />
) : (
<Feed
filters={filters}
@ -791,7 +810,6 @@ export default function App() {
view={view}
canRead={meQuery.data!.can_read}
isDemo={meQuery.data!.is_demo}
plexEnabled={meQuery.data!.plex_enabled}
onOpenWizard={() => setWizardOpen(true)}
ytSearch={ytSearch}
onYtSearch={enterYtSearch}