mirror of
https://github.com/Radiquum/YAMPD.git
synced 2025-05-20 15:49:34 +05:00
470 lines
13 KiB
TypeScript
470 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
DOWNLOAD_ENDPOINT,
|
|
MOD_ENDPOINT,
|
|
PACK_ENDPOINT,
|
|
PACKS_ENDPOINT,
|
|
} from "@/api/ENDPOINTS";
|
|
import { Pack } from "@/types/pack";
|
|
import {
|
|
Button,
|
|
Card,
|
|
Label,
|
|
Progress,
|
|
Spinner,
|
|
TextInput,
|
|
} from "flowbite-react";
|
|
import { useSearchParams } from "next/navigation";
|
|
import { useRouter } from "next/navigation";
|
|
import { useEffect, useState } from "react";
|
|
import { Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react";
|
|
|
|
import { HiDownload, HiPlusCircle, HiTrash } from "react-icons/hi";
|
|
import { ModTable } from "../components/ModTable";
|
|
import { toast } from "react-toastify";
|
|
|
|
import { io } from "socket.io-client";
|
|
const socketURL = process.env.NEXT_PUBLIC_SOCKET_URL
|
|
? process.env.NEXT_PUBLIC_SOCKET_URL
|
|
: undefined;
|
|
const socket = io(socketURL);
|
|
|
|
export default function PackPage() {
|
|
const [packData, setPackData] = useState<Pack | null>(null);
|
|
const [packDataLoading, setPackDataLoading] = useState(true);
|
|
const router = useRouter();
|
|
const id = useSearchParams().get("id") || "";
|
|
|
|
const [addModModalOpen, setAddModModalOpen] = useState(false);
|
|
const [modUrl, setModUrl] = useState("");
|
|
|
|
const [downloadModalOpen, setdownloadModalOpen] = useState(false);
|
|
const [downloadProgressFile, setDownloadProgressFile] = useState({
|
|
filename: "filename",
|
|
total: 1,
|
|
current: 1,
|
|
});
|
|
const [downloadProgressTotal, setDownloadProgressTotal] = useState(100);
|
|
const [downloadProgressCurrent, setDownloadProgressCurrent] = useState(100);
|
|
|
|
async function _getPacksData() {
|
|
const res = await fetch(PACK_ENDPOINT("getPack", id));
|
|
if (!res.ok) router.push("/404");
|
|
const data: Pack = await res.json();
|
|
if (data._id) {
|
|
setPackData(data);
|
|
setPackDataLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (id) {
|
|
_getPacksData();
|
|
} else {
|
|
router.push("/404");
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
function percentage(partialValue: number, totalValue: number) {
|
|
const flo = (100 * partialValue) / totalValue;
|
|
return Number(flo.toFixed(2));
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function onDownloadCurrent(data: any) {
|
|
switch (data.status) {
|
|
case "pending":
|
|
const progress = percentage(data.download_bytes, data.total_bytes);
|
|
setDownloadProgressCurrent(progress);
|
|
break;
|
|
case "ok":
|
|
setDownloadProgressCurrent(0);
|
|
break;
|
|
case "error":
|
|
toast.error(data.message, {
|
|
autoClose: 2500,
|
|
closeOnClick: true,
|
|
draggable: true,
|
|
});
|
|
break;
|
|
default:
|
|
console.log(`GET UNKNOWN WS STATUS: ${data.status}`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function onDownloadTotal(data: any) {
|
|
const progress = percentage(data.current, data.total);
|
|
setDownloadProgressTotal(progress);
|
|
setDownloadProgressFile({
|
|
filename: data.filename,
|
|
total: data.total,
|
|
current: data.current,
|
|
});
|
|
}
|
|
|
|
socket.on("download_current", (data) => onDownloadCurrent(data));
|
|
socket.on("download_total", (data) => onDownloadTotal(data));
|
|
|
|
return () => {
|
|
socket.off("download_current", (data) => onDownloadCurrent(data));
|
|
socket.off("download_total", (data) => onDownloadTotal(data));
|
|
};
|
|
}, []);
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const handleImageUpdate = (e: any) => {
|
|
if (!packData || !window) return;
|
|
|
|
const file = e.target.files[0];
|
|
const fileReader = new FileReader();
|
|
fileReader.onloadend = () => {
|
|
const content = fileReader.result;
|
|
fetch(`${PACK_ENDPOINT("editPackImage", packData._id)}`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
image: content,
|
|
mimetype: file.type,
|
|
}),
|
|
headers: {
|
|
"content-type": "application/json",
|
|
accept: "application/json",
|
|
},
|
|
}).then(() => location.reload());
|
|
e.target.value = "";
|
|
};
|
|
fileReader.readAsDataURL(file);
|
|
};
|
|
|
|
function deletePack() {
|
|
if (!packData || !window) return;
|
|
|
|
if (window.confirm(`Delete pack ${packData.title}?`)) {
|
|
fetch(`${PACKS_ENDPOINT("deletePack", packData._id)}`);
|
|
const ur = new URL(window.location.href);
|
|
ur.searchParams.delete("id");
|
|
ur.pathname = "/";
|
|
window.location.href = ur.href;
|
|
}
|
|
}
|
|
|
|
async function addMod() {
|
|
let slug = null;
|
|
let version = null;
|
|
|
|
if (!modUrl) {
|
|
toast.error("Mod url is required", {
|
|
autoClose: 2500,
|
|
closeOnClick: true,
|
|
draggable: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
let source = null;
|
|
|
|
if (modUrl.includes("modrinth.com")) {
|
|
source = "Modrinth";
|
|
} else if (modUrl.includes("curseforge.com")) {
|
|
source = "CurseForge";
|
|
} else {
|
|
toast.error("unknown source", {
|
|
autoClose: 2500,
|
|
closeOnClick: true,
|
|
draggable: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
switch (source) {
|
|
case "Modrinth":
|
|
const _tmp = modUrl.split("/mod/");
|
|
if (_tmp.length == 1) {
|
|
toast.error("invalid Modrinth url", {
|
|
autoClose: 2500,
|
|
closeOnClick: true,
|
|
draggable: true,
|
|
});
|
|
return;
|
|
}
|
|
const _tmp2 = _tmp[1].split("/version/");
|
|
slug = _tmp2[0];
|
|
if (_tmp2.length > 1) {
|
|
version = _tmp2[1];
|
|
}
|
|
break;
|
|
case "CurseForge":
|
|
const _tmp3 = modUrl.split("/mc-mods/");
|
|
if (_tmp3.length == 1) {
|
|
toast.error("invalid CurseForge url", {
|
|
autoClose: 2500,
|
|
closeOnClick: true,
|
|
draggable: true,
|
|
});
|
|
return;
|
|
}
|
|
const _tmp4 = _tmp3[1].split("/files/");
|
|
slug = _tmp4[0];
|
|
if (_tmp4.length > 1) {
|
|
version = _tmp4[1];
|
|
}
|
|
break;
|
|
default:
|
|
toast.error("failed to parse url", {
|
|
autoClose: 2500,
|
|
closeOnClick: true,
|
|
draggable: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
slug = slug.replace("/", "");
|
|
version = version ? version.replace("/", "") : null;
|
|
|
|
if (!packData) return;
|
|
const tid = toast.loading(`Adding mod`);
|
|
const res = await fetch(MOD_ENDPOINT("addMod", packData._id), {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
slug,
|
|
version,
|
|
source,
|
|
}),
|
|
headers: {
|
|
"content-type": "application/json",
|
|
accept: "application/json",
|
|
},
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.status != "ok") {
|
|
toast.update(tid, {
|
|
render: data.message,
|
|
type: "error",
|
|
isLoading: false,
|
|
autoClose: 2500,
|
|
closeOnClick: true,
|
|
draggable: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
toast.update(tid, {
|
|
render: data.message,
|
|
type: "success",
|
|
isLoading: false,
|
|
autoClose: 2500,
|
|
closeOnClick: true,
|
|
draggable: true,
|
|
});
|
|
// setModUrl({ ...modUrl, value: "" });
|
|
|
|
_getPacksData();
|
|
}
|
|
|
|
async function downloadPack() {
|
|
if (!packData) return;
|
|
|
|
fetch(`${DOWNLOAD_ENDPOINT["downloadPack"]}`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
pack_id: packData._id,
|
|
}),
|
|
headers: {
|
|
"content-type": "application/json",
|
|
accept: "application/json",
|
|
},
|
|
});
|
|
|
|
setdownloadModalOpen(true);
|
|
}
|
|
|
|
async function downloadMods(mods: string[]) {
|
|
if (!packData) return;
|
|
|
|
fetch(`${DOWNLOAD_ENDPOINT["downloadMods"]}`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
pack_id: packData._id,
|
|
mods: mods,
|
|
}),
|
|
headers: {
|
|
"content-type": "application/json",
|
|
accept: "application/json",
|
|
},
|
|
});
|
|
|
|
setdownloadModalOpen(true);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{packDataLoading && (
|
|
<div className="w-full flex justify-center items-center">
|
|
<Spinner></Spinner>
|
|
</div>
|
|
)}
|
|
{packData && (
|
|
<div>
|
|
<Card className="sticky top-0 left-0 right-0 z-10">
|
|
<div className="flex gap-4 items-center justify-between">
|
|
<div>
|
|
<p className="text-xl font-semibold">{packData.version}</p>
|
|
<p className="text-sm text-gray-400">{packData.modloader}</p>
|
|
</div>
|
|
<div className="flex gap-2 items-center">
|
|
<label htmlFor="pack-icon" className="cursor-pointer">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
alt=""
|
|
src={PACK_ENDPOINT("getPackImage", packData._id)}
|
|
className="w-12 h-12 rounded-md"
|
|
/>
|
|
</label>
|
|
<input
|
|
onChange={(e) => handleImageUpdate(e)}
|
|
id="pack-icon"
|
|
className="hidden"
|
|
name="image"
|
|
type="file"
|
|
/>
|
|
<div>
|
|
<p className="text-xl font-semibold">{packData.title}</p>
|
|
<p className="text-sm text-gray-400">by {packData.author}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2 items-center">
|
|
<div>
|
|
<p className="text-lg font-semibold">
|
|
{packData.mods.length} mods
|
|
</p>
|
|
<p className="text-sm text-gray-400">
|
|
v{packData.modpackVersion}
|
|
</p>
|
|
</div>
|
|
<Button onClick={() => setAddModModalOpen(true)}>
|
|
Add mod <HiPlusCircle className="ml-2 h-5 w-5" />
|
|
</Button>
|
|
<Button onClick={() => downloadPack()}>
|
|
Download <HiDownload className="ml-2 h-5 w-5" />
|
|
</Button>
|
|
<Button color={"red"} onClick={() => deletePack()}>
|
|
Delete <HiTrash className="ml-2 h-5 w-5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
<div className="mt-4">
|
|
<ModTable
|
|
mods={packData.mods}
|
|
updatePack={_getPacksData}
|
|
downloadMods={downloadMods}
|
|
packID={id}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<Modal
|
|
dismissible
|
|
show={addModModalOpen}
|
|
onClose={() => setAddModModalOpen(false)}
|
|
>
|
|
<ModalHeader>Add a mod</ModalHeader>
|
|
<ModalBody>
|
|
<div className="space-y-6">
|
|
<div className="flex-1">
|
|
<div className="mb-2 block">
|
|
<Label htmlFor="mod_url" className="text-lg">
|
|
Link
|
|
</Label>
|
|
</div>
|
|
<TextInput
|
|
id="mod_url"
|
|
type="text"
|
|
sizing="md"
|
|
name="mod_url"
|
|
onChange={(e) => setModUrl(e.target.value)}
|
|
value={modUrl}
|
|
required
|
|
/>
|
|
<p className="mt-2 text-gray-400">Can be one of:</p>
|
|
<ul className="text-gray-400 list-disc">
|
|
<li className="ml-4">
|
|
https://modrinth.com/mod/[slug][/version/file_id]
|
|
</li>
|
|
<li className="ml-4">
|
|
https://www.curseforge.com/minecraft/mc-mods/[slug][/files/file_id]
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</ModalBody>
|
|
<ModalFooter>
|
|
<Button onClick={() => addMod()}>Save</Button>
|
|
</ModalFooter>
|
|
</Modal>
|
|
<Modal
|
|
show={downloadModalOpen}
|
|
dismissible={downloadProgressFile.current == downloadProgressFile.total}
|
|
onClose={() => {
|
|
if (downloadProgressFile.current == downloadProgressFile.total) {
|
|
setdownloadModalOpen(false);
|
|
}
|
|
}}
|
|
>
|
|
<ModalHeader>Download progress</ModalHeader>
|
|
<ModalBody>
|
|
<div className="mb-4">
|
|
<Progress
|
|
progress={downloadProgressTotal}
|
|
progressLabelPosition="inside"
|
|
textLabel="Total"
|
|
textLabelPosition="outside"
|
|
size="lg"
|
|
labelProgress
|
|
labelText
|
|
/>
|
|
<div className="flex justify-end items-center mb-2">
|
|
<p>
|
|
{downloadProgressFile.current} / {downloadProgressFile.total}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Progress
|
|
progress={downloadProgressCurrent}
|
|
progressLabelPosition="inside"
|
|
textLabel={downloadProgressFile.filename}
|
|
textLabelPosition="outside"
|
|
size="lg"
|
|
labelProgress
|
|
labelText
|
|
/>
|
|
</div>
|
|
{downloadProgressFile.current == downloadProgressFile.total ? (
|
|
<p>
|
|
Mods downloaded, You can find them in path: packs/{packData?._id}
|
|
/mods
|
|
</p>
|
|
) : (
|
|
""
|
|
)}
|
|
</ModalBody>
|
|
<ModalFooter>
|
|
<Button
|
|
disabled={
|
|
downloadProgressFile.current != downloadProgressFile.total
|
|
}
|
|
onClick={() => setdownloadModalOpen(false)}
|
|
>
|
|
Close
|
|
</Button>
|
|
</ModalFooter>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|