diff options
Diffstat (limited to 'components')
29 files changed, 1591 insertions, 1813 deletions
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 ( - <div> - <div className="flex items-center justify-between lg:gap-3 px-5 z-40 "> - <h1 className="font-karla text-[20px] font-bold">Characters</h1> - {info?.length > 6 && ( - <div className="cursor-pointer font-karla" onClick={() => setShowAll(!showAll)}> - {showAll ? "show less" : "show more"} - </div> - )} - </div> - {/* for bigger device */} - <div className="hidden md:grid w-full grid-cols-1 gap-[10px] md:gap-4 md:grid-cols-3 md:pt-7 md:pb-5 px-3 md:px-5 pt-4"> - {info.slice(0, showAll ? info.length : 6).map((item, index) => { - return <a key={index} className="md:hover:scale-[1.02] snap-start hover:shadow-lg scale-100 transition-transform duration-200 ease-out w-full cursor-default"> - <div className="text-gray-300 space-x-4 col-span-1 flex w-full h-24 bg-secondary rounded-md overflow-hidden"> - <div className="relative h-full w-20"> - <Image - draggable={false} - src={ - item.node.image.large || - item.node.image.medium - } - width={500} - height={300} - alt={ - item.node.name.userPreferred || - item.node.name.full || - "Character Image" - } - className="h-full object-cover" - /> - </div> - <div className="py-2 flex flex-col justify-between"> - <p className="font-semibold">{item.node.name.full || item.node.name.userPreferred}</p> - <p>{item.role}</p> - </div> - </div> - </a> - })} - </div> - {/* for smaller devices */} - <div className="flex md:hidden h-full w-full select-none overflow-x-scroll overflow-y-hidden scrollbar-hide gap-4 pt-8 pb-4 px-5 z-30"> - {info.slice(0, showAll ? info.length : 6).map((item, index) => { - return <div key={index} className="flex flex-col gap-3 shrink-0 cursor-pointer"> - <a className="hover:scale-105 hover:shadow-lg duration-300 ease-out group relative"> - <div className="h-[190px] w-[135px] rounded-md z-30"> - <Image - draggable={false} - src={ - item.node.image.large || - item.node.image.medium - } - alt={ - item.node.name.userPreferred || - item.node.name.full || - "Character Image" - } - width={500} - height={300} - className="z-20 h-[190px] w-[135px] object-cover rounded-md brightness-90" - /> - </div> - </a> - <a className="w-[135px] lg:w-[185px] line-clamp-2"> - <h1 className="font-karla font-semibold text-[15px]">{item.node.name.full || item.node.name.userPreferred}</h1> - <h1 className="font-karla float-right italic text-[12px]">~{item.role}</h1> - </a> - </div> - })} + return ( + <div> + <div className="flex items-center justify-between lg:gap-3 px-5 z-40 "> + <h1 className="font-karla text-[20px] font-bold">Characters</h1> + {info?.length > 6 && ( + <div + className="cursor-pointer font-karla" + onClick={() => setShowAll(!showAll)} + > + {showAll ? "show less" : "show more"} + </div> + )} + </div> + {/* for bigger device */} + <div className="hidden md:grid w-full grid-cols-1 gap-[10px] md:gap-4 md:grid-cols-3 md:pt-7 md:pb-5 px-3 md:px-5 pt-4"> + {info.slice(0, showAll ? info.length : 6).map((item, index) => { + return ( + <a + key={index} + className="md:hover:scale-[1.02] snap-start hover:shadow-lg scale-100 transition-transform duration-200 ease-out w-full cursor-default" + > + <div className="text-gray-300 space-x-4 col-span-1 flex w-full h-24 bg-secondary rounded-md overflow-hidden"> + <div className="relative h-full w-20"> + <Image + draggable={false} + src={item.node.image.large || item.node.image.medium} + width={500} + height={300} + alt={ + item.node.name.userPreferred || + item.node.name.full || + "Character Image" + } + className="h-full object-cover" + /> + </div> + <div className="py-2 flex flex-col justify-between"> + <p className="font-semibold"> + {item.node.name.full || item.node.name.userPreferred} + </p> + <p>{item.role}</p> + </div> + </div> + </a> + ); + })} + </div> + {/* for smaller devices */} + <div className="flex md:hidden h-full w-full select-none overflow-x-scroll overflow-y-hidden scrollbar-hide gap-4 pt-8 pb-4 px-5 z-30"> + {info.slice(0, showAll ? info.length : 6).map((item, index) => { + return ( + <div + key={index} + className="flex flex-col gap-3 shrink-0 cursor-pointer" + > + <a className="hover:scale-105 hover:shadow-lg duration-300 ease-out group relative"> + <div className="h-[190px] w-[135px] rounded-md z-30"> + <Image + draggable={false} + src={item.node.image.large || item.node.image.medium} + alt={ + item.node.name.userPreferred || + item.node.name.full || + "Character Image" + } + width={500} + height={300} + className="z-20 h-[190px] w-[135px] object-cover rounded-md brightness-90" + /> + </div> + </a> + <a className="w-[135px] lg:w-[185px] line-clamp-2"> + <h1 className="font-karla font-semibold text-[15px]"> + {item.node.name.full || item.node.name.userPreferred} + </h1> + <h1 className="font-karla float-right italic text-[12px]"> + ~{item.role} + </h1> + </a> </div> - </div> - ); -}
\ No newline at end of file + ); + })} + </div> + </div> + ); +} 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 ( <> <div className="flex flex-col gap-5 px-3"> <div className="flex lg:flex-row flex-col gap-5 lg:gap-0 justify-between "> <div className="flex justify-between"> - <div className="flex items-center md:gap-5"> + <div className="flex items-center gap-4 md:gap-5"> {info && ( <h1 className="text-[20px] lg:text-2xl font-bold font-karla"> Episodes </h1> )} - {info.nextAiringEpisode?.timeUntilAiring && ( - <p className="hidden md:block bg-gray-100 text-gray-900 rounded-md px-2 font-karla font-medium"> - Ep {info.nextAiringEpisode.episode}{" "} - <span className="animate-pulse">{">>"}</span>{" "} - <span className="font-bold"> - {convertSecondsToTime( - info.nextAiringEpisode.timeUntilAiring - )}{" "} + {info?.status !== "NOT_YET_RELEASED" && ( + <button + type="button" + onClick={() => { + handleRefresh(); + setProviders(null); + setMapProviders(null); + }} + className="relative flex flex-col items-center w-5 h-5 group" + > + <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> + Refresh Episodes </span> - </p> + <svg + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + aria-hidden="true" + > + <path + clipRule="evenodd" + fillRule="evenodd" + d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" + /> + </svg> + </button> )} </div> @@ -267,7 +347,7 @@ export default function AnimeEpisode({ </> )} - <ChangeView + <ViewSelector view={view} setView={setView} episode={currentEpisodes} @@ -301,7 +381,7 @@ export default function AnimeEpisode({ key={index} index={index} info={info} - image={mapData?.image} + image={mapData?.img || mapData?.image} providerId={providerId} episode={episode} artStorage={artStorage} @@ -312,7 +392,7 @@ export default function AnimeEpisode({ {view === 2 && ( <ThumbnailDetail key={index} - image={mapData?.image} + image={mapData?.img || mapData?.image} title={mapData?.title} description={mapData?.description} index={index} @@ -346,7 +426,7 @@ export default function AnimeEpisode({ </div> ) ) : ( - <p>{providers.message}</p> + <p>{providers?.message}</p> )} </div> ) : ( 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 ( - <> - <div className="hidden lg:flex gap-8 w-full flex-nowrap"> - <div className="shrink-0 lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] relative"> - {info ? ( - <> - <div className="bg-image lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] bg-opacity-30 absolute backdrop-blur-lg z-10 -top-7" /> - <Image - src={info.coverImage.extraLarge || info.coverImage.large} - priority={true} - alt="poster anime" - height={700} - width={700} - className="object-cover lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] z-20 absolute rounded-md -top-7" - /> - <button - type="button" - className="bg-action flex-center z-20 h-[20px] w-[180px] absolute bottom-0 rounded-sm font-karla font-bold" - onClick={() => handleOpen()} - > - {!loading - ? statuses - ? statuses.name - : "Add to List" - : "Loading..."} - </button> - </> - ) : ( - <Skeleton className="h-[250px] w-[180px]" /> - )} - </div> - - <div className="hidden lg:flex w-full flex-col gap-5 h-[250px]"> - <div className="flex flex-col gap-2"> - <h1 - className="title font-inter font-bold text-[36px] text-white line-clamp-1" - title={info?.title?.romaji || info?.title?.english} - > - {info ? ( - info?.title?.romaji || info?.title?.english - ) : ( - <Skeleton width={450} /> - )} - </h1> - {info ? ( - <div className="flex gap-6"> - {info?.episodes && ( - <div - className={`dynamic-text rounded-md px-2 font-karla font-bold`} - style={color} - > - {info?.episodes} Episodes - </div> - )} - {info?.startDate?.year && ( - <div - className={`dynamic-text rounded-md px-2 font-karla font-bold`} - style={color} - > - {info?.startDate?.year} - </div> - )} - {info?.averageScore && ( - <div - className={`dynamic-text rounded-md px-2 font-karla font-bold`} - style={color} - > - {info?.averageScore}% - </div> - )} - {info?.type && ( - <div - className={`dynamic-text rounded-md px-2 font-karla font-bold`} - style={color} - > - {info?.type} - </div> - )} - {info?.status && ( - <div - className={`dynamic-text rounded-md px-2 font-karla font-bold`} - style={color} - > - {info?.status} - </div> - )} - <div - className={`dynamic-text rounded-md px-2 font-karla font-bold`} - style={color} - > - Sub | EN - </div> - </div> - ) : ( - <Skeleton width={240} height={32} /> - )} - </div> - {info ? ( - <p - dangerouslySetInnerHTML={{ __html: info?.description }} - className="overflow-y-scroll scrollbar-thin pr-2 scrollbar-thumb-secondary scrollbar-thumb-rounded-lg h-[140px]" - /> - ) : ( - <Skeleton className="h-[130px]" /> - )} - </div> - </div> - - <div> - <div className="flex gap-5 items-center"> - {info?.relations?.edges?.length > 0 && ( - <div className="p-3 lg:p-0 text-[20px] lg:text-2xl font-bold font-karla"> - Relations - </div> - )} - {info?.relations?.edges?.length > 3 && ( - <div - className="cursor-pointer" - onClick={() => setShowAll(!showAll)} - > - {showAll ? "show less" : "show more"} - </div> - )} - </div> - <div - className={`w-screen lg:w-full flex gap-5 overflow-x-scroll snap-x scroll-px-5 scrollbar-none lg:grid lg:grid-cols-3 justify-items-center lg:pt-7 lg:pb-5 px-3 lg:px-4 pt-4 rounded-xl`} - > - {info?.relations?.edges ? ( - info?.relations?.edges - .slice(0, showAll ? info?.relations?.edges.length : 3) - .map((r, index) => { - const rel = r.node; - return ( - <Link - key={rel.id} - href={ - rel.type === "ANIME" || - rel.type === "OVA" || - rel.type === "MOVIE" || - rel.type === "SPECIAL" || - rel.type === "ONA" - ? `/en/anime/${rel.id}` - : `/en/manga/${rel.id}` - } - className={`lg:hover:scale-[1.02] snap-start hover:shadow-lg scale-100 transition-transform duration-200 ease-out w-full ${ - rel.type === "MUSIC" ? "pointer-events-none" : "" - }`} - > - <div - key={rel.id} - className="w-[400px] lg:w-full h-[126px] bg-secondary flex rounded-md" - > - <div className="w-[90px] bg-image rounded-l-md shrink-0"> - <Image - src={rel.coverImage.extraLarge} - alt={rel.id} - height={500} - width={500} - className="object-cover h-full w-full shrink-0 rounded-l-md" - /> - </div> - <div className="h-full grid px-3 items-center"> - <div className="text-action font-outfit font-bold"> - {r.relationType} - </div> - <div className="font-outfit font-thin line-clamp-2"> - {rel.title.userPreferred} - </div> - <div className={``}>{rel.type}</div> - </div> - </div> - </Link> - ); - }) - ) : ( - <> - {[1, 2, 3].map((item) => ( - <div key={item} className="w-full hidden lg:block"> - <Skeleton className="h-[126px]" /> - </div> - ))} - <div className="w-full lg:hidden"> - <Skeleton className="h-[126px]" /> - </div> - </> - )} - </div> - </div> - </> - ); -} 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 ( - <> - <nav - className={`fixed z-[200] top-0 py-3 px-5 w-full ${ - scrollPosition?.y >= scrollP - ? "bg-tersier shadow-tersier shadow-sm" - : "" - } transition-all duration-200 ease-linear`} - > - <div className="flex items-center justify-between max-w-screen-2xl mx-auto"> - <div className="flex w-full items-center gap-4"> - {info ? ( - <> - <button - type="button" - className="flex-center w-7 h-7 text-white" - onClick={() => { - // router.back(); - router.push("/en"); - }} - > - <ArrowLeftIcon className="w-full h-full" /> - </button> - <span - className={`font-inter font-semibold w-[50%] line-clamp-1 select-none ${ - scrollPosition?.y >= scrollP + 80 - ? "opacity-100" - : "opacity-0" - } transition-all duration-200 ease-linear`} - > - {info.title.romaji} - </span> - </> - ) : ( - // <></> - <Link - href={"/en"} - className="flex-center text-white font-outfit text-2xl font-semibold" - > - moopa - </Link> - )} - </div> - <div className="flex items-center gap-4"> - <button - type="button" - onClick={() => setIsOpen(true)} - className="flex-center w-[26px] h-[26px]" - > - <svg - xmlns="http://www.w3.org/2000/svg" - width="32" - height="32" - viewBox="0 0 24 24" - > - <path - fill="none" - stroke="currentColor" - strokeLinecap="round" - strokeLinejoin="round" - strokeWidth="2" - d="M15 15l6 6m-11-4a7 7 0 110-14 7 7 0 010 14z" - ></path> - </svg> - </button> - {/* <div - className="bg-white" - // title={sessions ? "Go to Profile" : "Login With AniList"} - > */} - {session ? ( - <div className="w-7 h-7 relative flex flex-col items-center group"> - <button - type="button" - onClick={() => - router.push(`/en/profile/${session?.user.name}`) - } - className="rounded-full bg-white/30 overflow-hidden" - > - <Image - src={session?.user.image.large} - alt="avatar" - width={50} - height={50} - className="w-full h-full object-cover" - /> - </button> - <div className="hidden absolute z-50 w-28 text-center -bottom-20 text-white shadow-2xl opacity-0 bg-secondary p-1 py-2 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all md:grid place-items-center gap-1"> - <Link - href={`/en/profile/${session?.user.name}`} - className="hover:text-action" - > - Profile - </Link> - <div - onClick={() => signOut("AniListProvider")} - className="hover:text-action" - > - Log out - </div> - </div> - </div> - ) : ( - <button - type="button" - onClick={() => signIn("AniListProvider")} - title="Login With AniList" - className="w-7 h-7 bg-white/30 rounded-full overflow-hidden" - > - <UserIcon className="w-full h-full translate-y-2" /> - </button> - )} - {/* </div> */} - </div> - </div> - </nav> - {toTop && ( - <button - type="button" - onClick={() => { - window.scrollTo({ - top: 0, - behavior: "smooth", - }); - }} - className={`${ - scrollPosition?.y >= 180 - ? "-translate-x-6 opacity-100" - : "translate-x-[100%] opacity-0" - } transform transition-all duration-300 ease-in-out fixed bottom-24 lg:bottom-14 right-0 z-[500]`} - > - <ArrowUpCircleIcon className="w-10 h-10 text-white" /> - </button> - )} - </> - ); -} +import { NewNavbar } from "@/components/shared/NavBar"; export default function DetailTop({ info, - session, statuses, handleOpen, watchUrl, @@ -217,7 +44,7 @@ export default function DetailTop({ return ( <div className="gap-6 w-full px-3 pt-4 md:pt-10 flex flex-col items-center justify-center"> - <NewNavbar info={info} session={session} /> + <NewNavbar info={info} /> {/* MAIN */} <div className="flex flex-col md:flex-row w-full items-center md:items-end gap-5 pt-12"> 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({ <Image src={image || ""} alt={`Episode ${epi?.number} Thumbnail`} - width={1000} - height={1000} + width={420} + height={236} className="object-cover z-30 rounded-lg h-[110px] lg:h-[160px] brightness-[65%]" /> )} @@ -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({ }} /> <span className="absolute bottom-2 left-2 font-karla font-semibold text-sm lg:text-lg"> - Episode {epi?.number} + Episode {epi?.number || 0} </span> <div className="z-[9999] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 scale-[1.5]"> <svg @@ -68,7 +68,7 @@ export default function ThumbnailDetail({ className={`w-[70%] h-full select-none p-4 flex flex-col justify-center gap-3`} > <h1 className="font-karla font-bold text-base lg:text-lg xl:text-xl italic line-clamp-1"> - {title || `Episode ${epi?.number}`} + {title || `Episode ${epi?.number || 0}`} </h1> {description && ( <p className="line-clamp-2 text-xs lg:text-md xl:text-lg italic font-outfit font-extralight"> 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" > <span className="absolute text-sm z-40 bottom-1 left-2 font-karla font-semibold text-white"> - Episode {episode?.number} + Episode {episode?.number || 0} </span> <span className={`absolute bottom-7 left-0 h-[2px] bg-red-600`} @@ -40,7 +40,7 @@ export default function ThumbnailOnly({ {image && ( <Image src={image || ""} - alt="epi image" + alt={`Episode ${episode?.number} Thumbnail`} width={500} height={500} className="object-cover w-full h-[150px] sm:h-[100px] z-20 brightness-75" diff --git a/components/anime/changeView.js b/components/anime/viewSelector.js index 75ebdff..f114a8b 100644 --- a/components/anime/changeView.js +++ b/components/anime/viewSelector.js @@ -1,4 +1,4 @@ -export default function ChangeView({ view, setView, episode, map }) { +export default function ViewSelector({ view, setView, episode, map }) { return ( <div className="flex gap-3 rounded-sm items-center p-2"> <div @@ -6,6 +6,7 @@ export default function ChangeView({ view, setView, episode, map }) { episode?.length > 0 ? map?.every( (item) => + item?.img?.includes("https://s4.anilist.co/") || item?.image?.includes("https://s4.anilist.co/") || item.title === null ) || !map @@ -32,6 +33,7 @@ export default function ChangeView({ view, setView, episode, map }) { episode?.length > 0 ? map?.every( (item) => + item?.img?.includes("https://s4.anilist.co/") || item?.image?.includes("https://s4.anilist.co/") || item.title === null ) || !map @@ -50,6 +52,7 @@ export default function ChangeView({ view, setView, episode, map }) { episode?.length > 0 ? map?.every( (item) => + item?.img?.includes("https://s4.anilist.co/") || item?.image?.includes("https://s4.anilist.co/") || item.title === null ) || !map @@ -71,6 +74,7 @@ export default function ChangeView({ view, setView, episode, map }) { episode?.length > 0 ? map?.every( (item) => + item?.img?.includes("https://s4.anilist.co/") || item?.image?.includes("https://s4.anilist.co/") || item.title === null ) || !map 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 ( - <> - <Modal open={open} onClose={() => handleClose()}> - {!session && ( - <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md"> - <h1 className="text-md font-extrabold font-karla"> - Edit your list - </h1> - <button - className="flex items-center bg-[#363642] rounded-md text-white p-1" - onClick={() => signIn("AniListProvider")} - > - <h1 className="px-1 font-bold font-karla">Login with AniList</h1> - <div className="scale-[60%] pb-[1px]"> - <AniList /> - </div> - </button> - </div> - )} - </Modal> - <div className="w-full h-full"> - <div key={watchId} className="w-full aspect-video bg-black"> - {!loading ? ( - navigation && episodeData?.sources?.length !== 0 ? ( - <VideoPlayer - session={session} - info={info} - data={episodeData} - provider={providerId} - id={watchId} - progress={epiNumber} - skip={skip} - proxy={proxy} - aniId={info.id} - aniTitle={info.title?.romaji || info.title?.english} - track={navigation} - timeWatched={timeWatched} - dub={dub} - /> - ) : ( - <p className="h-full flex-center"> - Video is not available, please try other providers - </p> - ) - ) : ( - <div className="flex-center aspect-video bg-black"> - <div className="lds-ellipsis"> - <div></div> - <div></div> - <div></div> - <div></div> - </div> - </div> - )} - </div> - <div className="flex flex-col divide-y divide-white/20"> - {info && episodeList ? ( - <div className="flex items-center justify-between py-3 px-3"> - <div className="flex flex-col gap-2 w-[60%]"> - <h1 className="text-xl font-outfit font-semibold line-clamp-1"> - <Link - href={`/en/anime/${info.id}`} - className="hover:underline" - title={navigation?.playing?.title || info.title?.romaji} - > - {navigation?.playing?.title || info.title?.romaji} - </Link> - </h1> - <h3 className="text-sm font-karla font-light"> - Episode {epiNumber} - </h3> - </div> - <div className="flex gap-4 items-center justify-end"> - <div className="relative"> - <select - className="flex items-center gap-5 rounded-[3px] bg-secondary py-1 px-3 pr-8 font-karla appearance-none cursor-pointer" - value={epiNumber} - onChange={(e) => { - const selectedEpisode = episodeList.find( - (episode) => episode.number === parseInt(e.target.value) - ); - router.push( - `/en/anime/watch/${info.id}/${providerId}?id=${ - selectedEpisode.id - }&num=${selectedEpisode.number}${ - dub ? `&dub=${dub}` : "" - }` - ); - }} - > - {episodeList.map((episode) => ( - <option key={episode.number} value={episode.number}> - Episode {episode.number} - </option> - ))} - </select> - <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - <button - disabled={!navigation?.next} - className={`${ - !navigation?.next ? "pointer-events-none" : "" - }relative group`} - onClick={() => { - router.push( - `/en/anime/watch/${info.id}/${providerId}?id=${ - navigation?.next.id - }&num=${navigation?.next.number}${ - dub ? `&dub=${dub}` : "" - }` - ); - }} - > - <span className="absolute z-[9999] -left-11 -top-14 p-2 shadow-xl rounded-md transform transition-all whitespace-nowrap bg-secondary lg:group-hover:block group-hover:opacity-1 hidden font-karla font-bold"> - Next Episode - </span> - <ForwardIcon - className={`w-6 h-6 ${ - !navigation?.next ? "text-[#282828]" : "" - }`} - /> - </button> - </div> - </div> - ) : ( - <div className="py-3 px-4"> - <div className="text-xl font-outfit font-semibold line-clamp-2"> - <div className="inline hover:underline"> - <Skeleton width={240} /> - </div> - </div> - <h4 className="text-sm font-karla font-light"> - <Skeleton width={75} /> - </h4> - </div> - )} - <Details - info={info} - session={session} - description={navigation?.playing?.description || info?.description} - epiNumber={epiNumber} - id={watchId} - onList={onList} - setOnList={setOnList} - handleOpen={handleOpen} - disqus={disqus} - /> - </div> - </div> - </> - ); -} 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({ <p className="absolute z-40 text-center w-[86px] lg:w-[110px] top-1 -right-2 lg:top-[5.5px] lg:-right-2 font-karla text-sm lg:text-base"> Episode{" "} <span className="text-white"> - {anime?.episodeNumber} + {anime?.currentEpisode || anime?.episodeNumber} </span> </p> </Fragment> @@ -377,16 +378,6 @@ export default function Content({ className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item" > <div className="absolute flex flex-col gap-1 z-40 top-1 right-1 transition-all duration-200 ease-out opacity-0 group-hover/item:opacity-100 scale-90 group-hover/item:scale-100 group-hover/item:visible invisible "> - {/* <button - type="button" - className="flex flex-col items-center group/delete relative" - onClick={() => removeItem(i.watchId)} - > - <XMarkIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full hover:text-action scale-100 hover:scale-105 transition-all duration-200 ease-out" /> - <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/delete:visible group-hover/delete:scale-100 group-hover/delete:translate-x-0 group-hover/delete:opacity-100 opacity-0 translate-x-10 scale-50 invisible"> - Remove from history - </span> - </button> */} <HistoryOptions remove={removeItem} watchId={i.watchId} @@ -443,10 +434,10 @@ export default function Content({ {i?.image && ( <Image src={i?.image} - width="0" - height="0" + width={320} + height={180} alt="Episode Thumbnail" - className="w-fit group-hover:scale-[1.02] duration-300 ease-out z-10" + className="w-full object-cover group-hover:scale-[1.02] duration-300 ease-out z-10" /> )} </Link> 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 */} - <div className="flex items-center justify-center w-full"> - <div className="flex w-full items-center justify-between px-4 lg:w-[90%] lg:pt-7"> - <div className="flex items-center lg:gap-16"> - <Link - href="/en/" - className=" font-outfit lg:text-[40px] text-[30px] font-bold text-[#FF7F57]" - > - moopa - </Link> - <ul className="hidden items-center gap-10 pt-2 font-outfit text-[14px] lg:flex"> - <li> - <Link - href={`/en/search/anime?season=${season}&year=${year}`} - className="hover:text-action/80 transition-all duration-150 ease-linear" - > - This Season - </Link> - </li> - <li> - <Link - href="/en/search/manga" - className="hover:text-action/80 transition-all duration-150 ease-linear" - > - Manga - </Link> - </li> - <li> - <Link - href="/en/search/anime" - className="hover:text-action/80 transition-all duration-150 ease-linear" - > - Anime - </Link> - </li> - <li> - <Link - href="/en/schedule" - className="hover:text-action/80 transition-all duration-150 ease-linear" - > - Schedule - </Link> - </li> - - {status === "loading" ? ( - <li>Loading...</li> - ) : ( - <> - {!sessions && ( - <li> - <button - onClick={() => signIn("AniListProvider")} - className="hover:text-action/80 transition-all duration-150 ease-linear" - // className="px-2 py-1 ring-1 ring-action font-bold font-karla rounded-md" - > - Sign In - </button> - </li> - )} - {sessions && ( - <li className="text-center"> - <Link - href={`/en/profile/${sessions?.user.name}`} - className="hover:text-action/80 transition-all duration-150 ease-linear" - > - My List - </Link> - </li> - )} - </> - )} - </ul> - </div> - <div className="flex items-center gap-4"> - <button - type="button" - onClick={() => setIsOpen(true)} - className="flex-center w-[26px] h-[26px]" - > - <svg - xmlns="http://www.w3.org/2000/svg" - width="32" - height="32" - viewBox="0 0 24 24" - > - <path - fill="none" - stroke="currentColor" - strokeLinecap="round" - strokeLinejoin="round" - strokeWidth="2" - d="M15 15l6 6m-11-4a7 7 0 110-14 7 7 0 010 14z" - ></path> - </svg> - </button> - {/* <div - className="bg-white" - // title={sessions ? "Go to Profile" : "Login With AniList"} - > */} - {sessions ? ( - <div className="w-8 h-8 relative flex flex-col items-center group"> - <button - type="button" - onClick={() => - router.push(`/en/profile/${sessions?.user.name}`) - } - className="rounded-full bg-white/30 overflow-hidden" - > - <Image - src={sessions?.user.image.large} - alt="avatar" - width={50} - height={50} - className="w-full h-full object-cover" - /> - </button> - <div className="hidden absolute z-50 w-28 text-center -bottom-20 text-white shadow-2xl opacity-0 bg-secondary p-1 py-2 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all md:grid place-items-center gap-1"> - <Link - href={`/en/profile/${sessions?.user.name}`} - className="hover:text-action" - > - Profile - </Link> - <div - onClick={() => signOut("AniListProvider")} - className="hover:text-action cursor-pointer" - > - Log out - </div> - </div> - </div> - ) : ( - <button - type="button" - onClick={() => signIn("AniListProvider")} - title="Login With AniList" - className="w-7 h-7 bg-white/30 rounded-full overflow-hidden" - > - <UserIcon className="w-full h-full translate-y-2 text-white/50" /> - </button> - )} - {/* </div> */} - </div> - </div> - </div> - </> - ); -} 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: '<svg class="hi-solid hi-rewind inline-block w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M8.445 14.832A1 1 0 0010 14v-2.798l5.445 3.63A1 1 0 0017 14V6a1 1 0 00-1.555-.832L10 8.798V6a1 1 0 00-1.555-.832l-6 4a1 1 0 000 1.664l6 4z"/></svg>', - tooltip: "Backward 5s", - click: function () { - art.backward = 5; - }, - }, - { - name: "fast-forward", - position: "right", - html: '<svg class="hi-solid hi-fast-forward inline-block w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4.555 5.168A1 1 0 003 6v8a1 1 0 001.555.832L10 11.202V14a1 1 0 001.555.832l6-4a1 1 0 000-1.664l-6-4A1 1 0 0010 6v2.798l-5.445-3.63z"/></svg>', - 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 <div ref={artRef} {...rest}></div>; -} 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 && ( - <Player - key={`${url}`} - option={{ - url: `${url}`, - quality: source, - title: `${title}`, - autoplay: true, - screenshot: true, - poster: poster ? poster : "", - }} - res={resolution} - quality={source} - style={{ - width: "100%", - height: "100%", - margin: "0 auto 0", - }} - getInstance={(art) => { - 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: '<button class="skip-button">Skip Opening</button>', - 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: '<button class="skip-button">Skip Ending</button>', - 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 ( - <> - <main - className={`flex h-auto bg-[#121212] text-white flex-col ${props.className}`} - > - {/* PC/Tablet */} - <Navbar - className={`absolute z-50 hidden w-full duration-500 lg:fixed lg:top-0 lg:block lg:transition-all ${ - isAtTop - ? `px-2 pt-2 transition-all duration-1000 ${props.navTop}` - : isScrollingDown - ? "lg:h-16 lg:translate-y-[-100%] lg:shadow-sm lg:bg-[#0c0d10] " - : "lg:h-16 lg:translate-y-0 lg:shadow-sm lg:bg-[#0c0d10]" - }`} - /> - - {/* Mobile */} - <Navbar - className={`absolute z-50 w-full duration-300 lg:fixed lg:top-0 lg:hidden lg:transition-all`} - /> - <div className="grid items-center justify-center">{props.children}</div> - <Footer /> - </main> - </> - ); -} - -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 ( <div onClick={onClose} - className={`fixed z-50 inset-0 flex justify-center items-center transition-colors ${ + className={`fixed z-[999] inset-0 flex justify-center items-center transition-colors ${ open ? "visible bg-black bg-opacity-50 backdrop-blur-sm" : "invisible" }`} > 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 ( - <header className={`${props.className}`}> - <div className="flex h-16 w-auto items-center justify-between px-5 lg:mx-auto lg:w-[80%] lg:px-0 text-[#dbdcdd]"> - <div className="pb-2 font-outfit text-4xl font-semibold lg:block text-white"> - <Link href={`/${lang}/`}>moopa</Link> - </div> - - <MobileNav sessions={session} /> - - <nav className="left-0 top-[-100%] hidden w-auto items-center gap-10 px-5 lg:flex"> - <ul className="hidden gap-10 font-roboto text-md lg:flex items-center relative"> - <li> - <Link - href={`/${lang}/`} - className="p-2 transition-all duration-100 hover:text-orange-600" - > - home - </Link> - </li> - <li> - <Link - href={`/${lang}/about`} - className="p-2 transition-all duration-100 hover:text-orange-600" - > - about - </Link> - </li> - <li> - <Link - href={`/${lang}/search/anime`} - className="p-2 transition-all duration-100 hover:text-orange-600" - > - search - </Link> - </li> - {status === "loading" ? ( - <li>Loading...</li> - ) : ( - <> - {!session && ( - <li> - <button - onClick={() => signIn("AniListProvider")} - className="ring-1 ring-action font-karla font-bold px-2 py-1 rounded-md" - > - Sign in - </button> - </li> - )} - {session && ( - <li className="flex items-center justify-center group "> - <button> - <Image - src={session?.user.image.large} - alt="imagine" - width={500} - height={500} - className="object-cover h-10 w-10 rounded-full" - /> - </button> - <div className="absolute z-50 w-28 text-center -bottom-20 text-white shadow-2xl opacity-0 bg-secondary p-1 py-2 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all grid place-items-center gap-1"> - <Link - href={`/${lang}/profile/${session?.user.name}`} - className="hover:text-action" - > - Profile - </Link> - <button - onClick={() => signOut("AniListProvider")} - className="hover:text-action" - > - Log out - </button> - </div> - {/* My List */} - </li> - )} - </> - )} - </ul> - </nav> - </div> - </header> - ); -} - -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 ( + <> + <nav + className={`${home ? "" : "fixed"} z-[200] top-0 px-5 w-full ${ + scrollPosition?.y >= scrollP + ? home + ? "" + : `bg-tersier shadow-tersier shadow-sm ${ + shrink ? "py-1" : `${paddingY}` + }` + : `${paddingY}` + } transition-all duration-200 ease-linear`} + > + <div + className={`flex items-center justify-between mx-auto ${ + home ? "lg:max-w-[90%] gap-10" : "max-w-screen-2xl" + }`} + > + <div + className={`flex items-center ${ + withNav ? `${home ? "" : "w-[20%]"} gap-8` : " w-full gap-4" + }`} + > + {info ? ( + <> + <button + type="button" + className="flex-center w-7 h-7 text-white" + onClick={() => { + back + ? router.back() + : manga + ? router.push("/en/search/manga") + : router.push("/en"); + }} + > + <ArrowLeftIcon className="w-full h-full" /> + </button> + <span + className={`font-inter font-semibold w-[50%] line-clamp-1 select-none ${ + scrollPosition?.y >= scrollP + 80 + ? "opacity-100" + : "opacity-0" + } transition-all duration-200 ease-linear`} + > + {info.title.romaji} + </span> + </> + ) : ( + // <></> + <Link + href={"/en"} + className={`flex-center font-outfit font-semibold pb-2 ${ + home ? "text-4xl text-action" : "text-white text-3xl" + }`} + > + moopa + </Link> + )} + </div> + + {withNav && ( + <ul + className={`hidden w-full items-center gap-10 pt-2 font-outfit text-[14px] lg:pt-0 lg:flex ${ + home ? "justify-start" : "justify-center" + }`} + > + <li> + <Link + href={`/en/search/anime?season=${season}&year=${year}`} + className="hover:text-action/80 transition-all duration-150 ease-linear" + > + This Season + </Link> + </li> + <li> + <Link + href="/en/search/manga" + className="hover:text-action/80 transition-all duration-150 ease-linear" + > + Manga + </Link> + </li> + <li> + <Link + href="/en/search/anime" + className="hover:text-action/80 transition-all duration-150 ease-linear" + > + Anime + </Link> + </li> + <li> + <Link + href="/en/schedule" + className="hover:text-action/80 transition-all duration-150 ease-linear" + > + Schedule + </Link> + </li> + + {!session && ( + <li> + <button + onClick={() => signIn("AniListProvider")} + className="hover:text-action/80 transition-all duration-150 ease-linear" + // className="px-2 py-1 ring-1 ring-action font-bold font-karla rounded-md" + > + Sign In + </button> + </li> + )} + {session && ( + <li className="text-center"> + <Link + href={`/en/profile/${session?.user.name}`} + className="hover:text-action/80 transition-all duration-150 ease-linear" + > + My List + </Link> + </li> + )} + </ul> + )} + + <div className="flex w-[20%] justify-end items-center gap-4"> + <button + type="button" + onClick={() => setIsOpen(true)} + className="flex-center w-[26px] h-[26px]" + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="32" + height="32" + viewBox="0 0 24 24" + > + <path + fill="none" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth="2" + d="M15 15l6 6m-11-4a7 7 0 110-14 7 7 0 010 14z" + ></path> + </svg> + </button> + {/* <div + className="bg-white" + // title={sessions ? "Go to Profile" : "Login With AniList"} + > */} + {session ? ( + <div className="w-7 h-7 relative flex flex-col items-center group"> + <button + type="button" + onClick={() => + router.push(`/en/profile/${session?.user.name}`) + } + className="rounded-full bg-white/30 overflow-hidden" + > + <Image + src={session?.user.image.large} + alt="avatar" + width={50} + height={50} + className="w-full h-full object-cover" + /> + </button> + <div className="hidden absolute z-50 w-28 text-center -bottom-20 text-white shadow-2xl opacity-0 bg-secondary p-1 py-2 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all md:grid place-items-center gap-1"> + <Link + href={`/en/profile/${session?.user.name}`} + className="hover:text-action" + > + Profile + </Link> + <button + type="button" + onClick={() => signOut("AniListProvider")} + className="hover:text-action" + > + Log out + </button> + </div> + </div> + ) : ( + <button + type="button" + onClick={() => signIn("AniListProvider")} + title="Login With AniList" + className="w-7 h-7 bg-white/30 rounded-full overflow-hidden" + > + <UserIcon className="w-full h-full translate-y-1" /> + </button> + )} + {/* </div> */} + </div> + </div> + </nav> + {toTop && ( + <button + type="button" + onClick={() => { + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + }} + className={`${ + scrollPosition?.y >= 180 + ? "-translate-x-6 opacity-100" + : "translate-x-[100%] opacity-0" + } transform transition-all duration-300 ease-in-out fixed bottom-24 lg:bottom-14 right-0 z-[500]`} + > + <ArrowUpCircleIcon className="w-10 h-10 text-white" /> + </button> + )} + </> + ); +} 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 ( + <> + <Transition appear show={isOpen} as={Fragment}> + <Dialog as="div" className="relative z-[200]" onClose={closeModal}> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <div className="fixed inset-0 bg-black bg-opacity-90" /> + </Transition.Child> + + <div className="fixed inset-0 overflow-y-auto"> + <div className="flex min-h-full items-center justify-center p-4 "> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0 scale-95" + enterTo="opacity-100 scale-100" + leave="ease-in duration-200" + leaveFrom="opacity-100 scale-100" + leaveTo="opacity-0 scale-95" + > + <Dialog.Panel className="w-full max-w-md transition-all"> + <div className="bg-secondary p-6 rounded-lg shadow-xl"> + <h2 className={`text-action text-2xl font-semibold mb-4`}> + Report a Bug + </h2> + <form onSubmit={handleSubmit}> + <div className="space-y-4"> + <div> + <label + htmlFor="bugDescription" + className={`block text-txt text-sm font-medium mb-2`} + > + Bug Description + </label> + <textarea + id="bugDescription" + name="bugDescription" + rows="4" + className={`w-full bg-image text-txt rounded-md border border-txt focus:ring-action focus:border-action transition duration-300 focus:outline-none py-2 px-3`} + placeholder="Describe the bug you encountered..." + value={bugDescription} + onChange={(e) => setBugDescription(e.target.value)} + required + ></textarea> + </div> + <Listbox value={severity} onChange={setSeverity}> + <div className="relative mt-1"> + <label + htmlFor="severity" + className={`block text-txt text-sm font-medium mb-2`} + > + Severity + </label> + <Listbox.Button + type="button" + className="relative w-full cursor-pointer hover:shadow-xl hover:scale-[1.01] transition-all rounded-lg bg-image py-2 pl-3 pr-10 text-left shadow-md sm:text-base duration-300" + > + <span className="block truncate text-white font-semibold"> + {severity.name} + </span> + <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> + <ChevronDownIcon + className="h-5 w-5 text-gray-400" + aria-hidden="true" + /> + </span> + </Listbox.Button> + <Transition + as={Fragment} + leave="transition ease-in duration-100" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <Listbox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-image py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"> + {severityOptions.map((person, personIdx) => ( + <Listbox.Option + key={personIdx} + className={({ active }) => + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active + ? "bg-secondary/50 text-white" + : "text-gray-400" + }` + } + value={person} + > + {({ selected }) => ( + <> + <span + className={`block truncate ${ + selected + ? "font-medium text-white" + : "font-normal" + }`} + > + {person.name} + </span> + {selected ? ( + <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-action"> + <CheckIcon + className="h-5 w-5" + aria-hidden="true" + /> + </span> + ) : null} + </> + )} + </Listbox.Option> + ))} + </Listbox.Options> + </Transition> + </div> + </Listbox> + </div> + <div className="mt-4"> + <button + type="submit" + className={`w-full bg-action text-white py-2 px-4 rounded-md font-semibold hover:bg-action/80 focus:ring focus:ring-action focus:outline-none transition duration-300`} + > + Submit Bug Report + </button> + </div> + </form> + </div> + </Dialog.Panel> + </Transition.Child> + </div> + </div> + </Dialog> + </Transition> + </> + ); +}; + +export default BugReportForm; diff --git a/components/footer.js b/components/shared/footer.js index ca5a21f..91af5a8 100644 --- a/components/footer.js +++ b/components/shared/footer.js @@ -2,6 +2,7 @@ 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()); @@ -46,7 +47,7 @@ function Footer() { return ( <footer className="flex-col w-full"> <div className="text-[#dbdcdd] z-40 bg-[#0c0d10] lg:flex lg:h-[12rem] w-full lg:items-center lg:justify-between"> - <div className="mx-auto flex w-[85%] lg:w-[95%] xl:w-[80%] flex-col space-y-10 py-6 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:py-0"> + <div className="mx-auto flex w-[90%] lg:w-[95%] xl:w-[80%] flex-col space-y-10 py-6 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:py-0"> <div className="flex flex-col gap-2"> {/* <div className="flex items-center gap-2"> */} {/* <Image @@ -56,7 +57,7 @@ function Footer() { height={100} className="w-10 h-10" /> */} - <p className="font-outfit text-4xl">moopa</p> + <div className="flex gap-2 font-outfit text-4xl">moopa</div> <p className="font-karla lg:text-[0.8rem] text-[0.65rem] text-[#9c9c9c] lg:w-[520px] italic"> This site does not store any files on our server, we only linked to the media which is hosted on 3rd party services. @@ -80,7 +81,7 @@ function Footer() { <Link href={`/${lang}/search/manga`}>Popular Manga</Link> </li> <li className="cursor-pointer hover:text-action"> - <Link href={`https://ko-fi.com/factiven`}>Donate</Link> + <Link href={`/donate`}>Donate</Link> </li> </ul> <ul className="flex flex-col gap-y-[0.7rem]"> @@ -156,10 +157,7 @@ function Footer() { </Link> {/* Kofi */} - <Link - href="https://ko-fi.com/factiven" - className="w-6 h-6 hover:opacity-75" - > + <Link href="/donate" className="w-6 h-6 hover:opacity-75"> <svg xmlns="http://www.w3.org/2000/svg" fill="#fff" 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 && ( - <Player - key={url} - option={{ - url: `${url}`, - title: `${title}`, - autoplay: true, - screenshot: true, - moreVideoAttr: { - crossOrigin: "anonymous", - }, - poster: poster ? poster : "", - ...(provider !== "gogoanime" && { - plugins: [ - artplayerPluginHlsQuality({ - // Show quality in setting - setting: true, - - // Get the resolution text from level - getResolution: (level) => 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: '<div class="vid-con"><button class="next-button progress">Play Next</button></div>', - 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: '<button class="skip-button">Skip Opening</button>', - 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: '<button class="skip-button">Skip Ending</button>', - 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: '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24"><path fill="currentColor" d="M4.05 16.975q-.5.35-1.025.05t-.525-.9v-8.25q0-.6.525-.888t1.025.038l6.2 4.15q.45.3.45.825t-.45.825l-6.2 4.15Zm10 0q-.5.35-1.025.05t-.525-.9v-8.25q0-.6.525-.888t1.025.038l6.2 4.15q.45.3.45.825t-.45.825l-6.2 4.15Z"></path></svg>', + 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: '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24"><path fill="currentColor" d="M4.05 16.975q-.5.35-1.025.05t-.525-.9v-8.25q0-.6.525-.888t1.025.038l6.2 4.15q.45.3.45.825t-.45.825l-6.2 4.15Zm10 0q-.5.35-1.025.05t-.525-.9v-8.25q0-.6.525-.888t1.025.038l6.2 4.15q.45.3.45.825t-.45.825l-6.2 4.15Z"></path></svg>', + // icon: '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24"><path fill="currentColor" d="M5.59 7.41L7 6l6 6l-6 6l-1.41-1.41L10.17 12L5.59 7.41m6 0L13 6l6 6l-6 6l-1.41-1.41L16.17 12l-4.58-4.59Z"></path></svg>', + 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: '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 512 512"><path fill="currentColor" d="M381.25 112a48 48 0 0 0-90.5 0H48v32h242.75a48 48 0 0 0 90.5 0H464v-32ZM176 208a48.09 48.09 0 0 0-45.25 32H48v32h82.75a48 48 0 0 0 90.5 0H464v-32H221.25A48.09 48.09 0 0 0 176 208Zm160 128a48.09 48.09 0 0 0-45.25 32H48v32h242.75a48 48 0 0 0 90.5 0H464v-32h-82.75A48.09 48.09 0 0 0 336 336Z"></path></svg>', + 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: '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 32 32"><path fill="currentColor" d="m24.6 24.4l2.6 2.6l-2.6 2.6L26 31l4-4l-4-4zm-2.2 0L19.8 27l2.6 2.6L21 31l-4-4l4-4z"></path><circle cx="11" cy="8" r="1" fill="currentColor"></circle><circle cx="11" cy="16" r="1" fill="currentColor"></circle><circle cx="11" cy="24" r="1" fill="currentColor"></circle><path fill="currentColor" d="M24 3H8c-1.1 0-2 .9-2 2v22c0 1.1.9 2 2 2h7v-2H8v-6h18V5c0-1.1-.9-2-2-2zm0 16H8v-6h16v6zm0-8H8V5h16v6z"></path></svg>', + 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: '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24"><path fill="currentColor" d="M4 20q-.825 0-1.413-.588T2 18V6q0-.825.588-1.413T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.588 1.413T20 20H4Zm2-4h8v-2H6v2Zm10 0h2v-2h-2v2ZM6 12h2v-2H6v2Zm4 0h8v-2h-8v2Z"></path></svg>', + width: 300, + tooltip: "Settings", + selector: [ + { + html: "Display", + icon: '<svg xmlns="http://www.w3.org/2000/svg" width="35" height="26" viewBox="0 -960 960 960"><path d="M480.169-341.796q65.754 0 111.894-46.31 46.141-46.309 46.141-112.063t-46.31-111.894q-46.309-46.141-112.063-46.141t-111.894 46.31q-46.141 46.309-46.141 112.063t46.31 111.894q46.309 46.141 112.063 46.141zm-.371-48.307q-45.875 0-77.785-32.112-31.91-32.112-31.91-77.987 0-45.875 32.112-77.785 32.112-31.91 77.987-31.91 45.875 0 77.785 32.112 31.91 32.112 31.91 77.987 0 45.875-32.112 77.785-32.112 31.91-77.987 31.91zm.226 170.102q-130.921 0-239.6-69.821-108.679-69.82-167.556-186.476-2.687-4.574-3.892-10.811Q67.77-493.347 67.77-500t1.205-12.891q1.205-6.237 3.892-10.811Q131.745-640.358 240.4-710.178q108.655-69.821 239.576-69.821t239.6 69.821q108.679 69.82 167.556 186.476 2.687 4.574 3.892 10.811 1.205 6.238 1.205 12.891t-1.205 12.891q-1.205 6.237-3.892 10.811Q828.255-359.642 719.6-289.822q-108.655 69.821-239.576 69.821zM480-500zm-.112 229.744q117.163 0 215.048-62.347Q792.821-394.949 844.308-500q-51.487-105.051-149.26-167.397-97.772-62.347-214.936-62.347-117.163 0-215.048 62.347Q167.179-605.051 115.282-500q51.897 105.051 149.67 167.397 97.772 62.347 214.936 62.347z"></path></svg>', + 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: '<svg xmlns="http://www.w3.org/2000/svg" width="35" height="26" viewBox="0 -960 960 960"><path d="M619.861-177.694q-15.655 0-26.475-10.918-10.821-10.918-10.821-26.516v-492.309H415.128q-15.598 0-26.516-10.959-10.918-10.959-10.918-26.615 0-15.655 10.918-26.475 10.918-10.82 26.516-10.82h409.744q15.598 0 26.516 10.958 10.918 10.959 10.918 26.615 0 15.656-10.918 26.476-10.918 10.82-26.516 10.82H657.435v492.309q0 15.598-10.959 26.516-10.959 10.918-26.615 10.918zm-360 0q-15.655 0-26.475-10.918-10.821-10.918-10.821-26.516v-292.309h-87.437q-15.598 0-26.516-10.959-10.918-10.959-10.918-26.615 0-15.655 10.918-26.475 10.918-10.82 26.516-10.82h249.744q15.598 0 26.516 10.958 10.918 10.959 10.918 26.615 0 15.656-10.918 26.476-10.918 10.82-26.516 10.82h-87.437v292.309q0 15.598-10.959 26.516-10.959 10.918-26.615 10.918z"></path></svg>', + 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: '<svg xmlns="http://www.w3.org/2000/svg" width="35" height="26" viewBox="0 -960 960 960"><path d="M528.282-110.771q-21.744 0-31.308-14.013t-2.205-34.295l135.952-359.307q5.304-14.793 20.292-25.126 14.988-10.334 31.152-10.334 15.398 0 30.85 10.388 15.451 10.387 20.932 25.125l137.128 357.485q8.025 20.949-1.83 35.513-9.855 14.564-33.24 14.564-10.366 0-19.392-6.616-9.025-6.615-12.72-16.242l-30.997-91.808H594.769l-33.381 91.869q-3.645 9.181-13.148 15.989-9.504 6.808-19.958 6.808zm87.871-179.281h131.64l-64.615-180.717h-2.41l-64.615 180.717zM302.104-608.384q14.406 25.624 31.074 48.184 16.669 22.559 37.643 47.021 41.333-44.128 68.628-90.461t46.038-97.897H111.499q-15.674 0-26.278-10.615-10.603-10.616-10.603-26.308t10.615-26.307q10.616-10.616 26.308-10.616h221.537v-36.923q0-15.692 10.615-26.307 10.616-10.616 26.308-10.616t26.307 10.616q10.616 10.615 10.616 26.307v36.923h221.537q15.692 0 26.307 10.616 10.616 10.615 10.616 26.307 0 15.692-10.616 26.308-10.615 10.615-26.307 10.615h-69.088q-19.912 64.153-53.237 125.74-33.325 61.588-82.341 116.412l89.384 90.974-27.692 75.179-115.486-112.922-158.948 158.947q-10.615 10.616-25.667 10.616-15.051 0-25.666-11.026-11.026-10.615-11.026-25.666 0-15.052 11.026-26.077l161.614-161.358q-24.666-28.308-45.551-57.307-20.884-29-37.756-60.103-10.641-19.871-1.346-34.717t33.038-14.846q9.088 0 18.429 5.73 9.34 5.731 13.956 13.577z"></path></svg>', + 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: '<p class="theater"><svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="M19 3H1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm-1 12H2V5h16v10z"></path></svg></p>', + 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 <div ref={artRef} {...rest}></div>; +} 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: + '<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 20 20"><path fill="currentColor" d="M10 8a3 3 0 1 0 0 6a3 3 0 0 0 0-6zm8-3h-2.4a.888.888 0 0 1-.789-.57l-.621-1.861A.89.89 0 0 0 13.4 2H6.6c-.33 0-.686.256-.789.568L5.189 4.43A.889.889 0 0 1 4.4 5H2C.9 5 0 5.9 0 7v9c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-8 11a5 5 0 0 1-5-5a5 5 0 1 1 10 0a5 5 0 0 1-5 5zm7.5-7.8a.7.7 0 1 1 0-1.4a.7.7 0 0 1 0 1.4z"></path></svg>', + play: '<svg xmlns="http://www.w3.org/2000/svg" width="25px" height="25px" viewBox="0 0 20 20"><path fill="currentColor" d="M15 10.001c0 .299-.305.514-.305.514l-8.561 5.303C5.51 16.227 5 15.924 5 15.149V4.852c0-.777.51-1.078 1.135-.67l8.561 5.305c-.001 0 .304.215.304.514z"></path></svg>', + pause: + '<svg xmlns="http://www.w3.org/2000/svg" width="25px" height="25px" viewBox="0 0 20 20"><path fill="currentColor" d="M15 3h-2c-.553 0-1 .048-1 .6v12.8c0 .552.447.6 1 .6h2c.553 0 1-.048 1-.6V3.6c0-.552-.447-.6-1-.6zM7 3H5c-.553 0-1 .048-1 .6v12.8c0 .552.447.6 1 .6h2c.553 0 1-.048 1-.6V3.6c0-.552-.447-.6-1-.6z"></path></svg>', + volume: + '<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="M19 13.805c0 .657-.538 1.195-1.195 1.195H1.533c-.88 0-.982-.371-.229-.822l16.323-9.055C18.382 4.67 19 5.019 19 5.9v7.905z"></path></svg>', + fullscreenOff: + '<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="M3.28 2.22a.75.75 0 0 0-1.06 1.06L5.44 6.5H2.75a.75.75 0 0 0 0 1.5h4.5A.75.75 0 0 0 8 7.25v-4.5a.75.75 0 0 0-1.5 0v2.69L3.28 2.22Zm10.22.53a.75.75 0 0 0-1.5 0v4.5c0 .414.336.75.75.75h4.5a.75.75 0 0 0 0-1.5h-2.69l3.22-3.22a.75.75 0 0 0-1.06-1.06L13.5 5.44V2.75ZM3.28 17.78l3.22-3.22v2.69a.75.75 0 0 0 1.5 0v-4.5a.75.75 0 0 0-.75-.75h-4.5a.75.75 0 0 0 0 1.5h2.69l-3.22 3.22a.75.75 0 1 0 1.06 1.06Zm10.22-3.22l3.22 3.22a.75.75 0 1 0 1.06-1.06l-3.22-3.22h2.69a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0-.75.75v4.5a.75.75 0 0 0 1.5 0v-2.69Z"></path></svg>', + fullscreenOn: + '<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="m13.28 7.78l3.22-3.22v2.69a.75.75 0 0 0 1.5 0v-4.5a.75.75 0 0 0-.75-.75h-4.5a.75.75 0 0 0 0 1.5h2.69l-3.22 3.22a.75.75 0 0 0 1.06 1.06ZM2 17.25v-4.5a.75.75 0 0 1 1.5 0v2.69l3.22-3.22a.75.75 0 0 1 1.06 1.06L4.56 16.5h2.69a.75.75 0 0 1 0 1.5h-4.5a.747.747 0 0 1-.75-.75Zm10.22-3.97l3.22 3.22h-2.69a.75.75 0 0 0 0 1.5h4.5a.747.747 0 0 0 .75-.75v-4.5a.75.75 0 0 0-1.5 0v2.69l-3.22-3.22a.75.75 0 1 0-1.06 1.06ZM3.5 4.56l3.22 3.22a.75.75 0 0 0 1.06-1.06L4.56 3.5h2.69a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0-.75.75v4.5a.75.75 0 0 0 1.5 0V4.56Z"></path></svg>', +}; + +export const backButton = { + name: "back-button", + index: 10, + position: "top", + html: "<div class='parent-player-title'><div></div><div className='flex gap-2'><p className='pt-1'><ChevronLeftIcon className='w-7 h-7'/></p><div class='flex flex-col text-white'><p className='font-outfit font-bold text-2xl'>Komi-san wa, Komyushou desu.</p><p className=''>Episode 1</p></div></div></div>", + // 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: '<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="M17.959 4.571L10.756 9.52s-.279.201-.279.481s.279.479.279.479l7.203 4.951c.572.38 1.041.099 1.041-.626V5.196c0-.727-.469-1.008-1.041-.625zm-9.076 0L1.68 9.52s-.279.201-.279.481s.279.479.279.479l7.203 4.951c.572.381 1.041.1 1.041-.625v-9.61c0-.727-.469-1.008-1.041-.625z"></path></svg>', + tooltip: "Backward 5s", + click: function () { + art.backward = 5; + }, +}; + +export const seekForward = { + index: 11, + name: "fast-forward", + position: "left", + html: '<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="M9.244 9.52L2.041 4.571C1.469 4.188 1 4.469 1 5.196v9.609c0 .725.469 1.006 1.041.625l7.203-4.951s.279-.199.279-.478c0-.28-.279-.481-.279-.481zm9.356.481c0 .279-.279.478-.279.478l-7.203 4.951c-.572.381-1.041.1-1.041-.625V5.196c0-.727.469-1.008 1.041-.625L18.32 9.52s.28.201.28.481z"></path></svg>', + 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: '<button class="skip-button">Skip Opening</button>', + 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: '<button class="skip-button">Skip Ending</button>', + 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: '<div class="vid-con"><button class="next-button progress">Play Next</button></div>', + 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 ( + <div + id={id} + className={`${className} bg-black`} + style={{ aspectRatio: aspectRatio }} + > + <div className="w-full h-full"> + {!loading && track && url && ( + <NewPlayer + playerRef={playerRef} + res={resolution} + quality={source} + option={option} + provider={provider} + defSize={defSize} + defSub={defSub} + subSize={subSize} + subtitles={subtitle} + getInstance={getInstance} + style={{ + width: "100%", + height: "100%", + }} + /> + )} + </div> + </div> + ); +} diff --git a/components/watch/player/utils/getZoroSource.js b/components/watch/player/utils/getZoroSource.js new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/components/watch/player/utils/getZoroSource.js diff --git a/components/anime/watch/primary/details.js b/components/watch/primary/details.js index f092879..32e1391 100644 --- a/components/anime/watch/primary/details.js +++ b/components/watch/primary/details.js @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { useAniList } from "../../../../lib/anilist/useAnilist"; +import { useAniList } from "../../../lib/anilist/useAnilist"; import Skeleton from "react-loading-skeleton"; -import DisqusComments from "../../../disqus"; +import DisqusComments from "../../disqus"; import Image from "next/image"; export default function Details({ @@ -17,7 +17,6 @@ export default function Details({ }) { const [showComments, setShowComments] = useState(false); const { markPlanning } = useAniList(session); - const [url, setUrl] = useState(null); function handlePlan() { if (onList === false) { @@ -27,14 +26,13 @@ export default function Details({ } useEffect(() => { - const url = window.location.href; setShowComments(false); - setUrl(url); }, [id]); return ( <div className="flex flex-col gap-2"> - <div className="px-4 pt-7 pb-4 h-full flex"> + {/* <div className="px-4 pt-7 pb-4 h-full flex"> */} + <div className="pb-4 h-full flex"> <div className="aspect-[9/13] h-[240px]"> {info ? ( <Image @@ -58,7 +56,11 @@ export default function Details({ Studios </h2> <div className="row-start-2"> - {info ? info.studios.edges[0].node.name : <Skeleton width={80} />} + {info ? ( + info.studios?.edges[0].node.name + ) : ( + <Skeleton width={80} /> + )} </div> <div className="hidden xxs:grid col-start-2 place-content-end relative"> <div> @@ -114,7 +116,8 @@ export default function Details({ </div> </div> </div> - <div className="flex flex-wrap gap-3 px-4 pt-3"> + {/* <div className="flex flex-wrap gap-3 px-4 pt-3"> */} + <div className="flex flex-wrap gap-3 pt-3"> {info && info.genres?.map((item, index) => ( <div @@ -125,7 +128,8 @@ export default function Details({ </div> ))} </div> - <div className={`bg-secondary rounded-md mt-3 mx-3`}> + {/* <div className={`bg-secondary rounded-md mt-3 mx-3`}> */} + <div className={`bg-secondary rounded-md mt-3`}> {info && ( <p dangerouslySetInnerHTML={{ __html: description }} @@ -135,7 +139,7 @@ export default function Details({ </div> {/* {<div className="mt-5 px-5"></div>} */} {!showComments && ( - <div className="w-full flex justify-center py-2 font-karla px-3 lg:px-0"> + <div className="w-full flex justify-center py-2 font-karla lg:px-0"> <button onClick={() => setShowComments(true)} className={ @@ -164,14 +168,14 @@ export default function Details({ )} {showComments && ( <div> - {info && url && ( + {info && ( <div className="mt-5 px-5"> <DisqusComments key={id} post={{ id: id, title: info.title.romaji, - url: url, + url: window.location.href, episode: epiNumber, name: disqus, }} diff --git a/components/anime/watch/secondarySide.js b/components/watch/secondary/episodeLists.js index c9ef684..8a057ce 100644 --- a/components/anime/watch/secondarySide.js +++ b/components/watch/secondary/episodeLists.js @@ -2,7 +2,7 @@ import Skeleton from "react-loading-skeleton"; import Image from "next/image"; import Link from "next/link"; -export default function SecondarySide({ +export default function EpisodeLists({ info, map, providerId, @@ -13,11 +13,15 @@ export default function SecondarySide({ }) { const progress = info.mediaListEntry?.progress; return ( - <div className="lg:w-[35%] shrink-0 w-screen"> - <h1 className="text-xl font-karla pl-4 pb-5 font-semibold">Up Next</h1> + <div className="w-screen lg:max-w-sm xl:max-w-xl"> + <h1 className="text-xl font-karla pl-5 pb-5 font-semibold">Up Next</h1> <div className="flex flex-col gap-5 lg:pl-5 py-2 scrollbar-thin px-2 scrollbar-thumb-[#313131] scrollbar-thumb-rounded-full"> {episode && episode.length > 0 ? ( - map?.some((item) => item.title && item.description) > 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; @@ -41,9 +45,14 @@ export default function SecondarySide({ > <div className="w-[43%] lg:w-[40%] h-[110px] relative rounded-lg z-40 shrink-0 overflow-hidden shadow-[4px_0px_5px_0px_rgba(0,0,0,0.3)]"> <div className="relative"> - {/* {mapData?.image && ( */} + {/* <div className="absolute inset-0 w-full h-full z-40" /> */} <Image - src={mapData?.image || info?.coverImage?.extraLarge} + src={ + mapData?.img || + mapData?.image || + info?.coverImage?.extraLarge + } + draggable={false} alt="Anime Cover" width={1000} height={1000} @@ -88,10 +97,10 @@ export default function SecondarySide({ }`} > <h1 className="font-karla font-bold italic line-clamp-1"> - {mapData?.title} + {mapData?.title || info?.title?.romaji} </h1> <p className="line-clamp-2 text-xs italic font-outfit font-extralight"> - {mapData?.description} + {mapData?.description || `Episode ${item.number}`} </p> </div> </Link> @@ -107,7 +116,7 @@ export default function SecondarySide({ item.number }${dub ? `&dub=${dub}` : ""}`} key={item.id} - className={`bg-secondary flex-center w-full h-[50px] rounded-lg scale-100 transition-all duration-300 ease-out ${ + className={`bg-secondary flex-center h-[50px] rounded-lg scale-100 transition-all duration-300 ease-out ${ item.id == watchId ? "pointer-events-none ring-1 ring-action text-[#5d5d5d]" : "cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white" |