diff --git a/assets/screenshot1.png b/assets/screenshot1.png
index 859770f..4169001 100644
Binary files a/assets/screenshot1.png and b/assets/screenshot1.png differ
diff --git a/assets/screenshot2.png b/assets/screenshot2.png
index 8cbaf94..e421c59 100644
Binary files a/assets/screenshot2.png and b/assets/screenshot2.png differ
diff --git a/assets/screenshot3.png b/assets/screenshot3.png
index 546f7fc..43e3663 100644
Binary files a/assets/screenshot3.png and b/assets/screenshot3.png differ
diff --git a/backend/src/index.ts b/backend/src/index.ts
index dde7764..76359db 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -113,8 +113,12 @@ const noVerifyHttpsAgent = new https.Agent({
while (true) {
const r = await axios.get(`${process.env.PLEX_SERVER ?? "http://localhost:32400"}/identity`, {
timeout: 5000,
- }).catch(() => null);
-
+ httpAgent: process.env.DISABLE_TLS_VERIFY === "true" ? noVerifyHttpsAgent : undefined,
+ httpsAgent: process.env.DISABLE_TLS_VERIFY === "true" ? noVerifyHttpsAgent : undefined,
+ }).catch((e) => {
+ console.error('Error reaching PLEX_SERVER:', e.message);
+ return null;
+ });
if (r && r.status === 200) {
status.error = false;
return resolve();
diff --git a/frontend/src/components/HeroDisplay.tsx b/frontend/src/components/HeroDisplay.tsx
index 70f2f2f..44f7e57 100644
--- a/frontend/src/components/HeroDisplay.tsx
+++ b/frontend/src/components/HeroDisplay.tsx
@@ -11,7 +11,7 @@ import { useSearchParams, useNavigate } from "react-router-dom";
import { usePreviewPlayer } from "../states/PreviewPlayerState";
import ReactPlayer from "react-player";
import { useBigReader } from "./BigReader";
-import { WatchListButton } from "./MovieItem";
+import { HeroWatchListButton } from "./MovieItem";
import { getBackendURL } from "../backendURL";
import { queryBuilder } from "../plex/QuickFunctions";
import { getTranscodeImageURL } from "../plex";
@@ -318,7 +318,7 @@ function HeroDisplay({ item }: { item: Plex.Metadata }) {
-
+
diff --git a/frontend/src/components/MetaScreen.tsx b/frontend/src/components/MetaScreen.tsx
index 86be36a..bae48a5 100755
--- a/frontend/src/components/MetaScreen.tsx
+++ b/frontend/src/components/MetaScreen.tsx
@@ -50,7 +50,7 @@ import { usePreviewPlayer } from "../states/PreviewPlayerState";
import MovieItem from "./MovieItem";
import { useBigReader } from "./BigReader";
import { useInView } from "react-intersection-observer";
-import { WatchListButton } from "./MovieItem";
+import { HeroWatchListButton } from "./MovieItem";
import { alpha } from "@mui/material/styles";
import { AnimatePresence, motion } from "framer-motion";
import { useConfirmModal } from "./ConfirmModal";
@@ -668,7 +668,7 @@ function MetaScreen() {
-
+
{data && }
diff --git a/frontend/src/components/MovieItem.tsx b/frontend/src/components/MovieItem.tsx
index 2161fdc..8939e87 100644
--- a/frontend/src/components/MovieItem.tsx
+++ b/frontend/src/components/MovieItem.tsx
@@ -7,6 +7,7 @@ import {
CheckCircleRounded,
VolumeOffRounded,
VolumeUpRounded,
+ StarRounded,
} from "@mui/icons-material";
import {
Box,
@@ -360,7 +361,7 @@ function MovieItem({
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
- justifyContent: "flex-end",
+ justifyContent: "flex-start",
width: itemsPerPage
? `calc((100vw / ${itemsPerPage}) - 10px - (5vw / ${itemsPerPage}))`
: "100%",
@@ -370,38 +371,34 @@ function MovieItem({
backgroundColor: "rgba(18, 18, 22, 0.7)",
backdropFilter: "blur(15px)",
border: "1px solid rgba(255,255,255,0.08)",
-
- borderRadius: "6px",
+ borderRadius: "8px",
overflow: "hidden",
- mb: "0px",
position: "relative",
boxShadow: "0px 2px 8px rgba(0, 0, 0, 0.3)",
+ willChange: "transform",
+ backfaceVisibility: "hidden",
+ transformOrigin:
+ itemsPerPage && index !== undefined
+ ? (index % itemsPerPage) === 0
+ ? "left center"
+ : (index % itemsPerPage) === itemsPerPage - 1
+ ? "right center"
+ : "center center"
+ : "center center",
+ transition:
+ "transform 0.4s cubic-bezier(0.25,0.10,0.25,1.00), box-shadow 0.4s cubic-bezier(0.25,0.10,0.25,1.00), border-color 0.4s cubic-bezier(0.25,0.10,0.25,1.00)",
+ cursor: "pointer",
"&:hover": {
- transform: "scale(1.08)",
- transition: "all 0.4s cubic-bezier(0.25,0.10,0.25,1.00)",
+ transform: "scale(1.15)",
zIndex: 10,
- boxShadow: "0px 4px 15px rgba(0, 0, 0, 0.4)",
+ boxShadow: "0px 8px 24px rgba(0, 0, 0, 0.5)",
border: "1px solid rgba(255,255,255,0.15)",
- backgroundColor: "rgba(18, 18, 22, 0.85)",
- position: "relative",
- pb: "10px",
- mb: "-42px",
},
- [`&:hover > :nth-child(${
- item.type === "episode" ||
- (item.type === "movie" && item.viewOffset)
- ? 4
- : 3
- })`]: {
- height: "32px",
- opacity: 1,
+ "&:hover .movie-item-hover-overlay": {
+ opacity: "1 !important",
},
-
- 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) => {
e.preventDefault();
@@ -411,10 +408,7 @@ function MovieItem({
mouseX: e.clientX + 2,
mouseY: e.clientY - 6,
}
- : // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu
- // Other native context menus might behave different.
- // With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus.
- null
+ : null
);
}}
onClick={async () => {
@@ -444,35 +438,40 @@ function MovieItem({
setHovered(false);
}}
>
+ {/* Thumbnail area */}
+ {/* Background image */}
+
+ {/* Preview playback overlay */}
+
@@ -500,21 +499,76 @@ function MovieItem({
/>
+ {/* Hover overlay with buttons */}
+
+ {
+ e.stopPropagation();
+ await handlePlay();
+ }}
+ >
+ {playButtonLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {/* Mute button for preview */}
{
e.stopPropagation();
@@ -527,44 +581,88 @@ function MovieItem({
}}
>
{MetaScreenPlayerMuted ? (
-
+
) : (
-
+
)}
+ {/* Watched badge */}
{((item.type === "show" && item.leafCount === item.viewedLeafCount) ||
(item.type === "movie" &&
item?.viewCount !== undefined &&
item.viewCount > 0)) && (
- theme.palette.primary.light,
- }}
- />
+
+ theme.palette.primary.light,
+ }}
+ />
+
+ Watched
+
+
)}
+
+ {/* Content rating badge on thumbnail */}
+ {item.contentRating && (
+
+
+ {item.contentRating}
+
+
+ )}
+
+ {/* Progress bar */}
{(item.type === "episode" ||
(item.type === "movie" && item.viewOffset)) && (
theme.palette.primary.main,
- borderRadius: "2px",
},
}}
/>
)}
+
+ {/* Info section */}
- theme.palette.primary.light,
- textTransform: "uppercase",
- opacity: 0.9,
- mb: 0.5,
+ width: "100%",
+ display: "flex",
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ mb: "2px",
}}
>
- {item.type} {item.type === "episode" && item.index}
-
+ theme.palette.primary.light,
+ textTransform: "uppercase",
+ opacity: 0.9,
+ lineHeight: 1,
+ }}
+ >
+ {item.type === "episode"
+ ? `S${item.parentIndex} · E${item.index}`
+ : item.type}
+
+
+ {item.audienceRating && (
+
+
+
+ {item.audienceRating.toFixed(1)}
+
+
+ )}
+
+ {/* Title */}
theme.palette.text.primary,
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
- maxLines: 1,
- maxInlineSize: "100%",
- marginTop: ["episode"].includes(item.type) ? "2px" : "0px",
+ width: "100%",
lineHeight: 1.3,
"@media (max-width: 2000px)": {
- fontSize: "1rem",
+ fontSize: "0.9rem",
},
}}
>
{item.title}
+ {/* Show title for episodes */}
{["episode"].includes(item.type) && item.grandparentTitle && (
{
@@ -642,85 +780,76 @@ function MovieItem({
});
}}
sx={{
- fontSize: "0.85rem",
+ fontSize: "0.8rem",
fontWeight: "500",
color: (theme) => theme.palette.text.secondary,
- opacity: 0.8,
- mb: 1,
- transition: "all 0.4s cubic-bezier(0.25,0.10,0.25,1.00)",
+ opacity: 0.7,
+ transition: "all 0.3s ease",
cursor: "pointer",
"&:hover": {
opacity: 1,
color: (theme) => theme.palette.primary.light,
- textDecoration: "none",
},
textOverflow: "ellipsis",
overflow: "hidden",
- maxLines: 1,
- maxInlineSize: "100%",
+ whiteSpace: "nowrap",
+ width: "100%",
}}
>
{item.grandparentTitle}
)}
+
+ {/* Metadata row */}
- {/* {item.rating && (
-
- {item.rating}
-
- )}
- {item.contentRating && (
-
- {item.contentRating}
-
- )} */}
- {/* {item.type === "episode" && item.index && (
-
- S{item.parentIndex} E{item.index}
-
- )} */}
+ {item.year && (
+ theme.palette.text.secondary,
+ opacity: 0.7,
+ flexShrink: 0,
+ }}
+ >
+ {item.year}
+
+ )}
+ {item.year &&
+ ((item.duration && ["episode", "movie"].includes(item.type)) ||
+ (item.type === "show" &&
+ item.leafCount &&
+ (item.seasonCount ?? item.childCount))) && (
+
+ ·
+
+ )}
{item.duration && ["episode", "movie"].includes(item.type) && (
theme.palette.text.secondary,
- opacity: 0.8,
+ opacity: 0.7,
+ flexShrink: 0,
}}
>
{durationToText(item.duration)}
@@ -731,91 +860,49 @@ function MovieItem({
(item.seasonCount ?? item.childCount) && (
theme.palette.text.secondary,
- opacity: 0.8,
+ opacity: 0.7,
+ flexShrink: 0,
}}
>
{(item.seasonCount ?? item.childCount ?? 1) > 1
? `${item.childCount} Seasons`
- : `${item.leafCount} Episode${
- item.leafCount > 1 ? "s" : ""
- }`}
+ : `${item.leafCount} Ep${item.leafCount > 1 ? "s" : ""}`}
)}
- {item.year && (
- theme.palette.text.secondary,
- opacity: 0.8,
- }}
- >
- {item.year}
-
+ {item.Genre && item.Genre.length > 0 && (
+ <>
+
+ ·
+
+ theme.palette.text.secondary,
+ opacity: 0.7,
+ textOverflow: "ellipsis",
+ overflow: "hidden",
+ whiteSpace: "nowrap",
+ minWidth: 0,
+ }}
+ >
+ {item.Genre[0].tag}
+
+ >
)}
-
-
-
-
-
-
-
-
>
);
@@ -828,16 +915,20 @@ export function WatchListButton({ item }: { item: Plex.Metadata }) {
const [isLoading, setIsLoading] = React.useState(false);
return (
-