diff options
34 files changed, 528 insertions, 200 deletions
diff --git a/.env.example b/.env.example index a79878a..2f85f93 100644 --- a/.env.example +++ b/.env.example @@ -11,4 +11,7 @@ NEXTAUTH_URL="for development use http://localhost:3000/ and for production use PROXY_URI="I recommend you to use this cors-anywhere as a proxy https://github.com/Rob--W/cors-anywhere follow the instruction on how to use it there. Skip this if you only use gogoanime as a source" API_URI="host your own API from this repo https://github.com/consumet/api.consumet.org. Don't put / at the end of the url." API_KEY="this API key is used for schedules and manga page. get the key from https://anify.tv/discord" -DISQUS_SHORTNAME='put your disqus shortname here. (optional)'
\ No newline at end of file +DISQUS_SHORTNAME='put your disqus shortname here. (optional)' + +## Prisma +DATABASE_URL="Your postgresql connection url"
\ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index dfa8f73..dbda85f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,4 +1,7 @@ { "extends": "next/core-web-vitals", - "rules": { "react/no-unescaped-entities": 0 } + "rules": { + "react/no-unescaped-entities": 0, + "react/no-unknown-property": ["error", { "ignore": ["css"] }] + } } @@ -95,6 +95,7 @@ npm install 3. Generate Prisma : ```bash +npx prisma migrate dev npx prisma generate ``` diff --git a/components/anime/episode.js b/components/anime/episode.js index c889c25..5d3451b 100644 --- a/components/anime/episode.js +++ b/components/anime/episode.js @@ -246,6 +246,7 @@ export default function AnimeEpisode({ info, progress }) { info={info} episode={episode} index={index} + artStorage={artStorage} providerId={providerId} progress={progress} dub={isDub} diff --git a/components/anime/infoDetails.js b/components/anime/infoDetails.js index 0cf233c..814e49b 100644 --- a/components/anime/infoDetails.js +++ b/components/anime/infoDetails.js @@ -45,7 +45,10 @@ export default function DesktopDetails({ <div className="hidden lg:flex w-full flex-col gap-5 h-[250px]"> <div className="flex flex-col gap-2"> - <h1 className=" font-inter font-bold text-[36px] text-white line-clamp-1"> + <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 ) : ( diff --git a/components/anime/viewMode/listMode.js b/components/anime/viewMode/listMode.js index 2016262..f3bcf05 100644 --- a/components/anime/viewMode/listMode.js +++ b/components/anime/viewMode/listMode.js @@ -4,10 +4,16 @@ export default function ListMode({ info, episode, index, + artStorage, providerId, progress, dub, }) { + const time = artStorage?.[episode?.id]?.timeWatched; + const duration = artStorage?.[episode?.id]?.duration; + let prog = (time / duration) * 100; + if (prog > 90) prog = 100; + return ( <div key={episode.number} className="flex flex-col gap-3 px-2"> <Link @@ -15,7 +21,11 @@ export default function ListMode({ episode.id )}&num=${episode.number}${dub ? `&dub=${dub}` : ""}`} className={`text-start text-sm lg:text-lg ${ - progress && episode.number <= progress + progress + ? progress && episode.number <= progress + ? "text-[#5f5f5f]" + : "text-white" + : prog === 100 ? "text-[#5f5f5f]" : "text-white" }`} @@ -24,7 +34,11 @@ export default function ListMode({ {episode.title && ( <p className={`text-xs lg:text-sm ${ - progress && episode.number <= progress + progress + ? progress && episode.number <= progress + ? "text-[#5f5f5f]" + : "text-[#b1b1b1]" + : prog === 100 ? "text-[#5f5f5f]" : "text-[#b1b1b1]" } italic`} diff --git a/components/anime/viewMode/thumbnailDetail.js b/components/anime/viewMode/thumbnailDetail.js index a085bc7..6efeb77 100644 --- a/components/anime/viewMode/thumbnailDetail.js +++ b/components/anime/viewMode/thumbnailDetail.js @@ -10,7 +10,7 @@ export default function ThumbnailDetail({ progress, dub, }) { - const time = artStorage?.[epi?.id]?.time; + const time = artStorage?.[epi?.id]?.timeWatched; const duration = artStorage?.[epi?.id]?.duration; let prog = (time / duration) * 100; if (prog > 90) prog = 100; @@ -33,7 +33,7 @@ export default function ThumbnailDetail({ className="object-cover z-30 rounded-lg h-[110px] lg:h-[160px] brightness-[65%]" /> <span - className={`absolute bottom-0 left-0 h-[3px] bg-red-700`} + className={`absolute bottom-0 left-0 h-[2px] bg-red-700`} style={{ width: progress && artStorage && epi?.number <= progress diff --git a/components/anime/viewMode/thumbnailOnly.js b/components/anime/viewMode/thumbnailOnly.js index 6063dfc..99f02bd 100644 --- a/components/anime/viewMode/thumbnailOnly.js +++ b/components/anime/viewMode/thumbnailOnly.js @@ -9,7 +9,7 @@ export default function ThumbnailOnly({ progress, dub, }) { - const time = artStorage?.[episode?.id]?.time; + const time = artStorage?.[episode?.id]?.timeWatched; const duration = artStorage?.[episode?.id]?.duration; let prog = (time / duration) * 100; if (prog > 90) prog = 100; @@ -25,7 +25,7 @@ export default function ThumbnailOnly({ Episode {episode?.number} </span> <span - className={`absolute bottom-7 left-0 h-1 bg-red-600`} + className={`absolute bottom-7 left-0 h-[2px] bg-red-600`} style={{ width: progress && artStorage && episode?.number <= progress diff --git a/components/anime/watch/primary/details.js b/components/anime/watch/primary/details.js index 94c3360..f092879 100644 --- a/components/anime/watch/primary/details.js +++ b/components/anime/watch/primary/details.js @@ -8,6 +8,7 @@ export default function Details({ info, session, epiNumber, + description, id, onList, setOnList, @@ -48,7 +49,10 @@ export default function Details({ <Skeleton height={240} /> )} </div> - <div className="grid w-full pl-5 gap-3 h-[240px]"> + <div + className="grid w-full pl-5 gap-3 h-[240px]" + data-episode={info?.episodes || "0"} + > <div className="grid grid-cols-2 gap-1 items-center"> <h2 className="text-sm font-light font-roboto text-[#878787]"> Studios @@ -93,11 +97,15 @@ export default function Details({ <div className="grid grid-flow-dense grid-cols-2 gap-2 h-full w-full"> {info ? ( <> - <div className="line-clamp-3">{info.title?.romaji || ""}</div> - <div className="line-clamp-3"> + <div className="title-rm line-clamp-3"> + {info.title?.romaji || ""} + </div> + <div className="title-en line-clamp-3"> {info.title?.english || ""} </div> - <div className="line-clamp-3">{info.title?.native || ""}</div> + <div className="title-nt line-clamp-3"> + {info.title?.native || ""} + </div> </> ) : ( <Skeleton width={200} height={50} /> @@ -120,7 +128,7 @@ export default function Details({ <div className={`bg-secondary rounded-md mt-3 mx-3`}> {info && ( <p - dangerouslySetInnerHTML={{ __html: info?.description }} + dangerouslySetInnerHTML={{ __html: description }} className={`p-5 text-sm font-light font-roboto text-[#e4e4e4] `} /> )} diff --git a/components/anime/watch/primarySide.js b/components/anime/watch/primarySide.js index c601795..b032fd6 100644 --- a/components/anime/watch/primarySide.js +++ b/components/anime/watch/primarySide.js @@ -27,6 +27,7 @@ export default function PrimarySide({ setOnList, episodeList, timeWatched, + dub, }) { const [episodeData, setEpisodeData] = useState(); const [open, setOpen] = useState(false); @@ -148,6 +149,7 @@ export default function PrimarySide({ aniTitle={info.title?.romaji || info.title?.english} track={navigation} timeWatched={timeWatched} + dub={dub} /> ) ) : ( @@ -162,13 +164,14 @@ export default function PrimarySide({ <Link href={`/en/anime/${info.id}`} className="hover:underline" + title={navigation?.playing?.title || info.title?.romaji} > {navigation?.playing?.title || info.title?.romaji} </Link> </h1> - <h1 className="text-sm font-karla font-light"> + <h3 className="text-sm font-karla font-light"> Episode {epiNumber} - </h1> + </h3> </div> <div className="flex gap-4 items-center justify-end"> <div className="relative"> @@ -180,7 +183,11 @@ export default function PrimarySide({ (episode) => episode.number === parseInt(e.target.value) ); router.push( - `/en/anime/watch/${info.id}/${providerId}?id=${selectedEpisode.id}&num=${selectedEpisode.number}` + `/en/anime/watch/${info.id}/${providerId}?id=${ + selectedEpisode.id + }&num=${selectedEpisode.number}${ + dub ? `&dub=${dub}` : "" + }` ); }} > @@ -199,7 +206,11 @@ export default function PrimarySide({ }relative group`} onClick={() => { router.push( - `/en/anime/watch/${info.id}/${providerId}?id=${navigation?.next.id}&num=${navigation?.next.number}` + `/en/anime/watch/${info.id}/${providerId}?id=${ + navigation?.next.id + }&num=${navigation?.next.number}${ + dub ? `&dub=${dub}` : "" + }` ); }} > @@ -229,6 +240,7 @@ export default function PrimarySide({ <Details info={info} session={session} + description={navigation?.playing?.description || info?.description} epiNumber={epiNumber} id={watchId} onList={onList} diff --git a/components/anime/watch/secondarySide.js b/components/anime/watch/secondarySide.js index e3f0224..5d9b8f9 100644 --- a/components/anime/watch/secondarySide.js +++ b/components/anime/watch/secondarySide.js @@ -18,7 +18,7 @@ export default function SecondarySide({ {episode && episode.length > 0 ? ( episode.some((item) => item.title && item.description) > 0 ? ( episode.map((item) => { - const time = artStorage?.[item.id]?.time; + const time = artStorage?.[item.id]?.timeWatched; const duration = artStorage?.[item.id]?.duration; let prog = (time / duration) * 100; if (prog > 90) prog = 100; @@ -50,7 +50,7 @@ export default function SecondarySide({ }`} /> <span - className={`absolute bottom-0 left-0 h-[3px] bg-red-700`} + className={`absolute bottom-0 left-0 h-[2px] bg-red-700`} style={{ width: progress && artStorage && item?.number <= progress diff --git a/components/home/content.js b/components/home/content.js index f13c7a8..70f0e3f 100644 --- a/components/home/content.js +++ b/components/home/content.js @@ -5,6 +5,7 @@ import { MdChevronRight } from "react-icons/md"; import { ChevronRightIcon, ArrowRightCircleIcon, + XMarkIcon, } from "@heroicons/react/24/outline"; import { parseCookies } from "nookies"; @@ -12,6 +13,7 @@ import { parseCookies } from "nookies"; import { ChevronLeftIcon } from "@heroicons/react/20/solid"; import { ExclamationCircleIcon, PlayIcon } from "@heroicons/react/24/solid"; import { useRouter } from "next/router"; +import { toast } from "react-toastify"; export default function Content({ ids, @@ -20,6 +22,7 @@ export default function Content({ userData, og, userName, + setRemoved, }) { const router = useRouter(); @@ -139,10 +142,64 @@ export default function Content({ } }; + const removeItem = async (id) => { + if (userName) { + // remove from database + const res = await fetch(`/api/user/update/episode`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: userName, + id: id, + }), + }); + const data = await res.json(); + + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + + // update client + setRemoved(id); + + if (data?.message === "Episode deleted") { + toast.success("Episode removed from history", { + position: "bottom-right", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: true, + draggable: true, + theme: "dark", + }); + } + } else { + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + + setRemoved(id); + } + }; + return ( <div> <div - className={`flex items-center justify-between lg:justify-normal lg:gap-3 px-5 ${ + className={`flex items-center justify-between lg:justify-normal lg:gap-3 px-5 z-40 ${ section === "Recommendations" ? "" : "cursor-pointer" }`} onClick={goToPage} @@ -169,7 +226,6 @@ export default function Content({ onClick={handleClick} ref={containerRef} > - {ids !== "recentlyWatched" ? slicedData?.map((anime) => { const progress = og?.find((i) => i.mediaId === anime.id); @@ -273,14 +329,27 @@ export default function Content({ if (prog > 90) prog = 100; return ( - <Link + <div key={i.watchId} - className="flex flex-col gap-2 shrink-0 cursor-pointer" - href={`/en/anime/watch/${i.aniId}/${ - i.provider - }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`} + className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item" > - <div className="relative w-[320px] aspect-video rounded-md overflow-hidden group"> + <div className="absolute z-40 top-1 right-1 group-hover/item:visible invisible hover:text-action"> + <div + className="flex flex-col items-center group/delete" + onClick={() => removeItem(i.watchId)} + > + <XMarkIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full" /> + <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> + </div> + </div> + <Link + className="relative w-[320px] aspect-video rounded-md overflow-hidden group" + href={`/en/anime/watch/${i.aniId}/${ + i.provider + }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`} + > <div className="w-full h-full bg-gradient-to-t from-black/70 from-20% to-transparent group-hover:to-black/40 transition-all duration-300 ease-out absolute z-30" /> <div className="absolute bottom-3 left-0 mx-2 text-white flex gap-2 items-center w-[80%] z-30"> <PlayIcon className="w-5 h-5 shrink-0" /> @@ -299,6 +368,7 @@ export default function Content({ width: `${prog}%`, }} /> + {i?.image && ( <Image src={i?.image} @@ -308,9 +378,14 @@ export default function Content({ className="w-fit group-hover:scale-[1.02] duration-300 ease-out z-10" /> )} - </div> + </Link> - <div className="flex flex-col font-karla w-full"> + <Link + className="flex flex-col font-karla w-full" + href={`/en/anime/watch/${i.aniId}/${ + i.provider + }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`} + > {/* <h1 className="font-semibold">{i.title}</h1> */} <p className="flex items-center gap-1 text-sm text-gray-400 w-[320px]"> <span @@ -328,24 +403,25 @@ export default function Content({ </span>{" "} | Episode {i.episode} </p> - </div> - </Link> + </Link> + </div> ); })} - {userData?.length >= 10 && section !== "Recommendations" && ( - <div - key={section} - className="flex cursor-pointer" - onClick={goToPage} - > - <div className="w-[320px] aspect-video overflow-hidden object-cover rounded-md border-secondary border-2 flex flex-col gap-2 items-center text-center justify-center text-[#6a6a6a] hover:text-[#9f9f9f] hover:border-[#757575] transition-colors duration-200"> - <h1 className="whitespace-pre-wrap text-sm"> - More on {section} - </h1> - <ArrowRightCircleIcon className="w-5 h-5" /> + {userData?.filter((i) => i.aniId !== null)?.length >= 10 && + section !== "Recommendations" && ( + <div + key={section} + className="flex cursor-pointer" + onClick={goToPage} + > + <div className="w-[320px] aspect-video overflow-hidden object-cover rounded-md border-secondary border-2 flex flex-col gap-2 items-center text-center justify-center text-[#6a6a6a] hover:text-[#9f9f9f] hover:border-[#757575] transition-colors duration-200"> + <h1 className="whitespace-pre-wrap text-sm"> + More on {section} + </h1> + <ArrowRightCircleIcon className="w-5 h-5" /> + </div> </div> - </div> - )} + )} {filteredData?.length >= 10 && section !== "Recommendations" && ( <div key={section} diff --git a/components/home/schedule.js b/components/home/schedule.js index 73c63f0..4043c5e 100644 --- a/components/home/schedule.js +++ b/components/home/schedule.js @@ -117,10 +117,10 @@ export default function Schedule({ data, scheduleData, time }) { > <div className="flex flex-col gap-2 px-2 pt-2"> {scheduleData[days[currentPage]] - .filter((show, index, self) => { + ?.filter((show, index, self) => { return index === self.findIndex((s) => s.id === show.id); }) - .map((i, index) => { + ?.map((i, index) => { const currentTime = Date.now(); const hasAired = i.airingAt < currentTime; diff --git a/components/manga/info/topSection.js b/components/manga/info/topSection.js index 14dc5e5..40b5a37 100644 --- a/components/manga/info/topSection.js +++ b/components/manga/info/topSection.js @@ -66,7 +66,7 @@ export default function TopSection({ info, firstEp, setCookie }) { </div> <div className="w-full flex flex-col justify-start z-40"> <div className="md:h-1/2 py-2 md:py-5 flex flex-col md:gap-2 justify-end"> - <h1 className="text-xl md:text-2xl xl:text-3xl text-white font-semibold font-karla line-clamp-1 text-start"> + <h1 className="title text-xl md:text-2xl xl:text-3xl text-white font-semibold font-karla line-clamp-1 text-start"> {info.title?.romaji || info.title?.english || info.title?.native} </h1> <span className="flex flex-wrap text-xs lg:text-sm md:text-[#747478]"> diff --git a/components/manga/rightBar.js b/components/manga/rightBar.js index 6d37e4a..18c5e55 100644 --- a/components/manga/rightBar.js +++ b/components/manga/rightBar.js @@ -151,6 +151,7 @@ export default function RightBar({ Chapter Progress </label> <input + id="chapter-progress" type="number" placeholder="0" min={0} diff --git a/components/videoPlayer.js b/components/videoPlayer.js index 46129ab..dcde703 100644 --- a/components/videoPlayer.js +++ b/components/videoPlayer.js @@ -35,6 +35,7 @@ export default function VideoPlayer({ track, aniTitle, timeWatched, + dub, }) { const [url, setUrl] = useState(""); const [source, setSource] = useState([]); @@ -226,10 +227,7 @@ export default function VideoPlayer({ watchId: id, title: track?.playing?.title || aniTitle, aniTitle: aniTitle, - image: - track?.playing?.image || - info?.bannerImage || - info?.coverImage?.extraLarge, + image: track?.playing?.image || info?.coverImage?.extraLarge, number: Number(progress), duration: art.duration, timeWatched: art.currentTime, @@ -260,10 +258,7 @@ export default function VideoPlayer({ watchId: id, title: track?.playing?.title || aniTitle, aniTitle: aniTitle, - image: - track?.playing?.image || - info?.bannerImage || - info?.coverImage?.extraLarge, + image: track?.playing?.image || info?.coverImage?.extraLarge, episode: Number(progress), duration: art.duration, timeWatched: art.currentTime, @@ -285,6 +280,12 @@ export default function VideoPlayer({ }); }); + art.on("resize", () => { + art.subtitle.style({ + fontSize: art.height * 0.05 + "px", + }); + }); + art.on("video:timeupdate", async () => { if (!session) return; @@ -313,7 +314,9 @@ export default function VideoPlayer({ router.push( `/en/anime/watch/${aniId}/${provider}?id=${encodeURIComponent( track?.next?.id - )}&num=${track?.next?.number}` + )}&num=${track?.next?.number}${ + dub ? `&dub=${dub}` : "" + }` ); } }, @@ -332,7 +335,7 @@ export default function VideoPlayer({ router.push( `/en/anime/watch/${aniId}/${provider}?id=${encodeURIComponent( track?.next?.id - )}&num=${track?.next?.number}` + )}&num=${track?.next?.number}${dub ? `&dub=${dub}` : ""}` ); } }, 7000); diff --git a/lib/anilist/useAnilist.js b/lib/anilist/useAnilist.js index bedb4a5..72e11ca 100644 --- a/lib/anilist/useAnilist.js +++ b/lib/anilist/useAnilist.js @@ -1,14 +1,18 @@ import { useState, useEffect } from "react"; import { toast } from "react-toastify"; -function useMedia(username, accessToken, status) { +export const useAniList = (session, stats) => { const [media, setMedia] = useState([]); + const accessToken = session?.user?.token; + const username = session?.user?.name; + const status = stats || null; const fetchGraphQL = async (query, variables) => { const response = await fetch("https://graphql.anilist.co/", { method: "POST", headers: { "Content-Type": "application/json", + Authorization: accessToken ? `Bearer ${accessToken}` : undefined, }, body: JSON.stringify({ query, variables }), }); @@ -18,68 +22,47 @@ function useMedia(username, accessToken, status) { useEffect(() => { if (!username || !accessToken) return; const queryMedia = ` - query ($username: String, $status: MediaListStatus) { - MediaListCollection(userName: $username, type: ANIME, status: $status) { - lists { - status - name - entries { - id - mediaId + query ($username: String, $status: MediaListStatus) { + MediaListCollection(userName: $username, type: ANIME, status: $status) { + lists { status - progress - score - media { + name + entries { id + mediaId status - nextAiringEpisode { + progress + score + media { + id + status + nextAiringEpisode { timeUntilAiring episode - } - title { - english - romaji - } - episodes - coverImage { - large + } + title { + english + romaji + } + episodes + coverImage { + large + } } } } } } - } - `; + `; fetchGraphQL(queryMedia, { username, status: status?.stats }).then((data) => setMedia(data.data.MediaListCollection.lists) ); }, [username, accessToken, status?.stats]); - return media; -} - -export function useAniList(session, stats) { - const accessToken = session?.user?.token; - const username = session?.user?.name; - const status = stats || null; - const media = useMedia(username, accessToken, status); - - const fetchGraphQL = async (query, variables) => { - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: accessToken ? `Bearer ${accessToken}` : undefined, - }, - body: JSON.stringify({ query, variables }), - }); - return response.json(); - }; - - const markComplete = (mediaId) => { + const markComplete = async (mediaId) => { if (!accessToken) return; const completeQuery = ` - mutation($mediaId: Int ) { + mutation($mediaId: Int) { SaveMediaListEntry(mediaId: $mediaId, status: COMPLETED) { id mediaId @@ -87,14 +70,13 @@ export function useAniList(session, stats) { } } `; - fetchGraphQL(completeQuery, { mediaId }).then((data) => - console.log({ Complete: data }) - ); + const data = await fetchGraphQL(completeQuery, { mediaId }); + console.log({ Complete: data }); }; - const markPlanning = (mediaId) => { + const markPlanning = async (mediaId) => { if (!accessToken) return; - const completeQuery = ` + const planningQuery = ` mutation($mediaId: Int ) { SaveMediaListEntry(mediaId: $mediaId, status: PLANNING) { id @@ -103,40 +85,98 @@ export function useAniList(session, stats) { } } `; - fetchGraphQL(completeQuery, { mediaId }).then((data) => - console.log({ added_to_list: data }) - ); + const data = await fetchGraphQL(planningQuery, { mediaId }); + console.log({ added_to_list: data }); }; - const markProgress = (mediaId, progress, stats, volumeProgress) => { - if (!accessToken) return; - const progressWatched = ` - mutation($mediaId: Int, $progress: Int, $status: MediaListStatus, $progressVolumes: Int) { - SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status, progressVolumes: $progressVolumes) { + const getUserLists = async (id) => { + const getLists = ` + query ($id: Int) { + Media(id: $id) { + mediaListEntry { + customLists + } id - mediaId - progress - status + type + title { + romaji + english + native + } } } + `; + const data = await fetchGraphQL(getLists, { id }); + return data; + }; + + const customLists = async (lists) => { + const setList = ` + mutation($lists: [String]){ + UpdateUser(animeListOptions: { customLists: $lists }){ + id + } + } + `; + const data = await fetchGraphQL(setList, { lists }); + return data; + }; + + const markProgress = async (mediaId, progress, stats, volumeProgress) => { + if (!accessToken) return; + const progressWatched = ` + mutation($mediaId: Int, $progress: Int, $status: MediaListStatus, $progressVolumes: Int, $lists: [String]) { + SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status, progressVolumes: $progressVolumes, customLists: $lists) { + id + mediaId + progress + status + } + } `; - fetchGraphQL(progressWatched, { - mediaId, - progress, - status: stats, - progressVolumes: volumeProgress, - }).then(() => { - console.log(`Progress Updated: ${progress}`); - toast.success(`Progress Updated: ${progress}`, { - position: "bottom-right", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - draggable: true, - theme: "dark", - }); - }); + + const user = await getUserLists(mediaId); + const media = user?.data?.Media; + if (media) { + let checkList = media?.mediaListEntry?.customLists + ? Object.entries(media?.mediaListEntry?.customLists).map( + ([key, value]) => key + ) || [] + : []; + + if (!checkList?.includes("Watched using Moopa")) { + checkList.push("Watched using Moopa"); + await customLists(checkList); + } + + let lists = media?.mediaListEntry?.customLists + ? Object.entries(media?.mediaListEntry?.customLists) + .filter(([key, value]) => value === true) + .map(([key, value]) => key) || [] + : []; + if (!lists?.includes("Watched using Moopa")) { + lists.push("Watched using Moopa"); + } + if (lists.length > 0) { + await fetchGraphQL(progressWatched, { + mediaId, + progress, + status: stats, + progressVolumes: volumeProgress, + lists, + }); + console.log(`Progress Updated: ${progress}`); + toast.success(`Progress Updated: ${progress}`, { + position: "bottom-right", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: true, + draggable: true, + theme: "dark", + }); + } + } }; - return { media, markComplete, markProgress, markPlanning }; -} + return { media, markComplete, markProgress, markPlanning, getUserLists }; +}; diff --git a/next.config.js b/next.config.js index fcf654b..f7da518 100644 --- a/next.config.js +++ b/next.config.js @@ -18,7 +18,7 @@ module.exports = withPWA({ }, ], }, - distDir: process.env.BUILD_DIR || ".next", + // distDir: process.env.BUILD_DIR || ".next", trailingSlash: true, output: "standalone", // async headers() { diff --git a/package-lock.json b/package-lock.json index a1ff279..cecee3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "moopa", - "version": "3.9.1", + "version": "3.9.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "moopa", - "version": "3.9.1", + "version": "3.9.3", "dependencies": { "@apollo/client": "^3.7.3", "@headlessui/react": "^1.7.15", diff --git a/package.json b/package.json index b5ddad8..76d9adf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moopa", - "version": "3.9.1", + "version": "3.9.3", "private": true, "founder": "Factiven", "scripts": { diff --git a/pages/api/consumet/episode/[id].js b/pages/api/consumet/episode/[id].js index e6f40ce..6e7f318 100644 --- a/pages/api/consumet/episode/[id].js +++ b/pages/api/consumet/episode/[id].js @@ -1,4 +1,3 @@ -import axios from "axios"; import cacheData from "memory-cache"; const API_URL = process.env.API_URI; @@ -9,7 +8,7 @@ export default async function handler(req, res) { const dub = req.query.dub || false; const refresh = req.query.refresh || false; - const providers = ["enime", "gogoanime"]; + const providers = ["enime", "gogoanime", "zoro"]; const datas = []; const cached = cacheData.get(id + dub); @@ -59,7 +58,7 @@ export default async function handler(req, res) { if (datas.length === 0) { return res.status(404).json({ message: "Anime not found" }); } else { - cacheData.put(id + dub, { data: datas }, 1000 * 60 * 60 * 10); + cacheData.put(id + dub, { data: datas }, 1000 * 60 * 60 * 10); res.status(200).json({ data: datas }); } } diff --git a/pages/api/user/profile.js b/pages/api/user/profile.js index dd22bd8..e20aaca 100644 --- a/pages/api/user/profile.js +++ b/pages/api/user/profile.js @@ -43,13 +43,21 @@ export default async function handler(req, res) { } case "DELETE": { const { name } = req.body; - const user = await deleteUser(name); - if (!user) { - return res.status(404).json({ message: "User not found" }); + // return res.status(200).json({ name }); + if (session.user.name !== name) { + return res.status(401).json({ message: "Unauthorized" }); } else { - return res.status(200).json(user); + const user = await deleteUser(name); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } else { + return res.status(200).json(user); + } } } + default: { + return res.status(405).json({ message: "Method not allowed" }); + } } } catch (error) { console.log(error); diff --git a/pages/api/user/update/episode.js b/pages/api/user/update/episode.js index 7974446..52c9494 100644 --- a/pages/api/user/update/episode.js +++ b/pages/api/user/update/episode.js @@ -3,6 +3,7 @@ import { authOptions } from "../../auth/[...nextauth]"; import { createList, + deleteEpisode, getEpisode, updateUserEpisode, } from "../../../../prisma/user"; @@ -16,13 +17,17 @@ export default async function handler(req, res) { case "POST": { const { name, id } = JSON.parse(req.body); - const episode = await createList(name, id); - if (!episode) { - return res - .status(200) - .json({ message: "Episode is already created" }); + if (session.user.name !== name) { + return res.status(401).json({ message: "Unauthorized" }); } else { - return res.status(201).json(episode); + const episode = await createList(name, id); + if (!episode) { + return res + .status(200) + .json({ message: "Episode is already created" }); + } else { + return res.status(201).json(episode); + } } } case "PUT": { @@ -68,6 +73,19 @@ export default async function handler(req, res) { return res.status(200).json(episode); } } + case "DELETE": { + const { name, id } = req.body; + if (session.user.name !== name) { + return res.status(401).json({ message: "Unauthorized" }); + } else { + const episode = await deleteEpisode(name, id); + if (!episode) { + return res.status(404).json({ message: "Episode not found" }); + } else { + return res.status(200).json({ message: "Episode deleted" }); + } + } + } } } catch (error) { console.log(error); diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].js index 5e4aed8..534aa17 100644 --- a/pages/en/anime/[...id].js +++ b/pages/en/anime/[...id].js @@ -125,14 +125,14 @@ export default function Info({ info, color }) { }&image=${info.bannerImage || info.coverImage.extraLarge}`} /> </Head> - <ToastContainer pauseOnFocusLoss={false} /> + <ToastContainer pauseOnHover={false} /> <Modal open={open} onClose={() => handleClose()}> <div> {!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"> + <div className="text-md font-extrabold font-karla"> Edit your list - </h1> + </div> <button className="flex items-center bg-[#363642] rounded-md text-white p-1" onClick={() => signIn("AniListProvider")} diff --git a/pages/en/anime/recently-watched.js b/pages/en/anime/recently-watched.js index 0a7fbae..1cc713a 100644 --- a/pages/en/anime/recently-watched.js +++ b/pages/en/anime/recently-watched.js @@ -7,10 +7,13 @@ import Footer from "../../../components/footer"; import { getServerSession } from "next-auth"; import { authOptions } from "../../api/auth/[...nextauth]"; import MobileNav from "../../../components/home/mobileNav"; +import { ToastContainer, toast } from "react-toastify"; +import { XMarkIcon } from "@heroicons/react/24/outline"; export default function PopularAnime({ sessions }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); + const [remove, setRemoved] = useState(); useEffect(() => { setLoading(true); @@ -46,11 +49,66 @@ export default function PopularAnime({ sessions }) { } }; fetchData(); - }, []); + }, [remove]); + + const removeItem = async (id) => { + if (sessions?.user?.name) { + // remove from database + const res = await fetch(`/api/user/update/episode`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: sessions?.user?.name, + id: id, + }), + }); + const data = await res.json(); + + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + + // update client + setRemoved(id); + + if (data?.message === "Episode deleted") { + toast.success("Episode removed from history", { + position: "bottom-right", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: true, + draggable: true, + theme: "dark", + }); + } + } else { + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + + setRemoved(id); + } + }; return ( <> <MobileNav sessions={sessions} /> + <ToastContainer pauseOnHover={false} /> <div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10"> <div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed left-0 px-3"> <Link href="/en" className="flex gap-2 items-center font-karla"> @@ -68,14 +126,27 @@ export default function PopularAnime({ sessions }) { if (prog > 90) prog = 100; return ( - <Link + <div key={i.watchId} - className="flex flex-col gap-2 shrink-0 cursor-pointer" - href={`/en/anime/watch/${i.aniId}/${ - i.provider - }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`} + className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item" > - <div className="relative md:w-[320px] aspect-video rounded-md overflow-hidden group"> + <div className="absolute z-40 top-1 right-1 group-hover/item:visible invisible hover:text-action"> + <div + className="flex flex-col items-center group/delete" + onClick={() => removeItem(i.watchId)} + > + <XMarkIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full" /> + <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> + </div> + </div> + <Link + className="relative md:w-[320px] aspect-video rounded-md overflow-hidden group" + href={`/en/anime/watch/${i.aniId}/${ + i.provider + }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`} + > <div className="w-full h-full bg-gradient-to-t from-black/70 from-20% to-transparent group-hover:to-black/40 transition-all duration-300 ease-out absolute z-30" /> <div className="absolute bottom-3 left-0 mx-2 text-white flex gap-2 items-center w-[80%] z-30"> <PlayIcon className="w-5 h-5 shrink-0" /> @@ -101,8 +172,13 @@ export default function PopularAnime({ sessions }) { className="w-fit group-hover:scale-[1.02] duration-300 ease-out z-10" /> )} - </div> - <div className="flex flex-col font-karla w-full"> + </Link> + <Link + className="flex flex-col font-karla w-full" + href={`/en/anime/watch/${i.aniId}/${ + i.provider + }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`} + > {/* <h1 className="font-semibold">{i.title}</h1> */} <p className="flex items-center gap-1 text-sm text-gray-400 md:w-[320px]"> <span @@ -119,8 +195,8 @@ export default function PopularAnime({ sessions }) { </span>{" "} | Episode {i.episode} </p> - </div> - </Link> + </Link> + </div> ); })} diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js index e013c6b..c17d9c5 100644 --- a/pages/en/anime/watch/[...info].js +++ b/pages/en/anime/watch/[...info].js @@ -172,8 +172,6 @@ export default function Info({ }; }, [sessions?.user?.name, epiNumber, dub]); - // console.log(proxy); - return ( <> <Head> @@ -199,6 +197,7 @@ export default function Info({ setLoading={setLoading} loading={loading} timeWatched={userData?.timeWatched} + dub={dub} /> <SecondarySide info={info} @@ -230,8 +229,7 @@ export async function getServerSideProps(context) { const proxy = process.env.PROXY_URI; const disqus = process.env.DISQUS_SHORTNAME; - const aniId = query.info[0]; - const provider = query.info[1]; + const [aniId, provider] = query.info; const watchId = query.id; const epiNumber = query.num; const dub = query.dub; diff --git a/pages/en/dmca.js b/pages/en/dmca.js index 8dad7d7..fd93811 100644 --- a/pages/en/dmca.js +++ b/pages/en/dmca.js @@ -14,10 +14,7 @@ export default function DMCA() { property rights of others and complying with the Digital Millennium Copyright Act (DMCA)." /> - <meta - property="og:image" - content="https://cdn.discordapp.com/attachments/1068758633464201268/1081591948705546330/logo.png" - /> + <meta property="og:image" content="/icon-512x512.png" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/c.svg" /> </Head> diff --git a/pages/en/index.js b/pages/en/index.js index 159d257..73b4e94 100644 --- a/pages/en/index.js +++ b/pages/en/index.js @@ -9,7 +9,6 @@ import Content from "../../components/home/content"; import { motion } from "framer-motion"; import { signOut } from "next-auth/react"; -import { useAniList } from "../../lib/anilist/useAnilist"; import { getServerSession } from "next-auth/next"; import { authOptions } from "../api/auth/[...nextauth]"; import SearchBar from "../../components/searchBar"; @@ -25,6 +24,7 @@ import { createUser } from "../../prisma/user"; import { checkAdBlock } from "adblock-checker"; import { ToastContainer, toast } from "react-toastify"; +import { useAniList } from "../../lib/anilist/useAnilist"; export async function getServerSideProps(context) { const session = await getServerSession(context.req, context.res, authOptions); @@ -35,7 +35,6 @@ export async function getServerSideProps(context) { } } catch (error) { console.error(error); - // Handle the error here } const trendingDetail = await aniListData({ @@ -108,8 +107,14 @@ export default function Home({ detail, populars, sessions, upComing }) { useEffect(() => { const getSchedule = async () => { - const { data } = await axios.get(`/api/anify/schedule`); - setSchedules(data); + const res = await fetch(`/api/anify/schedule`); + const data = await res.json(); + + if (!res.ok) { + setSchedules(null); + } else { + setSchedules(data); + } }; getSchedule(); }, []); @@ -120,12 +125,16 @@ export default function Home({ detail, populars, sessions, upComing }) { function getRelease() { let releasingAnime = []; let progress = []; + let seenIds = new Set(); // Create a Set to store the IDs of seen anime release.map((list) => { list.entries.map((entry) => { - if (entry.media.status === "RELEASING") { + if ( + entry.media.status === "RELEASING" && + !seenIds.has(entry.media.id) + ) { releasingAnime.push(entry.media); + seenIds.add(entry.media.id); // Add the ID to the Set } - progress.push(entry); }); }); @@ -139,8 +148,7 @@ export default function Home({ detail, populars, sessions, upComing }) { const [planned, setPlanned] = useState(null); const [greeting, setGreeting] = useState(""); const [user, setUser] = useState(null); - - // console.log({ user }); + const [removed, setRemoved] = useState(); const [prog, setProg] = useState(null); @@ -194,7 +202,7 @@ export default function Home({ detail, populars, sessions, upComing }) { // const data = await res.json(); } userData(); - }, [sessions?.user?.name]); + }, [sessions?.user?.name, removed]); useEffect(() => { const time = new Date().getHours(); @@ -251,7 +259,11 @@ export default function Home({ detail, populars, sessions, upComing }) { /> <meta name="twitter:image" - content="https://cdn.discordapp.com/attachments/1084446049986420786/1093300833422168094/image.png" + content="https://beta.moopa.live/preview.png" + /> + <meta + name="description" + content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!" /> <link rel="icon" href="/c.svg" /> </Head> @@ -262,7 +274,7 @@ export default function Home({ detail, populars, sessions, upComing }) { <Navigasi /> <SearchBar /> <ToastContainer - pauseOnFocusLoss={false} + pauseOnHover={false} style={{ width: "400px", }} @@ -350,6 +362,8 @@ export default function Home({ detail, populars, sessions, upComing }) { ids="recentlyWatched" section="Recently Watched" userData={user} + userName={sessions?.user?.name} + setRemoved={setRemoved} /> </motion.div> )} diff --git a/pages/en/manga/read/[...params].js b/pages/en/manga/read/[...params].js index e608d16..301b646 100644 --- a/pages/en/manga/read/[...params].js +++ b/pages/en/manga/read/[...params].js @@ -115,6 +115,7 @@ export default function Read({ data, currentId, sessions }) { }` : "Getting Info..."} </title> + <meta id="CoverImage" data-manga-cover={info?.coverImage} /> </Head> <div className="w-screen flex justify-evenly relative"> <ToastContainer pauseOnFocusLoss={false} /> diff --git a/pages/index.js b/pages/index.js index 6f020fb..56b2c1f 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,7 +1,26 @@ +import Head from "next/head"; import { parseCookies } from "nookies"; export default function Home() { - return <></>; + return ( + <> + <Head> + <meta + name="twitter:title" + content="Moopa - Free Anime and Manga Streaming" + /> + <meta + name="twitter:description" + content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!" + /> + <meta name="twitter:image" content="/preview.png" /> + <meta + name="description" + content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!" + /> + </Head> + </> + ); } export async function getServerSideProps(context) { diff --git a/prisma/migrations/20230810051657_ondelete_cascade/migration.sql b/prisma/migrations/20230810051657_ondelete_cascade/migration.sql new file mode 100644 index 0000000..a521884 --- /dev/null +++ b/prisma/migrations/20230810051657_ondelete_cascade/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "WatchListEpisode" DROP CONSTRAINT "WatchListEpisode_userProfileId_fkey"; + +-- AddForeignKey +ALTER TABLE "WatchListEpisode" ADD CONSTRAINT "WatchListEpisode_userProfileId_fkey" FOREIGN KEY ("userProfileId") REFERENCES "UserProfile"("name") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f336e54..072415b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -25,7 +25,7 @@ model WatchListEpisode { duration Int? provider String? createdDate DateTime? @default(now()) - userProfile UserProfile @relation(fields: [userProfileId], references: [name]) + userProfile UserProfile @relation(fields: [userProfileId], references: [name], onDelete: Cascade) userProfileId String watchId String } diff --git a/prisma/user.js b/prisma/user.js index 8c436a5..dd61078 100644 --- a/prisma/user.js +++ b/prisma/user.js @@ -1,9 +1,9 @@ -// import { PrismaClient } from "@prisma/client"; +import { Prisma } from "@prisma/client"; // const prisma = new PrismaClient(); import { prisma } from "../lib/prisma"; -export const createUser = async (name, setting) => { +export const createUser = async (name) => { try { const checkUser = await prisma.userProfile.findUnique({ where: { @@ -22,6 +22,15 @@ export const createUser = async (name, setting) => { return null; } } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === "P2002") { + console.log( + "There is a unique constraint violation, a new user cannot be created with this name" + ); + } + } else if (error instanceof Prisma.PrismaClientUnknownRequestError) { + console.log("An unknown Prisma error occurred:", error.message); + } console.error(error); throw new Error("Error creating user"); } @@ -218,19 +227,38 @@ export const updateUserEpisode = async ({ } }; -export const updateTimeWatched = async (id, timeWatched) => { +export const deleteEpisode = async (name, id) => { try { - const user = await prisma.watchListEpisode.update({ + const user = await prisma.watchListEpisode.deleteMany({ where: { - id: id, - }, - data: { - timeWatched: timeWatched, + watchId: id, + userProfileId: name, }, }); - return user; + if (user) { + return user; + } else { + return { message: "Episode not found" }; + } } catch (error) { console.error(error); - throw new Error("Error updating time watched"); + throw new Error("Error deleting episode"); } }; + +// export const updateTimeWatched = async (id, timeWatched) => { +// try { +// const user = await prisma.watchListEpisode.update({ +// where: { +// id: id, +// }, +// data: { +// timeWatched: timeWatched, +// }, +// }); +// return user; +// } catch (error) { +// console.error(error); +// throw new Error("Error updating time watched"); +// } +// }; diff --git a/public/preview.png b/public/preview.png Binary files differnew file mode 100644 index 0000000..b5fd49f --- /dev/null +++ b/public/preview.png |