refactor(plex): extract shared PlexLibraryPicker (F6)

AddItemPanel (Collection page) and NewCollectionPanel (browse) had near-
duplicate debounced library-search + result-list rendering. Extract it into
one PlexLibraryPicker that owns the query, the loading/error/empty states and
the row markup; callers keep their own chrome and pass term + renderAction.
No behaviour change.
This commit is contained in:
2026-08-23 08:00:54 +02:00
parent 6fb369ffcc
commit 1e894d378d
3 changed files with 95 additions and 84 deletions
+19 -45
View File
@@ -6,10 +6,10 @@ import { qk } from "../lib/queryKeys";
import { api, isIntrinsicReadOnly, type PlexCard, type PlexCollectionDetail } from "../lib/api";
import { formatRuntime } from "../lib/format";
import { notifyActionError } from "../lib/actionErrors";
import { useDebounced } from "../lib/useDebounced";
import { useConfirm } from "./ConfirmProvider";
import { BackBtn, ActionBtn, DetailHero } from "../lib/plexDetailUi";
import { PlexPosterCard } from "./plexCards";
import PlexLibraryPicker from "./PlexLibraryPicker";
import { toggleWatchState } from "../lib/plexWatch";
import { LoadingState, StateMessage } from "./QueryState";
@@ -342,14 +342,7 @@ function AddItemPanel({
}) {
const { t } = useTranslation();
const [term, setTerm] = useState("");
const debounced = useDebounced(term.trim(), 300);
const [added, setAdded] = useState<Set<string>>(new Set());
const q = useQuery({
queryKey: qk.plexLibrary("collection-add", scope, debounced),
queryFn: () => api.plexLibrary({ scope, q: debounced, limit: 12 }),
enabled: debounced.length > 0,
});
const results = q.data?.items ?? [];
async function add(card: PlexCard) {
try {
@@ -381,43 +374,24 @@ function AddItemPanel({
<X className="h-4 w-4" />
</button>
</div>
{debounced.length === 0 ? (
<p className="text-muted text-sm">{t("plex.collection.addHint")}</p>
) : q.isLoading ? (
<LoadingState label={t("plex.loading")} />
) : q.isError ? (
<p className="text-muted text-sm">{t("common.loadError")}</p>
) : results.length === 0 ? (
<p className="text-muted text-sm">{t("plex.collection.addNoResults")}</p>
) : (
<ul className="flex flex-col gap-1">
{results.map((c) => {
const inColl = existing.has(c.id) || added.has(c.id);
return (
<li key={c.id} className="flex items-center gap-3">
<img
src={c.thumb}
alt=""
className="h-12 w-8 shrink-0 rounded object-cover"
loading="lazy"
/>
<span className="min-w-0 flex-1 truncate text-sm">
{c.title}
{c.year != null && <span className="text-muted"> · {c.year}</span>}
</span>
<button
onClick={() => add(c)}
disabled={inColl}
className="glass-card glass-hover inline-flex items-center gap-1 rounded-lg px-2.5 py-1 text-xs disabled:opacity-50"
>
{inColl ? <Check className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />}
{inColl ? t("plex.collection.added") : t("plex.collection.add")}
</button>
</li>
);
})}
</ul>
)}
<PlexLibraryPicker
scope={scope}
term={term}
emptyHint={<p className="text-muted text-sm">{t("plex.collection.addHint")}</p>}
renderAction={(c) => {
const inColl = existing.has(c.id) || added.has(c.id);
return (
<button
onClick={() => add(c)}
disabled={inColl}
className="glass-card glass-hover inline-flex items-center gap-1 rounded-lg px-2.5 py-1 text-xs disabled:opacity-50"
>
{inColl ? <Check className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />}
{inColl ? t("plex.collection.added") : t("plex.collection.add")}
</button>
);
}}
/>
</div>
);
}