From 1a85c2571690ba592ac5183d5eadaf9846fe532b Mon Sep 17 00:00:00 2001 From: Factiven Date: Mon, 25 Sep 2023 00:44:40 +0700 Subject: Update v4.1.0 (#79) * Update v4.1.0 * Update pages/_app.js --- components/anime/changeView.js | 119 ----- components/anime/charactersCard.js | 160 +++---- components/anime/episode.js | 114 ++++- components/anime/infoDetails.js | 204 --------- components/anime/mobile/topSection.js | 179 +------- components/anime/viewMode/thumbnailDetail.js | 10 +- components/anime/viewMode/thumbnailOnly.js | 4 +- components/anime/viewSelector.js | 123 ++++++ components/anime/watch/primary/details.js | 185 -------- components/anime/watch/primarySide.js | 276 ------------ components/anime/watch/secondarySide.js | 134 ------ components/footer.js | 225 ---------- components/home/content.js | 19 +- components/home/staticNav.js | 168 -------- components/id/player/Artplayer.js | 59 --- components/id/player/VideoPlayerId.js | 181 -------- components/layout.js | 63 --- components/manga/info/topSection.js | 1 + components/modal.js | 2 +- components/navbar.js | 128 ------ components/shared/MobileNav.js | 8 +- components/shared/NavBar.js | 265 ++++++++++++ components/shared/bugReport.js | 200 +++++++++ components/shared/footer.js | 223 ++++++++++ components/videoPlayer.js | 412 ------------------ components/watch/player/artplayer.js | 325 ++++++++++++++ .../watch/player/component/controls/quality.js | 15 + .../watch/player/component/controls/subtitle.js | 3 + components/watch/player/component/overlay.js | 57 +++ components/watch/player/playerComponent.js | 478 +++++++++++++++++++++ components/watch/player/utils/getZoroSource.js | 0 components/watch/primary/details.js | 189 ++++++++ components/watch/secondary/episodeLists.js | 143 ++++++ 33 files changed, 2225 insertions(+), 2447 deletions(-) delete mode 100644 components/anime/changeView.js delete mode 100644 components/anime/infoDetails.js create mode 100644 components/anime/viewSelector.js delete mode 100644 components/anime/watch/primary/details.js delete mode 100644 components/anime/watch/primarySide.js delete mode 100644 components/anime/watch/secondarySide.js delete mode 100644 components/footer.js delete mode 100644 components/home/staticNav.js delete mode 100644 components/id/player/Artplayer.js delete mode 100644 components/id/player/VideoPlayerId.js delete mode 100644 components/layout.js delete mode 100644 components/navbar.js create mode 100644 components/shared/NavBar.js create mode 100644 components/shared/bugReport.js create mode 100644 components/shared/footer.js delete mode 100644 components/videoPlayer.js create mode 100644 components/watch/player/artplayer.js create mode 100644 components/watch/player/component/controls/quality.js create mode 100644 components/watch/player/component/controls/subtitle.js create mode 100644 components/watch/player/component/overlay.js create mode 100644 components/watch/player/playerComponent.js create mode 100644 components/watch/player/utils/getZoroSource.js create mode 100644 components/watch/primary/details.js create mode 100644 components/watch/secondary/episodeLists.js (limited to 'components') diff --git a/components/anime/changeView.js b/components/anime/changeView.js deleted file mode 100644 index 75ebdff..0000000 --- a/components/anime/changeView.js +++ /dev/null @@ -1,119 +0,0 @@ -export default function ChangeView({ view, setView, episode, map }) { - return ( -
-
0 - ? map?.every( - (item) => - item?.image?.includes("https://s4.anilist.co/") || - item.title === null - ) || !map - ? "pointer-events-none" - : "cursor-pointer" - : "pointer-events-none" - } - onClick={() => { - setView(1); - localStorage.setItem("view", 1); - }} - > - - 0 - ? map?.every( - (item) => - item?.image?.includes("https://s4.anilist.co/") || - item.title === null - ) || !map - ? "fill-[#1c1c22]" - : view === 1 - ? "fill-action" - : "fill-[#3A3A44]" - : "fill-[#1c1c22]" - }`} - rx="3" - > - -
-
0 - ? map?.every( - (item) => - item?.image?.includes("https://s4.anilist.co/") || - item.title === null - ) || !map - ? "pointer-events-none" - : "cursor-pointer" - : "pointer-events-none" - } - onClick={() => { - setView(2); - localStorage.setItem("view", 2); - }} - > - 0 - ? map?.every( - (item) => - item?.image?.includes("https://s4.anilist.co/") || - item.title === null - ) || !map - ? "fill-[#1c1c22]" - : view === 2 - ? "fill-action" - : "fill-[#3A3A44]" - : "fill-[#1c1c22]" - }`} - viewBox="0 0 33 20" - > - - - -
-
0 ? `cursor-pointer` : "pointer-events-none" - } - onClick={() => { - setView(3); - localStorage.setItem("view", 3); - }} - > - 0 - ? view === 3 - ? "fill-action" - : "fill-[#3A3A44]" - : "fill-[#1c1c22]" - }`} - viewBox="0 0 33 20" - > - - - - -
-
- ); -} diff --git a/components/anime/charactersCard.js b/components/anime/charactersCard.js index abff2ba..6c9197a 100644 --- a/components/anime/charactersCard.js +++ b/components/anime/charactersCard.js @@ -3,79 +3,91 @@ import Image from "next/image"; import { useState } from "react"; export default function Characters({ info }) { + const [showAll, setShowAll] = useState(false); - const [showAll, setShowAll] = useState(false); - - return ( -
-
-

Characters

- {info?.length > 6 && ( -
setShowAll(!showAll)}> - {showAll ? "show less" : "show more"} -
- )} -
- {/* for bigger device */} -
- {info.slice(0, showAll ? info.length : 6).map((item, index) => { - return -
-
- { -
-
-

{item.node.name.full || item.node.name.userPreferred}

-

{item.role}

-
-
-
- })} -
- {/* for smaller devices */} -
- {info.slice(0, showAll ? info.length : 6).map((item, index) => { - return
- -
- { -
-
- -

{item.node.name.full || item.node.name.userPreferred}

-

~{item.role}

-
-
- })} + return ( +
+
+

Characters

+ {info?.length > 6 && ( +
setShowAll(!showAll)} + > + {showAll ? "show less" : "show more"} +
+ )} +
+ {/* for bigger device */} +
+ {info.slice(0, showAll ? info.length : 6).map((item, index) => { + return ( + +
+
+ { +
+
+

+ {item.node.name.full || item.node.name.userPreferred} +

+

{item.role}

+
+
+
+ ); + })} +
+ {/* for smaller devices */} +
+ {info.slice(0, showAll ? info.length : 6).map((item, index) => { + return ( + -
- ); -} \ No newline at end of file + ); + })} +
+
+ ); +} diff --git a/components/anime/episode.js b/components/anime/episode.js index b2f4bd7..e6420a7 100644 --- a/components/anime/episode.js +++ b/components/anime/episode.js @@ -1,10 +1,10 @@ import { useEffect, useState, Fragment } from "react"; import { ChevronDownIcon } from "@heroicons/react/20/solid"; -import ChangeView from "./changeView"; +import ViewSelector from "./viewSelector"; import ThumbnailOnly from "./viewMode/thumbnailOnly"; import ThumbnailDetail from "./viewMode/thumbnailDetail"; import ListMode from "./viewMode/listMode"; -import { convertSecondsToTime } from "../../utils/getTimes"; +import { toast } from "react-toastify"; export default function AnimeEpisode({ info, @@ -93,8 +93,9 @@ export default function AnimeEpisode({ !mapProviders || mapProviders?.every( (item) => + item?.img?.includes("https://s4.anilist.co/") || item?.image?.includes("https://s4.anilist.co/") || - item?.image === null + item?.img === null ) ) { setView(3); @@ -152,27 +153,106 @@ export default function AnimeEpisode({ } }, [providerId, artStorage, info.id, session?.user?.name]); + let debounceTimeout; + + const handleRefresh = async () => { + try { + setLoading(true); + clearTimeout(debounceTimeout); + debounceTimeout = setTimeout(async () => { + const res = await fetch( + `/api/v2/episode/${info.id}?releasing=${ + info.status === "RELEASING" ? "true" : "false" + }${isDub ? "&dub=true" : ""}&refresh=true` + ); + if (!res.ok) { + console.log(res); + toast.error("Something went wrong", { + position: "bottom-left", + autoClose: 3000, + hideProgressBar: true, + theme: "colored", + }); + setProviders([]); + setLoading(false); + } else { + const data = await res.json(); + const getMap = data.find((i) => i?.map === true); + let allProvider = data; + + if (getMap) { + allProvider = data.filter((i) => { + if (i?.providerId === "gogoanime" && i?.map !== true) { + return null; + } + return i; + }); + setMapProviders(getMap?.episodes); + } + + if (allProvider.length > 0) { + const defaultProvider = allProvider.find( + (x) => x.providerId === "gogoanime" || x.providerId === "9anime" + ); + setProviderId( + defaultProvider?.providerId || allProvider[0].providerId + ); // set to first provider id + } + + setView(Number(localStorage.getItem("view")) || 3); + setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); + setProviders(allProvider); + setLoading(false); + } + }, 1000); + } catch (err) { + console.log(err); + toast.error("Something went wrong", { + position: "bottom-left", + autoClose: 3000, + hideProgressBar: true, + theme: "colored", + }); + } + }; + return ( <>
-
+
{info && (

Episodes

)} - {info.nextAiringEpisode?.timeUntilAiring && ( -

- Ep {info.nextAiringEpisode.episode}{" "} - {">>"}{" "} - - {convertSecondsToTime( - info.nextAiringEpisode.timeUntilAiring - )}{" "} + {info?.status !== "NOT_YET_RELEASED" && ( + )}

@@ -267,7 +347,7 @@ export default function AnimeEpisode({ )} - ) ) : ( -

{providers.message}

+

{providers?.message}

)}
) : ( diff --git a/components/anime/infoDetails.js b/components/anime/infoDetails.js deleted file mode 100644 index 8200bfa..0000000 --- a/components/anime/infoDetails.js +++ /dev/null @@ -1,204 +0,0 @@ -import Image from "next/image"; -import Link from "next/link"; -import Skeleton from "react-loading-skeleton"; - -export default function DesktopDetails({ - info, - statuses, - handleOpen, - loading, - color, - setShowAll, - showAll, -}) { - return ( - <> -
-
- {info ? ( - <> -
- poster anime - - - ) : ( - - )} -
- -
-
-

- {info ? ( - info?.title?.romaji || info?.title?.english - ) : ( - - )} -

- {info ? ( -
- {info?.episodes && ( -
- {info?.episodes} Episodes -
- )} - {info?.startDate?.year && ( -
- {info?.startDate?.year} -
- )} - {info?.averageScore && ( -
- {info?.averageScore}% -
- )} - {info?.type && ( -
- {info?.type} -
- )} - {info?.status && ( -
- {info?.status} -
- )} -
- Sub | EN -
-
- ) : ( - - )} -
- {info ? ( -

- ) : ( - - )} -

-
- -
-
- {info?.relations?.edges?.length > 0 && ( -
- Relations -
- )} - {info?.relations?.edges?.length > 3 && ( -
setShowAll(!showAll)} - > - {showAll ? "show less" : "show more"} -
- )} -
-
- {info?.relations?.edges ? ( - info?.relations?.edges - .slice(0, showAll ? info?.relations?.edges.length : 3) - .map((r, index) => { - const rel = r.node; - return ( - -
-
- {rel.id} -
-
-
- {r.relationType} -
-
- {rel.title.userPreferred} -
-
{rel.type}
-
-
- - ); - }) - ) : ( - <> - {[1, 2, 3].map((item) => ( -
- -
- ))} -
- -
- - )} -
-
- - ); -} diff --git a/components/anime/mobile/topSection.js b/components/anime/mobile/topSection.js index 4420d24..8db1465 100644 --- a/components/anime/mobile/topSection.js +++ b/components/anime/mobile/topSection.js @@ -1,188 +1,15 @@ -import { - ArrowUpCircleIcon, - MagnifyingGlassIcon, -} from "@heroicons/react/24/solid"; - -import { - ArrowLeftIcon, - PlayIcon, - PlusIcon, - ShareIcon, - UserIcon, -} from "@heroicons/react/24/solid"; +import { PlayIcon, PlusIcon, ShareIcon } from "@heroicons/react/24/solid"; import Image from "next/image"; import { useRouter } from "next/router"; -import { useSearch } from "../../../lib/hooks/isOpenState"; import { useEffect, useState } from "react"; import { convertSecondsToTime } from "../../../utils/getTimes"; import Link from "next/link"; -import { signIn } from "next-auth/react"; import InfoChip from "./reused/infoChip"; import Description from "./reused/description"; - -const getScrollPosition = (el = window) => ({ - x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft, - y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop, -}); - -export function NewNavbar({ info, session, scrollP = 200, toTop = false }) { - const router = useRouter(); - const [scrollPosition, setScrollPosition] = useState(); - const { isOpen, setIsOpen } = useSearch(); - - useEffect(() => { - const handleScroll = () => { - setScrollPosition(getScrollPosition()); - }; - - // Add a scroll event listener when the component mounts - window.addEventListener("scroll", handleScroll); - - // Clean up the event listener when the component unmounts - return () => { - window.removeEventListener("scroll", handleScroll); - }; - }, []); - return ( - <> - - {toTop && ( - - )} - - ); -} +import { NewNavbar } from "@/components/shared/NavBar"; export default function DetailTop({ info, - session, statuses, handleOpen, watchUrl, @@ -217,7 +44,7 @@ export default function DetailTop({ return (
- + {/* MAIN */}
diff --git a/components/anime/viewMode/thumbnailDetail.js b/components/anime/viewMode/thumbnailDetail.js index db18651..2abfd0b 100644 --- a/components/anime/viewMode/thumbnailDetail.js +++ b/components/anime/viewMode/thumbnailDetail.js @@ -32,8 +32,8 @@ export default function ThumbnailDetail({ {`Episode )} @@ -41,7 +41,7 @@ export default function ThumbnailDetail({ className={`absolute bottom-0 left-0 h-[2px] bg-red-700`} style={{ width: - progress && artStorage && epi?.number <= progress + progress || (artStorage && epi?.number <= progress) ? "100%" : artStorage?.[epi?.id] ? `${prog}%` @@ -49,7 +49,7 @@ export default function ThumbnailDetail({ }} /> - Episode {epi?.number} + Episode {epi?.number || 0}

- {title || `Episode ${epi?.number}`} + {title || `Episode ${epi?.number || 0}`}

{description && (

diff --git a/components/anime/viewMode/thumbnailOnly.js b/components/anime/viewMode/thumbnailOnly.js index 69cd8c3..7259beb 100644 --- a/components/anime/viewMode/thumbnailOnly.js +++ b/components/anime/viewMode/thumbnailOnly.js @@ -23,7 +23,7 @@ export default function ThumbnailOnly({ className="transition-all duration-200 ease-out lg:hover:scale-105 hover:ring-1 hover:ring-white cursor-pointer bg-secondary shrink-0 relative w-full h-[180px] sm:h-[130px] subpixel-antialiased rounded-md overflow-hidden" > - Episode {episode?.number} + Episode {episode?.number || 0} +

0 + ? map?.every( + (item) => + item?.img?.includes("https://s4.anilist.co/") || + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map + ? "pointer-events-none" + : "cursor-pointer" + : "pointer-events-none" + } + onClick={() => { + setView(1); + localStorage.setItem("view", 1); + }} + > + + 0 + ? map?.every( + (item) => + item?.img?.includes("https://s4.anilist.co/") || + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map + ? "fill-[#1c1c22]" + : view === 1 + ? "fill-action" + : "fill-[#3A3A44]" + : "fill-[#1c1c22]" + }`} + rx="3" + > + +
+
0 + ? map?.every( + (item) => + item?.img?.includes("https://s4.anilist.co/") || + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map + ? "pointer-events-none" + : "cursor-pointer" + : "pointer-events-none" + } + onClick={() => { + setView(2); + localStorage.setItem("view", 2); + }} + > + 0 + ? map?.every( + (item) => + item?.img?.includes("https://s4.anilist.co/") || + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map + ? "fill-[#1c1c22]" + : view === 2 + ? "fill-action" + : "fill-[#3A3A44]" + : "fill-[#1c1c22]" + }`} + viewBox="0 0 33 20" + > + + + +
+
0 ? `cursor-pointer` : "pointer-events-none" + } + onClick={() => { + setView(3); + localStorage.setItem("view", 3); + }} + > + 0 + ? view === 3 + ? "fill-action" + : "fill-[#3A3A44]" + : "fill-[#1c1c22]" + }`} + viewBox="0 0 33 20" + > + + + + +
+
+ ); +} diff --git a/components/anime/watch/primary/details.js b/components/anime/watch/primary/details.js deleted file mode 100644 index f092879..0000000 --- a/components/anime/watch/primary/details.js +++ /dev/null @@ -1,185 +0,0 @@ -import { useEffect, useState } from "react"; -import { useAniList } from "../../../../lib/anilist/useAnilist"; -import Skeleton from "react-loading-skeleton"; -import DisqusComments from "../../../disqus"; -import Image from "next/image"; - -export default function Details({ - info, - session, - epiNumber, - description, - id, - onList, - setOnList, - handleOpen, - disqus, -}) { - const [showComments, setShowComments] = useState(false); - const { markPlanning } = useAniList(session); - const [url, setUrl] = useState(null); - - function handlePlan() { - if (onList === false) { - markPlanning(info.id); - setOnList(true); - } - } - - useEffect(() => { - const url = window.location.href; - setShowComments(false); - setUrl(url); - }, [id]); - - return ( -
-
-
- {info ? ( - Anime Cover - ) : ( - - )} -
-
-
-

- Studios -

-
- {info ? info.studios.edges[0].node.name : } -
-
-
- { - session ? handlePlan() : handleOpen(); - }} - className={`w-8 h-8 hover:fill-white text-white hover:cursor-pointer ${ - onList ? "fill-white" : "" - }`} - > - - -
-
-
-
-

- Status -

-
{info ? info.status : }
-
-
-

- Titles -

-
- {info ? ( - <> -
- {info.title?.romaji || ""} -
-
- {info.title?.english || ""} -
-
- {info.title?.native || ""} -
- - ) : ( - - )} -
-
-
-
-
- {info && - info.genres?.map((item, index) => ( -
- {item} -
- ))} -
-
- {info && ( -

- )} -

- {/* {
} */} - {!showComments && ( -
- -
- )} - {showComments && ( -
- {info && url && ( -
- -
- )} -
- )} -
- ); -} diff --git a/components/anime/watch/primarySide.js b/components/anime/watch/primarySide.js deleted file mode 100644 index a3d9f4f..0000000 --- a/components/anime/watch/primarySide.js +++ /dev/null @@ -1,276 +0,0 @@ -import { useEffect, useState } from "react"; -import { ChevronDownIcon } from "@heroicons/react/20/solid"; -import { ForwardIcon } from "@heroicons/react/24/solid"; -import { useRouter } from "next/router"; -import { signIn } from "next-auth/react"; -import Details from "./primary/details"; -import VideoPlayer from "../../videoPlayer"; -import Link from "next/link"; -import Skeleton from "react-loading-skeleton"; -import Modal from "../../modal"; -import AniList from "../../media/aniList"; - -export default function PrimarySide({ - info, - session, - epiNumber, - navigation, - providerId, - watchId, - onList, - proxy, - disqus, - setOnList, - episodeList, - timeWatched, - dub, -}) { - const [episodeData, setEpisodeData] = useState(); - const [open, setOpen] = useState(false); - const [skip, setSkip] = useState(); - - const [loading, setLoading] = useState(true); - - const router = useRouter(); - - useEffect(() => { - setLoading(true); - async function fetchData() { - if (info) { - const anify = await fetch("/api/v2/source", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - source: - providerId === "gogoanime" && !watchId.startsWith("/") - ? "consumet" - : "anify", - providerId: providerId, - watchId: watchId, - episode: epiNumber, - id: info.id, - sub: dub ? "dub" : "sub", - }), - }).then((res) => res.json()); - - const skip = await fetch( - `https://api.aniskip.com/v2/skip-times/${info.idMal}/${parseInt( - epiNumber - )}?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=` - ).then((res) => { - if (!res.ok) { - switch (res.status) { - case 404: { - return null; - } - } - } - return res.json(); - }); - - const op = - skip?.results?.find((item) => item.skipType === "op") || null; - const ed = - skip?.results?.find((item) => item.skipType === "ed") || null; - - setSkip({ op, ed }); - - setEpisodeData(anify); - setLoading(false); - } - } - - fetchData(); - return () => { - setEpisodeData(); - setSkip(); - }; - }, [providerId, watchId, info]); - - useEffect(() => { - const mediaSession = navigator.mediaSession; - if (!mediaSession) return; - - const now = navigation?.playing; - const poster = now?.image || info?.bannerImage; - const title = now?.title || info?.title?.romaji; - - const artwork = poster - ? [{ src: poster, sizes: "512x512", type: "image/jpeg" }] - : undefined; - - mediaSession.metadata = new MediaMetadata({ - title: title, - artist: `Moopa ${ - title === info?.title?.romaji - ? "- Episode " + epiNumber - : `- ${info?.title?.romaji || info?.title?.english}` - }`, - artwork, - }); - }, [navigation, info, epiNumber]); - - function handleOpen() { - setOpen(true); - document.body.style.overflow = "hidden"; - } - - function handleClose() { - setOpen(false); - document.body.style.overflow = "auto"; - } - - return ( - <> - handleClose()}> - {!session && ( -
-

- Edit your list -

- -
- )} -
-
-
- {!loading ? ( - navigation && episodeData?.sources?.length !== 0 ? ( - - ) : ( -

- Video is not available, please try other providers -

- ) - ) : ( -
-
-
-
-
-
-
-
- )} -
-
- {info && episodeList ? ( -
-
-

- - {navigation?.playing?.title || info.title?.romaji} - -

-

- Episode {epiNumber} -

-
-
-
- - -
- -
-
- ) : ( -
-
-
- -
-
-

- -

-
- )} -
-
-
- - ); -} diff --git a/components/anime/watch/secondarySide.js b/components/anime/watch/secondarySide.js deleted file mode 100644 index c9ef684..0000000 --- a/components/anime/watch/secondarySide.js +++ /dev/null @@ -1,134 +0,0 @@ -import Skeleton from "react-loading-skeleton"; -import Image from "next/image"; -import Link from "next/link"; - -export default function SecondarySide({ - info, - map, - providerId, - watchId, - episode, - artStorage, - dub, -}) { - const progress = info.mediaListEntry?.progress; - return ( -
-

Up Next

-
- {episode && episode.length > 0 ? ( - map?.some((item) => item.title && item.description) > 0 ? ( - episode.map((item) => { - const time = artStorage?.[item.id]?.timeWatched; - const duration = artStorage?.[item.id]?.duration; - let prog = (time / duration) * 100; - if (prog > 90) prog = 100; - - const mapData = map?.find((i) => i.number === item.number); - return ( - -
-
- {/* {mapData?.image && ( */} - Anime Cover - {/* )} */} - - - Episode {item?.number} - - {item.id == watchId && ( -
- - - -
- )} -
-
-
-

- {mapData?.title} -

-

- {mapData?.description} -

-
- - ); - }) - ) : ( - episode.map((item) => { - return ( - - Episode {item.number} - - ); - }) - ) - ) : ( - <> - {[1].map((item) => ( - - ))} - - )} -
-
- ); -} diff --git a/components/footer.js b/components/footer.js deleted file mode 100644 index ca5a21f..0000000 --- a/components/footer.js +++ /dev/null @@ -1,225 +0,0 @@ -import Link from "next/link"; -import { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import { parseCookies, setCookie } from "nookies"; - -function Footer() { - const [year] = useState(new Date().getFullYear()); - const [season] = useState(getCurrentSeason()); - - const [lang, setLang] = useState("en"); - const [checked, setChecked] = useState(false); - const [cookie, setCookies] = useState(null); - - const router = useRouter(); - - useEffect(() => { - let lang = null; - if (!cookie) { - const cookie = parseCookies(); - lang = cookie.lang || null; - setCookies(cookie); - } - if (lang === "en" || lang === null) { - setLang("en"); - setChecked(false); - } else if (lang === "id") { - setLang("id"); - setChecked(true); - } - }, []); - - function switchLang() { - setChecked(!checked); - if (checked) { - console.log("switching to en"); - setCookie(null, "lang", "en", { - maxAge: 365 * 24 * 60 * 60, - path: "/", - }); - router.push("/en"); - } else { - router.push("/id"); - } - } - - return ( -
-
-
-
- {/*
*/} - {/* Website Logo */} -

moopa

-

- This site does not store any files on our server, we only linked - to the media which is hosted on 3rd party services. -

- {/*
*/} -
-
-
-
    -
  • - - This Season - -
  • -
  • - Popular Anime -
  • -
  • - Popular Manga -
  • -
  • - Donate -
  • -
-
    -
  • - - Movies - -
  • -
  • - TV Shows -
  • -
  • - DMCA -
  • -
  • - - Github - -
  • -
-
-
-
-
-
-
-

- © {new Date().getFullYear()} moopa.live | Website Made by{" "} - Factiven -

-
- {/* Github Icon */} - - - - - - - - - - - - - - {/* Discord Icon */} - - - - - - - {/* Kofi */} - - - - - - - -
-
-
-
- ); -} - -export default Footer; - -function getCurrentSeason() { - const now = new Date(); - const month = now.getMonth() + 1; // getMonth() returns 0-based index - - switch (month) { - case 12: - case 1: - case 2: - return "WINTER"; - case 3: - case 4: - case 5: - return "SPRING"; - case 6: - case 7: - case 8: - return "SUMMER"; - case 9: - case 10: - case 11: - return "FALL"; - default: - return "UNKNOWN SEASON"; - } -} diff --git a/components/home/content.js b/components/home/content.js index c869f6b..9dd4408 100644 --- a/components/home/content.js +++ b/components/home/content.js @@ -305,6 +305,7 @@ export default function Content({ anime.image || anime.coverImage?.extraLarge || anime.coverImage?.large || + anime?.coverImage || "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png" } alt={ @@ -336,7 +337,7 @@ export default function Content({

Episode{" "} - {anime?.episodeNumber} + {anime?.currentEpisode || anime?.episodeNumber}

@@ -377,16 +378,6 @@ export default function Content({ className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item" >
- {/* */} )} diff --git a/components/home/staticNav.js b/components/home/staticNav.js deleted file mode 100644 index 3f43461..0000000 --- a/components/home/staticNav.js +++ /dev/null @@ -1,168 +0,0 @@ -import { signIn, signOut, useSession } from "next-auth/react"; -import { getCurrentSeason } from "../../utils/getTimes"; -import Link from "next/link"; -// import { } from "@heroicons/react/24/solid"; -import { useSearch } from "../../lib/hooks/isOpenState"; -import Image from "next/image"; -import { UserIcon } from "@heroicons/react/20/solid"; -import { useRouter } from "next/router"; - -export default function Navigasi() { - const { data: sessions, status } = useSession(); - const year = new Date().getFullYear(); - const season = getCurrentSeason(); - - const router = useRouter(); - - const { setIsOpen } = useSearch(); - - return ( - <> - {/* NAVBAR PC */} -
-
-
- - moopa - -
    -
  • - - This Season - -
  • -
  • - - Manga - -
  • -
  • - - Anime - -
  • -
  • - - Schedule - -
  • - - {status === "loading" ? ( -
  • Loading...
  • - ) : ( - <> - {!sessions && ( -
  • - -
  • - )} - {sessions && ( -
  • - - My List - -
  • - )} - - )} -
-
-
- - {/*
*/} - {sessions ? ( -
- -
- - Profile - -
signOut("AniListProvider")} - className="hover:text-action cursor-pointer" - > - Log out -
-
-
- ) : ( - - )} - {/*
*/} -
-
-
- - ); -} diff --git a/components/id/player/Artplayer.js b/components/id/player/Artplayer.js deleted file mode 100644 index e209433..0000000 --- a/components/id/player/Artplayer.js +++ /dev/null @@ -1,59 +0,0 @@ -import { useEffect, useRef } from "react"; -import Artplayer from "artplayer"; - -export default function Player({ option, res, getInstance, ...rest }) { - const artRef = useRef(); - - useEffect(() => { - const art = new Artplayer({ - ...option, - container: artRef.current, - fullscreen: true, - hotkey: true, - lock: true, - setting: true, - playbackRate: true, - autoOrientation: true, - pip: true, - theme: "#f97316", - controls: [ - { - name: "fast-rewind", - position: "right", - html: '', - tooltip: "Backward 5s", - click: function () { - art.backward = 5; - }, - }, - { - name: "fast-forward", - position: "right", - html: '', - tooltip: "Forward 5s", - click: function () { - art.forward = 5; - }, - }, - ], - }); - - art.events.proxy(document, "keydown", (event) => { - if (event.key === "f" || event.key === "F") { - art.fullscreen = !art.fullscreen; - } - }); - - if (getInstance && typeof getInstance === "function") { - getInstance(art); - } - - return () => { - if (art && art.destroy) { - art.destroy(false); - } - }; - }, []); - - return
; -} diff --git a/components/id/player/VideoPlayerId.js b/components/id/player/VideoPlayerId.js deleted file mode 100644 index 1168313..0000000 --- a/components/id/player/VideoPlayerId.js +++ /dev/null @@ -1,181 +0,0 @@ -import Player from "./Artplayer"; -import { useEffect, useState } from "react"; -import { useAniList } from "../../../lib/anilist/useAnilist"; - -export default function VideoPlayerId({ - data, - id, - progress, - session, - aniId, - stats, - op, - ed, - title, - poster, -}) { - const [url, setUrl] = useState(""); - const [source, setSource] = useState([]); - const { markProgress } = useAniList(session); - - const [resolution, setResolution] = useState("auto"); - - useEffect(() => { - const resol = localStorage.getItem("quality"); - if (resol) { - setResolution(resol); - } - - async function compiler() { - try { - const source = data.map((i) => { - return { - url: `${i.episode}`, - html: `${i.size}p`, - }; - }); - - const defSource = source.find( - (i) => - i?.html === "1080p" || - i?.html === "720p" || - i?.html === "480p" || - i?.html === "360p" - ); - - if (defSource) { - setUrl(defSource.url); - } - - setSource(source); - } catch (error) { - console.error(error); - } - } - compiler(); - }, [data, resolution]); - - return ( - <> - {url && ( - { - art.on("ready", () => { - const seek = art.storage.get(id); - const seekTime = seek?.time || 0; - const duration = art.duration; - const percentage = seekTime / duration; - - if (percentage >= 0.9) { - art.currentTime = 0; - console.log("Video started from the beginning"); - } else { - art.currentTime = seekTime; - } - }); - - art.on("video:timeupdate", () => { - if (!session) return; - const mediaSession = navigator.mediaSession; - const currentTime = art.currentTime; - const duration = art.duration; - const percentage = currentTime / duration; - - mediaSession.setPositionState({ - duration: art.duration, - playbackRate: art.playbackRate, - position: art.currentTime, - }); - - if (percentage >= 0.9) { - // use >= instead of > - markProgress(aniId, progress, stats); - art.off("video:timeupdate"); - console.log("Video progress marked"); - } - }); - - art.on("video:timeupdate", () => { - var currentTime = art.currentTime; - // console.log(art.currentTime); - art.storage.set(id, { - time: art.currentTime, - duration: art.duration, - }); - - if ( - op && - currentTime >= op.interval.startTime && - currentTime <= op.interval.endTime - ) { - // Add the layer if it's not already added - if (!art.controls["op"]) { - // Remove the other control if it's already added - if (art.controls["ed"]) { - art.controls.remove("ed"); - } - - // Add the control - art.controls.add({ - name: "op", - position: "top", - html: '', - click: function (...args) { - art.seek = op.interval.endTime; - }, - }); - } - } else if ( - ed && - currentTime >= ed.interval.startTime && - currentTime <= ed.interval.endTime - ) { - // Add the layer if it's not already added - if (!art.controls["ed"]) { - // Remove the other control if it's already added - if (art.controls["op"]) { - art.controls.remove("op"); - } - - // Add the control - art.controls.add({ - name: "ed", - position: "top", - html: '', - click: function (...args) { - art.seek = ed.interval.endTime; - }, - }); - } - } else { - // Remove the controls if they're added - if (art.controls["op"]) { - art.controls.remove("op"); - } - if (art.controls["ed"]) { - art.controls.remove("ed"); - } - } - }); - }} - /> - )} - - ); -} diff --git a/components/layout.js b/components/layout.js deleted file mode 100644 index 49850c9..0000000 --- a/components/layout.js +++ /dev/null @@ -1,63 +0,0 @@ -import Navbar from "./navbar"; -import Footer from "./footer"; -import { useEffect, useState } from "react"; - -function Layout(props) { - const [isAtTop, setIsAtTop] = useState(true); - const [isScrollingDown, setIsScrollingDown] = useState(false); - - useEffect(() => { - const handleScroll = () => { - const scrollY = window.scrollY; - - if (scrollY <= 200) { - setIsAtTop(true); - setIsScrollingDown(false); - } else if (scrollY > lastScrollY) { - setIsAtTop(false); - setIsScrollingDown(true); - } else { - setIsAtTop(false); - setIsScrollingDown(false); - } - - lastScrollY = scrollY; - }; - - let lastScrollY = window.scrollY; - - window.addEventListener("scroll", handleScroll); - - return () => { - window.removeEventListener("scroll", handleScroll); - }; - }, []); - - return ( - <> -
- {/* PC/Tablet */} -
- - ); -} - -export default Layout; diff --git a/components/manga/info/topSection.js b/components/manga/info/topSection.js index 40b5a37..45d5f11 100644 --- a/components/manga/info/topSection.js +++ b/components/manga/info/topSection.js @@ -28,6 +28,7 @@ export default function TopSection({ info, firstEp, setCookie }) { src={info.coverImage} width={500} height={500} + priority alt="cover image" className="hidden md:block object-cover h-[10rem] xs:h-[14rem] lg:h-[22rem] rounded-sm shadow-lg shadow-[#1b1b1f] bg-[#34343b]/20" /> diff --git a/components/modal.js b/components/modal.js index 78b76d7..5d6d0cc 100644 --- a/components/modal.js +++ b/components/modal.js @@ -2,7 +2,7 @@ export default function Modal({ open, onClose, children }) { return (
diff --git a/components/navbar.js b/components/navbar.js deleted file mode 100644 index 0bb254f..0000000 --- a/components/navbar.js +++ /dev/null @@ -1,128 +0,0 @@ -import React, { useState, useEffect } from "react"; -import Link from "next/link"; -import { useSession, signIn, signOut } from "next-auth/react"; -import Image from "next/image"; -import { parseCookies } from "nookies"; -import MobileNav from "./shared/MobileNav"; - -function Navbar(props) { - const { data: session, status } = useSession(); - const [isVisible, setIsVisible] = useState(false); - const [fade, setFade] = useState(false); - - const [lang, setLang] = useState("en"); - const [cookie, setCookies] = useState(null); - - const handleShowClick = () => { - setIsVisible(true); - setFade(true); - }; - - const handleHideClick = () => { - setIsVisible(false); - setFade(false); - }; - - useEffect(() => { - let lang = null; - if (!cookie) { - const cookie = parseCookies(); - lang = cookie.lang || null; - setCookies(cookie); - } - if (lang === "en" || lang === null) { - setLang("en"); - } else if (lang === "id") { - setLang("id"); - } - }, []); - - // console.log(session.user?.image); - - return ( -
-
-
- moopa -
- - - - -
-
- ); -} - -export default Navbar; diff --git a/components/shared/MobileNav.js b/components/shared/MobileNav.js index 6dd1e64..d0f29c2 100644 --- a/components/shared/MobileNav.js +++ b/components/shared/MobileNav.js @@ -1,12 +1,12 @@ import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; -import { CalendarIcon, ClockIcon, HomeIcon } from "@heroicons/react/24/outline"; -import { signIn, signOut } from "next-auth/react"; +import { CalendarIcon, HomeIcon } from "@heroicons/react/24/outline"; +import { signIn, signOut, useSession } from "next-auth/react"; import Image from "next/image"; import Link from "next/link"; -import { useRouter } from "next/router"; import { useState } from "react"; -export default function MobileNav({ sessions, hideProfile = false }) { +export default function MobileNav({ hideProfile = false }) { + const { data: sessions } = useSession(); const [isVisible, setIsVisible] = useState(false); const handleShowClick = () => { diff --git a/components/shared/NavBar.js b/components/shared/NavBar.js new file mode 100644 index 0000000..42fcff0 --- /dev/null +++ b/components/shared/NavBar.js @@ -0,0 +1,265 @@ +import { useSearch } from "@/lib/hooks/isOpenState"; +import { getCurrentSeason } from "@/utils/getTimes"; +import { ArrowLeftIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"; +import { UserIcon } from "@heroicons/react/24/solid"; +import { signIn, signOut, useSession } from "next-auth/react"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; + +const getScrollPosition = (el = window) => ({ + x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft, + y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop, +}); + +export function NewNavbar({ + info, + scrollP = 200, + toTop = false, + withNav = false, + paddingY = "py-3", + home = false, + back = false, + manga = false, + shrink = false, +}) { + const { data: session } = useSession(); + const router = useRouter(); + const [scrollPosition, setScrollPosition] = useState(); + const { setIsOpen } = useSearch(); + + const year = new Date().getFullYear(); + const season = getCurrentSeason(); + + useEffect(() => { + const handleScroll = () => { + setScrollPosition(getScrollPosition()); + }; + + // Add a scroll event listener when the component mounts + window.addEventListener("scroll", handleScroll); + + // Clean up the event listener when the component unmounts + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); + return ( + <> + + {toTop && ( + + )} + + ); +} diff --git a/components/shared/bugReport.js b/components/shared/bugReport.js new file mode 100644 index 0000000..9b99016 --- /dev/null +++ b/components/shared/bugReport.js @@ -0,0 +1,200 @@ +import { Fragment, useState } from "react"; +import { Dialog, Listbox, Transition } from "@headlessui/react"; +import { CheckIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; +import { toast } from "react-toastify"; + +const severityOptions = [ + { id: 1, name: "Low" }, + { id: 2, name: "Medium" }, + { id: 3, name: "High" }, + { id: 4, name: "Critical" }, +]; + +const BugReportForm = ({ isOpen, setIsOpen }) => { + const [bugDescription, setBugDescription] = useState(""); + const [severity, setSeverity] = useState(severityOptions[0]); + + function closeModal() { + setIsOpen(false); + setBugDescription(""); + setSeverity(severityOptions[0]); + } + + const handleSubmit = async (e) => { + e.preventDefault(); + + const bugReport = { + desc: bugDescription, + severity: severity.name, + url: window.location.href, + createdAt: new Date().toISOString(), + }; + + try { + const res = await fetch("/api/v2/admin/bug-report", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + data: bugReport, + }), + }); + + const json = await res.json(); + toast.success(json.message, { + hideProgressBar: true, + theme: "colored", + }); + closeModal(); + } catch (err) { + console.log(err); + toast.error("Something went wrong: " + err.message, { + hideProgressBar: true, + theme: "colored", + }); + } + }; + + return ( + <> + + + +
+ + +
+
+ + +
+

+ Report a Bug +

+
+
+
+ + +
+ +
+ + + + {severity.name} + + + + + + + {severityOptions.map((person, personIdx) => ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active + ? "bg-secondary/50 text-white" + : "text-gray-400" + }` + } + value={person} + > + {({ selected }) => ( + <> + + {person.name} + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ + ); +}; + +export default BugReportForm; diff --git a/components/shared/footer.js b/components/shared/footer.js new file mode 100644 index 0000000..91af5a8 --- /dev/null +++ b/components/shared/footer.js @@ -0,0 +1,223 @@ +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import { parseCookies, setCookie } from "nookies"; +import Image from "next/image"; + +function Footer() { + const [year] = useState(new Date().getFullYear()); + const [season] = useState(getCurrentSeason()); + + const [lang, setLang] = useState("en"); + const [checked, setChecked] = useState(false); + const [cookie, setCookies] = useState(null); + + const router = useRouter(); + + useEffect(() => { + let lang = null; + if (!cookie) { + const cookie = parseCookies(); + lang = cookie.lang || null; + setCookies(cookie); + } + if (lang === "en" || lang === null) { + setLang("en"); + setChecked(false); + } else if (lang === "id") { + setLang("id"); + setChecked(true); + } + }, []); + + function switchLang() { + setChecked(!checked); + if (checked) { + console.log("switching to en"); + setCookie(null, "lang", "en", { + maxAge: 365 * 24 * 60 * 60, + path: "/", + }); + router.push("/en"); + } else { + router.push("/id"); + } + } + + return ( +
+
+
+
+ {/*
*/} + {/* Website Logo */} +
moopa
+

+ This site does not store any files on our server, we only linked + to the media which is hosted on 3rd party services. +

+ {/*
*/} +
+
+
+
    +
  • + + This Season + +
  • +
  • + Popular Anime +
  • +
  • + Popular Manga +
  • +
  • + Donate +
  • +
+
    +
  • + + Movies + +
  • +
  • + TV Shows +
  • +
  • + DMCA +
  • +
  • + + Github + +
  • +
+
+
+
+
+
+
+

+ © {new Date().getFullYear()} moopa.live | Website Made by{" "} + Factiven +

+
+ {/* Github Icon */} + + + + + + + + + + + + + + {/* Discord Icon */} + + + + + + + {/* Kofi */} + + + + + + + +
+
+
+
+ ); +} + +export default Footer; + +function getCurrentSeason() { + const now = new Date(); + const month = now.getMonth() + 1; // getMonth() returns 0-based index + + switch (month) { + case 12: + case 1: + case 2: + return "WINTER"; + case 3: + case 4: + case 5: + return "SPRING"; + case 6: + case 7: + case 8: + return "SUMMER"; + case 9: + case 10: + case 11: + return "FALL"; + default: + return "UNKNOWN SEASON"; + } +} diff --git a/components/videoPlayer.js b/components/videoPlayer.js deleted file mode 100644 index f35f4f0..0000000 --- a/components/videoPlayer.js +++ /dev/null @@ -1,412 +0,0 @@ -import Player from "../lib/Artplayer"; -import { useEffect, useState } from "react"; -import { useAniList } from "../lib/anilist/useAnilist"; -import artplayerPluginHlsQuality from "artplayer-plugin-hls-quality"; -import { useRouter } from "next/router"; - -const fontSize = [ - { - html: "Small", - size: "16px", - }, - { - html: "Medium", - size: "36px", - }, - { - html: "Large", - size: "56px", - }, -]; - -export default function VideoPlayer({ - info, - data, - id, - progress, - session, - aniId, - skip, - title, - poster, - proxy, - provider, - track, - aniTitle, - timeWatched, - dub, -}) { - const [url, setUrl] = useState(""); - const [source, setSource] = useState([]); - const { markProgress } = useAniList(session); - - const router = useRouter(); - - const [resolution, setResolution] = useState("auto"); - const [subSize, setSubSize] = useState({ size: "16px", html: "Small" }); - const [defSize, setDefSize] = useState(); - const [subtitle, setSubtitle] = useState(); - const [defSub, setDefSub] = useState(); - - const [autoPlay, setAutoPlay] = useState(false); - - useEffect(() => { - const resol = localStorage.getItem("quality"); - const sub = JSON.parse(localStorage.getItem("subSize")); - if (resol) { - setResolution(resol); - } - - if (provider === "zoro") { - const size = fontSize.map((i) => { - const isDefault = !sub ? i.html === "Small" : i.html === sub?.html; - return { - ...(isDefault && { default: true }), - html: i.html, - size: i.size, - }; - }); - - const defSize = size?.find((i) => i?.default === true); - setDefSize(defSize); - setSubSize(size); - } - - async function compiler() { - try { - const referer = JSON.stringify(data?.headers); - const source = data.sources.map((items) => { - const isDefault = - provider !== "gogoanime" - ? items.quality === "default" || items.quality === "auto" - : resolution === "auto" - ? items.quality === "default" || items.quality === "auto" - : items.quality === resolution; - return { - ...(isDefault && { default: true }), - html: items.quality === "default" ? "adaptive" : items.quality, - url: `${proxy}/proxy/m3u8/${encodeURIComponent( - String(items.url) - )}/${encodeURIComponent(String(referer))}`, - }; - }); - - const defSource = source?.find((i) => i?.default === true); - - if (defSource) { - setUrl(defSource.url); - } - - if (provider === "zoro") { - const subtitle = data?.subtitles - .filter((subtitle) => subtitle.lang !== "Thumbnails") - .map((subtitle) => { - const isEnglish = subtitle.lang === "English"; - return { - ...(isEnglish && { default: true }), - url: subtitle.url, - html: `${subtitle.lang}`, - }; - }); - - const defSub = data?.subtitles.find((i) => i.lang === "English"); - - setDefSub(defSub?.url); - - setSubtitle(subtitle); - } - - setSource(source); - } catch (error) { - console.error(error); - } - } - compiler(); - }, [data, resolution]); - - return ( - <> - {url && ( - level.height + "P", - - // I18n - title: "Quality", - auto: "Auto", - }), - ], - }), - ...(provider === "zoro" && { - subtitle: { - url: `${defSub}`, - // type: "vtt", - encoding: "utf-8", - default: true, - name: "English", - escape: false, - style: { - color: "#FFFF", - fontSize: `${defSize?.size}`, - fontFamily: localStorage.getItem("font") - ? localStorage.getItem("font") - : "Arial", - textShadow: localStorage.getItem("subShadow") - ? JSON.parse(localStorage.getItem("subShadow")).value - : "0px 0px 10px #000000", - }, - }, - }), - }} - id={aniId} - res={resolution} - quality={source} - subSize={subSize} - subtitles={subtitle} - provider={provider} - track={track} - autoplay={autoPlay} - setautoplay={setAutoPlay} - style={{ - width: "100%", - height: "100%", - margin: "0 auto 0", - }} - getInstance={(art) => { - art.on("ready", () => { - const seek = art.storage.get(id); - const seekTime = seek?.timeWatched || 0; - const duration = art.duration; - const percentage = seekTime / duration; - const percentagedb = timeWatched / duration; - - if (subSize) { - art.subtitle.style.fontSize = subSize?.size; - } - - if (percentage >= 0.9 || percentagedb >= 0.9) { - art.currentTime = 0; - console.log("Video started from the beginning"); - } else if (timeWatched) { - art.currentTime = timeWatched; - } else { - art.currentTime = seekTime; - } - }); - - let marked = 0; - - art.on("video:playing", () => { - if (!session) return; - const intervalId = setInterval(async () => { - const resp = await fetch("/api/user/update/episode", { - method: "PUT", - body: JSON.stringify({ - name: session?.user?.name, - id: String(aniId), - watchId: id, - title: track.playing?.title || aniTitle, - aniTitle: aniTitle, - image: track.playing?.image || info?.coverImage?.extraLarge, - number: Number(progress), - duration: art.duration, - timeWatched: art.currentTime, - provider: provider, - nextId: track.next?.id, - nextNumber: Number(track.next?.number), - dub: dub ? true : false, - }), - }); - // console.log("updating db", { track }); - }, 5000); - - art.on("video:pause", () => { - clearInterval(intervalId); - }); - - art.on("video:ended", () => { - clearInterval(intervalId); - }); - - art.on("destroy", () => { - clearInterval(intervalId); - // console.log("clearing interval"); - }); - }); - - art.on("video:playing", () => { - const interval = setInterval(async () => { - art.storage.set(id, { - aniId: String(aniId), - watchId: id, - title: track?.playing?.title || aniTitle, - aniTitle: aniTitle, - image: track?.playing?.image || info?.coverImage?.extraLarge, - episode: Number(progress), - duration: art.duration, - timeWatched: art.currentTime, - provider: provider, - nextId: track?.next?.id, - nextNumber: track?.next?.number, - dub: dub ? true : false, - createdAt: new Date().toISOString(), - }); - }, 5000); - - art.on("video:pause", () => { - clearInterval(interval); - }); - - art.on("video:ended", () => { - clearInterval(interval); - }); - - art.on("destroy", () => { - clearInterval(interval); - }); - }); - - art.on("resize", () => { - art.subtitle.style({ - fontSize: art.height * 0.05 + "px", - }); - }); - - art.on("video:timeupdate", async () => { - if (!session) return; - - var currentTime = art.currentTime; - const duration = art.duration; - const percentage = currentTime / duration; - - if (percentage >= 0.9) { - // use >= instead of > - if (marked < 1) { - marked = 1; - markProgress(aniId, progress); - } - } - }); - - art.on("video:ended", () => { - if (!track?.next) return; - if (localStorage.getItem("autoplay") === "true") { - art.controls.add({ - name: "next-button", - position: "top", - html: '
', - click: function (...args) { - if (track?.next) { - router.push( - `/en/anime/watch/${aniId}/${provider}?id=${encodeURIComponent( - track?.next?.id - )}&num=${track?.next?.number}${ - dub ? `&dub=${dub}` : "" - }` - ); - } - }, - }); - - const button = document.querySelector(".next-button"); - - function stopTimeout() { - clearTimeout(timeoutId); - button.classList.remove("progress"); - } - - let timeoutId = setTimeout(() => { - art.controls.remove("next-button"); - if (track?.next) { - router.push( - `/en/anime/watch/${aniId}/${provider}?id=${encodeURIComponent( - track?.next?.id - )}&num=${track?.next?.number}${dub ? `&dub=${dub}` : ""}` - ); - } - }, 7000); - - button.addEventListener("mouseover", stopTimeout); - } - }); - - art.on("video:timeupdate", () => { - var currentTime = art.currentTime; - // console.log(art.currentTime); - - if ( - skip?.op && - currentTime >= skip.op.interval.startTime && - currentTime <= skip.op.interval.endTime - ) { - // Add the layer if it's not already added - if (!art.controls["op"]) { - // Remove the other control if it's already added - if (art.controls["ed"]) { - art.controls.remove("ed"); - } - - // Add the control - art.controls.add({ - name: "op", - position: "top", - html: '', - click: function (...args) { - art.seek = skip.op.interval.endTime; - }, - }); - } - } else if ( - skip?.ed && - currentTime >= skip.ed.interval.startTime && - currentTime <= skip.ed.interval.endTime - ) { - // Add the layer if it's not already added - if (!art.controls["ed"]) { - // Remove the other control if it's already added - if (art.controls["op"]) { - art.controls.remove("op"); - } - - // Add the control - art.controls.add({ - name: "ed", - position: "top", - html: '', - click: function (...args) { - art.seek = skip.ed.interval.endTime; - }, - }); - } - } else { - // Remove the controls if they're added - if (art.controls["op"]) { - art.controls.remove("op"); - } - if (art.controls["ed"]) { - art.controls.remove("ed"); - } - } - }); - }} - /> - )} - - ); -} diff --git a/components/watch/player/artplayer.js b/components/watch/player/artplayer.js new file mode 100644 index 0000000..4eb766d --- /dev/null +++ b/components/watch/player/artplayer.js @@ -0,0 +1,325 @@ +import { useEffect, useRef } from "react"; +import Artplayer from "artplayer"; +import Hls from "hls.js"; +import { useWatchProvider } from "../../../lib/hooks/watchPageProvider"; +import { seekBackward, seekForward } from "./component/overlay"; +import artplayerPluginHlsQuality from "artplayer-plugin-hls-quality"; + +export default function NewPlayer({ + playerRef, + option, + getInstance, + provider, + defSub, + defSize, + subtitles, + subSize, + res, + quality, + ...rest +}) { + const artRef = useRef(null); + const { setTheaterMode, setPlayerState, setAutoPlay } = useWatchProvider(); + + function playM3u8(video, url, art) { + if (Hls.isSupported()) { + if (art.hls) art.hls.destroy(); + const hls = new Hls(); + hls.loadSource(url); + hls.attachMedia(video); + art.hls = hls; + art.on("destroy", () => hls.destroy()); + } else if (video.canPlayType("application/vnd.apple.mpegurl")) { + video.src = url; + } else { + art.notice.show = "Unsupported playback format: m3u8"; + } + } + + useEffect(() => { + const art = new Artplayer({ + ...option, + container: artRef.current, + type: "m3u8", + customType: { + m3u8: playM3u8, + }, + ...(provider === "zoro" && { + subtitle: { + url: `${defSub}`, + // type: "vtt", + encoding: "utf-8", + default: true, + name: "English", + escape: false, + style: { + color: "#FFFF", + fontSize: `${defSize?.size}`, + fontFamily: localStorage.getItem("font") + ? localStorage.getItem("font") + : "Arial", + textShadow: localStorage.getItem("subShadow") + ? JSON.parse(localStorage.getItem("subShadow")).value + : "0px 0px 10px #000000", + }, + }, + }), + + plugins: [ + artplayerPluginHlsQuality({ + // Show quality in setting + setting: true, + + // Get the resolution text from level + getResolution: (level) => level.height + "P", + + // I18n + title: "Quality", + auto: "Auto", + }), + ], + + settings: [ + // provider === "gogoanime" && + { + html: "Autoplay Next", + icon: '', + tooltip: "ON/OFF", + switch: localStorage.getItem("autoplay") === "true" ? true : false, + onSwitch: function (item) { + // setPlayNext(!item.switch); + localStorage.setItem("autoplay", !item.switch); + return !item.switch; + }, + }, + { + html: "Autoplay Video", + icon: '', + // icon: '', + tooltip: "ON/OFF", + switch: + localStorage.getItem("autoplay_video") === "true" ? true : false, + onSwitch: function (item) { + setAutoPlay(!item.switch); + localStorage.setItem("autoplay_video", !item.switch); + return !item.switch; + }, + }, + { + html: "Alternative Quality", + width: 250, + tooltip: `${res}`, + selector: quality?.alt, + icon: '', + onSelect: function (item) { + art.switchQuality(item.url, item.html); + localStorage.setItem("quality", item.html); + return item.html; + }, + }, + { + html: "Server", + width: 250, + tooltip: `${quality?.server[0].html}`, + icon: '', + selector: quality?.server, + onSelect: function (item) { + art.switchQuality(item.url, item.html); + localStorage.setItem("quality", item.html); + return item.html; + }, + }, + provider === "zoro" && { + html: "Subtitles", + icon: '', + width: 300, + tooltip: "Settings", + selector: [ + { + html: "Display", + icon: '', + tooltip: "Show", + switch: true, + onSwitch: function (item) { + item.tooltip = item.switch ? "Hide" : "Show"; + art.subtitle.show = !item.switch; + return !item.switch; + }, + }, + { + html: "Font Size", + icon: '', + selector: subSize, + onSelect: function (item) { + if (item.html === "Small") { + art.subtitle.style({ fontSize: "16px" }); + localStorage.setItem( + "subSize", + JSON.stringify({ + size: "16px", + html: "Small", + }) + ); + } else if (item.html === "Medium") { + art.subtitle.style({ fontSize: "36px" }); + localStorage.setItem( + "subSize", + JSON.stringify({ + size: "36px", + html: "Medium", + }) + ); + } else if (item.html === "Large") { + art.subtitle.style({ fontSize: "56px" }); + localStorage.setItem( + "subSize", + JSON.stringify({ + size: "56px", + html: "Large", + }) + ); + } + }, + }, + { + html: "Language", + icon: '', + tooltip: "English", + selector: [...subtitles], + onSelect: function (item) { + art.subtitle.switch(item.url, { + name: item.html, + }); + return item.html; + }, + }, + { + html: "Font Family", + tooltip: localStorage.getItem("font") + ? localStorage.getItem("font") + : "Arial", + selector: [ + { html: "Arial" }, + { html: "Comic Sans MS" }, + { html: "Verdana" }, + { html: "Tahoma" }, + { html: "Trebuchet MS" }, + { html: "Times New Roman" }, + { html: "Georgia" }, + { html: "Impact " }, + { html: "Andalé Mono" }, + { html: "Palatino" }, + { html: "Baskerville" }, + { html: "Garamond" }, + { html: "Courier New" }, + { html: "Brush Script MT" }, + ], + onSelect: function (item) { + art.subtitle.style({ fontFamily: item.html }); + localStorage.setItem("font", item.html); + return item.html; + }, + }, + { + html: "Font Shadow", + tooltip: localStorage.getItem("subShadow") + ? JSON.parse(localStorage.getItem("subShadow")).shadow + : "Default", + selector: [ + { html: "None", value: "none" }, + { + html: "Uniform", + value: + "2px 2px 0px #000, -2px -2px 0px #000, 2px -2px 0px #000, -2px 2px 0px #000", + }, + { html: "Raised", value: "-1px 2px 3px rgba(0, 0, 0, 1)" }, + { html: "Depressed", value: "-2px -3px 3px rgba(0, 0, 0, 1)" }, + { html: "Glow", value: "0 0 10px rgba(0, 0, 0, 0.8)" }, + { + html: "Block", + value: + "-3px 3px 4px rgba(0, 0, 0, 1),2px 2px 4px rgba(0, 0, 0, 1),1px -1px 3px rgba(0, 0, 0, 1),-3px -2px 4px rgba(0, 0, 0, 1)", + }, + ], + onSelect: function (item) { + art.subtitle.style({ textShadow: item.value }); + localStorage.setItem( + "subShadow", + JSON.stringify({ shadow: item.html, value: item.value }) + ); + return item.html; + }, + }, + ], + }, + ].filter(Boolean), + controls: [ + { + name: "theater-button", + index: 11, + position: "right", + tooltip: "Theater (t)", + html: '

', + click: function (...args) { + setPlayerState((prev) => ({ + ...prev, + currentTime: art.currentTime, + isPlaying: art.playing, + })); + setTheaterMode((prev) => !prev); + }, + }, + seekBackward, + seekForward, + ], + }); + + playerRef.current = art; + + art.events.proxy(document, "keydown", (event) => { + // Check if the focus is on an input field or textarea + const isInputFocused = + document.activeElement.tagName === "INPUT" || + document.activeElement.tagName === "TEXTAREA"; + + if (!isInputFocused) { + if (event.key === "f" || event.key === "F") { + art.fullscreen = !art.fullscreen; + } + + if (event.key === "t" || event.key === "T") { + setPlayerState((prev) => ({ + ...prev, + currentTime: art.currentTime, + isPlaying: art.playing, + })); + setTheaterMode((prev) => !prev); + } + } + }); + + art.events.proxy(document, "keypress", (event) => { + // Check if the focus is on an input field or textarea + const isInputFocused = + document.activeElement.tagName === "INPUT" || + document.activeElement.tagName === "TEXTAREA"; + + if (!isInputFocused && event.code === "Space") { + event.preventDefault(); + art.playing ? art.pause() : art.play(); + } + }); + + if (getInstance && typeof getInstance === "function") { + getInstance(art); + } + + return () => { + if (art && art.destroy) { + art.destroy(false); + } + }; + }, []); + + return
; +} diff --git a/components/watch/player/component/controls/quality.js b/components/watch/player/component/controls/quality.js new file mode 100644 index 0000000..08dbd0e --- /dev/null +++ b/components/watch/player/component/controls/quality.js @@ -0,0 +1,15 @@ +import artplayerPluginHlsQuality from "artplayer-plugin-hls-quality"; + +export const QualityPlugins = [ + artplayerPluginHlsQuality({ + // Show quality in setting + setting: true, + + // Get the resolution text from level + getResolution: (level) => level.height + "P", + + // I18n + title: "Quality", + auto: "Auto", + }), +]; diff --git a/components/watch/player/component/controls/subtitle.js b/components/watch/player/component/controls/subtitle.js new file mode 100644 index 0000000..02075f7 --- /dev/null +++ b/components/watch/player/component/controls/subtitle.js @@ -0,0 +1,3 @@ +import { useState } from "react"; + +export default function getSubtitles() {} diff --git a/components/watch/player/component/overlay.js b/components/watch/player/component/overlay.js new file mode 100644 index 0000000..1d5ac27 --- /dev/null +++ b/components/watch/player/component/overlay.js @@ -0,0 +1,57 @@ +/** + * @type {import("artplayer/types/icons".Icons)} + */ +export const icons = { + screenshot: + '', + play: '', + pause: + '', + volume: + '', + fullscreenOff: + '', + fullscreenOn: + '', +}; + +export const backButton = { + name: "back-button", + index: 10, + position: "top", + html: "

Komi-san wa, Komyushou desu.

Episode 1

", + // tooltip: "Your Button", + click: function (...args) { + console.info("click", args); + }, + mounted: function (...args) { + console.info("mounted", args); + }, +}; + +export const seekBackward = { + index: 10, + name: "fast-rewind", + position: "left", + html: '', + tooltip: "Backward 5s", + click: function () { + art.backward = 5; + }, +}; + +export const seekForward = { + index: 11, + name: "fast-forward", + position: "left", + html: '', + tooltip: "Forward 5s", + click: function () { + art.forward = 5; + }, +}; + +// /** +// * @type {import("artplayer/types/component").ComponentOption} +// */ +// export const diff --git a/components/watch/player/playerComponent.js b/components/watch/player/playerComponent.js new file mode 100644 index 0000000..d498384 --- /dev/null +++ b/components/watch/player/playerComponent.js @@ -0,0 +1,478 @@ +import React, { useEffect, useState } from "react"; +import NewPlayer from "./artplayer"; +import { icons } from "./component/overlay"; +import { useWatchProvider } from "../../../lib/hooks/watchPageProvider"; +import { useRouter } from "next/router"; +import { useAniList } from "../../../lib/anilist/useAnilist"; + +export function calculateAspectRatio(width, height) { + const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b)); + const divisor = gcd(width, height); + const aspectRatio = `${width / divisor}/${height / divisor}`; + return aspectRatio; +} + +const fontSize = [ + { + html: "Small", + size: "16px", + }, + { + html: "Medium", + size: "36px", + }, + { + html: "Large", + size: "56px", + }, +]; + +export default function PlayerComponent({ + playerRef, + session, + id, + info, + watchId, + proxy, + dub, + timeWatched, + skip, + track, + data, + provider, + className, +}) { + const { + aspectRatio, + setAspectRatio, + playerState, + setPlayerState, + autoplay, + marked, + setMarked, + } = useWatchProvider(); + + const router = useRouter(); + + const { markProgress } = useAniList(session); + + const [url, setUrl] = useState(""); + const [resolution, setResolution] = useState("auto"); + const [source, setSource] = useState([]); + const [subSize, setSubSize] = useState({ size: "16px", html: "Small" }); + const [defSize, setDefSize] = useState(); + const [subtitle, setSubtitle] = useState(); + const [defSub, setDefSub] = useState(); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setLoading(true); + const resol = localStorage.getItem("quality"); + const sub = JSON.parse(localStorage.getItem("subSize")); + if (resol) { + setResolution(resol); + } + + if (provider === "zoro") { + const size = fontSize.map((i) => { + const isDefault = !sub ? i.html === "Small" : i.html === sub?.html; + return { + ...(isDefault && { default: true }), + html: i.html, + size: i.size, + }; + }); + + const defSize = size?.find((i) => i?.default === true); + setDefSize(defSize); + setSubSize(size); + } + + async function compiler() { + try { + const referer = JSON.stringify(data?.headers); + const source = data?.sources?.map((items) => { + const isDefault = + provider !== "gogoanime" + ? items.quality === "default" || items.quality === "auto" + : resolution === "auto" + ? items.quality === "default" || items.quality === "auto" + : items.quality === resolution; + return { + ...(isDefault && { default: true }), + html: items.quality === "default" ? "main" : items.quality, + url: `${proxy}/proxy/m3u8/${encodeURIComponent( + String(items.url) + )}/${encodeURIComponent(String(referer))}`, + }; + }); + + const defSource = source?.find((i) => i?.default === true); + + if (defSource) { + setUrl(defSource.url); + } + + if (provider === "zoro") { + const subtitle = data?.subtitles + .filter((subtitle) => subtitle.lang !== "Thumbnails") + .map((subtitle) => { + const isEnglish = subtitle.lang === "English"; + return { + ...(isEnglish && { default: true }), + url: subtitle.url, + html: `${subtitle.lang}`, + }; + }); + + const defSub = data?.subtitles.find((i) => i.lang === "English"); + + setDefSub(defSub?.url); + + setSubtitle(subtitle); + } + + const alt = source?.filter( + (i) => + i?.html !== "main" && + i?.html !== "auto" && + i?.html !== "default" && + i?.html !== "backup" + ); + const server = source?.filter( + (i) => + i?.html === "main" || + i?.html === "auto" || + i?.html === "default" || + i?.html === "backup" + ); + + setSource({ alt, server }); + setLoading(false); + } catch (error) { + console.error(error); + } + } + compiler(); + + return () => { + setUrl(""); + setSource([]); + setSubtitle([]); + setLoading(true); + }; + }, [provider, data]); + + /** + * @param {import("artplayer")} art + */ + function getInstance(art) { + art.on("ready", () => { + const autoplay = localStorage.getItem("autoplay_video") || false; + + if (autoplay === "true" || autoplay === true) { + if (playerState.currentTime === 0) { + art.play(); + } else { + if (playerState.isPlaying) { + art.play(); + } else { + art.pause(); + } + } + } else { + if (playerState.isPlaying) { + art.play(); + } else { + art.pause(); + } + } + art.seek = playerState.currentTime; + }); + + art.on("ready", () => { + if (playerState.currentTime !== 0) return; + const seek = art.storage.get(id); + const seekTime = seek?.timeWatched || 0; + const duration = art.duration; + const percentage = seekTime / duration; + const percentagedb = timeWatched / duration; + + if (subSize) { + art.subtitle.style.fontSize = subSize?.size; + } + + if (percentage >= 0.9 || percentagedb >= 0.9) { + art.currentTime = 0; + console.log("Video started from the beginning"); + } else if (timeWatched) { + art.currentTime = timeWatched; + } else { + art.currentTime = seekTime; + } + }); + + art.on("play", () => { + art.notice.show = ""; + setPlayerState({ ...playerState, isPlaying: true }); + }); + art.on("pause", () => { + art.notice.show = ""; + setPlayerState({ ...playerState, isPlaying: false }); + }); + + art.on("resize", () => { + art.subtitle.style({ + fontSize: art.height * 0.05 + "px", + }); + }); + + let mark = 0; + + art.on("video:timeupdate", async () => { + if (!session) return; + + var currentTime = art.currentTime; + const duration = art.duration; + const percentage = currentTime / duration; + + if (percentage >= 0.9) { + // use >= instead of > + if (mark < 1 && marked < 1) { + mark = 1; + setMarked(1); + markProgress(info.id, track.playing.number); + } + } + }); + + art.on("video:playing", () => { + if (!session) return; + const intervalId = setInterval(async () => { + await fetch("/api/user/update/episode", { + method: "PUT", + body: JSON.stringify({ + name: session?.user?.name, + id: String(info?.id), + watchId: watchId, + title: + track.playing?.title || info.title?.romaji || info.title?.english, + aniTitle: info.title?.romaji || info.title?.english, + image: track.playing?.img || info?.coverImage?.extraLarge, + number: Number(track.playing?.number), + duration: art.duration, + timeWatched: art.currentTime, + provider: provider, + nextId: track.next?.id, + nextNumber: Number(track.next?.number), + dub: dub ? true : false, + }), + }); + // console.log("updating db", { track }); + }, 5000); + + art.on("video:pause", () => { + clearInterval(intervalId); + }); + + art.on("video:ended", () => { + clearInterval(intervalId); + }); + + art.on("destroy", () => { + clearInterval(intervalId); + // console.log("clearing interval"); + }); + }); + + art.on("video:playing", () => { + const interval = setInterval(async () => { + art.storage.set(watchId, { + aniId: String(info.id), + watchId: watchId, + title: + track.playing?.title || info.title?.romaji || info.title?.english, + aniTitle: info.title?.romaji || info.title?.english, + image: track?.playing?.img || info?.coverImage?.extraLarge, + episode: Number(track.playing?.number), + duration: art.duration, + timeWatched: art.currentTime, + provider: provider, + nextId: track?.next?.id, + nextNumber: track?.next?.number, + dub: dub ? true : false, + createdAt: new Date().toISOString(), + }); + }, 5000); + + art.on("video:pause", () => { + clearInterval(interval); + }); + + art.on("video:ended", () => { + clearInterval(interval); + }); + + art.on("destroy", () => { + clearInterval(interval); + }); + }); + + art.on("video:loadedmetadata", () => { + // get raw video width and height + // console.log(art.video.videoWidth, art.video.videoHeight); + const aspect = calculateAspectRatio( + art.video.videoWidth, + art.video.videoHeight + ); + + setAspectRatio(aspect); + }); + + art.on("video:timeupdate", () => { + var currentTime = art.currentTime; + // console.log(art.currentTime); + + if ( + skip?.op && + currentTime >= skip.op.interval.startTime && + currentTime <= skip.op.interval.endTime + ) { + // Add the layer if it's not already added + if (!art.controls["op"]) { + // Remove the other control if it's already added + if (art.controls["ed"]) { + art.controls.remove("ed"); + } + + // Add the control + art.controls.add({ + name: "op", + position: "top", + html: '', + click: function (...args) { + art.seek = skip.op.interval.endTime; + }, + }); + } + } else if ( + skip?.ed && + currentTime >= skip.ed.interval.startTime && + currentTime <= skip.ed.interval.endTime + ) { + // Add the layer if it's not already added + if (!art.controls["ed"]) { + // Remove the other control if it's already added + if (art.controls["op"]) { + art.controls.remove("op"); + } + + // Add the control + art.controls.add({ + name: "ed", + position: "top", + html: '', + click: function (...args) { + art.seek = skip.ed.interval.endTime; + }, + }); + } + } else { + // Remove the controls if they're added + if (art.controls["op"]) { + art.controls.remove("op"); + } + if (art.controls["ed"]) { + art.controls.remove("ed"); + } + } + }); + + art.on("video:ended", () => { + if (!track?.next) return; + if (localStorage.getItem("autoplay") === "true") { + art.controls.add({ + name: "next-button", + position: "top", + html: '
', + click: function (...args) { + if (track?.next) { + router.push( + `/en/anime/watch/${ + info?.id + }/${provider}?id=${encodeURIComponent(track?.next?.id)}&num=${ + track?.next?.number + }${dub ? `&dub=${dub}` : ""}` + ); + } + }, + }); + + const button = document.querySelector(".next-button"); + + function stopTimeout() { + clearTimeout(timeoutId); + button.classList.remove("progress"); + } + + let timeoutId = setTimeout(() => { + art.controls.remove("next-button"); + if (track?.next) { + router.push( + `/en/anime/watch/${info?.id}/${provider}?id=${encodeURIComponent( + track?.next?.id + )}&num=${track?.next?.number}${dub ? `&dub=${dub}` : ""}` + ); + } + }, 7000); + + button.addEventListener("mouseover", stopTimeout); + } + }); + } + + /** + * @type {import("artplayer/types/option").Option} + */ + const option = { + url: url, + title: "title", + autoplay: autoplay ? true : false, + autoSize: false, + fullscreen: true, + autoOrientation: true, + icons: icons, + setting: true, + screenshot: true, + hotkey: true, + }; + + return ( +
+
+ {!loading && track && url && ( + + )} +
+
+ ); +} diff --git a/components/watch/player/utils/getZoroSource.js b/components/watch/player/utils/getZoroSource.js new file mode 100644 index 0000000..e69de29 diff --git a/components/watch/primary/details.js b/components/watch/primary/details.js new file mode 100644 index 0000000..32e1391 --- /dev/null +++ b/components/watch/primary/details.js @@ -0,0 +1,189 @@ +import { useEffect, useState } from "react"; +import { useAniList } from "../../../lib/anilist/useAnilist"; +import Skeleton from "react-loading-skeleton"; +import DisqusComments from "../../disqus"; +import Image from "next/image"; + +export default function Details({ + info, + session, + epiNumber, + description, + id, + onList, + setOnList, + handleOpen, + disqus, +}) { + const [showComments, setShowComments] = useState(false); + const { markPlanning } = useAniList(session); + + function handlePlan() { + if (onList === false) { + markPlanning(info.id); + setOnList(true); + } + } + + useEffect(() => { + setShowComments(false); + }, [id]); + + return ( +
+ {/*
*/} +
+
+ {info ? ( + Anime Cover + ) : ( + + )} +
+
+
+

+ Studios +

+
+ {info ? ( + info.studios?.edges[0].node.name + ) : ( + + )} +
+
+
+ { + session ? handlePlan() : handleOpen(); + }} + className={`w-8 h-8 hover:fill-white text-white hover:cursor-pointer ${ + onList ? "fill-white" : "" + }`} + > + + +
+
+
+
+

+ Status +

+
{info ? info.status : }
+
+
+

+ Titles +

+
+ {info ? ( + <> +
+ {info.title?.romaji || ""} +
+
+ {info.title?.english || ""} +
+
+ {info.title?.native || ""} +
+ + ) : ( + + )} +
+
+
+
+ {/*
*/} +
+ {info && + info.genres?.map((item, index) => ( +
+ {item} +
+ ))} +
+ {/*
*/} +
+ {info && ( +

+ )} +

+ {/* {
} */} + {!showComments && ( +
+ +
+ )} + {showComments && ( +
+ {info && ( +
+ +
+ )} +
+ )} +
+ ); +} diff --git a/components/watch/secondary/episodeLists.js b/components/watch/secondary/episodeLists.js new file mode 100644 index 0000000..8a057ce --- /dev/null +++ b/components/watch/secondary/episodeLists.js @@ -0,0 +1,143 @@ +import Skeleton from "react-loading-skeleton"; +import Image from "next/image"; +import Link from "next/link"; + +export default function EpisodeLists({ + info, + map, + providerId, + watchId, + episode, + artStorage, + dub, +}) { + const progress = info.mediaListEntry?.progress; + return ( +
+

Up Next

+
+ {episode && episode.length > 0 ? ( + map?.some( + (item) => + (item?.img || item?.image) && + !item?.img?.includes("https://s4.anilist.co/") + ) > 0 ? ( + episode.map((item) => { + const time = artStorage?.[item.id]?.timeWatched; + const duration = artStorage?.[item.id]?.duration; + let prog = (time / duration) * 100; + if (prog > 90) prog = 100; + + const mapData = map?.find((i) => i.number === item.number); + return ( + +
+
+ {/*
*/} + Anime Cover + {/* )} */} + + + Episode {item?.number} + + {item.id == watchId && ( +
+ + + +
+ )} +
+
+
+

+ {mapData?.title || info?.title?.romaji} +

+

+ {mapData?.description || `Episode ${item.number}`} +

+
+ + ); + }) + ) : ( + episode.map((item) => { + return ( + + Episode {item.number} + + ); + }) + ) + ) : ( + <> + {[1].map((item) => ( + + ))} + + )} +
+
+ ); +} -- cgit v1.2.3