aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.env.example1
-rw-r--r--.eslintrc.json2
-rw-r--r--.gitignore2
-rw-r--r--.prettierrc.json5
-rw-r--r--README.md34
-rw-r--r--components/admin/dashboard/index.js220
-rw-r--r--components/admin/meta/AppendMeta.js3
-rw-r--r--components/anime/episode.js34
-rw-r--r--components/anime/mobile/topSection.js68
-rw-r--r--components/anime/viewMode/listMode.js2
-rw-r--r--components/home/content.js17
-rw-r--r--components/home/genres.js1
-rw-r--r--components/home/schedule.js27
-rw-r--r--components/listEditor.js53
-rw-r--r--components/manga/chapters.js236
-rw-r--r--components/manga/info/mobile/mobileButton.js39
-rw-r--r--components/manga/info/mobile/topMobile.js16
-rw-r--r--components/manga/info/topSection.js107
-rw-r--r--components/manga/leftBar.js56
-rw-r--r--components/manga/mobile/bottomBar.js36
-rw-r--r--components/manga/mobile/hamburgerMenu.js228
-rw-r--r--components/manga/panels/firstPanel.js72
-rw-r--r--components/manga/panels/secondPanel.js61
-rw-r--r--components/manga/panels/thirdPanel.js46
-rw-r--r--components/manga/rightBar.js128
-rw-r--r--components/search/searchByImage.js119
-rw-r--r--components/searchPalette.js22
-rw-r--r--components/secret.js36
-rw-r--r--components/shared/NavBar.js7
-rw-r--r--components/shared/bugReport.js12
-rw-r--r--components/shared/footer.js2
-rw-r--r--components/watch/player/artplayer.js8
-rw-r--r--components/watch/player/component/controls/subtitle.js3
-rw-r--r--components/watch/player/playerComponent.js84
-rw-r--r--components/watch/player/utils/getZoroSource.js0
-rw-r--r--components/watch/secondary/episodeLists.js47
-rw-r--r--lib/Artplayer.js290
-rw-r--r--lib/anify/getMangaId.js40
-rw-r--r--lib/anify/page.js13
-rw-r--r--lib/anilist/aniAdvanceSearch.js127
-rw-r--r--lib/anilist/getMedia.js2
-rw-r--r--lib/anilist/useAnilist.js7
-rw-r--r--lib/consumet/manga/getChapters.js80
-rw-r--r--lib/consumet/manga/getPage.js49
-rw-r--r--lib/graphql/query.js14
-rw-r--r--next.config.js4
-rw-r--r--package-lock.json471
-rw-r--r--package.json4
-rw-r--r--pages/404.js51
-rw-r--r--pages/_app.js68
-rw-r--r--pages/_error.js41
-rw-r--r--pages/_offline.js45
-rw-r--r--pages/admin/index.js7
-rw-r--r--pages/api/v2/admin/broadcast/index.js54
-rw-r--r--pages/api/v2/admin/bug-report/index.js30
-rw-r--r--pages/api/v2/episode/[id].js171
-rw-r--r--pages/api/v2/etc/recent/[page].js6
-rw-r--r--pages/api/v2/info/[id].js47
-rw-r--r--pages/api/v2/info/index.js60
-rw-r--r--pages/api/v2/pages/[...id].js34
-rw-r--r--pages/api/v2/source/index.js8
-rw-r--r--pages/en/anime/[...id].js11
-rw-r--r--pages/en/anime/recently-watched.js7
-rw-r--r--pages/en/anime/watch/[...info].js23
-rw-r--r--pages/en/index.js38
-rw-r--r--pages/en/manga/[...id].js425
-rw-r--r--pages/en/manga/[id].js146
-rw-r--r--pages/en/manga/read/[...params].js202
-rw-r--r--pages/en/profile/[user].js2
-rw-r--r--pages/en/search/[...param].js305
-rw-r--r--release.md21
-rw-r--r--styles/globals.css7
-rw-r--r--utils/appendMetaToEpisodes.js2
-rw-r--r--utils/getRedisWithPrefix.js13
-rw-r--r--utils/getTimes.js7
-rw-r--r--utils/imageUtils.js22
76 files changed, 3144 insertions, 1644 deletions
diff --git a/.env.example b/.env.example
index b3184ba..ac71088 100644
--- a/.env.example
+++ b/.env.example
@@ -10,7 +10,6 @@ NEXTAUTH_URL="for development use http://localhost:3000/ and for production use
## NextJS
PROXY_URI="This is what I use for proxying video https://github.com/chaycee/M3U8Proxy. Don't put / at the end of the url."
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, anime and manga page. get the key from https://anify.tv/discord"
DISQUS_SHORTNAME='put your disqus shortname here (optional)'
# ADMIN_USERNAME=""
diff --git a/.eslintrc.json b/.eslintrc.json
index dbda85f..4658cc5 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,6 +1,8 @@
{
"extends": "next/core-web-vitals",
+ // ignore react-hooks/exhaustive-deps
"rules": {
+ "react-hooks/exhaustive-deps": "off",
"react/no-unescaped-entities": 0,
"react/no-unknown-property": ["error", { "ignore": ["css"] }]
}
diff --git a/.gitignore b/.gitignore
index 4d91deb..1e8ff29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,7 +8,7 @@
# testing
/coverage
-/pages/test.js
+/pages/en/test.js
/components/devComp
# next.js
diff --git a/.prettierrc.json b/.prettierrc.json
index 0967ef4..08df606 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -1 +1,4 @@
-{}
+{
+ "bracketSpacing": true,
+ "printWidth": 80
+}
diff --git a/README.md b/README.md
index 6d3abd6..06b95b8 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,11 @@
</p>
<h3 align="center">Watch Page</h3>
-<img src="https://github.com/Ani-Moopa/Moopa/assets/97084324/c654aa13-76d7-47fe-ac02-924fbbb40f76"/>
+<p align="center">Normal Mode</p>
+<img src="https://github.com/Ani-Moopa/Moopa/assets/97084324/03b2c9c7-eb25-4f2c-8f26-a9ae817bfbaa"/>
+<br/>
+<p align="center">Theater Mode</p>
+<img src="https://github.com/Ani-Moopa/Moopa/assets/97084324/767a0335-f6a3-4969-b415-3c45d07cce64"/>
<h3 align="center">Manga Reader</h3>
<img src="https://github.com/DevanAbinaya/Ani-Moopa/assets/97084324/ccd2ee11-4ee3-411c-b634-d48c84f1a9e2"/>
@@ -54,13 +58,23 @@
## Features
-- Free ad-supported streaming service
-- Anime tracking through Anilist API
-- Skip OP/ED buttons
-- Dub Anime support
-- User-friendly interface
-- Mobile-responsive design
-- PWA supported
+- General
+ - Free ad-supported streaming service
+ - Dub Anime support
+ - User-friendly interface
+ - Auto sync with AniList
+ - Add Anime/Manga to your AniList
+ - Scene Searching powered by [trace.moe](https://trace.moe)
+ - PWA supported
+ - Mobile responsive
+ - Fast page load
+- Watch Page
+ - Player
+ - Autoplay next episode
+ - Skip op/ed button
+ - Theater mode
+ - Comment section
+- Profile page to see your watch list
## To Do List
@@ -116,9 +130,7 @@ NEXTAUTH_URL="for development use http://localhost:3000/ and for production use
## NextJS
PROXY_URI="This is what I use for proxying video https://github.com/chaycee/M3U8Proxy. Don't put / at the end of the url."
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, anime and manga page. get the key from https://anify.tv/discord"
DISQUS_SHORTNAME='put your disqus shortname here (optional)'
-# ADMIN_USERNAME=""
## Prisma
DATABASE_URL="Your postgresql connection url"
@@ -131,7 +143,7 @@ REDIS_URL="rediss://username:password@host:port"
5. Add this endpoint as Redirect Url on AniList Developer :
```bash
-https://your-website-url/api/auth/callback/AniListProvider
+https://your-website-domain/api/auth/callback/AniListProvider
```
6. Start local server :
diff --git a/components/admin/dashboard/index.js b/components/admin/dashboard/index.js
index 64a1d6f..d0c9963 100644
--- a/components/admin/dashboard/index.js
+++ b/components/admin/dashboard/index.js
@@ -1,4 +1,6 @@
-import React, { useState } from "react";
+import Link from "next/link";
+import React, { useEffect, useState } from "react";
+import { toast } from "sonner";
export default function AdminDashboard({
animeCount,
@@ -10,13 +12,90 @@ export default function AdminDashboard({
const [selectedTime, setSelectedTime] = useState("");
const [unixTimestamp, setUnixTimestamp] = useState(null);
- const handleSubmit = (e) => {
+ const [broadcast, setBroadcast] = useState();
+ const [reportId, setReportId] = useState();
+
+ useEffect(() => {
+ async function getBroadcast() {
+ const res = await fetch("/api/v2/admin/broadcast", {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Broadcast-Key": "get-broadcast",
+ },
+ });
+ const data = await res.json();
+ if (data) {
+ setBroadcast(data);
+ }
+ }
+ getBroadcast();
+ }, []);
+
+ const handleSubmit = async (e) => {
e.preventDefault();
+ let unixTime;
+
if (selectedTime) {
- const unixTime = Math.floor(new Date(selectedTime).getTime() / 1000);
+ unixTime = Math.floor(new Date(selectedTime).getTime() / 1000);
setUnixTimestamp(unixTime);
}
+
+ const res = await fetch("/api/v2/admin/broadcast", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Broadcast-Key": "get-broadcast",
+ },
+ body: JSON.stringify({
+ message,
+ startAt: unixTime,
+ show: true,
+ }),
+ });
+
+ const data = await res.json();
+
+ console.log({ message, unixTime, data });
+ };
+
+ const handleRemove = async () => {
+ try {
+ const res = await fetch("/api/v2/admin/broadcast", {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Broadcast-Key": "get-broadcast",
+ },
+ });
+ const data = await res.json();
+ console.log(data);
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ const handleResolved = async () => {
+ try {
+ console.log(reportId);
+ if (!reportId) return toast.error("reportId is required");
+ const res = await fetch("/api/v2/admin/bug-report", {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ reportId,
+ }),
+ });
+ const data = await res.json();
+ if (res.status === 200) {
+ toast.success(data.message);
+ }
+ } catch (error) {
+ console.log(`error while resolving ${error}`);
+ }
};
return (
<div className="flex flex-col gap-5 px-5 py-10 h-full">
@@ -39,7 +118,21 @@ export default function AdminDashboard({
</div>
<div className="grid grid-cols-2 gap-5 h-full">
<div className="flex flex-col gap-2">
- <p className="font-semibold">Broadcast</p>
+ <p className="flex items-center gap-2 font-semibold">
+ Broadcast
+ <span className="relative w-5 h-5 flex-center shrink-0">
+ <span
+ className={`absolute animate-ping inline-flex h-full w-full rounded-full ${
+ broadcast?.show === true ? "bg-green-500" : "hidden"
+ } opacity-75`}
+ ></span>
+ <span
+ className={`relative inline-flex rounded-full h-3 w-3 ${
+ broadcast?.show === true ? "bg-green-500" : "bg-red-500"
+ }`}
+ ></span>
+ </span>
+ </p>
<div className="flex flex-col justify-between bg-secondary rounded p-5 h-full">
<form onSubmit={handleSubmit}>
<div className="mb-4">
@@ -70,16 +163,24 @@ export default function AdminDashboard({
id="selectedTime"
value={selectedTime}
onChange={(e) => setSelectedTime(e.target.value)}
- required
className="w-full px-3 py-2 border rounded-md focus:outline-none text-black"
/>
</div>
- <button
- type="submit"
- className="bg-image text-white py-2 px-4 rounded-md hover:bg-opacity-80 transition duration-300"
- >
- Submit
- </button>
+ <div className="flex font-karla font-semibold gap-2">
+ <button
+ type="submit"
+ className="bg-image text-white py-2 px-4 rounded-md hover:bg-opacity-80 transition duration-300"
+ >
+ Broadcast
+ </button>
+ <button
+ type="button"
+ onClick={handleRemove}
+ className="bg-red-700 text-white py-2 px-4 rounded-md hover:bg-opacity-80 transition duration-300"
+ >
+ Remove
+ </button>
+ </div>
</form>
{unixTimestamp && (
<p>
@@ -95,40 +196,85 @@ export default function AdminDashboard({
{report?.map((i, index) => (
<div
key={index}
- className="odd:bg-primary/80 even:bg-primary/40 p-2 flex justify-between items-center"
+ className="odd:bg-primary/80 even:bg-primary/40 hover:odd:bg-image/20 hover:even:bg-image/20 p-2 flex justify-between items-center"
>
- {i.desc}{" "}
- {i.severity === "Low" && (
- <span className="relative w-5 h-5 flex-center shrink-0">
- {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */}
- <span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
- </span>
- )}
- {i.severity === "Medium" && (
- <span className="relative w-5 h-5 flex-center shrink-0">
- {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */}
- <span className="relative inline-flex rounded-full h-3 w-3 bg-amber-500"></span>
- </span>
- )}
- {i.severity === "High" && (
- <span className="relative w-5 h-5 flex-center shrink-0">
- {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */}
- <span className="relative animate-pulse inline-flex rounded-full h-3 w-3 bg-rose-500"></span>
- </span>
- )}
- {i.severity === "Critical" && (
- <span className="relative w-5 h-5 flex-center shrink-0">
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-900 opacity-75"></span>
- <span className="relative inline-flex rounded-full h-3 w-3 bg-red-900"></span>
+ <Link
+ href={i.url}
+ className="flex font-inter items-center gap-2 group"
+ >
+ {i.desc}{" "}
+ <span className="w-4 h-4 text-image group-hover:text-white">
+ <svg
+ fill="none"
+ stroke="currentColor"
+ strokeWidth={1.5}
+ viewBox="0 0 24 24"
+ xmlns="http://www.w3.org/2000/svg"
+ aria-hidden="true"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
+ />
+ </svg>
</span>
- )}
+ </Link>
+ <div className="flex items-center gap-2">
+ {i.severity === "Low" && (
+ <span className="relative w-5 h-5 flex-center shrink-0">
+ {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */}
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
+ </span>
+ )}
+ {i.severity === "Medium" && (
+ <span className="relative w-5 h-5 flex-center shrink-0">
+ {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */}
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-amber-500"></span>
+ </span>
+ )}
+ {i.severity === "High" && (
+ <span className="relative w-5 h-5 flex-center shrink-0">
+ {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */}
+ <span className="relative animate-pulse inline-flex rounded-full h-3 w-3 bg-rose-500"></span>
+ </span>
+ )}
+ {i.severity === "Critical" && (
+ <span className="relative w-5 h-5 flex-center shrink-0">
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-900 opacity-75"></span>
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-red-900"></span>
+ </span>
+ )}
+ <button
+ type="button"
+ onClick={() => {
+ setReportId(i?.id);
+ handleResolved();
+ }}
+ className="w-6 h-6 hover:text-green-500"
+ >
+ <svg
+ fill="none"
+ stroke="currentColor"
+ strokeWidth={1.5}
+ viewBox="0 0 24 24"
+ xmlns="http://www.w3.org/2000/svg"
+ aria-hidden="true"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M4.5 12.75l6 6 9-13.5"
+ />
+ </svg>
+ </button>
+ </div>
</div>
))}
</div>
</div>
</div>
</div>
- <div className="w-full h-full">a</div>
</div>
);
}
diff --git a/components/admin/meta/AppendMeta.js b/components/admin/meta/AppendMeta.js
index 1707ed2..e49fcad 100644
--- a/components/admin/meta/AppendMeta.js
+++ b/components/admin/meta/AppendMeta.js
@@ -1,7 +1,7 @@
import Loading from "@/components/shared/loading";
import Image from "next/image";
import { useState } from "react";
-import { toast } from "react-toastify";
+import { toast } from "sonner";
// Define a function to convert the data
function convertData(episodes) {
@@ -217,6 +217,7 @@ export default function AppendMeta({ api }) {
</p>
<Image
src={i.image}
+ alt="query-image"
width={500}
height={500}
className="w-[160px] h-[210px] object-cover"
diff --git a/components/anime/episode.js b/components/anime/episode.js
index 25ed997..a42307f 100644
--- a/components/anime/episode.js
+++ b/components/anime/episode.js
@@ -4,7 +4,7 @@ import ViewSelector from "./viewSelector";
import ThumbnailOnly from "./viewMode/thumbnailOnly";
import ThumbnailDetail from "./viewMode/thumbnailDetail";
import ListMode from "./viewMode/listMode";
-import { toast } from "react-toastify";
+import { toast } from "sonner";
export default function AnimeEpisode({
info,
@@ -34,16 +34,12 @@ export default function AnimeEpisode({
info.status === "RELEASING" ? "true" : "false"
}${isDub ? "&dub=true" : ""}`
).then((res) => res.json());
- const getMap = response.find((i) => i?.map === true) || response[0];
+ const getMap = response.find((i) => i?.map === true);
let allProvider = response;
if (getMap) {
allProvider = response.filter((i) => {
- if (
- i?.providerId === "gogoanime" &&
- i?.providerId === "9anime" &&
- i?.map !== true
- ) {
+ if (i?.providerId === "gogoanime" && i?.map !== true) {
return null;
}
return i;
@@ -66,9 +62,12 @@ export default function AnimeEpisode({
fetchData();
return () => {
+ setCurrentPage(1);
setProviders(null);
setMapProviders(null);
};
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [info.id, isDub]);
const episodes =
@@ -79,9 +78,7 @@ export default function AnimeEpisode({
const lastEpisodeIndex = currentPage * itemsPerPage;
const firstEpisodeIndex = lastEpisodeIndex - itemsPerPage;
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) => {
@@ -104,6 +101,7 @@ export default function AnimeEpisode({
) {
setView(3);
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [providerId, episodes]);
useEffect(() => {
@@ -122,6 +120,7 @@ export default function AnimeEpisode({
setWatch(null);
}
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [episodes]);
useEffect(() => {
@@ -157,6 +156,7 @@ export default function AnimeEpisode({
return;
}
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [providerId, artStorage, info.id, session?.user?.name]);
let debounceTimeout;
@@ -173,12 +173,7 @@ export default function AnimeEpisode({
);
if (!res.ok) {
console.log(res);
- toast.error("Something went wrong", {
- position: "bottom-left",
- autoClose: 3000,
- hideProgressBar: true,
- theme: "colored",
- });
+ toast.error("Something went wrong");
setProviders([]);
setLoading(false);
} else {
@@ -213,12 +208,7 @@ export default function AnimeEpisode({
}, 1000);
} catch (err) {
console.log(err);
- toast.error("Something went wrong", {
- position: "bottom-left",
- autoClose: 3000,
- hideProgressBar: true,
- theme: "colored",
- });
+ toast.error("Something went wrong");
}
};
diff --git a/components/anime/mobile/topSection.js b/components/anime/mobile/topSection.js
index 761a9fd..e5f58da 100644
--- a/components/anime/mobile/topSection.js
+++ b/components/anime/mobile/topSection.js
@@ -1,4 +1,9 @@
-import { PlayIcon, PlusIcon, ShareIcon } from "@heroicons/react/24/solid";
+import {
+ BookOpenIcon,
+ PlayIcon,
+ PlusIcon,
+ ShareIcon,
+} from "@heroicons/react/24/solid";
import Image from "next/image";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
@@ -21,6 +26,8 @@ export default function DetailTop({
const [showAll, setShowAll] = useState(false);
+ const isAnime = info.type === "ANIME";
+
useEffect(() => {
setReadMore(false);
}, [info.id]);
@@ -29,7 +36,7 @@ export default function DetailTop({
try {
if (navigator.share) {
await navigator.share({
- title: `Watch Now - ${info?.title?.english}`,
+ title: `${isAnime ? "Watch" : "Read"} Now - ${info?.title?.english}`,
// text: `Watch [${info?.title?.romaji}] and more on Moopa. Join us for endless anime entertainment"`,
url: window.location.href,
});
@@ -50,7 +57,7 @@ export default function DetailTop({
<div className="flex flex-col md:flex-row w-full items-center md:items-end gap-5 pt-12">
<div className="shrink-0 w-[180px] h-[250px] rounded overflow-hidden">
<Image
- src={info?.coverImage?.extraLarge}
+ src={info?.coverImage?.extraLarge || info?.coverImage}
alt="poster anime"
width={300}
height={300}
@@ -59,8 +66,9 @@ export default function DetailTop({
</div>
<div className="flex flex-col gap-4 items-center md:items-start justify-end w-full">
<div className="flex flex-col gap-1 text-center md:text-start">
- <h3 className="font-karla text-lg capitalize leading-none">
- {info?.season?.toLowerCase()} {info.seasonYear}
+ <h3 className="font-karla text-lg capitalize leading-none">
+ {info?.season?.toLowerCase() || getMonth(info?.startDate?.month)}{" "}
+ {info.seasonYear || info?.startDate?.year}
</h3>
<h1 className="font-outfit font-extrabold text-2xl md:text-4xl line-clamp-2 text-white">
{info?.title?.romaji || info?.title?.english}
@@ -87,12 +95,20 @@ export default function DetailTop({
onClick={() => router.push(watchUrl)}
className={`${
!watchUrl ? "opacity-30 pointer-events-none" : ""
- } w-[180px] flex-center text-lg font-karla font-semibold gap-1 border-black border-opacity-10 text-black rounded-full py-1 px-4 bg-white hover:opacity-80`}
+ } w-[180px] flex-center text-lg font-karla font-semibold gap-2 border-black border-opacity-10 text-black rounded-full py-1 px-4 bg-white hover:opacity-80`}
>
- <PlayIcon className="w-5 h-5" />
+ {isAnime ? (
+ <PlayIcon className="w-5 h-5" />
+ ) : (
+ <BookOpenIcon className="w-5 h-5" />
+ )}
{progress > 0 ? (
statuses?.value === "COMPLETED" ? (
- "Rewatch"
+ isAnime ? (
+ "Rewatch"
+ ) : (
+ "Reread"
+ )
) : !watchUrl && info?.nextAiringEpisode ? (
<span>
{convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "}
@@ -100,8 +116,10 @@ export default function DetailTop({
) : (
"Continue"
)
- ) : (
+ ) : isAnime ? (
"Watch Now"
+ ) : (
+ "Read Now"
)}
</button>
<div className="flex gap-2">
@@ -121,14 +139,14 @@ export default function DetailTop({
onClick={handleShareClick}
>
<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">
- Share Anime
+ Share {isAnime ? "Anime" : "Manga"}
</span>
<ShareIcon className="w-5 h-5" />
</button>
<a
target="_blank"
rel="noopener noreferrer"
- href={`https://anilist.co/anime/${info.id}`}
+ href={`https://anilist.co/${info.type.toLowerCase()}/${info.id}`}
className="flex-center group relative w-10 h-10 bg-secondary rounded-full"
>
<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">
@@ -156,18 +174,24 @@ export default function DetailTop({
<PlusIcon className="w-5 h-5" />
</button>
<button
- // href={watchUrl || ""}
type="button"
- // disabled={!watchUrl || info?.nextAiringEpisode}
onClick={() => router.push(watchUrl)}
className={`${
!watchUrl ? "opacity-30 pointer-events-none" : ""
} flex items-center text-lg font-karla font-semibold gap-1 border-black border-opacity-10 text-black rounded-full py-2 px-4 bg-white`}
>
- <PlayIcon className="w-5 h-5" />
+ {isAnime ? (
+ <PlayIcon className="w-5 h-5" />
+ ) : (
+ <BookOpenIcon className="w-5 h-5" />
+ )}
{progress > 0 ? (
statuses?.value === "COMPLETED" ? (
- "Rewatch"
+ isAnime ? (
+ "Rewatch"
+ ) : (
+ "Reread"
+ )
) : !watchUrl && info?.nextAiringEpisode ? (
<span>
{convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "}
@@ -175,8 +199,10 @@ export default function DetailTop({
) : (
"Continue"
)
- ) : (
+ ) : isAnime ? (
"Watch Now"
+ ) : (
+ "Read Now"
)}
</button>
<button
@@ -185,7 +211,7 @@ export default function DetailTop({
onClick={handleShareClick}
>
<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">
- Share Anime
+ Share {isAnime ? "Anime" : "Manga"}
</span>
<ShareIcon className="w-5 h-5" />
</button>
@@ -287,3 +313,11 @@ export default function DetailTop({
</div>
);
}
+
+function getMonth(month) {
+ if (!month) return "";
+ const formattedMonth = new Date(0, month).toLocaleString("default", {
+ month: "long",
+ });
+ return formattedMonth;
+}
diff --git a/components/anime/viewMode/listMode.js b/components/anime/viewMode/listMode.js
index 5beded1..a6a1cf6 100644
--- a/components/anime/viewMode/listMode.js
+++ b/components/anime/viewMode/listMode.js
@@ -19,7 +19,7 @@ export default function ListMode({
href={`/en/anime/watch/${info.id}/${providerId}?id=${encodeURIComponent(
episode.id
)}&num=${episode.number}${dub ? `&dub=${dub}` : ""}`}
- className={`flex gap-3 py-4 hover:bg-secondary/10 odd:bg-secondary/30 even:bg-primary`}
+ className={`flex gap-3 py-4 hover:bg-secondary odd:bg-secondary/30 even:bg-primary`}
>
<div className="flex w-full">
<span className="shrink-0 px-4 text-center text-white/50">
diff --git a/components/home/content.js b/components/home/content.js
index 651d276..678549c 100644
--- a/components/home/content.js
+++ b/components/home/content.js
@@ -13,8 +13,8 @@ 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";
import HistoryOptions from "./content/historyOptions";
+import { toast } from "sonner";
export default function Content({
ids,
@@ -24,6 +24,7 @@ export default function Content({
og,
userName,
setRemoved,
+ type = "anime",
}) {
const router = useRouter();
@@ -53,6 +54,7 @@ export default function Content({
} else if (lang === "id") {
setLang("id");
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [scrollLeft, setScrollLeft] = useState(false);
@@ -174,14 +176,7 @@ export default function Content({
setRemoved(id || aniId);
if (data?.message === "Episode deleted") {
- toast.success("Episode removed from history", {
- position: "bottom-right",
- autoClose: 5000,
- hideProgressBar: false,
- closeOnClick: true,
- draggable: true,
- theme: "dark",
- });
+ toast.success("Episode removed from history");
}
} else {
if (id) {
@@ -259,7 +254,7 @@ export default function Content({
href={
ids === "listManga"
? `/en/manga/${anime.id}`
- : `/${lang}/anime/${anime.id}`
+ : `/en/${type}/${anime.id}`
}
className="hover:scale-105 hover:shadow-lg duration-300 ease-out group relative"
title={anime.title.romaji}
@@ -352,7 +347,7 @@ export default function Content({
href={
ids === "listManga"
? `/en/manga/${anime.id}`
- : `/en/anime/${anime.id}`
+ : `/en/${type.toLowerCase()}/${anime.id}`
}
className="w-[135px] lg:w-[185px] line-clamp-2"
title={anime.title.romaji}
diff --git a/components/home/genres.js b/components/home/genres.js
index f054fc9..cd247ce 100644
--- a/components/home/genres.js
+++ b/components/home/genres.js
@@ -47,6 +47,7 @@ export default function Genres() {
} else if (lang === "id") {
setLang("id");
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="antialiased">
diff --git a/components/home/schedule.js b/components/home/schedule.js
index a0ab691..bb35d08 100644
--- a/components/home/schedule.js
+++ b/components/home/schedule.js
@@ -48,21 +48,26 @@ export default function Schedule({ data, scheduleData, anime, update }) {
<h1 className="font-bold font-karla text-[20px] lg:px-5">
Don't miss out!
</h1>
- <div className="rounded mb-5 shadow-md shadow-black">
+ <div className="rounded mb-5 shadow-tersier/50 shadow-button">
<div className="overflow-hidden w-full h-[96px] lg:h-[10rem] rounded relative">
- <div className="absolute flex flex-col lg:gap-1 justify-center pl-5 lg:pl-16 rounded z-20 bg-gradient-to-r from-30% from-tersier to-transparent w-full h-full">
- <h1 className="text-xs lg:text-lg">Coming Up Next!</h1>
- <div className="w-1/2 lg:w-2/5 hidden lg:block font-karla font-medium">
+ <div className="absolute flex flex-col -space-y-1 lg:gap-1 justify-center pl-5 lg:pl-16 rounded z-20 bg-gradient-to-r from-30% from-tersier to-transparent w-full h-full">
+ <h1 className="text-xs lg:text-lg font-karla font-thin">
+ Coming Up Next!
+ </h1>
+ <div className="w-1/2 lg:w-2/5 hidden lg:flex font-karla font-semibold line-clamp-2">
<Link
href={`/en/anime/${data.id}`}
- className="hover:underline underline-offset-4 decoration-2 leading-3 lg:text-[1.5vw]"
+ className="hover:underline underline-offset-4 decoration-2 leading-8 line-clamp-2 lg:text-[1.5vw]"
>
{data.title.romaji || data.title.english || data.title.native}
</Link>
</div>
- <h1 className="w-1/2 lg:hidden font-medium font-karla leading-9 text-white line-clamp-1">
+ <Link
+ href={`/en/anime/${data.id}`}
+ className="w-1/2 lg:hidden font-medium font-karla leading-9 text-white line-clamp-1"
+ >
{data.title.romaji || data.title.english || data.title.native}
- </h1>
+ </Link>
</div>
{data.bannerImage ? (
<Image
@@ -79,16 +84,16 @@ export default function Schedule({ data, scheduleData, anime, update }) {
height={500}
sizes="100vw"
alt="banner next anime"
- className="absolute z-10 top-0 right-0 h-full object-contain object-right brightness-[90%]"
+ className="absolute z-10 top-0 right-0 w-3/4 lg:w-auto h-full object-cover lg:object-contain object-right opacity-30 lg:opacity-100 brightness-[90%]"
/>
)}
<div
- className={`absolute flex justify-end items-center pr-5 gap-5 md:gap-10 z-20 w-1/2 h-full right-0 ${
- data.bannerImage ? "md:pr-16" : "md:pr-48"
+ className={`absolute flex justify-end items-center pr-5 gap-5 lg:gap-10 z-20 w-1/2 h-full right-0 ${
+ data.bannerImage ? "lg:pr-16" : "lg:pr-48"
}`}
>
{/* Countdown Timer */}
- <div className="flex items-center gap-2 md:gap-5 font-bold font-karla text-sm md:text-xl">
+ <div className="flex items-center gap-2 lg:gap-5 font-bold font-karla text-sm lg:text-xl">
{/* Countdown Timer */}
<div className="flex flex-col items-center">
<span className="text-action/80">{day}</span>
diff --git a/components/listEditor.js b/components/listEditor.js
index fa249e3..f4f46ea 100644
--- a/components/listEditor.js
+++ b/components/listEditor.js
@@ -1,7 +1,7 @@
import { useState } from "react";
import Image from "next/image";
-import { toast } from "react-toastify";
import { useRouter } from "next/router";
+import { toast } from "sonner";
const ListEditor = ({
animeId,
@@ -9,11 +9,12 @@ const ListEditor = ({
stats,
prg,
max,
- image = null,
+ info = null,
close,
}) => {
const [status, setStatus] = useState(stats ?? "CURRENT");
const [progress, setProgress] = useState(prg ?? 0);
+ const isAnime = info?.type === "ANIME";
const router = useRouter();
@@ -47,27 +48,11 @@ const ListEditor = ({
});
const { data } = await response.json();
if (data.SaveMediaListEntry === null) {
- toast.error("Something went wrong", {
- position: "bottom-right",
- autoClose: 5000,
- hideProgressBar: true,
- closeOnClick: false,
- pauseOnHover: true,
- draggable: true,
- theme: "colored",
- });
+ toast.error("Something went wrong");
return;
}
console.log("Saved media list entry", data);
- toast.success("Media list entry saved", {
- position: "bottom-right",
- autoClose: 5000,
- hideProgressBar: true,
- closeOnClick: false,
- pauseOnHover: true,
- draggable: true,
- theme: "dark",
- });
+ toast.success("Media list entry saved");
close();
setTimeout(() => {
// window.location.reload();
@@ -75,15 +60,7 @@ const ListEditor = ({
}, 1000);
// showAlert("Media list entry saved", "success");
} catch (error) {
- toast.error("Something went wrong", {
- position: "bottom-right",
- autoClose: 5000,
- hideProgressBar: true,
- closeOnClick: false,
- pauseOnHover: true,
- draggable: true,
- theme: "colored",
- });
+ toast.error("Something went wrong");
console.error(error);
}
};
@@ -95,10 +72,10 @@ const ListEditor = ({
</div>
<div className="relative bg-secondary rounded-sm w-screen md:w-auto">
<div className="md:flex">
- {image && (
+ {info?.bannerImage && (
<div>
<Image
- src={image.coverImage.large}
+ src={info.coverImage.large}
alt="image"
height={500}
width={500}
@@ -106,9 +83,9 @@ const ListEditor = ({
/>
<Image
src={
- image.bannerImage ||
- image.coverImage.extraLarge ||
- image.coverImage.large
+ info.bannerImage ||
+ info.coverImage.extraLarge ||
+ info.coverImage.large
}
alt="image"
height={500}
@@ -136,11 +113,15 @@ const ListEditor = ({
onChange={(e) => setStatus(e.target.value)}
className="rounded-sm px-2 py-1 bg-[#363642] w-[50%] sm:w-[150px] text-sm sm:text-base"
>
- <option value="CURRENT">Watching</option>
+ <option value="CURRENT">
+ {isAnime ? "Watching" : "Reading"}
+ </option>
<option value="COMPLETED">Completed</option>
<option value="PAUSED">Paused</option>
<option value="DROPPED">Dropped</option>
- <option value="PLANNING">Plan to watch</option>
+ <option value="PLANNING">
+ Plan to {isAnime ? "watch" : "read"}
+ </option>
</select>
</div>
<div className="flex justify-between items-center mt-2">
diff --git a/components/manga/chapters.js b/components/manga/chapters.js
index fd7beea..2150686 100644
--- a/components/manga/chapters.js
+++ b/components/manga/chapters.js
@@ -1,13 +1,16 @@
import Link from "next/link";
import { useState, useEffect } from "react";
-import { ChevronDownIcon } from "@heroicons/react/24/outline";
-import { setCookie } from "nookies";
+import {
+ ChevronDownIcon,
+ ChevronLeftIcon,
+ ChevronRightIcon,
+} from "@heroicons/react/24/outline";
-const ChapterSelector = ({ chaptersData, data, setFirstEp, userManga }) => {
+const ChapterSelector = ({ chaptersData, data, setWatch, mangaId }) => {
const [selectedProvider, setSelectedProvider] = useState(
chaptersData[0]?.providerId || ""
);
- const [selectedChapter, setSelectedChapter] = useState("");
+ // const [selectedChapter, setSelectedChapter] = useState("");
const [chapters, setChapters] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [chaptersPerPage] = useState(10);
@@ -16,13 +19,15 @@ const ChapterSelector = ({ chaptersData, data, setFirstEp, userManga }) => {
const selectedChapters = chaptersData.find(
(c) => c.providerId === selectedProvider
);
- if (selectedChapters) {
- setSelectedChapter(selectedChapters);
- setFirstEp(selectedChapters);
- }
setChapters(selectedChapters?.chapters || []);
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedProvider, chaptersData]);
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [data.id]);
+
// Get current posts
const indexOfLastChapter = currentPage * chaptersPerPage;
const indexOfFirstChapter = indexOfLastChapter - chaptersPerPage;
@@ -31,24 +36,6 @@ const ChapterSelector = ({ chaptersData, data, setFirstEp, userManga }) => {
indexOfLastChapter
);
- // Change page
- const paginate = (pageNumber) => setCurrentPage(pageNumber);
- const nextPage = () => setCurrentPage((prev) => prev + 1);
- const prevPage = () => setCurrentPage((prev) => prev - 1);
-
- function saveManga() {
- localStorage.setItem(
- "manga",
- JSON.stringify({ manga: selectedChapter, data: data })
- );
- setCookie(null, "manga", data.id, {
- maxAge: 24 * 60 * 60,
- path: "/",
- });
- }
-
- // console.log(selectedChapter);
-
// Create page numbers
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(chapters.length / chaptersPerPage); i++) {
@@ -59,7 +46,7 @@ const ChapterSelector = ({ chaptersData, data, setFirstEp, userManga }) => {
const getDisplayedPageNumbers = (currentPage, totalPages, margin) => {
const pageRange = [...Array(totalPages).keys()].map((i) => i + 1);
- if (totalPages <= 10) {
+ if (totalPages <= 5) {
return pageRange;
}
@@ -83,104 +70,147 @@ const ChapterSelector = ({ chaptersData, data, setFirstEp, userManga }) => {
const displayedPageNumbers = getDisplayedPageNumbers(
currentPage,
pageNumbers.length,
- 9
+ 3
);
- // console.log(currentChapters);
+ useEffect(() => {
+ if (chapters) {
+ const getEpi = data?.nextAiringEpisode
+ ? chapters[data?.mediaListEntry?.progress]
+ : chapters[0];
+ if (getEpi) {
+ const watchUrl = `/en/manga/read/${selectedProvider}?id=${mangaId}&chapterId=${encodeURIComponent(
+ getEpi.id
+ )}&anilist=${data.id}&num=${getEpi.number}`;
+ setWatch(watchUrl);
+ } else {
+ setWatch(null);
+ }
+ }
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [chapters]);
return (
- <div className="flex flex-col items-center z-40">
- <div className="flex flex-col w-full">
- <label htmlFor="provider" className="text-sm md:text-base font-medium">
- Select a Provider
- </label>
- <div className="relative w-full">
+ <div className="flex flex-col gap-2 px-3">
+ <div className="flex justify-between">
+ <h1 className="text-[20px] lg:text-2xl font-bold font-karla">
+ Chapters
+ </h1>
+ <div className="relative flex gap-2 items-center group">
<select
id="provider"
- className="w-full text-xs md:text-base cursor-pointer mt-2 p-2 focus:outline-none rounded-md appearance-none bg-secondary"
+ className="flex items-center text-sm gap-5 rounded-[3px] bg-secondary py-1 px-3 pr-8 font-karla appearance-none cursor-pointer outline-none focus: focus:ring-action group-hover: group-hover:ring-action"
value={selectedProvider}
onChange={(e) => setSelectedProvider(e.target.value)}
>
{/* <option value="">--Select a provider--</option> */}
{chaptersData.map((provider, index) => (
- <option key={index} value={provider.providerId}>
+ <option key={provider.providerId} value={provider.providerId}>
{provider.providerId}
</option>
))}
</select>
- <ChevronDownIcon className="absolute md:right-5 right-3 md:bottom-2 m-auto md:w-6 md:h-6 bottom-[0.5rem] h-4 w-4" />
+ <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
</div>
</div>
- <div className="mt-4 w-full py-5 flex justify-between gap-5">
- <button
- onClick={prevPage}
- disabled={currentPage === 1}
- className={`w-24 py-1 shrink-0 rounded-md font-karla ${
- currentPage === 1
- ? "bg-[#1D1D20] text-[#313135]"
- : `bg-secondary hover:bg-[#363639]`
- }`}
- >
- Previous
- </button>
- <div className="flex gap-5 overflow-x-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-thumb- w-[420px] lg:w-auto">
- {displayedPageNumbers.map((number, index) =>
- number === "..." ? (
- <span key={index + 2} className="w-10 py-1 text-center">
- ...
- </span>
- ) : (
+
+ <div className="flex flex-col items-center z-40">
+ <div className="mt-4 w-full">
+ {currentChapters.map((chapter, index) => {
+ const isRead = chapter.number <= data?.mediaListEntry?.progress;
+ return (
+ <Link
+ key={index}
+ href={`/en/manga/read/${selectedProvider}?id=${mangaId}&chapterId=${encodeURIComponent(
+ chapter.id
+ )}${data?.id?.length > 6 ? "" : `&anilist=${data.id}`}&num=${
+ chapter.number
+ }`}
+ className={`flex gap-3 py-4 hover:bg-secondary odd:bg-secondary/30 even:bg-primary`}
+ >
+ <div className="flex w-full">
+ <span className="shrink-0 px-4 text-center text-white/50">
+ {chapter.number}
+ </span>
+ <p
+ className={`w-full line-clamp-1 ${
+ isRead ? "text-[#5f5f5f]" : "text-white"
+ }
+ `}
+ >
+ {chapter.title || `Chapter ${chapter.number}`}
+ </p>
+ <p className="capitalize text-sm text-white/50 px-4">
+ {selectedProvider}
+ </p>
+ </div>
+ </Link>
+ );
+ })}
+ </div>
+
+ <div className="flex flex-col mt-5 md:flex-row w-full sm:items-center sm:justify-between">
+ <div className="flex-center">
+ <p className="text-sm text-txt">
+ Showing{" "}
+ <span className="font-medium">{indexOfFirstChapter + 1}</span> to{" "}
+ <span className="font-medium">
+ {indexOfLastChapter > chapters.length
+ ? chapters.length
+ : indexOfLastChapter}
+ </span>{" "}
+ of <span className="font-medium">{chapters.length}</span> chapters
+ </p>
+ </div>
+ <div className="flex-center">
+ <nav
+ className="isolate inline-flex space-x-1 rounded-md shadow-sm"
+ aria-label="Pagination"
+ >
<button
- key={number}
- onClick={() => paginate(number)}
- className={`w-10 shrink-0 py-1 rounded-md hover:bg-[#363639] ${
- number === currentPage ? "bg-[#363639]" : "bg-secondary"
+ onClick={() => setCurrentPage((prev) => prev - 1)}
+ disabled={currentPage === 1}
+ className={`relative inline-flex items-center rounded px-2 py-2 text-gray-400 hover:bg-secondary focus:z-20 focus:outline-offset-0 ${
+ currentPage === 1
+ ? "opacity-50 cursor-default pointer-events-none"
+ : ""
}`}
>
- {number}
+ <span className="sr-only">Previous</span>
+ <ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
</button>
- )
- )}
- </div>
- <button
- onClick={nextPage}
- disabled={currentPage === pageNumbers.length}
- className={`w-24 py-1 shrink-0 rounded-md font-karla ${
- currentPage === pageNumbers.length
- ? "bg-[#1D1D20] text-[#313135]"
- : `bg-secondary hover:bg-[#363639]`
- }`}
- >
- Next
- </button>
- </div>
- <div className="mt-4 w-full">
- {currentChapters.map((chapter, index) => {
- const isRead = chapter.number <= userManga?.progress;
- return (
- <div key={index} className="p-2 border-b hover:bg-[#232325]">
- <Link
- href={`/en/manga/read/${selectedProvider}?id=${
- data.id
- }&chapterId=${encodeURIComponent(chapter.id)}`}
- onClick={saveManga}
+ <div className="flex w-full gap-1 overflow-x-scroll scrollbar-thin scrollbar-thumb-image scrollbar-thumb-rounded">
+ {displayedPageNumbers.map((pageNumber, index) => (
+ <button
+ key={index}
+ onClick={() => setCurrentPage(pageNumber)}
+ disabled={pageNumber === "..."}
+ className={`relative rounded inline-flex items-center px-4 py-2 text-sm font-semibold text-txt hover:bg-secondary focus:z-20 focus:outline-offset-0 ${
+ currentPage === pageNumber
+ ? "z-10 bg-secondary rounded text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-none"
+ : ""
+ }`}
+ >
+ {pageNumber}
+ </button>
+ ))}
+ </div>
+ <button
+ onClick={() => setCurrentPage((prev) => prev + 1)}
+ disabled={currentPage === pageNumbers.length}
+ className={`relative inline-flex items-center rounded px-2 py-2 text-gray-400 hover:bg-secondary focus:z-20 focus:outline-offset-0 ${
+ currentPage === pageNumbers.length
+ ? "opacity-50 cursor-default"
+ : ""
+ }`}
>
- <h2
- className={`text-lg font-medium ${
- isRead ? "text-[#424245]" : ""
- }`}
- >
- {chapter.title}
- </h2>
- <p
- className={`text-[#59595d] ${isRead ? "text-[#313133]" : ""}`}
- >
- Updated At: {new Date(chapter.updatedAt).toLocaleString()}
- </p>
- </Link>
- </div>
- );
- })}
+ <span className="sr-only">Next</span>
+ <ChevronRightIcon className="h-5 w-5" aria-hidden="true" />
+ </button>
+ </nav>
+ </div>
+ </div>
</div>
</div>
);
diff --git a/components/manga/info/mobile/mobileButton.js b/components/manga/info/mobile/mobileButton.js
deleted file mode 100644
index 0016b59..0000000
--- a/components/manga/info/mobile/mobileButton.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import Link from "next/link";
-import AniList from "../../../media/aniList";
-import { BookOpenIcon } from "@heroicons/react/24/outline";
-
-export default function MobileButton({ info, firstEp, saveManga }) {
- return (
- <div className="md:hidden flex items-center gap-4 w-full pb-3">
- <button
- disabled={!firstEp}
- onClick={saveManga}
- className={`${
- !firstEp
- ? "pointer-events-none text-white/50 bg-secondary/50"
- : "bg-secondary text-white"
- } lg:w-full font-bold shadow-md shadow-secondary hover:bg-secondary/90 hover:text-white/50 rounded`}
- >
- <Link
- href={`/en/manga/read/${firstEp?.providerId}?id=${
- info.id
- }&chapterId=${encodeURIComponent(
- firstEp?.chapters[firstEp.chapters.length - 1].id
- )}`}
- className="flex items-center text-xs font-karla gap-2 h-[30px] px-2"
- >
- <h1>Read Now</h1>
- <BookOpenIcon className="w-4 h-4" />
- </Link>
- </button>
- <Link
- href={`https://anilist.co/manga/${info.id}`}
- className="flex-center rounded bg-secondary shadow-md shadow-secondary h-[30px] lg:px-4 px-2"
- >
- <div className="flex-center w-5 h-5">
- <AniList />
- </div>
- </Link>
- </div>
- );
-}
diff --git a/components/manga/info/mobile/topMobile.js b/components/manga/info/mobile/topMobile.js
deleted file mode 100644
index 2e6b23a..0000000
--- a/components/manga/info/mobile/topMobile.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Image from "next/image";
-
-export default function TopMobile({ info }) {
- return (
- <div className="md:hidden">
- <Image
- src={info.coverImage}
- width={500}
- height={500}
- alt="cover image"
- className="md:hidden absolute top-0 left-0 -translate-y-24 w-full h-[30rem] object-cover rounded-sm shadow-lg brightness-75"
- />
- <div className="absolute top-0 left-0 w-full -translate-y-24 h-[32rem] bg-gradient-to-t from-primary to-transparent from-50%"></div>
- </div>
- );
-}
diff --git a/components/manga/info/topSection.js b/components/manga/info/topSection.js
deleted file mode 100644
index 45d5f11..0000000
--- a/components/manga/info/topSection.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import Image from "next/image";
-import { BookOpenIcon } from "@heroicons/react/24/outline";
-import AniList from "../../media/aniList";
-import Link from "next/link";
-import TopMobile from "./mobile/topMobile";
-import MobileButton from "./mobile/mobileButton";
-
-export default function TopSection({ info, firstEp, setCookie }) {
- const slicedGenre = info.genres?.slice(0, 3);
-
- function saveManga() {
- localStorage.setItem(
- "manga",
- JSON.stringify({ manga: firstEp, data: info })
- );
-
- setCookie(null, "manga", info.id, {
- maxAge: 24 * 60 * 60,
- path: "/",
- });
- }
-
- return (
- <div className="flex md:gap-5 w-[90%] xl:w-[70%] z-30">
- <TopMobile info={info} />
- <div className="hidden md:block w-[7rem] xs:w-[10rem] lg:w-[15rem] space-y-3 shrink-0 rounded-sm">
- <Image
- 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"
- />
-
- <div className="hidden md:flex items-center justify-between w-full lg:gap-5 pb-3">
- <button
- disabled={!firstEp}
- onClick={saveManga}
- className={`${
- !firstEp
- ? "pointer-events-none text-white/50 bg-tersier/50"
- : "bg-tersier text-white"
- } lg:w-full font-bold shadow-md shadow-[#0E0E0F] hover:bg-tersier/90 hover:text-white/50 rounded-md`}
- >
- <Link
- href={`/en/manga/read/${firstEp?.providerId}?id=${
- info.id
- }&chapterId=${encodeURIComponent(
- firstEp?.chapters[firstEp.chapters.length - 1].id
- )}`}
- className="flex items-center lg:justify-center text-sm lg:text-base font-karla gap-2 h-[35px] lg:h-[40px] px-2"
- >
- <h1>Read Now</h1>
- <BookOpenIcon className="w-5 h-5" />
- </Link>
- </button>
- <Link
- href={`https://anilist.co/manga/${info.id}`}
- className="flex-center rounded-md bg-tersier shadow-md shadow-[#0E0E0F] h-[35px] lg:h-[40px] lg:px-4 px-2"
- >
- <div className="flex-center w-5 h-5">
- <AniList />
- </div>
- </Link>
- </div>
- </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="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]">
- {slicedGenre &&
- slicedGenre.map((genre, index) => {
- return (
- <div key={index} className="flex">
- {genre}
- {index < slicedGenre?.length - 1 && (
- <span className="mx-2 text-sm text-[#747478]">•</span>
- )}
- </div>
- );
- })}
- </span>
- </div>
-
- <MobileButton info={info} firstEp={firstEp} saveManga={saveManga} />
-
- <div className="hidden md:block relative h-1/2">
- {/* <span className="font-semibold text-sm">Description</span> */}
- <div
- className={`relative group h-[8rem] lg:h-[12.5rem] text-sm lg:text-base overflow-y-scroll scrollbar-hide`}
- >
- <p
- dangerouslySetInnerHTML={{ __html: info.description }}
- className="pb-5 pt-2 leading-5"
- />
- </div>
- <div
- className={`absolute bottom-0 w-full bg-gradient-to-b from-transparent to-secondary to-50% h-[2rem]`}
- />
- </div>
- </div>
- </div>
- );
-}
diff --git a/components/manga/leftBar.js b/components/manga/leftBar.js
index 17acd55..5a98115 100644
--- a/components/manga/leftBar.js
+++ b/components/manga/leftBar.js
@@ -1,14 +1,23 @@
+import { getHeaders, getRandomId } from "@/utils/imageUtils";
import { ArrowLeftIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
-export function LeftBar({ data, page, info, currentId, setSeekPage }) {
+export function LeftBar({
+ data,
+ page,
+ info,
+ currentId,
+ setSeekPage,
+ number,
+ mediaId,
+ providerId,
+}) {
const router = useRouter();
function goBack() {
router.push(`/en/manga/${info.id}`);
}
- // console.log(info);
return (
<div className="hidden lg:block shrink-0 w-[16rem] h-screen overflow-y-auto scrollbar-none bg-secondary relative group">
<div className="grid">
@@ -37,23 +46,27 @@ export function LeftBar({ data, page, info, currentId, setSeekPage }) {
<h1 className="font-bold xl:text-lg">Chapters</h1>
<div className="px-2">
<div className="w-full text-sm xl:text-base px-1 h-[8rem] xl:h-[30vh] bg-[#161617] rounded-md overflow-auto scrollbar-thin scrollbar-thumb-[#363639] scrollbar-thumb-rounded-md hover:scrollbar-thumb-[#424245]">
- {data?.chapters?.map((x) => {
+ {data?.chapters?.map((x, index) => {
return (
<div
- key={x.id}
+ key={getRandomId()}
className={`${
x.id === currentId && "text-action"
} py-1 px-2 hover:bg-[#424245] rounded-sm`}
>
<Link
- href={`/en/manga/read/${data.providerId}?id=${
- info.id
- }&chapterId=${encodeURIComponent(x.id)}`}
+ href={`/en/manga/read/${
+ data.providerId
+ }?id=${mediaId}&chapterId=${encodeURIComponent(x.id)}${
+ info?.id?.length > 6 ? "" : `&anilist=${info?.id}`
+ }&num=${x.number}`}
className=""
>
<h1 className="line-clamp-1">
- <span className="font-bold">{x.number}.</span>{" "}
- {x.title}
+ <span className="font-bold">
+ {x.number || index + 1}.
+ </span>{" "}
+ {x.title || `Chapter ${x.number || index + 1}`}
</h1>
</Link>
</div>
@@ -69,28 +82,37 @@ export function LeftBar({ data, page, info, currentId, setSeekPage }) {
<div className="text-center w-full px-1 h-[30vh] bg-[#161617] rounded-md overflow-auto scrollbar-thin scrollbar-thumb-[#363639] scrollbar-thumb-rounded-md hover:scrollbar-thumb-[#424245]">
{Array.isArray(page) ? (
<div className="grid grid-cols-2 gap-5 py-4 px-2 place-items-center">
- {page?.map((x) => {
+ {page?.map((x, index) => {
return (
<div
- key={x.url}
+ key={getRandomId()}
className="hover:bg-[#424245] cursor-pointer rounded-sm w-full"
>
<div
className="flex flex-col items-center cursor-pointer"
- onClick={() => setSeekPage(x.index)}
+ onClick={() => setSeekPage(index)}
>
<Image
src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent(
x.url
- )}&headers=${encodeURIComponent(
- JSON.stringify({ Referer: x.headers.Referer })
- )}`}
+ )}${
+ x?.headers?.Referer
+ ? `&headers=${encodeURIComponent(
+ JSON.stringify(x?.headers)
+ )}`
+ : `&headers=${encodeURIComponent(
+ JSON.stringify(getHeaders(providerId))
+ )}`
+ }`}
+ // &headers=${encodeURIComponent(
+ // JSON.stringify({ Referer: x.headers.Referer })
+ // )}
alt="chapter image"
width={100}
height={200}
className="w-full h-[120px] object-contain scale-90"
/>
- <h1>Page {x.index + 1}</h1>
+ <h1>Page {index + 1}</h1>
</div>
</div>
);
@@ -98,7 +120,7 @@ export function LeftBar({ data, page, info, currentId, setSeekPage }) {
</div>
) : (
<div className="py-4">
- <p>{page.error || "No Pages."}</p>
+ <p>{page?.error || "No Pages."}</p>
</div>
)}
</div>
diff --git a/components/manga/mobile/bottomBar.js b/components/manga/mobile/bottomBar.js
index 6493dca..5b28de4 100644
--- a/components/manga/mobile/bottomBar.js
+++ b/components/manga/mobile/bottomBar.js
@@ -1,3 +1,4 @@
+import { getHeaders } from "@/utils/imageUtils";
import {
ChevronLeftIcon,
ChevronRightIcon,
@@ -14,12 +15,15 @@ export default function BottomBar({
nextChapter,
currentPage,
chapter,
- page,
+ data,
setSeekPage,
setIsOpen,
+ number,
+ mangadexId,
}) {
const [openPage, setOpenPage] = useState(false);
const router = useRouter();
+
return (
<div
className={`fixed lg:hidden flex flex-col gap-3 z-50 h-auto w-screen ${
@@ -39,7 +43,9 @@ export default function BottomBar({
router.push(
`/en/manga/read/${
chapter.providerId
- }?id=${id}&chapterId=${encodeURIComponent(prevChapter)}`
+ }?id=${mangadexId}&chapterId=${encodeURIComponent(
+ prevChapter.id
+ )}${id > 6 ? "" : `&anilist=${id}`}&num=${prevChapter.number}`
)
}
>
@@ -56,7 +62,9 @@ export default function BottomBar({
router.push(
`/en/manga/read/${
chapter.providerId
- }?id=${id}&chapterId=${encodeURIComponent(nextChapter)}`
+ }?id=${mangadexId}&chapterId=${encodeURIComponent(
+ nextChapter.id
+ )}${id > 6 ? "" : `&anilist=${id}`}&num=${nextChapter.number}`
)
}
>
@@ -82,13 +90,14 @@ export default function BottomBar({
<RectangleStackIcon className="w-5 h-5" />
</button>
</div>
- <span className="flex bg-secondary shadow-lg ring-1 ring-black ring-opacity-5 p-2 rounded-md">{`${currentPage}/${page.length}`}</span>
+ <span className="flex bg-secondary shadow-lg ring-1 ring-black ring-opacity-5 p-2 rounded-md">{`${currentPage}/${data?.length}`}</span>
</div>
{openPage && (
<div className="bg-secondary flex justify-center h-full w-screen py-2">
<div className="flex overflow-scroll">
- {Array.isArray(page) ? (
- page.map((x) => {
+ {Array.isArray(data) ? (
+ data.map((x, index) => {
+ const indx = index + 1;
return (
<div
key={x.url}
@@ -101,9 +110,18 @@ export default function BottomBar({
<Image
src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent(
x.url
- )}&headers=${encodeURIComponent(
- JSON.stringify({ Referer: x.headers.Referer })
- )}`}
+ )}${
+ x?.headers?.Referer
+ ? `&headers=${encodeURIComponent(
+ JSON.stringify(x?.headers)
+ )}`
+ : `&headers=${encodeURIComponent(
+ JSON.stringify(getHeaders(chapter.providerId))
+ )}`
+ }`}
+ // &headers=${encodeURIComponent(
+ // JSON.stringify({ Referer: x.headers.Referer })
+ // )}
alt="chapter image"
width={100}
height={200}
diff --git a/components/manga/mobile/hamburgerMenu.js b/components/manga/mobile/hamburgerMenu.js
deleted file mode 100644
index fcdbcce..0000000
--- a/components/manga/mobile/hamburgerMenu.js
+++ /dev/null
@@ -1,228 +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";
-
-export default function HamburgerMenu() {
- const { data: session } = 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");
- }
- }, []);
- return (
- <>
- {!isVisible && (
- <button
- onClick={handleShowClick}
- className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden"
- id="bars"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- className="h-[42px] w-[61.5px] text-[#8BA0B2] fill-orange-500"
- viewBox="0 0 20 20"
- fill="currentColor"
- >
- <path
- fillRule="evenodd"
- d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
- clipRule="evenodd"
- />
- </svg>
- </button>
- )}
-
- {/* Mobile Menu */}
- <div
- className={`transition-all duration-150 ${
- fade ? "opacity-100" : "opacity-0"
- } z-50`}
- >
- {isVisible && session && (
- <Link
- href={`/${lang}/profile/${session?.user?.name}`}
- className="fixed lg:hidden bottom-[100px] w-[60px] h-[60px] flex items-center justify-center right-[20px] rounded-full z-50 bg-[#17171f]"
- >
- <Image
- src={session?.user.image.large}
- alt="user avatar"
- height={500}
- width={500}
- className="object-cover w-[60px] h-[60px] rounded-full"
- />
- </Link>
- )}
- {isVisible && (
- <div className="fixed bottom-[30px] right-[20px] z-50 flex h-[51px] w-[300px] items-center justify-center gap-8 rounded-[8px] text-[11px] bg-[#17171f] shadow-lg lg:hidden">
- <div className="grid grid-cols-4 place-items-center gap-6">
- <button className="group flex flex-col items-center">
- <Link href={`/${lang}/`} className="">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="w-6 h-6 group-hover:stroke-action"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
- />
- </svg>
- </Link>
- <Link
- href={`/${lang}/`}
- className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
- >
- home
- </Link>
- </button>
- <button className="group flex flex-col items-center">
- <Link href={`/${lang}/about`}>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="w-6 h-6 group-hover:stroke-action"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
- />
- </svg>
- </Link>
- <Link
- href={`/${lang}/about`}
- className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
- >
- about
- </Link>
- </button>
- <button className="group flex gap-[1.5px] flex-col items-center ">
- <div>
- <Link href={`/${lang}/search/anime`}>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="w-6 h-6 group-hover:stroke-action"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
- />
- </svg>
- </Link>
- </div>
- <Link
- href={`/${lang}/search/anime`}
- className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
- >
- search
- </Link>
- </button>
- {session ? (
- <button
- onClick={() => signOut("AniListProvider")}
- className="group flex gap-[1.5px] flex-col items-center "
- >
- <div>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 96 960 960"
- className="group-hover:fill-action w-6 h-6 fill-txt"
- >
- <path d="M186.666 936q-27 0-46.833-19.833T120 869.334V282.666q0-27 19.833-46.833T186.666 216H474v66.666H186.666v586.668H474V936H186.666zm470.668-176.667l-47-48 102-102H370v-66.666h341.001l-102-102 46.999-48 184 184-182.666 182.666z"></path>
- </svg>
- </div>
- <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action">
- logout
- </h1>
- </button>
- ) : (
- <button
- onClick={() => signIn("AniListProvider")}
- className="group flex gap-[1.5px] flex-col items-center "
- >
- <div>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 96 960 960"
- className="group-hover:fill-action w-6 h-6 fill-txt mr-2"
- >
- <path d="M486 936v-66.666h287.334V282.666H486V216h287.334q27 0 46.833 19.833T840 282.666v586.668q0 27-19.833 46.833T773.334 936H486zm-78.666-176.667l-47-48 102-102H120v-66.666h341l-102-102 47-48 184 184-182.666 182.666z"></path>
- </svg>
- </div>
- <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action">
- login
- </h1>
- </button>
- )}
- </div>
- <button onClick={handleHideClick}>
- <svg
- width="20"
- height="21"
- className="fill-orange-500"
- viewBox="0 0 20 21"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- >
- <rect
- x="2.44043"
- y="0.941467"
- width="23.5842"
- height="3.45134"
- rx="1.72567"
- transform="rotate(45 2.44043 0.941467)"
- />
- <rect
- x="19.1172"
- y="3.38196"
- width="23.5842"
- height="3.45134"
- rx="1.72567"
- transform="rotate(135 19.1172 3.38196)"
- />
- </svg>
- </button>
- </div>
- )}
- </div>
- </>
- );
-}
diff --git a/components/manga/panels/firstPanel.js b/components/manga/panels/firstPanel.js
index f1ee859..596fa58 100644
--- a/components/manga/panels/firstPanel.js
+++ b/components/manga/panels/firstPanel.js
@@ -4,10 +4,13 @@ import {
ArrowsPointingInIcon,
ChevronLeftIcon,
ChevronRightIcon,
+ PlusIcon,
+ MinusIcon,
} from "@heroicons/react/24/outline";
import Image from "next/image";
import { useRouter } from "next/router";
import { useAniList } from "../../../lib/anilist/useAnilist";
+import { getHeaders, getRandomId } from "@/utils/imageUtils";
export default function FirstPanel({
aniId,
@@ -26,14 +29,20 @@ export default function FirstPanel({
mobileVisible,
setMobileVisible,
setCurrentPage,
+ number,
+ mangadexId,
}) {
const { markProgress } = useAniList(session);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const imageRefs = useRef([]);
const scrollContainerRef = useRef();
+ const [imageQuality, setImageQuality] = useState(80);
+
const router = useRouter();
+ // console.log({ chapter });
+
useEffect(() => {
const handleScroll = () => {
const scrollTop = scrollContainerRef.current.scrollTop;
@@ -53,13 +62,17 @@ export default function FirstPanel({
}
}
- if (index === data.length - 3 && !hasRun.current) {
+ if (index === data?.length - 3 && !hasRun.current) {
if (session) {
+ if (aniId?.length > 6) return;
const currentChapter = chapter.chapters?.find(
(x) => x.id === currentId
);
if (currentChapter) {
- markProgress(aniId, currentChapter.number);
+ const chapterNumber =
+ currentChapter.number ??
+ chapter.chapters.indexOf(currentChapter) + 1;
+ markProgress(aniId, chapterNumber);
console.log("marking progress");
}
}
@@ -82,8 +95,12 @@ export default function FirstPanel({
});
}
};
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, session, chapter]);
+ // console.log({ imageQuality });
+
useEffect(() => {
if (scrollContainerRef.current && seekPage !== currentImageIndex) {
const targetImageRef = imageRefs.current[seekPage];
@@ -119,19 +136,26 @@ export default function FirstPanel({
{data && Array.isArray(data) && data?.length > 0 ? (
data.map((i, index) => (
<div
- key={i.url}
+ key={getRandomId()}
className="w-screen lg:h-auto lg:w-full"
ref={(el) => (imageRefs.current[index] = el)}
>
<Image
src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent(
i.url
- )}&headers=${encodeURIComponent(
- JSON.stringify({ Referer: i.headers.Referer })
- )}`}
- alt={i.index}
+ )}${
+ i?.headers?.Referer
+ ? `&headers=${encodeURIComponent(
+ JSON.stringify(i?.headers)
+ )}`
+ : `&headers=${encodeURIComponent(
+ JSON.stringify(getHeaders(chapter.providerId))
+ )}`
+ }`}
+ alt={index}
width={500}
height={500}
+ quality={imageQuality}
onClick={() => setMobileVisible(!mobileVisible)}
className="w-screen lg:w-full h-auto bg-[#bbb]"
/>
@@ -145,6 +169,26 @@ export default function FirstPanel({
)}
</div>
<div className="absolute hidden lg:flex bottom-5 left-5 gap-5">
+ {/* <button
+ type="button"
+ disabled={imageQuality >= 100}
+ onClick={() => {
+ setImageQuality((prev) => (prev <= 100 ? prev + 10 : prev));
+ }}
+ className="flex-center p-2 bg-secondary"
+ >
+ <PlusIcon className="w-5 h-5" />
+ </button>
+ <button
+ type="button"
+ disabled={imageQuality <= 10}
+ onClick={() => {
+ setImageQuality((prev) => (prev >= 10 ? prev - 10 : prev));
+ }}
+ className="flex-center p-2 bg-secondary"
+ >
+ <MinusIcon className="w-5 h-5" />
+ </button> */}
<span className="flex bg-secondary p-2 rounded-sm">
{visible ? (
<button type="button" onClick={() => setVisible(!visible)}>
@@ -168,7 +212,11 @@ export default function FirstPanel({
router.push(
`/en/manga/read/${
chapter.providerId
- }?id=${aniId}&chapterId=${encodeURIComponent(prevChapter)}`
+ }?id=${mangadexId}&chapterId=${encodeURIComponent(
+ prevChapter?.id
+ )}${aniId?.length > 6 ? "" : `&anilist=${aniId}`}&num=${
+ prevChapter?.number
+ }`
)
}
>
@@ -185,7 +233,11 @@ export default function FirstPanel({
router.push(
`/en/manga/read/${
chapter.providerId
- }?id=${aniId}&chapterId=${encodeURIComponent(nextChapter)}`
+ }?id=${mangadexId}&chapterId=${encodeURIComponent(
+ nextChapter?.id
+ )}${aniId?.length > 6 ? "" : `&anilist=${aniId}`}&num=${
+ nextChapter?.number
+ }`
)
}
>
@@ -195,7 +247,7 @@ export default function FirstPanel({
</div>
<span className="hidden lg:flex bg-secondary p-2 rounded-sm absolute bottom-5 right-5">{`Page ${
currentImageIndex + 1
- }/${data.length}`}</span>
+ }/${data?.length}`}</span>
</section>
);
}
diff --git a/components/manga/panels/secondPanel.js b/components/manga/panels/secondPanel.js
index 9323822..fa158b2 100644
--- a/components/manga/panels/secondPanel.js
+++ b/components/manga/panels/secondPanel.js
@@ -5,9 +5,11 @@ import {
ArrowsPointingInIcon,
} from "@heroicons/react/24/outline";
import { useAniList } from "../../../lib/anilist/useAnilist";
+import { getHeaders } from "@/utils/imageUtils";
export default function SecondPanel({
aniId,
+ chapterData,
data,
hasRun,
currentChapter,
@@ -17,6 +19,7 @@ export default function SecondPanel({
visible,
setVisible,
session,
+ providerId,
}) {
const [index, setIndex] = useState(0);
const [image, setImage] = useState(null);
@@ -26,6 +29,7 @@ export default function SecondPanel({
useEffect(() => {
setIndex(0);
setSeekPage(0);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, currentId]);
const seekToIndex = (newIndex) => {
@@ -41,6 +45,7 @@ export default function SecondPanel({
useEffect(() => {
seekToIndex(seekPage);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [seekPage]);
useEffect(() => {
@@ -63,13 +68,14 @@ export default function SecondPanel({
}
if (index + 1 >= image.length - 4 && !hasRun.current) {
- let chapterNumber = currentChapter?.number;
- if (chapterNumber % 1 !== 0) {
- // If it's a decimal, round it
- chapterNumber = Math.round(chapterNumber);
- }
+ const current = chapterData.chapters?.find(
+ (x) => x.id === currentChapter.id
+ );
+ const chapterNumber = chapterData.chapters.indexOf(current) + 1;
- markProgress(aniId, chapterNumber);
+ if (chapterNumber) {
+ markProgress(aniId, chapterNumber);
+ }
hasRun.current = true;
}
}
@@ -80,6 +86,7 @@ export default function SecondPanel({
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [index, image]);
const handleNext = () => {
@@ -90,10 +97,13 @@ export default function SecondPanel({
if (index + 1 >= image.length - 4 && !hasRun.current) {
console.log("marking progress");
- let chapterNumber = currentChapter?.number;
- if (chapterNumber % 1 !== 0) {
- // If it's a decimal, round it
- chapterNumber = Math.round(chapterNumber);
+ const current = chapterData.chapters?.find(
+ (x) => x.id === currentChapter.id
+ );
+ const chapterNumber = chapterData.chapters.indexOf(current) + 1;
+
+ if (chapterNumber) {
+ markProgress(aniId, chapterNumber);
}
markProgress(aniId, chapterNumber);
@@ -107,6 +117,7 @@ export default function SecondPanel({
setSeekPage(index - 2);
}
};
+
return (
<div className="flex-grow h-screen">
<div className="flex items-center w-full relative group">
@@ -127,11 +138,17 @@ export default function SecondPanel({
className="w-1/2 h-screen object-contain"
src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent(
image[image.length - index - 2]?.url
- )}&headers=${encodeURIComponent(
- JSON.stringify({
- Referer: image[image.length - index - 2]?.headers.Referer,
- })
- )}`}
+ )}${
+ image[image.length - index - 2]?.headers?.Referer
+ ? `&headers=${encodeURIComponent(
+ JSON.stringify(
+ image[image.length - index - 2]?.headers
+ )
+ )}`
+ : `&headers=${encodeURIComponent(
+ JSON.stringify(getHeaders(providerId))
+ )}`
+ }`}
alt="Manga Page"
/>
)}
@@ -142,11 +159,15 @@ export default function SecondPanel({
className="w-1/2 h-screen object-contain"
src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent(
image[image.length - index - 1]?.url
- )}&headers=${encodeURIComponent(
- JSON.stringify({
- Referer: image[image.length - index - 1]?.headers.Referer,
- })
- )}`}
+ )}${
+ image[image.length - index - 1]?.headers?.Referer
+ ? `&headers=${encodeURIComponent(
+ JSON.stringify(image[image.length - index - 1]?.headers)
+ )}`
+ : `&headers=${encodeURIComponent(
+ JSON.stringify(getHeaders(providerId))
+ )}`
+ }`}
alt="Manga Page"
/>
</div>
diff --git a/components/manga/panels/thirdPanel.js b/components/manga/panels/thirdPanel.js
index d402f07..f13b49d 100644
--- a/components/manga/panels/thirdPanel.js
+++ b/components/manga/panels/thirdPanel.js
@@ -5,10 +5,12 @@ import {
ArrowsPointingInIcon,
} from "@heroicons/react/24/outline";
import { useAniList } from "../../../lib/anilist/useAnilist";
+import { getHeaders } from "@/utils/imageUtils";
export default function ThirdPanel({
aniId,
data,
+ chapterData,
hasRun,
currentId,
currentChapter,
@@ -20,6 +22,7 @@ export default function ThirdPanel({
scaleImg,
setMobileVisible,
mobileVisible,
+ providerId,
}) {
const [index, setIndex] = useState(0);
const [image, setImage] = useState(null);
@@ -28,6 +31,7 @@ export default function ThirdPanel({
useEffect(() => {
setIndex(0);
setSeekPage(0);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, currentId]);
const seekToIndex = (newIndex) => {
@@ -39,6 +43,7 @@ export default function ThirdPanel({
useEffect(() => {
seekToIndex(seekPage);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [seekPage]);
useEffect(() => {
@@ -60,13 +65,14 @@ export default function ThirdPanel({
setSeekPage(index + 1);
}
if (index + 1 >= image.length - 2 && !hasRun.current) {
- let chapterNumber = currentChapter?.number;
- if (chapterNumber % 1 !== 0) {
- // If it's a decimal, round it
- chapterNumber = Math.round(chapterNumber);
- }
+ const current = chapterData.chapters?.find(
+ (x) => x.id === currentChapter.id
+ );
+ const chapterNumber = chapterData.chapters.indexOf(current) + 1;
- markProgress(aniId, chapterNumber);
+ if (chapterNumber) {
+ markProgress(aniId, chapterNumber);
+ }
hasRun.current = true;
}
}
@@ -77,6 +83,8 @@ export default function ThirdPanel({
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [index, image]);
const handleNext = () => {
@@ -85,13 +93,15 @@ export default function ThirdPanel({
setSeekPage(index + 1);
}
if (index + 1 >= image.length - 2 && !hasRun.current) {
- let chapterNumber = currentChapter?.number;
- if (chapterNumber % 1 !== 0) {
- // If it's a decimal, round it
- chapterNumber = Math.round(chapterNumber);
+ const current = chapterData.chapters?.find(
+ (x) => x.id === currentChapter.id
+ );
+ const chapterNumber = chapterData.chapters.indexOf(current) + 1;
+
+ if (chapterNumber) {
+ markProgress(aniId, chapterNumber);
}
- markProgress(aniId, chapterNumber);
hasRun.current = true;
}
};
@@ -119,11 +129,15 @@ export default function ThirdPanel({
onClick={() => setMobileVisible(!mobileVisible)}
src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent(
image[image.length - index - 1]?.url
- )}&headers=${encodeURIComponent(
- JSON.stringify({
- Referer: image[image.length - index - 1]?.headers.Referer,
- })
- )}`}
+ )}${
+ image[image.length - index - 1]?.headers?.Referer
+ ? `&headers=${encodeURIComponent(
+ JSON.stringify(image[image.length - index - 1]?.headers)
+ )}`
+ : `&headers=${encodeURIComponent(
+ JSON.stringify(getHeaders(providerId))
+ )}`
+ }`}
alt="Manga Page"
style={{
transform: `scale(${scaleImg})`,
diff --git a/components/manga/rightBar.js b/components/manga/rightBar.js
index 82d577d..9672fc4 100644
--- a/components/manga/rightBar.js
+++ b/components/manga/rightBar.js
@@ -4,16 +4,15 @@ import {
} from "@heroicons/react/24/outline";
import { useEffect, useState } from "react";
import { useAniList } from "../../lib/anilist/useAnilist";
-import { toast } from "react-toastify";
import AniList from "../media/aniList";
import { signIn } from "next-auth/react";
+import { toast } from "sonner";
export default function RightBar({
id,
hasRun,
session,
data,
- error,
currentChapter,
paddingX,
setPaddingX,
@@ -47,19 +46,13 @@ export default function RightBar({
markProgress(id, progress, status, volumeProgress);
hasRun.current = true;
} else {
- toast.error("Progress must be a whole number!", {
- position: "bottom-right",
- autoClose: 5000,
- hideProgressBar: true,
- closeOnClick: false,
- pauseOnHover: true,
- draggable: true,
- theme: "colored",
- });
+ toast.error("Progress must be a whole number!");
}
}
};
+ // console.log({ id });
+
const changeMode = (e) => {
setLayout(Number(e.target.value));
// console.log(e.target.value);
@@ -129,63 +122,72 @@ export default function RightBar({
</button>
</div>
</div>
+ {/* <div className="flex flex-col gap-3 w-full">
+ <h1 className="font-karla font-bold xl:text-lg">Set Quality</h1>
+ </div> */}
<div className="flex flex-col gap-3 w-full">
<h1 className="font-karla font-bold xl:text-lg">Tracking</h1>
{session ? (
- <div className="flex flex-col gap-2">
- <div className="space-y-1">
- <label className="font-karla font-semibold text-gray-500 text-xs">
- Status
- </label>
- <div className="relative">
- <select
- onChange={(e) => setStatus(e.target.value)}
- className="w-full px-2 py-1 font-karla rounded-md bg-[#161617] appearance-none text-sm"
- >
- <option value="CURRENT">Reading</option>
- <option value="PLANNING">Plan to Read</option>
- <option value="COMPLETED">Completed</option>
- <option value="REPEATING">Rereading</option>
- <option value="PAUSED">Paused</option>
- <option value="DROPPED">Dropped</option>
- </select>
- <ChevronDownIcon className="w-5 h-5 text-white absolute inset-0 my-auto mx-52" />
+ id?.length > 6 ? (
+ <p className="flex-center w-full py-2 font-karla">
+ Not available on AniList
+ </p>
+ ) : (
+ <div className="flex flex-col gap-2">
+ <div className="space-y-1">
+ <label className="font-karla font-semibold text-gray-500 text-xs">
+ Status
+ </label>
+ <div className="relative">
+ <select
+ onChange={(e) => setStatus(e.target.value)}
+ className="w-full px-2 py-1 font-karla rounded-md bg-[#161617] appearance-none text-sm"
+ >
+ <option value="CURRENT">Reading</option>
+ <option value="PLANNING">Plan to Read</option>
+ <option value="COMPLETED">Completed</option>
+ <option value="REPEATING">Rereading</option>
+ <option value="PAUSED">Paused</option>
+ <option value="DROPPED">Dropped</option>
+ </select>
+ <ChevronDownIcon className="w-5 h-5 text-white absolute inset-0 my-auto mx-52" />
+ </div>
</div>
+ <div className="space-y-1">
+ <label className="font-karla font-semibold text-gray-500 text-xs">
+ Chapter Progress
+ </label>
+ <input
+ id="chapter-progress"
+ type="number"
+ placeholder="0"
+ min={0}
+ value={progress}
+ onChange={(e) => setProgress(e.target.value)}
+ className="w-full px-2 py-1 rounded-md bg-[#161617] text-sm"
+ />
+ </div>
+ <div className="space-y-1">
+ <label className="font-karla font-semibold text-gray-500 text-xs">
+ Volume Progress
+ </label>
+ <input
+ type="number"
+ placeholder="0"
+ min={0}
+ onChange={(e) => setVolumeProgress(e.target.value)}
+ className="w-full px-2 py-1 rounded-md bg-[#161617] text-sm"
+ />
+ </div>
+ <button
+ type="button"
+ onClick={saveProgress}
+ className="w-full bg-[#424245] py-1 my-5 rounded-md text-white text-sm xl:text-base shadow-md font-karla font-semibold"
+ >
+ Save Progress
+ </button>
</div>
- <div className="space-y-1">
- <label className="font-karla font-semibold text-gray-500 text-xs">
- Chapter Progress
- </label>
- <input
- id="chapter-progress"
- type="number"
- placeholder="0"
- min={0}
- value={progress}
- onChange={(e) => setProgress(e.target.value)}
- className="w-full px-2 py-1 rounded-md bg-[#161617] text-sm"
- />
- </div>
- <div className="space-y-1">
- <label className="font-karla font-semibold text-gray-500 text-xs">
- Volume Progress
- </label>
- <input
- type="number"
- placeholder="0"
- min={0}
- onChange={(e) => setVolumeProgress(e.target.value)}
- className="w-full px-2 py-1 rounded-md bg-[#161617] text-sm"
- />
- </div>
- <button
- type="button"
- onClick={saveProgress}
- className="w-full bg-[#424245] py-1 my-5 rounded-md text-white text-sm xl:text-base shadow-md font-karla font-semibold"
- >
- Save Progress
- </button>
- </div>
+ )
) : (
<button
type="button"
diff --git a/components/search/searchByImage.js b/components/search/searchByImage.js
new file mode 100644
index 0000000..f95c2ad
--- /dev/null
+++ b/components/search/searchByImage.js
@@ -0,0 +1,119 @@
+import { PhotoIcon } from "@heroicons/react/24/outline";
+import { useRouter } from "next/router";
+import React, { useEffect } from "react";
+import { toast } from "sonner";
+
+export default function SearchByImage({
+ searchPalette = false,
+ setIsOpen,
+ setData,
+ setMedia,
+}) {
+ const router = useRouter();
+
+ async function findImage(formData) {
+ const response = new Promise((resolve, reject) => {
+ fetch("https://api.trace.moe/search?anilistInfo", {
+ method: "POST",
+ body: formData,
+ })
+ .then((resp) => {
+ resolve(resp.json());
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ });
+
+ toast.promise(response, {
+ loading: "Finding episodes...",
+ success: `Episodes found!`,
+ error: "Error",
+ });
+
+ response
+ .then((data) => {
+ if (data?.result?.length > 0) {
+ const id = data.result[0].anilist.id;
+ const datas = data.result.filter((i) => i.anilist.isAdult === false);
+ if (setData) setData(datas);
+ if (searchPalette) router.push(`/en/anime/${id}`);
+ if (setIsOpen) setIsOpen(false);
+ if (setMedia) setMedia();
+ }
+ })
+ .catch((error) => {
+ console.error("Error:", error);
+ });
+ }
+
+ const handleImageSelect = async (e) => {
+ const selectedImage = e.target.files[0];
+
+ if (selectedImage) {
+ const formData = new FormData();
+ formData.append("image", selectedImage);
+
+ try {
+ await findImage(formData);
+ } catch (error) {
+ console.error("An error occurred:", error);
+ }
+ }
+ };
+
+ useEffect(() => {
+ // Add a global event listener for the paste event
+ const handlePaste = async (e) => {
+ e.preventDefault();
+
+ const items = e.clipboardData.items;
+
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].type.indexOf("image") !== -1) {
+ const blob = items[i].getAsFile();
+
+ // Create a FormData object and append the pasted image
+ const formData = new FormData();
+ formData.append("image", blob);
+
+ try {
+ // Send the pasted image to your API for processing
+ await findImage(formData);
+ } catch (error) {
+ console.error("An error occurred:", error);
+ }
+ break; // Stop after finding the first image
+ }
+ }
+ };
+
+ // Add the event listener to the document
+ document.addEventListener("paste", handlePaste);
+
+ // Clean up the event listener when the component unmounts
+ return () => {
+ document.removeEventListener("paste", handlePaste);
+ };
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+ <div>
+ <label
+ className={`${
+ searchPalette ? "w-9 h-9" : "py-2 px-2"
+ } bg-secondary rounded flex justify-center items-center cursor-pointer hover:bg-opacity-75 transition-all duration-100 group`}
+ >
+ <PhotoIcon className="w-6 h-6" />
+ <input
+ type="file"
+ name="image"
+ onChange={handleImageSelect}
+ className="hidden"
+ />
+ </label>
+ </div>
+ );
+}
diff --git a/components/searchPalette.js b/components/searchPalette.js
index 38a0bc0..10b9003 100644
--- a/components/searchPalette.js
+++ b/components/searchPalette.js
@@ -1,4 +1,4 @@
-import { Fragment, useEffect, useState } from "react";
+import { Fragment, useEffect, useRef, useState } from "react";
import { Combobox, Dialog, Menu, Transition } from "@headlessui/react";
import useDebounce from "../lib/hooks/useDebounce";
import Image from "next/image";
@@ -8,6 +8,7 @@ import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
import { BookOpenIcon, PlayIcon } from "@heroicons/react/20/solid";
import { useAniList } from "../lib/anilist/useAnilist";
import { getFormat } from "../utils/getFormat";
+import SearchByImage from "./search/searchByImage";
export default function SearchPalette() {
const { isOpen, setIsOpen } = useSearch();
@@ -21,6 +22,7 @@ export default function SearchPalette() {
const [nextPage, setNextPage] = useState(false);
+ let focusInput = useRef(null);
const router = useRouter();
function closeModal() {
@@ -44,6 +46,7 @@ export default function SearchPalette() {
useEffect(() => {
advance();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [debounceSearch, type]);
useEffect(() => {
@@ -62,11 +65,17 @@ export default function SearchPalette() {
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Transition appear show={isOpen} as={Fragment}>
- <Dialog as="div" className="relative z-[6969]" onClose={closeModal}>
+ <Dialog
+ as="div"
+ className="relative z-[6969]"
+ initialFocus={focusInput}
+ onClose={closeModal}
+ >
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@@ -112,13 +121,13 @@ export default function SearchPalette() {
<span>S</span>
</div>
</div>
- <div>
+ <div className="flex gap-1 items-center">
<Menu
as="div"
className="relative inline-block text-left"
>
<div>
- <Menu.Button className="capitalize bg-secondary inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-medium text-white hover:bg-opacity-80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
+ <Menu.Button className="capitalize bg-secondary inline-flex w-full justify-center rounded px-3 py-2 text-sm font-medium text-white hover:bg-opacity-80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
{type.toLowerCase()}
<ChevronDownIcon
className="ml-2 -mr-1 h-5 w-5 text-violet-200 hover:text-violet-100"
@@ -171,10 +180,15 @@ export default function SearchPalette() {
</Menu.Items>
</Transition>
</Menu>
+ <SearchByImage
+ searchPalette={true}
+ setIsOpen={setIsOpen}
+ />
</div>
</div>
<div className="flex items-center text-base font-medium rounded bg-secondary">
<Combobox.Input
+ ref={focusInput}
className="p-5 text-white w-full bg-transparent border-0 outline-none"
placeholder="Search something..."
onChange={(event) => setQuery(event.target.value)}
diff --git a/components/secret.js b/components/secret.js
new file mode 100644
index 0000000..782fcf5
--- /dev/null
+++ b/components/secret.js
@@ -0,0 +1,36 @@
+import { useEffect, useState } from "react";
+
+export default function SecretPage({ cheatCode, onCheatCodeEntered }) {
+ const [typedCode, setTypedCode] = useState("");
+ const [timer, setTimer] = useState(null);
+
+ const handleKeyPress = (e) => {
+ const newTypedCode = typedCode + e.key;
+
+ if (newTypedCode === cheatCode) {
+ onCheatCodeEntered();
+ setTypedCode("");
+ } else {
+ setTypedCode(newTypedCode);
+
+ // Reset the timer if the user stops typing for 2 seconds
+ clearTimeout(timer);
+ const newTimer = setTimeout(() => {
+ setTypedCode("");
+ }, 2000);
+ setTimer(newTimer);
+ }
+ };
+
+ useEffect(() => {
+ window.addEventListener("keydown", handleKeyPress);
+
+ return () => {
+ window.removeEventListener("keydown", handleKeyPress);
+ };
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [typedCode]);
+
+ return;
+}
diff --git a/components/shared/NavBar.js b/components/shared/NavBar.js
index 7bbd617..034a06b 100644
--- a/components/shared/NavBar.js
+++ b/components/shared/NavBar.js
@@ -56,7 +56,7 @@ export function NewNavbar({
shrink ? "py-1" : `${paddingY}`
}`
: `${paddingY}`
- } transition-all duration-200 ease-linear`}
+ } transition-all duration-200 ease-linear`}
>
<div
className={`flex items-center justify-between mx-auto ${
@@ -83,6 +83,7 @@ export function NewNavbar({
>
<ArrowLeftIcon className="w-full h-full" />
</button>
+
<span
className={`font-inter font-semibold w-[50%] line-clamp-1 select-none ${
scrollPosition?.y >= scrollP + 80
@@ -196,7 +197,7 @@ export function NewNavbar({
// title={sessions ? "Go to Profile" : "Login With AniList"}
> */}
{session ? (
- <div className="w-7 h-7 relative flex flex-col items-center group">
+ <div className="w-7 h-7 relative flex flex-col items-center group shrink-0">
<button
type="button"
onClick={() =>
@@ -233,7 +234,7 @@ export function NewNavbar({
type="button"
onClick={() => signIn("AniListProvider")}
title="Login With AniList"
- className="w-7 h-7 bg-white/30 rounded-full overflow-hidden"
+ className="w-7 h-7 bg-white/30 rounded-full overflow-hidden shrink-0"
>
<UserIcon className="w-full h-full translate-y-1" />
</button>
diff --git a/components/shared/bugReport.js b/components/shared/bugReport.js
index 9b99016..f6bd9f1 100644
--- a/components/shared/bugReport.js
+++ b/components/shared/bugReport.js
@@ -1,7 +1,7 @@
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";
+import { toast } from "sonner";
const severityOptions = [
{ id: 1, name: "Low" },
@@ -42,17 +42,11 @@ const BugReportForm = ({ isOpen, setIsOpen }) => {
});
const json = await res.json();
- toast.success(json.message, {
- hideProgressBar: true,
- theme: "colored",
- });
+ toast.success(json.message);
closeModal();
} catch (err) {
console.log(err);
- toast.error("Something went wrong: " + err.message, {
- hideProgressBar: true,
- theme: "colored",
- });
+ toast.error("Something went wrong: " + err.message);
}
};
diff --git a/components/shared/footer.js b/components/shared/footer.js
index 91af5a8..0e19f13 100644
--- a/components/shared/footer.js
+++ b/components/shared/footer.js
@@ -28,6 +28,8 @@ function Footer() {
setLang("id");
setChecked(true);
}
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
function switchLang() {
diff --git a/components/watch/player/artplayer.js b/components/watch/player/artplayer.js
index 4ae8aa1..666c103 100644
--- a/components/watch/player/artplayer.js
+++ b/components/watch/player/artplayer.js
@@ -46,7 +46,7 @@ export default function NewPlayer({
customType: {
m3u8: playM3u8,
},
- ...(provider === "zoro" && {
+ ...(subtitles?.length > 0 && {
subtitle: {
url: `${defSub}`,
// type: "vtt",
@@ -131,7 +131,7 @@ export default function NewPlayer({
return item.html;
},
},
- provider === "zoro" && {
+ subtitles?.length > 0 && {
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,
@@ -261,7 +261,7 @@ export default function NewPlayer({
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>',
+ html: '<i 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></i>',
click: function (...args) {
setPlayerState((prev) => ({
...prev,
@@ -379,6 +379,8 @@ export default function NewPlayer({
art.destroy(false);
}
};
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div ref={artRef} {...rest}></div>;
diff --git a/components/watch/player/component/controls/subtitle.js b/components/watch/player/component/controls/subtitle.js
deleted file mode 100644
index 02075f7..0000000
--- a/components/watch/player/component/controls/subtitle.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { useState } from "react";
-
-export default function getSubtitles() {}
diff --git a/components/watch/player/playerComponent.js b/components/watch/player/playerComponent.js
index 37c5810..665919b 100644
--- a/components/watch/player/playerComponent.js
+++ b/components/watch/player/playerComponent.js
@@ -4,6 +4,7 @@ import { icons } from "./component/overlay";
import { useWatchProvider } from "@/lib/context/watchPageProvider";
import { useRouter } from "next/router";
import { useAniList } from "@/lib/anilist/useAnilist";
+import Loading from "@/components/shared/loading";
export function calculateAspectRatio(width, height) {
const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
@@ -74,20 +75,18 @@ export default function PlayerComponent({
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 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);
- }
+ const defSize = size?.find((i) => i?.default === true);
+ setDefSize(defSize);
+ setSubSize(size);
async function compiler() {
try {
@@ -114,19 +113,26 @@ export default function PlayerComponent({
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 subtitle = data?.subtitles
+ ?.filter(
+ (subtitle) =>
+ subtitle.lang !== "Thumbnails" && subtitle.lang !== "thumbnails"
+ )
+ ?.map((subtitle) => {
+ const isEnglish =
+ subtitle.lang === "English" ||
+ subtitle.lang === "English / English (US)";
+ return {
+ ...(isEnglish && { default: true }),
+ url: subtitle.url,
+ html: `${subtitle.lang}`,
+ };
+ });
- const defSub = data?.subtitles.find((i) => i.lang === "English");
+ if (subtitle) {
+ const defSub = data?.subtitles.find(
+ (i) => i.lang === "English" || i.lang === "English / English (US)"
+ );
setDefSub(defSub?.url);
@@ -162,6 +168,8 @@ export default function PlayerComponent({
setSubtitle([]);
setLoading(true);
};
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [provider, data]);
/**
@@ -171,6 +179,17 @@ export default function PlayerComponent({
art.on("ready", () => {
const autoplay = localStorage.getItem("autoplay_video") || false;
+ // check media queries for mobile devices
+ const isMobile = window.matchMedia("(max-width: 768px)").matches;
+
+ // console.log(art.fullscreen);
+
+ if (isMobile) {
+ art.controls.remove("theater-button");
+ // art.controls.remove("fast-rewind");
+ // art.controls.remove("fast-forward");
+ }
+
if (autoplay === "true" || autoplay === true) {
if (playerState.currentTime === 0) {
art.play();
@@ -465,10 +484,13 @@ export default function PlayerComponent({
style={{ aspectRatio: aspectRatio }}
>
<div className="flex-center w-full h-full">
+ {!data?.error && !url && (
+ <div className="flex-center w-full h-full">
+ <Loading />
+ </div>
+ )}
{!error ? (
- !loading &&
- track &&
- url && (
+ !loading && track && url && !data?.error ? (
<NewPlayer
playerRef={playerRef}
res={resolution}
@@ -486,6 +508,12 @@ export default function PlayerComponent({
height: "100%",
}}
/>
+ ) : (
+ <p className="text-center">
+ {data?.status === 404 && "Not Found"}
+ <br />
+ {data?.error}
+ </p>
)
) : (
<p className="text-center">
diff --git a/components/watch/player/utils/getZoroSource.js b/components/watch/player/utils/getZoroSource.js
deleted file mode 100644
index e69de29..0000000
--- a/components/watch/player/utils/getZoroSource.js
+++ /dev/null
diff --git a/components/watch/secondary/episodeLists.js b/components/watch/secondary/episodeLists.js
index 41f1a76..485b43e 100644
--- a/components/watch/secondary/episodeLists.js
+++ b/components/watch/secondary/episodeLists.js
@@ -1,6 +1,8 @@
import Skeleton from "react-loading-skeleton";
import Image from "next/image";
import Link from "next/link";
+import { ChevronDownIcon } from "@heroicons/react/24/outline";
+import { useRouter } from "next/router";
export default function EpisodeLists({
info,
@@ -9,13 +11,56 @@ export default function EpisodeLists({
watchId,
episode,
artStorage,
+ track,
dub,
}) {
const progress = info.mediaListEntry?.progress;
+ const router = useRouter();
+
return (
<div className="w-screen lg:max-w-sm xl:max-w-lg">
- <h1 className="text-xl font-karla pl-5 pb-5 font-semibold">Up Next</h1>
+ <div className="flex gap-4 pl-5 pb-5">
+ <button
+ disabled={!track?.next}
+ onClick={() => {
+ router.push(
+ `/en/anime/watch/${info.id}/${providerId}?id=${
+ track?.next?.id
+ }&num=${track?.next?.number}${dub ? `&dub=${dub}` : ""}`
+ );
+ }}
+ className="text-xl font-karla font-semibold"
+ >
+ Next Episode {">"}
+ </button>
+ {episode && (
+ <div className="relative flex gap-2 items-center group">
+ <select
+ value={track?.playing?.number}
+ onChange={(e) => {
+ const selectedEpisode = episode.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}` : ""}`
+ );
+ }}
+ className="flex items-center text-sm gap-5 rounded-[3px] bg-secondary py-1 px-3 pr-8 font-karla appearance-none cursor-pointer outline-none focus:ring-1 focus:ring-action group-hover:ring-1 group-hover:ring-action"
+ >
+ {episode?.map((x) => (
+ <option key={x.id} value={x.number}>
+ Episode {x.number}
+ </option>
+ ))}
+ </select>
+ <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
+ </div>
+ )}
+ </div>
<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(
diff --git a/lib/Artplayer.js b/lib/Artplayer.js
deleted file mode 100644
index 48da24d..0000000
--- a/lib/Artplayer.js
+++ /dev/null
@@ -1,290 +0,0 @@
-import { useEffect, useRef } from "react";
-import Artplayer from "artplayer";
-import Hls from "hls.js";
-import { useRouter } from "next/router";
-
-export default function Player({
- option,
- res,
- quality,
- subSize,
- subtitles,
- provider,
- getInstance,
- id,
- track,
- // socket
- // isPlay,
- // watchdata,
- // room,
- autoplay,
- setautoplay,
- ...rest
-}) {
- const artRef = useRef();
-
- const router = useRouter();
-
- 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,
- },
- fullscreen: true,
- hotkey: true,
- lock: true,
- setting: true,
- playbackRate: true,
- autoOrientation: true,
- pip: true,
- theme: "#f97316",
- controls: [
- {
- index: 10,
- name: "fast-rewind",
- position: "left",
- html: '<svg class="hi-solid hi-rewind inline-block w-7 h-7" 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;
- },
- },
- {
- index: 11,
- name: "fast-forward",
- position: "left",
- html: '<svg class="hi-solid hi-fast-forward inline-block w-7 h-7" 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;
- },
- },
- ],
- settings: [
- {
- html: "Autoplay Next",
- // icon: '<img width="22" heigth="22" src="/assets/img/state.svg">',
- tooltip: "ON/OFF",
- switch: localStorage.getItem("autoplay") === "true" ? true : false,
- onSwitch: function (item) {
- setautoplay(!item.switch);
- localStorage.setItem("autoplay", !item.switch);
- return !item.switch;
- },
- },
- provider === "zoro" && {
- html: "Subtitles",
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="35" height="28" viewBox="0 -960 960 960"><path d="M240-350h360v-60H240v60zm420 0h60v-60h-60v60zM240-470h60v-60h-60v60zm120 0h360v-60H360v60zM140-160q-24 0-42-18t-18-42v-520q0-24 18-42t42-18h680q24 0 42 18t18 42v520q0 24-18 42t-42 18H140zm0-60h680v-520H140v520zm0 0v-520 520z"></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;
- },
- },
- ],
- },
- provider === "gogoanime" && {
- html: "Quality",
- width: 150,
- tooltip: `${res}`,
- selector: quality,
- onSelect: function (item) {
- art.switchQuality(item.url, item.html);
- localStorage.setItem("quality", item.html);
- return item.html;
- },
- },
- ].filter(Boolean),
- });
-
- if ("mediaSession" in navigator) {
- art.on("video:timeupdate", () => {
- const session = navigator.mediaSession;
- if (!session) return;
- session.setPositionState({
- duration: art.duration,
- playbackRate: art.playbackRate,
- position: art.currentTime,
- });
- });
-
- navigator.mediaSession.setActionHandler("play", () => {
- art.play();
- });
-
- navigator.mediaSession.setActionHandler("pause", () => {
- art.pause();
- });
-
- navigator.mediaSession.setActionHandler("previoustrack", () => {
- if (track?.prev) {
- router.push(
- `/en/anime/watch/${id}/${provider}?id=${encodeURIComponent(
- track?.prev?.id
- )}&num=${track?.prev?.number}`
- );
- }
- });
-
- navigator.mediaSession.setActionHandler("nexttrack", () => {
- if (track?.next) {
- router.push(
- `/en/anime/watch/${id}/${provider}?id=${encodeURIComponent(
- track?.next?.id
- )}&num=${track?.next?.number}`
- );
- }
- });
- }
-
- art.events.proxy(document, "keydown", (event) => {
- if (event.key === "f" || event.key === "F") {
- art.fullscreen = !art.fullscreen;
- }
- });
-
- // artInstanceRef.current = art;
-
- if (getInstance && typeof getInstance === "function") {
- getInstance(art);
- }
-
- return () => {
- if (art && art.destroy) {
- art.destroy(false);
- }
- };
- }, []);
-
- return <div ref={artRef} {...rest}></div>;
-}
diff --git a/lib/anify/getMangaId.js b/lib/anify/getMangaId.js
new file mode 100644
index 0000000..e18da65
--- /dev/null
+++ b/lib/anify/getMangaId.js
@@ -0,0 +1,40 @@
+import axios from "axios";
+
+export async function fetchInfo(romaji, english, native) {
+ try {
+ const { data: getManga } = await axios.get(
+ `https://api.anify.tv/search-advanced?query=${
+ english || romaji
+ }&type=manga`
+ );
+
+ const findManga = getManga.find(
+ (manga) =>
+ manga.title.romaji === romaji ||
+ manga.title.english === english ||
+ manga.title.native === native
+ );
+
+ if (!findManga) {
+ return null;
+ }
+
+ return { id: findManga.id };
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ return null;
+ }
+}
+
+export default async function getMangaId(romaji, english, native) {
+ try {
+ const data = await fetchInfo(romaji, english, native);
+ if (data) {
+ return data;
+ } else {
+ return { message: "Schedule not found" };
+ }
+ } catch (error) {
+ return { error };
+ }
+}
diff --git a/lib/anify/page.js b/lib/anify/page.js
index 65ed309..0f0bb93 100644
--- a/lib/anify/page.js
+++ b/lib/anify/page.js
@@ -1,10 +1,10 @@
import { redis } from "../redis";
// Function to fetch new data
-async function fetchData(id, providerId, chapterId, key) {
+async function fetchData(id, chapterNumber, providerId, chapterId, key) {
try {
const res = await fetch(
- `https://api.anify.tv/pages?id=${id}&providerId=${providerId}&readId=${chapterId}&apikey=${key}`
+ `https://api.anify.tv/pages/${id}/${chapterNumber}/${providerId}/${chapterId}&apikey=${key}`
);
const data = await res.json();
return data;
@@ -16,6 +16,7 @@ async function fetchData(id, providerId, chapterId, key) {
export default async function getAnifyPage(
mediaId,
+ chapterNumber,
providerId,
chapterId,
key
@@ -28,7 +29,13 @@ export default async function getAnifyPage(
if (cached) {
return JSON.parse(cached);
} else {
- const data = await fetchData(mediaId, providerId, chapterId, key);
+ const data = await fetchData(
+ mediaId,
+ chapterNumber,
+ providerId,
+ chapterId,
+ key
+ );
if (!data.error) {
if (redis) {
await redis.set(chapterId, JSON.stringify(data), "EX", 60 * 10);
diff --git a/lib/anilist/aniAdvanceSearch.js b/lib/anilist/aniAdvanceSearch.js
index 02a5c53..cf344b0 100644
--- a/lib/anilist/aniAdvanceSearch.js
+++ b/lib/anilist/aniAdvanceSearch.js
@@ -23,37 +23,104 @@ export async function aniAdvanceSearch({
return result;
}, {});
- const response = await fetch("https://graphql.anilist.co/", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- query: advanceSearchQuery,
- variables: {
- ...(search && {
- search: search,
- ...(!sort && { sort: "SEARCH_MATCH" }),
- }),
- ...(type && { type: type }),
- ...(seasonYear && { seasonYear: seasonYear }),
- ...(season && {
- season: season,
- ...(!seasonYear && { seasonYear: new Date().getFullYear() }),
- }),
- ...(categorizedGenres && { ...categorizedGenres }),
- ...(format && { format: format }),
- // ...(genres && { genres: genres }),
- // ...(tags && { tags: tags }),
- ...(perPage && { perPage: perPage }),
- ...(sort && { sort: sort }),
+ if (type === "MANGA") {
+ const response = await fetch("https://api.anify.tv/search-advanced", {
+ method: "POST",
+ body: JSON.stringify({
+ type: "manga",
+ genres: categorizedGenres,
+ ...(search && { query: search }),
...(page && { page: page }),
+ ...(perPage && { perPage: perPage }),
+ ...(format && { format: format }),
+ ...(seasonYear && { year: seasonYear }),
+ ...(type && { type: type }),
+ }),
+ });
+
+ const data = await response.json();
+ return {
+ pageInfo: {
+ hasNextPage: data.length >= (perPage ?? 20),
+ currentPage: page,
+ lastPage: Math.ceil(data.length / (perPage ?? 20)),
+ perPage: perPage ?? 20,
+ total: data.length,
+ },
+ media: data.map((item) => ({
+ averageScore: item.averageRating,
+ bannerImage: item.bannerImage,
+ chapters: item.totalChapters,
+ coverImage: {
+ color: item.color,
+ extraLarge: item.coverImage,
+ large: item.coverImage,
+ },
+ description: item.description,
+ duration: item.duration ?? null,
+ endDate: {
+ day: null,
+ month: null,
+ year: null,
+ },
+ mappings: item.mappings,
+ format: item.format,
+ genres: item.genres,
+ id: item.id,
+ isAdult: false,
+ mediaListEntry: null,
+ nextAiringEpisode: null,
+ popularity: item.averagePopularity,
+ season: null,
+ seasonYear: item.year,
+ startDate: {
+ day: null,
+ month: null,
+ year: item.year,
+ },
+ status: item.status,
+ studios: { edges: [] },
+ title: {
+ userPreferred:
+ item.title.english ?? item.title.romaji ?? item.title.native,
+ },
+ type: item.type,
+ volumes: item.totalVolumes ?? null,
+ })),
+ };
+ } else {
+ const response = await fetch("https://graphql.anilist.co/", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
},
- }),
- });
+ body: JSON.stringify({
+ query: advanceSearchQuery,
+ variables: {
+ ...(search && {
+ search: search,
+ ...(!sort && { sort: "SEARCH_MATCH" }),
+ }),
+ ...(type && { type: type }),
+ ...(seasonYear && { seasonYear: seasonYear }),
+ ...(season && {
+ season: season,
+ ...(!seasonYear && { seasonYear: new Date().getFullYear() }),
+ }),
+ ...(categorizedGenres && { ...categorizedGenres }),
+ ...(format && { format: format }),
+ // ...(genres && { genres: genres }),
+ // ...(tags && { tags: tags }),
+ ...(perPage && { perPage: perPage }),
+ ...(sort && { sort: sort }),
+ ...(page && { page: page }),
+ },
+ }),
+ });
- const datas = await response.json();
- // console.log(datas);
- const data = datas.data.Page;
- return data;
+ const datas = await response.json();
+ // console.log(datas);
+ const data = datas.data.Page;
+ return data;
+ }
}
diff --git a/lib/anilist/getMedia.js b/lib/anilist/getMedia.js
index 66bb1b0..2e1b0d0 100644
--- a/lib/anilist/getMedia.js
+++ b/lib/anilist/getMedia.js
@@ -115,6 +115,8 @@ export default function GetMedia(session, stats) {
data.data.Page.recommendations.map((i) => i.mediaRecommendation)
);
});
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [username, accessToken, status?.stats]);
return { anime, manga, recommendations };
diff --git a/lib/anilist/useAnilist.js b/lib/anilist/useAnilist.js
index 17ab11b..20c1964 100644
--- a/lib/anilist/useAnilist.js
+++ b/lib/anilist/useAnilist.js
@@ -1,4 +1,4 @@
-import { toast } from "react-toastify";
+import { toast } from "sonner";
export const useAniList = (session) => {
const accessToken = session?.user?.token;
@@ -238,11 +238,6 @@ export const useAniList = (session) => {
console.log(`Progress Updated: ${progress}`, status);
toast.success(`Progress Updated: ${progress}`, {
position: "bottom-right",
- autoClose: 5000,
- hideProgressBar: false,
- closeOnClick: true,
- draggable: true,
- theme: "dark",
});
}
};
diff --git a/lib/consumet/manga/getChapters.js b/lib/consumet/manga/getChapters.js
new file mode 100644
index 0000000..7a19bbc
--- /dev/null
+++ b/lib/consumet/manga/getChapters.js
@@ -0,0 +1,80 @@
+let API_URL;
+API_URL = process.env.API_URI;
+// remove / from the end of the url if it exists
+if (API_URL.endsWith("/")) {
+ API_URL = API_URL.slice(0, -1);
+}
+
+async function fetchInfo(id) {
+ try {
+ const providers = [
+ "mangadex",
+ "mangahere",
+ "mangakakalot",
+ // "mangapark",
+ // "mangapill",
+ "mangasee123",
+ ];
+ let datas = [];
+
+ async function promiseMe(provider) {
+ try {
+ const data = await fetch(
+ `${API_URL}/meta/anilist-manga/info/${id}?provider=${provider}`
+ ).then((res) => {
+ if (!res.ok) {
+ switch (res.status) {
+ case 404: {
+ return null;
+ }
+ }
+ }
+ return res.json();
+ });
+ if (data.chapters.length > 0) {
+ datas.push({
+ providerId: provider,
+ chapters: data.chapters,
+ });
+ }
+ } catch (error) {
+ console.error(`Error fetching data for provider '${provider}':`, error);
+ }
+ }
+
+ await Promise.all(providers.map((provider) => promiseMe(provider)));
+
+ return datas;
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ return null;
+ }
+}
+
+export default async function getConsumetChapters(id, redis) {
+ try {
+ let cached;
+ let chapters;
+
+ if (redis) {
+ cached = await redis.get(`chapter:${id}`);
+ }
+
+ if (cached) {
+ chapters = JSON.parse(cached);
+ } else {
+ chapters = await fetchInfo(id);
+ }
+
+ if (chapters?.length === 0) {
+ return null;
+ }
+ if (redis) {
+ await redis.set(`chapter:${id}`, JSON.stringify(chapters), "EX", 60 * 60); // 1 hour
+ }
+
+ return chapters;
+ } catch (error) {
+ return { error };
+ }
+}
diff --git a/lib/consumet/manga/getPage.js b/lib/consumet/manga/getPage.js
new file mode 100644
index 0000000..832c1d7
--- /dev/null
+++ b/lib/consumet/manga/getPage.js
@@ -0,0 +1,49 @@
+let API_URL;
+API_URL = process.env.API_URI;
+// remove / from the end of the url if it exists
+if (API_URL.endsWith("/")) {
+ API_URL = API_URL.slice(0, -1);
+}
+
+// Function to fetch new data
+async function fetchData(id, providerId, chapterId, key) {
+ try {
+ const res = await fetch(
+ `${API_URL}/meta/anilist-manga/read?chapterId=${chapterId}&provider=${providerId}`
+ );
+ const data = await res.json();
+ return data;
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ return null;
+ }
+}
+
+export default async function getConsumetPages(
+ mediaId,
+ providerId,
+ chapterId,
+ key
+) {
+ try {
+ // let cached;
+ // if (redis) {
+ // cached = await redis.get(chapterId);
+ // }
+ // if (cached) {
+ // return JSON.parse(cached);
+ // } else {
+ const data = await fetchData(mediaId, providerId, chapterId, key);
+ if (!data.error) {
+ // if (redis) {
+ // await redis.set(chapterId, JSON.stringify(data), "EX", 60 * 10);
+ // }
+ return data;
+ } else {
+ return { message: "Manga/Novel not found :(" };
+ }
+ // }
+ } catch (error) {
+ return { error };
+ }
+}
diff --git a/lib/graphql/query.js b/lib/graphql/query.js
index a09c6ac..45d3d68 100644
--- a/lib/graphql/query.js
+++ b/lib/graphql/query.js
@@ -176,8 +176,14 @@ query {
}`;
const mediaInfoQuery = `
- query ($id: Int) {
- Media(id: $id) {
+ query ($id: Int, $type:MediaType) {
+ Media(id: $id, type:$type) {
+ mediaListEntry {
+ status
+ progress
+ progressVolumes
+ status
+ }
id
type
format
@@ -191,6 +197,10 @@ const mediaInfoQuery = `
large
color
}
+ startDate {
+ year
+ month
+ }
bannerImage
description
episodes
diff --git a/next.config.js b/next.config.js
index a0ec43c..d3fd882 100644
--- a/next.config.js
+++ b/next.config.js
@@ -24,6 +24,10 @@ module.exports = withPWA({
protocol: "https",
hostname: "simkl.in",
},
+ {
+ protocol: "https",
+ hostname: "tenor.com",
+ },
],
},
// distDir: process.env.BUILD_DIR || ".next",
diff --git a/package-lock.json b/package-lock.json
index 1edee36..d90eeb3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "moopa",
- "version": "4.1.3",
+ "version": "4.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "moopa",
- "version": "4.1.3",
+ "version": "4.2.0",
"dependencies": {
"@apollo/client": "^3.7.3",
"@headlessui/react": "^1.7.15",
@@ -35,6 +35,8 @@
"react-loading-skeleton": "^3.2.0",
"react-toastify": "^9.1.3",
"react-use-draggable-scroll": "^0.4.7",
+ "sharp": "^0.32.6",
+ "sonner": "^1.0.3",
"tailwind-scrollbar-hide": "^1.1.7",
"workbox-webpack-plugin": "^7.0.0"
},
@@ -3267,6 +3269,11 @@
"dequal": "^2.0.3"
}
},
+ "node_modules/b4a": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
+ "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw=="
+ },
"node_modules/babel-loader": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz",
@@ -3359,6 +3366,16 @@
"node": ">=8"
}
},
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -3410,11 +3427,53 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
+ "node_modules/buffer/node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
@@ -3558,6 +3617,11 @@
"node": ">= 6"
}
},
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
+ },
"node_modules/chrome-trace-event": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
@@ -3618,6 +3682,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -3631,6 +3707,31 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
+ "node_modules/color/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -3802,6 +3903,28 @@
}
}
},
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -3975,6 +4098,14 @@
"node": ">=6"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
+ "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -4052,6 +4183,14 @@
"node": ">= 4"
}
},
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
@@ -4782,11 +4921,24 @@
"node": ">=0.8.x"
}
},
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
+ },
"node_modules/fast-glob": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
@@ -5008,6 +5160,11 @@
"react-dom": "^18.0.0"
}
},
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+ },
"node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@@ -5133,6 +5290,11 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
+ },
"node_modules/glob": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
@@ -5362,6 +5524,25 @@
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
"integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"node_modules/ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@@ -5415,6 +5596,11 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
+ },
"node_modules/internal-slot": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
@@ -6339,6 +6525,17 @@
"node": ">= 0.6"
}
},
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -6354,11 +6551,15 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
+ },
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -6411,6 +6612,11 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/napi-build-utils": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
+ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
+ },
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -6761,6 +6967,22 @@
"react": ">= 16.0.0"
}
},
+ "node_modules/node-abi": {
+ "version": "3.51.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz",
+ "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
+ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="
+ },
"node_modules/node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
@@ -7402,6 +7624,57 @@
"preact": ">=10"
}
},
+ "node_modules/prebuild-install": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
+ "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^1.0.1",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/tar-fs": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
+ "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -7458,6 +7731,15 @@
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
+ "node_modules/pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
@@ -7495,6 +7777,11 @@
}
]
},
+ "node_modules/queue-tick": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
+ "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
+ },
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -7508,6 +7795,28 @@
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-3.0.0.tgz",
"integrity": "sha512-janAJkWxWxmLka0hV+XvCTo0M8keeSeOuz8ZL33cTXrkS4ek9mQ2VJm9ri7fm03oTVth19Sfqb1ijCmo7K/vAg=="
},
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/rc/node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@@ -7593,6 +7902,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -8037,7 +8359,6 @@
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
- "dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -8058,7 +8379,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -8069,8 +8389,7 @@
"node_modules/semver/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/serialize-javascript": {
"version": "6.0.1",
@@ -8099,6 +8418,28 @@
"node": ">= 0.4"
}
},
+ "node_modules/sharp": {
+ "version": "0.32.6",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
+ "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.2",
+ "node-addon-api": "^6.1.0",
+ "prebuild-install": "^7.1.1",
+ "semver": "^7.5.4",
+ "simple-get": "^4.0.1",
+ "tar-fs": "^3.0.4",
+ "tunnel-agent": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -8133,6 +8474,62 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/simple-swizzle/node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+ },
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -8141,6 +8538,15 @@
"node": ">=8"
}
},
+ "node_modules/sonner": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.0.3.tgz",
+ "integrity": "sha512-hBoA2zKuYW3lUnpx4K0vAn8j77YuYiwvP9sLQfieNS2pd5FkT20sMyPTDJnl9S+5T27ZJbwQRPiujwvDBwhZQg==",
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"node_modules/source-list-map": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@@ -8196,6 +8602,23 @@
"node": ">=10.0.0"
}
},
+ "node_modules/streamx": {
+ "version": "2.15.1",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz",
+ "integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==",
+ "dependencies": {
+ "fast-fifo": "^1.1.0",
+ "queue-tick": "^1.0.1"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -8538,6 +8961,26 @@
"node": ">=6"
}
},
+ "node_modules/tar-fs": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz",
+ "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==",
+ "dependencies": {
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^3.1.5"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz",
+ "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
"node_modules/temp-dir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
@@ -8761,6 +9204,17 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
},
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -8986,8 +9440,7 @@
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "dev": true
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uuid": {
"version": "8.3.2",
diff --git a/package.json b/package.json
index 91fba68..a13c9fe 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "moopa",
- "version": "4.1.3",
+ "version": "4.2.0",
"private": true,
"founder": "Factiven",
"scripts": {
@@ -38,6 +38,8 @@
"react-loading-skeleton": "^3.2.0",
"react-toastify": "^9.1.3",
"react-use-draggable-scroll": "^0.4.7",
+ "sharp": "^0.32.6",
+ "sonner": "^1.0.3",
"tailwind-scrollbar-hide": "^1.1.7",
"workbox-webpack-plugin": "^7.0.0"
},
diff --git a/pages/404.js b/pages/404.js
index f6e609f..085d984 100644
--- a/pages/404.js
+++ b/pages/404.js
@@ -1,27 +1,13 @@
import Head from "next/head";
import Link from "next/link";
-import { useEffect, useState } from "react";
-import { parseCookies } from "nookies";
import Image from "next/image";
import Footer from "@/components/shared/footer";
+import { NewNavbar } from "@/components/shared/NavBar";
+import { useRouter } from "next/router";
+import { ArrowLeftIcon } from "@heroicons/react/24/outline";
export default function Custom404() {
- const [lang, setLang] = useState("en");
- const [cookie, setCookies] = useState(null);
-
- 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");
- }
- }, []);
+ const router = useRouter();
return (
<>
<Head>
@@ -30,6 +16,7 @@ export default function Custom404() {
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/svg/c.svg" />
</Head>
+ <NewNavbar withNav shrink />
<div className="min-h-screen w-screen flex flex-col items-center justify-center ">
<Image
width={500}
@@ -44,11 +31,29 @@ export default function Custom404() {
<p className="text-base sm:text-lg xl:text-xl text-gray-300 mb-6 text-center">
The page you're looking for doesn't seem to exist.
</p>
- <Link href={`/${lang}/`}>
- <div className="bg-[#fa7d56] xl:text-xl text-white font-bold py-2 px-4 rounded hover:bg-[#fb6f44]">
- Go back home
- </div>
- </Link>
+ <div className="flex gap-5 font-karla">
+ <button
+ type="button"
+ onClick={() => {
+ router.back();
+ }}
+ className="flex items-center gap-2 py-2 px-4 ring-1 ring-action/70 rounded hover:text-white transition-all duration-200 ease-out"
+ >
+ <span>
+ <ArrowLeftIcon className="w-5 h-5" />
+ </span>
+ Go back
+ </button>
+ <button
+ type="button"
+ onClick={() => {
+ router.push("/en");
+ }}
+ className="bg-action xl:text-xl text-white font-bold py-2 px-4 rounded hover:bg-opacity-80 hover:text-white transition-all duration-200 ease-out"
+ >
+ Home Page
+ </button>
+ </div>
</div>
<Footer />
</>
diff --git a/pages/_app.js b/pages/_app.js
index f553a98..e2f780d 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -3,22 +3,23 @@ import { AnimatePresence, motion as m } from "framer-motion";
import NextNProgress from "nextjs-progressbar";
import { SessionProvider } from "next-auth/react";
import "../styles/globals.css";
-import "react-toastify/dist/ReactToastify.css";
import "react-loading-skeleton/dist/skeleton.css";
import { SkeletonTheme } from "react-loading-skeleton";
import SearchPalette from "@/components/searchPalette";
import { SearchProvider } from "@/lib/context/isOpenState";
import Head from "next/head";
import { WatchPageProvider } from "@/lib/context/watchPageProvider";
-import { ToastContainer, toast } from "react-toastify";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { unixTimestampToRelativeTime } from "@/utils/getTimes";
+import SecretPage from "@/components/secret";
+import { Toaster, toast } from "sonner";
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
const router = useRouter();
+ const [info, setInfo] = useState(null);
useEffect(() => {
async function getBroadcast() {
@@ -31,29 +32,31 @@ export default function App({
},
});
const data = await res.json();
- if (
- data &&
- data?.message !== "No broadcast" &&
- data?.message !== "unauthorized"
- ) {
- toast(
- `${data.message} ${
+ if (data?.show === true) {
+ toast.message(
+ `🚧${data.message} ${
data?.startAt ? unixTimestampToRelativeTime(data.startAt) : ""
- }`,
+ }🚧`,
{
- position: "top-center",
- autoClose: false,
- closeOnClick: true,
- draggable: true,
- theme: "colored",
- className: "toaster",
- style: {
- background: "#232329",
- color: "#fff",
- },
+ position: "bottom-right",
+ important: true,
+ duration: 100000,
+ className: "flex-center font-karla text-white",
+ // description: `🚧${info}🚧`,
}
);
+ // toast.message(`Announcement`, {
+ // position: "top-center",
+ // important: true,
+ // // duration: 10000,
+ // description: `🚧${info}🚧`,
+ // });
}
+ setInfo(
+ `${data.message} ${
+ data?.startAt ? unixTimestampToRelativeTime(data.startAt) : ""
+ }`
+ );
} catch (err) {
console.log(err);
}
@@ -61,12 +64,16 @@ export default function App({
getBroadcast();
}, []);
+ const handleCheatCodeEntered = () => {
+ alert("Cheat code entered!"); // You can replace this with your desired action
+ };
+
return (
<>
<Head>
<meta
name="viewport"
- content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover"
+ content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, viewport-fit=cover"
/>
</Head>
<SessionProvider session={session}>
@@ -74,7 +81,22 @@ export default function App({
<WatchPageProvider>
<AnimatePresence mode="wait">
<SkeletonTheme baseColor="#232329" highlightColor="#2a2a32">
- <ToastContainer pauseOnFocusLoss={false} pauseOnHover={false} />
+ <Toaster richColors theme="dark" closeButton />
+ <SecretPage
+ cheatCode={"aofienaef"}
+ onCheatCodeEntered={handleCheatCodeEntered}
+ />
+ {/* {info && (
+ <div className="relative px-3 flex items-center justify-center font-karla w-full py-2 bg-secondary/80 text-white text-center">
+ <span className="line-clamp-1 mr-5">🚧{info}🚧</span>
+ <span
+ onClick={() => setInfo()}
+ className="absolute right-3 cursor-pointer"
+ >
+ <XMarkIcon className="w-6 h-6" />
+ </span>
+ </div>
+ )} */}
<m.div
key={`route-${router.route}`}
transition={{ duration: 0.5 }}
diff --git a/pages/_error.js b/pages/_error.js
new file mode 100644
index 0000000..19dfcff
--- /dev/null
+++ b/pages/_error.js
@@ -0,0 +1,41 @@
+import MobileNav from "@/components/shared/MobileNav";
+import { NewNavbar } from "@/components/shared/NavBar";
+import Footer from "@/components/shared/footer";
+import Head from "next/head";
+import Link from "next/link";
+
+function Error({ statusCode }) {
+ return (
+ <>
+ <Head>
+ <title>An Error Has Occurred</title>
+ </Head>
+ <NewNavbar withNav shrink />
+ <MobileNav hideProfile />
+ <div className="w-screen h-screen flex-center flex-col gap-5">
+ <div className="relative text-3xl">(╯°□°)╯︵ ┻━┻</div>
+ <div className="flex items-center gap-2 text-xl">
+ <span>
+ {statusCode
+ ? `An error ${statusCode} occurred on server.`
+ : "An error occurred on client."}
+ </span>
+ </div>
+ <Link
+ href="/en"
+ className="rounded ring-action/50 ring-1 p-2 font-karla bg-action bg-opacity-0 hover:bg-opacity-20 hover:scale-105 text-white transition-all duration-300"
+ >
+ Back to home
+ </Link>
+ </div>
+ <Footer />
+ </>
+ );
+}
+
+Error.getInitialProps = ({ res, err }) => {
+ const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
+ return { statusCode };
+};
+
+export default Error;
diff --git a/pages/_offline.js b/pages/_offline.js
new file mode 100644
index 0000000..f440b39
--- /dev/null
+++ b/pages/_offline.js
@@ -0,0 +1,45 @@
+import Image from "next/image";
+import React from "react";
+
+export default function Fallback() {
+ return (
+ <div className="w-screen h-screen flex-center flex-col gap-5">
+ <div className="relative">
+ <Image
+ src="/svg/c.svg"
+ alt="logo"
+ height={160}
+ width={160}
+ quality={100}
+ className="object-cover"
+ />
+ </div>
+ <p className="flex items-center gap-2 text-2xl">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="1em"
+ height="1em"
+ viewBox="0 0 512 512"
+ >
+ <path
+ fill="none"
+ stroke="currentColor"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth="32"
+ d="M93.72 183.25C49.49 198.05 16 233.1 16 288c0 66 54 112 120 112h184.37m147.45-22.26C485.24 363.3 496 341.61 496 312c0-59.82-53-85.76-96-88c-8.89-89.54-71-144-144-144c-26.16 0-48.79 6.93-67.6 18.14"
+ ></path>
+ <path
+ fill="none"
+ stroke="currentColor"
+ strokeLinecap="round"
+ strokeMiterlimit="10"
+ strokeWidth="32"
+ d="M448 448L64 64"
+ ></path>
+ </svg>
+ <span>You are Offline :\</span>
+ </p>
+ </div>
+ );
+}
diff --git a/pages/admin/index.js b/pages/admin/index.js
index cbb5086..2a73fc1 100644
--- a/pages/admin/index.js
+++ b/pages/admin/index.js
@@ -27,7 +27,12 @@ export async function getServerSideProps(context) {
}
const admin = sessions?.user?.name === process.env.ADMIN_USERNAME;
- const api = process.env.API_URI;
+
+ let api;
+ api = process.env.API_URI;
+ if (api.endsWith("/")) {
+ api = api.slice(0, -1);
+ }
if (!admin) {
return {
diff --git a/pages/api/v2/admin/broadcast/index.js b/pages/api/v2/admin/broadcast/index.js
index d3d3af0..470d61d 100644
--- a/pages/api/v2/admin/broadcast/index.js
+++ b/pages/api/v2/admin/broadcast/index.js
@@ -1,9 +1,17 @@
import { rateLimitStrict, redis } from "@/lib/redis";
-// import { getServerSession } from "next-auth";
-// import { authOptions } from "pages/api/auth/[...nextauth]";
+import { getServerSession } from "next-auth";
+import { authOptions } from "pages/api/auth/[...nextauth]";
export default async function handler(req, res) {
// Check if the custom header "X-Your-Custom-Header" is present and has a specific value
+ const sessions = await getServerSession(req, res, authOptions);
+
+ const admin = sessions?.user?.name === process.env.ADMIN_USERNAME;
+ // if req.method === POST and admin === false return 401
+ if (!admin && req.method === "DELETE") {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+
const customHeaderValue = req.headers["x-broadcast-key"];
if (customHeaderValue !== "get-broadcast") {
@@ -21,14 +29,40 @@ export default async function handler(req, res) {
});
}
- const getId = await redis.get(`broadcast`);
- if (getId) {
- const broadcast = JSON.parse(getId);
- return res
- .status(200)
- .json({ message: broadcast.message, startAt: broadcast.startAt });
- } else {
- return res.status(200).json({ message: "No broadcast" });
+ if (req.method === "POST") {
+ const { message, startAt = undefined, show = false } = req.body;
+ if (!message) {
+ return res.status(400).json({ message: "Message is required" });
+ }
+
+ const broadcastContent = {
+ message,
+ startAt,
+ show,
+ };
+ await redis.set(`broadcasts`, JSON.stringify(broadcastContent));
+ return res.status(200).json({ message: "Broadcast created" });
+ } else if (req.method === "DELETE") {
+ const br = await redis.get(`broadcasts`);
+ // set broadcast show as false
+ if (br) {
+ const broadcast = JSON.parse(br);
+ broadcast.show = false;
+ await redis.set(`broadcasts`, JSON.stringify(broadcast));
+ }
+ return res.status(200).json({ message: "Broadcast deleted" });
+ } else if (req.method === "GET") {
+ const getId = await redis.get(`broadcasts`);
+ if (getId) {
+ const broadcast = JSON.parse(getId);
+ return res.status(200).json({
+ message: broadcast.message,
+ startAt: broadcast.startAt,
+ show: broadcast.show,
+ });
+ } else {
+ return res.status(200).json({ message: "No broadcast" });
+ }
}
}
diff --git a/pages/api/v2/admin/bug-report/index.js b/pages/api/v2/admin/bug-report/index.js
index fc5ee77..508e6cd 100644
--- a/pages/api/v2/admin/bug-report/index.js
+++ b/pages/api/v2/admin/bug-report/index.js
@@ -8,16 +8,6 @@ export default async function handler(req, res) {
// create random id each time the endpoint is called
const id = Math.random().toString(36).substr(2, 9);
- // if (!admin) {
- // return res.status(401).json({ message: "Unauthorized" });
- // }
- const { data } = req.body;
-
- // if method is not POST return message "Method not allowed"
- if (req.method !== "POST") {
- return res.status(405).json({ message: "Method not allowed" });
- }
-
try {
if (redis) {
try {
@@ -29,16 +19,22 @@ export default async function handler(req, res) {
});
}
- const getId = await redis.get(`report:${id}`);
- if (getId) {
+ if (req.method === "POST") {
+ const { data } = req.body;
+
+ data.id = id;
+
+ await redis.set(`report:${id}`, JSON.stringify(data));
return res
.status(200)
- .json({ message: `Data already exist for id: ${id}` });
+ .json({ message: `Report has successfully sent, with Id of ${id}` });
+ } else if (req.method === "DELETE") {
+ const { reportId } = req.body;
+ await redis.del(`report:${reportId}`);
+ return res.status(200).json({ message: `Report has been deleted` });
+ } else {
+ return res.status(405).json({ message: "Method not allowed" });
}
- await redis.set(`report:${id}`, JSON.stringify(data));
- return res
- .status(200)
- .json({ message: `Report has successfully sent, with Id of ${id}` });
}
return res.status(200).json({ message: "redis is not defined" });
diff --git a/pages/api/v2/episode/[id].js b/pages/api/v2/episode/[id].js
index c1fac8b..3f1372b 100644
--- a/pages/api/v2/episode/[id].js
+++ b/pages/api/v2/episode/[id].js
@@ -3,7 +3,13 @@ import { rateLimitStrict, rateLimiterRedis, redis } from "@/lib/redis";
import appendImagesToEpisodes from "@/utils/combineImages";
import appendMetaToEpisodes from "@/utils/appendMetaToEpisodes";
-const CONSUMET_URI = process.env.API_URI;
+let CONSUMET_URI;
+
+CONSUMET_URI = process.env.API_URI;
+if (CONSUMET_URI.endsWith("/")) {
+ CONSUMET_URI = CONSUMET_URI.slice(0, -1);
+}
+
const API_KEY = process.env.API_KEY;
const isAscending = (data) => {
@@ -15,37 +21,70 @@ const isAscending = (data) => {
return true;
};
-async function fetchConsumet(id, dub) {
- try {
- if (dub) {
- return [];
+function filterData(data, type) {
+ // Filter the data based on the type (sub or dub) and providerId
+ const filteredData = data.map((item) => {
+ if (item?.map === true) {
+ if (item.episodes[type].length === 0) {
+ return null;
+ } else {
+ return {
+ ...item,
+ episodes: Object?.entries(item.episodes[type]).map(
+ ([id, episode]) => ({
+ ...episode,
+ })
+ ),
+ };
+ }
}
+ return item;
+ });
- const { data } = await axios.get(
- `${CONSUMET_URI}/meta/anilist/episodes/${id}`
- );
+ const noEmpty = filteredData.filter((i) => i !== null);
+ return noEmpty;
+}
- if (data?.message === "Anime not found" && data?.length < 1) {
- return [];
+async function fetchConsumet(id) {
+ try {
+ async function fetchData(dub) {
+ const { data } = await axios.get(
+ `${CONSUMET_URI}/meta/anilist/episodes/${id}${dub ? "?dub=true" : ""}`
+ );
+ if (data?.message === "Anime not found" && data?.length < 1) {
+ return [];
+ }
+
+ if (dub) {
+ if (!data?.some((i) => i.id.includes("dub"))) return [];
+ }
+
+ const reformatted = data.map((item) => ({
+ id: item?.id || null,
+ title: item?.title || null,
+ img: item?.image || null,
+ number: item?.number || null,
+ createdAt: item?.createdAt || null,
+ description: item?.description || null,
+ url: item?.url || null,
+ }));
+
+ return reformatted;
}
- const reformatted = data.map((item) => ({
- id: item?.id || null,
- title: item?.title || null,
- img: item?.image || null,
- number: item?.number || null,
- createdAt: item?.createdAt || null,
- description: item?.description || null,
- url: item?.url || null,
- }));
+ const [subData, dubData] = await Promise.all([
+ fetchData(),
+ fetchData(true),
+ ]);
const array = [
{
map: true,
providerId: "gogoanime",
- episodes: isAscending(reformatted)
- ? reformatted
- : reformatted.reverse(),
+ episodes: {
+ sub: isAscending(subData) ? subData : subData.reverse(),
+ dub: isAscending(dubData) ? dubData : dubData.reverse(),
+ },
},
];
@@ -73,7 +112,15 @@ async function fetchAnify(id) {
const filtered = data.filter(
(item) => item.providerId !== "animepahe" && item.providerId !== "kass"
);
-
+ // const modifiedData = filtered.map((provider) => {
+ // if (provider.providerId === "gogoanime") {
+ // const reversedEpisodes = [...provider.episodes].reverse();
+ // return { ...provider, episodes: reversedEpisodes };
+ // }
+ // return provider;
+ // });
+
+ // return modifiedData;
return filtered;
} catch (error) {
console.error("Error fetching and processing data:", error.message);
@@ -81,12 +128,16 @@ async function fetchAnify(id) {
}
}
-async function fetchCoverImage(id) {
+async function fetchCoverImage(id, available = false) {
try {
if (!process.env.API_KEY) {
return [];
}
+ if (available) {
+ return null;
+ }
+
const { data } = await axios.get(
`https://api.anify.tv/content-metadata/${id}?apikey=${API_KEY}`
);
@@ -95,7 +146,9 @@ async function fetchCoverImage(id) {
return [];
}
- return data;
+ const getData = data[0].data;
+
+ return getData;
} catch (error) {
console.error("Error fetching and processing data:", error.message);
return [];
@@ -124,10 +177,10 @@ export default async function handler(req, res) {
}
if (refresh) {
- await redis.del(id);
+ await redis.del(`episode:${id}`);
console.log("deleted cache");
} else {
- cached = await redis.get(id);
+ cached = await redis.get(`episode:${id}`);
console.log("using redis");
}
@@ -136,49 +189,75 @@ export default async function handler(req, res) {
if (cached && !refresh) {
if (dub) {
- const filtered = JSON.parse(cached).filter((item) =>
- item.episodes.some((epi) => epi.hasDub === true)
+ const filteredData = filterData(JSON.parse(cached), "dub");
+
+ let filtered = filteredData.filter((item) =>
+ item?.episodes?.some((epi) => epi.hasDub !== false)
);
+
+ if (meta) {
+ filtered = await appendMetaToEpisodes(filtered, JSON.parse(meta));
+ }
+
return res.status(200).json(filtered);
} else {
- return res.status(200).json(JSON.parse(cached));
+ const filteredData = filterData(JSON.parse(cached), "sub");
+
+ let filtered = filteredData;
+
+ if (meta) {
+ filtered = await appendMetaToEpisodes(filteredData, JSON.parse(meta));
+ }
+
+ return res.status(200).json(filtered);
}
} else {
const [consumet, anify, cover] = await Promise.all([
fetchConsumet(id, dub),
fetchAnify(id),
- fetchCoverImage(id),
+ fetchCoverImage(id, meta),
]);
- const hasImage = consumet.map((i) =>
- i.episodes.some(
- (e) => e.img !== null || !e.img.includes("https://s4.anilist.co/")
- )
- );
+ // const hasImage = consumet.map((i) =>
+ // i.episodes?.sub?.some(
+ // (e) => e.img !== null || !e.img.includes("https://s4.anilist.co/")
+ // )
+ // );
+
+ let subDub = "sub";
+ if (dub) {
+ subDub = "dub";
+ }
- const rawData = [...consumet, ...(anify[0]?.data ?? [])];
+ const rawData = [...consumet, ...anify];
- let data = rawData;
+ const filteredData = filterData(rawData, subDub);
+
+ let data = filteredData;
if (meta) {
- data = await appendMetaToEpisodes(rawData, JSON.parse(meta));
- } else if (cover && cover?.length > 0 && !hasImage.includes(true))
- data = await appendImagesToEpisodes(rawData, cover);
+ data = await appendMetaToEpisodes(filteredData, JSON.parse(meta));
+ } else if (cover && !cover.some((e) => e.img === null)) {
+ await redis.set(`meta:${id}`, JSON.stringify(cover));
+ data = await appendMetaToEpisodes(filteredData, cover);
+ }
if (redis && cacheTime !== null) {
await redis.set(
- id,
- JSON.stringify(data.filter((i) => i.episodes.length > 0)),
+ `episode:${id}`,
+ JSON.stringify(rawData),
"EX",
cacheTime
);
}
if (dub) {
- const filtered = data.filter((item) =>
- item.episodes.some((epi) => epi.hasDub === true)
+ const filtered = data.filter(
+ (item) => !item.episodes.some((epi) => epi.hasDub === false)
);
- return res.status(200).json(filtered);
+ return res
+ .status(200)
+ .json(filtered.filter((i) => i.episodes.length > 0));
}
console.log("fresh data");
diff --git a/pages/api/v2/etc/recent/[page].js b/pages/api/v2/etc/recent/[page].js
index 6727787..b1bda0f 100644
--- a/pages/api/v2/etc/recent/[page].js
+++ b/pages/api/v2/etc/recent/[page].js
@@ -1,6 +1,10 @@
import { rateLimiterRedis, redis } from "@/lib/redis";
-const API_URL = process.env.API_URI;
+let API_URL;
+API_URL = process.env.API_URI;
+if (API_URL.endsWith("/")) {
+ API_URL = API_URL.slice(0, -1);
+}
export default async function handler(req, res) {
try {
diff --git a/pages/api/v2/info/[id].js b/pages/api/v2/info/[id].js
deleted file mode 100644
index 243756c..0000000
--- a/pages/api/v2/info/[id].js
+++ /dev/null
@@ -1,47 +0,0 @@
-import axios from "axios";
-import { rateLimiterRedis, redis } from "@/lib/redis";
-
-const API_KEY = process.env.API_KEY;
-
-export async function fetchInfo(id) {
- try {
- const { data } = await axios.get(
- `https://api.anify.tv/info/${id}?apikey=${API_KEY}`
- );
- return data;
- } catch (error) {
- console.error("Error fetching data:", error);
- return null;
- }
-}
-
-export default async function handler(req, res) {
- const id = req.query.id;
- let cached;
- if (redis) {
- try {
- const ipAddress = req.socket.remoteAddress;
- await rateLimiterRedis.consume(ipAddress);
- } catch (error) {
- return res.status(429).json({
- error: `Too Many Requests, retry after ${error.msBeforeNext / 1000}`,
- });
- }
- cached = await redis.get(id);
- }
- if (cached) {
- // console.log("Using cached data");
- return res.status(200).json(JSON.parse(cached));
- } else {
- const data = await fetchInfo(id);
- if (data) {
- // console.log("Setting cache");
- if (redis) {
- await redis.set(id, JSON.stringify(data), "EX", 60 * 10);
- }
- return res.status(200).json(data);
- } else {
- return res.status(404).json({ message: "Schedule not found" });
- }
- }
-}
diff --git a/pages/api/v2/info/index.js b/pages/api/v2/info/index.js
new file mode 100644
index 0000000..95770bd
--- /dev/null
+++ b/pages/api/v2/info/index.js
@@ -0,0 +1,60 @@
+import { redis } from "@/lib/redis";
+import axios from "axios";
+
+const API_KEY = process.env.API_KEY;
+
+export async function fetchInfo(id) {
+ try {
+ // console.log(id);
+ const { data } = await axios
+ .get(`https://api.anify.tv/info/${id}?apikey=${API_KEY}`)
+ .catch((err) => {
+ return {
+ data: null,
+ };
+ });
+
+ if (!data) {
+ return null;
+ }
+
+ const { data: Chapters } = await axios.get(
+ `https://api.anify.tv/chapters/${data.id}?apikey=${API_KEY}`
+ );
+
+ if (!Chapters) {
+ return null;
+ }
+
+ return { id: data.id, chapters: Chapters };
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ return null;
+ }
+}
+
+export default async function handler(req, res) {
+ //const [romaji, english, native] = req.query.title;
+ const { id } = req.query;
+ try {
+ let cached;
+ // const data = await fetchInfo(id);
+ cached = await redis.get(`manga:${id}`);
+
+ if (cached) {
+ return res.status(200).json(JSON.parse(cached));
+ }
+
+ const manga = await fetchInfo(id);
+
+ if (!manga) {
+ return res.status(404).json({ error: "Manga not found" });
+ }
+
+ await redis.set(`manga:${id}`, JSON.stringify(manga), "ex", 60 * 60 * 24);
+
+ res.status(200).json(manga);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+}
diff --git a/pages/api/v2/pages/[...id].js b/pages/api/v2/pages/[...id].js
new file mode 100644
index 0000000..a9fe0f9
--- /dev/null
+++ b/pages/api/v2/pages/[...id].js
@@ -0,0 +1,34 @@
+import axios from "axios";
+
+async function fetchData(id, number, provider, readId) {
+ try {
+ const { data } = await axios.get(
+ `https://api.anify.tv/pages?id=${id}&chapterNumber=${number}&providerId=${provider}&readId=${encodeURIComponent(
+ readId
+ )}`
+ );
+
+ if (!data) {
+ return null;
+ }
+
+ return data;
+ } catch (error) {
+ return null;
+ }
+}
+
+export default async function handler(req, res) {
+ const [id, number, provider, readId] = req.query.id;
+
+ try {
+ const data = await fetchData(id, number, provider, readId);
+ // if (!data) {
+ // return res.status(400).json({ error: "Invalid query" });
+ // }
+
+ return res.status(200).json(data);
+ } catch (error) {
+ return res.status(500).json({ error: error.message });
+ }
+}
diff --git a/pages/api/v2/source/index.js b/pages/api/v2/source/index.js
index f15e47d..9ec6082 100644
--- a/pages/api/v2/source/index.js
+++ b/pages/api/v2/source/index.js
@@ -1,7 +1,11 @@
import { rateLimiterRedis, redis } from "@/lib/redis";
import axios from "axios";
-const CONSUMET_URI = process.env.API_URI;
+let CONSUMET_URI;
+CONSUMET_URI = process.env.API_URI;
+if (CONSUMET_URI.endsWith("/")) {
+ CONSUMET_URI = CONSUMET_URI.slice(0, -1);
+}
const API_KEY = process.env.API_KEY;
async function consumetSource(id) {
@@ -25,7 +29,7 @@ async function anifySource(providerId, watchId, episode, id, sub) {
);
return data;
} catch (error) {
- return null;
+ return { error: error.message, status: error.response.status };
}
}
diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].js
index 910bbc6..e2c0039 100644
--- a/pages/en/anime/[...id].js
+++ b/pages/en/anime/[...id].js
@@ -72,6 +72,8 @@ export default function Info({ info, color }) {
}
}
fetchData();
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, info, session?.user?.name]);
function handleOpen() {
@@ -143,7 +145,7 @@ export default function Info({ info, color }) {
stats={statuses?.value}
prg={progress}
max={info?.episodes}
- image={info}
+ info={info}
close={handleClose}
/>
)}
@@ -208,7 +210,12 @@ export default function Info({ info, color }) {
export async function getServerSideProps(ctx) {
const { id } = ctx.query;
- const API_URI = process.env.API_URI;
+
+ let API_URI;
+ API_URI = process.env.API_URI;
+ if (API_URI.endsWith("/")) {
+ API_URI = API_URI.slice(0, -1);
+ }
let cache;
diff --git a/pages/en/anime/recently-watched.js b/pages/en/anime/recently-watched.js
index c723394..6abf09d 100644
--- a/pages/en/anime/recently-watched.js
+++ b/pages/en/anime/recently-watched.js
@@ -6,12 +6,12 @@ import Skeleton from "react-loading-skeleton";
import Footer from "@/components/shared/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
-import { toast } from "react-toastify";
import { ChevronRightIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import HistoryOptions from "@/components/home/content/historyOptions";
import Head from "next/head";
import MobileNav from "@/components/shared/MobileNav";
+import { toast } from "sonner";
export default function PopularAnime({ sessions }) {
const [data, setData] = useState(null);
@@ -105,11 +105,6 @@ export default function PopularAnime({ sessions }) {
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 {
diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js
index f918f86..a838b7f 100644
--- a/pages/en/anime/watch/[...info].js
+++ b/pages/en/anime/watch/[...info].js
@@ -29,8 +29,12 @@ export async function getServerSideProps(context) {
};
}
- const proxy = process.env.PROXY_URI;
- const disqus = process.env.DISQUS_SHORTNAME || null;
+ let proxy;
+ proxy = process.env.PROXY_URI;
+ if (proxy.endsWith("/")) {
+ proxy = proxy.slice(0, -1);
+ }
+ const disqus = process.env.DISQUS_SHORTNAME;
const [aniId, provider] = query?.info;
const watchId = query?.id;
@@ -114,7 +118,7 @@ export async function getServerSideProps(context) {
epiNumber: epiNumber || null,
dub: dub || null,
userData: userData?.[0] || null,
- info: data.data.Media || null,
+ info: data?.data?.Media || null,
proxy,
disqus,
},
@@ -179,9 +183,10 @@ export default function Watch({
if (episodes) {
const getProvider = episodes?.find((i) => i.providerId === provider);
- const episodeList = dub
- ? getProvider?.episodes?.filter((x) => x.hasDub === true)
- : getProvider?.episodes.slice(0, getMap?.episodes.length);
+ const episodeList = getProvider?.episodes.slice(
+ 0,
+ getMap?.episodes.length
+ );
const playingData = getMap?.episodes.find(
(i) => i.number === Number(epiNumber)
);
@@ -219,6 +224,7 @@ export default function Watch({
return () => {
setEpisodeNavigation(null);
};
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessions?.user?.name, epiNumber, dub]);
useEffect(() => {
@@ -287,6 +293,8 @@ export default function Watch({
});
setMarked(0);
};
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [provider, watchId, info?.id]);
useEffect(() => {
@@ -524,7 +532,7 @@ export default function Watch({
</div>
<div
id="secondary"
- className={`relative ${theaterMode ? "pt-2" : ""}`}
+ className={`relative ${theaterMode ? "pt-5" : "pt-4 lg:pt-0"}`}
>
<EpisodeLists
info={info}
@@ -534,6 +542,7 @@ export default function Watch({
watchId={watchId}
episode={episodesList}
artStorage={artStorage}
+ track={episodeNavigation}
dub={dub}
/>
</div>
diff --git a/pages/en/index.js b/pages/en/index.js
index 9be3c2c..29b0778 100644
--- a/pages/en/index.js
+++ b/pages/en/index.js
@@ -118,23 +118,23 @@ export default function Home({ detail, populars, upComing }) {
}
}, [upComing]);
- useEffect(() => {
- const getSchedule = async () => {
- try {
- const res = await fetch(`/api/v2/etc/schedule`);
- const data = await res.json();
-
- if (!res.ok) {
- setSchedules(null);
- } else {
- setSchedules(data);
- }
- } catch (err) {
- console.log(err);
- }
- };
- getSchedule();
- }, []);
+ // useEffect(() => {
+ // const getSchedule = async () => {
+ // try {
+ // const res = await fetch(`/api/v2/etc/schedule`);
+ // const data = await res.json();
+
+ // if (!res.ok) {
+ // setSchedules(null);
+ // } else {
+ // setSchedules(data);
+ // }
+ // } catch (err) {
+ // console.log(err);
+ // }
+ // };
+ // getSchedule();
+ // }, []);
const [releaseData, setReleaseData] = useState([]);
@@ -290,6 +290,8 @@ export default function Home({ detail, populars, upComing }) {
}
}
userData();
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessions?.user?.name, currentAnime, plan]);
// console.log({ recentAdded });
@@ -402,7 +404,7 @@ export default function Home({ detail, populars, upComing }) {
</div>
)}
- <div className="lg:mt-16 mt-5 flex flex-col gap-5 items-center">
+ <div className="lg:mt-16 mt-5 flex flex-col items-center">
<motion.div
className="w-screen flex-none lg:w-[95%] xl:w-[87%]"
initial={{ opacity: 0 }}
diff --git a/pages/en/manga/[...id].js b/pages/en/manga/[...id].js
new file mode 100644
index 0000000..106bce2
--- /dev/null
+++ b/pages/en/manga/[...id].js
@@ -0,0 +1,425 @@
+import ChapterSelector from "@/components/manga/chapters";
+import Footer from "@/components/shared/footer";
+import Head from "next/head";
+import { useEffect, useState } from "react";
+import { getServerSession } from "next-auth";
+import { authOptions } from "../../api/auth/[...nextauth]";
+import { mediaInfoQuery } from "@/lib/graphql/query";
+import Modal from "@/components/modal";
+import { signIn, useSession } from "next-auth/react";
+import AniList from "@/components/media/aniList";
+import ListEditor from "@/components/listEditor";
+import MobileNav from "@/components/shared/MobileNav";
+import Image from "next/image";
+import DetailTop from "@/components/anime/mobile/topSection";
+import Characters from "@/components/anime/charactersCard";
+import Content from "@/components/home/content";
+import { toast } from "sonner";
+import axios from "axios";
+import getAnifyInfo from "@/lib/anify/info";
+import { redis } from "@/lib/redis";
+import getMangaId from "@/lib/anify/getMangaId";
+
+export default function Manga({ info, anifyData, color, chapterNotFound }) {
+ const [domainUrl, setDomainUrl] = useState("");
+ const { data: session } = useSession();
+
+ const [loading, setLoading] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [statuses, setStatuses] = useState(null);
+ const [watch, setWatch] = useState();
+
+ const [chapter, setChapter] = useState(null);
+
+ const [open, setOpen] = useState(false);
+
+ const rec = info?.recommendations?.nodes?.map(
+ (data) => data.mediaRecommendation
+ );
+
+ useEffect(() => {
+ setDomainUrl(window.location.origin);
+ }, []);
+
+ useEffect(() => {
+ if (chapterNotFound) {
+ toast.error("Chapter not found");
+ const cleanUrl = window.location.origin + window.location.pathname;
+ window.history.replaceState(null, null, cleanUrl);
+ }
+ }, [chapterNotFound]);
+
+ useEffect(() => {
+ async function fetchData() {
+ try {
+ setLoading(true);
+
+ const { data } = await axios.get(`/api/v2/info?id=${anifyData.id}`);
+
+ if (!data.chapters) {
+ setLoading(false);
+ return;
+ }
+
+ setChapter(data);
+ setLoading(false);
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ fetchData();
+
+ return () => {
+ setChapter(null);
+ };
+ }, [info?.id]);
+
+ function handleOpen() {
+ setOpen(true);
+ document.body.style.overflow = "hidden";
+ }
+
+ function handleClose() {
+ setOpen(false);
+ document.body.style.overflow = "auto";
+ }
+
+ return (
+ <>
+ <Head>
+ <title>
+ {info
+ ? `Manga - ${
+ info.title.romaji || info.title.english || info.title.native
+ }`
+ : "Getting Info..."}
+ </title>
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta
+ name="twitter:title"
+ content={`Moopa - ${info.title.romaji || info.title.english}`}
+ />
+ <meta
+ name="twitter:description"
+ content={`${info.description?.slice(0, 180)}...`}
+ />
+ <meta
+ name="twitter:image"
+ content={`${domainUrl}/api/og?title=${
+ info.title.romaji || info.title.english
+ }&image=${info.bannerImage || info.coverImage}`}
+ />
+ <meta
+ name="title"
+ data-title-romaji={info?.title?.romaji}
+ data-title-english={info?.title?.english}
+ data-title-native={info?.title?.native}
+ />
+ </Head>
+ <Modal open={open} onClose={() => handleClose()}>
+ <div>
+ {!session && (
+ <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md">
+ <div className="text-md font-extrabold font-karla">
+ Edit your list
+ </div>
+ <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>
+ )}
+ {session && info && (
+ <ListEditor
+ animeId={info?.id}
+ session={session}
+ stats={statuses?.value}
+ prg={progress}
+ max={info?.episodes}
+ info={info}
+ close={handleClose}
+ />
+ )}
+ </div>
+ </Modal>
+ <MobileNav sessions={session} hideProfile={true} />
+ <main className="w-screen min-h-screen overflow-hidden relative flex flex-col items-center gap-5">
+ {/* <div className="absolute bg-gradient-to-t from-primary from-85% to-100% to-transparent w-screen h-full z-10" /> */}
+ <div className="w-screen absolute">
+ <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[280px] w-screen z-10 inset-0" />
+ {info?.bannerImage && (
+ <Image
+ src={info?.bannerImage}
+ alt="banner anime"
+ height={1000}
+ width={1000}
+ blurDataURL={info?.bannerImage}
+ className="object-cover bg-image blur-[2px] w-screen absolute top-0 left-0 h-[250px] brightness-[55%] z-0"
+ />
+ )}
+ </div>
+ <div className="w-full lg:max-w-screen-lg xl:max-w-screen-2xl z-30 flex flex-col gap-5 pb-10">
+ <DetailTop
+ info={info}
+ session={session}
+ handleOpen={handleOpen}
+ loading={loading}
+ statuses={statuses}
+ watchUrl={watch}
+ progress={progress}
+ color={color}
+ />
+
+ {!loading ? (
+ chapter?.chapters?.length > 0 ? (
+ <ChapterSelector
+ chaptersData={chapter.chapters}
+ mangaId={chapter.id}
+ data={info}
+ setWatch={setWatch}
+ />
+ ) : (
+ <div className="h-[20vh] lg:w-full flex-center flex-col gap-5">
+ <p className="text-center font-karla font-bold lg:text-lg">
+ Oops!<br></br> It looks like this manga is not available.
+ </p>
+ </div>
+ )
+ ) : (
+ <div className="flex justify-center">
+ <div className="lds-ellipsis">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ </div>
+ </div>
+ )}
+
+ {info?.characters?.edges?.length > 0 && (
+ <div className="w-full">
+ <Characters info={info?.characters?.edges} />
+ </div>
+ )}
+
+ {info && rec && rec?.length !== 0 && (
+ <div className="w-full">
+ <Content
+ ids="recommendAnime"
+ section="Recommendations"
+ type="manga"
+ data={rec}
+ />
+ </div>
+ )}
+ </div>
+ </main>
+ <Footer />
+ </>
+ );
+}
+
+export async function getServerSideProps(context) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+ const accessToken = session?.user?.token || null;
+
+ const { chapter } = context.query;
+ const [id1, id2] = context.query.id;
+
+ let cached;
+ let aniId, mangadexId;
+ let info, data, color, chapterNotFound;
+
+ if (String(id1).length > 6) {
+ aniId = id2;
+ mangadexId = id1;
+ } else {
+ aniId = id1;
+ mangadexId = id2;
+ }
+
+ if (chapter) {
+ // create random id string
+ chapterNotFound = Math.random().toString(36).substring(7);
+ }
+
+ if (aniId === "na" && mangadexId) {
+ const datas = await getAnifyInfo(mangadexId);
+
+ aniId =
+ datas.mappings.filter((i) => i.providerId === "anilist")[0]?.id || null;
+
+ if (!aniId) {
+ info = datas;
+ data = datas;
+ color = {
+ backgroundColor: `${"#ffff"}`,
+ color: "#000",
+ };
+ // return {
+ // redirect: {
+ // destination: "/404",
+ // permanent: false,
+ // },
+ // };
+ }
+ } else if (aniId && !mangadexId) {
+ // console.log({ aniId });
+ const response = await fetch("https://graphql.anilist.co/", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
+ },
+ body: JSON.stringify({
+ query: `query ($id: Int, $type: MediaType) {
+ Media (id: $id, type: $type) {
+ id
+ title {
+ romaji
+ english
+ native
+ }
+ }
+ }`,
+ variables: {
+ id: parseInt(aniId),
+ type: "MANGA",
+ },
+ }),
+ });
+ const aniListData = await response.json();
+ const info = aniListData?.data?.Media;
+
+ const mangaId = await getMangaId(
+ info?.title?.romaji,
+ info?.title?.english,
+ info?.title?.native
+ );
+ mangadexId = mangaId?.id;
+
+ if (!mangadexId) {
+ return {
+ redirect: {
+ destination: "/404",
+ permanent: false,
+ },
+ };
+ }
+
+ return {
+ redirect: {
+ destination: `/en/manga/${aniId}/${mangadexId}${
+ chapter ? "?chapter=404" : ""
+ }`,
+ permanent: true,
+ },
+ };
+ } else if (!aniId && mangadexId) {
+ const data = await getAnifyInfo(mangadexId);
+
+ aniId =
+ data.mappings.filter((i) => i.providerId === "anilist")[0]?.id || null;
+
+ if (!aniId) {
+ info = data;
+ // return {
+ // redirect: {
+ // destination: "/404",
+ // permanent: false,
+ // },
+ // };
+ }
+
+ return {
+ redirect: {
+ destination: `/en/manga/${aniId ? aniId : "na"}${`/${mangadexId}`}${
+ chapter ? "?chapter=404" : ""
+ }`,
+ permanent: true,
+ },
+ };
+ } else {
+ const getCached = await redis.get(`mangaPage:${mangadexId}`);
+
+ if (getCached) {
+ cached = JSON.parse(getCached);
+ }
+
+ // let chapters;
+
+ if (cached) {
+ data = cached.data;
+ info = cached.info;
+ color = cached.color;
+ } else {
+ data = await getAnifyInfo(mangadexId);
+
+ const aniListId =
+ data.mappings.filter((i) => i.providerId === "anilist")[0]?.id || null;
+
+ const response = await fetch("https://graphql.anilist.co/", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
+ },
+ body: JSON.stringify({
+ query: mediaInfoQuery,
+ variables: {
+ id: parseInt(aniListId),
+ type: "MANGA",
+ },
+ }),
+ });
+ const aniListData = await response.json();
+ if (aniListData?.data?.Media) info = aniListData?.data?.Media;
+
+ const textColor = setTxtColor(info?.color);
+
+ color = {
+ backgroundColor: `${info?.color || "#ffff"}`,
+ color: textColor,
+ };
+
+ await redis.set(
+ `mangaPage:${mangadexId}`,
+ JSON.stringify({ data, info, color }),
+ "ex",
+ 60 * 60 * 24
+ );
+ }
+ }
+
+ return {
+ props: {
+ info: info || null,
+ anifyData: data || null,
+ chapterNotFound: chapterNotFound || null,
+ color: color || null,
+ },
+ };
+}
+
+function getBrightness(hexColor) {
+ if (!hexColor) {
+ return 200;
+ }
+ const rgb = hexColor
+ .match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i)
+ .slice(1)
+ .map((x) => parseInt(x, 16));
+ return (299 * rgb[0] + 587 * rgb[1] + 114 * rgb[2]) / 1000;
+}
+
+function setTxtColor(hexColor) {
+ const brightness = getBrightness(hexColor);
+ return brightness < 150 ? "#fff" : "#000";
+}
diff --git a/pages/en/manga/[id].js b/pages/en/manga/[id].js
deleted file mode 100644
index 6f25532..0000000
--- a/pages/en/manga/[id].js
+++ /dev/null
@@ -1,146 +0,0 @@
-import ChapterSelector from "@/components/manga/chapters";
-import HamburgerMenu from "@/components/manga/mobile/hamburgerMenu";
-import TopSection from "@/components/manga/info/topSection";
-import Footer from "@/components/shared/footer";
-import Head from "next/head";
-import { useEffect, useState } from "react";
-import { setCookie } from "nookies";
-import { getServerSession } from "next-auth";
-import { authOptions } from "../../api/auth/[...nextauth]";
-import getAnifyInfo from "@/lib/anify/info";
-import { NewNavbar } from "@/components/shared/NavBar";
-
-export default function Manga({ info, userManga }) {
- const [domainUrl, setDomainUrl] = useState("");
- const [firstEp, setFirstEp] = useState();
- const chaptersData = info.chapters.data;
-
- useEffect(() => {
- setDomainUrl(window.location.origin);
- }, []);
-
- return (
- <>
- <Head>
- <title>
- {info
- ? `Manga - ${
- info.title.romaji || info.title.english || info.title.native
- }`
- : "Getting Info..."}
- </title>
- <meta name="twitter:card" content="summary_large_image" />
- <meta
- name="twitter:title"
- content={`Moopa - ${info.title.romaji || info.title.english}`}
- />
- <meta
- name="twitter:description"
- content={`${info.description?.slice(0, 180)}...`}
- />
- <meta
- name="twitter:image"
- content={`${domainUrl}/api/og?title=${
- info.title.romaji || info.title.english
- }&image=${info.bannerImage || info.coverImage}`}
- />
- <meta
- name="title"
- data-title-romaji={info?.title?.romaji}
- data-title-english={info?.title?.english}
- data-title-native={info?.title?.native}
- />
- </Head>
- <div className="min-h-screen w-screen flex flex-col items-center relative">
- <HamburgerMenu />
- <NewNavbar info={info} manga={true} />
- <div className="flex flex-col w-screen items-center gap-5 md:gap-10 py-10 pt-nav">
- <div className="flex-center w-full relative z-30">
- <TopSection info={info} firstEp={firstEp} setCookie={setCookie} />
- <>
- <div className="absolute hidden md:block z-20 bottom-0 h-1/2 w-full bg-secondary" />
- <div className="absolute hidden md:block z-20 top-0 h-1/2 w-full bg-transparent" />
- </>
- </div>
- <div className="w-[90%] xl:w-[70%] min-h-[35vh] z-40">
- {chaptersData.length > 0 ? (
- <ChapterSelector
- chaptersData={chaptersData}
- data={info}
- setFirstEp={setFirstEp}
- setCookie={setCookie}
- userManga={userManga}
- />
- ) : (
- <p>No Chapter Available :(</p>
- )}
- </div>
- </div>
- <Footer />
- </div>
- </>
- );
-}
-
-export async function getServerSideProps(context) {
- const session = await getServerSession(context.req, context.res, authOptions);
- const accessToken = session?.user?.token || null;
-
- const { id } = context.query;
- const key = process.env.API_KEY;
- const data = await getAnifyInfo(id, key);
-
- let userManga = null;
-
- if (session) {
- const response = await fetch("https://graphql.anilist.co/", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
- },
- body: JSON.stringify({
- query: `
- query ($id: Int) {
- Media (id: $id) {
- mediaListEntry {
- status
- progress
- progressVolumes
- status
- }
- id
- idMal
- title {
- romaji
- english
- native
- }
- }
- }
- `,
- variables: {
- id: parseInt(id),
- },
- }),
- });
- const data = await response.json();
- const user = data?.data?.Media?.mediaListEntry;
- if (user) {
- userManga = user;
- }
- }
-
- if (!data?.chapters) {
- return {
- notFound: true,
- };
- }
-
- return {
- props: {
- info: data,
- userManga,
- },
- };
-}
diff --git a/pages/en/manga/read/[...params].js b/pages/en/manga/read/[...params].js
index a7769e2..1076601 100644
--- a/pages/en/manga/read/[...params].js
+++ b/pages/en/manga/read/[...params].js
@@ -10,13 +10,27 @@ import { authOptions } from "../../../api/auth/[...nextauth]";
import BottomBar from "@/components/manga/mobile/bottomBar";
import TopBar from "@/components/manga/mobile/topBar";
import Head from "next/head";
-import nookies from "nookies";
import ShortCutModal from "@/components/manga/modals/shortcutModal";
import ChapterModal from "@/components/manga/modals/chapterModal";
-import getAnifyPage from "@/lib/anify/page";
+// import getConsumetPages from "@/lib/consumet/manga/getPage";
+import { mediaInfoQuery } from "@/lib/graphql/query";
+// import { redis } from "@/lib/redis";
+// import getConsumetChapters from "@/lib/consumet/manga/getChapters";
+import { toast } from "sonner";
+import axios from "axios";
+import { redis } from "@/lib/redis";
+import getAnifyInfo from "@/lib/anify/info";
-export default function Read({ data, currentId, sessions }) {
- const [info, setInfo] = useState();
+export default function Read({
+ data,
+ info,
+ chaptersData,
+ currentId,
+ sessions,
+ provider,
+ mangaDexId,
+ number,
+}) {
const [chapter, setChapter] = useState([]);
const [layout, setLayout] = useState(1);
@@ -30,8 +44,8 @@ export default function Read({ data, currentId, sessions }) {
const [paddingX, setPaddingX] = useState(208);
const [scaleImg, setScaleImg] = useState(1);
- const [nextChapterId, setNextChapterId] = useState(null);
- const [prevChapterId, setPrevChapterId] = useState(null);
+ const [nextChapter, setNextChapter] = useState(null);
+ const [prevChapter, setPrevChapter] = useState(null);
const [currentChapter, setCurrentChapter] = useState(null);
const [currentPage, setCurrentPage] = useState(0);
@@ -40,17 +54,22 @@ export default function Read({ data, currentId, sessions }) {
const router = useRouter();
+ // console.log({ info });
+
useEffect(() => {
- hasRun.current = false;
- }, [currentId]);
+ toast.message("This page is still under development", {
+ description: "If you found any bugs, please report it to us!",
+ position: "top-center",
+ duration: 10000,
+ });
+ }, []);
useEffect(() => {
- const get = JSON.parse(localStorage.getItem("manga"));
- const chapters = get.manga;
+ hasRun.current = false;
+ const chapters = chaptersData.find((x) => x.providerId === provider);
const currentChapter = chapters.chapters?.find((x) => x.id === currentId);
setCurrentChapter(currentChapter);
- setInfo(get.data);
setChapter(chapters);
if (Array.isArray(chapters?.chapters)) {
@@ -60,25 +79,36 @@ export default function Read({ data, currentId, sessions }) {
if (currentIndex !== -1) {
const nextChapter = chapters.chapters[currentIndex - 1];
const prevChapter = chapters.chapters[currentIndex + 1];
- setNextChapterId(nextChapter ? nextChapter.id : null);
- setPrevChapterId(prevChapter ? prevChapter.id : null);
+ setNextChapter(nextChapter ? nextChapter : null);
+ setPrevChapter(prevChapter ? prevChapter : null);
}
}
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentId]);
useEffect(() => {
const handleKeyDown = (event) => {
- if (event.key === "ArrowRight" && event.ctrlKey && nextChapterId) {
+ event.preventDefault();
+ if (event.key === "ArrowRight" && event.ctrlKey && nextChapter?.id) {
router.push(
- `/en/manga/read/${chapter.providerId}?id=${
- info.id
- }&chapterId=${encodeURIComponent(nextChapterId)}`
+ `/en/manga/read/${
+ chapter.providerId
+ }?id=${mangaDexId}&chapterId=${encodeURIComponent(nextChapter?.id)}${
+ info?.id?.length > 6 ? "" : `&anilist=${info?.id}`
+ }&num=${nextChapter?.number}`
);
- } else if (event.key === "ArrowLeft" && event.ctrlKey && prevChapterId) {
+ } else if (
+ event.key === "ArrowLeft" &&
+ event.ctrlKey &&
+ prevChapter?.id
+ ) {
router.push(
- `/en/manga/read/${chapter.providerId}?id=${
- info.id
- }&chapterId=${encodeURIComponent(prevChapterId)}`
+ `/en/manga/read/${
+ chapter.providerId
+ }?id=${mangaDexId}&chapterId=${encodeURIComponent(prevChapter?.id)}${
+ info?.id?.length > 6 ? "" : `&anilist=${info?.id}`
+ }&num=${prevChapter?.number}`
);
}
if (event.code === "Slash" && event.ctrlKey) {
@@ -99,7 +129,9 @@ export default function Read({ data, currentId, sessions }) {
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
- }, [nextChapterId, prevChapterId, visible, isKeyOpen, paddingX]);
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [nextChapter?.id, prevChapter?.id, visible, isKeyOpen, paddingX]);
return (
<>
@@ -134,13 +166,15 @@ export default function Read({ data, currentId, sessions }) {
<TopBar info={info} />
<BottomBar
id={info?.id}
- prevChapter={prevChapterId}
- nextChapter={nextChapterId}
+ prevChapter={prevChapter}
+ nextChapter={nextChapter}
currentPage={currentPage}
chapter={chapter}
- page={data}
+ data={data}
setSeekPage={setSeekPage}
setIsOpen={setIsChapOpen}
+ number={number}
+ mangadexId={mangaDexId}
/>
</>
)}
@@ -149,13 +183,17 @@ export default function Read({ data, currentId, sessions }) {
data={chapter}
page={data}
info={info}
+ number={number}
+ mediaId={mangaDexId}
currentId={currentId}
setSeekPage={setSeekPage}
+ providerId={provider}
/>
)}
{layout === 1 && (
<FirstPanel
aniId={info?.id}
+ providerId={provider}
data={data}
hasRun={hasRun}
currentId={currentId}
@@ -164,19 +202,22 @@ export default function Read({ data, currentId, sessions }) {
visible={visible}
setVisible={setVisible}
chapter={chapter}
- nextChapter={nextChapterId}
- prevChapter={prevChapterId}
+ nextChapter={nextChapter}
+ prevChapter={prevChapter}
paddingX={paddingX}
session={sessions}
mobileVisible={mobileVisible}
setMobileVisible={setMobileVisible}
setCurrentPage={setCurrentPage}
+ mangadexId={mangaDexId}
+ number={number}
/>
)}
{layout === 2 && (
<SecondPanel
aniId={info?.id}
data={data}
+ chapterData={chapter}
hasRun={hasRun}
currentChapter={currentChapter}
currentId={currentId}
@@ -185,12 +226,14 @@ export default function Read({ data, currentId, sessions }) {
visible={visible}
setVisible={setVisible}
session={sessions}
+ providerId={provider}
/>
)}
{layout === 3 && (
<ThirdPanel
aniId={info?.id}
data={data}
+ chapterData={chapter}
hasRun={hasRun}
currentId={currentId}
currentChapter={currentChapter}
@@ -202,6 +245,7 @@ export default function Read({ data, currentId, sessions }) {
scaleImg={scaleImg}
setMobileVisible={setMobileVisible}
mobileVisible={mobileVisible}
+ providerId={provider}
/>
)}
{visible && (
@@ -224,42 +268,130 @@ export default function Read({ data, currentId, sessions }) {
)}
</div>
</>
+ // <p></p>
);
}
-export async function getServerSideProps(context) {
- const cookies = nookies.get(context);
+async function fetchAnifyPages(id, number, provider, readId, key) {
+ try {
+ let cached;
+ cached = await redis.get(`pages:${readId}`);
+
+ if (cached) {
+ return JSON.parse(cached);
+ }
+
+ const url = `https://api.anify.tv/pages?id=${id}&chapterNumber=${number}&providerId=${provider}&readId=${encodeURIComponent(
+ readId
+ )}`;
+
+ const { data } = await axios.get(url);
+
+ if (!data) {
+ return null;
+ }
+
+ await redis.set(
+ `pages:${readId}`,
+ JSON.stringify(data),
+ "EX",
+ 60 * 60 * 24 * 7
+ );
+
+ return data;
+ } catch (error) {
+ return { error: "Error fetching data" };
+ }
+}
+
+export async function getServerSideProps(context) {
const key = process.env.API_KEY;
const query = context.query;
const providerId = query.params[0];
const chapterId = query.chapterId;
const mediaId = query.id;
+ const number = query.num;
+ const anilistId = query.anilist;
+
+ const session = await getServerSession(context.req, context.res, authOptions);
+ const accessToken = session?.user?.token || null;
+
+ // const data = await getConsumetPages(mediaId, providerId, chapterId, key);
+ // const chapters = await getConsumetChapters(mediaId, redis);
+
+ const dataManga = await fetchAnifyPages(
+ mediaId,
+ number,
+ providerId,
+ chapterId,
+ mediaId,
+ key
+ );
+
+ let info;
- if (!cookies.manga || cookies.manga !== mediaId) {
+ if (anilistId) {
+ const response = await fetch("https://graphql.anilist.co/", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
+ },
+ body: JSON.stringify({
+ query: mediaInfoQuery,
+ variables: {
+ id: parseInt(anilistId),
+ type: "MANGA",
+ },
+ }),
+ });
+ const json = await response.json();
+ info = json?.data?.Media;
+ } else {
+ const datas = await getAnifyInfo(mediaId);
+ if (datas) {
+ info = datas;
+ }
+ }
+
+ const chapters = await (
+ await fetch("https://api.anify.tv/chapters/" + mediaId + "?apikey=" + key)
+ ).json();
+
+ if ((dataManga && dataManga?.error) || dataManga?.length === 0) {
return {
redirect: {
- destination: `/en/manga/${mediaId}`,
+ destination: `/en/manga/${anilistId}?chapter=404`,
},
};
}
- const session = await getServerSession(context.req, context.res, authOptions);
-
- const data = await getAnifyPage(mediaId, providerId, chapterId, key);
+ /*
+ const { data } = await axios.get(
+ `https://beta.moopa.live/api/v2/info/${romaji}${
+ english ? `/${english}` : ""
+ }${native ? `/${native}` : ""}?id=${anilistId}`
+ );
if (data.error) {
return {
notFound: true,
};
}
+ */
return {
props: {
- data: data,
+ data: dataManga,
+ mangaDexId: mediaId,
+ info: info,
+ number: number,
+ chaptersData: chapters,
currentId: chapterId,
sessions: session,
+ provider: providerId,
},
};
}
diff --git a/pages/en/profile/[user].js b/pages/en/profile/[user].js
index b931597..7ef5de3 100644
--- a/pages/en/profile/[user].js
+++ b/pages/en/profile/[user].js
@@ -5,8 +5,8 @@ import Link from "next/link";
import Head from "next/head";
import { useEffect, useState } from "react";
import { getUser } from "@/prisma/user";
-import { toast } from "react-toastify";
import { NewNavbar } from "@/components/shared/NavBar";
+import { toast } from "sonner";
export default function MyList({ media, sessions, user, time, userSettings }) {
const [listFilter, setListFilter] = useState("all");
diff --git a/pages/en/search/[...param].js b/pages/en/search/[...param].js
index 603cd17..2cb609f 100644
--- a/pages/en/search/[...param].js
+++ b/pages/en/search/[...param].js
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
-import { AnimatePresence, motion as m } from "framer-motion";
+import { motion as m } from "framer-motion";
import Skeleton from "react-loading-skeleton";
import { useRouter } from "next/router";
import Link from "next/link";
@@ -25,6 +25,8 @@ import { Cog6ToothIcon, TrashIcon } from "@heroicons/react/20/solid";
import useDebounce from "@/lib/hooks/useDebounce";
import { NewNavbar } from "@/components/shared/NavBar";
import MobileNav from "@/components/shared/MobileNav";
+import SearchByImage from "@/components/search/searchByImage";
+import { PlayIcon } from "@heroicons/react/24/outline";
export async function getServerSideProps(context) {
const { param } = context.query;
@@ -91,9 +93,10 @@ export default function Card({
}) {
const inputRef = useRef(null);
const router = useRouter();
- // const { data: session } = useSession();
const [data, setData] = useState();
+ const [imageSearch, setImageSearch] = useState();
+
const [loading, setLoading] = useState(true);
const [search, setQuery] = useState(query);
@@ -125,16 +128,18 @@ export default function Card({
});
if (data?.media?.length === 0) {
setNextPage(false);
+ setLoading(false);
} else if (data !== null && page > 1) {
setData((prevData) => {
return [...(prevData ?? []), ...data?.media];
});
setNextPage(data?.pageInfo.hasNextPage);
+ setLoading(false);
} else {
setData(data?.media);
+ setNextPage(data?.pageInfo.hasNextPage);
+ setLoading(false);
}
- setNextPage(data?.pageInfo.hasNextPage);
- setLoading(false);
}
useEffect(() => {
@@ -142,6 +147,7 @@ export default function Card({
setPage(1);
setNextPage(true);
advance();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [
debounceSearch,
type?.value,
@@ -153,11 +159,17 @@ export default function Card({
]);
useEffect(() => {
+ if (imageSearch) return;
advance();
- }, [page]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [page, imageSearch]);
useEffect(() => {
function handleScroll() {
+ if (imageSearch) {
+ window.removeEventListener("scroll", handleScroll);
+ return;
+ }
if (page > 10 || !nextPage) {
window.removeEventListener("scroll", handleScroll);
return;
@@ -174,7 +186,7 @@ export default function Card({
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
- }, [page, nextPage]);
+ }, [page, nextPage, imageSearch]);
const handleKeyDown = async (event) => {
if (event.key === "Enter") {
@@ -189,6 +201,7 @@ export default function Card({
};
function trash() {
+ setImageSearch();
setQuery();
setGenre();
setFormat();
@@ -202,6 +215,18 @@ export default function Card({
setIsVisible(!isVisible);
}
+ const handleVideoHover = (hovered, id) => {
+ const updatedImageSearch = imageSearch?.map((item) => {
+ if (item.filename === id) {
+ return { ...item, hovered };
+ }
+ return item;
+ });
+ setImageSearch(updatedImageSearch);
+ };
+
+ // console.log({ loading, data });
+
return (
<>
<Head>
@@ -290,6 +315,7 @@ export default function Card({
>
<Cog6ToothIcon className="w-5 h-5" />
</div>
+ <SearchByImage setMedia={setData} setData={setImageSearch} />
<div
className="py-2 px-2 bg-secondary rounded flex justify-center items-center cursor-pointer hover:bg-opacity-75 transition-all duration-100 group"
onClick={trash}
@@ -343,91 +369,200 @@ export default function Card({
)}
{/* <div> */}
<div className="flex flex-col gap-14 items-center z-30">
- <AnimatePresence>
- <div
- key="card-keys"
- className="grid pt-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-6 justify-items-center grid-cols-2 xxs:grid-cols-3 w-screen px-2 xl:w-auto xl:gap-10 gap-2 xl:gap-y-24 gap-y-12 overflow-hidden"
- >
- {loading
- ? ""
- : !data?.length && (
- <div className="w-screen text-[#ff7f57] xl:col-start-3 col-start-2 items-center flex justify-center text-center font-bold font-karla xl:text-2xl">
- Oops!<br></br> Nothing's Found...
+ <div
+ key="card-keys"
+ className={`${
+ imageSearch ? "hidden" : ""
+ } grid pt-3 px-5 xl:px-0 xxs:grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-6 justify-items-center grid-cols-2 w-screen xl:w-auto xl:gap-7 gap-5 gap-y-10`}
+ >
+ {loading
+ ? ""
+ : !data && (
+ <div className="w-full text-[#ff7f57] col-span-6 items-center flex justify-center text-center font-bold font-karla xl:text-2xl">
+ Oops!<br></br> Nothing's Found...
+ </div>
+ )}
+
+ {data &&
+ data?.length > 0 &&
+ !imageSearch &&
+ data?.map((anime, index) => {
+ const anilistId = anime?.mappings?.find(
+ (x) => x.providerId === "anilist"
+ )?.id;
+ return (
+ <m.div
+ initial={{ scale: 0.98 }}
+ animate={{ scale: 1, transition: { duration: 0.35 } }}
+ className="w-full"
+ key={index}
+ >
+ <Link
+ href={
+ anime.format === "MANGA" || anime.format === "NOVEL"
+ ? `/en/manga/${
+ anilistId ? anilistId : ""
+ }${`/${anime.id}`}`
+ : `/en/anime/${anime.id}`
+ }
+ title={anime.title.userPreferred}
+ className="block relative overflow-hidden bg-secondary hover:scale-[1.03] scale-100 transition-all cursor-pointer duration-200 ease-out rounded"
+ style={{
+ paddingTop: "145%", // 2:3 aspect ratio (3/2 * 100%)
+ }}
+ >
+ <Image
+ className="object-cover"
+ src={anime.coverImage.extraLarge}
+ alt={anime.title.userPreferred}
+ sizes="(min-width: 808px) 50vw, 100vw"
+ quality={100}
+ fill
+ />
+ </Link>
+ <Link
+ href={
+ anime.format === "MANGA" || anime.format === "NOVEL"
+ ? `/en/manga/${
+ anilistId ? anilistId : ""
+ }${`/${anime.id}`}`
+ : `/en/anime/${anime.id}`
+ }
+ title={anime.title.userPreferred}
+ >
+ <h1 className="font-outfit font-bold xl:text-base text-[15px] pt-4 line-clamp-2">
+ {anime.status === "RELEASING" ? (
+ <span className="dots bg-green-500" />
+ ) : anime.status === "NOT_YET_RELEASED" ? (
+ <span className="dots bg-red-500" />
+ ) : null}
+ {anime.title.userPreferred}
+ </h1>
+ </Link>
+ <h2 className="font-outfit xl:text-[15px] text-[11px] font-light pt-2 text-[#8B8B8B]">
+ {anime.format || <p>-</p>} &#183;{" "}
+ {anime.status || <p>-</p>} &#183;{" "}
+ {anime.episodes
+ ? `${anime.episodes || "N/A"} Episodes`
+ : `${anime.chapters || "N/A"} Chapters`}
+ </h2>
+ </m.div>
+ );
+ })}
+
+ {loading && (
+ <>
+ {[1, 2, 4, 5, 6, 7, 8].map((item) => (
+ <div className="w-full" key={item}>
+ <div className="w-full">
+ <Skeleton
+ className="w-full rounded"
+ style={{
+ paddingTop: "140%", // 2:3 aspect ratio (3/2 * 100%)
+ width: "(min-width: 808px) 50vw, 100vw",
+ lineHeight: 1,
+ }}
+ />
</div>
- )}
- {data &&
- data?.map((anime, index) => {
- return (
- <m.div
- initial={{ scale: 0.9 }}
- animate={{ scale: 1, transition: { duration: 0.35 } }}
- className="w-[146px] xxs:w-[115px] xs:w-[135px] xl:w-[185px]"
- key={index}
+ <div>
+ <h1 className="font-outfit w-[320px] font-bold xl:text-base text-[15px] pt-4 line-clamp-2">
+ <Skeleton width={120} height={26} />
+ </h1>
+ </div>
+ </div>
+ ))}
+ </>
+ )}
+ </div>
+
+ {imageSearch && (
+ <div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 gap-3 md:gap-7 px-5 lg:px-0">
+ {imageSearch.map((a, index) => {
+ return (
+ <m.div
+ key={index}
+ initial={{ scale: 0.9 }}
+ animate={{ scale: 1, transition: { duration: 0.35 } }}
+ className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item"
+ >
+ <Link
+ className="relative aspect-video rounded-md overflow-hidden group"
+ href={`/en/anime/${a.anilist.id}`}
+ onMouseEnter={() => {
+ handleVideoHover(true, a.filename);
+ }}
+ onMouseLeave={() => handleVideoHover(false, a.filename)}
>
- <Link
- href={
- anime.format === "MANGA" || anime.format === "NOVEL"
- ? `/en/manga/${anime.id}`
- : `/en/anime/${anime.id}`
- }
- title={anime.title.userPreferred}
- >
+ <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" />
+ <h1
+ className="font-semibold font-karla line-clamp-1"
+ title={a?.anilist.title.romaji}
+ >
+ {`Episode ${a.episode}`}
+ </h1>
+ </div>
+
+ {a?.image && (
<Image
- className="object-cover bg-[#3B3C41] w-[146px] h-[208px] xxs:w-[115px] xxs:h-[163px] xs:w-[135px] xs:h-[192px] xl:w-[185px] xl:h-[265px] hover:scale-105 scale-100 transition-all cursor-pointer duration-200 ease-out rounded-[10px]"
- src={anime.coverImage.extraLarge}
- alt={anime.title.userPreferred}
- width={500}
- height={500}
+ src={a?.image}
+ width={200}
+ height={200}
+ alt="Episode Thumbnail"
+ className={`w-full object-cover group-hover:scale-[1.02] duration-300 ease-out z-10 ${
+ !a.hovered ? "visible" : "hidden"
+ }`}
/>
- </Link>
- <Link
- href={`/en/anime/${anime.id}`}
- title={anime.title.userPreferred}
- >
- <h1 className="font-outfit font-bold xl:text-base text-[15px] pt-4 line-clamp-2">
- {anime.status === "RELEASING" ? (
- <span className="dots bg-green-500" />
- ) : anime.status === "NOT_YET_RELEASED" ? (
- <span className="dots bg-red-500" />
- ) : null}
- {anime.title.userPreferred}
- </h1>
- </Link>
- <h2 className="font-outfit xl:text-[15px] text-[11px] font-light pt-2 text-[#8B8B8B]">
- {anime.format || <p>-</p>} &#183;{" "}
- {anime.status || <p>-</p>} &#183;{" "}
- {anime.episodes
- ? `${anime.episodes || "N/A"} Episodes`
- : `${anime.chapters || "N/A"} Chapters`}
- </h2>
- </m.div>
- );
- })}
-
- {loading && (
- <>
- {[1, 2, 4, 5, 6, 7, 8].map((item) => (
- <div
- key={item}
- className="flex flex-col w-[135px] xl:w-[185px] gap-5"
- style={{ scale: 0.98 }}
+ )}
+ {a?.video && (
+ <video
+ src={a.video}
+ className={`w-full object-cover group-hover:scale-[1.02] duration-300 ease-out z-10 ${
+ a.hovered ? "visible" : "hidden"
+ }`}
+ autoPlay
+ muted
+ loop
+ playsInline
+ />
+ )}
+ </Link>
+
+ <Link
+ className="flex flex-col font-karla w-full"
+ href={`/en/anime/${a.anilist.id}`}
>
- <Skeleton className="h-[192px] w-[135px] xl:h-[265px] xl:w-[185px]" />
- <Skeleton width={110} height={30} />
- </div>
- ))}
- </>
- )}
+ {/* <h1 className="font-semibold">{a.title}</h1> */}
+ <p className="flex items-center gap-1 text-sm text-gray-400 w-[320px]">
+ <span
+ className="text-white max-w-[120px] md:max-w-[200px] lg:max-w-[220px]"
+ style={{
+ display: "inline-block",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ }}
+ title={a?.anilist.title.romaji}
+ >
+ {a?.anilist.title.romaji}
+ </span>{" "}
+ | Episode {a.episode}
+ </p>
+ </Link>
+ </m.div>
+ );
+ })}
</div>
- {!loading && page > 10 && nextPage && (
- <button
- onClick={() => setPage((p) => p + 1)}
- className="bg-secondary xl:w-[30%] w-[80%] h-10 rounded-md"
- >
- Load More
- </button>
- )}
- </AnimatePresence>
+ )}
+ {!loading && page > 10 && nextPage && (
+ <button
+ onClick={() => setPage((p) => p + 1)}
+ className="bg-secondary xl:w-[30%] w-[80%] h-10 rounded-md"
+ >
+ Load More
+ </button>
+ )}
</div>
{/* </div> */}
</div>
diff --git a/release.md b/release.md
index aeb900c..cfb7bd1 100644
--- a/release.md
+++ b/release.md
@@ -2,9 +2,24 @@
This document contains a summary of all significant changes made to this release.
-## 🎉 Update v4.1.3
+## 🎉 Update v4.2.0
+
+### Added
+
+- Added scene search for anime
+- Added next episode button on watch page
+- Added episode selector on watch page
+- Added dub gogoanime from consumet
### Fixed
-- Resolved issue with seek button not working
-- Improved homepage and watchpage responsiveness
+- Greatly improved search ui/ux
+- Fixed when using search palette it focused on other button instead of search input
+- Resolved issue: home button on error page doesn't work
+- Resolved issue: website showing error when user pressing `pages` button on reader page
+
+### Changed
+
+- Searching manga now using Anify instead of AniList
+- Info page for Manga now has a similar UI as Anime making it more consistent
+- API Key isn't needed anymore
diff --git a/styles/globals.css b/styles/globals.css
index 256a4f5..17ca472 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -443,6 +443,13 @@ pre code {
}
}
+/* create media queries for mobile */
+/* @media (max-width: 768px) {
+ .theater {
+ display: none;
+ }
+} */
+
/* Hide the default checkbox */
.containers input {
position: absolute;
diff --git a/utils/appendMetaToEpisodes.js b/utils/appendMetaToEpisodes.js
index eedcbf5..197788b 100644
--- a/utils/appendMetaToEpisodes.js
+++ b/utils/appendMetaToEpisodes.js
@@ -2,7 +2,7 @@ async function appendMetaToEpisodes(episodesData, images) {
// Create a dictionary for faster lookup of images based on episode number
const episodeImages = {};
images.forEach((image) => {
- episodeImages[image.episode] = image;
+ episodeImages[image.number || image.episode] = image;
});
// Iterate through each provider's episodes data
diff --git a/utils/getRedisWithPrefix.js b/utils/getRedisWithPrefix.js
index 31a466d..b85589b 100644
--- a/utils/getRedisWithPrefix.js
+++ b/utils/getRedisWithPrefix.js
@@ -63,6 +63,19 @@ export async function getValuesWithNumericKeys() {
return values;
}
+export async function getKeysWithNumericKeys() {
+ const allKeys = await redis.keys("*"); // Fetch all keys in Redis
+ const numericKeys = allKeys.filter((key) => /^\d+$/.test(key)); // Filter keys that contain only numbers
+
+ const values = [];
+
+ for (const key of numericKeys) {
+ const value = await redis.del(key);
+ }
+
+ return values;
+}
+
export async function countNumericKeys() {
const allKeys = await redis.keys("*"); // Fetch all keys in Redis
const numericKeys = allKeys.filter((key) => /^\d+$/.test(key)); // Filter keys that contain only numbers
diff --git a/utils/getTimes.js b/utils/getTimes.js
index d06f797..491d139 100644
--- a/utils/getTimes.js
+++ b/utils/getTimes.js
@@ -132,3 +132,10 @@ export function unixTimestampToRelativeTime(unixTimestamp) {
return "just now";
}
+
+export function unixToSeconds(unixTimestamp) {
+ const now = Math.floor(Date.now() / 1000); // Current Unix timestamp in seconds
+ const secondsAgo = now - unixTimestamp;
+
+ return secondsAgo;
+}
diff --git a/utils/imageUtils.js b/utils/imageUtils.js
new file mode 100644
index 0000000..e5ac6a9
--- /dev/null
+++ b/utils/imageUtils.js
@@ -0,0 +1,22 @@
+export function getHeaders(providerId) {
+ switch (providerId) {
+ case "mangahere":
+ return { Referer: "https://mangahere.org" };
+ case "mangadex":
+ return { Referer: "https://mangadex.org" };
+ case "mangakakalot":
+ return { Referer: "https://mangakakalot.com" };
+ case "mangapill":
+ return { Referer: "https://mangapill.com" };
+ case "mangasee123":
+ return { Referer: "https://mangasee123.com" };
+ case "comick":
+ return { Referer: "https://comick.app" };
+ default:
+ return null;
+ }
+}
+
+export function getRandomId() {
+ return Math.random().toString(36).substr(2, 9);
+}