From 530b8e46cea86a1baeb0e81112ff6494e882d81c Mon Sep 17 00:00:00 2001 From: Ipmake Date: Sat, 27 Sep 2025 18:06:19 +0200 Subject: [PATCH 01/10] Fixed backend not retrying plex server connection --- backend/src/index.ts | 27 +++++++++++++++++---------- frontend/src/pages/Startup.tsx | 3 ++- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index d5b16a7..52887c2 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -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}/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/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); }; From 963943858683912462eb600636f7671c5be034e2 Mon Sep 17 00:00:00 2001 From: Ipmake Date: Sat, 27 Sep 2025 18:41:36 +0200 Subject: [PATCH 02/10] Enhance AppBar and Watch components with improved library handling and UI updates --- frontend/src/components/AppBar.tsx | 205 ++++++++++++++++-- .../src/components/WatchShowChildView.tsx | 59 ++--- frontend/src/pages/Watch.tsx | 67 ++++-- 3 files changed, 265 insertions(+), 66 deletions(-) diff --git a/frontend/src/components/AppBar.tsx b/frontend/src/components/AppBar.tsx index 5ad6da5..cfa4c00 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,36 @@ 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)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), + ]); + }); + }, [settings]); return ( {!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 && ( + + )} @@ -698,6 +719,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/WatchShowChildView.tsx b/frontend/src/components/WatchShowChildView.tsx index ef921f4..1b7cf61 100644 --- a/frontend/src/components/WatchShowChildView.tsx +++ b/frontend/src/components/WatchShowChildView.tsx @@ -4,8 +4,8 @@ import { SubscriptionsRounded, } from "@mui/icons-material"; import { + Backdrop, Box, - ClickAwayListener, Divider, Fade, IconButton, @@ -63,6 +63,18 @@ function WatchShowChildView({ item }: { item: Plex.Metadata }) { return ( <> + { + e.stopPropagation(); + setAnchorEl(null); + }} + /> + e.stopPropagation()} > {({ TransitionProps }) => ( @@ -100,32 +113,27 @@ function WatchShowChildView({ item }: { item: Plex.Metadata }) { userSelect: "none", }} > - { - e.stopPropagation(); - setAnchorEl(null); + e.stopPropagation()} > - - {/* Season selector */} - {(seasons?.length ?? 1) > 1 && ( - + {/* Season selector */} + {(seasons?.length ?? 1) > 1 && ( + - )} diff --git a/frontend/src/pages/Watch.tsx b/frontend/src/pages/Watch.tsx index 75c2765..0cabecf 100755 --- a/frontend/src/pages/Watch.tsx +++ b/frontend/src/pages/Watch.tsx @@ -1751,6 +1751,9 @@ function Watch() { if (playing) socket?.emit("EVNT_SYNC_PAUSE"); else socket?.emit("EVNT_SYNC_RESUME"); }} + onKeyDown={(e) => { + e.preventDefault(); + }} > {playing ? ( @@ -1811,30 +1814,44 @@ function Watch() { vertical: "bottom", horizontal: "center", }} + elevation={6} sx={{ - "& .MuiPaper-root": { - py: 2, - }, + userSelect: "none", }} > - { - setVolume(value as number); - localStorage.setItem("volume", value.toString()); + height: "auto", + userSelect: "none", + bgcolor: "#000", + border: (theme) => + `1px solid ${alpha(theme.palette.divider, 0.1)}`, + py: "20px", }} - aria-labelledby="continuous-slider" - min={0} - max={100} - step={1} - orientation="vertical" - /> + > + { + setVolume(value as number); + localStorage.setItem("volume", value.toString()); + }} + aria-labelledby="continuous-slider" + min={0} + max={100} + step={1} + orientation="vertical" + /> + { + e.preventDefault(); + }} onClick={(event) => { setVolumePopoverAnchor(event.currentTarget); }} @@ -1847,6 +1864,9 @@ function Watch() { )} { + e.preventDefault(); + }} onClick={(event) => { setShowTune(!showTune); setTunePage(0); @@ -1858,6 +1878,9 @@ function Watch() { {room && ( { + e.preventDefault(); + }} onClick={() => { setSyncInterfaceOpen(true); }} @@ -1867,6 +1890,9 @@ function Watch() { )} { + e.preventDefault(); + }} onClick={() => { if (!document.fullscreenElement) document.documentElement.requestFullscreen(); @@ -2123,6 +2149,9 @@ function NextEPButton({ queue }: { queue?: Plex.Metadata[] }) { onClick={() => { navigate(`/watch/${queue[1].ratingKey}`); }} + onKeyDown={(e) => { + e.preventDefault(); + }} onMouseEnter={(e) => setAnchorEl(e.currentTarget)} onMouseLeave={() => setAnchorEl(null)} > @@ -2246,17 +2275,17 @@ export function getCurrentVideoLevels( { title: "Convert to 4K", bitrate: 40000, - extra: "(High) 40Mbps", + extra: "(High) 60Mbps", }, { title: "Convert to 4K", bitrate: 30000, - extra: "(Medium) 30Mbps", + extra: "(Medium) 40Mbps", }, { title: "Convert to 4K", bitrate: 20000, - extra: "20Mbps", + extra: "30Mbps", }, { title: "Convert to 1080p", From 25f5e00711d7b7f089b3f71b55ddffb747b0aaaf Mon Sep 17 00:00:00 2001 From: Ipmake Date: Sat, 27 Sep 2025 18:43:27 +0200 Subject: [PATCH 03/10] Removed testing functions --- frontend/src/components/AppBar.tsx | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/frontend/src/components/AppBar.tsx b/frontend/src/components/AppBar.tsx index cfa4c00..933d720 100755 --- a/frontend/src/components/AppBar.tsx +++ b/frontend/src/components/AppBar.tsx @@ -89,26 +89,7 @@ function Appbar() { return rawValue === undefined || rawValue === "true"; }); - setLibraries([ - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ...filtered.filter((lib) => ["movie", "show"].includes(lib.type)), - ]); + setLibraries(filtered.filter((lib) => ["movie", "show"].includes(lib.type))); }); }, [settings]); From e8e2833576a670b97de025e55a2cee99cef2b896 Mon Sep 17 00:00:00 2001 From: Ipmake Date: Sat, 27 Sep 2025 18:53:15 +0200 Subject: [PATCH 04/10] Fixed slow Homescreen loading --- frontend/src/pages/Home.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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", From c5fe81f55c6874a58933b45cc701a0dce1394725 Mon Sep 17 00:00:00 2001 From: Ipmake Date: Sat, 27 Sep 2025 18:53:46 +0200 Subject: [PATCH 05/10] Fixed view qualities and removed condition on mark as watched/unwatched --- frontend/src/components/MetaScreen.tsx | 109 ++++++++++++++++------- frontend/src/components/MovieItem.tsx | 118 +++++++++++-------------- frontend/src/pages/Watch.tsx | 14 +-- 3 files changed, 136 insertions(+), 105 deletions(-) diff --git a/frontend/src/components/MetaScreen.tsx b/frontend/src/components/MetaScreen.tsx index 3eaa801..b019944 100755 --- a/frontend/src/components/MetaScreen.tsx +++ b/frontend/src/components/MetaScreen.tsx @@ -1481,7 +1481,12 @@ function MetaPageReviews({ data }: { data: Plex.Metadata | undefined }) { {username?.charAt(0) || "U"} - + {username || "Anonymous User"} @@ -1979,23 +1984,51 @@ function EpisodeItem({ Play + { + e.stopPropagation(); + if (setSelectMode) setSelectMode(!selectMode); + if (setSelected) setSelected(!selected); + handleClose(); + }} + > + + {selectMode ? ( + + ) : ( + + )} + + {selectMode ? "Disable Selection" : "Enable Selection"} + + + + { if (!item) return; - let state = !(!Boolean(item.viewOffset) && Boolean(item.viewCount)) - ? "unwatched" - : "watched"; useConfirmModal.getState().setModal({ - title: `Mark as ${state === "watched" ? "Unwatched" : "Watched"}`, - message: `Are you sure you want to mark "${item.title}" as ${ - state === "watched" ? "Unwatched" : "Watched" - }?`, + title: `Mark as Watched`, + message: `Are you sure you want to mark "${item.title}" as Watched?`, onConfirm: async () => { - await setMediaPlayedStatus( - !(!Boolean(item.viewOffset) && Boolean(item.viewCount)), - item.ratingKey - ); + switch (item.type) { + case "movie": + case "episode": + item.viewCount = 1; + await setMediaPlayedStatus(true, item.ratingKey); + break; + case "show": + item.viewedLeafCount = item.leafCount; + await setMediaPlayedStatus(true, item.ratingKey); + break; + default: + break; + } handleClose(); refetchData?.(); @@ -2007,33 +2040,45 @@ function EpisodeItem({ }} > - {!Boolean(item.viewOffset) && Boolean(item.viewCount) ? ( - - ) : ( - - )} + - - {!Boolean(item.viewOffset) && Boolean(item.viewCount) - ? "Mark as Unwatched" - : "Mark as Watched"} + Mark as Watched { - e.stopPropagation(); - if (setSelectMode) setSelectMode(!selectMode); - if (setSelected) setSelected(!selected); - handleClose(); + onClick={async () => { + if (!item) return; + + useConfirmModal.getState().setModal({ + title: `Mark as Unwatched`, + message: `Are you sure you want to mark "${item.title}" as Unwatched?`, + onConfirm: async () => { + switch (item.type) { + case "movie": + case "episode": + item.viewCount = 0; + await setMediaPlayedStatus(false, item.ratingKey); + break; + case "show": + item.viewedLeafCount = 0; + await setMediaPlayedStatus(false, item.ratingKey); + break; + default: + break; + } + + handleClose(); + refetchData?.(); + }, + onCancel: () => { + handleClose(); + }, + }); }} > - {selectMode ? ( - - ) : ( - - )} + - {selectMode ? "Disable Selection" : "Enable Selection"} + Mark as Unwatched View Similar + + + { if (!item) return; - let state = "unwatched"; - - if ( - (item.type === "movie" && Boolean(item.viewCount)) || - (item.type === "episode" && - (item.viewOffset ?? 0) >= item.duration) - ) { - state = "watched"; - } - - if (item.type === "show") { - state = - item.viewedLeafCount === item.leafCount - ? "watched" - : "unwatched"; - } useConfirmModal.getState().setModal({ - title: `Mark as ${state === "watched" ? "Unwatched" : "Watched"}`, - message: `Are you sure you want to mark "${item.title}" as ${ - state === "watched" ? "Unwatched" : "Watched" - }?`, + title: `Mark as Watched`, + message: `Are you sure you want to mark "${item.title}" as Watched?`, onConfirm: async () => { switch (item.type) { case "movie": case "episode": - if ( - item.type === "episode" && - (item?.viewOffset ?? 0) < item.duration - ) { - item.viewCount = 1; - } else { - item.viewCount = !Boolean(item.viewCount) ? 1 : 0; - } - await setMediaPlayedStatus( - Boolean(item.viewCount), - item.ratingKey - ); + item.viewCount = 1; + await setMediaPlayedStatus(true, item.ratingKey); break; case "show": - const newViewedLeafCount = - item.viewedLeafCount === item.leafCount - ? 0 - : item.leafCount; - item.viewedLeafCount = newViewedLeafCount; - await setMediaPlayedStatus( - newViewedLeafCount === item.leafCount, - item.ratingKey - ); + item.viewedLeafCount = item.leafCount; + await setMediaPlayedStatus(true, item.ratingKey); break; default: break; @@ -341,32 +313,45 @@ function MovieItem({ }} > - {item?.type === "movie" || item?.type === "episode" ? ( - !((item?.viewCount ?? 0) > 0) ? ( - - ) : ( - - ) - ) : item?.type === "show" ? ( - item?.viewedLeafCount === item?.leafCount ? ( - - ) : ( - - ) - ) : null} + + Mark as Watched + + { + if (!item) return; - {item?.type === "movie" || item?.type === "episode" - ? item.type === "episode" && (item.viewOffset ?? 0) < item.duration - ? "Mark as Watched" - : !((item?.viewCount ?? 0) > 0) - ? "Mark as Watched" - : "Mark as Unwatched" - : item?.type === "show" - ? item?.viewedLeafCount === item?.leafCount - ? "Mark as Unwatched" - : "Mark as Watched" - : null} + useConfirmModal.getState().setModal({ + title: `Mark as Unwatched`, + message: `Are you sure you want to mark "${item.title}" as Unwatched?`, + onConfirm: async () => { + switch (item.type) { + case "movie": + case "episode": + item.viewCount = 0; + await setMediaPlayedStatus(false, item.ratingKey); + break; + case "show": + item.viewedLeafCount = 0; + await setMediaPlayedStatus(false, item.ratingKey); + break; + default: + break; + } + + handleClose(); + refetchData?.(); + }, + onCancel: () => { + handleClose(); + }, + }); + }} + > + + + + Mark as Unwatched @@ -410,7 +395,8 @@ function MovieItem({ opacity: 1, }, - transition: "all 0.4s cubic-bezier(0.25,0.10,0.25,1.00), transform 0.4s cubic-bezier(0.25,0.10,0.25,1.00)", + transition: + "all 0.4s cubic-bezier(0.25,0.10,0.25,1.00), transform 0.4s cubic-bezier(0.25,0.10,0.25,1.00)", cursor: "pointer", }} onContextMenu={(e) => { diff --git a/frontend/src/pages/Watch.tsx b/frontend/src/pages/Watch.tsx index 0cabecf..5c96e76 100755 --- a/frontend/src/pages/Watch.tsx +++ b/frontend/src/pages/Watch.tsx @@ -2274,17 +2274,17 @@ export function getCurrentVideoLevels( ...[ { title: "Convert to 4K", - bitrate: 40000, + bitrate: 60000, extra: "(High) 60Mbps", }, { title: "Convert to 4K", - bitrate: 30000, + bitrate: 40000, extra: "(Medium) 40Mbps", }, { title: "Convert to 4K", - bitrate: 20000, + bitrate: 30000, extra: "30Mbps", }, { @@ -2299,8 +2299,8 @@ export function getCurrentVideoLevels( }, { title: "Convert to 1080p", - bitrate: 10000, - extra: "10Mbps", + bitrate: 8000, + extra: "8Mbps", }, { title: "Convert to 720p", @@ -2336,8 +2336,8 @@ export function getCurrentVideoLevels( }, { title: "Convert to 1080p", - bitrate: 10000, - extra: "10Mbps", + bitrate: 8000, + extra: "8Mbps", }, { title: "Convert to 720p", From a679c92071578aec2633f102487ec9e258359ed6 Mon Sep 17 00:00:00 2001 From: Ipmake Date: Wed, 1 Oct 2025 15:52:40 +0200 Subject: [PATCH 06/10] Refactor UI components and styles for improved aesthetics and functionality --- frontend/src/components/AppBar.tsx | 43 +- frontend/src/components/ConfirmModal.tsx | 2 - frontend/src/components/HeroDisplay.tsx | 9 - frontend/src/components/MetaScreen.tsx | 36 -- frontend/src/components/MovieItem.tsx | 101 ++-- frontend/src/components/MovieItemLegacy.tsx | 15 - .../src/components/PlaybackNextEPButton.tsx | 177 +++--- .../src/components/WatchShowChildView.tsx | 557 +++++++++--------- frontend/src/index.tsx | 235 +++++++- frontend/src/pages/Browse.tsx | 14 - frontend/src/pages/Watch.tsx | 554 ++++++++++------- 11 files changed, 1042 insertions(+), 701 deletions(-) diff --git a/frontend/src/components/AppBar.tsx b/frontend/src/components/AppBar.tsx index 933d720..4f4fb46 100755 --- a/frontend/src/components/AppBar.tsx +++ b/frontend/src/components/AppBar.tsx @@ -106,12 +106,15 @@ function Appbar() { height: 64, transition: "all 0.5s ease-in-out", - bgcolor: (theme) => (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, }} > @@ -296,13 +299,6 @@ S - Skip onscreen markers (intro, credits, etc) }) ); }} - sx={{ - borderRadius: "10px", - padding: 1, - "&:hover": { - backgroundColor: "#000000AA", - }, - }} > @@ -312,13 +308,6 @@ S - Skip onscreen markers (intro, credits, etc) onClick={() => { useSyncInterfaceState.getState().setOpen(true); }} - sx={{ - borderRadius: "10px", - padding: 1, - "&:hover": { - backgroundColor: "#000000AA", - }, - }} > @@ -332,7 +321,7 @@ S - Skip onscreen markers (intro, credits, etc) sx={{ width: 45, height: 45, - borderRadius: "10px", + borderRadius: "4px", cursor: "pointer", "&:hover": { @@ -524,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, }} @@ -544,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", @@ -588,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", @@ -628,7 +611,7 @@ function SearchBar() { style={{ aspectRatio: 1, objectFit: "cover", - borderRadius: "5px", + borderRadius: "4px", width: 50, height: 50, }} @@ -656,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", @@ -750,7 +733,7 @@ function LibrariesDropdown({ libraries }: { libraries: Plex.LibarySection[] }) { sx={{ backgroundColor: "#121212EE", backdropFilter: "blur(20px)", - borderRadius: "10px", + borderRadius: "4px", boxShadow: "0px 4px 20px 0px #000000AA", padding: "15px", maxWidth: "600px", @@ -789,7 +772,7 @@ function LibrariesDropdown({ libraries }: { libraries: Plex.LibarySection[] }) { padding: "12px 16px", cursor: "pointer", transition: "all 0.2s ease-in-out", - borderRadius: "6px", + borderRadius: "4px", textAlign: "left", minHeight: "48px", display: "flex", 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 1b7cf61..d4a7862 100644 --- a/frontend/src/components/WatchShowChildView.tsx +++ b/frontend/src/components/WatchShowChildView.tsx @@ -12,6 +12,7 @@ import { 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,325 +73,342 @@ function WatchShowChildView({ item }: { item: Plex.Metadata }) { })(); }, [item]); + useEffect(() => { + setControlElementsVisible(Boolean(anchorEl)); + }, [anchorEl, setControlElementsVisible]); + return ( <> - { - e.stopPropagation(); - setAnchorEl(null); - }} - /> - - e.stopPropagation()} - > - {({ TransitionProps }) => ( - - + { + e.stopPropagation(); + setAnchorEl(null); + }} + > + - e.stopPropagation()} - > - {/* Season selector */} - {(seasons?.length ?? 1) > 1 && ( + borderRadius: 1, + background: "transparent", + }, + }} + transition + modifiers={[ + { + name: "offset", + options: { + offset: [0, 8], + }, + }, + { + name: "preventOverflow", + enabled: true, + options: { + altAxis: false, + altBoundary: false, + tether: false, + rootBoundary: "viewport", + padding: 16, + }, + }, + ]} + onClick={(e) => e.stopPropagation()} + > + {({ TransitionProps }) => ( + + e.stopPropagation()} > - - - {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 selector */} + {(seasons?.length ?? 1) > 1 && ( + + - {season.title} + {item.grandparentTitle} - - {selectedSeason !== season.index && ( - - )} - ))} - - )} - {/* Episodes list */} - - - {episodes - ?.filter( - (episode) => episode.parentIndex === selectedSeason - ) - .map((episode) => ( + + + {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/Watch.tsx b/frontend/src/pages/Watch.tsx index 5c96e76..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() { >