From 7327a69b55a20b99b14ee0803d6cf5f8b88c45ef Mon Sep 17 00:00:00 2001 From: Factiven Date: Wed, 13 Sep 2023 00:45:53 +0700 Subject: Update v4 - Merge pre-push to main (#71) * Create build-test.yml * initial v4 commit * update: github workflow * update: push on branch * Update .github/ISSUE_TEMPLATE/bug_report.md * configuring next.config.js file --- components/anime/changeView.js | 30 +- components/anime/episode.js | 185 +++++++--- components/anime/infoDetails.js | 6 +- components/anime/mobile/reused/description.js | 44 +++ components/anime/mobile/reused/infoChip.js | 43 +++ components/anime/mobile/topSection.js | 504 ++++++++++++++++++++++---- components/anime/viewMode/listMode.js | 58 ++- components/anime/viewMode/thumbnailDetail.js | 25 +- components/anime/viewMode/thumbnailOnly.js | 30 +- components/anime/watch/primarySide.js | 45 ++- components/anime/watch/secondarySide.js | 19 +- 11 files changed, 781 insertions(+), 208 deletions(-) create mode 100644 components/anime/mobile/reused/description.js create mode 100644 components/anime/mobile/reused/infoChip.js (limited to 'components/anime') diff --git a/components/anime/changeView.js b/components/anime/changeView.js index cab9054..75ebdff 100644 --- a/components/anime/changeView.js +++ b/components/anime/changeView.js @@ -1,14 +1,14 @@ -import { useEffect, useState } from "react"; - -export default function ChangeView({ view, setView, episode }) { - // const [view, setView] = useState(1); - // const episode = null; +export default function ChangeView({ view, setView, episode, map }) { return (
0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "pointer-events-none" : "cursor-pointer" : "pointer-events-none" @@ -30,7 +30,11 @@ export default function ChangeView({ view, setView, episode }) { height="20" className={`${ episode?.length > 0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "fill-[#1c1c22]" : view === 1 ? "fill-action" @@ -44,7 +48,11 @@ export default function ChangeView({ view, setView, episode }) {
0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "pointer-events-none" : "cursor-pointer" : "pointer-events-none" @@ -61,7 +69,11 @@ export default function ChangeView({ view, setView, episode }) { fill="none" className={`${ episode?.length > 0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "fill-[#1c1c22]" : view === 2 ? "fill-action" diff --git a/components/anime/episode.js b/components/anime/episode.js index 5d3451b..b2f4bd7 100644 --- a/components/anime/episode.js +++ b/components/anime/episode.js @@ -1,13 +1,18 @@ import { useEffect, useState, Fragment } from "react"; -import { ChevronDownIcon, ClockIcon } from "@heroicons/react/20/solid"; -import { convertSecondsToTime } from "../../utils/getTimes"; +import { ChevronDownIcon } from "@heroicons/react/20/solid"; import ChangeView from "./changeView"; import ThumbnailOnly from "./viewMode/thumbnailOnly"; import ThumbnailDetail from "./viewMode/thumbnailDetail"; import ListMode from "./viewMode/listMode"; -import axios from "axios"; +import { convertSecondsToTime } from "../../utils/getTimes"; -export default function AnimeEpisode({ info, progress }) { +export default function AnimeEpisode({ + info, + session, + progress, + setProgress, + setWatch, +}) { const [providerId, setProviderId] = useState(); // default provider const [currentPage, setCurrentPage] = useState(1); // for pagination const [visible, setVisible] = useState(false); // for mobile view @@ -19,42 +24,60 @@ export default function AnimeEpisode({ info, progress }) { const [isDub, setIsDub] = useState(false); const [providers, setProviders] = useState(null); + const [mapProviders, setMapProviders] = useState(null); useEffect(() => { setLoading(true); - setProviders(null); const fetchData = async () => { - try { - const { data: firstResponse } = await axios.get( - `/api/consumet/episode/${info.id}${isDub === true ? "?dub=true" : ""}` - ); - if (firstResponse.data.length > 0) { - const defaultProvider = firstResponse.data?.find( - (x) => x.providerId === "gogoanime" - ); - setProviderId( - defaultProvider?.providerId || firstResponse.data[0].providerId - ); // set to first provider id - } + const response = await fetch( + `/api/v2/episode/${info.id}?releasing=${ + info.status === "RELEASING" ? "true" : "false" + }${isDub ? "&dub=true" : ""}` + ).then((res) => res.json()); + const getMap = response.find((i) => i?.map === true); + let allProvider = response; - setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); - setProviders(firstResponse.data); - setLoading(false); - } catch (error) { - setLoading(false); - setProviders([]); + if (getMap) { + allProvider = response.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); }; fetchData(); + + return () => { + setProviders(null); + setMapProviders(null); + }; }, [info.id, isDub]); const episodes = - providers?.find((provider) => provider.providerId === providerId) - ?.episodes || []; + providers + ?.find((provider) => provider.providerId === providerId) + ?.episodes?.slice(0, mapProviders?.length) || []; const lastEpisodeIndex = currentPage * itemsPerPage; const firstEpisodeIndex = lastEpisodeIndex - itemsPerPage; - const currentEpisodes = episodes.slice(firstEpisodeIndex, lastEpisodeIndex); + let currentEpisodes = episodes.slice(firstEpisodeIndex, lastEpisodeIndex); + if (isDub) { + currentEpisodes = currentEpisodes.filter((i) => i.hasDub === true); + } const totalPages = Math.ceil(episodes.length / itemsPerPage); const handleChange = (event) => { @@ -66,36 +89,90 @@ export default function AnimeEpisode({ info, progress }) { }; useEffect(() => { - if (episodes?.some((item) => item?.title === null)) { + if ( + !mapProviders || + mapProviders?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item?.image === null + ) + ) { setView(3); } }, [providerId, episodes]); + useEffect(() => { + if (episodes) { + const getEpi = info?.nextAiringEpisode + ? episodes.find((i) => i.number === progress + 1) + : episodes[0]; + if (getEpi) { + const watchUrl = `/en/anime/watch/${ + info.id + }/${providerId}?id=${encodeURIComponent(getEpi.id)}&num=${ + getEpi.number + }${isDub ? `&dub=${isDub}` : ""}`; + setWatch(watchUrl); + } else { + setWatch(null); + } + } + }, [episodes]); + + useEffect(() => { + if (artStorage) { + // console.log({ artStorage }); + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + + // Create a new object to store the updated data + const updatedData = {}; + + // Iterate through the current data and copy items with different aniId to the updated object + for (const key in currentData) { + const item = currentData[key]; + if (Number(item.aniId) === info.id && item.provider === providerId) { + updatedData[key] = item; + } + } + + if (!session?.user?.name) { + setProgress( + Object.keys(updatedData).length > 0 + ? Math.max( + ...Object.keys(updatedData).map( + (key) => updatedData[key].episode + ) + ) + : 0 + ); + } else { + return; + } + } + }, [providerId, artStorage, info.id, session?.user?.name]); + return ( <> -
+
-
+
{info && (

Episodes

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

Next :

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

+ Ep {info.nextAiringEpisode.episode}{" "} + {">>"}{" "} + + {convertSecondsToTime( + info.nextAiringEpisode.timeUntilAiring + )}{" "} + +

)}
@@ -165,9 +242,6 @@ export default function AnimeEpisode({ info, progress }) { ))} - {/* - Select Providers - */}
@@ -197,6 +271,7 @@ export default function AnimeEpisode({ info, progress }) { view={view} setView={setView} episode={currentEpisodes} + map={mapProviders} />
@@ -204,15 +279,21 @@ export default function AnimeEpisode({ info, progress }) { {/* Episodes */} {!loading ? (
{Array.isArray(providers) ? ( providers.length > 0 ? ( currentEpisodes.map((episode, index) => { + const mapData = mapProviders?.find( + (i) => i.number === episode.number + ); + return ( {view === 1 && ( @@ -220,17 +301,20 @@ export default function AnimeEpisode({ info, progress }) { key={index} index={index} info={info} + image={mapData?.image} providerId={providerId} episode={episode} artStorage={artStorage} progress={progress} dub={isDub} - // image={thumbnail} /> )} {view === 2 && (
{rel.id}
- {rel.title.userPreferred || rel.title.romaji} + {rel.title.userPreferred}
{rel.type}
diff --git a/components/anime/mobile/reused/description.js b/components/anime/mobile/reused/description.js new file mode 100644 index 0000000..99973d3 --- /dev/null +++ b/components/anime/mobile/reused/description.js @@ -0,0 +1,44 @@ +export default function Description({ + info, + readMore, + setReadMore, + className, +}) { + return ( +
+
]*>/g, "").length > 240 + ? "" + : "pointer-events-none" + } ${ + readMore ? "hidden" : "" + } absolute z-30 flex items-end justify-center top-0 w-full h-full transition-all duration-200 ease-linear md:opacity-0 md:hover:opacity-100 bg-gradient-to-b from-transparent to-primary to-95%`} + > + +
+

]*>/g, ""), + }} + /> +

+ ); +} diff --git a/components/anime/mobile/reused/infoChip.js b/components/anime/mobile/reused/infoChip.js new file mode 100644 index 0000000..41def85 --- /dev/null +++ b/components/anime/mobile/reused/infoChip.js @@ -0,0 +1,43 @@ +import React from "react"; +import { getFormat } from "../../../../utils/getFormat"; + +export default function InfoChip({ info, color, className }) { + return ( +
+ {info?.episodes && ( +
+ {info?.episodes} Episodes +
+ )} + {info?.averageScore && ( +
+ {info?.averageScore}% +
+ )} + {info?.format && ( +
+ {getFormat(info?.format)} +
+ )} + {info?.status && ( +
+ {info?.status} +
+ )} +
+ ); +} diff --git a/components/anime/mobile/topSection.js b/components/anime/mobile/topSection.js index e9c9c7d..25d387f 100644 --- a/components/anime/mobile/topSection.js +++ b/components/anime/mobile/topSection.js @@ -1,81 +1,459 @@ -import { HeartIcon } from "@heroicons/react/20/solid"; +import { + ArrowUpCircleIcon, + MagnifyingGlassIcon, +} from "@heroicons/react/24/solid"; import { - TvIcon, - ArrowTrendingUpIcon, - RectangleStackIcon, -} from "@heroicons/react/24/outline"; + ArrowLeftIcon, + PlayIcon, + PlusIcon, + ShareIcon, + UserIcon, +} 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()); + }; -export default function DetailTop({ info, statuses, handleOpen, loading }) { + // 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 ( -
-
-

- {info?.title?.romaji || info?.title?.english} -

-

-

- {info?.genres - ?.slice(0, info?.genres?.length > 3 ? info?.genres?.length : 3) - .map((item, index) => ( - + + {toTop && ( + + )} + + ); +} + +export default function DetailTop({ + info, + session, + statuses, + handleOpen, + watchUrl, + progress, + color, +}) { + const router = useRouter(); + const [readMore, setReadMore] = useState(false); + + const [showAll, setShowAll] = useState(false); + + useEffect(() => { + setReadMore(false); + }, [info.id]); + + const handleShareClick = async () => { + try { + if (navigator.share) { + await navigator.share({ + title: `Watch Now - ${info?.title?.english}`, + // text: `Watch [${info?.title?.romaji}] and more on Moopa. Join us for endless anime entertainment"`, + url: window.location.href, + }); + } else { + // Web Share API is not supported, provide a fallback or show a message + alert("Web Share API is not supported in this browser."); + } + } catch (error) { + console.error("Error sharing:", error); + } + }; + + return ( +
+ + + {/* MAIN */} +
+
+ coverImage +
+
+
+

+ {info?.season?.toLowerCase()} {info.seasonYear} +

+

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

+

+ {info.title?.english} +

+ + {info?.description && ( + + )} +
+
-
-
- {info && info.status !== "NOT_YET_RELEASED" ? ( - <> -
- -

{info?.type}

-
-
- -

{info?.averageScore ? `${info?.averageScore}%` : "N/A"}

-
-
- - {info?.episodes ? ( -

{info?.episodes} Episodes

- ) : ( -

N/A

- )} -
- + +
+ +
+ + + + + See on AniList + + anilist_icon +
+ +
+ + + +
+ + {info.nextAiringEpisode?.timeUntilAiring && ( +

+ Episode {info.nextAiringEpisode.episode} in{" "} + + {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "} + +

+ )} + + {info?.description && ( + + )} + + + + {info?.relations?.edges?.length > 0 && ( +
+
+ {info?.relations?.edges?.length > 0 && ( +
+ Relations +
+ )} + {info?.relations?.edges?.length > 3 && ( +
setShowAll(!showAll)} + > + {showAll ? "show less" : "show more"} +
+ )} +
+
+ {info?.relations?.edges + .slice(0, showAll ? info?.relations?.edges.length : 3) + .map((r, index) => { + const rel = r.node; + return ( + +
+
+ {rel.id} +
+
+
+ {r.relationType.replace(/_/g, " ")} +
+
+ {rel.title.userPreferred} +
+
{rel.format}
+
+
+ + ); + })} +
+
+ )}
); } diff --git a/components/anime/viewMode/listMode.js b/components/anime/viewMode/listMode.js index f3bcf05..5beded1 100644 --- a/components/anime/viewMode/listMode.js +++ b/components/anime/viewMode/listMode.js @@ -3,7 +3,6 @@ import Link from "next/link"; export default function ListMode({ info, episode, - index, artStorage, providerId, progress, @@ -15,39 +14,32 @@ export default function ListMode({ if (prog > 90) prog = 100; return ( -
- +
+ + {episode.number} + +

-

Episode {episode.number}

- {episode.title && ( -

- "{episode.title}" -

- )} - - {index !== episode?.length - 1 && } -
+ }`} + > + {episode?.title || `Episode ${episode.number}`} +

+

{providerId}

+
+ ); } diff --git a/components/anime/viewMode/thumbnailDetail.js b/components/anime/viewMode/thumbnailDetail.js index 6efeb77..296e0d2 100644 --- a/components/anime/viewMode/thumbnailDetail.js +++ b/components/anime/viewMode/thumbnailDetail.js @@ -5,6 +5,9 @@ export default function ThumbnailDetail({ index, epi, info, + image, + title, + description, provider, artStorage, progress, @@ -25,13 +28,15 @@ export default function ThumbnailDetail({ >
- Anime Cover + {image && ( + Anime Cover + )}

- {epi?.title} + {title}

- {epi?.description && ( + {description && (

- {epi?.description} + {description}

)}
diff --git a/components/anime/viewMode/thumbnailOnly.js b/components/anime/viewMode/thumbnailOnly.js index 99f02bd..69cd8c3 100644 --- a/components/anime/viewMode/thumbnailOnly.js +++ b/components/anime/viewMode/thumbnailOnly.js @@ -3,6 +3,7 @@ import Link from "next/link"; export default function ThumbnailOnly({ info, + image, providerId, episode, artStorage, @@ -35,25 +36,16 @@ export default function ThumbnailOnly({ : "0%", }} /> -
- epi image + {/*
*/} + {image && ( + epi image + )} ); } diff --git a/components/anime/watch/primarySide.js b/components/anime/watch/primarySide.js index b032fd6..a3d9f4f 100644 --- a/components/anime/watch/primarySide.js +++ b/components/anime/watch/primarySide.js @@ -9,18 +9,14 @@ import Link from "next/link"; import Skeleton from "react-loading-skeleton"; import Modal from "../../modal"; import AniList from "../../media/aniList"; -import axios from "axios"; export default function PrimarySide({ info, session, epiNumber, - setLoading, navigation, - loading, providerId, watchId, - status, onList, proxy, disqus, @@ -33,15 +29,31 @@ export default function PrimarySide({ 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 { data } = await axios.get( - `/api/consumet/source/${providerId}/${watchId}` - ); + 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( @@ -65,10 +77,9 @@ export default function PrimarySide({ setSkip({ op, ed }); - setEpisodeData(data); + setEpisodeData(anify); setLoading(false); } - // setMal(malId); } fetchData(); @@ -134,7 +145,7 @@ export default function PrimarySide({
{!loading ? ( - episodeData && ( + navigation && episodeData?.sources?.length !== 0 ? ( + ) : ( +

+ Video is not available, please try other providers +

) ) : ( -
+
+
+
+
+
+
+
+
)}
diff --git a/components/anime/watch/secondarySide.js b/components/anime/watch/secondarySide.js index 5d9b8f9..c9ef684 100644 --- a/components/anime/watch/secondarySide.js +++ b/components/anime/watch/secondarySide.js @@ -4,24 +4,27 @@ import Link from "next/link"; export default function SecondarySide({ info, + map, providerId, watchId, episode, - progress, artStorage, dub, }) { + const progress = info.mediaListEntry?.progress; return (

Up Next

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

- {item.title} + {mapData?.title}

- {item?.description} + {mapData?.description}

-- cgit v1.2.3