From d84edebc090f50cba12173053d54e20a46b4d504 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Wed, 8 Jul 2026 23:19:23 +0200 Subject: [PATCH 1/2] fix(plex): explain an empty search when filters are also active MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Plex search that returns nothing while a collection/rating filter is active showed a bare "No matches", hiding that the filters — not the query — were the cause. Show how many filters are narrowing the results and offer a one-click Clear filters inline. Translated in all three languages. --- frontend/src/components/PlexBrowse.tsx | 25 +++++++++++++++++++++++-- frontend/src/i18n/locales/de/plex.json | 2 ++ frontend/src/i18n/locales/en/plex.json | 2 ++ frontend/src/i18n/locales/hu/plex.json | 2 ++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx index ab80ddb..02a32a7 100644 --- a/frontend/src/components/PlexBrowse.tsx +++ b/frontend/src/components/PlexBrowse.tsx @@ -2,7 +2,14 @@ import { lazy, Suspense, useEffect, useLayoutEffect, useRef, useState, type CSSP import { useTranslation } from "react-i18next"; import { useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, CheckCircle2, Info, ListPlus, Play } from "lucide-react"; -import { api, type PlexCard, type PlexFilters, type PlexPerson } from "../lib/api"; +import { + api, + EMPTY_PLEX_FILTERS, + plexFilterCount, + type PlexCard, + type PlexFilters, + type PlexPerson, +} from "../lib/api"; import { useDebounced } from "../lib/useDebounced"; import { useHistorySubview } from "../lib/history"; import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd"; @@ -267,7 +274,21 @@ export default function PlexBrowse({ {browseQ.isLoading ? (

{t("plex.loading")}

) : items.length === 0 ? ( -

{dq ? t("plex.noMatches") : t("plex.empty")}

+ dq && plexFilterCount(filters) > 0 ? ( + // A search that comes up empty WHILE filters are active is usually the filters, not the + // query — say so and offer a one-click escape, instead of a bare "No matches". +
+ {t("plex.noMatchesFiltered", { count: plexFilterCount(filters) })} + +
+ ) : ( +

{dq ? t("plex.noMatches") : t("plex.empty")}

+ ) ) : (
{items.map((c) => ( diff --git a/frontend/src/i18n/locales/de/plex.json b/frontend/src/i18n/locales/de/plex.json index 91489ca..c2beb8e 100644 --- a/frontend/src/i18n/locales/de/plex.json +++ b/frontend/src/i18n/locales/de/plex.json @@ -57,6 +57,8 @@ "count": "{{count}} Titel", "searchCount": "{{count}} Treffer", "noMatches": "Keine Treffer.", + "noMatchesFiltered_one": "Keine Treffer — ein Filter schränkt die Ergebnisse zusätzlich ein.", + "noMatchesFiltered_other": "Keine Treffer — {{count}} Filter schränken die Ergebnisse zusätzlich ein.", "loading": "Wird geladen…", "empty": "Noch nichts hier. Starte eine Plex-Synchronisierung auf der Admin-Konfigurationsseite.", "loadMore": "Mehr laden", diff --git a/frontend/src/i18n/locales/en/plex.json b/frontend/src/i18n/locales/en/plex.json index 8601de1..6909939 100644 --- a/frontend/src/i18n/locales/en/plex.json +++ b/frontend/src/i18n/locales/en/plex.json @@ -57,6 +57,8 @@ "count": "{{count}} titles", "searchCount": "{{count}} matches", "noMatches": "No matches.", + "noMatchesFiltered_one": "No matches — a filter is also narrowing the results.", + "noMatchesFiltered_other": "No matches — {{count}} filters are also narrowing the results.", "loading": "Loading…", "empty": "Nothing here yet. Run a Plex sync from the admin Config page.", "loadMore": "Load more", diff --git a/frontend/src/i18n/locales/hu/plex.json b/frontend/src/i18n/locales/hu/plex.json index a753fbe..53d8a79 100644 --- a/frontend/src/i18n/locales/hu/plex.json +++ b/frontend/src/i18n/locales/hu/plex.json @@ -57,6 +57,8 @@ "count": "{{count}} cím", "searchCount": "{{count}} találat", "noMatches": "Nincs találat.", + "noMatchesFiltered_one": "Nincs találat — egy szűrő is szűkíti az eredményt.", + "noMatchesFiltered_other": "Nincs találat — {{count}} szűrő is szűkíti az eredményt.", "loading": "Betöltés…", "empty": "Még nincs itt semmi. Futtass egy Plex-szinkront az admin Konfiguráció oldalról.", "loadMore": "Több betöltése", From a005a1bcbb885437b43f9547d53fce08218238ef Mon Sep 17 00:00:00 2001 From: npeter83 Date: Wed, 8 Jul 2026 23:19:36 +0200 Subject: [PATCH 2/2] fix(plex): give the library search its own ephemeral state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Plex library search box wrote to the shared, persisted feed filter (filters.q). That leaked a Plex query into the feed search box and, being persisted, restored a stale query after a reload — which then collided with a persisted collection filter to produce a confusing empty grid. Plex now has its own search state: kept across page switches within a session, but not persisted and not shared with the feed, so a reload starts clean. --- frontend/src/App.tsx | 12 ++++++++++-- frontend/src/components/Header.tsx | 24 ++++++++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) 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 && (