mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add mini player features
This commit is contained in:
parent
a978ed6aeb
commit
2c724eae5c
@ -19,57 +19,68 @@
|
||||
*/
|
||||
|
||||
export type Item = {
|
||||
/**
|
||||
* The slug of this episode.
|
||||
*/
|
||||
slug: string;
|
||||
/**
|
||||
* The title of the show containing this episode.
|
||||
*/
|
||||
showTitle?: string;
|
||||
/**
|
||||
* The slug of the show containing this episode
|
||||
*/
|
||||
showSlug?: string;
|
||||
/**
|
||||
* The season in witch this episode is in.
|
||||
*/
|
||||
seasonNumber?: number;
|
||||
/**
|
||||
* The number of this episode is it's season.
|
||||
*/
|
||||
episodeNumber?: number;
|
||||
/**
|
||||
* The absolute number of this episode. It's an episode number that is not reset to 1 after a new season.
|
||||
*/
|
||||
absoluteNumber?: number;
|
||||
/**
|
||||
* The title of this episode.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* true if this is a movie, false otherwise.
|
||||
*/
|
||||
isMovie: boolean;
|
||||
/**
|
||||
* An url to the poster of this resource. If this resource does not have an image, the link will be null. If the kyoo's instance is not capable of handling this kind of image for the specific resource, this field won't be present.
|
||||
*/
|
||||
poster?: string | null;
|
||||
/**
|
||||
* An url to the thumbnail of this resource. If this resource does not have an image, the link will be null. If the kyoo's instance is not capable of handling this kind of image for the specific resource, this field won't be present.
|
||||
*/
|
||||
thumbnail?: string | null;
|
||||
/**
|
||||
* An url to the logo of this resource. If this resource does not have an image, the link will be null. If the kyoo's instance is not capable of handling this kind of image for the specific resource, this field won't be present.
|
||||
*/
|
||||
logo?: string | null;
|
||||
/**
|
||||
* The slug of this episode.
|
||||
*/
|
||||
slug: string;
|
||||
/**
|
||||
* The title of the show containing this episode.
|
||||
*/
|
||||
showTitle?: string;
|
||||
/**
|
||||
* The slug of the show containing this episode
|
||||
*/
|
||||
showSlug?: string;
|
||||
/**
|
||||
* The season in witch this episode is in.
|
||||
*/
|
||||
seasonNumber?: number;
|
||||
/**
|
||||
* The number of this episode is it's season.
|
||||
*/
|
||||
episodeNumber?: number;
|
||||
/**
|
||||
* The absolute number of this episode. It's an episode number that is not reset to 1 after a
|
||||
* new season.
|
||||
*/
|
||||
absoluteNumber?: number;
|
||||
/**
|
||||
* The title of this episode.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The air date of this episode.
|
||||
*/
|
||||
releaseDate: Date;
|
||||
/**
|
||||
* True if this is a movie, false otherwise.
|
||||
*/
|
||||
isMovie: boolean;
|
||||
/**
|
||||
* An url to the poster of this resource. If this resource does not have an image, the link will
|
||||
* be null. If the kyoo's instance is not capable of handling this kind of image for the
|
||||
* specific resource, this field won't be present.
|
||||
*/
|
||||
poster?: string | null;
|
||||
/**
|
||||
* An url to the thumbnail of this resource. If this resource does not have an image, the link
|
||||
* will be null. If the kyoo's instance is not capable of handling this kind of image for the
|
||||
* specific resource, this field won't be present.
|
||||
*/
|
||||
thumbnail?: string | null;
|
||||
/**
|
||||
* An url to the logo of this resource. If this resource does not have an image, the link will
|
||||
* be null. If the kyoo's instance is not capable of handling this kind of image for the
|
||||
* specific resource, this field won't be present.
|
||||
*/
|
||||
logo?: string | null;
|
||||
/**
|
||||
* The links to the videos of this watch item.
|
||||
*/
|
||||
link: {
|
||||
direct: string,
|
||||
transmux: string,
|
||||
}
|
||||
direct: string;
|
||||
transmux: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const getItem = async (slug: string, apiUrl: string) => {
|
||||
@ -79,13 +90,38 @@ export const getItem = async (slug: string, apiUrl: string) => {
|
||||
console.error(await resp.text());
|
||||
return null;
|
||||
}
|
||||
const ret = await resp.json() as Item;
|
||||
const ret = (await resp.json()) as Item;
|
||||
if (!ret) return null;
|
||||
ret.name = (ret as any).title;
|
||||
ret.releaseDate = new Date(ret.releaseDate);
|
||||
ret.link.direct = `${apiUrl}/${ret.link.direct}`;
|
||||
ret.link.transmux = `${apiUrl}/${ret.link.transmux}`;
|
||||
ret.thumbnail = `${apiUrl}/${ret.thumbnail}`;
|
||||
ret.poster = `${apiUrl}/${ret.poster}`;
|
||||
ret.logo = `${apiUrl}/${ret.logo}`;
|
||||
return ret;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
console.error("Fetch error", e);
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const itemToTvMetadata = (item: Item) => {
|
||||
const metadata = new cast.framework.messages.TvShowMediaMetadata();
|
||||
metadata.title = item.name;
|
||||
metadata.season = item.seasonNumber;
|
||||
metadata.episode = item.episodeNumber;
|
||||
metadata.seriesTitle = item.showTitle;
|
||||
metadata.originalAirdate = item.releaseDate.toISOString().substring(0, 10);
|
||||
metadata.images = item.poster ? [new cast.framework.messages.Image(item.poster)] : [];
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export const itemToMovie = (item: Item) => {
|
||||
const metadata = new cast.framework.messages.MovieMediaMetadata();
|
||||
metadata.title = item.name;
|
||||
metadata.releaseDate = item.releaseDate.toISOString().substring(0, 10);
|
||||
metadata.images = item.poster ? [new cast.framework.messages.Image(item.poster)] : [];
|
||||
return metadata;
|
||||
}
|
||||
|
||||
|
@ -18,23 +18,44 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getItem } from "./api";
|
||||
import { getItem, itemToMovie, itemToTvMetadata } from "./api";
|
||||
const Command = cast.framework.messages.Command;
|
||||
|
||||
const context = cast.framework.CastReceiverContext.getInstance();
|
||||
const playerManager = context.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(cast.framework.messages.MessageType.LOAD, async (loadRequestData) => {
|
||||
if (loadRequestData.media.contentUrl && loadRequestData.media.metadata) return loadRequestData;
|
||||
playerManager.setSupportedMediaCommands(
|
||||
Command.PAUSE |
|
||||
Command.SEEK |
|
||||
Command.QUEUE_NEXT |
|
||||
Command.QUEUE_PREV |
|
||||
Command.EDIT_TRACKS |
|
||||
Command.STREAM_MUTE |
|
||||
Command.STREAM_VOLUME |
|
||||
Command.STREAM_TRANSFER,
|
||||
);
|
||||
|
||||
const item = await getItem(loadRequestData.media.contentId, loadRequestData.media.customData.serverUrl);
|
||||
if (!item) {
|
||||
return new cast.framework.messages.ErrorData(
|
||||
cast.framework.messages.ErrorType.LOAD_FAILED,
|
||||
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
cast.framework.messages.MessageType.LOAD,
|
||||
async (loadRequestData) => {
|
||||
if (loadRequestData.media.contentUrl && loadRequestData.media.metadata) return loadRequestData;
|
||||
|
||||
const item = await getItem(
|
||||
loadRequestData.media.contentId,
|
||||
loadRequestData.media.customData.serverUrl,
|
||||
);
|
||||
}
|
||||
loadRequestData.media.contentUrl = item.link.direct;
|
||||
loadRequestData.media.metadata = item;
|
||||
return loadRequestData;
|
||||
});
|
||||
if (!item) {
|
||||
return new cast.framework.messages.ErrorData(cast.framework.messages.ErrorType.LOAD_FAILED);
|
||||
}
|
||||
loadRequestData.media.contentUrl = item.link.direct;
|
||||
loadRequestData.media.metadata = item.isMovie
|
||||
? itemToMovie(item)
|
||||
: itemToTvMetadata(item);
|
||||
loadRequestData.media.customData = item;
|
||||
return loadRequestData;
|
||||
},
|
||||
);
|
||||
|
||||
context.start();
|
||||
|
@ -37,6 +37,7 @@ services:
|
||||
environment:
|
||||
- KYOO_URL=http://back:5000
|
||||
- NEXT_PUBLIC_BACK_URL=${PUBLIC_BACK_URL}
|
||||
- NEXT_PUBLIC_CAST_APPLICATION_ID=${CAST_APPLICATION_ID}
|
||||
ingress:
|
||||
image: nginx
|
||||
restart: on-failure
|
||||
|
@ -23,6 +23,7 @@ services:
|
||||
restart: on-failure
|
||||
environment:
|
||||
- KYOO_URL=http://back:5000
|
||||
- NEXT_PUBLIC_CAST_APPLICATION_ID=${CAST_APPLICATION_ID}
|
||||
ingress:
|
||||
image: nginx
|
||||
restart: on-failure
|
||||
@ -30,7 +31,6 @@ services:
|
||||
- PORT=8901
|
||||
- FRONT_URL=http://front:8901
|
||||
- BACK_URL=http://back:5000
|
||||
- CAST_APPLICATION_ID=${CAST_APPLICATION_ID}
|
||||
volumes:
|
||||
- ./nginx.conf.template:/etc/nginx/templates/kyoo.conf.template:ro
|
||||
depends_on:
|
||||
|
@ -34,9 +34,10 @@ export const CastProvider = () => {
|
||||
window.__onGCastApiAvailable = (isAvailable) => {
|
||||
if (!isAvailable) return;
|
||||
cast.framework.CastContext.getInstance().setOptions({
|
||||
receiverApplicationId: process.env.CAST_APPLICATION_ID,
|
||||
receiverApplicationId: process.env.NEXT_PUBLIC_CAST_APPLICATION_ID,
|
||||
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
|
||||
});
|
||||
setLoaded(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -45,7 +46,6 @@ export const CastProvider = () => {
|
||||
<Script
|
||||
src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
|
||||
strategy="lazyOnload"
|
||||
onReady={() => setLoaded(true)}
|
||||
/>
|
||||
{loaded && <CastController />}
|
||||
</>
|
||||
|
111
front/src/player/cast/internal-mini-player.tsx
Normal file
111
front/src/player/cast/internal-mini-player.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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 { useAtom, useAtomValue } from "jotai";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import { useRouter } from "next/router";
|
||||
import { episodeDisplayNumber } from "~/components/episode";
|
||||
import { Image } from "~/components/poster";
|
||||
import { ProgressText, VolumeSlider } from "~/player/components/left-buttons";
|
||||
import { ProgressBar } from "../components/progress-bar";
|
||||
import { durationAtom, mediaAtom, progressAtom, playAtom } from "./state";
|
||||
|
||||
export const _CastMiniPlayer = () => {
|
||||
const { t } = useTranslation("player");
|
||||
const router = useRouter();
|
||||
|
||||
const [media, setMedia] = useAtom(mediaAtom);
|
||||
const [isPlaying, togglePlay] = useAtom(playAtom);
|
||||
|
||||
if (!media) return null;
|
||||
|
||||
const previousSlug = "sng";
|
||||
const nextSlug = "toto";
|
||||
|
||||
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={media.thumbnail} alt="" height="100%" aspectRatio="16/9" />
|
||||
</Box>
|
||||
<Box>
|
||||
{!media.isMovie && (
|
||||
<Typography>{media.showTitle + " " + episodeDisplayNumber(media)}</Typography>
|
||||
)}
|
||||
<Typography>{media.name}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: { xs: "none", md: "flex" },
|
||||
alignItems: "center",
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
<ProgressBar
|
||||
progressAtom={progressAtom}
|
||||
durationAtom={durationAtom}
|
||||
sx={{ flexShrink: 1 }}
|
||||
/>
|
||||
<ProgressText
|
||||
sx={{ flexShrink: 0 }}
|
||||
progressAtom={progressAtom}
|
||||
durationAtom={durationAtom}
|
||||
/>
|
||||
</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={() => togglePlay()} 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>
|
||||
);
|
||||
};
|
@ -18,82 +18,18 @@
|
||||
* 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 { useAtom } from "jotai";
|
||||
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";
|
||||
import { mediaAtom } from "./state";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
export const connectedAtom = atom(false);
|
||||
|
||||
const _CastMiniPlayer = dynamic(() =>
|
||||
import("./internal-mini-player").then((x) => x._CastMiniPlayer),
|
||||
);
|
||||
|
||||
export const CastMiniPlayer = () => {
|
||||
const { t } = useTranslation("player");
|
||||
const router = useRouter();
|
||||
const isConnected = useAtomValue(connectedAtom);
|
||||
|
||||
const [media, setMedia] = useAtom(mediaAtom);
|
||||
console.log(media)
|
||||
|
||||
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>
|
||||
);
|
||||
if (!isConnected) return null;
|
||||
return <_CastMiniPlayer />;
|
||||
};
|
||||
|
@ -20,17 +20,10 @@
|
||||
|
||||
import { atom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { useEffect } from "react";
|
||||
import { WatchItem } from "~/models/resources/watch-item";
|
||||
import { bakedAtom } from "~/utils/jotai-utils";
|
||||
import { stopAtom, localMediaAtom } from "../state";
|
||||
|
||||
export type Media = {
|
||||
name: string;
|
||||
episodeName?: null;
|
||||
episodeNumber?: number;
|
||||
seasonNumber?: number;
|
||||
absoluteNumber?: string;
|
||||
thunbnail?: string;
|
||||
};
|
||||
import { connectedAtom } from "./mini-player";
|
||||
|
||||
const playerAtom = atom(() => {
|
||||
const player = new cast.framework.RemotePlayer();
|
||||
@ -40,25 +33,25 @@ const playerAtom = atom(() => {
|
||||
};
|
||||
});
|
||||
|
||||
export const [_playAtom, playAtom] = bakedAtom<boolean, never>(true, (get) => {
|
||||
export const [_playAtom, playAtom] = bakedAtom<boolean, undefined>(true, (get) => {
|
||||
const { controller } = get(playerAtom);
|
||||
controller.playOrPause();
|
||||
});
|
||||
export const [_durationAtom, durationAtom] = bakedAtom(1, (get, _, value) => {
|
||||
export const durationAtom = atom(0);
|
||||
export const [_progressAtom, progressAtom] = bakedAtom(1, (get, _, value) => {
|
||||
const { player, controller } = get(playerAtom);
|
||||
player.currentTime = value;
|
||||
controller.seek();
|
||||
});
|
||||
|
||||
export const [_mediaAtom, mediaAtom] = bakedAtom<Media | null, string>(
|
||||
export const [_mediaAtom, mediaAtom] = bakedAtom<WatchItem | null, string>(
|
||||
null,
|
||||
async (_, _2, value) => {
|
||||
const session = cast.framework.CastContext.getInstance().getCurrentSession();
|
||||
if (!session) return;
|
||||
const mediaInfo = new chrome.cast.media.MediaInfo(
|
||||
value, "application/json"
|
||||
);
|
||||
if (!process.env.NEXT_PUBLIC_BACK_URL) console.error("PUBLIC_BACK_URL is not defined. Chromecast won't work.");
|
||||
const mediaInfo = new chrome.cast.media.MediaInfo(value, "application/json");
|
||||
if (!process.env.NEXT_PUBLIC_BACK_URL)
|
||||
console.error("PUBLIC_BACK_URL is not defined. Chromecast won't work.");
|
||||
mediaInfo.customData = { serverUrl: process.env.NEXT_PUBLIC_BACK_URL };
|
||||
session.loadMedia(new chrome.cast.media.LoadRequest(mediaInfo));
|
||||
},
|
||||
@ -67,8 +60,10 @@ export const [_mediaAtom, mediaAtom] = bakedAtom<Media | null, string>(
|
||||
export const useCastController = () => {
|
||||
const { player, controller } = useAtomValue(playerAtom);
|
||||
const setPlay = useSetAtom(_playAtom);
|
||||
const setDuration = useSetAtom(_durationAtom);
|
||||
const setProgress = useSetAtom(_progressAtom);
|
||||
const setDuration = useSetAtom(durationAtom);
|
||||
const setMedia = useSetAtom(_mediaAtom);
|
||||
const setConnected = useSetAtom(connectedAtom);
|
||||
const loadMedia = useSetAtom(mediaAtom);
|
||||
const stopPlayer = useAtomValue(stopAtom);
|
||||
const localMedia = useAtomValue(localMediaAtom);
|
||||
@ -76,15 +71,27 @@ export const useCastController = () => {
|
||||
useEffect(() => {
|
||||
const context = cast.framework.CastContext.getInstance();
|
||||
|
||||
const session = cast.framework.CastContext.getInstance().getCurrentSession();
|
||||
if (session) {
|
||||
setConnected(true);
|
||||
setDuration(player.duration);
|
||||
setMedia(player.mediaInfo?.metadata);
|
||||
setPlay(!player.isPaused);
|
||||
}
|
||||
|
||||
const eventListeners: [
|
||||
cast.framework.RemotePlayerEventType,
|
||||
(event: cast.framework.RemotePlayerChangedEvent<any>) => void,
|
||||
][] = [
|
||||
[cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED, (event) => setPlay(event.value)],
|
||||
[cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED, (event) => setPlay(!event.value)],
|
||||
[
|
||||
cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED,
|
||||
(event) => setProgress(event.value),
|
||||
],
|
||||
[cast.framework.RemotePlayerEventType.DURATION_CHANGED, (event) => setDuration(event.value)],
|
||||
[
|
||||
cast.framework.RemotePlayerEventType.MEDIA_INFO_CHANGED,
|
||||
() => setMedia(player.mediaInfo?.metadata),
|
||||
() => setMedia(player.mediaInfo?.customData),
|
||||
],
|
||||
];
|
||||
|
||||
@ -92,16 +99,38 @@ export const useCastController = () => {
|
||||
if (event.sessionState === cast.framework.SessionState.SESSION_STARTED && localMedia) {
|
||||
stopPlayer[0]();
|
||||
loadMedia(localMedia);
|
||||
setConnected(true);
|
||||
} else if (event.sessionState === cast.framework.SessionState.SESSION_RESUMED) {
|
||||
setConnected(true);
|
||||
} else if (event.sessionState === cast.framework.SessionState.SESSION_ENDED) {
|
||||
setConnected(false);
|
||||
}
|
||||
};
|
||||
|
||||
context.addEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, sessionStateHandler);
|
||||
context.addEventListener(
|
||||
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
||||
sessionStateHandler,
|
||||
);
|
||||
for (const [key, handler] of eventListeners) controller.addEventListener(key, handler);
|
||||
return () => {
|
||||
context.removeEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, sessionStateHandler);
|
||||
context.removeEventListener(
|
||||
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
||||
sessionStateHandler,
|
||||
);
|
||||
for (const [key, handler] of eventListeners) controller.removeEventListener(key, handler);
|
||||
};
|
||||
}, [player, controller, setPlay, setDuration, setMedia, stopPlayer, localMedia, loadMedia]);
|
||||
}, [
|
||||
player,
|
||||
controller,
|
||||
setPlay,
|
||||
setDuration,
|
||||
setMedia,
|
||||
stopPlayer,
|
||||
localMedia,
|
||||
loadMedia,
|
||||
setConnected,
|
||||
setProgress,
|
||||
]);
|
||||
};
|
||||
|
||||
export const CastController = () => {
|
||||
|
@ -32,7 +32,7 @@ import useTranslation from "next-translate/useTranslation";
|
||||
import NextLink from "next/link";
|
||||
import { Poster } from "~/components/poster";
|
||||
import { WatchItem } from "~/models/resources/watch-item";
|
||||
import { loadAtom } from "../state";
|
||||
import { durationAtom, loadAtom, progressAtom } from "../state";
|
||||
import { episodeDisplayNumber } from "~/components/episode";
|
||||
import { LeftButtons } from "./left-buttons";
|
||||
import { RightButtons } from "./right-buttons";
|
||||
@ -76,7 +76,7 @@ export const Hover = ({
|
||||
{name ?? <Skeleton />}
|
||||
</Typography>
|
||||
|
||||
<ProgressBar chapters={data?.chapters} />
|
||||
<ProgressBar chapters={data?.chapters} progressAtom={progressAtom} durationAtom={durationAtom} />
|
||||
|
||||
<Box sx={{ display: "flex", flexDirection: "row", justifyContent: "space-between" }}>
|
||||
<LeftButtons
|
||||
|
@ -19,7 +19,7 @@
|
||||
*/
|
||||
|
||||
import { Box, IconButton, Slider, SxProps, Tooltip, Typography } from "@mui/material";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { Atom, useAtom, useAtomValue } from "jotai";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import { useRouter } from "next/router";
|
||||
import { durationAtom, mutedAtom, playAtom, progressAtom, volumeAtom } from "../state";
|
||||
@ -84,12 +84,12 @@ export const LeftButtons = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
<VolumeSlider color="white" />
|
||||
<ProgressText sx={{ color: "white" }} />
|
||||
<ProgressText sx={{ color: "white" }} progressAtom={progressAtom} durationAtom={durationAtom} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const VolumeSlider = ({ color, className }: { color?: string, className?: string }) => {
|
||||
export const VolumeSlider = ({ color, className }: { color?: string; className?: string }) => {
|
||||
const [volume, setVolume] = useAtom(volumeAtom);
|
||||
const [isMuted, setMuted] = useAtom(mutedAtom);
|
||||
const { t } = useTranslation("player");
|
||||
@ -105,11 +105,7 @@ export const VolumeSlider = ({ color, className }: { color?: string, className?:
|
||||
className={className}
|
||||
>
|
||||
<Tooltip title={t("mute")}>
|
||||
<IconButton
|
||||
onClick={() => setMuted(!isMuted)}
|
||||
aria-label={t("mute")}
|
||||
sx={{ color: color }}
|
||||
>
|
||||
<IconButton onClick={() => setMuted(!isMuted)} aria-label={t("mute")} sx={{ color: color }}>
|
||||
{isMuted || volume == 0 ? (
|
||||
<VolumeOff />
|
||||
) : volume < 25 ? (
|
||||
@ -142,7 +138,15 @@ export const VolumeSlider = ({ color, className }: { color?: string, className?:
|
||||
);
|
||||
};
|
||||
|
||||
export const ProgressText = ({ sx }: { sx?: SxProps }) => {
|
||||
export const ProgressText = ({
|
||||
sx,
|
||||
progressAtom,
|
||||
durationAtom,
|
||||
}: {
|
||||
sx?: SxProps;
|
||||
progressAtom: Atom<number>;
|
||||
durationAtom: Atom<number>;
|
||||
}) => {
|
||||
const progress = useAtomValue(progressAtom);
|
||||
const duration = useAtomValue(durationAtom);
|
||||
|
||||
|
@ -19,12 +19,22 @@
|
||||
*/
|
||||
|
||||
import { Box, SxProps } from "@mui/material";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { Atom, WritableAtom, useAtom, useAtomValue } from "jotai";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Chapter } from "~/models/resources/watch-item";
|
||||
import { bufferedAtom, durationAtom, progressAtom } from "../state";
|
||||
import { bufferedAtom } from "../state";
|
||||
|
||||
export const ProgressBar = ({ chapters, sx }: { chapters?: Chapter[], sx?: SxProps }) => {
|
||||
export const ProgressBar = ({
|
||||
progressAtom,
|
||||
durationAtom,
|
||||
chapters,
|
||||
sx,
|
||||
}: {
|
||||
progressAtom: WritableAtom<number, number>;
|
||||
durationAtom: Atom<number>;
|
||||
chapters?: Chapter[];
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [isSeeking, setSeek] = useState(false);
|
||||
const [progress, setProgress] = useAtom(progressAtom);
|
||||
@ -75,7 +85,7 @@ export const ProgressBar = ({ chapters, sx }: { chapters?: Chapter[], sx?: SxPro
|
||||
".thumb": { opacity: 1 },
|
||||
".bar": { transform: "unset" },
|
||||
},
|
||||
...sx
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
|
Loading…
x
Reference in New Issue
Block a user