Skip to content

Commit 2f8d111

Browse files
committed
feat: Enhance keyboard controls for video playback and improved Next Episode button
1 parent a8a303b commit 2f8d111

File tree

1 file changed

+205
-34
lines changed

1 file changed

+205
-34
lines changed

frontend/src/pages/Watch.tsx

Lines changed: 205 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {
1919
Button,
2020
Fade,
2121
IconButton,
22+
Paper,
2223
Popover,
24+
Popper,
2325
Slider,
2426
Theme,
2527
Typography,
@@ -268,34 +270,84 @@ function Watch() {
268270
// . (period): Forward 1 frame
269271
useEffect(() => {
270272
const handleKeyDown = (e: KeyboardEvent) => {
271-
if (e.key === " " && player.current) {
272-
setPlaying((state) => !state);
273-
}
274-
if (e.key === "ArrowLeft" && player.current) {
275-
player.current.seekTo(player.current.getCurrentTime() - 10);
276-
}
277-
if (e.key === "ArrowRight" && player.current) {
278-
player.current.seekTo(player.current.getCurrentTime() + 10);
279-
}
280-
if (e.key === "ArrowUp" && player.current) {
281-
setVolume((state) => Math.min(state + 5, 100));
282-
}
283-
if (e.key === "ArrowDown" && player.current) {
284-
setVolume((state) => Math.max(state - 5, 0));
285-
}
286-
if (e.key === "," && player.current) {
287-
player.current.seekTo(player.current.getCurrentTime() - 0.04);
288-
}
289-
if (e.key === "." && player.current) {
290-
player.current.seekTo(player.current.getCurrentTime() + 0.04);
291-
}
273+
const actions: { [key: string]: () => void } = {
274+
" ": () => setPlaying((state) => !state),
275+
k: () => setPlaying((state) => !state),
276+
j: () => player.current?.seekTo(player.current.getCurrentTime() - 10),
277+
l: () => player.current?.seekTo(player.current.getCurrentTime() + 10),
278+
s: () => {
279+
if (!metadata || !player.current) return;
280+
// if there is a marker like credits skip it
281+
const time = player.current.getCurrentTime();
282+
for (const marker of metadata.Marker ?? []) {
283+
if (
284+
!(
285+
marker.startTimeOffset / 1000 <= time &&
286+
marker.endTimeOffset / 1000 >= time
287+
)
288+
)
289+
continue;
290+
291+
switch (marker.type) {
292+
case "credits":
293+
{
294+
if (!marker.final) {
295+
player.current.seekTo(marker.endTimeOffset / 1000 + 1);
296+
return;
297+
}
298+
299+
if (metadata.type === "movie")
300+
return navigate(
301+
`/browse/${metadata.librarySectionID}?${queryBuilder({
302+
mid: metadata.ratingKey,
303+
})}`
304+
);
305+
306+
if (!playQueue) return;
307+
const next = playQueue[1];
308+
if (!next)
309+
return navigate(
310+
`/browse/${metadata.librarySectionID}?${queryBuilder({
311+
mid: metadata.grandparentRatingKey,
312+
pid: metadata.parentRatingKey,
313+
iid: metadata.ratingKey,
314+
})}`
315+
);
316+
317+
navigate(`/watch/${next.ratingKey}`);
318+
}
319+
break;
320+
case "intro":
321+
player.current.seekTo(marker.endTimeOffset / 1000 + 1);
322+
break;
323+
}
324+
}
325+
},
326+
f: () => {
327+
if (!document.fullscreenElement) {
328+
document.documentElement.requestFullscreen();
329+
} else document.exitFullscreen();
330+
},
331+
ArrowLeft: () =>
332+
player.current?.seekTo(player.current.getCurrentTime() - 10),
333+
ArrowRight: () =>
334+
player.current?.seekTo(player.current.getCurrentTime() + 10),
335+
ArrowUp: () => setVolume((state) => Math.min(state + 5, 100)),
336+
ArrowDown: () => setVolume((state) => Math.max(state - 5, 0)),
337+
",": () =>
338+
player.current?.seekTo(player.current.getCurrentTime() - 0.04),
339+
".": () =>
340+
player.current?.seekTo(player.current.getCurrentTime() + 0.04),
341+
};
342+
343+
if (actions[e.key]) actions[e.key]();
292344
};
293345

294346
document.addEventListener("keydown", handleKeyDown);
295347
return () => {
296348
document.removeEventListener("keydown", handleKeyDown);
297349
};
298-
}, []);
350+
}, [metadata, navigate, playQueue]);
299351

