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());
|
||||
|
||||
return (
|
||||
<AppBar position="sticky" {...barProps}>
|
||||
<AppBar position="relative" {...barProps}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
size="large"
|
||||
|
@ -18,8 +18,8 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Box, Skeleton, styled } from "@mui/material";
|
||||
import { SyntheticEvent, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { Box, Skeleton, SxProps } from "@mui/material";
|
||||
import { SyntheticEvent, useLayoutEffect, useRef, useState } from "react";
|
||||
import { ComponentsOverrides, ComponentsProps, ComponentsVariants } from "@mui/material";
|
||||
import { withThemeProps } from "~/utils/with-theme";
|
||||
import type { Property } from "csstype";
|
||||
@ -42,7 +42,7 @@ type ImagePropsWithLoading =
|
||||
type Width = ResponsiveStyleValue<Property.Width<(string & {}) | 0>>;
|
||||
type Height = ResponsiveStyleValue<Property.Height<(string & {}) | 0>>;
|
||||
|
||||
const _Image = ({
|
||||
export const Image = ({
|
||||
img,
|
||||
alt,
|
||||
radius,
|
||||
@ -51,9 +51,9 @@ const _Image = ({
|
||||
aspectRatio = undefined,
|
||||
width = undefined,
|
||||
height = undefined,
|
||||
sx,
|
||||
...others
|
||||
}: ImagePropsWithLoading &
|
||||
(
|
||||
}: ImagePropsWithLoading & { sx?: SxProps } & (
|
||||
| { aspectRatio?: string; width: Width; height: Height }
|
||||
| { aspectRatio: string; width?: Width; height?: Height }
|
||||
)) => {
|
||||
@ -76,6 +76,7 @@ const _Image = ({
|
||||
height,
|
||||
backgroundColor: "grey.300",
|
||||
"& > *": { width: "100%", height: "100%" },
|
||||
...sx,
|
||||
}}
|
||||
{...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 }) => (
|
||||
<_Image aspectRatio="2 / 3" {...props} />
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
<Image aspectRatio="2 / 3" {...props} />
|
||||
);
|
||||
|
||||
declare module "@mui/material/styles" {
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
import React, { useState } from "react";
|
||||
import appWithI18n from "next-translate/appWithI18n";
|
||||
import { ThemeProvider } from "@mui/material";
|
||||
import { Box, ThemeProvider } from "@mui/material";
|
||||
import NextApp, { AppContext } from "next/app";
|
||||
import type { AppProps } from "next/app";
|
||||
import { Hydrate, QueryClientProvider } from "react-query";
|
||||
@ -73,8 +73,10 @@ const App = ({ Component, pageProps }: AppProps) => {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Hydrate state={queryState}>
|
||||
<ThemeProvider theme={defaultTheme}>
|
||||
<Box >
|
||||
{getLayout(<Component {...props} />)}
|
||||
{castEnabled && <CastProvider />}
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
</Hydrate>
|
||||
</QueryClientProvider>
|
||||
|
@ -44,6 +44,8 @@ import { InfiniteScroll } from "~/utils/infinite-scroll";
|
||||
import { Link } from "~/utils/link";
|
||||
import { withRoute } from "~/utils/router";
|
||||
import { QueryIdentifier, QueryPage, useInfiniteFetch } from "~/utils/query";
|
||||
import { CastMiniPlayer } from "~/player/cast/mini-player";
|
||||
import { styled } from "@mui/system";
|
||||
|
||||
enum SortBy {
|
||||
Name = "name",
|
||||
@ -422,12 +424,17 @@ const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Main = styled("main")({});
|
||||
|
||||
BrowsePage.getLayout = (page) => {
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", height: "100vh" }}>
|
||||
<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/>.
|
||||
*/
|
||||
|
||||
import { Box, styled } from "@mui/material";
|
||||
import { BoxProps } from "@mui/system";
|
||||
import { styled } from "@mui/material";
|
||||
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/>.
|
||||
*/
|
||||
|
||||
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 useTranslation from "next-translate/useTranslation";
|
||||
import { useRouter } from "next/router";
|
||||
@ -83,13 +83,13 @@ export const LeftButtons = ({
|
||||
</NextLink>
|
||||
</Tooltip>
|
||||
)}
|
||||
<VolumeSlider />
|
||||
<VolumeSlider color="white" />
|
||||
<ProgressText />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const VolumeSlider = () => {
|
||||
export const VolumeSlider = ({ color, className }: { color?: string, className?: string }) => {
|
||||
const [volume, setVolume] = useAtom(volumeAtom);
|
||||
const [isMuted, setMuted] = useAtom(mutedAtom);
|
||||
const { t } = useTranslation("player");
|
||||
@ -102,12 +102,13 @@ const VolumeSlider = () => {
|
||||
p: "8px",
|
||||
"body.hoverEnabled &:hover .slider": { width: "100px", px: "16px" },
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
<Tooltip title={t("mute")}>
|
||||
<IconButton
|
||||
onClick={() => setMuted(!isMuted)}
|
||||
aria-label={t("mute")}
|
||||
sx={{ color: "white" }}
|
||||
sx={{ color: color }}
|
||||
>
|
||||
{isMuted || volume == 0 ? (
|
||||
<VolumeOff />
|
||||
@ -141,12 +142,12 @@ const VolumeSlider = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ProgressText = () => {
|
||||
export const ProgressText = ({ sx }: { sx?: SxProps }) => {
|
||||
const progress = useAtomValue(progressAtom);
|
||||
const duration = useAtomValue(durationAtom);
|
||||
|
||||
return (
|
||||
<Typography color="white" sx={{ alignSelf: "center" }}>
|
||||
<Typography sx={{ alignSelf: "center", ...sx }}>
|
||||
{toTimerString(progress, duration)} : {toTimerString(duration)}
|
||||
</Typography>
|
||||
);
|
||||
|
@ -18,13 +18,13 @@
|
||||
* 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 { useEffect, useRef, useState } from "react";
|
||||
import { Chapter } from "~/models/resources/watch-item";
|
||||
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 [isSeeking, setSeek] = useState(false);
|
||||
const [progress, setProgress] = useAtom(progressAtom);
|
||||
@ -75,6 +75,7 @@ export const ProgressBar = ({ chapters }: { chapters?: Chapter[] }) => {
|
||||
".thumb": { opacity: 1 },
|
||||
".bar": { transform: "unset" },
|
||||
},
|
||||
...sx
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
|
Loading…
x
Reference in New Issue
Block a user