Skip to content

Commit aaa3691

Browse files
committed
feat: implement watchlist functionality & major redesign
1 parent 5dddf0f commit aaa3691

17 files changed

+1415
-451
lines changed

frontend/package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
"dependencies": {
66
"@emotion/react": "^11.11.4",
77
"@emotion/styled": "^11.11.5",
8+
"@fontsource-variable/inter": "^5.1.1",
89
"@fontsource-variable/quicksand": "^5.1.0",
10+
"@fontsource-variable/rubik": "^5.1.1",
11+
"@fontsource/ibm-plex-sans": "^5.1.1",
912
"@mui/icons-material": "^5.15.15",
1013
"@mui/material": "^5.15.15",
1114
"axios": "^1.6.8",

frontend/src/App.tsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,54 @@ import Login from "./pages/Login";
99
import Search from "./pages/Search";
1010
import Home from "./pages/Home";
1111
import Library from "./pages/Library";
12+
import BigReader from "./components/BigReader";
13+
import { useWatchListCache } from "./states/WatchListCache";
1214

1315
function App() {
1416
const location = useLocation();
1517
const navigate = useNavigate();
1618

1719
useEffect(() => {
18-
if(!localStorage.getItem("accessToken") && !location.pathname.startsWith("/login")) navigate("/login")
20+
if (
21+
!localStorage.getItem("accessToken") &&
22+
!location.pathname.startsWith("/login")
23+
)
24+
navigate("/login");
1925
}, [location.pathname]);
2026

27+
useEffect(() => {
28+
useWatchListCache.getState().loadWatchListCache();
29+
30+
const interval = setInterval(() => {
31+
useWatchListCache.getState().loadWatchListCache();
32+
}, 60000);
33+
34+
return () => clearInterval(interval);
35+
}, []);
36+
2137
return (
2238
<>
39+
<BigReader />
2340
<Routes>
2441
<Route path="*" element={<AppBar />} />
2542
<Route path="/watch/:itemID" element={<></>} />
2643
</Routes>
27-
<Box sx={{
28-
width: "100%",
29-
height: "auto",
30-
}}>
44+
<Box
45+
sx={{
46+
width: "100%",
47+
height: "auto",
48+
}}
49+
>
3150
<Routes>
3251
<Route path="/" element={<Home />} />
3352
<Route path="/browse/:libraryID" element={<Browse />} />
3453
<Route path="/watch/:itemID" element={<Watch />} />
3554
<Route path="/login" element={<Login />} />
3655
<Route path="/search/:query?" element={<Search />} />
37-
<Route path="/library/:libraryKey/dir/:dir/:subdir?" element={<Library />} />
56+
<Route
57+
path="/library/:libraryKey/dir/:dir/:subdir?"
58+
element={<Library />}
59+
/>
3860
</Routes>
3961
</Box>
4062
</>

frontend/src/components/AppBar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ function HeadLink({
230230
color: "inherit",
231231
fontWeight: 500,
232232
transition: "all 0.2s ease-in-out",
233+
fontFamily: '"Inter Variable", sans-serif',
233234
}}
234235
aria-current={active ? "page" : undefined}
235236
>

frontend/src/components/BigReader.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Backdrop, Box, Button, Typography } from "@mui/material";
2+
import React from "react";
3+
import { create } from "zustand";
4+
5+
interface BigReaderState {
6+
bigReader: string | null;
7+
setBigReader: (bigReader: string) => void;
8+
closeBigReader: () => void;
9+
}
10+
11+
export const useBigReader = create<BigReaderState>((set) => ({
12+
bigReader: null,
13+
setBigReader: (bigReader) => set({ bigReader }),
14+
closeBigReader: () => set({ bigReader: null }),
15+
}));
16+
17+
function BigReader() {
18+
const { bigReader, closeBigReader } = useBigReader();
19+
if (!bigReader) return null;
20+
return (
21+
<Backdrop
22+
open={Boolean(bigReader)}
23+
onClick={closeBigReader}
24+
sx={{
25+
zIndex: "100000",
26+
}}
27+
>
28+
<Box
29+
sx={{
30+
display: "flex",
31+
flexDirection: "column",
32+
alignItems: "center",
33+
justifyContent: "flex-start",
34+
width: "600px",
35+
maxheight: "500px",
36+
backgroundColor: "#171717",
37+
padding: "20px",
38+
borderRadius: "10px",
39+
overflowY: "auto",
40+
}}
41+
onClick={(e) => e.stopPropagation()}
42+
>
43+
<Typography
44+
sx={{
45+
fontSize: "1rem",
46+
fontWeight: "light",
47+
color: "white",
48+
mb: "10px",
49+
}}
50+
>
51+
{bigReader}
52+
</Typography>
53+
54+
<Button onClick={closeBigReader}>Close</Button>
55+
</Box>
56+
</Backdrop>
57+
);
58+
}
59+
60+
export default BigReader;

frontend/src/components/HeroDisplay.tsx

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,57 @@
1-
import { PlayArrow, InfoOutlined } from "@mui/icons-material";
2-
import { Box, Typography, Button } from "@mui/material";
3-
import React from "react";
1+
import {
2+
PlayArrow,
3+
InfoOutlined,
4+
VolumeOff,
5+
VolumeUp,
6+
Pause,
7+
} from "@mui/icons-material";
8+
import { Box, Typography, Button, IconButton } from "@mui/material";
9+
import React, { useEffect, useState } from "react";
410
import { useSearchParams, useNavigate } from "react-router-dom";
11+
import { usePreviewPlayer } from "../states/PreviewPlayerState";
12+
import ReactPlayer from "react-player";
13+
import { useBigReader } from "./BigReader";
514

615
function HeroDisplay({ item }: { item: Plex.Metadata }) {
716
const [searchParams, setSearchParams] = useSearchParams();
817
const navigate = useNavigate();
918

19+
const { MetaScreenPlayerMuted, setMetaScreenPlayerMuted } =
20+
usePreviewPlayer();
21+
22+
const previewVidURL = item?.Extras?.Metadata?.[0]?.Media?.[0]?.Part?.[0]?.key
23+
? `${localStorage.getItem("server")}${
24+
item?.Extras?.Metadata?.[0]?.Media?.[0]?.Part?.[0]?.key
25+
}&X-Plex-Token=${localStorage.getItem("accessToken")}`
26+
: null;
27+
28+
const [previewVidPlaying, setPreviewVidPlaying] = useState<boolean>(false);
29+
30+
useEffect(() => {
31+
setPreviewVidPlaying(false);
32+
33+
if (!previewVidURL) return;
34+
35+
const timeout = setTimeout(() => {
36+
if (window.scrollY > 100) return;
37+
if(searchParams.has("mid")) return;
38+
if(document.location.href.includes("mid=")) return;
39+
setPreviewVidPlaying(true);
40+
}, 3000);
41+
42+
const onScroll = () => {
43+
if (window.scrollY > 100) setPreviewVidPlaying(false);
44+
else setPreviewVidPlaying(true);
45+
};
46+
47+
window.addEventListener("scroll", onScroll);
48+
49+
return () => {
50+
clearTimeout(timeout);
51+
window.removeEventListener("scroll", onScroll);
52+
};
53+
}, []);
54+
1055
return (
1156
<Box
1257
sx={{
@@ -18,6 +63,47 @@ function HeroDisplay({ item }: { item: Plex.Metadata }) {
1863
justifyContent: "flex-start",
1964
}}
2065
>
66+
<Box
67+
sx={{
68+
position: "absolute",
69+
right: "1vw",
70+
bottom: "20vh",
71+
opacity: previewVidURL ? 1 : 0,
72+
transition: "all 1s ease",
73+
zIndex: 1000,
74+
cursor: "pointer",
75+
pointerEvents: "all",
76+
77+
display: "flex",
78+
flexDirection: "row",
79+
alignItems: "center",
80+
justifyContent: "center",
81+
gap: 1,
82+
}}
83+
>
84+
<IconButton
85+
sx={{
86+
backgroundColor: "#00000088",
87+
}}
88+
onClick={() => {
89+
setPreviewVidPlaying(!previewVidPlaying);
90+
}}
91+
>
92+
{previewVidPlaying ? <Pause /> : <PlayArrow />}
93+
</IconButton>
94+
95+
<IconButton
96+
sx={{
97+
backgroundColor: "#00000088",
98+
}}
99+
onClick={() => {
100+
setMetaScreenPlayerMuted(!MetaScreenPlayerMuted);
101+
}}
102+
>
103+
{MetaScreenPlayerMuted ? <VolumeOff /> : <VolumeUp />}
104+
</IconButton>
105+
</Box>
106+
21107
<Box
22108
sx={{
23109
width: "100%",
@@ -33,8 +119,47 @@ function HeroDisplay({ item }: { item: Plex.Metadata }) {
33119
backgroundPosition: "center",
34120
backgroundRepeat: "no-repeat",
35121
zIndex: 0,
122+
position: "relative",
36123
}}
37124
>
125+
<Box
126+
sx={{
127+
position: "absolute",
128+
// make it take up the full width of the parent
129+
width: "100%",
130+
height: "100vh",
131+
aspectRatio: "16/9",
132+
left: 0,
133+
top: 0,
134+
filter: "brightness(0.5)",
135+
opacity: previewVidPlaying ? 1 : 0,
136+
transition: "all 2s ease",
137+
backgroundColor: previewVidPlaying ? "#000000" : "transparent",
138+
pointerEvents: "none",
139+
140+
overflow: "hidden",
141+
}}
142+
>
143+
<ReactPlayer
144+
url={previewVidURL ?? undefined}
145+
controls={false}
146+
width="100%"
147+
height="100%"
148+
playing={previewVidPlaying}
149+
volume={MetaScreenPlayerMuted ? 0 : 0.5}
150+
muted={MetaScreenPlayerMuted}
151+
onEnded={() => {
152+
setPreviewVidPlaying(false);
153+
}}
154+
pip={false}
155+
config={{
156+
file: {
157+
attributes: { disablePictureInPicture: true },
158+
},
159+
}}
160+
/>
161+
</Box>
162+
38163
<Box
39164
sx={{
40165
ml: 10,
@@ -94,6 +219,12 @@ function HeroDisplay({ item }: { item: Plex.Metadata }) {
94219
WebkitBoxOrient: "vertical",
95220
overflow: "hidden",
96221
textOverflow: "ellipsis",
222+
223+
userSelect: "none",
224+
cursor: "zoom-in",
225+
}}
226+
onClick={() => {
227+
useBigReader.getState().setBigReader(item.summary);
97228
}}
98229
>
99230
{item.summary}
@@ -150,6 +281,7 @@ function HeroDisplay({ item }: { item: Plex.Metadata }) {
150281
}}
151282
onClick={() => {
152283
if (!item) return;
284+
setPreviewVidPlaying(false);
153285
setSearchParams({
154286
...searchParams,
155287
mid: item.ratingKey.toString(),

0 commit comments

Comments
 (0)