Add mini cast player

This commit is contained in:
Zoe Roux 2022-10-19 13:24:35 +09:00
parent 92a38d2c0a
commit 846c0d77d3
No known key found for this signature in database
GPG Key ID: B2AB52A2636E5C46
9 changed files with 190 additions and 26 deletions

View File

@ -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"

View File

@ -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" {

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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";

View 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>
);
};

View 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]);
};

View File

@ -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>
);

View File

@ -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