300352
return (
301353
<>
@@ -475,7 +527,7 @@ function Watch() {
475527
sx={{
476528
fontSize: "1vw",
477529
color: "#FFF",
478-
mt: "-0.75vw",
530+
mt: "-0.70vw",
479531
}}
480532
>
481533
{showmetadata?.childCount &&
@@ -487,7 +539,7 @@ function Watch() {
487539
fontSize: "1vw",
488540
fontWeight: "bold",
489541
color: "#FFF",
490-
mt: "10px",
542+
mt: "6px",
491543
}}
492544
>
493545
{metadata?.title}: EP. {metadata?.index}
@@ -499,7 +551,7 @@ function Watch() {
499551
flexDirection: "row",
500552
alignItems: "center",
501553
justifyContent: "flex-start",
502-
mt: 0.5,
554+
mt: "2px",
503555
gap: 1,
504556
}}
505557
>
@@ -565,6 +617,7 @@ function Watch() {
565617
sx={{
566618
fontSize: "0.75vw",
567619
color: "#FFF",
620+
mt: "2px",
568621
}}
569622
>
570623
{metadata?.summary}
@@ -1450,15 +1503,7 @@ function Watch() {
14501503
)}
14511504
</IconButton>
14521505

1453-
{playQueue && playQueue[1] && (
1454-
<IconButton
1455-
onClick={() => {
1456-
navigate(`/watch/${playQueue[1].ratingKey}`);
1457-
}}
1458-
>
1459-
<SkipNext fontSize="large" />
1460-
</IconButton>
1461-
)}
1506+
{playQueue && <NextEPButton queue={playQueue} />}
14621507
</Box>
14631508

14641509
{metadata.type === "movie" && (
@@ -1628,6 +1673,7 @@ function Watch() {
16281673
if (showError) return;
16291674

16301675
// filter out links from the error messages
1676+
if (!err.error) return;
16311677
const message = err.error.message.replace(
16321678
/https?:\/\/[^\s]+/g,
16331679
"Media"
@@ -1682,6 +1728,131 @@ function Watch() {
16821728
}
16831729

16841730
export default Watch;
1731+
1732+
function NextEPButton({ queue }: { queue?: Plex.Metadata[] }) {
1733+
const navigate = useNavigate();
1734+
1735+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
1736+
1737+
if (!queue) return <></>;
1738+
1739+
return (
1740+
<>
1741+
<Popper
1742+
open={Boolean(anchorEl)}
1743+
anchorEl={anchorEl}
1744+
placement="top-start"
1745+
transition
1746+
sx={{ zIndex: 10000 }}
1747+
modifiers={[
1748+
{
1749+
name: "offset",
1750+
options: {
1751+
offset: [0, 10],
1752+
},
1753+
},
1754+
]}
1755+
>
1756+
{({ TransitionProps }) => (
1757+
<Fade {...TransitionProps} timeout={350}>
1758+
<Paper
1759+
sx={{
1760+
width: "575px",
1761+
height: "160px",
1762+
overflow: "hidden",
1763+
background: "#101010",
1764+
1765+
display: "flex",
1766+
flexDirection: "row",
1767+
alignItems: "flex-start",
1768+
justifyContent: "flex-start",
1769+
}}
1770+
>
1771+
<img
1772+
src={`${getTranscodeImageURL(
1773+
`${queue[1].thumb}?X-Plex-Token=${localStorage.getItem(
1774+
"accessToken"
1775+
)}`,
1776+
500,
1777+
500
1778+
)}`}
1779+
alt=""
1780+
style={{
1781+
height: "100%",
1782+
aspectRatio: "16/9",
1783+
width: "auto",
1784+
}}
1785+
/>
1786+
1787+
<Box
1788+
sx={{
1789+
width: "100%",
1790+
height: "100%",
1791+
display: "flex",
1792+
flexDirection: "column",
1793+
alignItems: "flex-start",
1794+
justifyContent: "flex-start",
1795+
p: 2,
1796+
}}
1797+
>
1798+
<Typography
1799+
sx={{
1800+
fontSize: "12px",
1801+
fontWeight: "700",
1802+
letterSpacing: "0.15em",
1803+
color: "#e6a104",
1804+
textTransform: "uppercase",
1805+
}}
1806+
>
1807+
{queue[1].type}{" "}
1808+
{queue[1].type === "episode" && queue[1].index}
1809+
</Typography>
1810+
<Typography
1811+
sx={{
1812+
fontSize: "14px",
1813+
fontWeight: "bold",
1814+
color: "#FFF",
1815+
}}
1816+
>
1817+
{queue[1].title}
1818+
</Typography>
1819+
1820+
<Typography
1821+
sx={{
1822+
mt: "2px",
1823+
fontSize: "10px",
1824+
color: "#FFF",
1825+
1826+
// max 5 lines
1827+
display: "-webkit-box",
1828+
WebkitLineClamp: 5,
1829+
WebkitBoxOrient: "vertical",
1830+
overflow: "hidden",
1831+
textOverflow: "ellipsis",
1832+
}}
1833+
>
1834+
{queue[1].summary}
1835+
</Typography>
1836+
</Box>
1837+
</Paper>
1838+
</Fade>
1839+
)}
1840+
</Popper>
1841+
{queue && queue[1] && (
1842+
<IconButton
1843+
onClick={() => {
1844+
navigate(`/watch/${queue[1].ratingKey}`);
1845+
}}
1846+
onMouseEnter={(e) => setAnchorEl(e.currentTarget)}
1847+
onMouseLeave={() => setAnchorEl(null)}
1848+
>
1849+
<SkipNext fontSize="large" />
1850+
</IconButton>
1851+
)}
1852+
</>
1853+
);
1854+
}
1855+
16851856
function TuneSettingTab(
16861857
theme: Theme,
16871858
setTunePage: React.Dispatch<React.SetStateAction<number>>,

0 commit comments

Comments
 (0)