mirror of
https://github.com/Radiquum/AniX.git
synced 2025-04-19 16:04:41 +00:00
feat: add a menu for selecting voiceover, source and episode inside of a player
This commit is contained in:
parent
0a5b8a59e6
commit
5264534693
5 changed files with 266 additions and 142 deletions
|
@ -0,0 +1,131 @@
|
|||
"use client";
|
||||
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useEffect, useState } from "react";
|
||||
import { _fetchAPI } from "./PlayerParsing";
|
||||
|
||||
import { Voiceover } from "./VoiceoverSelectorMenu";
|
||||
import { Source } from "./SourceSelectorMenu";
|
||||
import { getAnonEpisodesWatched } from "./ReleasePlayer";
|
||||
|
||||
export interface Episode {
|
||||
position: number;
|
||||
name: string;
|
||||
is_watched: boolean;
|
||||
}
|
||||
interface EpisodeSelectorMenuProps {
|
||||
release_id: number;
|
||||
voiceover: Voiceover;
|
||||
source: Source;
|
||||
token: string | null;
|
||||
setEpisode: (state) => void;
|
||||
episode: Episode;
|
||||
episodeList: Episode[];
|
||||
setPlayerError: (state) => void;
|
||||
}
|
||||
|
||||
export const EpisodeSelectorMenu = ({
|
||||
release_id,
|
||||
token,
|
||||
voiceover,
|
||||
source,
|
||||
setEpisode,
|
||||
episode,
|
||||
episodeList,
|
||||
setPlayerError,
|
||||
}: EpisodeSelectorMenuProps) => {
|
||||
const [watchedEpisodes, setWatchedEpisodes] = useState([]);
|
||||
useEffect(() => {
|
||||
const __getInfo = async () => {
|
||||
let url = `${ENDPOINTS.release.episode}/${release_id}/${voiceover.id}/${source.id}`;
|
||||
if (token) {
|
||||
url += `?token=${token}`;
|
||||
}
|
||||
const episodes = await _fetchAPI(
|
||||
url,
|
||||
"Не удалось получить информацию о эпизодах",
|
||||
setPlayerError
|
||||
);
|
||||
if (episodes) {
|
||||
let anonEpisodesWatched = getAnonEpisodesWatched(
|
||||
release_id,
|
||||
source.id,
|
||||
voiceover.id
|
||||
);
|
||||
let lastEpisodeWatched = Math.max.apply(
|
||||
0,
|
||||
Object.keys(anonEpisodesWatched[release_id][source.id][voiceover.id])
|
||||
);
|
||||
let selectedEpisode =
|
||||
episodes.episodes.find(
|
||||
(episode: Episode) => episode.position == lastEpisodeWatched
|
||||
) || episodes.episodes[0];
|
||||
|
||||
setEpisode({
|
||||
selected: selectedEpisode,
|
||||
available: episodes.episodes,
|
||||
});
|
||||
}
|
||||
};
|
||||
if (source) {
|
||||
__getInfo();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [source]);
|
||||
|
||||
useEffect(() => {
|
||||
if (release_id && source && voiceover) {
|
||||
const anonEpisodesWatched = getAnonEpisodesWatched(
|
||||
release_id,
|
||||
source.id,
|
||||
voiceover.id
|
||||
);
|
||||
setWatchedEpisodes(
|
||||
anonEpisodesWatched[release_id][source.id][voiceover.id]
|
||||
);
|
||||
}
|
||||
}, [release_id, source, voiceover]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-start gap-4">
|
||||
<p className="text-[20px] px-2 pt-2 pb-1 font-bold">Эпизод</p>
|
||||
<div className="max-h-full flex flex-col gap-4 items-start justify-start overflow-x-hidden overflow-y-auto px-2 pb-2 scrollbar-thin scrollbar-thumb-[rgb(60_60_60_/_.8)] scrollbar-track-[rgb(30_30_30_/_.8)]">
|
||||
{episodeList && episodeList.length > 0 ?
|
||||
episodeList.map((epis: Episode) => {
|
||||
return (
|
||||
<button
|
||||
key={`release-${release_id}-voiceover-${voiceover.id}-source-${source.id}-episode-${epis.position}`}
|
||||
className={`h-fit px-2 justify-start items-start ${episode.position == epis.position ? "text-white" : "text-gray-400 hover:text-gray-100"} transition-colors`}
|
||||
onClick={() => {
|
||||
setEpisode({
|
||||
selected: epis,
|
||||
available: episodeList,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 min-w-32">
|
||||
<p className="text-[16px] leading-none whitespace-nowrap">
|
||||
{epis.name ?
|
||||
epis.name
|
||||
: ["Sibnet"].includes(source.name) ?
|
||||
`${epis.position + 1} Серия`
|
||||
: `${epis.position} Серия`}
|
||||
</p>
|
||||
{(
|
||||
epis.is_watched ||
|
||||
Object.keys(watchedEpisodes).includes(
|
||||
epis.position.toString()
|
||||
)
|
||||
) ?
|
||||
<span className="w-4 h-4 ml-2 iconify material-symbols--check-circle"></span>
|
||||
: <span className="w-4 h-4 ml-2 iconify material-symbols--check-circle-outline"></span>
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
--media-primary-color: #fff;
|
||||
--media-secondary-color: transparent;
|
||||
--media-menu-background: rgba(28, 28, 28, 0.6);
|
||||
--media-menu-background: rgba(28, 28, 28, 0.8);
|
||||
--media-text-color: var(--_primary-color);
|
||||
--media-control-hover-background: var(--media-secondary-color);
|
||||
|
||||
|
@ -116,11 +116,12 @@
|
|||
--media-settings-menu-min-width: calc(10 * var(--base));
|
||||
--media-menu-transform-in: translateY(0) scale(1);
|
||||
--media-menu-transform-out: translateY(20px) rotate(3deg) scale(1);
|
||||
background: rgb(30 30 30 / .6);
|
||||
background: rgba(28, 28, 28, 0.8);
|
||||
min-width: var(--media-settings-menu-min-width, 170px);
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: calc(3 * var(--base));
|
||||
padding: 0;
|
||||
padding-block: calc(0.15 * var(--base));
|
||||
padding-inline: calc(0.6 * var(--base));
|
||||
margin-right: 10px;
|
||||
|
@ -131,6 +132,10 @@
|
|||
max-height: 50%;
|
||||
}
|
||||
|
||||
.media-controller media-chrome-dialog > div {
|
||||
word-wrap: normal !important;
|
||||
}
|
||||
|
||||
.media-settings-menu[hidden] {
|
||||
display: block;
|
||||
visibility: visible;
|
||||
|
|
|
@ -43,6 +43,12 @@ import {
|
|||
} from "media-chrome/react/menu";
|
||||
import { VoiceoverSelectorMenu } from "./VoiceoverSelectorMenu";
|
||||
import { SourceSelectorMenu } from "./SourceSelectorMenu";
|
||||
import {
|
||||
_fetchAnilibriaManifest,
|
||||
_fetchKodikManifest,
|
||||
_fetchSibnetManifest,
|
||||
} from "./PlayerParsing";
|
||||
import { EpisodeSelectorMenu } from "./EpisodeSelectorMenu";
|
||||
|
||||
export const ReleasePlayerCustom = (props: {
|
||||
id: number;
|
||||
|
@ -60,120 +66,87 @@ export const ReleasePlayerCustom = (props: {
|
|||
selected: null,
|
||||
available: null,
|
||||
});
|
||||
const [playerProps, SetPlayerProps] = useState({
|
||||
const [playerProps, SetPlayerProps] = useState<{
|
||||
src: string | null;
|
||||
poster: string | null;
|
||||
type: "hls" | "mp4" | null;
|
||||
}>({
|
||||
src: null,
|
||||
poster: null,
|
||||
type: null,
|
||||
useCustom: false,
|
||||
});
|
||||
const [playerError, setPlayerError] = useState(null);
|
||||
// const [playbackRate, setPlaybackRate] = useState(1);
|
||||
const [playbackRate, setPlaybackRate] = useState(1);
|
||||
// const [isErrorDetailsOpen, setIsErrorDetailsOpen] = useState(false);
|
||||
// const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// useEffect(() => {
|
||||
// const __getInfo = async () => {
|
||||
// let url = `${ENDPOINTS.release.episode}/${props.id}/${voiceover.selected.id}/${source.selected.id}`;
|
||||
// if (props.token) {
|
||||
// url += `?token=${props.token}`;
|
||||
// }
|
||||
// const episodes = await _fetchAPI(
|
||||
// url,
|
||||
// "Не удалось получить информацию о эпизодах"
|
||||
// );
|
||||
// if (episodes) {
|
||||
// let anonEpisodesWatched = getAnonEpisodesWatched(
|
||||
// props.id,
|
||||
// source.selected.id,
|
||||
// voiceover.selected.id
|
||||
// );
|
||||
// let lastEpisodeWatched = Math.max.apply(
|
||||
// 0,
|
||||
// Object.keys(
|
||||
// anonEpisodesWatched[props.id][source.selected.id][
|
||||
// voiceover.selected.id
|
||||
// ]
|
||||
// )
|
||||
// );
|
||||
// let selectedEpisode =
|
||||
// episodes.episodes.find(
|
||||
// (episode: any) => episode.position == lastEpisodeWatched
|
||||
// ) || episodes.episodes[0];
|
||||
useEffect(() => {
|
||||
const __getInfo = async () => {
|
||||
if (source.selected.name == "Kodik") {
|
||||
const { manifest, poster } = await _fetchKodikManifest(
|
||||
episode.selected.url,
|
||||
setPlayerError
|
||||
);
|
||||
if (manifest) {
|
||||
SetPlayerProps({
|
||||
src: manifest,
|
||||
poster: poster,
|
||||
type: "hls",
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (source.selected.name == "Libria") {
|
||||
const { manifest, poster } = await _fetchAnilibriaManifest(
|
||||
episode.selected.url,
|
||||
setPlayerError
|
||||
);
|
||||
if (manifest) {
|
||||
SetPlayerProps({
|
||||
src: manifest,
|
||||
poster: poster,
|
||||
type: "hls",
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (source.selected.name == "Sibnet") {
|
||||
const { manifest, poster } = await _fetchSibnetManifest(
|
||||
episode.selected.url,
|
||||
setPlayerError
|
||||
);
|
||||
if (manifest) {
|
||||
SetPlayerProps({
|
||||
src: manifest,
|
||||
poster: poster,
|
||||
type: "mp4",
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
SetPlayerProps({
|
||||
src: episode.selected.url,
|
||||
poster: null,
|
||||
type: null,
|
||||
});
|
||||
setIsLoading(false);
|
||||
};
|
||||
if (episode.selected) {
|
||||
setIsLoading(true);
|
||||
setPlayerError(null);
|
||||
__getInfo();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [episode.selected]);
|
||||
|
||||
// setEpisode({
|
||||
// selected: selectedEpisode,
|
||||
// available: episodes.episodes,
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
// if (source.selected) {
|
||||
// __getInfo();
|
||||
// }
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, [source.selected]);
|
||||
|
||||
// useEffect(() => {
|
||||
// const __getInfo = async () => {
|
||||
// if (source.selected.name == "Kodik") {
|
||||
// const { manifest, poster } = await _fetchKodikManifest(
|
||||
// episode.selected.url
|
||||
// );
|
||||
// if (manifest) {
|
||||
// SetPlayerProps({
|
||||
// src: manifest,
|
||||
// poster: poster,
|
||||
// useCustom: true,
|
||||
// type: "hls",
|
||||
// });
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
// if (source.selected.name == "Libria") {
|
||||
// const { manifest, poster } = await _fetchAnilibriaManifest(
|
||||
// episode.selected.url
|
||||
// );
|
||||
// if (manifest) {
|
||||
// SetPlayerProps({
|
||||
// src: manifest,
|
||||
// poster: poster,
|
||||
// useCustom: true,
|
||||
// type: "hls",
|
||||
// });
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
// if (source.selected.name == "Sibnet") {
|
||||
// const { manifest, poster } = await _fetchSibnetManifest(
|
||||
// episode.selected.url
|
||||
// );
|
||||
// if (manifest) {
|
||||
// SetPlayerProps({
|
||||
// src: manifest,
|
||||
// poster: poster,
|
||||
// useCustom: true,
|
||||
// type: "mp4",
|
||||
// });
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
// SetPlayerProps({
|
||||
// src: episode.selected.url,
|
||||
// poster: null,
|
||||
// useCustom: false,
|
||||
// type: null,
|
||||
// });
|
||||
// setIsLoading(false);
|
||||
// };
|
||||
// if (episode.selected) {
|
||||
// setIsLoading(true);
|
||||
// setPlayerError(null);
|
||||
// __getInfo();
|
||||
// }
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, [episode.selected]);
|
||||
useEffect(() => {
|
||||
if (document && document.querySelector("media-chrome-dialog")) {
|
||||
document.querySelector("media-chrome-dialog").shadowRoot.querySelector("slot").style.width = "100%";
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card className="">
|
||||
|
@ -255,35 +228,40 @@ export const ReleasePlayerCustom = (props: {
|
|||
defaultStreamType="on-demand"
|
||||
className={`relative w-full overflow-hidden ${Styles["media-controller"]}`}
|
||||
>
|
||||
{/* <div className="absolute flex flex-wrap w-full gap-2 top-2 left-2">
|
||||
{voiceover.selected && (
|
||||
<VoiceoverSelector
|
||||
availableVoiceover={voiceover.available}
|
||||
voiceover={voiceover.selected}
|
||||
setVoiceover={setVoiceover}
|
||||
release_id={props.id}
|
||||
/>
|
||||
)}
|
||||
{source.selected && (
|
||||
<SourceSelector
|
||||
availableSource={source.available}
|
||||
source={source.selected}
|
||||
setSource={setSource}
|
||||
release_id={props.id}
|
||||
/>
|
||||
)}
|
||||
</div> */}
|
||||
<HlsVideo
|
||||
className="object-contain h-full aspect-video"
|
||||
slot="media"
|
||||
src={playerProps.src}
|
||||
poster={playerProps.poster}
|
||||
// defaultPlaybackRate={playbackRate}
|
||||
// onRateChange={(e) => {
|
||||
// // @ts-ignore
|
||||
// setPlaybackRate(e.target.playbackRate || 1);
|
||||
// }}
|
||||
/>
|
||||
{playerProps.type == "hls" && (
|
||||
<HlsVideo
|
||||
className="object-contain h-full aspect-video"
|
||||
slot="media"
|
||||
src={playerProps.src}
|
||||
poster={playerProps.poster}
|
||||
defaultPlaybackRate={playbackRate}
|
||||
onRateChange={(e) => {
|
||||
// @ts-ignore
|
||||
setPlaybackRate(e.target.playbackRate || 1);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{playerProps.type == "mp4" && (
|
||||
<VideoJS
|
||||
className="object-contain h-full aspect-video"
|
||||
slot="media"
|
||||
src={playerProps.src}
|
||||
poster={playerProps.poster}
|
||||
defaultPlaybackRate={playbackRate}
|
||||
onRateChange={(e) => {
|
||||
// @ts-ignore
|
||||
setPlaybackRate(e.target.playbackRate || 1);
|
||||
}}
|
||||
></VideoJS>
|
||||
)}
|
||||
{(playerProps.type == null || playerProps.src == null) && (
|
||||
<VideoJS
|
||||
src={null}
|
||||
slot="media"
|
||||
poster="https://wallpapers.com/images/featured/cute-red-panda-pictures-sererbq0fdjum7rn.jpg"
|
||||
className="object-contain h-full aspect-video"
|
||||
></VideoJS>
|
||||
)}
|
||||
<div className={`${Styles["media-gradient-bottom"]}`}></div>
|
||||
<MediaSettingsMenu
|
||||
id="settings"
|
||||
|
@ -333,7 +311,17 @@ export const ReleasePlayerCustom = (props: {
|
|||
sourceList={source.available}
|
||||
setSource={setSource}
|
||||
setPlayerError={setPlayerError}
|
||||
/>
|
||||
/>
|
||||
<EpisodeSelectorMenu
|
||||
release_id={props.id}
|
||||
token={props.token}
|
||||
voiceover={voiceover.selected}
|
||||
source={source.selected}
|
||||
episode={episode.selected}
|
||||
episodeList={episode.available}
|
||||
setEpisode={setEpisode}
|
||||
setPlayerError={setPlayerError}
|
||||
/>
|
||||
</div>
|
||||
</MediaChromeDialog>
|
||||
<MediaControlBar className={`${Styles["media-control-bar"]}`}>
|
||||
|
|
|
@ -77,7 +77,7 @@ export const SourceSelectorMenu = ({
|
|||
return (
|
||||
<button
|
||||
key={`release-${release_id}-voiceover-${voiceover.id}-source-${src.id}`}
|
||||
className={`h-fit justify-start items-start ${source.id == src.id ? "text-white" : "text-gray-500 hover:text-gray-300"} transition-colors`}
|
||||
className={`h-fit ${source.id == src.id ? "text-white" : "text-gray-400 hover:text-gray-100"} transition-colors`}
|
||||
onClick={() => {
|
||||
setSource({
|
||||
selected: src,
|
||||
|
|
|
@ -69,7 +69,7 @@ export const VoiceoverSelectorMenu = ({
|
|||
voiceoverList.map((vo: Voiceover) => {
|
||||
return (
|
||||
<button key={`release-${release_id}-voiceover-${vo.id}`}
|
||||
className={`h-fit justify-start items-start ${voiceover.id == vo.id ? "text-white" : "text-gray-500 hover:text-gray-300"} transition-colors`}
|
||||
className={`h-fit px-2 ${voiceover.id == vo.id ? "text-white" : "text-gray-400 hover:text-gray-100"} transition-colors`}
|
||||
onClick={() => {
|
||||
setVoiceover({
|
||||
selected: vo,
|
||||
|
@ -86,9 +86,9 @@ export const VoiceoverSelectorMenu = ({
|
|||
<div className="flex items-center gap-2">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
{vo.icon ? <img alt="" className="w-6 h-6 rounded-full" src={vo.icon}></img> : ""}
|
||||
<span className="text-[16px] leading-none">{vo.name}</span>
|
||||
<span className="text-[16px] leading-none whitespace-nowrap">{vo.name}</span>
|
||||
{vo.pinned && (
|
||||
<span className={`h-4 iconify material-symbols--push-pin ${voiceover.id == vo.id ? "bg-white" : "bg-gray-500 hover:bg-gray-300"} transition-colors`}></span>
|
||||
<span className={`h-4 iconify material-symbols--push-pin ${voiceover.id == vo.id ? "bg-white" : "bg-gray-400 hover:bg-gray-100"} transition-colors`}></span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
|
Loading…
Add table
Reference in a new issue