- {seasons.map((season, index) => (
-
-
- {/* Dots Pattern Overlay */}
-
')`,
- backgroundSize: '3px 3px'
- }}
- />
- {/* Dark Gradient Overlay */}
-
- {/* Title Container */}
-
-
- ))}
+ {/* Sections */}
+
+ {seasons?.length > 0 && (
+
+
More Seasons
+
+ {seasons.map((season, index) => (
+
+
+
')`,
+ backgroundSize: '3px 3px'
+ }}
+ />
+
+
+
+ ))}
+
-
- )}
+ )}
- {/* Voice Actors Section */}
- {animeInfo?.charactersVoiceActors.length > 0 && (
-
-
-
- )}
+ {animeInfo?.charactersVoiceActors?.length > 0 && (
+
+
+
+ )}
- {/* Recommendations Section */}
- {animeInfo.recommended_data.length > 0 && (
-
-
-
- )}
+ {animeInfo?.recommended_data?.length > 0 && (
+
+
+
+ )}
+
>
);
diff --git a/src/pages/watch/Watch.jsx b/src/pages/watch/Watch.jsx
index 3f637ae..92ffc59 100644
--- a/src/pages/watch/Watch.jsx
+++ b/src/pages/watch/Watch.jsx
@@ -1,8 +1,7 @@
/* eslint-disable react/prop-types */
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useRef, useState, useMemo, useCallback } from "react";
import { useLocation, useParams, Link, useNavigate } from "react-router-dom";
import { useLanguage } from "@/src/context/LanguageContext";
-import { useHomeInfo } from "@/src/context/HomeInfoContext";
import { useWatch } from "@/src/hooks/useWatch";
import BouncingLoader from "@/src/components/ui/bouncingloader/Bouncingloader";
import IframePlayer from "@/src/components/player/IframePlayer";
@@ -12,6 +11,7 @@ import Sidecard from "@/src/components/sidecard/Sidecard";
import {
faClosedCaptioning,
faMicrophone,
+ faPlay,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Servers from "@/src/components/servers/Servers";
@@ -22,6 +22,7 @@ import useWatchControl from "@/src/hooks/useWatchControl";
import Player from "@/src/components/player/Player";
import getSafeTitle from "@/src/utils/getSafetitle";
import { Helmet } from 'react-helmet-async';
+import InfoTag from "@/src/components/ui/InfoTag/InfoTag";
import {
generateDescription,
generateKeywords,
@@ -39,13 +40,11 @@ export default function Watch() {
const { id: animeId } = useParams();
const queryParams = new URLSearchParams(location.search);
let initialEpisodeId = queryParams.get("ep");
- const [tags, setTags] = useState([]);
const { language } = useLanguage();
- const { homeInfo } = useHomeInfo();
const isFirstSet = useRef(true);
const [showNextEpisodeSchedule, setShowNextEpisodeSchedule] = useState(true);
+
const {
- // error,
buffering,
streamInfo,
streamUrl,
@@ -61,7 +60,6 @@ export default function Watch() {
thumbnail,
setIsFullOverview,
activeEpisodeNum,
- seasons,
episodeId,
setEpisodeId,
activeServerId,
@@ -71,8 +69,10 @@ export default function Watch() {
activeServerType,
setActiveServerType,
activeServerName,
- setActiveServerName
+ setActiveServerName,
+ seasons
} = useWatch(animeId, initialEpisodeId);
+
const {
autoPlay,
setAutoPlay,
@@ -81,511 +81,280 @@ export default function Watch() {
autoNext,
setAutoNext,
} = useWatchControl();
- const playerRef = useRef(null);
+
const videoContainerRef = useRef(null);
const controlsRef = useRef(null);
const episodesRef = useRef(null);
+ // Sync URL with episodeId
useEffect(() => {
- if (!episodes || episodes.length === 0) return;
+ if (!episodes?.length) return;
- const isValidEpisode = episodes.some(ep => {
- const epNumber = ep.id.split('ep=')[1];
- return epNumber === episodeId;
- });
+ const currentEpNum = episodeId;
+ const isValidEpisode = episodes.some(ep => ep.id.split('ep=')[1] === currentEpNum);
- // If missing or invalid episodeId, fallback to first
- if (!episodeId || !isValidEpisode) {
+ if (!currentEpNum || !isValidEpisode) {
const fallbackId = episodes[0].id.match(/ep=(\d+)/)?.[1];
- if (fallbackId && fallbackId !== episodeId) {
- setEpisodeId(fallbackId);
- }
+ if (fallbackId && fallbackId !== currentEpNum) setEpisodeId(fallbackId);
return;
}
- const newUrl = `/watch/${animeId}?ep=${episodeId}`;
+ const newUrl = `/watch/${animeId}?ep=${currentEpNum}`;
if (isFirstSet.current) {
navigate(newUrl, { replace: true });
isFirstSet.current = false;
} else {
navigate(newUrl);
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [episodeId, animeId, navigate, episodes]);
-
-
-
- // ... inside Watch component ...
- // Update document title
+ }, [episodeId, animeId, navigate, episodes, setEpisodeId]);
// Redirect if no episodes
useEffect(() => {
- if (totalEpisodes !== null && totalEpisodes === 0) {
- navigate(`/${animeId}`);
+ if (totalEpisodes === 0) navigate(`/${animeId}`);
+ }, [animeId, totalEpisodes, navigate]);
+
+ // Height adjustment logic
+ const adjustHeight = useCallback(() => {
+ if (window.innerWidth > 1200) {
+ if (videoContainerRef.current && controlsRef.current && episodesRef.current) {
+ const totalHeight = videoContainerRef.current.offsetHeight + controlsRef.current.offsetHeight;
+ episodesRef.current.style.height = `${totalHeight}px`;
+ }
+ } else if (episodesRef.current) {
+ episodesRef.current.style.height = 'auto';
}
- }, [streamInfo, episodeId, animeId, totalEpisodes, navigate]);
+ }, []);
useEffect(() => {
- // Function to adjust the height of episodes list to match only video + controls
- const adjustHeight = () => {
- if (window.innerWidth > 1200) {
- if (videoContainerRef.current && controlsRef.current && episodesRef.current) {
- // Calculate combined height of video container and controls
- const videoHeight = videoContainerRef.current.offsetHeight;
- const controlsHeight = controlsRef.current.offsetHeight;
- const totalHeight = videoHeight + controlsHeight;
+ const resizeObserver = new ResizeObserver(adjustHeight);
- // Apply the combined height to episodes container
- episodesRef.current.style.height = `${totalHeight}px`;
- }
- } else {
- if (episodesRef.current) {
- episodesRef.current.style.height = 'auto';
- }
- }
- };
+ if (videoContainerRef.current) resizeObserver.observe(videoContainerRef.current);
+ if (controlsRef.current) resizeObserver.observe(controlsRef.current);
- // Initial adjustment with delay to ensure player is fully rendered
- const initialTimer = setTimeout(() => {
- adjustHeight();
- }, 500);
-
- // Set up resize listener
window.addEventListener('resize', adjustHeight);
+ adjustHeight();
- // Create MutationObserver to monitor player changes
- const observer = new MutationObserver(() => {
- setTimeout(adjustHeight, 100);
- });
-
- // Start observing both video container and controls
- if (videoContainerRef.current) {
- observer.observe(videoContainerRef.current, {
- attributes: true,
- childList: true,
- subtree: true
- });
- }
-
- if (controlsRef.current) {
- observer.observe(controlsRef.current, {
- attributes: true,
- childList: true,
- subtree: true
- });
- }
-
- // Set up additional interval for continuous adjustments
- const intervalId = setInterval(adjustHeight, 1000);
-
- // Clean up
return () => {
- clearTimeout(initialTimer);
- clearInterval(intervalId);
- observer.disconnect();
+ resizeObserver.disconnect();
window.removeEventListener('resize', adjustHeight);
};
- }, [buffering, activeServerType, activeServerName, episodeId, streamUrl, episodes]);
+ }, [adjustHeight, buffering, animeInfoLoading]);
- function Tag({ bgColor, index, icon, text }) {
- return (
-
- );
- }
+ const seoData = useMemo(() => {
+ if (!animeInfo) return null;
+ const safeT = getSafeTitle(animeInfo.title, language, animeInfo.japanese_title);
+ return {
+ safeTitle: safeT,
+ pageTitle: optimizeTitle(`Watch ${safeT} Episode ${activeEpisodeNum} Sub Dub Online Free`),
+ pageDescription: generateDescription(`Stream ${safeT} Episode ${activeEpisodeNum} in HD with English Sub and Dub. ${animeInfo.animeInfo?.Overview}`),
+ pageKeywords: `${generateKeywords(animeInfo)}, episode ${activeEpisodeNum}`,
+ canonicalUrl: generateCanonicalUrl(`/watch/${animeId}?ep=${episodeId}`),
+ ogImage: generateOGImage(animeInfo.poster),
+ structured: generateAnimeStructuredData(animeInfo, { number: activeEpisodeNum, id: episodeId }),
+ videoStructured: generateVideoStructuredData(animeInfo, { number: activeEpisodeNum, id: episodeId }, streamUrl),
+ breadcrumb: generateBreadcrumbStructuredData([
+ { name: 'Home', url: '/' },
+ { name: animeInfo.title, url: `/${animeId}` },
+ { name: `Episode ${activeEpisodeNum}`, url: `/watch/${animeId}?ep=${episodeId}` }
+ ])
+ };
+ }, [animeId, animeInfo, activeEpisodeNum, episodeId, language, streamUrl]);
- useEffect(() => {
- setTags([
- {
- condition: animeInfo?.animeInfo?.tvInfo?.rating,
- bgColor: "#ffffff",
- text: animeInfo?.animeInfo?.tvInfo?.rating,
- },
- {
- condition: animeInfo?.animeInfo?.tvInfo?.quality,
- bgColor: "#FFBADE",
- text: animeInfo?.animeInfo?.tvInfo?.quality,
- },
- {
- condition: animeInfo?.animeInfo?.tvInfo?.sub,
- icon: faClosedCaptioning,
- bgColor: "#B0E3AF",
- text: animeInfo?.animeInfo?.tvInfo?.sub,
- },
- {
- condition: animeInfo?.animeInfo?.tvInfo?.dub,
- icon: faMicrophone,
- bgColor: "#B9E7FF",
- text: animeInfo?.animeInfo?.tvInfo?.dub,
- },
- ]);
- }, [animeId, animeInfo]);
+ const tags = useMemo(() => {
+ const info = animeInfo?.animeInfo?.tvInfo;
+ if (!info) return [];
+ return [
+ { condition: info.rating, text: info.rating, bgColor: "#ffffff" },
+ { condition: info.quality, text: info.quality, bgColor: "#FFBADE" },
+ { condition: info.sub, text: info.sub, icon: faClosedCaptioning, bgColor: "#B0E3AF" },
+ { condition: info.dub, text: info.dub, icon: faMicrophone, bgColor: "#B9E7FF" },
+ ];
+ }, [animeInfo]);
- const safeTitle = animeInfo ? getSafeTitle(animeInfo.title, language, animeInfo.japanese_title) : '';
- const pageTitle = animeInfo ? optimizeTitle(`Watch ${safeTitle} Episode ${activeEpisodeNum} Sub Dub Online Free`) : `${website_name} | Free anime streaming platform`;
- const pageDescription = animeInfo ? generateDescription(`Stream ${safeTitle} Episode ${activeEpisodeNum} in HD with English Sub and Dub. ${animeInfo.animeInfo?.Overview}`) : '';
- const pageKeywords = animeInfo ? generateKeywords(animeInfo) + `, episode ${activeEpisodeNum}` : '';
- const canonicalUrl = generateCanonicalUrl(`/watch/${animeId}?ep=${episodeId}`);
- const ogImage = animeInfo ? generateOGImage(animeInfo.poster) : '';
-
- const animeStructuredData = animeInfo ? generateAnimeStructuredData(animeInfo, { number: activeEpisodeNum, id: episodeId }) : null;
- const videoStructuredData = animeInfo ? generateVideoStructuredData(animeInfo, { number: activeEpisodeNum, id: episodeId }, streamUrl) : null;
- const breadcrumbData = animeInfo ? generateBreadcrumbStructuredData([
- { name: 'Home', url: '/' },
- { name: animeInfo.title, url: `/${animeId}` },
- { name: `Episode ${activeEpisodeNum}`, url: `/watch/${animeId}?ep=${episodeId}` }
- ]) : null;
return (
<>
-
- {pageTitle}
-
-
-
+ {seoData && (
+
+ {seoData.pageTitle}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
-
-
-
-
-
-
-
-
-
-
-
- {animeStructuredData && (
-
- )}
- {videoStructuredData && (
-
- )}
- {breadcrumbData && (
-
- )}
-
-
-
- {/* Left Column - Player, Controls, Servers */}
+
+
+
+ {/* Left Column */}
-
- {/* Video Container */}
+
- {!buffering ? (["hd-1", "hd-4"].includes(activeServerName.toLowerCase()) ?
-
setEpisodeId(id)}
- autoNext={autoNext}
- /> : setEpisodeId(id)}
- animeInfo={animeInfo}
- episodeNum={activeEpisodeNum}
- streamInfo={streamInfo}
- />
+ {!buffering ? (
+ ["hd-1", "hd-4"].includes(activeServerName.toLowerCase()) ? (
+
+ ) : (
+
+ )
) : (
)}
-
- {!buffering && !activeServerType ? (
- servers ? (
- <>
- Probably this server is down, try other servers
-
- Either reload or try again after sometime
- >
- ) : (
- <>
- Probably streaming server is down
-
- Either reload or try again after sometime
- >
- )
- ) : null}
-
+ {!buffering && !activeServerType && (
+
+
+ Streaming server seems to be down. Please try another server or reload the page.
+
+
+ )}
- {/* Controls Section */}
{!buffering && (
setEpisodeId(id)}
+ autoPlay={autoPlay} setAutoPlay={setAutoPlay}
+ autoSkipIntro={autoSkipIntro} setAutoSkipIntro={setAutoSkipIntro}
+ autoNext={autoNext} setAutoNext={setAutoNext}
+ episodes={episodes} totalEpisodes={totalEpisodes}
+ episodeId={episodeId} onButtonClick={setEpisodeId}
/>
)}
- {/* Title and Server Selection */}
- {/* Next Episode Schedule */}
{nextEpisodeSchedule?.nextEpisodeSchedule && showNextEpisodeSchedule && (
-
+
-
🚀
-
-
Next episode estimated at
-
- {new Date(
- new Date(nextEpisodeSchedule.nextEpisodeSchedule).getTime() -
- new Date().getTimezoneOffset() * 60000
- ).toLocaleDateString("en-GB", {
- day: "2-digit",
- month: "2-digit",
- year: "numeric",
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit",
- hour12: true,
+ 🚀
+
+ Next episode around:
+
+ {new Date(nextEpisodeSchedule.nextEpisodeSchedule).toLocaleString("en-GB", {
+ day: "2-digit", month: "2-digit", year: "numeric",
+ hour: "2-digit", minute: "2-digit", hour12: true
})}
-
setShowNextEpisodeSchedule(false)}
- >
- ×
-
+
setShowNextEpisodeSchedule(false)} className="text-2xl text-gray-500 hover:text-white transition-colors">×
)}
- {/* Mobile-only Seasons Section */}
- {seasons?.length > 0 && (
-
-
More Seasons
-
- {seasons.map((season, index) => (
-
-
- {/* Dots Pattern Overlay */}
-
')`,
- backgroundSize: '3px 3px'
- }}
- />
- {/* Dark Gradient Overlay */}
-
- {/* Title Container */}
-
-
- ))}
+ {/* Info Section */}
+
+
+
+ {animeInfo ? (
+
+ ) :
}
-
- )}
-
- {/* Mobile-only Episodes Section */}
-
-
- {!episodes ? (
-
-
-
- ) : (
-
setEpisodeId(id)}
- totalEpisodes={totalEpisodes}
- />
- )}
-
-
-
- {/* Anime Info Section */}
-
-
- {animeInfo && animeInfo?.poster ? (
-
- ) : (
-
- )}
-
- {animeInfo && animeInfo?.title ? (
-
-
- {getSafeTitle(animeInfo.title, language, animeInfo.japanese_title)}
+
+ {animeInfo ? (
+
+
+ {seoData?.safeTitle}
-
+
- ) : (
-
- )}
-
- {animeInfo ? (
- tags.map(
- ({ condition, icon, text }, index) =>
- condition && (
-
- {icon && }
- {text}
-
- )
- )
- ) : (
-
- )}
+ ) :
}
+
+
+ {tags.map((tag, idx) => tag.condition && (
+
+ ))}
+
{animeInfo?.animeInfo?.Overview && (
-
- {animeInfo?.animeInfo?.Overview.length > 270 ? (
+
+ {animeInfo.animeInfo.Overview.length > 270 ? (
<>
- {isFullOverview
- ? animeInfo?.animeInfo?.Overview
- : `${animeInfo?.animeInfo?.Overview.slice(0, 270)}...`}
- setIsFullOverview(!isFullOverview)}
- >
+ {isFullOverview ? animeInfo.animeInfo.Overview : `${animeInfo.animeInfo.Overview.slice(0, 270)}...`}
+ setIsFullOverview(!isFullOverview)} className="ml-2 text-white font-medium hover:underline text-xs sm:text-sm">
{isFullOverview ? "Show Less" : "Read More"}
>
- ) : (
- animeInfo?.animeInfo?.Overview
- )}
-
+ ) : animeInfo.animeInfo.Overview}
+
)}
- {/* Desktop-only Seasons Section */}
+ {/* Seasons (Mobile only) */}
{seasons?.length > 0 && (
-
-
More Seasons
-
- {seasons.map((season, index) => (
+
+
More Seasons
+
+ {seasons.map((season, idx) => (
-
- {/* Dots Pattern Overlay */}
-
')`,
- backgroundSize: '3px 3px'
- }}
- />
- {/* Dark Gradient Overlay */}
-
- {/* Title Container */}
-
-
- {season.season}
-
+
+
+
))}
@@ -594,48 +363,34 @@ export default function Watch() {
)}
- {/* Right Column - Episodes and Related (Desktop Only) */}
-
- {/* Episodes Section */}
-
+ {/* Right Column (Desktop Only) */}
+
+
{!episodes ? (
-
-
-
+
) : (
setEpisodeId(id)}
+ onEpisodeClick={setEpisodeId}
totalEpisodes={totalEpisodes}
/>
)}
- {/* Related Anime Section */}
- {animeInfoLoading ? (
-
-
-
- ) : animeInfo?.related_data?.length > 0 && (
-
-
Related Anime
-
+ {!animeInfoLoading && animeInfo?.related_data?.length > 0 && (
+
+
Related Anime
+
)}
- {/* Mobile-only Related Section */}
+ {/* Related Anime (Mobile only) */}
{!animeInfoLoading && animeInfo?.related_data?.length > 0 && (
-
-
Related Anime
-
+
+
Related Anime
+
)}