= React.memo(
icon="forward_10"
tooltip="+10s"
/>
- {/* */}
+ />
;
+ togglePlayPause: () => void;
+ toggleFullscreen: () => void;
+ toggleMute: () => void;
+ increaseVolume: () => void;
+ decreaseVolume: () => void;
+ seekBackward: () => void;
+ seekForward: () => void;
+ toggleSubtitles: () => void;
+ cycleSubtitleTracks: () => void;
+ jumpToPercentage: (percentage: number) => void;
+ changePlaybackSpeed: (speed: number) => void;
+}
+
+const KeyboardShortcutsHandler: React.FC = ({
videoRef,
togglePlayPause,
toggleFullscreen,
@@ -17,10 +32,30 @@ const KeyboardShortcutsHandler = ({
}) => {
const speeds = [0.5, 0.75, 1, 1.25, 1.5, 2];
- const shortcuts = {
+ const handleArrowKey = (event: KeyboardEvent) => {
+ // Prevent default arrow key behavior if video is focused
+ if (
+ event.key.startsWith("Arrow") &&
+ document.activeElement === videoRef.current
+ ) {
+ event.preventDefault();
+ }
+ };
+
+ const shortcuts: Record void> = {
k: togglePlayPause,
- j: seekBackward,
- l: seekForward,
+ j: () => {
+ const video = videoRef.current;
+ if (video) {
+ video.currentTime -= 10;
+ }
+ },
+ l: () => {
+ const video = videoRef.current;
+ if (video) {
+ video.currentTime += 10;
+ }
+ },
" ": togglePlayPause,
f: toggleFullscreen,
m: toggleMute,
@@ -30,22 +65,17 @@ const KeyboardShortcutsHandler = ({
ArrowLeft: seekBackward,
c: toggleSubtitles,
t: cycleSubtitleTracks,
-
- ...Array.from({ length: 10 }, (_, i) => ({
- [`${i}`]: () => jumpToPercentage(i * 10),
- })),
-
">": () => {
- const currentSpeed = videoRef.current.playbackRate;
- const currentIndex = speeds.indexOf(currentSpeed);
+ const currentSpeed = videoRef.current?.playbackRate;
+ const currentIndex = currentSpeed ? speeds.indexOf(currentSpeed) : -1;
if (currentIndex < speeds.length - 1) {
const newSpeed = speeds[currentIndex + 1];
changePlaybackSpeed(newSpeed);
}
},
"<": () => {
- const currentSpeed = videoRef.current.playbackRate;
- const currentIndex = speeds.indexOf(currentSpeed);
+ const currentSpeed = videoRef.current?.playbackRate;
+ const currentIndex = currentSpeed ? speeds.indexOf(currentSpeed) : -1;
if (currentIndex > 0) {
const newSpeed = speeds[currentIndex - 1];
changePlaybackSpeed(newSpeed);
@@ -53,10 +83,35 @@ const KeyboardShortcutsHandler = ({
},
};
+ // Dynamically assign number keys for percentage-based seeking
+ for (let i = 0; i <= 9; i++) {
+ shortcuts[i.toString()] = () => jumpToPercentage(i * 10);
+ }
+
Object.entries(shortcuts).forEach(([key, callback]) => {
- useKeyboardShortcut(key, callback, videoRef);
+ useKeyboardShortcut(key, callback, videoRef, { when: "keydown" });
+ // Also listen for capitalized letter shortcuts when Caps Lock is on
+ if (key.length === 1 && key.toUpperCase() !== key) {
+ useKeyboardShortcut(
+ key.toUpperCase(),
+ callback,
+ videoRef,
+ { when: "keydown" },
+ (event) => event.getModifierState("CapsLock")
+ );
+ }
});
+ // Listen for arrow key events to prevent default scrolling if the video is focused
+ document.addEventListener("keydown", handleArrowKey);
+
+ // Cleanup function to remove event listener when component unmounts
+ React.useEffect(() => {
+ return () => {
+ document.removeEventListener("keydown", handleArrowKey);
+ };
+ }, []);
+
return null;
};
diff --git a/src/components/Watch/Video/VideoPlayer.tsx b/src/components/Watch/Video/VideoPlayer.tsx
index 8ca3931e..6a67f460 100644
--- a/src/components/Watch/Video/VideoPlayer.tsx
+++ b/src/components/Watch/Video/VideoPlayer.tsx
@@ -19,6 +19,9 @@ const VideoPlayerContainer = styled.div`
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
+ @media (max-width: 1000px) {
+ border: 0; // no border on phone
+ }
`;
type VideoPlayerWrapperProps = {
@@ -29,21 +32,18 @@ type VideoPlayerWrapperProps = {
const LargePlayIcon = styled.div`
position: absolute;
- border-radius: 0.8rem; //var(--global-border-radius);
+ border-radius: 3rem; //var(--global-border-radius);
z-index: 2;
- background-color: rgba(24, 24, 24, 0.836);
- opacity: 0.5;
+ background-color: rgba(24, 24, 24, 0.85);
background-size: cover; // Optional: if you want the image to cover the whole area
background-position: center; // Optional: for centering the image
- box-shadow: 0 0 10px rgba(0, 0, 0, 3.5);
color: white;
top: 50%;
left: 50%;
- padding: 0.3rem 1rem;
- transform: translate(-50%, -50%) scaleX(1.1);
- opacity: 1; /* Set opacity to 1 */
+ padding: 0.7rem 0.8rem;
+ transform: translate(-50%, -50%) scaleX(1);
visibility: ${({ $isPlaying }) => ($isPlaying ? "hidden" : "visible")};
- transition: transform 0.2s ease-in-out;
+ transition: background-color 0.3s ease, transform 0.2s ease-in-out; // Define the transition in the default state
${({ $isPlaying }) =>
!$isPlaying &&
@@ -53,7 +53,7 @@ const LargePlayIcon = styled.div`
&:hover {
/* color: var(--primary-accent-bg); */
background-color: var(--primary-accent-bg);
- transform: translate(-50%, -50%) scaleX(1.1) scale(1.1);
+ transform: translate(-50%, -50%) scale(1.1);
}
`;
@@ -70,6 +70,7 @@ const VideoPlayerWrapper = styled.div`
: "pointer"};
&:hover ${LargePlayIcon} {
background-color: var(--primary-accent-bg);
+ // No need to repeat the transition here if it's already defined in LargePlayIcon
}
`;
@@ -473,6 +474,7 @@ const VideoPlayer: React.FC = ({
$isLoading={isLoading}
$isVideoChanging={isVideoChanging}
ref={videoPlayerWrapperRef}
+ onDoubleClick={handleDoubleClick}
>
{isLoading && !isVideoChanging ? (
diff --git a/src/hooks/old-useapi-anify.js b/src/hooks/old-useapi-anify.js
index 0cd7946c..9d7b3334 100644
--- a/src/hooks/old-useapi-anify.js
+++ b/src/hooks/old-useapi-anify.js
@@ -108,7 +108,7 @@ async function fetchFromProxy(url) {
//? Consumet* | miruro-api.vercel.app/
-export async function fetchAnimeData(
+export async function fetchAdvancedSearch(
searchQuery = "",
page = 1,
perPage = 16,
@@ -167,7 +167,7 @@ export async function fetchAnimeData(
}
}
-export async function fetchAnimeInfo(animeId, provider = "gogoanime") {
+export async function fetchAnimeData(animeId, provider = "gogoanime") {
const cacheKey = generateCacheKey("animeInfo", animeId, provider);
const cachedData = cachedResults.get(cacheKey);
if (cachedData) {
@@ -192,7 +192,7 @@ export async function fetchTopAnime(page = 1, perPage = 16) {
type: "ANIME",
sort: ["SCORE_DESC"],
};
- return fetchAnimeData("", page, perPage, options);
+ return fetchAdvancedSearch("", page, perPage, options);
}
export async function fetchTrendingAnime(page = 1, perPage = 16) {
@@ -264,7 +264,7 @@ export async function fetchWatchInfo(episodeId) {
//? Anify* | localhost:3060/
-export async function fetchAnimeInfo2(id, fields = []) {
+export async function fetchAnimeInfo(id, fields = []) {
const cacheKey = generateCacheKey("animeInfo", id, fields.join("-"));
const cachedData = cachedResults.get(cacheKey);
if (cachedData) {
diff --git a/src/hooks/useApi.ts b/src/hooks/useApi.ts
index 26789a76..364e4d87 100644
--- a/src/hooks/useApi.ts
+++ b/src/hooks/useApi.ts
@@ -133,10 +133,10 @@ interface FetchOptions {
// Individual caches for different types of data
// Creating caches for anime data, anime info, and video sources
-const animeDataCache = createCache("animeDataCache");
-const animeInfoCache = createCache("animeInfoCache");
-const animeEpisodesCache = createCache("animeEpisodesCache");
-const videoSourcesCache = createCache("videoSourcesCache");
+const advancedSearchCache = createCache("Advanced Search");
+const animeDataCache = createCache("Data");
+const animeEpisodesCache = createCache("Episodes");
+const videoSourcesCache = createCache("Video Sources");
// Fetch data from proxy with caching
// Function to fetch data from proxy with caching
@@ -160,7 +160,7 @@ async function fetchFromProxy(url: string, cache: any, cacheKey: string) {
}
// Function to fetch anime data
-export async function fetchAnimeData(
+export async function fetchAdvancedSearch(
searchQuery: string = "",
page: number = 1,
perPage: number = 16,
@@ -181,18 +181,18 @@ export async function fetchAnimeData(
});
const url = `${BASE_URL}meta/anilist/advanced-search?${queryParams.toString()}`;
- const cacheKey = generateCacheKey("animeData", queryParams.toString());
+ const cacheKey = generateCacheKey("advancedSearch", queryParams.toString());
- return fetchFromProxy(url, animeDataCache, cacheKey);
+ return fetchFromProxy(url, advancedSearchCache, cacheKey);
}
// Fetch Anime DATA Function
-export async function fetchAnimeInfo(animeId: string, provider: string = "gogoanime") {
- const cacheKey = generateCacheKey('animeInfo', animeId, provider);
+export async function fetchAnimeData(animeId: string, provider: string = "gogoanime") {
+ const cacheKey = generateCacheKey('animeData', animeId, provider);
try {
// Check if data is in cache
- const cachedData = animeInfoCache.get(cacheKey);
+ const cachedData = animeDataCache.get(cacheKey);
if (cachedData) {
return cachedData;
}
@@ -204,7 +204,7 @@ export async function fetchAnimeInfo(animeId: string, provider: string = "gogoan
const data = response.data;
// Store data in cache
- animeInfoCache.set(cacheKey, data);
+ animeDataCache.set(cacheKey, data);
return data;
} catch (error) {
handleError(error, "anime info");
@@ -212,7 +212,7 @@ export async function fetchAnimeInfo(animeId: string, provider: string = "gogoan
}
// Fetch Anime INFO Function
-export async function fetchAnimeInfo2(animeId: string, provider: string = "gogoanime") {
+/* export async function fetchAnimeInfo(animeId: string, provider: string = "gogoanime") {
const cacheKey = generateCacheKey('animeInfo', animeId, provider);
try {
@@ -235,7 +235,7 @@ export async function fetchAnimeInfo2(animeId: string, provider: string = "gogoa
} catch (error) {
handleError(error, "anime info");
}
-}
+} */
// Function to fetch list of anime based on type (Top, Trending, Popular)
@@ -262,7 +262,7 @@ async function fetchList(type: string, page: number = 1, perPage: number = 16, o
// params already defined above
}
- const specificCache = createCache(`${type.toLowerCase()}AnimeCache`);
+ const specificCache = createCache(`${type}`);
return fetchFromProxy(`${url}?${params.toString()}`, specificCache, cacheKey);
}
diff --git a/src/pages/SearchResults.tsx b/src/pages/SearchResults.tsx
index 7294a5e1..1a7aa937 100644
--- a/src/pages/SearchResults.tsx
+++ b/src/pages/SearchResults.tsx
@@ -3,7 +3,7 @@ import styled from "styled-components";
import { useSearchParams } from "react-router-dom";
import CardGrid from "../components/Cards/CardGrid";
import { StyledCardGrid } from "../components/Cards/CardGrid";
-import { fetchAnimeData } from "../hooks/useApi";
+import { fetchAdvancedSearch } from "../hooks/useApi";
import CardSkeleton from "../components/Skeletons/CardSkeleton";
const Container = styled.div`
@@ -43,21 +43,21 @@ const SearchResults = () => {
lastCachedPage.current = 0;
}, [query]);
- const initiateFetchAnimeData = async () => {
+ const initiatefetchAdvancedSearch = async () => {
setIsLoading(true);
if (page > 1) {
setLoadingStates((prev) => [
...prev,
- ...Array.from({ length: 20 }, () => true),
+ ...Array.from({ length: 16 }, () => true),
]);
}
try {
- const fetchedData = await fetchAnimeData(
+ const fetchedData = await fetchAdvancedSearch(
query,
page,
- 20,
+ 16,
(isCached: boolean) => {
if (!isCached) {
preloadNextPage(page + 1);
@@ -97,7 +97,7 @@ const SearchResults = () => {
nextPage > lastCachedPage.current &&
hasNextPage
) {
- fetchAnimeData(query, nextPage, 25, (isCached: boolean) => {
+ fetchAdvancedSearch(query, nextPage, 25, (isCached: boolean) => {
if (!isCached) {
lastCachedPage.current = nextPage;
preloadNextPage(nextPage + 1);
@@ -109,7 +109,7 @@ const SearchResults = () => {
useEffect(() => {
if (delayTimeout.current) clearTimeout(delayTimeout.current);
delayTimeout.current = setTimeout(() => {
- initiateFetchAnimeData();
+ initiatefetchAdvancedSearch();
}, 0);
return () => {
@@ -124,7 +124,7 @@ const SearchResults = () => {
{isLoading && page === 1 ? (
- {Array.from({ length: 20 }).map((_, index) => (
+ {Array.from({ length: 16 }).map((_, index) => (
))}
diff --git a/src/pages/Watch.tsx b/src/pages/Watch.tsx
index 6932bc12..3e9ed699 100644
--- a/src/pages/Watch.tsx
+++ b/src/pages/Watch.tsx
@@ -3,11 +3,8 @@ import { useParams, useNavigate } from "react-router-dom";
import styled from "styled-components";
import EpisodeList from "../components/Watch/EpisodeList";
import VideoPlayer from "../components/Watch/Video/VideoPlayer";
-import {
- fetchAnimeInfo2,
- fetchAnimeInfo,
- fetchAnimeEpisodes,
-} from "../hooks/useApi";
+import CardGrid from "../components/Cards/CardGrid";
+import { fetchAnimeEpisodes, fetchAnimeData } from "../hooks/useApi";
const LOCAL_STORAGE_KEYS = {
LAST_WATCHED_EPISODE: "last-watched-",
@@ -15,29 +12,37 @@ const LOCAL_STORAGE_KEYS = {
};
const WatchContainer = styled.div`
- /* margin-right: 5rem;
- margin-left: 5rem; */
- gap: 0.8rem;
+ //just comment these two lines if you dont want margin while developing.
+ margin-left: 5rem;
+ margin-right: 5rem;
+ @media (max-width: 1500px) {
+ margin-left: 0rem;
+ margin-right: 0rem;
+ }
+`;
+
+const WatchWrapper = styled.div`
+ font-size: 0.9rem;
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--global-primary-bg);
color: var(--global-text);
-
- @media (min-width: 1200px) {
+ @media (min-width: 1000px) {
flex-direction: row;
align-items: flex-start;
- margin-right: 5rem;
- margin-left: 5rem;
}
`;
const VideoPlayerContainer = styled.div`
position: relative;
width: 100%;
- border-radius: 0.2rem;
+ border-radius: var(--global-border-radius);
@media (min-width: 1000px) {
- flex: 3 1 auto;
+ flex: 1 1 auto;
+ }
+ @media (max-width: 1000px) {
+ padding-bottom: 0.8rem;
}
`;
@@ -46,6 +51,29 @@ const VideoPlayerImageWrapper = styled.div`
overflow: hidden; /* Add overflow property */
`;
+const EpisodeListContainer = styled.div`
+ padding-left: 0.8rem;
+ width: 100%;
+ max-height: 100%;
+ @media (min-width: 1000px) {
+ aspect-ratio: 2 / 3;
+ flex: 1 1 500px;
+ max-height: 100%; // Ensures it doesn't exceed the parent's height
+ }
+ @media (max-width: 1000px) {
+ padding-left: 0rem;
+ }
+`;
+
+const SideContainer = styled.div``;
+
+const AnimeInfoContainers = styled.div`
+ width: 100%;
+ @media (max-width: 1000px) {
+ width: 100%;
+ }
+`;
+
const AnimeInfoContainer = styled.div`
border-radius: var(--global-border-radius);
margin-top: 0.8rem;
@@ -53,44 +81,213 @@ const AnimeInfoContainer = styled.div`
background-color: var(--global-secondary-bg);
color: var(--global-text);
display: flex;
- flex-direction: column;
align-items: center;
+ flex-direction: row;
+ align-items: flex-start;
+`;
- @media (min-width: 1000px) {
- flex-direction: row;
- align-items: flex-start;
+const AnimeInfoText = styled.div`
+ text-align: left;
+ line-height: 1rem;
+
+ p {
+ margin-top: 0rem; /* Reset margin */
+ }
+ .episode-name {
+ line-height: 1.6rem;
+ font-size: 1.5rem;
+ font-weight: bold;
+ margin-bottom: 0.5rem;
}
`;
+const AnimeInfoContainer2 = styled.div`
+ border-radius: var(--global-border-radius);
+ margin-top: 0.8rem;
+ padding: 0.6rem;
+ padding-bottom: 0rem;
+ background-color: var(--global-secondary-bg);
+ color: var(--global-text);
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ align-items: flex-start;
+`;
+
+const ShowTrailerButton = styled.button`
+ padding: 0.5rem 0.6rem;
+ margin-bottom: 0.5rem;
+ background-color: var(--primary-accent-bg);
+ color: white;
+ border: none;
+ border-radius: var(--global-border-radius);
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+ outline: none;
+
+ &:hover {
+ background-color: var(--primary-accent);
+ }
+ @media (max-width: 1000px) {
+ display: block; /* Ensure the button is displayed as a block element */
+ margin: 0 auto; /* Center the button horizontally */
+ margin-bottom: 0.5rem;
+ }
+`;
+
+const ShowMoreButton = styled.button`
+ padding: 0.5rem 0.6rem;
+ background-color: var(--primary-accent-bg);
+ color: white;
+ border: none;
+ /* border-radius: var(--global-border-radius); */
+ border-radius: 0.8rem;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+ outline: none;
+ margin-left: 15rem;
+ margin-right: 15rem;
+ display: block;
+
+ &:hover {
+ background-color: var(--primary-accent);
+ }
+ @media (max-width: 1000px) {
+ margin-left: 1rem;
+ margin-right: 1rem;
+ display: block; /* Ensure the button is displayed as a block element */
+ }
+`;
+
+const AnimeInfoContainer3 = styled.div`
+ border-radius: var(--global-border-radius);
+ margin-top: 0.8rem;
+ padding: 0.6rem;
+ background-color: var(--global-secondary-bg);
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ gap: 20px;
+ padding: 0.6rem;
+`;
+
+const AnimeInfoContainer4 = styled.div`
+ border-radius: var(--global-border-radius);
+ margin-top: 0.8rem;
+ padding: 0.6rem;
+ background-color: var(--global-secondary-bg);
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ gap: 20px;
+ padding: 0.6rem;
+`;
+
+const AnimeRecommendations = styled.div`
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ gap: 20px;
+ padding: 0.6rem;
+`;
+
+const AnimeRelations = styled.div`
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ gap: 20px;
+ padding: 0.6rem;
+`;
+
const AnimeInfoImage = styled.img`
border-radius: var(--global-border-radius);
- max-height: 120px;
+ max-height: 150px;
margin-right: 1rem;
- @media (min-width: 1000px) {
- max-height: 200px;
- margin-bottom: 0;
- }
`;
-const AnimeInfoText = styled.div`
- text-align: left;
+const AnimeCharacterContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ gap: 20px;
+ padding: 0.6rem;
`;
+
+const CharacterCard = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ max-width: 150px;
+ gap: 10px;
+ padding: 0.6rem;
+`;
+
+const CharacterImages = styled.img`
+ max-height: 150px;
+ height: auto;
+ border-radius: var(--global-border-radius);
+`;
+
+const CharacterName = styled.div`
+ text-align: center;
+ word-wrap: break-word;
+`;
+
const DescriptionText = styled.p`
text-align: left;
+ line-height: 1.2rem;
display: -webkit-box;
- -webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
`;
-const EpisodeListContainer = styled.div`
+const VideoTrailer = styled.div`
+ margin-top: 0.5rem;
+ margin-bottom: -1.5rem;
+ overflow: hidden;
+ position: relative;
+ width: 50%; // Default to full width to maintain responsiveness
+ height: auto; // Attempt to maintain aspect ratio based on width
+ border: none; // Remove quotation marks from "none"
+ @media (max-width: 1000px) {
+ aspect-ratio: 16/9; /* 16:9 aspect ratio (change as needed) */
+ width: 100%; // Ensure full width on larger screens
+ height: 100%;
+ margin-bottom: 0.5rem;
+ }
+`;
+
+const IframeTrailer = styled.iframe`
+ aspect-ratio: 16/9; /* 16:9 aspect ratio (change as needed) */
+ margin-bottom: 2rem;
+ position: relative;
+ border: none; // Remove quotation marks from "none"
+ top: 0;
+ left: 0;
width: 100%;
+ height: 100%;
+ @media (max-width: 1000px) {
+ width: 100%; // Ensure full width on larger screens
+ height: 100%;
+ }
+`;
- @media (min-width: 1000px) {
- aspect-ratio: 2 / 3;
- flex: 1 1 500px;
- max-height: 100%; // Ensures it doesn't exceed the parent's height
+const GoToHomePageButton = styled.a`
+ position: absolute;
+ color: black;
+ border-radius: var(--global-border-radius);
+ background-color: var(--primary-accent-bg);
+ margin-top: 1rem;
+ padding: 0.7rem 0.8rem;
+ transform: translate(-50%, -50%) scaleX(1.1);
+ transition: transform 0.2s ease-in-out;
+ text-decoration: none; /* Remove underline */
+
+ &:hover {
+ /* color: var(--primary-accent-bg); */
+ transform: translate(-50%, -50%) scaleX(1.1) scale(1.1);
}
`;
@@ -114,6 +311,8 @@ const Watch: React.FC = () => {
episodeNumber?: string;
}>();
const navigate = useNavigate();
+ const [selectedBackgroundImage, setSelectedBackgroundImage] =
+ useState("");
const [episodes, setEpisodes] = useState([]);
const [currentEpisode, setCurrentEpisode] = useState({
id: "0",
@@ -132,33 +331,64 @@ const Watch: React.FC = () => {
};
useEffect(() => {
- const fetchData = async () => {
+ let isMounted = true;
+
+ const fetchInfo = async () => {
if (!animeId) {
console.error("Anime ID is null.");
setLoading(false);
return;
}
- setLoading(true);
try {
- const info = await fetchAnimeInfo(animeId);
- setAnimeInfo(info);
+ const info = await fetchAnimeData(animeId);
+ if (isMounted) {
+ setAnimeInfo(info);
+ // Do not set loading to false here to allow for independent loading states
+ }
+ } catch (error) {
+ console.error("Failed to fetch anime info:", error);
+ if (isMounted) setLoading(false); // Set loading false only on error to prevent early termination of loading state
+ }
+ };
+
+ fetchInfo();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [animeId]); // Depends only on animeId
+ useEffect(() => {
+ let isMounted = true;
- const animeData = await fetchAnimeInfo2(animeId);
- if (animeData) {
+ const fetchData = async () => {
+ if (!animeId) return;
+
+ try {
+ const animeData = await fetchAnimeEpisodes(animeId);
+ if (isMounted && animeData) {
const transformedEpisodes = animeData.map((ep: Episode) => ({
id: ep.id,
- number: ep.number,
title: ep.title,
image: ep.image,
+ // Convert decimal episode numbers to dash-separated format do avoid crashing.
+ number:
+ ep.number % 1 === 0
+ ? ep.number
+ : `${Math.floor(ep.number)}-${
+ ep.number.toString().split(".")[1]
+ }`,
}));
setEpisodes(transformedEpisodes);
+ // Determine the episode to navigate to
+ let navigateToEpisode = transformedEpisodes[0]; // Default to the first episode
+
if (animeTitle && episodeNumber) {
const episodeId = `${animeTitle}-episode-${episodeNumber}`;
const matchingEpisode = transformedEpisodes.find(
- (ep: Episode) => ep.id === episodeId
+ (ep: any) => ep.id === episodeId
);
if (matchingEpisode) {
setCurrentEpisode({
@@ -166,8 +396,9 @@ const Watch: React.FC = () => {
number: matchingEpisode.number,
image: matchingEpisode.image,
});
+ navigateToEpisode = matchingEpisode;
} else {
- navigate(`/watch/${animeId}`);
+ navigate(`/watch/${animeId}`, { replace: true });
}
} else {
let savedEpisodeData = localStorage.getItem(
@@ -178,43 +409,91 @@ const Watch: React.FC = () => {
: null;
if (savedEpisode && savedEpisode.number) {
- const animeTitle = savedEpisode.id.split("-episode")[0];
- navigate(
- `/watch/${animeId}/${animeTitle}/${savedEpisode.number}`,
- { replace: true }
+ const foundEpisode = transformedEpisodes.find(
+ (ep: any) => ep.number === savedEpisode.number
);
- setCurrentEpisode({
- id: savedEpisode.id || "",
- number: savedEpisode.number,
- image: "",
- });
- } else {
- const firstEpisode = transformedEpisodes[0];
- if (firstEpisode) {
- const animeTitle = firstEpisode.id.split("-episode")[0];
- navigate(
- `/watch/${animeId}/${animeTitle}/${firstEpisode.number}`,
- { replace: true }
- );
+ if (foundEpisode) {
setCurrentEpisode({
- id: firstEpisode.id,
- number: firstEpisode.number,
- image: firstEpisode.image,
+ id: foundEpisode.id,
+ number: foundEpisode.number,
+ image: foundEpisode.image,
});
+ navigateToEpisode = foundEpisode;
}
+ } else {
+ // Default to the first episode if no saved data
+ setCurrentEpisode({
+ id: navigateToEpisode.id,
+ number: navigateToEpisode.number,
+ image: navigateToEpisode.image,
+ });
}
}
+
+ // Update URL if needed (for uncached anime or when defaulting to the first/saved episode)
+ if (isMounted && navigateToEpisode) {
+ const newAnimeTitle = navigateToEpisode.id.split("-episode")[0];
+ navigate(
+ `/watch/${animeId}/${newAnimeTitle}/${navigateToEpisode.number}`,
+ { replace: true }
+ );
+ }
}
} catch (error) {
- console.error("Failed to fetch anime info:", error);
+ console.error("Failed to fetch additional anime data:", error);
} finally {
- setLoading(false);
+ if (isMounted) setLoading(false); // End loading state when data fetching is complete or fails
}
};
fetchData();
+
+ return () => {
+ isMounted = false;
+ };
}, [animeId, animeTitle, episodeNumber, navigate]);
+ // Right after your existing useEffect hooks
+ useEffect(() => {
+ // Automatically show the "No episodes found" message if loading is done and no episodes are available
+ if (!loading && episodes.length === 0) {
+ setShowNoEpisodesMessage(true);
+ } else {
+ setShowNoEpisodesMessage(false);
+ }
+ }, [loading, episodes]); // This useEffect depends on the loading and episodes states
+
+ useEffect(() => {
+ const updateBackgroundImage = () => {
+ const episodeImage = currentEpisode.image;
+ const bannerImage = animeInfo?.cover;
+
+ if (episodeImage && episodeImage !== animeInfo.image) {
+ const img = new Image();
+ img.onload = () => {
+ if (img.width > 500) {
+ setSelectedBackgroundImage(episodeImage);
+ } else {
+ setSelectedBackgroundImage(bannerImage);
+ }
+ };
+ img.onerror = () => {
+ // Fallback in case of an error loading the episode image
+ setSelectedBackgroundImage(bannerImage);
+ };
+ img.src = episodeImage;
+ } else {
+ // If no episode image is available or it's the same as animeInfo image, use the banner image
+ setSelectedBackgroundImage(bannerImage);
+ }
+ };
+
+ if (animeInfo && currentEpisode.id !== "0") {
+ // Check if animeInfo is loaded and a current episode is selected
+ updateBackgroundImage();
+ }
+ }, [animeInfo, currentEpisode]); // Depend on animeInfo and currentEpisode
+
const handleEpisodeSelect = useCallback(
async (selectedEpisode: Episode) => {
setIsEpisodeChanging(true);
@@ -239,7 +518,6 @@ const Watch: React.FC = () => {
// Update watched episodes list
updateWatchedEpisodes(selectedEpisode);
- console.log(selectedEpisode);
// Use title in navigation if necessary. Here, we're assuming animeTitle is needed in the URL, adjust as necessary.
navigate(
@@ -255,6 +533,13 @@ const Watch: React.FC = () => {
},
[animeId, navigate]
);
+ const [isDescriptionExpanded, setDescriptionExpanded] = useState(false);
+ const [showCharacters, setShowCharacters] = useState(false);
+ // Function to toggle the description expanded state
+ const toggleDescription = () => {
+ setDescriptionExpanded(!isDescriptionExpanded);
+ setShowCharacters(!isDescriptionExpanded); // Toggle the state for showing characters
+ };
useEffect(() => {
if (animeInfo) {
@@ -262,8 +547,14 @@ const Watch: React.FC = () => {
} else {
document.title = "Miruro";
}
+
const handleKeyPress = (event: KeyboardEvent) => {
- if (event.code === "Space") {
+ // Check if the target element is an input of type "text" or "search"
+ const isSearchBox =
+ event.target instanceof HTMLInputElement &&
+ (event.target.type === "text" || event.target.type === "search");
+
+ if (event.code === "Space" && !isSearchBox) {
event.preventDefault();
}
};
@@ -277,7 +568,7 @@ const Watch: React.FC = () => {
useEffect(() => {
const timeoutId = setTimeout(() => {
- if (loading && (!episodes || episodes.length === 0)) {
+ if (!episodes || episodes.length === 0) {
setShowNoEpisodesMessage(true);
}
}, 10000);
@@ -286,7 +577,7 @@ const Watch: React.FC = () => {
}, [loading, episodes]);
const removeHTMLTags = (description: string): string => {
- return description.replace(/<[^>]+>/g, "");
+ return description.replace(/<[^>]+>/g, "").replace(/\([^)]*\)/g, "");
};
if (showNoEpisodesMessage) {
@@ -299,10 +590,32 @@ const Watch: React.FC = () => {
}}
>
No episodes found :(
+ HomePage
);
}
+ function getDateString(date: any) {
+ const monthNames = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
+ const formattedDate = `${monthNames[date.month - 1]} ${date.day}, ${
+ date.year
+ }`;
+ return formattedDate;
+ }
+
const updateWatchedEpisodes = (episode: Episode) => {
// Retrieve the existing watched episodes array
const watchedEpisodesJson = localStorage.getItem(
@@ -324,96 +637,217 @@ const Watch: React.FC = () => {
return (
-
-
-
+
+
+
+
+
+
+ {
+ const episode = episodes.find((e) => e.id === episodeId);
+ if (episode) {
+ handleEpisodeSelect(episode);
+ }
+ }}
/>
-
+
+
+
+
{animeInfo && (
- {animeInfo.title.english}
-
- Description:
- {removeHTMLTags(animeInfo.description)}
-
+
+ {episodes.find((episode) => episode.id === currentEpisode.id)
+ ?.title || `Episode ${currentEpisode.number}`}
+
- Genres: {animeInfo.genres.join(", ")}
+ {animeInfo.title.english}
- Released: {" "}
- {animeInfo.releaseDate ? animeInfo.releaseDate : "Unknown"}
+ Status: {animeInfo.status}
- Status:
- {animeInfo.status}
+ Year:{" "}
+
+ {animeInfo.releaseDate ? animeInfo.releaseDate : "Unknown"}
+
- Rating:
- {animeInfo.rating}/100
+ Rating: {animeInfo.rating / 10}
- {animeInfo.trailer && (
-
- {showTrailer ? "Hide Trailer" : "Show Trailer"}
-
- )}
- {showTrailer && (
-
- VIDEO
-
- )}
)}
-
-
- {
- const episode = episodes.find((e) => e.id === episodeId);
- if (episode) {
- handleEpisodeSelect(episode);
- }
- }}
- />
-
+ {animeInfo &&
+ (animeInfo.genres.length > 0 ||
+ animeInfo.startDate ||
+ animeInfo.endDate ||
+ animeInfo.studios ||
+ animeInfo.trailer ||
+ animeInfo.description ||
+ (animeInfo.characters && animeInfo.characters.length > 0)) && (
+
+
+ {animeInfo.genres.length > 0 && (
+
+ Genres: {animeInfo.genres.join(", ")}
+
+ )}
+ {animeInfo.startDate && (
+
+ Date aired:{" "}
+
+ {getDateString(animeInfo.startDate)}
+ {animeInfo.endDate
+ ? ` to ${
+ animeInfo.endDate.month && animeInfo.endDate.year
+ ? getDateString(animeInfo.endDate)
+ : "?"
+ }`
+ : animeInfo.status === "Ongoing"
+ ? ""
+ : " to ?"}
+
+
+ )}
+ {animeInfo.studios && (
+
+ Studios: {animeInfo.studios}
+
+ )}
+ {animeInfo.trailer && (
+ <>
+
+ {showTrailer ? "Hide Trailer" : "Show Trailer"}
+
+ {showTrailer && (
+
+
+
+ )}
+ >
+ )}
+ {animeInfo.description && (
+
+ Description:
+ {isDescriptionExpanded
+ ? removeHTMLTags(animeInfo.description || "")
+ : `${removeHTMLTags(
+ animeInfo.description || ""
+ ).substring(0, 300)}...`}
+
+
+
+ {isDescriptionExpanded ? "Show Less" : "Show More"}
+
+
+ )}
+ {animeInfo.characters &&
+ animeInfo.characters.length > 0 &&
+ showCharacters && (
+ <>
+ Characters:
+
+ {animeInfo.characters
+ .filter(
+ (character: any) =>
+ character.role === "MAIN" ||
+ character.role === "SUPPORTING"
+ )
+ .map((character: any) => (
+
+
+
+ {character.name.full}
+
+
+ ))}
+
+ >
+ )}
+
+
+ )}
+ {animeInfo &&
+ animeInfo.relations.filter((relation: any) =>
+ ["OVA", "SPECIAL", "TV", "MOVIE", "ONA", "NOVEL"].includes(
+ relation.type
+ )
+ ).length > 0 && (
+
+ Seasons/Related:
+
+
+ [
+ "OVA",
+ "SPECIAL",
+ "TV",
+ "MOVIE",
+ "ONA",
+ "NOVEL",
+ ].includes(relation.type)
+ )
+ .slice(0, 6)}
+ totalPages={0}
+ hasNextPage={false}
+ />
+
+
+ )}
+
+ {animeInfo &&
+ animeInfo.recommendations.filter((recommendation: any) =>
+ ["OVA", "SPECIAL", "TV", "MOVIE", "ONA", "NOVEL"].includes(
+ recommendation.type
+ )
+ ).length > 0 && (
+
+ Recommendations:
+
+
+ [
+ "OVA",
+ "SPECIAL",
+ "TV",
+ "MOVIE",
+ "ONA",
+ "NOVEL",
+ ].includes(recommendation.type)
+ )
+ .slice(0, 6)}
+ totalPages={0}
+ hasNextPage={false}
+ />
+
+
+ )}
+
);
};
diff --git a/src/styles/globalStyles.tsx b/src/styles/globalStyles.tsx
index 90b475b6..c74112a7 100644
--- a/src/styles/globalStyles.tsx
+++ b/src/styles/globalStyles.tsx
@@ -86,7 +86,7 @@ body {
color: var(--global-text);
transition: 0.1s ease;
- @media (max-width: 768px) {
+ @media (max-width: 1000px) {
padding: 0 0.5rem 0.5rem 0.5rem;
}
}