diff --git a/frontend/src/components/PlexSidebar.tsx b/frontend/src/components/PlexSidebar.tsx
index c16933ba..a4f7e2e7 100644
--- a/frontend/src/components/PlexSidebar.tsx
+++ b/frontend/src/components/PlexSidebar.tsx
@@ -65,21 +65,10 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
// the header search box); this rail is the primary editor of them. Opening a playlist is a
// hand-off to PlexBrowse via the shared `playlistOpen` slot.
const { scope, show, filters } = usePlex();
- const { setFilters, setPlaylistOpen: onOpenPlaylist } = usePlexActions();
- const playlistsQ = useQuery({ queryKey: qk.plexPlaylists(), queryFn: () => api.plexPlaylists() });
- const [newPlaylist, setNewPlaylist] = useState("");
+ const { setFilters } = usePlexActions();
// This rail's media source (the Plex library) — names the abstraction the feed and Plex rails
// share; drives the panel layout key. `FilterRail` owns the customize `editing` state now.
const source: MediaSourceKind = "plex";
- const qc = useQueryClient();
- async function createPlaylist() {
- const title = newPlaylist.trim();
- if (!title) return;
- const pl = await api.plexCreatePlaylist(title);
- setNewPlaylist("");
- qc.invalidateQueries({ queryKey: qk.plexPlaylists() });
- onOpenPlaylist(pl.id);
- }
const { collectionTitle: _ct, ...facetKey } = filters;
const lang = plexLang(); // HU/EN genre vocabulary — refetch facets on a language switch
const facetsQ = useQuery({
@@ -111,44 +100,7 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
{
id: "playlists",
title: t("plex.playlist.section"),
- body: (
- <>
-
-
setNewPlaylist(e.target.value)}
- onKeyDown={(e) => e.key === "Enter" && createPlaylist()}
- placeholder={t("plex.playlist.newPlaceholder")}
- className="glass-card min-w-0 flex-1 rounded-lg px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
- />
-
-
- {(playlistsQ.data?.playlists ?? []).length === 0 ? (
- {t("plex.playlist.none")}
- ) : (
-
- {playlistsQ.data!.playlists.map((p) => (
-
- ))}
-
- )}
- >
- ),
+ body: ,
},
...(peopleActive
? [
@@ -352,6 +304,75 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
);
}
+// The playlists section: a combobox that stays compact (just a field + chevron) until opened, instead
+// of an always-expanded list that grew the rail with every playlist. Picking a playlist opens it (the
+// PlexBrowse `playlistOpen` hand-off); typing a name that doesn't exist yet offers a "Create …" row so
+// you can still add one — the same reuse-the-Combobox idea as the feed's "Your tags" widget. `selected`
+// stays empty: this picks an action (open / create), it doesn't hold a persistent selection.
+const CREATE_PLAYLIST = "__create__";
+function PlaylistPicker() {
+ const { t } = useTranslation();
+ const { setPlaylistOpen: onOpenPlaylist } = usePlexActions();
+ const qc = useQueryClient();
+ const [query, setQuery] = useState("");
+ const playlistsQ = useQuery({ queryKey: qk.plexPlaylists(), queryFn: () => api.plexPlaylists() });
+ const playlists = playlistsQ.data?.playlists ?? [];
+
+ const q = query.trim();
+ const ql = q.toLowerCase();
+ const matches = ql ? playlists.filter((p) => p.title.toLowerCase().includes(ql)) : playlists;
+ const exact = playlists.some((p) => p.title.toLowerCase() === ql);
+ const options: ComboboxOption[] = [
+ ...matches.map((p) => ({ value: `pl:${p.id}`, label: p.title, count: p.item_count })),
+ // Offer to create only when the typed name isn't already a playlist.
+ ...(q && !exact ? [{ value: CREATE_PLAYLIST, label: q }] : []),
+ ];
+
+ async function createPlaylist(title: string) {
+ const pl = await api.plexCreatePlaylist(title);
+ qc.invalidateQueries({ queryKey: qk.plexPlaylists() });
+ onOpenPlaylist(pl.id);
+ }
+
+ return (
+
+ o.value === CREATE_PLAYLIST ? (
+ <>
+
+
+ {t("plex.playlist.createNamed", { name: o.label })}
+
+ >
+ ) : (
+ <>
+
+ {o.label}
+ {o.count != null && (
+ {o.count}
+ )}
+ >
+ )
+ }
+ onChange={(next) => {
+ const v = next[0];
+ if (!v) return;
+ if (v === CREATE_PLAYLIST) createPlaylist(q);
+ else onOpenPlaylist(Number(v.slice(3)));
+ }}
+ />
+ );
+}
+
// The collection filter (E5 S7 → combobox rework): a single-select combi-input matching the
// genre/studio pickers, its vocabulary searched SERVER-side (the union can run to hundreds). The
// generic owns the field/keyboard/ARIA; this wrapper only owns the query→fetch (gated on
diff --git a/frontend/src/i18n/locales/en/plex.json b/frontend/src/i18n/locales/en/plex.json
index 1949c832..4feda835 100644
--- a/frontend/src/i18n/locales/en/plex.json
+++ b/frontend/src/i18n/locales/en/plex.json
@@ -243,6 +243,8 @@
"playlist": {
"section": "Playlists",
"newPlaceholder": "New playlist…",
+ "pickerPlaceholder": "Find or create a playlist…",
+ "createNamed": "Create \"{{name}}\"",
"create": "Create playlist",
"none": "No playlists yet.",
"deleteConfirm": "Delete the playlist \"{{title}}\"?",
diff --git a/frontend/src/i18n/locales/hu/plex.json b/frontend/src/i18n/locales/hu/plex.json
index 40f19229..e4d41545 100644
--- a/frontend/src/i18n/locales/hu/plex.json
+++ b/frontend/src/i18n/locales/hu/plex.json
@@ -243,6 +243,8 @@
"playlist": {
"section": "Lejátszási listák",
"newPlaceholder": "Új lista…",
+ "pickerPlaceholder": "Lista keresése vagy létrehozása…",
+ "createNamed": "„{{name}}” létrehozása",
"create": "Lista létrehozása",
"none": "Még nincs lista.",
"deleteConfirm": "Törlöd a(z) „{{title}}” listát?",