diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 127844e..0a573e6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -260,6 +260,12 @@ export default function App() { } }, [plexFiltersRaw]); const setPlexFilters = (f: PlexFilters) => setPlexFiltersRaw(JSON.stringify(f)); + // The Plex library search term is its OWN ephemeral state — deliberately NOT the persisted feed + // `filters.q`. Sharing that box meant a Plex search leaked into the feed and, being persisted, + // greeted you with a stale query (colliding with a persisted collection filter → confusing + // "0 matches") after a reload. Kept in App so it survives page switches within a session, but + // resets on reload. + const [plexQ, setPlexQ] = useState(""); // 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); @@ -767,6 +773,8 @@ export default function App() { me={meQuery.data!} filters={filters} setFilters={setFilters} + plexQ={plexQ} + setPlexQ={setPlexQ} page={page} onYtSearch={enterYtSearch} /> @@ -819,8 +827,8 @@ export default function App() { /> ) : page === "plex" && meQuery.data!.plex_enabled ? ( setFilters({ ...filters, q: "" })} + q={plexQ} + onClearSearch={() => setPlexQ("")} library={plexLib} show={plexShowFilter} sort={plexSort} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index a5b93a4..2038bbf 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -12,12 +12,17 @@ export default function Header({ me, filters, setFilters, + plexQ, + setPlexQ, page, onYtSearch, }: { me: Me; filters: FeedFilters; setFilters: (f: FeedFilters) => void; + // Plex has its own ephemeral search term (see App) — the box is shared UI, the state is not. + plexQ: string; + setPlexQ: (q: string) => void; page: Page; // Trigger a live YouTube search for the current term (hidden for the demo account, which // can't spend the shared quota). @@ -25,11 +30,14 @@ export default function Header({ }) { const { t } = useTranslation(); const canSearchYt = !me.is_demo; - const trimmedQ = filters.q.trim(); // The search box serves both the YouTube feed and the Plex module (integrated search); the live - // YouTube-search escalation (Enter / button) is feed-only. + // YouTube-search escalation (Enter / button) is feed-only. On Plex it drives `plexQ`, on the feed + // the persisted `filters.q`. const isSearchPage = page === "feed" || page === "plex"; + const isPlex = page === "plex"; const isYtCapable = page === "feed" && canSearchYt; + const searchValue = isPlex ? plexQ : filters.q; + const trimmedQ = searchValue.trim(); return (
@@ -38,14 +46,18 @@ export default function Header({
{ const q = e.target.value; + if (isPlex) { + setPlexQ(q); + return; + } // When a search first appears, rank the feed by relevance — set atomically with // the query (race-free vs. per-keystroke updates). Only overrides the default // "newest" sort; a custom sort the user chose is left alone. Cleared in Feed. const startSearch = - page === "feed" && !filters.q.trim() && !!q.trim() && filters.sort === "newest"; + !filters.q.trim() && !!q.trim() && filters.sort === "newest"; setFilters({ ...filters, q, ...(startSearch ? { sort: "relevance" } : {}) }); }} onKeyDown={(e) => { @@ -56,9 +68,9 @@ export default function Header({ placeholder={page === "plex" ? t("plex.searchPlaceholder") : t("header.searchPlaceholder")} className="w-full bg-card border border-border rounded-full pl-9 pr-9 py-2 text-sm outline-none focus:border-accent" /> - {filters.q && ( + {searchValue && (