diff --git a/.env.example b/.env.example index 91ad7978..aa6ea8ac 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ # VITE_BACKEND_URL: This is the base URL for the primary backend server. # It is used for making API requests to fetch anime data, metadata, and other related information. # Example: VITE_BACKEND_URL="https://api.consumet.org/" -VITE_BACKEND_URL="https://api.consumet.org/" +VITE_BACKEND_URL="https://public-miruro-consumet-api.vercel.app/" # VITE_API_KEY: A specific key required for accessing certain backend services or APIs. # This key might be used for authentication or tracking API usage. diff --git a/README.md b/README.md index ee4376c0..0cf38904 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ - -

Logo @@ -7,7 +5,7 @@

- MIRURO.COM || + MIRURO.COM // MIRURO.TV

Discord Shield @@ -22,32 +20,27 @@

-### ⚠️ Heads Up: -#### `JS` to `TS` codebase transition alert; It's a work in progress, so expect some hiccups and weird stuff :) - - ## What is Miruro? -

Miruro is a cutting-edge anime streaming platform powered by the Consumet API. Crafted using React and Vite, it boasts a stylish and contemporary interface. Our platform, Miruro.com, is completely ad-free, ensuring an immersive viewing journey without interruptions.

+

Miruro is a anime streaming platform powered by the Consumet API. Crafted using React and Vite, it boasts a stylish and contemporary interface. Miruro.com offers an immersive anime streaming experience where you can enjoy a vast library of anime titles in HD. Watch your favorite shows with English subtitles or dubbing, and conveniently download any anime without the need for registration.

## Features 🪴 - General - - Free ad-supported streaming service - Dub Anime support - User-friendly interface - - Add Anime/Manga to your AniList - Mobile responsive - Fast page load + - White/Dark theme - Watch Page - Player - Autoplay next episode - Skip op/ed button - Theater mode +- Coming Soon + - Profile page to see your watch list + - Profile page to see your continue watching - Comment section -- Profile page to see your watch list -- Profile page to see your continue watching -- Check new commits to see new features and changes!

Home Page

