Skip to content

Commit

Permalink
Merge pull request #2 from GokuxSS4/dev
Browse files Browse the repository at this point in the history
added settings menu for video player
  • Loading branch information
GokuxSS4 authored Feb 16, 2025
2 parents e490a38 + 1801c79 commit 0fb8a79
Show file tree
Hide file tree
Showing 4 changed files with 378 additions and 27 deletions.
173 changes: 155 additions & 18 deletions components/watch/VideoContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import React from "react";

import { HiAnime } from "aniwatch";
import { useEffect, useState } from "react";
import { BsBadgeCc } from "react-icons/bs";
import { BsBadgeCc, BsFileEarmark } from "react-icons/bs";
import { MdMicNone } from "react-icons/md";
import { BsFileEarmark } from "react-icons/bs";
import { FaCheck } from "react-icons/fa6";
import { RiCheckboxBlankFill } from "react-icons/ri";
import { TbPlayerTrackPrev, TbPlayerTrackNext } from "react-icons/tb";
import {
VidstackDefaultPlayer,
VidStackPlayerSkeleton,
} from "./VidstackDefaultPlayer";

import { useSettings } from "./VideoSettingsProvider";

type ServerInfoType = {
watchCategory: "sub" | "dub" | "raw";
serverName: HiAnime.AnimeServers;
Expand Down Expand Up @@ -49,11 +53,15 @@ export function VideoContainer({
title,
isVideoSkeletonVisible,
handleVideoSkeletonVisibilty,
animeEpisodes,
handleCurrentEpisode,
}: {
currentEpisode: string;
title: string;
isVideoSkeletonVisible: boolean;
handleVideoSkeletonVisibilty: (isVisible: boolean) => void;
animeEpisodes: HiAnime.ScrapedAnimeEpisodes;
handleCurrentEpisode: (episode: string) => void;
}) {
const [availableServers, setAvailableServers] =
useState<HiAnime.ScrapedEpisodeServers | null>(null);
Expand All @@ -64,6 +72,73 @@ export function VideoContainer({

const [isServerResourceError, setIsServerResourceError] = useState(false);

const [isCurrentVideoEnded, setIsCurrentVideoEnded] = useState(false);

const { settings, setSettings } = useSettings();
const { autoPlay, autoNext, autoSkip } = settings;

const toggleAutoPlay = () =>
setSettings({ ...settings, autoPlay: !autoPlay });
const toggleAutoNext = () =>
setSettings({ ...settings, autoNext: !autoNext });
const toggleAutoSkip = () =>
setSettings({ ...settings, autoSkip: !autoSkip });

const handlePrevEpisode = () => {
const { episodes } = animeEpisodes;
const firstEpisode = episodes[0]?.episodeId;

if (currentEpisode !== firstEpisode) {
const currentEpisodeIndex = episodes.findIndex(
(episode) => episode.episodeId === currentEpisode,
);

if (currentEpisodeIndex !== -1 && currentEpisodeIndex > 0) {
const previoudEpisodeId = episodes[currentEpisodeIndex - 1].episodeId;
if (previoudEpisodeId) {
handleCurrentEpisode(previoudEpisodeId);
setIsCurrentVideoEnded(false);
setServerResources(null);
handleVideoSkeletonVisibilty(true);
}
}
}
};

const handleNextEpisode = () => {
const { totalEpisodes, episodes } = animeEpisodes;
const lastEpisode = episodes[totalEpisodes - 1]?.episodeId;

if (currentEpisode !== lastEpisode) {
const currentEpisodeIndex = episodes.findIndex(
(episode) => episode.episodeId === currentEpisode,
);

if (
currentEpisodeIndex !== -1 &&
currentEpisodeIndex < totalEpisodes - 1
) {
const nextEpisodeId = episodes[currentEpisodeIndex + 1].episodeId;
if (nextEpisodeId) {
handleCurrentEpisode(nextEpisodeId);
setIsCurrentVideoEnded(false);
setServerResources(null);
handleVideoSkeletonVisibilty(true);
}
}
}
};

useEffect(() => {
if (
animeEpisodes &&
animeEpisodes.episodes?.length > 0 &&
isCurrentVideoEnded
) {
handleNextEpisode();
}
}, [animeEpisodes, currentEpisode, isCurrentVideoEnded]);

// Fetch Available Servers
useEffect(() => {
const abortController = new AbortController();
Expand Down Expand Up @@ -135,22 +210,26 @@ export function VideoContainer({
const resources = await response.json();

if (!abortController.signal.aborted) {
const proxy_url =
process.env.NODE_ENV === "production"
? "https://proxy.aniweeb.com"
: "https://hls_proxy:8080";
// const proxy_url =
// process.env.NODE_ENV === "production"
// ? "https://proxy.aniweeb.com"
// : "https://hls_proxy:8080";

const proxy_url = "https://proxy.aniweeb.com";

const file_extension = ".m3u8";

resources.sources = resources.sources
.filter((source: any) => source.type === "hls")
.map((source: any) => {
const encodedUrl = btoa(source.url);
return {
...source,
url: `${proxy_url}/${encodedUrl}${file_extension}`,
};
});
if (process.env.NODE_ENV === "production") {
resources.sources = resources.sources
.filter((source: any) => source.type === "hls")
.map((source: any) => {
const encodedUrl = btoa(source.url);
return {
...source,
url: `${proxy_url}/${encodedUrl}${file_extension}`,
};
});
}

setServerResources(resources);
handleVideoSkeletonVisibilty(false);
Expand Down Expand Up @@ -208,7 +287,7 @@ export function VideoContainer({
);

return (
<div className="w-full flex flex-col gap-6">
<div className="w-full flex flex-col">
<div className="relative aspect-video w-full">
{isServerResourceError ? (
<div className="w-full h-full relative bg-gray-300">
Expand All @@ -225,6 +304,12 @@ export function VideoContainer({
subtitleUrls={serverResources.tracks.filter(
(track: any) => track.kind === "captions",
)}
introTiming={serverResources.intro}
outroTiming={serverResources.outro}
onVideoEnd={() => setIsCurrentVideoEnded(true)}
autoPlay={autoPlay}
autoNext={autoNext}
autoSkip={autoSkip}
/>
</div>
) : (
Expand All @@ -234,7 +319,59 @@ export function VideoContainer({
)}
</div>

<div>
<div className="bg-[#0f0f11] flex flex-wrap gap-2 md:gap-4 px-2 py-1 mt-1 rounded-lg text-xs">
<button
className="flex items-center gap-1 text-white/50 hover:text-white transition-colors"
onClick={toggleAutoPlay}
>
{autoPlay ? (
<FaCheck className="text-blue-400 text-xs" />
) : (
<RiCheckboxBlankFill className="text-xs" />
)}{" "}
Auto Play
</button>

<button
className="flex items-center gap-1 text-white/50 hover:text-white transition-colors"
onClick={toggleAutoSkip}
>
{autoSkip ? (
<FaCheck className="text-blue-400 text-xs" />
) : (
<RiCheckboxBlankFill className="text-xs" />
)}{" "}
Auto Skip
</button>

<button
className="flex items-center gap-1 text-white/50 hover:text-white transition-colors"
onClick={toggleAutoNext}
>
{autoNext ? (
<FaCheck className="text-blue-400 text-xs" />
) : (
<RiCheckboxBlankFill className="text-xs" />
)}{" "}
Auto Next
</button>

<button
className="flex items-center gap-1 text-white/50 hover:text-white transition-colors"
onClick={handlePrevEpisode}
>
<TbPlayerTrackPrev className="text-xs" /> Prev
</button>

<button
className="flex items-center gap-1 text-white/50 hover:text-white transition-colors"
onClick={handleNextEpisode}
>
<TbPlayerTrackNext className="text-xs" /> Next
</button>
</div>

<div className="">
{availableServers !== null ? (
<div className="bg-[rgb(15,14,15)] rounded-lg mt-4 overflow-hidden border border-gray-800">
<div className="flex flex-col lg:flex-row">
Expand Down Expand Up @@ -280,7 +417,7 @@ export function VideoContainer({
</div>
</div>
) : (
<div className="rounded-lg mt-4 border border-gray-800 animate-pulse transform transition-transform duration-500 bg-gray-700">
<div className="rounded-lg mt-4 border border-gray-800 animate-pulse transform transition-transform duration-500 bg-gray-700">
<div className="flex flex-col lg:flex-row">
<div className="lg:w-[250px] p-6 bg-gray-900/50">
<div className="flex flex-col items-center text-center gap-2">
Expand Down
70 changes: 70 additions & 0 deletions components/watch/VideoSettingsProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use client";

import React, {
createContext,
useContext,
useState,
useEffect,
ReactNode,
} from "react";

interface VideoSettingsContextType {
settings: {
autoSkip: boolean;
autoPlay: boolean;
autoNext: boolean;
defaultLanguage: string;
};
setSettings: (
settings: Partial<VideoSettingsContextType["settings"]>,
) => void;
}

const VideoSettingsContext = createContext<
VideoSettingsContextType | undefined
>(undefined);

export function useSettings() {
const context = useContext(VideoSettingsContext);
if (context === undefined) {
throw new Error("useSettings must be used within a SettingsProvider");
}
return context;
}

interface VideoSettingsProviderProps {
children: ReactNode;
}

export const VideoSettingsProvider: React.FC<VideoSettingsProviderProps> = ({
children,
}) => {
const [settings, setSettingsState] = useState({
autoSkip: localStorage.getItem("autoSkip") === "true",
autoPlay: localStorage.getItem("autoPlay") === "true",
autoNext: localStorage.getItem("autoNext") === "true",
defaultLanguage: localStorage.getItem("defaultLanguage") || "sub",
});

useEffect(() => {
localStorage.setItem("autoSkip", settings.autoSkip ? "true" : "false");
localStorage.setItem("autoPlay", settings.autoPlay ? "true" : "false");
localStorage.setItem("autoNext", settings.autoNext ? "true" : "false");
localStorage.setItem("defaultLanguage", settings.defaultLanguage);
}, [settings]);

const setSettings = (
newSettings: Partial<VideoSettingsContextType["settings"]>,
) => {
setSettingsState((prev) => {
const updatedSettings = { ...prev, ...newSettings };
return updatedSettings;
});
};

return (
<VideoSettingsContext.Provider value={{ settings, setSettings }}>
{children}
</VideoSettingsContext.Provider>
);
};
Loading

0 comments on commit 0fb8a79

Please sign in to comment.