diff --git a/frontend/src/components/AppBar.tsx b/frontend/src/components/AppBar.tsx index 795f336..36cf44d 100755 --- a/frontend/src/components/AppBar.tsx +++ b/frontend/src/components/AppBar.tsx @@ -2,20 +2,21 @@ import { Theme } from "@emotion/react"; import { AppBar, Avatar, + Backdrop, Box, CircularProgress, Divider, InputAdornment, Menu, MenuItem, + Popper, SxProps, TextField, Typography, } from "@mui/material"; import React, { useEffect, useState } from "react"; -import { useQuery } from "react-query"; -import { Link, useLocation, useNavigate } from "react-router-dom"; -import { getAllLibraries } from "../plex"; +import { Link, useLocation, useNavigate, useSearchParams } from "react-router-dom"; +import { getAllLibraries, getSearch, getTranscodeImageURL } from "../plex"; import MetaScreen from "./MetaScreen"; import { useUserSessionStore } from "../states/UserSession"; import { Search } from "@mui/icons-material"; @@ -47,11 +48,17 @@ function Appbar() { }; }, []); - const libraries = useQuery("libraries", getAllLibraries); + const [libraries, setLibraries] = useState(null); const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); + useEffect(() => { + getAllLibraries().then((res) => { + setLibraries(res); + }); + }, []); + return ( Home - {libraries.isLoading && } - {libraries.data + {!libraries && } + {libraries ?.filter((e) => ["movie", "show"].includes(e.type)) .map((library) => ( - - - - ), - }} - placeholder="Search" - variant="outlined" - size="small" - onChange={(e) => - navigate(`/search/${encodeURIComponent(e.target.value.trim())}`) - } - sx={{ - backgroundColor: "rgba(255, 255, 255, 0.2)", - borderRadius: "7px", + - // round the corners of the input - "& .MuiOutlinedInput-root": { - borderRadius: "7px", - }, - }} - /> (null); + const searchOpen = Boolean(searchAnchorEl); + const searchAnchorElRef = React.useRef(null); + const [searchValue, setSearchValue] = React.useState(""); + const [searchResults, setSearchResults] = React.useState( + [] + ); + const [searchLoading, setSearchLoading] = React.useState(false); + + const [, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); + + useEffect(() => { + // listen to strg + f + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "f" && e.ctrlKey) { + e.preventDefault(); + e.stopPropagation(); + if(searchAnchorElRef.current) { + searchAnchorElRef.current.blur(); + setSearchAnchorEl(null); + return; + } + + setSearchAnchorEl(document.getElementById("search-bar")); + document.getElementById("search-bar")?.focus(); + } + }; + + window.addEventListener("keydown", onKeyDown); + + return () => { + window.removeEventListener("keydown", onKeyDown); + }; + }, []); + + useEffect(() => { + searchAnchorElRef.current = searchAnchorEl; + }, [searchAnchorEl]); + + useEffect(() => { + if (searchValue.length === 0) { + setSearchResults([]); + return; + } + + setSearchLoading(true); + + const delayDebounceFn = setTimeout(() => { + getSearch(searchValue).then((res) => { + if (!res) { + setSearchLoading(false); + return setSearchResults([]); + } + setSearchResults( + res + .filter( + (item) => + (item.Metadata && + ["movie", "show"].includes(item.Metadata.type)) || + item.Directory + ) + .sort((a, b) => { + // directories first + if (a.Directory && !b.Directory) return -1; + if (!a.Directory && b.Directory) return 1; + return 0; + }) + .slice(0, 8) + ); + + setSearchLoading(false); + }); + }, 500); // Adjust the delay as needed + + return () => clearTimeout(delayDebounceFn); + }, [searchValue]); + + return ( + <> + { + setSearchAnchorEl(null); + }} + /> + + + + ), + }} + placeholder="Search" + variant="outlined" + size="small" + onKeyDown={(e) => { + if (e.key === "Enter") { + navigate(`/search/${encodeURIComponent(searchValue.trim())}`); + + searchAnchorEl?.blur(); + setSearchAnchorEl(null); + } + }} + onChange={(e) => { + setSearchValue(e.target.value); + //navigate(`/search/${encodeURIComponent(e.target.value.trim())}`); + }} + onFocus={(e) => { + setSearchAnchorEl(e.currentTarget); + }} + sx={{ + backgroundColor: "#121212AA", + borderRadius: "7px", + + // round the corners of the input + "& .MuiOutlinedInput-root": { + borderRadius: "7px", + }, + transition: "all 0.2s ease-in-out", + zIndex: 11000, + }} + style={{ + ...(searchOpen + ? { width: "20vw", zIndex: 10000 } + : { width: "300px" }), + }} + /> + 0} + placement="bottom-end" + sx={{ + borderRadius: "7px", + backgroundColor: "#121212AA", + backdropFilter: "blur(10px)", + transition: "width 0.2s ease-in-out", + padding: "20px 10px", + pt: "10px", + + display: "flex", + flexDirection: "column", + gap: "10px", + }} + style={{ + ...(searchOpen + ? { width: "20vw", zIndex: 11000 } + : { width: "300px" }), + }} + onClick={(e) => { + e.stopPropagation(); + e.preventDefault(); + }} + > + {searchLoading && ( + + + + )} + + {!searchLoading && searchResults.length === 0 && ( + No Results + )} + + {!searchLoading && + searchResults.length > 0 && + searchResults.map((item) => { + if (item.Metadata) { + return ( + { + e.stopPropagation(); + e.preventDefault(); + searchAnchorEl?.blur(); + setSearchAnchorEl(null); + if (item.Metadata?.ratingKey) { + setSearchParams({ + mid: item.Metadata.ratingKey, + }); + } + }} + > + + + + {item.Metadata.title} + + + {item.Metadata.librarySectionTitle} + + + + ); + } else if (item.Directory) { + return ( + { + console.log("test"); + e.stopPropagation(); + e.preventDefault(); + navigate(`/library/${item.Directory?.librarySectionID}/dir/genre/${item.Directory?.id}`); + searchAnchorEl?.blur(); + setSearchAnchorEl(null); + }} + > + + {item.Directory.librarySectionTitle} - {item.Directory.tag} + + + ); + } + })} + + + ); +} + function HeadLink({ to, children, diff --git a/frontend/src/components/MetaScreen.tsx b/frontend/src/components/MetaScreen.tsx index 237e720..0fff7d9 100755 --- a/frontend/src/components/MetaScreen.tsx +++ b/frontend/src/components/MetaScreen.tsx @@ -18,18 +18,14 @@ import React, { useEffect, useState } from "react"; import { getLibraryMeta, getLibraryMetaChildren, - getSimilar, getTranscodeImageURL, } from "../plex"; -import { useQuery, UseQueryResult } from "react-query"; import { - Add, Bookmark, BookmarkBorder, CheckCircle, Close, PlayArrow, - StarRate, VolumeOff, VolumeUp, } from "@mui/icons-material"; @@ -46,15 +42,8 @@ function MetaScreen() { const { MetaScreenPlayerMuted, setMetaScreenPlayerMuted } = usePreviewPlayer(); - const { data, status } = useQuery( - ["meta", searchParams.get("mid")], - async () => await getLibraryMeta(searchParams.get("mid") as string) - ); - - const similar = useQuery( - ["similar", searchParams.get("mid")], - async () => await getSimilar(searchParams.get("mid") as string) - ); + const [loading, setLoading] = useState(true); + const [data, setData] = useState(undefined); const [page, setPage] = useState(0); @@ -69,7 +58,10 @@ function MetaScreen() { const WatchList = useWatchListCache(); + const mid = searchParams.get("mid"); + useEffect(() => { + setLoading(true); setEpisodes(null); setSelectedSeason(0); setLanguages(null); @@ -77,7 +69,13 @@ function MetaScreen() { setPreviewVidURL(null); setPreviewVidPlaying(false); setPage(0); - }, [data?.ratingKey]); + + if (!mid) return; + getLibraryMeta(mid).then((res) => { + setData(res); + setLoading(false); + }); + }, [mid]); useEffect(() => { if (!data) return; @@ -178,18 +176,17 @@ function MetaScreen() { }); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedSeason, data?.ratingKey]); + }, [selectedSeason, data]); if (!searchParams.has("mid")) return <>; - if (status === "loading") + if (loading) return ( ); - console.log(data); return ( { + if (e.target.value === selectedSeason) return; setSelectedSeason(e.target.value as number); }} > @@ -824,7 +822,7 @@ function MetaScreen() { - {page === 0 && MetaPage1(data, similar, episodes, navigate)} + {page === 0 && MetaPage1(data, loading, episodes, navigate)} {page === 1 && MetaPage2(data)} {page === 2 && MetaPage3(data)} @@ -837,13 +835,13 @@ export default MetaScreen; function MetaPage1( data: Plex.Metadata | undefined, - similar: UseQueryResult, + loading: boolean, episodes: Plex.Metadata[] | null | undefined, navigate: (path: string) => void ) { return ( <> - {data?.type === "movie" && similar.status === "loading" && ( + {data?.type === "movie" && !data && ( )} - {data?.type === "movie" && similar.status === "success" && ( + {data?.type === "movie" && data.Related?.Hub?.[0] && ( - {similar.data?.slice(0, 10).map((movie) => ( + {data.Related?.Hub?.[0]?.Metadata?.slice(0, 10).map((movie) => ( @@ -927,6 +925,8 @@ function MetaPage2(data: Plex.Metadata | undefined) { alignItems: "flex-start", justifyContent: "flex-start", gap: "60px", + + userSelect: "none", }} > {data.Related?.Hub?.map((hub) => ( @@ -974,6 +974,8 @@ function MetaPage3(data: Plex.Metadata | undefined) { alignItems: "flex-start", justifyContent: "flex-start", gap: "60px", + + userSelect: "none", }} > (); - const library = useQuery(["library", libraryID], async () => - getLibrary(libraryID as string) - ); + const [library, setLibrary] = React.useState(null); - if (library.isLoading) { + useEffect(() => { + if(!libraryID) return; + getLibrary(libraryID).then((data) => { + setLibrary(data); + }); + }, [libraryID]); + + if (!library) { return ( ; + return ; case "show": - return ; + return ; default: return
Unknown
; } diff --git a/frontend/src/pages/Library.tsx b/frontend/src/pages/Library.tsx index f141d62..b202f68 100644 --- a/frontend/src/pages/Library.tsx +++ b/frontend/src/pages/Library.tsx @@ -1,8 +1,7 @@ import { Box, CircularProgress, Grid, Typography } from "@mui/material"; import React from "react"; import { useParams } from "react-router-dom"; -import { getLibraryDir } from "../plex"; -import { useQuery } from "react-query"; +import { getLibraryDir, LibraryDir } from "../plex"; import MovieItem from "../components/MovieItem"; export default function Library() { @@ -12,10 +11,24 @@ export default function Library() { subdir?: string; }; - const results = useQuery( - ["library", { query: dir, libraryKey: Number(libraryKey), subdir: subdir }], - () => getLibraryDir(Number(libraryKey), dir, subdir) - ); + const [results, setResults] = React.useState(null); + const [isLoading, setIsLoading] = React.useState(true); + const [isError, setIsError] = React.useState(false); + + React.useEffect(() => { + const fetchData = async () => { + try { + const data = await getLibraryDir(Number(libraryKey), dir, subdir); + setResults(data); + } catch (error) { + setIsError(true); + } finally { + setIsLoading(false); + } + }; + + fetchData(); + }, [libraryKey, dir, subdir]); return ( - {results.isLoading && } - {results.isError && Error} + {isLoading && } + {isError && Error} - {results.isSuccess && ( + {results && ( <> - {results.data.library} - {results.data.title} + {results.library} - {results.title} {!results && } {results && - results.data.Metadata.map((item) => ( + results.Metadata.map((item) => ( {directories.map((item) => ( - + { @@ -105,8 +111,8 @@ export default function Search() { { return res.MediaContainer.SearchResult; } -export async function getLibraryDir(library: number, directory: string, subDir?: string): Promise<{ +export interface LibraryDir { title: string; library: string; Metadata: Plex.Metadata[]; -}> { +} + +export async function getLibraryDir(library: number, directory: string, subDir?: string): Promise { const res = await authedGet(`/library/sections/${library}/${directory}/${subDir ?? ""}`); return { title: res.MediaContainer.title2,