@@ -71,7 +64,7 @@ ### Before starting installation ⚠️ -> Before we proceed with the installation, we strongly recommend using `bun` for a seamless and efficient setup. While `Node.js` is an alternative, Bun provides a comprehensive solution tailored for our project. +> Before we proceed with the installation, we strongly recommend using `bun` for a seamless and efficient setup. ### 1. Clone this repository using @@ -87,42 +80,19 @@ cd Miruro ### Basic Pre-Requisites -As you might expect, Miruro relies on Node.js. However, for optimal performance, Miruro leverages Bun to achieve the fastest response times possible. - -#### Download and install Bun - -```bash -curl -fsSL https://bun.sh/install | bash -``` - -#### Download and install Node.js +This platform is built on `Node.js` and utilizes `bun` to ensure the quickest response times achievable. While `npm` can also be used, the commands for npm would mirror those of bun, simply substituting the specific commands accordingly. -- [Download Node.js](https://nodejs.org/) - -#### Verify the installation +### Verify installations ```bash -bun -v node -v -npm -v -``` - -### Install Dependencies - -The following are custom installation commands, you can allways do it manually. - -```bash -npm iu -or -npm install && cd server && npm install +bun -v ``` -or +### Install Dependencies (npm also works) ```bash -bun iu -or -bun install && cd server && npm install +bun install && cd server && bun install ``` ### Copy the `.env.example` contents to `.env` in the root folder @@ -131,17 +101,19 @@ bun install && cd server && npm install cp .env.example .env ``` -### 3. Run on development &/or production +#### ⚠ Please remember to change the value of VITE_BACKEND_URL to a consumet deployment that is functional.⚠ + +### 3. Run on development &/or production (npm also works) + +#### Start development version ```bash -npm run dev -npm start +bun run dev ``` -or +#### Start production version ```bash -bun run dev bun start ``` @@ -151,7 +123,7 @@ Please be aware that self-hosting this application is strictly limited to person ## License 📝 -This project is licensed under the Custom BY-NC [License](LICENSE). You are free to use, share, and modify the code for non-commercial purposes with proper attribution to the original author(s). Commercial use is not allowed. For details, see the [LICENSE](LICENSE) file. Feel free to contact the author(s) for questions or additional permissions. +This project is licensed under the Custom BY-NC [License](LICENSE). You are free to use, share, and modify the code for non-commercial purposes with proper attribution to our platform miruro.com. Commercial use is not allowed. For details, see the [LICENSE](LICENSE) file. Feel free to contact the author(s) for questions or additional permissions. ## Bug Report 🐞 @@ -171,7 +143,7 @@ If you have any questions or feedback, please reach out to us at [miruro@proton. - ![Discord Banner 2](https://discordapp.com/api/guilds/1199699127190167643/widget.png?style=banner2) +![Discord Banner 2](https://discordapp.com/api/guilds/1199699127190167643/widget.png?style=banner2) ## Support & Contributions 🤲 @@ -185,7 +157,3 @@ Feel free to contribute to this project! Whether you're an experienced developer
Star History Chart
- -### Note for Beginners 💬 - -> If you're new to JavaScript or programming in general, no worries! Take some time to familiarize yourself with the basics before diving into this project. I'm here to help answer any questions you might have along the way. Don't hesitate to reach out, and let's learn and build together! 😊 diff --git a/src/App.tsx b/src/App.tsx index fd7203fe..c960b2c5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,5 @@ import { useEffect, useRef } from "react"; -import { - BrowserRouter as Router, - Routes, - Route, - useLocation, -} from "react-router-dom"; +import { BrowserRouter as Router, Routes, Route, useLocation } from "react-router-dom"; import Navbar from "./components/Navbar"; import Footer from "./components/Footer"; import Home from "./pages/Home"; @@ -16,32 +11,75 @@ import PolicyTerms from "./pages/PolicyTerms"; import ShortcutsPopup from "./components/ShortcutsPopup"; function ScrollToTop() { - const { pathname } = useLocation(); - const prevPathnameRef = useRef(pathname); + const location = useLocation(); + const prevPathnameRef = useRef(null); useEffect(() => { + // Attempt to restore the scroll position if it exists + const restoreScrollPosition = () => { + const savedPosition = sessionStorage.getItem(location.pathname); + if (savedPosition) { + window.scrollTo(0, parseInt(savedPosition, 10)); + } + }; + + // Save the scroll position for the current path before navigating away + const saveScrollPosition = () => sessionStorage.setItem(location.pathname, window.scrollY.toString()); + + // Add event listeners + window.addEventListener('beforeunload', saveScrollPosition); + window.addEventListener('popstate', restoreScrollPosition); + + // Initial scroll restoration or scroll to top const ignoreRoutePattern = /^\/watch\/[^/]+\/[^/]+\/[^/]+$/; // Only scroll to if pathname has changed and does not match the ignore pattern if ( - prevPathnameRef.current !== pathname && - !ignoreRoutePattern.test(pathname) + prevPathnameRef.current !== location.pathname && + !ignoreRoutePattern.test(location.pathname) ) { - window.setTimeout(() => { - window.scrollTo({ - top: 0, - // behavior: "smooth", - }); - }); + if (location.state?.preserveScroll) { + restoreScrollPosition(); + } else { + window.scrollTo(0, 0); + } } // Update the previous pathname reference for the next render - prevPathnameRef.current = pathname; - }, [pathname]); + prevPathnameRef.current = location.pathname; + + // Cleanup event listeners + return () => { + window.removeEventListener('beforeunload', saveScrollPosition); + window.removeEventListener('popstate', restoreScrollPosition); + }; + }, [location]); return null; } +const usePreserveScrollOnReload = () => { + useEffect(() => { + // Restore scroll position + const savedScrollPosition = sessionStorage.getItem('scrollPosition'); + if (savedScrollPosition) { + window.scrollTo(0, parseInt(savedScrollPosition, 10)); + } + + // Save scroll position before reload + const handleBeforeUnload = () => { + sessionStorage.setItem('scrollPosition', window.scrollY.toString()); + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, []); +}; + function App() { + usePreserveScrollOnReload(); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if ( diff --git a/src/components/Cards/CardItem.tsx b/src/components/Cards/CardItem.tsx index 507a108a..a2578a89 100644 --- a/src/components/Cards/CardItem.tsx +++ b/src/components/Cards/CardItem.tsx @@ -47,13 +47,13 @@ interface CardItemContentProps { } const popInAnimation = keyframes` - 0% { - opacity: 0.4; - } - 100% { - opacity: 1; - transform: scale(1); - } + // 0% { + // opacity: 0.4; + // } + // 100% { + // opacity: 1; + // transform: scale(1); + // } `; const StyledCardWrapper = styled.div` diff --git a/src/components/Cards/ImageDisplay.tsx b/src/components/Cards/ImageDisplay.tsx index c1a59f8e..d5c79bba 100644 --- a/src/components/Cards/ImageDisplay.tsx +++ b/src/components/Cards/ImageDisplay.tsx @@ -19,14 +19,14 @@ interface ImageDisplayProps { } const popInAnimation = keyframes` - 0% { - opacity: 0.4; - transform: scale(0.9); - } - 100% { - opacity: 1; - transform: scale(1); - } + // 0% { + // opacity: 0.4; + // transform: scale(1); + // } + // 100% { + // opacity: 1; + // transform: scale(1); + // } `; const AnimeImage = styled.div<{ @@ -45,7 +45,7 @@ const AnimeImage = styled.div<{ &:hover { background: ${({ $ishovered, color }) => - $ishovered ? color : "var(--global-card-bg)"}; + $ishovered ? color : "var(--global-card-bg)"}; } `; @@ -127,7 +127,7 @@ const Button = styled.span<{ $ishovered?: boolean; color?: string }>` &:hover { color: ${({ $ishovered, color }) => - $ishovered ? color : "var(--global-button-text)"}; + $ishovered ? color : "var(--global-button-text)"}; } @media (max-width: 1000px) { font-size: 0.6rem; diff --git a/src/components/Home/Carousel.tsx b/src/components/Home/Carousel.tsx index db600a19..bc0b7211 100644 --- a/src/components/Home/Carousel.tsx +++ b/src/components/Home/Carousel.tsx @@ -43,15 +43,15 @@ const StyledSwiperSlide = styled(SwiperSlide)` justify-content: flex-start; align-items: center; animation: ${keyframes` - 0% { - opacity: 0.4; - transform: scale(0.965); - } - 100% { - opacity: 1; - transform: scale(1); - } - `} 0.2s ease-in-out forwards; + // 0% { + // opacity: 0.4; + // transform: scale(1); + // } + // 100% { + // opacity: 1; + // transform: scale(1); + // } + // `} 0.2s ease-in-out forwards; `; const DarkOverlay = styled.div` diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 43f243e5..60914584 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -405,7 +405,7 @@ const Navbar = () => { return ( - 見るろ の 久遠 + window.scrollTo(0, 0)}>見るろ の 久遠 diff --git a/src/components/ShortcutsPopup.tsx b/src/components/ShortcutsPopup.tsx index 9c6941c8..487659b0 100644 --- a/src/components/ShortcutsPopup.tsx +++ b/src/components/ShortcutsPopup.tsx @@ -144,7 +144,12 @@ const ShortcutsPopup = () => { const [showPopup, setShowPopup] = useState(false); useEffect(() => { - const togglePopupWithShortcut = (e: any) => { + const togglePopupWithShortcut = (e) => { + // Check if the event's target is an input, textarea, or select element + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.tagName === "SELECT") { + return; // Do nothing if the event is from one of these elements + } + if (e.shiftKey && e.key === "?") { e.preventDefault(); // Prevent the default action of the key press setShowPopup(!showPopup); @@ -153,6 +158,7 @@ const ShortcutsPopup = () => { } }; + // Add the event listener window.addEventListener("keydown", togglePopupWithShortcut); diff --git a/src/components/Skeletons/CardSkeleton.tsx b/src/components/Skeletons/CardSkeleton.tsx index 1668be91..9e14f015 100644 --- a/src/components/Skeletons/CardSkeleton.tsx +++ b/src/components/Skeletons/CardSkeleton.tsx @@ -46,7 +46,7 @@ const SkeletonCard = styled.div` !loading && css` animation: ${css` - ${pulseAnimation} 1s infinite, ${popInAnimation} 1s infinite + // ${pulseAnimation} 1s infinite, ${popInAnimation} 1s infinite `}; `} `; diff --git a/src/components/Skeletons/CarouselSkeleton.tsx b/src/components/Skeletons/CarouselSkeleton.tsx index 61016c64..0dab5e02 100644 --- a/src/components/Skeletons/CarouselSkeleton.tsx +++ b/src/components/Skeletons/CarouselSkeleton.tsx @@ -14,18 +14,18 @@ const pulseAnimation = keyframes` `; const popInAnimation = keyframes` - 0%, 100%{ - opacity: 0; - transform: scale(0.975); - } - 50% { - opacity: 1; - transform: scale(1); - } - 75% { - opacity: 0.5; - transform: scale(1); - } + // 0%, 100%{ + // opacity: 0; + // transform: scale(1); + // } + // 50% { + // opacity: 1; + // transform: scale(1); + // } + // 75% { + // opacity: 0.5; + // transform: scale(1); + // } `; interface SkeletonProps { @@ -38,10 +38,11 @@ const SkeletonSlide = styled.div` height: 24rem; background: var(--global-card-bg); border-radius: var(--global-border-radius); - margin-bottom: 2rem; + margin-bottom: 1rem; @media (max-width: 1000px) { height: 20rem; + margin-bottom: 0.5rem; } @media (max-width: 500px) { height: 18rem; diff --git a/src/components/Watch/EpisodeList.tsx b/src/components/Watch/EpisodeList.tsx index 45935582..c524001d 100644 --- a/src/components/Watch/EpisodeList.tsx +++ b/src/components/Watch/EpisodeList.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useCallback, useEffect } from "react"; +import React, { useState, useMemo, useCallback, useEffect, useRef } from "react"; import styled from "styled-components"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { @@ -6,6 +6,7 @@ import { faThList, faTh, faSearch, + faImage, } from "@fortawesome/free-solid-svg-icons"; // Define TypeScript interfaces for episode and props @@ -32,7 +33,7 @@ const ListContainer = styled.div` flex-grow: 1; display: flex; flex-direction: column; - max-height: 46rem; + max-height: 50rem; @media (max-width: 1900px) { max-height: 35rem; } @@ -53,6 +54,14 @@ const EpisodeGrid = styled.div<{ $isRowLayout: boolean }>` overflow-y: auto; flex-grow: 1; `; +const EpisodeImage = styled.img` + max-width: 100%; + height: auto; + margin-top: 0.5rem; +@media (max-width: 1000px) { + max-height: 75px; /* Adjust this value to control the image size */ +} +`; const ListItem = styled.button<{ $isSelected: boolean; @@ -65,8 +74,8 @@ const ListItem = styled.button<{ ? "var(--primary-accent-bg)" // Selected and watched : "var(--primary-accent-bg)" // Selected but not watched : $isWatched - ? "var(--primary-accent-bg); filter: brightness(0.8);" // Not selected but watched - : "var(--global-tertiary-bg)"}; + ? "var(--primary-accent-bg); filter: brightness(0.8);" // Not selected but watched + : "var(--global-tertiary-bg)"}; border: none; border-radius: var(--global-border-radius); @@ -76,8 +85,8 @@ const ListItem = styled.button<{ ? "var(--global-text)" // Selected and watched : "var(--global-text)" // Selected but not watched : $isWatched - ? "var(--primary-accent); filter: brightness(0.8);" // Not selected but watched - : "grey"}; // Not selected and not watched + ? "var(--primary-accent); filter: brightness(0.8);" // Not selected but watched + : "grey"}; // Not selected and not watched padding: ${({ $isRowLayout }) => $isRowLayout ? "0.6rem 0.5rem" : "0.4rem 0"}; @@ -86,15 +95,14 @@ const ListItem = styled.button<{ justify-content: ${({ $isRowLayout }) => $isRowLayout ? "space-between" : "center"}; align-items: center; - transition: 0.15s; &:hover { ${({ $isSelected, $isWatched }) => - $isSelected - ? $isWatched - ? "filter: brightness(1.1)" // Selected and watched - : "filter: brightness(1.1)" // Selected but not watched - : $isWatched + $isSelected + ? $isWatched + ? "filter: brightness(1.1)" // Selected and watched + : "filter: brightness(1.1)" // Selected but not watched + : $isWatched ? "filter: brightness(1.1)" // Not selected but watched : "background-color: var(--global-button-hover-bg); filter: brightness(1.05); color: #ffffff"}; } @@ -183,6 +191,8 @@ const EpisodeList: React.FC = ({ onEpisodeSelect, }) => { // State for interval, layout, user layout preference, search term, and watched episodes + const episodeGridRef = useRef(null); + const [scrollPosition, setScrollPosition] = useState(0); const [interval, setInterval] = useState<[number, number]>([0, 99]); const [isRowLayout, setIsRowLayout] = useState(true); const [userLayoutPreference, setUserLayoutPreference] = useState< @@ -190,6 +200,12 @@ const EpisodeList: React.FC = ({ >(null); const [searchTerm, setSearchTerm] = useState(""); const [watchedEpisodes, setWatchedEpisodes] = useState([]); + const defaultLayoutMode = episodes.every(episode => episode.title) ? 'list' : 'grid'; + const [displayMode, setDisplayMode] = useState<'list' | 'grid' | 'imageList'>(() => { + const savedMode = animeId ? localStorage.getItem(`layout-preference-${animeId}`) : null; + return savedMode as 'list' | 'grid' | 'imageList' || defaultLayoutMode; + }); + const [selectionInitiatedByUser, setSelectionInitiatedByUser] = useState(false); // Update local storage when watched episodes change @@ -204,6 +220,7 @@ const EpisodeList: React.FC = ({ // Load watched episodes from local storage when animeId changes useEffect(() => { if (animeId) { + localStorage.setItem(`layout-preference-${animeId}`, displayMode); const watched = localStorage.getItem("watched-episodes"); if (watched) { const watchedEpisodesObject = JSON.parse(watched); @@ -292,12 +309,14 @@ const EpisodeList: React.FC = ({ // Toggle layout preference const toggleLayoutPreference = useCallback(() => { - setIsRowLayout((prevLayout) => { - const newLayout = !prevLayout; - setUserLayoutPreference(newLayout); - return newLayout; + setDisplayMode(prevMode => { + const nextMode = prevMode === 'list' ? 'grid' : prevMode === 'grid' ? 'imageList' : 'list'; + if (animeId) { + localStorage.setItem(`layout-preference-${animeId}`, nextMode); + } + return nextMode; }); - }, []); + }, [animeId]); // Filter episodes based on search input const filteredEpisodes = useMemo(() => { @@ -355,6 +374,38 @@ const EpisodeList: React.FC = ({ selectionInitiatedByUser, ]); + useEffect(() => { + const handleScroll = () => { + if (animeId && episodeGridRef.current) { + const scroll = episodeGridRef.current.scrollTop; + localStorage.setItem(`scroll-position-${animeId}`, scroll.toString()); + } + }; + + const savedScrollPosition = animeId ? localStorage.getItem(`scroll-position-${animeId}`) : null; + if (savedScrollPosition) { + setScrollPosition(parseInt(savedScrollPosition, 10)); + } + + const grid = episodeGridRef.current; + if (grid) { + grid.addEventListener('scroll', handleScroll); + grid.scrollTo(0, scrollPosition); + } + + return () => { + if (grid) { + grid.removeEventListener('scroll', handleScroll); + } + }; + }, [animeId, scrollPosition]); + useEffect(() => { + if (episodeGridRef.current) { + episodeGridRef.current.scrollTo(0, scrollPosition); + } + }, [scrollPosition]); + + // Render the EpisodeList component return ( @@ -381,17 +432,14 @@ const EpisodeList: React.FC = ({ onChange={(e) => setSearchTerm(e.target.value)} /> - - {isRowLayout ? ( - - ) : ( - - )} + {displayMode === 'list' && } + {displayMode === 'grid' && } + {displayMode === 'imageList' && } - - + + {displayedEpisodes.map((episode) => { const $isSelected = episode.id === selectedEpisodeId; const $isWatched = watchedEpisodes.some((e) => e.id === episode.id); @@ -400,21 +448,36 @@ const EpisodeList: React.FC = ({ handleEpisodeSelect(episode.id)} aria-selected={$isSelected} > - {isRowLayout ? ( + {displayMode === 'imageList' ? ( + <> +
+ {episode.number}. + {episode.title} +
+ + + ) : displayMode === 'grid' ? ( <> - {episode.number} +
+ {$isSelected ? ( + + ) : ( + {episode.number} + )} +
+ + ) : ( + // Render for 'list' layout + <> + {episode.number}. {episode.title} {$isSelected && } - ) : $isSelected ? ( - - ) : ( - {episode.number} )}
); diff --git a/src/components/Watch/VideSourceSelector.tsx b/src/components/Watch/VideSourceSelector.tsx index 2ceba664..24291e4f 100644 --- a/src/components/Watch/VideSourceSelector.tsx +++ b/src/components/Watch/VideSourceSelector.tsx @@ -1,76 +1,149 @@ import React from "react"; import styled from "styled-components"; +import { FaDownload } from "react-icons/fa"; -// Props interface to define the types of props the component expects +// Props interface interface VideoSourceSelectorProps { - sourceType: string; // 'regular' or 'embedded' + sourceType: string; setSourceType: (sourceType: string) => void; - language: string; // 'sub' or 'dub' + language: string; setLanguage: (language: string) => void; + downloadLink: string; } -// Styled components for the selector const SelectorContainer = styled.div` display: flex; - justify-content: center; + flex-direction: column; align-items: center; - gap: 1rem; - margin-bottom: 1rem; - margin-top: 1rem; + gap: 2rem; + margin: 1rem; + + @media (min-width: 1000px) { + flex-direction: row; + align-items: center; + justify-content: center; + gap: 1rem; + } +`; + +const Group = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + + @media (min-width: 1000px) { + flex-direction: row; + align-items: center; + gap: 0.5rem; + } +`; + +const ButtonRow = styled.div` + display: flex; + flex-direction: row; + gap: 0.5rem; + justify-content: center; `; const Button = styled.button` padding: 0.5rem 1rem; border: none; - border-radius: 5px; + border-radius: var(--global-border-radius); cursor: pointer; background-color: #505050; color: white; + transition: background-color 0.3s ease; + &:hover { - opacity: 0.8; + background-color: var(--primary-accent); } &.active { - /* background-color: var( - --highlight-color - ); // Define this color in your theme or directly here */ background-color: var(--primary-accent-bg); } `; +const DownloadLink = styled.a` + display: flex; + align-items: center; + padding: 0.5rem 1rem; + border-radius: var(--global-border-radius); + cursor: pointer; + background-color: #505050; + color: white; + text-decoration: none; + transition: background-color 0.3s ease; + + svg { + margin-right: 0.5rem; + } + + &:hover { + background-color: var(--primary-accent); + } +`; + +const Label = styled.p` + margin: 0; + font-weight: bold; + @media (min-width: 1000px) { + margin-right: 0.5rem; + } +`; + const VideoSourceSelector: React.FC = ({ sourceType, setSourceType, language, setLanguage, + downloadLink, }) => { return ( - {/* Source Type Selection */} - - - {/* Language Selection */} - - + + + + + + + + + + + + + + + + Download + + + ); }; diff --git a/src/components/Watch/Video/EmbeddedVideoPlayer.tsx b/src/components/Watch/Video/EmbeddedVideoPlayer.tsx new file mode 100644 index 00000000..427e563f --- /dev/null +++ b/src/components/Watch/Video/EmbeddedVideoPlayer.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import styled from "styled-components"; + + +interface EmbeddedVideoPlayerProps { + src: string; +} + +const Container = styled.div` + width: 100%; + height: 0; + padding-bottom: 56.25%; /* 16:9 aspect ratio (height / width * 100) */ + position: relative; + @media (max-width: 1000px) { + padding-bottom:16rem; /* Adjust aspect ratio for smaller screens */ + } +`; + +const Iframe = styled.iframe` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: var(--global-border-radius); + +`; + + +const EmbeddedVideoPlayer: React.FC = ({ src }) => { + return ( + + + + ); +}; + +export default EmbeddedVideoPlayer; diff --git a/src/components/Watch/Video/VideoPlayer.tsx b/src/components/Watch/Video/VideoPlayer.tsx index 4f492691..15c2dc87 100644 --- a/src/components/Watch/Video/VideoPlayer.tsx +++ b/src/components/Watch/Video/VideoPlayer.tsx @@ -13,13 +13,9 @@ const VideoPlayerContainer = styled.div` background: var(--global-secondary-bg); border-radius: var(--global-border-radius); user-select: none; - border: 0.6rem solid var(--global-secondary-bg); -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; - @media (max-width: 1000px) { - border: 0; // no border on phone - } `; type VideoPlayerWrapperProps = { @@ -65,8 +61,8 @@ const VideoPlayerWrapper = styled.div` $isLoading || $isVideoChanging ? "default" : $isCursorIdle - ? "none" - : "pointer"}; + ? "none" + : "pointer"}; &:hover ${LargePlayIcon} { background-color: var(--primary-accent-bg); // No need to repeat the transition here if it's already defined in LargePlayIcon @@ -159,6 +155,7 @@ type VideoPlayerProps = { episodeId: string; bannerImage: string; isEpisodeChanging: boolean; + setDownloadLink: (link: string) => void; }; // Apply the props type to your component @@ -166,6 +163,7 @@ const VideoPlayer: React.FC = ({ episodeId, bannerImage, isEpisodeChanging, + setDownloadLink, }) => { interface VideoSource { quality: string; @@ -211,7 +209,8 @@ const VideoPlayer: React.FC = ({ setSelectedSource, setCurrentTime, setError, - videoRef + videoRef, + setDownloadLink ); const { handleSubtitleChange } = useSubtitleLogic( @@ -243,7 +242,7 @@ const VideoPlayer: React.FC = ({ useEffect(() => { setSelectedSource( videoSources.find((s) => s.quality === selectedQuality)?.url || - videoSources[0]?.url + videoSources[0]?.url ); if (isEpisodeChanging) { setIsPlaying(false); @@ -254,7 +253,7 @@ const VideoPlayer: React.FC = ({ const handleQualityChange = () => { setSelectedSource( videoSources.find((s) => s.quality === selectedQuality)?.url || - videoSources[0]?.url + videoSources[0]?.url ); }; diff --git a/src/components/Watch/Video/hooks/useFetchAndSetupSources.ts b/src/components/Watch/Video/hooks/useFetchAndSetupSources.ts index ba248297..9050afc9 100644 --- a/src/components/Watch/Video/hooks/useFetchAndSetupSources.ts +++ b/src/components/Watch/Video/hooks/useFetchAndSetupSources.ts @@ -10,7 +10,8 @@ const useFetchAndSetupSources = ( // setSubtitleTracks: (tracks: SubtitleTrack[]) => void, // Added this line setCurrentTime: (time: number) => void, // Added this line setError: (error: string) => void, - videoRef: RefObject + videoRef: RefObject, + setDownloadLink: (link: string) => void ) => { useEffect(() => { const deduplicateAndProcessSources = (sources: VideoSource[]): VideoSource[] => { @@ -73,7 +74,7 @@ const useFetchAndSetupSources = ( setIsLoading(true); try { const data = await fetchAnimeStreamingLinks(episodeId); - + setDownloadLink(data.download); // Assuming the API response has a 'download' property with the link // Process and deduplicate sources const uniqueSources = deduplicateAndProcessSources(data.sources); setVideoSources(uniqueSources); diff --git a/src/components/Watch/WatchAnimeData.tsx b/src/components/Watch/WatchAnimeData.tsx index 2ab07df0..cbf6aa90 100644 --- a/src/components/Watch/WatchAnimeData.tsx +++ b/src/components/Watch/WatchAnimeData.tsx @@ -7,7 +7,6 @@ const AnimeDataContainer = styled.div``; const AnimeDataContainerTop = styled.div` border-radius: var(--global-border-radius); - margin-top: 0.8rem; padding: 0.6rem; color: var(--global-text); align-items: center; @@ -220,7 +219,7 @@ const WatchAnimeData: React.FC = ({ animeData }) => { -

