mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Add mini cast player
This commit is contained in:
parent
92a38d2c0a
commit
846c0d77d3
@ -77,7 +77,7 @@ export const Navbar = (barProps: AppBarProps) => {
|
|||||||
const { data, error, isSuccess, isError } = useFetch(Navbar.query());
|
const { data, error, isSuccess, isError } = useFetch(Navbar.query());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position="sticky" {...barProps}>
|
<AppBar position="relative" {...barProps}>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="large"
|
size="large"
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Box, Skeleton, styled } from "@mui/material";
|
import { Box, Skeleton, SxProps } from "@mui/material";
|
||||||
import { SyntheticEvent, useEffect, useLayoutEffect, useRef, useState } from "react";
|
import { SyntheticEvent, useLayoutEffect, useRef, useState } from "react";
|
||||||
import { ComponentsOverrides, ComponentsProps, ComponentsVariants } from "@mui/material";
|
import { ComponentsOverrides, ComponentsProps, ComponentsVariants } from "@mui/material";
|
||||||
import { withThemeProps } from "~/utils/with-theme";
|
import { withThemeProps } from "~/utils/with-theme";
|
||||||
import type { Property } from "csstype";
|
import type { Property } from "csstype";
|
||||||
@ -42,7 +42,7 @@ type ImagePropsWithLoading =
|
|||||||
type Width = ResponsiveStyleValue<Property.Width<(string & {}) | 0>>;
|
type Width = ResponsiveStyleValue<Property.Width<(string & {}) | 0>>;
|
||||||
type Height = ResponsiveStyleValue<Property.Height<(string & {}) | 0>>;
|
type Height = ResponsiveStyleValue<Property.Height<(string & {}) | 0>>;
|
||||||
|
|
||||||
const _Image = ({
|
export const Image = ({
|
||||||
img,
|
img,
|
||||||
alt,
|
alt,
|
||||||
radius,
|
radius,
|
||||||
@ -51,9 +51,9 @@ const _Image = ({
|
|||||||
aspectRatio = undefined,
|
aspectRatio = undefined,
|
||||||
width = undefined,
|
width = undefined,
|
||||||
height = undefined,
|
height = undefined,
|
||||||
|
sx,
|
||||||
...others
|
...others
|
||||||
}: ImagePropsWithLoading &
|
}: ImagePropsWithLoading & { sx?: SxProps } & (
|
||||||
(
|
|
||||||
| { aspectRatio?: string; width: Width; height: Height }
|
| { aspectRatio?: string; width: Width; height: Height }
|
||||||
| { aspectRatio: string; width?: Width; height?: Height }
|
| { aspectRatio: string; width?: Width; height?: Height }
|
||||||
)) => {
|
)) => {
|
||||||
@ -76,6 +76,7 @@ const _Image = ({
|
|||||||
height,
|
height,
|
||||||
backgroundColor: "grey.300",
|
backgroundColor: "grey.300",
|
||||||
"& > *": { width: "100%", height: "100%" },
|
"& > *": { width: "100%", height: "100%" },
|
||||||
|
...sx,
|
||||||
}}
|
}}
|
||||||
{...others}
|
{...others}
|
||||||
>
|
>
|
||||||
@ -98,11 +99,9 @@ const _Image = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Image = styled(_Image)({});
|
|
||||||
|
|
||||||
// eslint-disable-next-line jsx-a11y/alt-text
|
|
||||||
const _Poster = (props: ImagePropsWithLoading & { width?: Width; height?: Height }) => (
|
const _Poster = (props: ImagePropsWithLoading & { width?: Width; height?: Height }) => (
|
||||||
<_Image aspectRatio="2 / 3" {...props} />
|
// eslint-disable-next-line jsx-a11y/alt-text
|
||||||
|
<Image aspectRatio="2 / 3" {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
declare module "@mui/material/styles" {
|
declare module "@mui/material/styles" {
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import appWithI18n from "next-translate/appWithI18n";
|
import appWithI18n from "next-translate/appWithI18n";
|
||||||
import { ThemeProvider } from "@mui/material";
|
import { Box, ThemeProvider } from "@mui/material";
|
||||||
import NextApp, { AppContext } from "next/app";
|
import NextApp, { AppContext } from "next/app";
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import { Hydrate, QueryClientProvider } from "react-query";
|
import { Hydrate, QueryClientProvider } from "react-query";
|
||||||
@ -73,8 +73,10 @@ const App = ({ Component, pageProps }: AppProps) => {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Hydrate state={queryState}>
|
<Hydrate state={queryState}>
|
||||||
<ThemeProvider theme={defaultTheme}>
|
<ThemeProvider theme={defaultTheme}>
|
||||||
{getLayout(<Component {...props} />)}
|
<Box >
|
||||||
{castEnabled && <CastProvider />}
|
{getLayout(<Component {...props} />)}
|
||||||
|
{castEnabled && <CastProvider />}
|
||||||
|
</Box>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Hydrate>
|
</Hydrate>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
@ -44,6 +44,8 @@ import { InfiniteScroll } from "~/utils/infinite-scroll";
|
|||||||
import { Link } from "~/utils/link";
|
import { Link } from "~/utils/link";
|
||||||
import { withRoute } from "~/utils/router";
|
import { withRoute } from "~/utils/router";
|
||||||
import { QueryIdentifier, QueryPage, useInfiniteFetch } from "~/utils/query";
|
import { QueryIdentifier, QueryPage, useInfiniteFetch } from "~/utils/query";
|
||||||
|
import { CastMiniPlayer } from "~/player/cast/mini-player";
|
||||||
|
import { styled } from "@mui/system";
|
||||||
|
|
||||||
enum SortBy {
|
enum SortBy {
|
||||||
Name = "name",
|
Name = "name",
|
||||||
@ -422,12 +424,17 @@ const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Main = styled("main")({});
|
||||||
|
|
||||||
BrowsePage.getLayout = (page) => {
|
BrowsePage.getLayout = (page) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<Box sx={{ display: "flex", flexDirection: "column", height: "100vh" }}>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<main>{page}</main>
|
<Main sx={{ flexGrow: 1, flexShrink: 1, overflow: "auto" }}>{page}</Main>
|
||||||
</>
|
<footer>
|
||||||
|
<CastMiniPlayer />
|
||||||
|
</footer>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,8 +18,7 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Box, styled } from "@mui/material";
|
import { styled } from "@mui/material";
|
||||||
import { BoxProps } from "@mui/system";
|
|
||||||
import { ComponentProps, forwardRef } from "react";
|
import { ComponentProps, forwardRef } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
94
front/src/player/cast/mini-player.tsx
Normal file
94
front/src/player/cast/mini-player.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* Kyoo - A portable and vast media library solution.
|
||||||
|
* Copyright (c) Kyoo.
|
||||||
|
*
|
||||||
|
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
*
|
||||||
|
* Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* Kyoo is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Pause, PlayArrow, SkipNext, SkipPrevious } from "@mui/icons-material";
|
||||||
|
import { Box, IconButton, Paper, Tooltip, Typography } from "@mui/material";
|
||||||
|
import useTranslation from "next-translate/useTranslation";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { Image } from "~/components/poster";
|
||||||
|
import { ProgressText, VolumeSlider } from "~/player/components/left-buttons";
|
||||||
|
import { ProgressBar } from "../components/progress-bar";
|
||||||
|
|
||||||
|
export const CastMiniPlayer = () => {
|
||||||
|
const { t } = useTranslation("player");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const name = "Ansatsu Kyoushitsu";
|
||||||
|
const episodeName = "S1:E1 Assassination Time";
|
||||||
|
const thumbnail = "/api/show/ansatsu-kyoushitsu/thumbnail";
|
||||||
|
const previousSlug = "sng";
|
||||||
|
const nextSlug = "toto";
|
||||||
|
const isPlaying = true;
|
||||||
|
const setPlay = (bool: boolean) => {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
elevation={16}
|
||||||
|
/* onClick={() => router.push("/remote")} */
|
||||||
|
sx={{ height: "100px", display: "flex", justifyContent: "space-between" }}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<Box sx={{ height: "100%", p: 2, boxSizing: "border-box" }}>
|
||||||
|
<Image img={thumbnail} alt="" height="100%" aspectRatio="16/9" />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography>{name}</Typography>
|
||||||
|
<Typography>{episodeName}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: { xs: "none", md: "flex" }, alignItems: "center", flexGrow: 1, flexShrink: 1 }}>
|
||||||
|
<ProgressBar sx={{ flexShrink: 1 }} />
|
||||||
|
<ProgressText sx={{ flexShrink: 0 }} />
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
"> *": { mx: "16px !important" },
|
||||||
|
"> .desktop": { display: { xs: "none", md: "inline-flex" } },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VolumeSlider className="desktop" />
|
||||||
|
{previousSlug && (
|
||||||
|
<Tooltip title={t("previous")} className="desktop">
|
||||||
|
<IconButton aria-label={t("previous")}>
|
||||||
|
<SkipPrevious />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip title={isPlaying ? t("pause") : t("play")}>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setPlay(!isPlaying)}
|
||||||
|
aria-label={isPlaying ? t("pause") : t("play")}
|
||||||
|
>
|
||||||
|
{isPlaying ? <Pause /> : <PlayArrow />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{nextSlug && (
|
||||||
|
<Tooltip title={t("next")}>
|
||||||
|
<IconButton aria-label={t("next")}>
|
||||||
|
<SkipNext />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
61
front/src/player/cast/state.tsx
Normal file
61
front/src/player/cast/state.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Kyoo - A portable and vast media library solution.
|
||||||
|
* Copyright (c) Kyoo.
|
||||||
|
*
|
||||||
|
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
*
|
||||||
|
* Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* Kyoo is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { atom, useAtomValue, useSetAtom } from "jotai";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { bakedAtom } from "~/utils/jotai-utils";
|
||||||
|
|
||||||
|
const playerAtom = atom(() => {
|
||||||
|
const player = new cast.framework.RemotePlayer();
|
||||||
|
return {
|
||||||
|
player,
|
||||||
|
controller: new cast.framework.RemotePlayerController(player),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const [_playAtom, playAtom] = bakedAtom<boolean, never>(true, (get) => {
|
||||||
|
const {controller} = get(playerAtom);
|
||||||
|
controller.playOrPause();
|
||||||
|
});
|
||||||
|
export const [_durationAtom, durationAtom] = bakedAtom(1, (get, _, value) => {
|
||||||
|
const {controller} = get(playerAtom);
|
||||||
|
controller.seek()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useCastController = () => {
|
||||||
|
const { player, controller } = useAtomValue(playerAtom);
|
||||||
|
const setPlay = useSetAtom(_playAtom);
|
||||||
|
const setDuration = useSetAtom(_durationAtom);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const eventListeners: [
|
||||||
|
cast.framework.RemotePlayerEventType,
|
||||||
|
(event: cast.framework.RemotePlayerChangedEvent<any>) => void,
|
||||||
|
][] = [
|
||||||
|
[cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED, (event) => setPlay(event.value)],
|
||||||
|
[cast.framework.RemotePlayerEventType.DURATION_CHANGED, (event) => setDuration(event.value)],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [key, handler] of eventListeners) controller.addEventListener(key, handler);
|
||||||
|
return () => {
|
||||||
|
for (const [key, handler] of eventListeners) controller.removeEventListener(key, handler);
|
||||||
|
};
|
||||||
|
}, [player, controller, setPlay]);
|
||||||
|
};
|
@ -18,7 +18,7 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Box, IconButton, Slider, Tooltip, Typography } from "@mui/material";
|
import { Box, IconButton, Slider, SxProps, Tooltip, Typography } from "@mui/material";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import useTranslation from "next-translate/useTranslation";
|
import useTranslation from "next-translate/useTranslation";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@ -83,13 +83,13 @@ export const LeftButtons = ({
|
|||||||
</NextLink>
|
</NextLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<VolumeSlider />
|
<VolumeSlider color="white" />
|
||||||
<ProgressText />
|
<ProgressText />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const VolumeSlider = () => {
|
export const VolumeSlider = ({ color, className }: { color?: string, className?: string }) => {
|
||||||
const [volume, setVolume] = useAtom(volumeAtom);
|
const [volume, setVolume] = useAtom(volumeAtom);
|
||||||
const [isMuted, setMuted] = useAtom(mutedAtom);
|
const [isMuted, setMuted] = useAtom(mutedAtom);
|
||||||
const { t } = useTranslation("player");
|
const { t } = useTranslation("player");
|
||||||
@ -102,12 +102,13 @@ const VolumeSlider = () => {
|
|||||||
p: "8px",
|
p: "8px",
|
||||||
"body.hoverEnabled &:hover .slider": { width: "100px", px: "16px" },
|
"body.hoverEnabled &:hover .slider": { width: "100px", px: "16px" },
|
||||||
}}
|
}}
|
||||||
|
className={className}
|
||||||
>
|
>
|
||||||
<Tooltip title={t("mute")}>
|
<Tooltip title={t("mute")}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => setMuted(!isMuted)}
|
onClick={() => setMuted(!isMuted)}
|
||||||
aria-label={t("mute")}
|
aria-label={t("mute")}
|
||||||
sx={{ color: "white" }}
|
sx={{ color: color }}
|
||||||
>
|
>
|
||||||
{isMuted || volume == 0 ? (
|
{isMuted || volume == 0 ? (
|
||||||
<VolumeOff />
|
<VolumeOff />
|
||||||
@ -141,12 +142,12 @@ const VolumeSlider = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProgressText = () => {
|
export const ProgressText = ({ sx }: { sx?: SxProps }) => {
|
||||||
const progress = useAtomValue(progressAtom);
|
const progress = useAtomValue(progressAtom);
|
||||||
const duration = useAtomValue(durationAtom);
|
const duration = useAtomValue(durationAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Typography color="white" sx={{ alignSelf: "center" }}>
|
<Typography sx={{ alignSelf: "center", ...sx }}>
|
||||||
{toTimerString(progress, duration)} : {toTimerString(duration)}
|
{toTimerString(progress, duration)} : {toTimerString(duration)}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
|
@ -18,13 +18,13 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Box } from "@mui/material";
|
import { Box, SxProps } from "@mui/material";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Chapter } from "~/models/resources/watch-item";
|
import { Chapter } from "~/models/resources/watch-item";
|
||||||
import { bufferedAtom, durationAtom, progressAtom } from "../state";
|
import { bufferedAtom, durationAtom, progressAtom } from "../state";
|
||||||
|
|
||||||
export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
export const ProgressBar = ({ chapters, sx }: { chapters?: Chapter[], sx?: SxProps }) => {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [isSeeking, setSeek] = useState(false);
|
const [isSeeking, setSeek] = useState(false);
|
||||||
const [progress, setProgress] = useAtom(progressAtom);
|
const [progress, setProgress] = useAtom(progressAtom);
|
||||||
@ -75,6 +75,7 @@ export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
|||||||
".thumb": { opacity: 1 },
|
".thumb": { opacity: 1 },
|
||||||
".bar": { transform: "unset" },
|
".bar": { transform: "unset" },
|
||||||
},
|
},
|
||||||
|
...sx
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
Loading…
x
Reference in New Issue
Block a user