diff --git a/gui/app/components/ModTable.tsx b/gui/app/components/ModTable.tsx index 4a70b95..81f4376 100644 --- a/gui/app/components/ModTable.tsx +++ b/gui/app/components/ModTable.tsx @@ -1,18 +1,15 @@ import { MOD_ENDPOINT } from "@/api/ENDPOINTS"; import { Mod } from "@/types/mod"; -import { - Button, - Checkbox, - Table, - TableBody, - TableCell, - TableHead, - TableHeadCell, - TableRow, -} from "flowbite-react"; +import { Button } from "flowbite-react"; import { useState } from "react"; import { HiDownload, HiTrash } from "react-icons/hi"; import { toast } from "react-toastify"; +import { + Accordion, + AccordionContent, + AccordionPanel, + AccordionTitle, +} from "flowbite-react"; export const ModTable = (props: { mods: Mod[]; @@ -20,7 +17,13 @@ export const ModTable = (props: { packID: string; downloadMods: (mods: string[]) => void; }) => { - const [selectedMods, setSelectedMods] = useState([]); + function bytesToSize(bytes) { + var sizes = ["Bytes", "KB", "MB", "GB", "TB"]; + if (bytes == 0) return "n/a"; + var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); + if (i == 0) return bytes + " " + sizes[i]; + return (bytes / Math.pow(1024, i)).toFixed(1) + " " + sizes[i]; + } async function deleteMod(slug: string, title: string) { if (!window) return; @@ -41,144 +44,151 @@ export const ModTable = (props: { } } - function selectAll() { - const deselect = selectedMods.length == props.mods.length; - console.log(selectedMods.length, props.mods.length, deselect); - if (deselect) { - setSelectedMods([]); - return; - } - props.mods.forEach((item) => { - if (!selectedMods.includes(item.slug)) { - setSelectedMods((state) => [item.slug, ...state]); - } - }); - } - - function handleCheckbox(slug: string) { - if (!selectedMods.includes(slug)) { - setSelectedMods((state) => [slug, ...state]); - } else { - const newArray = selectedMods.map((i) => i); - const idx = newArray.findIndex((item) => item == slug); - newArray.splice(idx, 1); - setSelectedMods(newArray); - } - } - - async function deleteSelectedMods() { - await fetch(MOD_ENDPOINT("deleteModBulk", props.packID), { - method: "POST", - body: JSON.stringify(selectedMods), - headers: { - "content-type": "application/json", - accept: "application/json", - }, - }); - setSelectedMods([]); - props.updatePack(); + if (!props.mods || props.mods.length == 0) { + return <>; } return ( -
- - - - - selectAll()} - /> - - Icon - Title - Version - Developer - Source - Source URL - - Actions - - - - - {props.mods && - props.mods.length > 0 && - props.mods.map((mod) => { - return ( - - - { - handleCheckbox(mod.slug); - }} - /> - - - {/* eslint-disable-next-line @next/next/no-img-element */} - - - - {mod.title} - - {mod.file.version} - {mod.developers.join(", ")} - {mod.source} - {mod.url} - -
- - -
-
-
- ); - })} - - - - - - - - - -
+ + {props.mods.map((mod) => { + return ( + + +
+ + {mod.title} ({mod.slug}) +
+
+ +
+
+
+

Developers

+ {mod.developers.join(", ")} +
+
+

Source

+

+ title: {mod.source} +

+

+ id:{" "} + {mod.project_id} +

+

+ link: {mod.url} +

+
+
+
+

Version info

+

+ filename:{" "} + {mod.file.filename} +

+

+ version:{" "} + {mod.file.version} +

+

+ file size:{" "} + {bytesToSize(mod.file.size)} +

+
+
+

Environment

+ {mod.environment.client &&

client

} + {mod.environment.server &&

server

} + {mod.environment.client && mod.environment.server && ( +

client & server

+ )} + {/*

+ filename:{" "} + {mod.file.filename} +

+

+ version:{" "} + {mod.file.version} +

+

+ file size:{" "} + {bytesToSize(mod.file.size)} +

*/} +
+
+
+

Hashes

+ {Object.entries(mod.file.hashes).map((hash) => { + return ( +

+ {hash[0]}:{" "} + {hash[1]} +

+ ); + })} +
+ {mod.dependencies.length > 0 ? ( +
+

Dependencies

+
+ {mod.dependencies.map((dep) => { + return ( +
+
+ + {dep.title} ({dep.slug}) +
+
+

+ filename:{" "} + {dep.file.filename} +

+

+ version:{" "} + {dep.file.version} +

+

+ file size:{" "} + {bytesToSize(dep.file.size)} +

+
+
+ ); + })} +
+
+ ) : ( + "" + )} +
- - - -
-
+ + + ); + })} + ); }; diff --git a/gui/app/components/_ModTable.tsx b/gui/app/components/_ModTable.tsx new file mode 100644 index 0000000..412cf37 --- /dev/null +++ b/gui/app/components/_ModTable.tsx @@ -0,0 +1,234 @@ +import { MOD_ENDPOINT } from "@/api/ENDPOINTS"; +import { Mod } from "@/types/mod"; +import { + Button, + Checkbox, + Table, + TableBody, + TableCell, + TableHead, + TableHeadCell, + TableRow, +} from "flowbite-react"; +import { useState } from "react"; +import { HiDownload, HiTrash } from "react-icons/hi"; +import { toast } from "react-toastify"; + +export const ModTable = (props: { + mods: Mod[]; + updatePack: () => void; + packID: string; + downloadMods: (mods: string[]) => void; +}) => { + const [selectedMods, setSelectedMods] = useState([]); + + async function deleteMod(slug: string, title: string) { + if (!window) return; + if (window.confirm(`Delete mod ${title}?`)) { + const res = await fetch(MOD_ENDPOINT("deleteMod", props.packID, slug)); + const data = await res.json(); + + if (data.status != "ok") { + toast.error(data.message, { + autoClose: 2500, + closeOnClick: true, + draggable: true, + }); + return; + } + + props.updatePack(); + } + } + + function selectAll() { + const deselect = selectedMods.length == props.mods.length; + console.log(selectedMods.length, props.mods.length, deselect); + if (deselect) { + setSelectedMods([]); + return; + } + props.mods.forEach((item) => { + if (!selectedMods.includes(item.slug)) { + setSelectedMods((state) => [item.slug, ...state]); + } + }); + } + + function handleCheckbox(slug: string) { + if (!selectedMods.includes(slug)) { + setSelectedMods((state) => [slug, ...state]); + } else { + const newArray = selectedMods.map((i) => i); + const idx = newArray.findIndex((item) => item == slug); + newArray.splice(idx, 1); + setSelectedMods(newArray); + } + } + + async function deleteSelectedMods() { + await fetch(MOD_ENDPOINT("deleteModBulk", props.packID), { + method: "POST", + body: JSON.stringify(selectedMods), + headers: { + "content-type": "application/json", + accept: "application/json", + }, + }); + setSelectedMods([]); + props.updatePack(); + } + + return ( +
+ + + + + selectAll()} + /> + + Icon + Title + Version + Developer + Source + Source URL + + Actions + + + + + {props.mods && + props.mods.length > 0 && + props.mods.map((mod) => { + return ( + <> + + + { + handleCheckbox(mod.slug); + }} + /> + + + {/* eslint-disable-next-line @next/next/no-img-element */} + + + + {mod.title} + + {mod.file.version} + {mod.developers.join(", ")} + {mod.source} + {mod.url} + +
+ + +
+
+
+ {mod.dependencies && + mod.dependencies.length > 0 && + mod.dependencies.map((dep) => { + return ( + + + + {/* eslint-disable-next-line @next/next/no-img-element */} + + + + {dep.title} + + {dep.file.version} + {dep.developers.join(", ")} + {dep.source} + {dep.url} + + {/*
+ + +
*/} +
+
+ ); + })} + + ); + })} + + + + + + + + + +
+ + +
+
+
+
+
+
+ ); +}; diff --git a/gui/types/file.ts b/gui/types/file.ts index 817dafd..36411ee 100644 --- a/gui/types/file.ts +++ b/gui/types/file.ts @@ -1,6 +1,6 @@ export type ModFile = { "version": string, - "hashes": unknown, + "hashes": Record, "url": string, "filename": string, "size": number, diff --git a/gui/types/mod.ts b/gui/types/mod.ts index 7bb415c..b6336a8 100644 --- a/gui/types/mod.ts +++ b/gui/types/mod.ts @@ -2,6 +2,7 @@ import { ModFile } from "./file"; export type Mod = { "slug": string, + "project_id": string; "icon": string, "title":string, "developers": string[], @@ -11,5 +12,6 @@ export type Mod = { "client": boolean, "server": boolean, }, + "dependencies": Mod[], "file": ModFile, } \ No newline at end of file diff --git a/src/api/source/CurseForge.py b/src/api/source/CurseForge.py index 4442370..01864ff 100644 --- a/src/api/source/CurseForge.py +++ b/src/api/source/CurseForge.py @@ -16,6 +16,10 @@ def getCurseForgeMod(slug, version, mod_loader, game_version): return { "status": "error", "message": f"failed to fetch curseforge mod: {metaR.status_code}", + "slug": slug, + "version": version, + "mod_loader": mod_loader, + "game_version": game_version } meta: dict = metaR.json() @@ -42,6 +46,10 @@ def getCurseForgeMod(slug, version, mod_loader, game_version): return { "status": "error", "message": f"failed to fetch curseforge mod versions: {versR.status_code}", + "slug": slug, + "version": version, + "mod_loader": mod_loader, + "game_version": game_version } vers: dict = versR.json() @@ -49,6 +57,10 @@ def getCurseForgeMod(slug, version, mod_loader, game_version): return { "status": "error", "message": f"mod is not compatible with this game version or mod loader", + "slug": slug, + "version": version, + "mod_loader": mod_loader, + "game_version": game_version } if version: @@ -64,10 +76,13 @@ def getCurseForgeMod(slug, version, mod_loader, game_version): for hash in selected_version.get("hashes"): hashes[HASHALGO_ENUM[hash.get("algo")]] = hash.get("value") + dependencies = [] + return { "status": "ok", "mod": { "slug": slug, + "project_id": meta.get("id"), "icon": meta.get("logo").get("url"), "title": meta.get("name"), "developers": developers, @@ -77,6 +92,7 @@ def getCurseForgeMod(slug, version, mod_loader, game_version): "client": True, "server": True, }, + "dependencies": dependencies, "file": { "version": selected_version.get("id"), "hashes": hashes, diff --git a/src/api/source/Modrinth.py b/src/api/source/Modrinth.py index f0a270d..2fafcb3 100644 --- a/src/api/source/Modrinth.py +++ b/src/api/source/Modrinth.py @@ -10,6 +10,10 @@ def getModrinthMod(slug, version, mod_loader, game_version): return { "status": "error", "message": f"failed to fetch modrinth description: {descR.status_code}", + "slug": slug, + "version": version, + "mod_loader": mod_loader, + "game_version": game_version } versR = requests.get( @@ -21,6 +25,10 @@ def getModrinthMod(slug, version, mod_loader, game_version): return { "status": "error", "message": f"failed to fetch modrinth mod versions: {versR.status_code}", + "slug": slug, + "version": version, + "mod_loader": mod_loader, + "game_version": game_version } devsR = requests.get( @@ -32,6 +40,10 @@ def getModrinthMod(slug, version, mod_loader, game_version): return { "status": "error", "message": f"failed to fetch modrinth mod developers: {devsR.status_code}", + "slug": slug, + "version": version, + "mod_loader": mod_loader, + "game_version": game_version } desc: dict = descR.json() @@ -42,6 +54,10 @@ def getModrinthMod(slug, version, mod_loader, game_version): return { "status": "error", "message": f"mod is not compatible with this game version or mod loader", + "slug": slug, + "version": version, + "mod_loader": mod_loader, + "game_version": game_version } selected_version = vers[0] @@ -61,6 +77,10 @@ def getModrinthMod(slug, version, mod_loader, game_version): return { "status": "error", "message": f"failed to get primary mod file", + "slug": slug, + "version": version, + "mod_loader": mod_loader, + "game_version": game_version } developers = [] @@ -74,10 +94,20 @@ def getModrinthMod(slug, version, mod_loader, game_version): if desc.get("server_side") in ["optional", "required"]: isServer = True + dependencies = [] + for dep in selected_version.get("dependencies"): + depDescR = requests.get(f"https://api.modrinth.com/v2/project/{dep.get('project_id')}", headers=headers) + if depDescR.status_code != 200: + continue + depDesc: dict = depDescR.json() + depMod = getModrinthMod(depDesc.get("slug"), None, mod_loader, game_version) + dependencies.append(depMod.get("mod")) + return { "status": "ok", "mod": { "slug": slug, + "project_id": desc.get("id"), "icon": desc.get("icon_url"), "title": desc.get("title"), "developers": developers, @@ -87,6 +117,7 @@ def getModrinthMod(slug, version, mod_loader, game_version): "client": isClient, "server": isServer, }, + "dependencies": dependencies, "file": { "version": selected_version.get("version_number"), "hashes": primary_file.get("hashes"),