From 846c0d77d313597c1698a68346718d5c0f25506f Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 19 Oct 2022 13:24:35 +0900 Subject: [PATCH] Add mini cast player --- front/src/components/navbar.tsx | 2 +- front/src/components/poster.tsx | 17 ++-- front/src/pages/_app.tsx | 8 +- front/src/pages/browse/index.tsx | 13 ++- front/src/player/cast/cast-button.tsx | 3 +- front/src/player/cast/mini-player.tsx | 94 ++++++++++++++++++++ front/src/player/cast/state.tsx | 61 +++++++++++++ front/src/player/components/left-buttons.tsx | 13 +-- front/src/player/components/progress-bar.tsx | 5 +- 9 files changed, 190 insertions(+), 26 deletions(-) create mode 100644 front/src/player/cast/mini-player.tsx create mode 100644 front/src/player/cast/state.tsx diff --git a/front/src/components/navbar.tsx b/front/src/components/navbar.tsx index 331df71f..18f01984 100644 --- a/front/src/components/navbar.tsx +++ b/front/src/components/navbar.tsx @@ -77,7 +77,7 @@ export const Navbar = (barProps: AppBarProps) => { const { data, error, isSuccess, isError } = useFetch(Navbar.query()); return ( - + . */ -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>; type Height = ResponsiveStyleValue>; -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 + ); declare module "@mui/material/styles" { diff --git a/front/src/pages/_app.tsx b/front/src/pages/_app.tsx index 6f1faa1e..5d21036f 100755 --- a/front/src/pages/_app.tsx +++ b/front/src/pages/_app.tsx @@ -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) => { - {getLayout()} - {castEnabled && } + + {getLayout()} + {castEnabled && } + diff --git a/front/src/pages/browse/index.tsx b/front/src/pages/browse/index.tsx index 8c0211c0..4cdf79c2 100644 --- a/front/src/pages/browse/index.tsx +++ b/front/src/pages/browse/index.tsx @@ -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 ( - <> + -
{page}
- +
{page}
+
+ +
+
); }; diff --git a/front/src/player/cast/cast-button.tsx b/front/src/player/cast/cast-button.tsx index 8e14cf41..8cfae086 100644 --- a/front/src/player/cast/cast-button.tsx +++ b/front/src/player/cast/cast-button.tsx @@ -18,8 +18,7 @@ * along with Kyoo. If not, see . */ -import { Box, styled } from "@mui/material"; -import { BoxProps } from "@mui/system"; +import { styled } from "@mui/material"; import { ComponentProps, forwardRef } from "react"; diff --git a/front/src/player/cast/mini-player.tsx b/front/src/player/cast/mini-player.tsx new file mode 100644 index 00000000..341b5dc2 --- /dev/null +++ b/front/src/player/cast/mini-player.tsx @@ -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 . + */ + +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 ( + router.push("/remote")} */ + sx={{ height: "100px", display: "flex", justifyContent: "space-between" }} + > + + + + + + {name} + {episodeName} + + + + + + + *": { mx: "16px !important" }, + "> .desktop": { display: { xs: "none", md: "inline-flex" } }, + }} + > + + {previousSlug && ( + + + + + + )} + + setPlay(!isPlaying)} + aria-label={isPlaying ? t("pause") : t("play")} + > + {isPlaying ? : } + + + {nextSlug && ( + + + + + + )} + + + ); +}; diff --git a/front/src/player/cast/state.tsx b/front/src/player/cast/state.tsx new file mode 100644 index 00000000..9e0d3e63 --- /dev/null +++ b/front/src/player/cast/state.tsx @@ -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 . + */ + +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(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) => 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]); +}; diff --git a/front/src/player/components/left-buttons.tsx b/front/src/player/components/left-buttons.tsx index 10ada8d3..f147fcc8 100644 --- a/front/src/player/components/left-buttons.tsx +++ b/front/src/player/components/left-buttons.tsx @@ -18,7 +18,7 @@ * along with Kyoo. If not, see . */ -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 = ({ )} - + ); }; -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} > setMuted(!isMuted)} aria-label={t("mute")} - sx={{ color: "white" }} + sx={{ color: color }} > {isMuted || volume == 0 ? ( @@ -141,12 +142,12 @@ const VolumeSlider = () => { ); }; -const ProgressText = () => { +export const ProgressText = ({ sx }: { sx?: SxProps }) => { const progress = useAtomValue(progressAtom); const duration = useAtomValue(durationAtom); return ( - + {toTimerString(progress, duration)} : {toTimerString(duration)} ); diff --git a/front/src/player/components/progress-bar.tsx b/front/src/player/components/progress-bar.tsx index a3866661..887a86f7 100644 --- a/front/src/player/components/progress-bar.tsx +++ b/front/src/player/components/progress-bar.tsx @@ -18,13 +18,13 @@ * along with Kyoo. If not, see . */ -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(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 }} >