{animeData.title.english}

+

{animeData.title.english ? animeData.title.english : animeData.title.romaji}

Type: {animeData.type}

diff --git a/src/hooks/useApi.ts b/src/hooks/useApi.ts index 608e9d0a..3f92e9f9 100644 --- a/src/hooks/useApi.ts +++ b/src/hooks/useApi.ts @@ -136,7 +136,7 @@ interface FetchOptions { const advancedSearchCache = createCache("Advanced Search"); const animeDataCache = createCache("Data"); const animeEpisodesCache = createCache("Episodes"); -const fetchAnimeEmbeddedEpisodesCache = createCache("Episodes"); +const fetchAnimeEmbeddedEpisodesCache = createCache("Video Embedded Sources"); const videoSourcesCache = createCache("Video Sources"); // Fetch data from proxy with caching @@ -287,13 +287,14 @@ export async function fetchAnimeEpisodes(animeId: string, provider: string = "go return fetchFromProxy(url, animeEpisodesCache, cacheKey); } -//Fetch Embedded Anime Episodes Servers -export async function fetchAnimeEmbeddedEpisodes(animeId: string, provider: string = "gogoanime", dub: boolean = false) { - const params = new URLSearchParams({ provider, dub: dub.toString() }); - const url = `${BASE_URL}meta/anilist/servers/${animeId}?${params.toString()}`; - const cacheKey = generateCacheKey('fetchAnimeEmbeddedServers', animeId, provider); +// Fetch Embedded Anime Episodes Servers +export async function fetchAnimeEmbeddedEpisodes(episodeId: string) { + const url = `${BASE_URL}meta/anilist/servers/${episodeId}`; + console.log("Fetching servers from URL:", url); // Debugging log + const cacheKey = generateCacheKey('animeEmbeddedServers', episodeId); return fetchFromProxy(url, fetchAnimeEmbeddedEpisodesCache, cacheKey); + } // Function to fetch anime streaming links diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index c41056ef..89e25672 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -168,7 +168,7 @@ const Home = () => { animeData={animeData} totalPages={1} // Adjust as necessary hasNextPage={false} // Adjust as necessary - onLoadMore={() => {}} // Placeholder for actual logic + onLoadMore={() => { }} // Placeholder for actual logic /> )} diff --git a/src/pages/Watch.tsx b/src/pages/Watch.tsx index 2253005a..e6780563 100644 --- a/src/pages/Watch.tsx +++ b/src/pages/Watch.tsx @@ -3,6 +3,7 @@ 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 EmbeddedVideoPlayer from "../components/Watch/Video/EmbeddedVideoPlayer"; import AnimeData from "../components/Watch/WatchAnimeData"; import VideoSourceSelector from "../components/Watch/VideSourceSelector"; import { @@ -16,8 +17,8 @@ import VideoPlayerSkeleton from "../components/Skeletons/VideoPlayerSkeleton"; // Styled Components const WatchContainer = styled.div` - margin-left: 5rem; - margin-right: 5rem; + margin-left: 1rem; + margin-right: 1rem; @media (max-width: 1000px) { margin-left: 0rem; margin-right: 0rem; @@ -116,13 +117,19 @@ interface CurrentEpisode { } // Main Component - +const getSourceTypeKey = (animeId) => `sourceType-${animeId}`; +const getLanguageKey = (animeId) => `language-${animeId}`; const Watch: React.FC = () => { const { animeId, animeTitle, episodeNumber } = useParams<{ animeId: string; animeTitle?: string; episodeNumber?: string; }>(); + const STORAGE_KEYS = { + SOURCE_TYPE: `sourceType-${animeId}`, + LANGUAGE: `language-${animeId}`, + }; + const navigate = useNavigate(); const [selectedBackgroundImage, setSelectedBackgroundImage] = useState(""); @@ -138,9 +145,37 @@ const Watch: React.FC = () => { const [showNoEpisodesMessage, setShowNoEpisodesMessage] = useState(false); const [lastKeypressTime, setLastKeypressTime] = useState(0); const LANGUAGE_PREFERENCE_PREFIX = "language-preference-"; - const [language, setLanguage] = useState(() => { - return localStorage.getItem(LANGUAGE_PREFERENCE_PREFIX + animeId) || "sub"; // Fallback to 'sub' if not set - }); + const [sourceType, setSourceType] = useState( + () => localStorage.getItem(STORAGE_KEYS.SOURCE_TYPE) || "default" + ); + const [embeddedVideoUrl, setEmbeddedVideoUrl] = useState(""); + const [language, setLanguage] = useState( + () => localStorage.getItem(STORAGE_KEYS.LANGUAGE) || "sub" + ); + const [downloadLink, setDownloadLink] = useState(""); + useEffect(() => { + const defaultSourceType = "default"; + const defaultLanguage = "sub"; + + // Optionally, you can implement logic here to decide if you want to reset to defaults + // or maintain the setting from the previous anime. This example resets to defaults. + + setSourceType( + localStorage.getItem(getSourceTypeKey(animeId)) || defaultSourceType + ); + setLanguage( + localStorage.getItem(getLanguageKey(animeId)) || defaultLanguage + ); + }, [animeId]); + + // Effects to save settings to localStorage + useEffect(() => { + localStorage.setItem(getSourceTypeKey(animeId), sourceType); + }, [sourceType, animeId]); + + useEffect(() => { + localStorage.setItem(getLanguageKey(animeId), language); + }, [language, animeId]); const [languageChanged, setLanguageChanged] = useState(false); useEffect(() => { let isMounted = true; @@ -208,8 +243,8 @@ const Watch: React.FC = () => { ep.number % 1 === 0 ? ep.number : Math.floor(ep.number) + - "-" + - ep.number.toString().split(".")[1], + "-" + + ep.number.toString().split(".")[1], })); setEpisodes(transformedEpisodes); @@ -242,8 +277,8 @@ const Watch: React.FC = () => { : null; return savedEpisode ? transformedEpisodes.find( - (ep) => ep.number === savedEpisode.number - ) || transformedEpisodes[0] + (ep) => ep.number === savedEpisode.number + ) || transformedEpisodes[0] : transformedEpisodes[0]; } })(); @@ -356,6 +391,7 @@ const Watch: React.FC = () => { }, [animeId, navigate] ); + //next episode shortcut with 500ms delay. useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -389,8 +425,9 @@ const Watch: React.FC = () => { }, [episodes, currentEpisode, handleEpisodeSelect, lastKeypressTime]); useEffect(() => { - if (animeInfo) { - document.title = "Miruro | " + animeInfo.title.english; + if (animeInfo && animeInfo.title) { + document.title = + "Miruro | " + (animeInfo.title.english || animeInfo.title.romaji || ""); } else { document.title = "Miruro"; } @@ -441,9 +478,62 @@ const Watch: React.FC = () => { //Saving language preference to cache. useEffect(() => { localStorage.setItem(LANGUAGE_PREFERENCE_PREFIX + animeId, language); - // console.log("Current language setting for anime", animeId, ":", language); }, [language, animeId]); + // Assuming you need to determine which episode's URL to use + const fetchEmbeddedUrl = async (episodeId: string) => { + try { + const embeddedServers = await fetchAnimeEmbeddedEpisodes(episodeId); + if (embeddedServers && embeddedServers.length > 0) { + // Attempt to find the "Gogo server" in the list of servers + const gogoServer = embeddedServers.find( + (server: any) => server.name === "Gogo server" + ); + // If "Gogo server" is found, use it; otherwise, use the first server + const selectedServer = gogoServer || embeddedServers[0]; + setEmbeddedVideoUrl(selectedServer.url); // Use the URL of the selected server + } + } catch (error) { + console.error( + "Error fetching gogo servers for episode ID:", + episodeId, + error + ); + } + }; + + const fetchVidstreamingUrl = async (episodeId: string) => { + try { + // Fetch embedded servers for the episode + const embeddedServers = await fetchAnimeEmbeddedEpisodes(episodeId); + if (embeddedServers && embeddedServers.length > 0) { + // Attempt to find the "Vidstreaming" server in the list of servers + const vidstreamingServer = embeddedServers.find( + (server: any) => server.name === "Vidstreaming" + ); + // If "Vidstreaming" server is found, use it; otherwise, use the first server + const selectedServer = vidstreamingServer || embeddedServers[0]; + setEmbeddedVideoUrl(selectedServer.url); // Use the URL of the selected server + } + } catch (error) { + console.error( + "Error fetching Vidstreaming servers for episode ID:", + episodeId, + error + ); + } + }; + + + // Call this function with the appropriate episode ID when an episode is selected + useEffect(() => { + if (sourceType === "vidstreaming" && currentEpisode.id) { + fetchVidstreamingUrl(currentEpisode.id).catch(console.error); + } else if (sourceType === "gogo" && currentEpisode.id) { + fetchEmbeddedUrl(currentEpisode.id).catch(console.error); + } + }, [sourceType, currentEpisode.id]); + return ( {showNoEpisodesMessage && ( @@ -459,12 +549,15 @@ const Watch: React.FC = () => { {loading ? ( - ) : ( + ) : sourceType === "default" ? ( + ) : ( + )} @@ -488,10 +581,11 @@ const Watch: React.FC = () => { )} {animeInfo && }