diff --git a/.github/workflows/docker-latest.yml b/.github/workflows/docker-latest.yml index b79ef2f..ed59d8b 100644 --- a/.github/workflows/docker-latest.yml +++ b/.github/workflows/docker-latest.yml @@ -31,6 +31,20 @@ jobs: run: npm run build working-directory: frontend + - name: Extract version from tag + id: extract_version + run: | + if [ "${{ github.event_name }}" = "release" ]; then + VERSION=${{ github.event.release.tag_name }} + # Remove 'v' prefix if present + VERSION=${VERSION#v} + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "is_release=true" >> $GITHUB_OUTPUT + else + echo "version=latest" >> $GITHUB_OUTPUT + echo "is_release=false" >> $GITHUB_OUTPUT + fi + - name: Log in to Docker Hub uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a with: @@ -41,10 +55,40 @@ jobs: run: docker buildx create --name nevuBuilder --use - name: Build and push the amd64 image - run: docker buildx build --platform linux/amd64 -t ipmake/nevu:latest-amd64 . --push + run: | + if [ "${{ steps.extract_version.outputs.is_release }}" = "true" ]; then + docker buildx build --platform linux/amd64 \ + -t ipmake/nevu:latest-amd64 \ + -t ipmake/nevu:${{ steps.extract_version.outputs.version }}-amd64 \ + . --push + else + docker buildx build --platform linux/amd64 -t ipmake/nevu:latest-amd64 . --push + fi - name: Build and push the arm64 image - run: docker buildx build --platform linux/arm64 -t ipmake/nevu:latest-arm64 . --push + run: | + if [ "${{ steps.extract_version.outputs.is_release }}" = "true" ]; then + docker buildx build --platform linux/arm64 \ + -t ipmake/nevu:latest-arm64 \ + -t ipmake/nevu:${{ steps.extract_version.outputs.version }}-arm64 \ + . --push + else + docker buildx build --platform linux/arm64 -t ipmake/nevu:latest-arm64 . --push + fi - name: Build and push multi-platform Docker image - run: docker buildx imagetools create --tag ipmake/nevu:latest ipmake/nevu:latest-amd64 ipmake/nevu:latest-arm64 \ No newline at end of file + run: | + if [ "${{ steps.extract_version.outputs.is_release }}" = "true" ]; then + # Create versioned multi-platform image + docker buildx imagetools create \ + --tag ipmake/nevu:${{ steps.extract_version.outputs.version }} \ + ipmake/nevu:${{ steps.extract_version.outputs.version }}-amd64 \ + ipmake/nevu:${{ steps.extract_version.outputs.version }}-arm64 + # Create latest multi-platform image + docker buildx imagetools create \ + --tag ipmake/nevu:latest \ + ipmake/nevu:latest-amd64 \ + ipmake/nevu:latest-arm64 + else + docker buildx imagetools create --tag ipmake/nevu:latest ipmake/nevu:latest-amd64 ipmake/nevu:latest-arm64 + fi \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index d5b16a7..dde7764 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -51,7 +51,7 @@ discovery.announce("Nevu", { const proxy = httpProxy.createProxyServer({ ws: true, autoRewrite: false, - cookieDomainRewrite: (new URL(process.env.PLEX_SERVER as string)).hostname, + cookieDomainRewrite: (new URL(process.env.PLEX_SERVER || "http://localhost:32400")).hostname, changeOrigin: true, secure: false, followRedirects: true, @@ -109,16 +109,23 @@ const noVerifyHttpsAgent = new https.Agent({ } // check whether the PLEX_SERVER is reachable - try { - await axios.get(`${process.env.PLEX_SERVER}/identity`, { - timeout: 5000, - }); - } catch (error) { - status.error = true; - status.message = 'Proxy cannot reach PLEX_SERVER'; - console.error('Proxy cannot reach PLEX_SERVER'); - return; - } + await new Promise(async (resolve) => { + while (true) { + const r = await axios.get(`${process.env.PLEX_SERVER ?? "http://localhost:32400"}/identity`, { + timeout: 5000, + }).catch(() => null); + + if (r && r.status === 200) { + status.error = false; + return resolve(); + } else { + status.error = true; + status.message = 'Proxy cannot reach PLEX_SERVER'; + console.error('Proxy cannot reach PLEX_SERVER'); + await new Promise(r => setTimeout(r, 3000)); + } + } + }) } diff --git a/frontend/src/components/AppBar.tsx b/frontend/src/components/AppBar.tsx index 5ad6da5..4f4fb46 100755 --- a/frontend/src/components/AppBar.tsx +++ b/frontend/src/components/AppBar.tsx @@ -17,6 +17,7 @@ import { SxProps, TextField, Typography, + Grid } from "@mui/material"; import React, { JSX, useEffect, useState } from "react"; import { @@ -80,17 +81,17 @@ function Appbar() { const navigate = useNavigate(); useEffect(() => { - getAllLibraries().then((res) => { - const filtered = res.filter((library) => { - const key = `LIBRARY_${library.uuid}`; - const rawValue = settings[key]; + getAllLibraries().then((res) => { + const filtered = res.filter((library) => { + const key = `LIBRARY_${library.uuid}`; + const rawValue = settings[key]; - return rawValue === undefined || rawValue === "true"; - }); + return rawValue === undefined || rawValue === "true"; + }); - setLibraries(filtered); - }); -}, [settings]); + setLibraries(filtered.filter((lib) => ["movie", "show"].includes(lib.type))); + }); + }, [settings]); return ( (scrollAtTop ? "#00000000" : "#121216EE"), + bgcolor: (theme) => (scrollAtTop ? "#00000000" : theme.palette.background.default + "88"), backdropFilter: scrollAtTop ? "blur(0px)" : "blur(20px)", boxShadow: scrollAtTop ? "none" : "0px 0px 10px 0px #000000AA", - borderBottomLeftRadius: "10px", - borderBottomRightRadius: "10px", + borderRadius: "0px", + border: "none", + + borderBottomLeftRadius: "4px", + borderBottomRightRadius: "4px", zIndex: 99, }} > @@ -262,18 +266,19 @@ S - Skip onscreen markers (intro, credits, etc) Home {!libraries && } - {libraries - ?.filter((e) => ["movie", "show"].includes(e.type)) - .map((library) => ( - - {library.title} - - ))} + {libraries?.slice(0, 4).map((library) => ( + + {library.title} + + ))} + {libraries && libraries.length > 4 && ( + + )} @@ -294,13 +299,6 @@ S - Skip onscreen markers (intro, credits, etc) }) ); }} - sx={{ - borderRadius: "10px", - padding: 1, - "&:hover": { - backgroundColor: "#000000AA", - }, - }} > @@ -310,13 +308,6 @@ S - Skip onscreen markers (intro, credits, etc) onClick={() => { useSyncInterfaceState.getState().setOpen(true); }} - sx={{ - borderRadius: "10px", - padding: 1, - "&:hover": { - backgroundColor: "#000000AA", - }, - }} > @@ -330,7 +321,7 @@ S - Skip onscreen markers (intro, credits, etc) sx={{ width: 45, height: 45, - borderRadius: "10px", + borderRadius: "4px", cursor: "pointer", "&:hover": { @@ -522,12 +513,6 @@ function SearchBar() { }} sx={{ backgroundColor: "#121212AA", - borderRadius: "7px", - - // round the corners of the input - "& .MuiOutlinedInput-root": { - borderRadius: "7px", - }, transition: "all 0.2s ease-in-out", zIndex: 11000, }} @@ -542,7 +527,7 @@ function SearchBar() { open={searchOpen && searchValue.length > 0} placement="bottom-end" sx={{ - borderRadius: "7px", + borderRadius: "4px", backgroundColor: "#121212AA", backdropFilter: "blur(10px)", transition: "width 0.2s ease-in-out", @@ -586,7 +571,7 @@ function SearchBar() { alignItems: "flex-start", justifyContent: "flex-start", width: "100%", - borderRadius: "5px", + borderRadius: "4px", backgroundColor: (theme) => theme.palette.background.paper, padding: "7px 10px", @@ -626,7 +611,7 @@ function SearchBar() { style={{ aspectRatio: 1, objectFit: "cover", - borderRadius: "5px", + borderRadius: "4px", width: 50, height: 50, }} @@ -654,7 +639,7 @@ function SearchBar() { alignItems: "flex-start", justifyContent: "flex-start", width: "100%", - borderRadius: "5px", + borderRadius: "4px", backgroundColor: (theme) => theme.palette.background.paper, padding: "7px 10px", @@ -698,6 +683,148 @@ function SearchBar() { ); } +function LibrariesDropdown({ libraries }: { libraries: Plex.LibarySection[] }) { + const [librariesAnchorEl, setLibrariesAnchorEl] = React.useState(null); + const librariesOpen = Boolean(librariesAnchorEl); + const navigate = useNavigate(); + const [, setSearchParams] = useSearchParams(); + + const remainingLibraries = libraries.slice(4); + + return ( + <> + setLibrariesAnchorEl(e.currentTarget)} + onMouseLeave={() => setLibrariesAnchorEl(null)} + > + theme.palette.primary.main, + }, + }} + > + +{remainingLibraries.length} more + + + + setLibrariesAnchorEl(librariesAnchorEl)} + onMouseLeave={() => setLibrariesAnchorEl(null)} + > + theme.palette.primary.main + "60", + borderRadius: "2px", + }, + "&::-webkit-scrollbar-thumb:hover": { + background: (theme) => theme.palette.primary.main + "80", + }, + }} + > + + {remainingLibraries.map((library) => ( + + { + navigate(`/browse/${library.key}`); + setLibrariesAnchorEl(null); + }} + onContextMenu={(e) => { + e.preventDefault(); + setSearchParams( + new URLSearchParams({ + bkey: `/library/sections/${library.key}/all`, + }) + ); + setLibrariesAnchorEl(null); + }} + > + + {library.title} + + + + ))} + + + + + + + ); +} + function HeadLink({ to, library, diff --git a/frontend/src/components/ConfirmModal.tsx b/frontend/src/components/ConfirmModal.tsx index 1fdaea7..ff188c5 100644 --- a/frontend/src/components/ConfirmModal.tsx +++ b/frontend/src/components/ConfirmModal.tsx @@ -122,7 +122,6 @@ function ConfirmModal() { sx={{ textTransform: "none", fontWeight: 500, - borderRadius: 1.5, }} > Cancel @@ -136,7 +135,6 @@ function ConfirmModal() { sx={{ textTransform: "none", fontWeight: 500, - borderRadius: 1.5, }} > Confirm diff --git a/frontend/src/components/HeroDisplay.tsx b/frontend/src/components/HeroDisplay.tsx index 6807160..70f2f2f 100644 --- a/frontend/src/components/HeroDisplay.tsx +++ b/frontend/src/components/HeroDisplay.tsx @@ -263,15 +263,10 @@ function HeroDisplay({ item }: { item: Plex.Metadata }) { + )} + + + + + {metadata.type === "movie" + ? "Skip Credits" + : playQueue && playQueue[1] + ? `Next Episode` + : "Return to Show"} + {countdown !== null && countdown > 0 && ( + + ({Math.ceil(countdown)}s) + + )} + + + + ); } diff --git a/frontend/src/components/WatchShowChildView.tsx b/frontend/src/components/WatchShowChildView.tsx index ef921f4..d4a7862 100644 --- a/frontend/src/components/WatchShowChildView.tsx +++ b/frontend/src/components/WatchShowChildView.tsx @@ -4,14 +4,15 @@ import { SubscriptionsRounded, } from "@mui/icons-material"; import { + Backdrop, Box, - ClickAwayListener, Divider, Fade, IconButton, LinearProgress, Paper, Popper, + Portal, Typography, alpha, useTheme, @@ -22,7 +23,16 @@ import { getLibraryDir, getTranscodeImageURL } from "../plex"; import { getMinutes } from "./MetaScreen"; import { useNavigate } from "react-router-dom"; -function WatchShowChildView({ item }: { item: Plex.Metadata }) { +function WatchShowChildView({ + item, + controlElementsVisibleState, +}: { + item: Plex.Metadata; + controlElementsVisibleState: [ + boolean, + React.Dispatch> + ]; +}) { const [anchorEl, setAnchorEl] = useState(null); const [seasons, setSeasons] = useState(null); const [episodes, setEpisodes] = useState(null); @@ -30,6 +40,8 @@ function WatchShowChildView({ item }: { item: Plex.Metadata }) { const navigate = useNavigate(); const theme = useTheme(); + const [, setControlElementsVisible] = controlElementsVisibleState; + useEffect(() => { setSeasons(null); setEpisodes(null); @@ -61,318 +73,342 @@ function WatchShowChildView({ item }: { item: Plex.Metadata }) { })(); }, [item]); + useEffect(() => { + setControlElementsVisible(Boolean(anchorEl)); + }, [anchorEl, setControlElementsVisible]); + return ( <> - - {({ TransitionProps }) => ( - - + { + e.stopPropagation(); + setAnchorEl(null); + }} + > + - { - e.stopPropagation(); - setAnchorEl(null); - }} - > - e.stopPropagation()} + > + {({ TransitionProps }) => ( + + - {/* Season selector */} - {(seasons?.length ?? 1) > 1 && ( - - - - {item.grandparentTitle} - - - - - - {seasons?.map((season) => ( - setSelectedSeason(season.index ?? 1)} - sx={{ - py: 1.5, - px: 2, - display: "flex", - alignItems: "center", - justifyContent: "space-between", - cursor: "pointer", - transition: "all 0.2s", - bgcolor: - selectedSeason === season.index - ? alpha(theme.palette.primary.main, 0.15) - : "transparent", - "&:hover": { - bgcolor: - selectedSeason === season.index - ? alpha(theme.palette.primary.main, 0.2) - : alpha(theme.palette.action.hover, 0.1), - }, - }} - > - - {season.title} - - - {selectedSeason !== season.index && ( - - )} - - ))} - - )} - - {/* Episodes list */} e.stopPropagation()} > - - {episodes - ?.filter( - (episode) => episode.parentIndex === selectedSeason - ) - .map((episode) => ( + {/* Season selector */} + {(seasons?.length ?? 1) > 1 && ( + + + + {item.grandparentTitle} + + + + + + {seasons?.map((season) => ( { - if (episode.ratingKey !== item.ratingKey) { - navigate(`/watch/${episode.ratingKey}`); - setAnchorEl(null); - } - }} + key={season.ratingKey} + onClick={() => setSelectedSeason(season.index ?? 1)} sx={{ + py: 1.5, + px: 2, display: "flex", - borderRadius: 1, - overflow: "hidden", + alignItems: "center", + justifyContent: "space-between", + cursor: "pointer", transition: "all 0.2s", - ...(episode.ratingKey !== item.ratingKey && { - cursor: "pointer", - "&:hover": { - bgcolor: alpha( - theme.palette.action.hover, - 0.1 - ), - "& .playIcon": { - opacity: 1, - }, - }, - }), + bgcolor: + selectedSeason === season.index + ? alpha(theme.palette.primary.main, 0.15) + : "transparent", + "&:hover": { + bgcolor: + selectedSeason === season.index + ? alpha(theme.palette.primary.main, 0.2) + : alpha(theme.palette.action.hover, 0.1), + }, }} > - {/* Episode thumbnail */} + + {season.title} + + + {selectedSeason !== season.index && ( + + )} + + ))} + + )} + + {/* Episodes list */} + + + {episodes + ?.filter( + (episode) => episode.parentIndex === selectedSeason + ) + .map((episode) => ( { + if (episode.ratingKey !== item.ratingKey) { + navigate(`/watch/${episode.ratingKey}`); + setAnchorEl(null); + } + }} sx={{ - position: "relative", - width: 160, - aspectRatio: "16/9", - flexShrink: 0, + display: "flex", borderRadius: 1, overflow: "hidden", + transition: "all 0.2s", + ...(episode.ratingKey !== item.ratingKey && { + cursor: "pointer", + "&:hover": { + bgcolor: alpha( + theme.palette.action.hover, + 0.1 + ), + "& .playIcon": { + opacity: 1, + }, + }, + }), }} > + {/* Episode thumbnail */} - - - - {(episode.viewOffset || - (episode.viewCount && - episode.viewCount >= 1)) && ( - + - )} - - {/* Episode details */} - - + + + {(episode.viewOffset || + (episode.viewCount && + episode.viewCount >= 1)) && ( + + )} + + + {/* Episode details */} + - - {episode.index} - + + {episode.index} + + + • + + + {episode.title} + + + - • - - - {episode.title} + {getMinutes(episode.duration)} min - {getMinutes(episode.duration)} min + {episode.summary} - - - {episode.summary} - - - ))} + ))} + - - - - - )} - + + + )} + + + setAnchorEl(event.currentTarget)}> - + ); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 2b8c946..ce7d93b 100755 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -39,10 +39,12 @@ const root = ReactDOM.createRoot( ); getPlatform().then(async (platformData) => { - if(!platformData) return; + if (!platformData) return; // make platformData.platform lowercase but capitalize the first letter - platformData.platform = platformData.platform.charAt(0).toUpperCase() + platformData.platform.slice(1).toLowerCase(); + platformData.platform = + platformData.platform.charAt(0).toUpperCase() + + platformData.platform.slice(1).toLowerCase(); switch (platformData.platform) { case "Win32": @@ -58,8 +60,7 @@ getPlatform().then(async (platformData) => { if (platformCache.platform) { console.log("Platform detected:", platformCache.platform); platformCache.isDesktop = true; - } - else console.warn("Platform detection failed."); + } else console.warn("Platform detection failed."); }); root.render( @@ -116,15 +117,233 @@ root.render( styleOverrides: { root: { fontFamily: '"Inter Variable", sans-serif', - borderRadius: "7px", + borderRadius: "4px", + textTransform: "none", + fontWeight: 600, + letterSpacing: "0.025em", + transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", + }, + contained: { + backgroundColor: "rgba(18, 25, 39, 0.8)", + backdropFilter: "blur(20px)", + border: "1px solid rgba(255,255,255,0.2)", + color: "#fff", + "&:hover": { + backgroundColor: "rgba(18, 25, 39, 0.95)", + border: "1px solid rgba(99, 102, 241, 0.5)", + }, + }, + outlined: { + backgroundColor: "rgba(18, 25, 39, 0.6)", + backdropFilter: "blur(20px)", + border: "1px solid rgba(255,255,255,0.2)", + color: "#fff", + "&:hover": { + backgroundColor: "rgba(18, 25, 39, 0.85)", + border: "1px solid rgba(255,255,255,0.3)", + }, + }, + text: { + color: "rgba(255,255,255,0.9)", + "&:hover": { + backgroundColor: "rgba(18, 25, 39, 0.5)", + backdropFilter: "blur(10px)", + transform: "none", + boxShadow: "none", + }, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + width: 40, + height: 40, + borderRadius: "4px", + transition: "all 0.2s ease", + "&:hover": { + backgroundColor: "rgba(255,255,255,0.1)", + transform: "scale(1.05)", + }, + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundColor: "rgba(18, 25, 39, 0.9)", + backdropFilter: "blur(20px)", + border: "1px solid rgba(255,255,255,0.1)", + borderRadius: "4px", + }, + elevation1: { + boxShadow: "0 4px 12px rgba(0,0,0,0.2)", + }, + elevation2: { + boxShadow: "0 8px 24px rgba(0,0,0,0.3)", + }, + elevation6: { + boxShadow: "0 12px 32px rgba(0,0,0,0.4)", + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + backgroundColor: "rgba(18, 25, 39, 0.8)", + backdropFilter: "blur(20px)", + border: "1px solid rgba(255,255,255,0.1)", + borderRadius: "6px", + transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", + "&:hover": { + transform: "translateY(-4px)", + boxShadow: "0 12px 32px rgba(0,0,0,0.4)", + border: "1px solid rgba(255,255,255,0.2)", + }, + }, + }, + }, + MuiDialog: { + styleOverrides: { + paper: { + backgroundColor: "rgba(0,0,0,0.95)", + backdropFilter: "blur(24px)", + border: "1px solid rgba(255,255,255,0.1)", + borderRadius: "6px", + }, + }, + }, + MuiPopover: { + styleOverrides: { + paper: { + backgroundColor: "rgba(0,0,0,0.9)", + backdropFilter: "blur(20px)", + border: "1px solid rgba(255,255,255,0.2)", + borderRadius: "4px", + }, + }, + }, + MuiMenu: { + styleOverrides: { + paper: { + backgroundColor: "rgba(0,0,0,0.9)", + backdropFilter: "blur(20px)", + border: "1px solid rgba(255,255,255,0.2)", + borderRadius: "4px", + }, + }, + }, + MuiMenuItem: { + styleOverrides: { + root: { + borderRadius: "3px", + margin: "2px 4px", + transition: "all 0.2s ease", + "&:hover": { + transform: "translateX(2px)", + }, + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: { + "& .MuiOutlinedInput-root": { + backgroundColor: "rgba(255,255,255,0.05)", + backdropFilter: "blur(20px)", + borderRadius: "4px", + transition: "all 0.2s ease", + "&:hover": { + backgroundColor: "rgba(255,255,255,0.08)", + }, + "&.Mui-focused": { + backgroundColor: "rgba(255,255,255,0.1)", + "& .MuiOutlinedInput-notchedOutline": { + borderColor: "rgba(99, 102, 241, 0.8)", + borderWidth: "2px", + }, + }, + "& .MuiOutlinedInput-notchedOutline": { + borderColor: "rgba(255,255,255,0.2)", + transition: "all 0.2s ease", + }, + }, + }, + }, + }, + MuiSlider: { + styleOverrides: { + root: { + "& .MuiSlider-thumb": { + width: 16, + height: 16, + backgroundColor: "#6366F1", + border: "2px solid rgba(255,255,255,0.3)", + transition: "all 0.2s ease", + }, + "& .MuiSlider-track": { + backgroundColor: "#6366F1", + border: "none", + borderRadius: "4px", + }, + "& .MuiSlider-rail": { + backgroundColor: "rgba(255,255,255,0.2)", + borderRadius: "4px", + }, + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { + backgroundColor: "rgba(255,255,255,0.1)", + backdropFilter: "blur(20px)", + border: "1px solid rgba(255,255,255,0.2)", + borderRadius: "4px", + transition: "all 0.2s ease", + "&:hover": { + backgroundColor: "rgba(255,255,255,0.15)", + transform: "scale(1.05)", + }, + }, + }, + }, + MuiTab: { + styleOverrides: { + root: { + textTransform: "none", + fontWeight: 600, + borderRadius: "4px", + margin: "0 4px", + transition: "all 0.2s ease", + "&:hover": { + backgroundColor: "rgba(255,255,255,0.08)", + }, + "&.Mui-selected": { + backgroundColor: "rgba(99, 102, 241, 0.15)", + color: "#818CF8", + }, }, }, }, - MuiBackdrop: { + MuiSwitch: { styleOverrides: { root: { - height: "100vh", - zIndex: 200, + "& .MuiSwitch-switchBase": { + "&.Mui-checked": { + "& + .MuiSwitch-track": { + backgroundColor: "#6366F1", + }, + }, + }, + "& .MuiSwitch-track": { + backgroundColor: "rgba(255,255,255,0.2)", + borderRadius: "6px", + }, + "& .MuiSwitch-thumb": { + backgroundColor: "#fff", + boxShadow: "0 2px 4px rgba(0,0,0,0.2)", + }, }, }, }, diff --git a/frontend/src/pages/Browse.tsx b/frontend/src/pages/Browse.tsx index 68b9bfe..ab1f5a9 100755 --- a/frontend/src/pages/Browse.tsx +++ b/frontend/src/pages/Browse.tsx @@ -61,13 +61,6 @@ function Library() { textTransform: "uppercase", gap: "10px", transition: "all 0.2s ease-in-out", - - backgroundColor: (theme) => page === "recommendations" ? theme.palette.primary.dark : "transparent", - color: theme => theme.palette.text.primary, - "&:hover": { - backgroundColor: (theme) => page === "recommendations" ? theme.palette.primary.dark : "transparent", - transition: "all 0.4s ease", - }, }} onClick={() => setPage("recommendations")} > @@ -81,13 +74,6 @@ function Library() { textTransform: "uppercase", gap: "10px", transition: "all 0.2s ease-in-out", - - backgroundColor: (theme) => page === "browse" ? theme.palette.primary.dark : "transparent", - color: theme => theme.palette.text.primary, - "&:hover": { - backgroundColor: (theme) => page === "browse" ? theme.palette.primary.dark : "transparent", - transition: "all 0.4s ease", - }, }} onClick={() => setPage("browse")} > diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 3ea3cec..5b39a37 100755 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -28,7 +28,7 @@ export default function Home() { const [loading, setLoading] = React.useState(true); useEffect(() => { - if(!settingsLoaded) return; + if (!settingsLoaded) return; async function fetchData() { setLoading(true); @@ -42,9 +42,9 @@ export default function Home() { }); setLibraries(enabledLibraries); - const filteredLibraries = enabledLibraries.filter((lib) => - ["movie", "show"].includes(lib.type) - ); + const filteredLibraries = enabledLibraries + .filter((lib) => ["movie", "show"].includes(lib.type)) + .slice(0, 4); // limit to first 4 libraries const featuredData = await getRecommendations(filteredLibraries); setFeatured(featuredData); @@ -126,7 +126,7 @@ export default function Home() { width: "100%", height: "auto", aspectRatio: "16/9", - display: + display: settings["DISABLE_HOME_SCREEN_LIBRARIES"] === "true" ? "none" : "flex", diff --git a/frontend/src/pages/Startup.tsx b/frontend/src/pages/Startup.tsx index 701bb21..1661327 100644 --- a/frontend/src/pages/Startup.tsx +++ b/frontend/src/pages/Startup.tsx @@ -16,7 +16,7 @@ interface StartupState { setLastStatus: (status: PerPlexed.Status) => void; frontEndStatus?: PerPlexed.Status; - setFrontEndStatus: (frontEndStatus: PerPlexed.Status) => void; + setFrontEndStatus: (frontEndStatus: PerPlexed.Status | undefined) => void; } export const useStartupState = create((set) => ({ @@ -68,6 +68,7 @@ function Startup() { } setLastStatus(res); + setFrontEndStatus(undefined); if (res.error) setShowDiagnostic(true); }; diff --git a/frontend/src/pages/Watch.tsx b/frontend/src/pages/Watch.tsx index 75c2765..c30815c 100755 --- a/frontend/src/pages/Watch.tsx +++ b/frontend/src/pages/Watch.tsx @@ -145,6 +145,12 @@ function Watch() { const { open: syncInterfaceOpen, setOpen: setSyncInterfaceOpen } = useSyncInterfaceState(); + const [controlElementsVisible, setControlElementsVisible] = useState(false); + + useEffect(() => { + setControlElementsVisible(volumePopoverOpen || showTune); + }, [volumePopoverOpen, showTune]); + const loadMetadata = async (itemID: string) => { await getUniversalDecision(itemID, { maxVideoBitrate: quality.bitrate, @@ -1026,19 +1032,18 @@ function Watch() { "& .MuiPaper-root": { overflow: "hidden", borderRadius: 1, + background: "transparent", }, }} > - `1px solid ${alpha(theme.palette.divider, 0.1)}`, + backdropFilter: "blur(20px)", + border: `1px solid ${alpha(theme.palette.divider, 0.1)}`, }} > {tunePage === 0 && ( @@ -1080,12 +1085,11 @@ function Watch() { px: 2, userSelect: "none", cursor: "pointer", - transition: "all 0.2s ease", + transition: "all 0.3s ease-in-out", + backgroundColor: "#00000088", "&:hover": { - backgroundColor: alpha( - theme.palette.primary.main, - 0.15 - ), + transition: "all 0s ease-in-out", + backgroundColor: "#000000ee", }, }} onClick={async () => { @@ -1174,12 +1178,11 @@ function Watch() { px: 2, userSelect: "none", cursor: "pointer", - transition: "all 0.2s ease", + transition: "all 0.3s ease-in-out", + backgroundColor: "#00000088", "&:hover": { - backgroundColor: alpha( - theme.palette.primary.main, - 0.15 - ), + transition: "all 0s ease-in-out", + backgroundColor: "#000000ee", }, }} onClick={async () => { @@ -1256,9 +1259,11 @@ function Watch() { px: 2, userSelect: "none", cursor: "pointer", - transition: "all 0.2s ease", + transition: "all 0.3s ease-in-out", + backgroundColor: "#00000088", "&:hover": { - backgroundColor: alpha(theme.palette.primary.main, 0.15), + transition: "all 0s ease-in-out", + backgroundColor: "#000000ee", }, }} onClick={async () => { @@ -1320,12 +1325,11 @@ function Watch() { px: 2, userSelect: "none", cursor: "pointer", - transition: "all 0.2s ease", + transition: "all 0.3s ease-in-out", + backgroundColor: "#00000088", "&:hover": { - backgroundColor: alpha( - theme.palette.primary.main, - 0.15 - ), + transition: "all 0s ease-in-out", + backgroundColor: "#000000ee", }, }} onClick={async () => { @@ -1418,20 +1422,20 @@ function Watch() { >