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?",