diff --git a/deno.json b/deno.json index 4d6d49e..73ad739 100644 --- a/deno.json +++ b/deno.json @@ -31,8 +31,8 @@ "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3", "$std/": "https://deno.land/std@0.193.0/", - "puppet": "https://deno.land/x/sockpuppet@0.6.0/mod.ts", - "puppet/client": "https://deno.land/x/sockpuppet@0.6.0/client/mod.ts" + "puppet": "https://deno.land/x/sockpuppet@0.6.1/mod.ts", + "puppet/client": "https://deno.land/x/sockpuppet@0.6.1/client/mod.ts" }, "compilerOptions": { "jsx": "react-jsx", diff --git a/fresh.gen.ts b/fresh.gen.ts index f52716f..b842a45 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -8,16 +8,19 @@ import * as $2 from "./routes/api/fabric/index.ts"; import * as $3 from "./routes/api/manage.ts"; import * as $4 from "./routes/api/players.ts"; import * as $5 from "./routes/index.tsx"; -import * as $6 from "./routes/players.tsx"; -import * as $7 from "./routes/properties.tsx"; -import * as $8 from "./routes/setup/eula.tsx"; -import * as $9 from "./routes/setup/fabric.tsx"; -import * as $10 from "./routes/setup/index.tsx"; -import * as $11 from "./routes/terminal.tsx"; +import * as $6 from "./routes/mods/index.tsx"; +import * as $7 from "./routes/players.tsx"; +import * as $8 from "./routes/properties.tsx"; +import * as $9 from "./routes/setup/eula.tsx"; +import * as $10 from "./routes/setup/fabric.tsx"; +import * as $11 from "./routes/setup/index.tsx"; +import * as $12 from "./routes/terminal.tsx"; +import * as $13 from "./routes/upload.ts"; import * as $$0 from "./islands/fabricVersions.tsx"; -import * as $$1 from "./islands/players.tsx"; -import * as $$2 from "./islands/statusManager.tsx"; -import * as $$3 from "./islands/terminal.tsx"; +import * as $$1 from "./islands/fileUploader.tsx"; +import * as $$2 from "./islands/players.tsx"; +import * as $$3 from "./islands/statusManager.tsx"; +import * as $$4 from "./islands/terminal.tsx"; const manifest = { routes: { @@ -27,18 +30,21 @@ const manifest = { "./routes/api/manage.ts": $3, "./routes/api/players.ts": $4, "./routes/index.tsx": $5, - "./routes/players.tsx": $6, - "./routes/properties.tsx": $7, - "./routes/setup/eula.tsx": $8, - "./routes/setup/fabric.tsx": $9, - "./routes/setup/index.tsx": $10, - "./routes/terminal.tsx": $11, + "./routes/mods/index.tsx": $6, + "./routes/players.tsx": $7, + "./routes/properties.tsx": $8, + "./routes/setup/eula.tsx": $9, + "./routes/setup/fabric.tsx": $10, + "./routes/setup/index.tsx": $11, + "./routes/terminal.tsx": $12, + "./routes/upload.ts": $13, }, islands: { "./islands/fabricVersions.tsx": $$0, - "./islands/players.tsx": $$1, - "./islands/statusManager.tsx": $$2, - "./islands/terminal.tsx": $$3, + "./islands/fileUploader.tsx": $$1, + "./islands/players.tsx": $$2, + "./islands/statusManager.tsx": $$3, + "./islands/terminal.tsx": $$4, }, baseUrl: import.meta.url, }; diff --git a/islands/fileUploader.tsx b/islands/fileUploader.tsx new file mode 100644 index 0000000..514d1ff --- /dev/null +++ b/islands/fileUploader.tsx @@ -0,0 +1,71 @@ +import { useState } from "preact/hooks"; +import { FunctionComponent } from "preact"; + +export const FileUploader: FunctionComponent<{ path: string }> = ( + { children, path }, +) => { + const [hovered, setHovered] = useState(false); + + const playSound = () => { + const sound = new Audio( + "https://cdn.pixabay.com/audio/2023/07/21/audio_5634777127.mp3", + ); + sound.play(); + }; + + const defaultPreventer = (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const uploadFiles = async (files: File[]) => { + if (!files.length) return; + + const formData = new FormData(); + for (const file of files) { + formData.append(file.name, file); + } + + await fetch("/upload", { + method: "POST", + body: formData, + headers: { + "x-grizz-path": path, + }, + }); + + location.reload(); + }; + + return ( +
{ + defaultPreventer(e); + setHovered(true); + }} + onDragOver={(e) => { + defaultPreventer(e); + setHovered(true); + }} + onDrop={(e) => { + defaultPreventer(e); + playSound(); + const files = Array.from(e.dataTransfer?.files || []); + uploadFiles(files); + setHovered(false); + }} + onDragLeave={(e) => { + defaultPreventer(e); + setHovered(false); + }} + > + {children} + {hovered && ( +
+ +
+ )} +
+ ); +}; diff --git a/lib/modrinth.ts b/lib/modrinth.ts new file mode 100644 index 0000000..d971264 --- /dev/null +++ b/lib/modrinth.ts @@ -0,0 +1,112 @@ +import { Loader } from "../types/mcgrizzconf.ts"; + +export type ModrinthProjectSearchResult = { + project_id: string; + project_type: string; + slug: string; + author: string; + title: string; + description: string; + categories: string[]; + display_categories: string[]; + versions: string[]; + downloads: number; + follows: number; + icon_url: string; + date_created: string; + date_modified: string; + latest_version: string; + license: string; + client_side: string; + server_side: string; + gallery: string[]; + featured_gallery: string; + color: number; +}; + +export type ModrinthProject = { + id: string; + slug: string; + project_type: string; + team: string; + title: string; + description: string; + // Markdown + body: string; + body_url: string | null; + published: string; + updated: string; + approved: string; + queued: string; + status: string; + requested_status: string; + moderator_message: string | null; + license: { + id: string; + name: string; + url: string | null; + }; + client_side: string; + server_side: string; + downloads: number; + followers: number; + categories: string[]; + additional_categories: []; + game_versions: string[]; + loaders: string[]; + versions: string[]; + icon_url: string; + issues_url: string; + source_url: string; + wiki_url: string | null; + discord_url: string; + donation_urls: string[]; + gallery: { + url: string; + featured: boolean; + title: string; + description: string; + created: string; + ordering: number; + }[]; + color: number; + thread_id: string; + monetization_status: string; +}; + +export class Modrinth { + static apiRoot = "https://api.modrinth.com/v2"; + + static async searchMods( + q: string, + version: string, + loader: Loader, + offset = 0, + limit = 12, + ) { + const facets = [ + `"versions:${version}"`, + '"project_type:mod"', + ]; + + if (loader && loader !== "vanilla" && loader !== "unset") { + facets.push(`"categories:${loader}"`); + } + const qString = `/search?query=${q}&facets=[[${ + facets.join("],[") + }]]&offset=${offset * limit}&limit=${limit}`.trim(); + + const res = await fetch(this.apiRoot + qString); + return await res.json() as { + hits: ModrinthProjectSearchResult; + offset: number; + limit: number; + total_hits: number; + }; + } + + static async getProject(id: string) { + const res = await fetch(this.apiRoot + "/project/" + id); + return await res.json() as ModrinthProject; + } +} diff --git a/routes/_app.tsx b/routes/_app.tsx index 56b31a6..df09160 100644 --- a/routes/_app.tsx +++ b/routes/_app.tsx @@ -21,7 +21,7 @@ export default function App({ Component }: AppProps) { > - +
diff --git a/routes/mods/index.tsx b/routes/mods/index.tsx new file mode 100644 index 0000000..4095434 --- /dev/null +++ b/routes/mods/index.tsx @@ -0,0 +1,61 @@ +import { FunctionComponent } from "preact"; +import { Content } from "../../components/Content.tsx"; +import { SERVER_STATE } from "../../state/serverState.ts"; +import { FileUploader } from "../../islands/fileUploader.tsx"; + +export default async function ModsFolder() { + const files: string[] = []; + if ( + SERVER_STATE.serverType !== "unset" && SERVER_STATE.serverType !== "vanilla" + ) { + for await (const fileEntry of Deno.readDir("./server/mods")) { + if (fileEntry.isFile) { + files.push(fileEntry.name); + } + } + } + + return ( +
+ +

Active Mods

+ +
+ {!files.length && ( +
Drop files here to upload
+ )} + {files.map((f) => ( +
+ + {f} +
+ ))} +
+
+
+
+ ); +} + +const FileIcon: FunctionComponent<{ fileName: string }> = ({ fileName }) => { + let icon; + switch (fileName.split(".")[1]) { + case "jar": + icon = "fa-brand fa-java"; + break; + case "tmp": + case "temp": + icon = "fas fa-ghost"; + break; + case "png": + case "jpg": + case "jpeg": + case "webp": + icon = "fas fa-image"; + break; + default: + icon = "fas fa-file"; + } + + return ; +}; diff --git a/routes/upload.ts b/routes/upload.ts new file mode 100644 index 0000000..58681b5 --- /dev/null +++ b/routes/upload.ts @@ -0,0 +1,21 @@ +import { Handlers } from "$fresh/server.ts"; +import { ensureFile } from "$std/fs/ensure_file.ts"; + +export const handler: Handlers = { + async POST(req, _ctx) { + const path = req.headers.get("x-grizz-path"); + if (!path) return new Response("Upload path not included", { status: 400 }); + + const files = Array.from((await req.formData()).values()) as File[]; + + for (const file of files) { + const filePath = path.replace(/.$/, (e) => e.replace("/", "") + "/") + + file.name; + await ensureFile(filePath); + const newFile = await Deno.open(filePath, { write: true }); + file.stream().pipeTo(newFile.writable); + } + + return new Response("Success"); + }, +}; diff --git a/state/serverState.ts b/state/serverState.ts index 2f9d1c2..cf5ac65 100644 --- a/state/serverState.ts +++ b/state/serverState.ts @@ -1,7 +1,7 @@ import { Sockpuppet } from "puppet/client"; import { acceptEULA, checkEULA } from "../util/EULA.ts"; import { Loader } from "../types/mcgrizzconf.ts"; -import { updateConfFile } from "../util/confFile.ts"; +import { getConfFile, updateConfFile } from "../util/confFile.ts"; import { IS_BROWSER } from "$fresh/runtime.ts"; type MCServerEvent = 'message'; @@ -16,7 +16,7 @@ class ServerState { private command!: Deno.Command; private process!: Deno.ChildProcess; - private _eulaAccepted: boolean; + private _eulaAccepted = false; private sockpuppet!: Sockpuppet; private _channelId = "blanaba"; @@ -30,15 +30,29 @@ class ServerState { private _serverType: Loader = 'unset'; public get serverType(): Loader { - return this.serverType; + return this._serverType; } public set serverType(loader: Loader) { updateConfFile({loader}); this._serverType = loader; } + private _serverVersion: string; + public get serverVersion(): string { + return this._serverVersion; + } + public set serverVersion(version: string) { + updateConfFile({version}); + this._serverVersion = version; + } + constructor() { - this._eulaAccepted = checkEULA(); + const conf = getConfFile(); + this._serverType = conf.loader; + this._serverVersion = conf.version; + + // if (this.serverType !== 'unset') this._eulaAccepted = checkEULA(); + this.sockpuppet = new Sockpuppet( "ws://sockpuppet.cyborggrizzly.com", () => { diff --git a/static/javaicon.png b/static/javaicon.png new file mode 100644 index 0000000..4ced60f Binary files /dev/null and b/static/javaicon.png differ diff --git a/static/styles/tailwind.css b/static/styles/tailwind.css index 391543d..5693bb4 100644 --- a/static/styles/tailwind.css +++ b/static/styles/tailwind.css @@ -1,2 +1,2 @@ @font-face{font-family:Minecraft;font-style:normal;font-weight:400;src:url(/fonts/minecraft/MinecraftRegular.otf)}@font-face{font-family:Minecraft;font-style:normal;font-weight:700;src:url(/fonts/minecraft/MinecraftBold.otf)}@font-face{font-family:Minecraft;font-style:italic;font-weight:700;src:url(/fonts/minecraft/MinecraftBoldItalic.otf)}@font-face{font-family:Minecraft;font-style:italic;font-weight:400;src:url(/fonts/minecraft/MinecraftItalic.otf)} -/*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{margin-left:auto;margin-right:auto;width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}input,select{--tw-text-opacity:1;border-radius:.75rem;color:rgb(0 0 0/var(--tw-text-opacity));padding:.5rem}.absolute{position:absolute}.relative{position:relative}.bottom-4{bottom:1rem}.left-0{left:0}.-bottom-1{bottom:-.25rem}.right-0{right:0}.col-span-2{grid-column:span 2/span 2}.m-auto{margin:auto}.mx-auto{margin-left:auto;margin-right:auto}.my-6{margin-bottom:1.5rem;margin-top:1.5rem}.my-4{margin-bottom:1rem;margin-top:1rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mt-8{margin-top:2rem}.mt-4{margin-top:1rem}.block{display:block}.flex{display:flex}.grid{display:grid}.contents{display:contents}.h-\[100vh\]{height:100vh}.h-full{height:100%}.h-\[600px\]{height:600px}.h-20{height:5rem}.h-5{height:1.25rem}.h-16{height:4rem}.max-h-full{max-height:100%}.w-full{width:100%}.w-16{width:4rem}.w-20{width:5rem}.w-5{width:1.25rem}.w-24{width:6rem}.min-w-\[400px\]{min-width:400px}.max-w-screen-md{max-width:768px}.flex-1{flex:1 1 0%}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-y-4{row-gap:1rem}.gap-x-16{-moz-column-gap:4rem;column-gap:4rem}.overflow-auto{overflow:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.rounded-3xl{border-radius:1.5rem}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-r-2{border-right-width:2px}.border-sky{--tw-border-opacity:1;border-color:rgb(49 167 230/var(--tw-border-opacity))}.border-sky-950{--tw-border-opacity:1;border-color:rgb(13 44 68/var(--tw-border-opacity))}.border-licorice-800{--tw-border-opacity:1;border-color:rgb(150 60 151/var(--tw-border-opacity))}.border-sky-800{--tw-border-opacity:1;border-color:rgb(17 82 123/var(--tw-border-opacity))}.border-grape-800{--tw-border-opacity:1;border-color:rgb(109 33 135/var(--tw-border-opacity))}.border-fire-800{--tw-border-opacity:1;border-color:rgb(160 20 20/var(--tw-border-opacity))}.border-wasabi-800{--tw-border-opacity:1;border-color:rgb(79 84 32/var(--tw-border-opacity))}.bg-licorice{--tw-bg-opacity:1;background-color:rgb(22 10 22/var(--tw-bg-opacity))}.bg-smoke-900{--tw-bg-opacity:1;background-color:rgb(57 57 65/var(--tw-bg-opacity))}.bg-grape{--tw-bg-opacity:1;background-color:rgb(64 10 80/var(--tw-bg-opacity))}.bg-smoke-500{--tw-bg-opacity:1;background-color:rgb(115 117 132/var(--tw-bg-opacity))}.bg-smoke-600{--tw-bg-opacity:1;background-color:rgb(93 94 108/var(--tw-bg-opacity))}.bg-sky{--tw-bg-opacity:1;background-color:rgb(49 167 230/var(--tw-bg-opacity))}.bg-fire{--tw-bg-opacity:1;background-color:rgb(230 28 28/var(--tw-bg-opacity))}.bg-wasabi-600{--tw-bg-opacity:1;background-color:rgb(128 134 39/var(--tw-bg-opacity))}.bg-smoke-200{--tw-bg-opacity:1;background-color:rgb(217 217 222/var(--tw-bg-opacity))}.bg-black\/50{background-color:#00000080}.bg-smoke{--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity))}.p-4{padding:1rem}.p-8{padding:2rem}.p-2{padding:.5rem}.p-1{padding:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.text-center{text-align:center}.text-right{text-align:right}.font-pixel{font-family:Minecraft,cursive}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem}.text-lg,.text-xl{line-height:1.75rem}.text-xl{font-size:1.25rem}.text-6xl{font-size:3.75rem;line-height:1}.text-5xl{font-size:3rem;line-height:1}.font-bold{font-weight:700}.text-sky{--tw-text-opacity:1;color:rgb(49 167 230/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-smoke-800\/30{color:#41414b4d}.underline{text-decoration-line:underline}.opacity-50{opacity:.5}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.loader-ball{animation:color-cycle 3s linear infinite;animation-delay:var(--loader-delay);margin:auto}.loader{animation:spin 5s ease-in-out;animation-direction:alternate;animation-iteration-count:infinite}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(4turn)}}@keyframes color-cycle{0%{--tw-bg-opacity:1;background-color:rgb(230 28 28/var(--tw-bg-opacity))}25%{--tw-bg-opacity:1;background-color:rgb(64 10 80/var(--tw-bg-opacity))}50%{--tw-bg-opacity:1;background-color:rgb(49 167 230/var(--tw-bg-opacity))}75%{--tw-bg-opacity:1;background-color:rgb(128 134 39/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(230 28 28/var(--tw-bg-opacity))}}*{font-family:Titillium Web,sans-serif}.even\:bg-black\/10:nth-child(2n){background-color:#0000001a}.hover\:bg-smoke-500:hover{--tw-bg-opacity:1;background-color:rgb(115 117 132/var(--tw-bg-opacity))}.disabled\:opacity-30:disabled{opacity:.3}@media (prefers-color-scheme:dark){.dark\:border-sky-950{--tw-border-opacity:1;border-color:rgb(13 44 68/var(--tw-border-opacity))}.dark\:bg-smoke{--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity))}.dark\:bg-smoke-900{--tw-bg-opacity:1;background-color:rgb(57 57 65/var(--tw-bg-opacity))}.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}} \ No newline at end of file +/*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{margin-left:auto;margin-right:auto;width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}input,select{--tw-text-opacity:1;border-radius:.75rem;color:rgb(0 0 0/var(--tw-text-opacity));padding:.5rem}.absolute{position:absolute}.relative{position:relative}.bottom-4{bottom:1rem}.left-0{left:0}.-bottom-1{bottom:-.25rem}.right-0{right:0}.top-0{top:0}.bottom-0{bottom:0}.-z-10{z-index:-10}.col-span-2{grid-column:span 2/span 2}.m-auto{margin:auto}.mx-auto{margin-left:auto;margin-right:auto}.my-6{margin-bottom:1.5rem;margin-top:1.5rem}.my-4{margin-bottom:1rem;margin-top:1rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mt-8{margin-top:2rem}.mt-4{margin-top:1rem}.block{display:block}.flex{display:flex}.grid{display:grid}.contents{display:contents}.h-\[100vh\]{height:100vh}.h-full{height:100%}.h-4{height:1rem}.h-\[600px\]{height:600px}.h-20{height:5rem}.h-5{height:1.25rem}.h-16{height:4rem}.h-24{height:6rem}.h-40{height:10rem}.max-h-full{max-height:100%}.min-h-\[100px\]{min-height:100px}.w-full{width:100%}.w-24{width:6rem}.w-20{width:5rem}.w-5{width:1.25rem}.w-16{width:4rem}.min-w-\[400px\]{min-width:400px}.max-w-screen-md{max-width:768px}.flex-1{flex:1 1 0%}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.place-items-center{place-items:center}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.gap-2{gap:.5rem}.gap-8{gap:2rem}.gap-y-4{row-gap:1rem}.gap-x-16{-moz-column-gap:4rem;column-gap:4rem}.place-self-center{place-self:center}.overflow-auto{overflow:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.rounded-3xl{border-radius:1.5rem}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-r-2{border-right-width:2px}.border-sky{--tw-border-opacity:1;border-color:rgb(49 167 230/var(--tw-border-opacity))}.border-sky-950{--tw-border-opacity:1;border-color:rgb(13 44 68/var(--tw-border-opacity))}.border-licorice-800{--tw-border-opacity:1;border-color:rgb(150 60 151/var(--tw-border-opacity))}.border-sky-800{--tw-border-opacity:1;border-color:rgb(17 82 123/var(--tw-border-opacity))}.border-grape-800{--tw-border-opacity:1;border-color:rgb(109 33 135/var(--tw-border-opacity))}.border-fire-800{--tw-border-opacity:1;border-color:rgb(160 20 20/var(--tw-border-opacity))}.border-wasabi-800{--tw-border-opacity:1;border-color:rgb(79 84 32/var(--tw-border-opacity))}.bg-licorice{--tw-bg-opacity:1;background-color:rgb(22 10 22/var(--tw-bg-opacity))}.bg-smoke-900{--tw-bg-opacity:1;background-color:rgb(57 57 65/var(--tw-bg-opacity))}.bg-grape{--tw-bg-opacity:1;background-color:rgb(64 10 80/var(--tw-bg-opacity))}.bg-smoke-500{--tw-bg-opacity:1;background-color:rgb(115 117 132/var(--tw-bg-opacity))}.bg-black\/50{background-color:#00000080}.bg-smoke{--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity))}.bg-smoke-600{--tw-bg-opacity:1;background-color:rgb(93 94 108/var(--tw-bg-opacity))}.bg-sky{--tw-bg-opacity:1;background-color:rgb(49 167 230/var(--tw-bg-opacity))}.bg-fire{--tw-bg-opacity:1;background-color:rgb(230 28 28/var(--tw-bg-opacity))}.bg-wasabi-600{--tw-bg-opacity:1;background-color:rgb(128 134 39/var(--tw-bg-opacity))}.bg-smoke-200{--tw-bg-opacity:1;background-color:rgb(217 217 222/var(--tw-bg-opacity))}.bg-smoke-600\/20{background-color:#5d5e6c33}.p-4{padding:1rem}.p-8{padding:2rem}.p-2{padding:.5rem}.p-1{padding:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.text-center{text-align:center}.text-right{text-align:right}.font-pixel{font-family:Minecraft,cursive}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem}.text-lg,.text-xl{line-height:1.75rem}.text-xl{font-size:1.25rem}.text-6xl{font-size:3.75rem;line-height:1}.text-5xl{font-size:3rem;line-height:1}.font-bold{font-weight:700}.text-sky{--tw-text-opacity:1;color:rgb(49 167 230/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-smoke-800\/30{color:#41414b4d}.underline{text-decoration-line:underline}.opacity-50{opacity:.5}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.loader-ball{animation:color-cycle 3s linear infinite;animation-delay:var(--loader-delay);margin:auto}.loader{animation:spin 5s ease-in-out;animation-direction:alternate;animation-iteration-count:infinite}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(4turn)}}@keyframes color-cycle{0%{--tw-bg-opacity:1;background-color:rgb(230 28 28/var(--tw-bg-opacity))}25%{--tw-bg-opacity:1;background-color:rgb(64 10 80/var(--tw-bg-opacity))}50%{--tw-bg-opacity:1;background-color:rgb(49 167 230/var(--tw-bg-opacity))}75%{--tw-bg-opacity:1;background-color:rgb(128 134 39/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(230 28 28/var(--tw-bg-opacity))}}*{font-family:Titillium Web,sans-serif}.even\:bg-black\/10:nth-child(2n){background-color:#0000001a}.hover\:bg-smoke-500:hover{--tw-bg-opacity:1;background-color:rgb(115 117 132/var(--tw-bg-opacity))}.disabled\:opacity-30:disabled{opacity:.3}@media (prefers-color-scheme:dark){.dark\:border-sky-950{--tw-border-opacity:1;border-color:rgb(13 44 68/var(--tw-border-opacity))}.dark\:bg-smoke{--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity))}.dark\:bg-smoke-900{--tw-bg-opacity:1;background-color:rgb(57 57 65/var(--tw-bg-opacity))}.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file diff --git a/test.json b/test.json new file mode 100644 index 0000000..f733b38 --- /dev/null +++ b/test.json @@ -0,0 +1,469 @@ +{ + "hits": [ + { + "project_id": "FTeXqI9v", + "project_type": "mod", + "slug": "create-new-age", + "author": "nullBlade", + "title": "Create: New Age", + "description": "Create: New Age is an addon for the Create mod that adds integration with electricity.", + "categories": ["technology", "fabric", "forge"], + "display_categories": ["technology", "fabric", "forge"], + "versions": ["1.19.2", "1.20.1", "1.20.2"], + "downloads": 473, + "follows": 10, + "icon_url": "https://cdn.modrinth.com/data/FTeXqI9v/fe75695f6f2e085ac9fb56204de7f88b6d716e8d.png", + "date_created": "2023-08-30T02:15:52.063627Z", + "date_modified": "2023-09-24T07:07:15.073267Z", + "latest_version": "1.20.2", + "license": "BSD-3-Clause", + "client_side": "required", + "server_side": "required", + "gallery": [ + "https://cdn.modrinth.com/data/FTeXqI9v/images/156cd2e6ce38d8647a7c1e073753baaebb7c0474.png", + "https://cdn.modrinth.com/data/FTeXqI9v/images/197a77d6e98b80486481a7b7fe7cb28fa8b87f30.png", + "https://cdn.modrinth.com/data/FTeXqI9v/images/59be34a6c51e72f31616917af146d02c0a5a3cb3.png", + "https://cdn.modrinth.com/data/FTeXqI9v/images/e35cc2dc1e0aa47bc15fa2ef1aa9f2121f4d0539.png" + ], + "featured_gallery": "https://cdn.modrinth.com/data/FTeXqI9v/images/8bdaf7546c4d46ff9305ed9aba18df6e32d9234a.jpeg", + "color": 3220514 + }, + { + "project_id": "nr7cSJlY", + "project_type": "mod", + "slug": "brewery", + "author": "Patbox", + "title": "Patbox's Brewery", + "description": "Create alcoholic and non-alcoholic drinks with cauldrons and barrels!", + "categories": ["food", "game-mechanics", "fabric", "quilt"], + "display_categories": ["food", "game-mechanics", "fabric", "quilt"], + "versions": [ + "1.19.2", + "1.19.3", + "1.19.4", + "1.19.4-rc2", + "1.20", + "1.20.1", + "1.20.2", + "1.20.2-rc2", + "1.20-rc1" + ], + "downloads": 3924, + "follows": 45, + "icon_url": "https://cdn.modrinth.com/data/nr7cSJlY/c9c1d9b61922adda30b53d739922231c18d2823c.png", + "date_created": "2022-09-25T20:30:15.568551Z", + "date_modified": "2023-09-21T13:19:47.739240Z", + "latest_version": "1.20-rc1", + "license": "LGPL-3.0-only", + "client_side": "optional", + "server_side": "required", + "gallery": [], + "featured_gallery": "https://cdn.modrinth.com/data/nr7cSJlY/images/8965a6402256474bb5d7a5ff7141751f1f22992a.png", + "color": 13625834 + }, + { + "project_id": "sUlkLN1E", + "project_type": "mod", + "slug": "azure-paxels", + "author": "AzureDoom", + "title": "Azure Paxels", + "description": "Created becasue Fabric 1.19.4 has no good paxel mods updated.", + "categories": ["equipment", "fabric", "neoforge", "quilt"], + "display_categories": ["equipment", "fabric", "neoforge", "quilt"], + "versions": [ + "1.19.4", + "1.20", + "1.20.1", + "1.20.2", + "1.20-pre1", + "1.20-rc1" + ], + "downloads": 573, + "follows": 10, + "icon_url": "https://cdn.modrinth.com/data/sUlkLN1E/1e026932468f5dc227444892b11cc5e06f1587a7.png", + "date_created": "2023-05-07T01:19:46.079261Z", + "date_modified": "2023-10-03T18:20:58.140878Z", + "latest_version": "1.20-rc1", + "license": "MIT", + "client_side": "required", + "server_side": "required", + "gallery": [ + "https://cdn.modrinth.com/data/sUlkLN1E/images/2f5f219a20627ab1e3d344d99eb4e725848df1b7.png" + ], + "featured_gallery": null, + "color": 3549487 + }, + { + "project_id": "llV8wfkk", + "project_type": "mod", + "slug": "betterconsolemc", + "author": "Jonas_Jones", + "title": "BetterConsoleMC", + "description": "Create custom ingame commads that run system commands and tasks.\nThis is a new and improved version of the ConsoleMC mod. It works by defining the command first to avoid the big security risk.", + "categories": ["utility", "fabric", "quilt", "transportation"], + "display_categories": ["utility", "fabric", "quilt"], + "versions": [ + "1.17.1", + "1.18", + "1.18.1", + "1.18.2", + "1.19", + "1.19.1", + "1.19.2", + "1.19.3", + "1.19.4", + "1.20", + "1.20.1", + "1.20.2" + ], + "downloads": 119, + "follows": 3, + "icon_url": "https://cdn.modrinth.com/data/llV8wfkk/04f4393f5149d5b29a20b5170ac57eb4dfb7f303.png", + "date_created": "2022-12-16T01:35:12.431459Z", + "date_modified": "2023-09-26T21:59:10.564546Z", + "latest_version": "1.20.2", + "license": "CC0-1.0", + "client_side": "unsupported", + "server_side": "required", + "gallery": [], + "featured_gallery": null, + "color": 16516316 + }, + { + "project_id": "NWvsqJ2Z", + "project_type": "mod", + "slug": "areas", + "author": "Serilum", + "title": "Areas", + "description": "✍️ Create custom named regions/towns/zones with a radius using signs, with join/leave messages via GUI.", + "categories": [ + "decoration", + "game-mechanics", + "library", + "fabric", + "forge", + "neoforge", + "quilt" + ], + "display_categories": [ + "decoration", + "game-mechanics", + "library", + "fabric", + "forge", + "neoforge", + "quilt" + ], + "versions": [ + "1.16.5", + "1.18.2", + "1.19.2", + "1.19.3", + "1.19.4", + "1.20", + "1.20.1", + "1.20.2" + ], + "downloads": 96662, + "follows": 114, + "icon_url": "https://cdn.modrinth.com/data/NWvsqJ2Z/icon.png", + "date_created": "2022-09-01T15:43:14.157586Z", + "date_modified": "2023-09-22T00:10:53.971185Z", + "latest_version": "1.20.2", + "license": "LicenseRef-All-Rights-Reserved", + "client_side": "required", + "server_side": "optional", + "gallery": [], + "featured_gallery": null, + "color": 4234903 + }, + { + "project_id": "Ot5JFxuv", + "project_type": "mod", + "slug": "death-backup", + "author": "Serilum", + "title": "Death Backup", + "description": "💾 Creates back-ups of player inventories before death, which can be loaded via commands.", + "categories": [ + "management", + "utility", + "fabric", + "forge", + "neoforge", + "quilt" + ], + "display_categories": [ + "management", + "utility", + "fabric", + "forge", + "neoforge", + "quilt" + ], + "versions": [ + "1.16.5", + "1.18.2", + "1.19.2", + "1.19.3", + "1.19.4", + "1.20", + "1.20.1", + "1.20.2" + ], + "downloads": 2684, + "follows": 23, + "icon_url": "https://cdn.modrinth.com/data/Ot5JFxuv/icon.jpg", + "date_created": "2022-09-01T14:40:08.430843Z", + "date_modified": "2023-09-21T23:20:00.640383Z", + "latest_version": "1.20.2", + "license": "LicenseRef-All-Rights-Reserved", + "client_side": "optional", + "server_side": "required", + "gallery": [], + "featured_gallery": null, + "color": 5655906 + }, + { + "project_id": "IPbFTPzw", + "project_type": "mod", + "slug": "quick-paths", + "author": "Serilum", + "title": "Quick Paths", + "description": "🚶 Create long paths instantly by setting a start and end point.", + "categories": [ + "transportation", + "utility", + "fabric", + "forge", + "neoforge", + "quilt" + ], + "display_categories": [ + "transportation", + "utility", + "fabric", + "forge", + "neoforge", + "quilt" + ], + "versions": [ + "1.16.5", + "1.18.2", + "1.19.2", + "1.19.3", + "1.19.4", + "1.20", + "1.20.1", + "1.20.2" + ], + "downloads": 2213, + "follows": 25, + "icon_url": "https://cdn.modrinth.com/data/IPbFTPzw/icon.jpg", + "date_created": "2022-09-01T09:45:53.732436Z", + "date_modified": "2023-09-21T22:01:33.176715Z", + "latest_version": "1.20.2", + "license": "LicenseRef-All-Rights-Reserved", + "client_side": "optional", + "server_side": "required", + "gallery": [], + "featured_gallery": null, + "color": 6455609 + }, + { + "project_id": "fgmhI8kH", + "project_type": "mod", + "slug": "ct-overhaul-village", + "author": "ChoiceTheorem", + "title": "ChoiceTheorem's Overhauled Village", + "description": "Enhances and creates new villages and pillager outposts, that perfectly fit into your Minecraft world.", + "categories": [ + "adventure", + "worldgen", + "fabric", + "forge", + "neoforge", + "quilt", + "economy", + "utility" + ], + "display_categories": [ + "adventure", + "worldgen", + "fabric", + "forge", + "neoforge", + "quilt" + ], + "versions": [ + "1.18.2", + "1.18.2-rc1", + "1.19", + "1.19.1", + "1.19.2", + "1.19.3", + "1.19.4", + "1.20", + "1.20.1", + "1.20.2" + ], + "downloads": 158413, + "follows": 630, + "icon_url": "https://cdn.modrinth.com/data/fgmhI8kH/76dd7230a35c12d4956985317ffd0c079d6a9148.jpeg", + "date_created": "2022-05-16T03:06:55.644362Z", + "date_modified": "2023-09-29T16:52:26.973688Z", + "latest_version": "1.20.2", + "license": "CC-BY-NC-ND-4.0", + "client_side": "optional", + "server_side": "required", + "gallery": [ + "https://cdn.modrinth.com/data/fgmhI8kH/images/2f7b8e2fc46cbb9e7a83368a93ab1ba1feb110ed.webp", + "https://cdn.modrinth.com/data/fgmhI8kH/images/576fb3e920f242512932434a51cba40b14da2750.png" + ], + "featured_gallery": "https://cdn.modrinth.com/data/fgmhI8kH/images/15d7bf1aa1b7174fde4a5dac2ed81d4b8adb4b06.png", + "color": 5587502 + }, + { + "project_id": "kOuPUitF", + "project_type": "mod", + "slug": "healing-campfire", + "author": "Serilum", + "title": "Healing Campfire", + "description": "🔥🩹 Creates an area around the (soul) campfire where players and passive mobs receive regeneration.", + "categories": [ + "adventure", + "game-mechanics", + "utility", + "fabric", + "forge", + "neoforge", + "quilt" + ], + "display_categories": [ + "adventure", + "game-mechanics", + "utility", + "fabric", + "forge", + "neoforge", + "quilt" + ], + "versions": [ + "1.16.5", + "1.18.2", + "1.19.2", + "1.19.3", + "1.19.4", + "1.20", + "1.20.1", + "1.20.2" + ], + "downloads": 16604, + "follows": 111, + "icon_url": "https://cdn.modrinth.com/data/kOuPUitF/icon.png", + "date_created": "2022-09-01T13:26:56.104570Z", + "date_modified": "2023-09-21T22:40:49.277734Z", + "latest_version": "1.20.2", + "license": "LicenseRef-All-Rights-Reserved", + "client_side": "optional", + "server_side": "required", + "gallery": [], + "featured_gallery": null, + "color": 5921840 + }, + { + "project_id": "QktnymFN", + "project_type": "mod", + "slug": "lightning-podoboo", + "author": "LostLuma", + "title": "Lightning Podoboo", + "description": "Makes fire created by natural lightning cosmetic, meaning no blocks are destroyed during thunderstorms.", + "categories": ["game-mechanics", "utility", "fabric", "quilt", "mobs"], + "display_categories": ["game-mechanics", "utility", "fabric", "quilt"], + "versions": [ + "1.18", + "1.18.1", + "1.18.2", + "1.19", + "1.19.1", + "1.19.2", + "1.19.3", + "1.19.4", + "1.20", + "1.20.1", + "1.20.2" + ], + "downloads": 488, + "follows": 12, + "icon_url": "", + "date_created": "2022-02-08T11:09:41.222962Z", + "date_modified": "2023-09-21T16:15:18.747778Z", + "latest_version": "1.20.2", + "license": "MIT", + "client_side": "optional", + "server_side": "required", + "gallery": [], + "featured_gallery": null, + "color": null + }, + { + "project_id": "hQbzUScT", + "project_type": "mod", + "slug": "dynamichud", + "author": "tanishisherewithhh", + "title": "DynamicHUD", + "description": "A library to create Hud Widgets and display them on the screen. AutoSave and Autoload included. Fabric only", + "categories": ["game-mechanics", "library", "utility", "fabric"], + "display_categories": ["game-mechanics", "library", "utility", "fabric"], + "versions": ["1.19.4", "1.20", "1.20.1", "1.20.2"], + "downloads": 249, + "follows": 6, + "icon_url": "https://cdn.modrinth.com/data/hQbzUScT/76bde52a3696a533af510f47e8f93b35ad4eb2ab.png", + "date_created": "2023-06-17T00:27:34.710323Z", + "date_modified": "2023-10-02T06:26:09.404685Z", + "latest_version": "1.20.2", + "license": "MIT", + "client_side": "required", + "server_side": "unsupported", + "gallery": [ + "https://cdn.modrinth.com/data/hQbzUScT/images/76bde52a3696a533af510f47e8f93b35ad4eb2ab.png" + ], + "featured_gallery": "https://cdn.modrinth.com/data/hQbzUScT/images/de25273afeb40a17b77821ae4b56434c3953a47b.png", + "color": 9757053 + }, + { + "project_id": "O5BnVXcp", + "project_type": "mod", + "slug": "shulker-nbt-fix", + "author": "qpcrummer", + "title": "ShulkerNbtFix", + "description": "All shulkers are created equal: placed and unplaced", + "categories": ["game-mechanics", "fabric", "quilt", "utility"], + "display_categories": ["game-mechanics", "fabric", "quilt"], + "versions": [ + "1.19", + "1.19.1", + "1.19.2", + "1.19.3", + "1.19.4", + "1.20", + "1.20.1", + "1.20.2" + ], + "downloads": 120, + "follows": 4, + "icon_url": "https://cdn.modrinth.com/data/O5BnVXcp/096802aeb7ee5284d3dca7f38b6e00b46c1530ce.png", + "date_created": "2023-06-17T00:38:18.136269Z", + "date_modified": "2023-09-22T00:16:16.841957Z", + "latest_version": "1.20.2", + "license": "LGPL-3.0-only", + "client_side": "unsupported", + "server_side": "required", + "gallery": [], + "featured_gallery": null, + "color": 5980251 + } + ], + "offset": 0, + "limit": 12, + "total_hits": 23 +} diff --git a/types/mcgrizzconf.ts b/types/mcgrizzconf.ts index e2ac25b..450d66a 100644 --- a/types/mcgrizzconf.ts +++ b/types/mcgrizzconf.ts @@ -1,5 +1,13 @@ -export type Loader = 'forge' | 'fabric' | 'vanilla' | 'unset'; +import { ModrinthProject } from "../lib/modrinth.ts"; + +export type Loader = "forge" | "fabric" | "vanilla" | "unset"; export type MCGrizzConf = { - loader: Loader -} \ No newline at end of file + loader: Loader; + version: string; + mods?: { + source: "modrinth"; + details: ModrinthProject; + jarName?: string; + }[]; +}; diff --git a/util/EULA.ts b/util/EULA.ts index 824d954..589565b 100644 --- a/util/EULA.ts +++ b/util/EULA.ts @@ -3,7 +3,6 @@ import { IS_BROWSER } from "$fresh/runtime.ts"; const eulaRegex = /(eula=false)/; export const checkEULA = (instance = "server") => !IS_BROWSER && !eulaRegex.test(Deno.readTextFileSync(`./${instance}/eula.txt`)); - // true; export const acceptEULA = (instance = "server") => { const eula = Deno.readTextFileSync(`./${instance}/eula.txt`); diff --git a/util/confFile.ts b/util/confFile.ts index cb4e11d..427d35f 100644 --- a/util/confFile.ts +++ b/util/confFile.ts @@ -1,7 +1,10 @@ +import { ensureFileSync } from "$std/fs/ensure_file.ts"; +import { IS_BROWSER } from "$fresh/runtime.ts"; import { MCGrizzConf } from "../types/mcgrizzconf.ts"; const defaultConf: MCGrizzConf = { loader: 'unset', + version: '' } const confPath = 'mcgrizz.json' @@ -14,7 +17,9 @@ export function makeConfFile(): MCGrizzConf { } export function getConfFile(): MCGrizzConf { - const conf = JSON.parse(Deno.readTextFileSync(confPath)); + if (IS_BROWSER) return defaultConf; + ensureFileSync(confPath); + const conf = JSON.parse(Deno.readTextFileSync(confPath) || 'null'); if (!conf) { return makeConfFile(); @@ -24,7 +29,8 @@ export function getConfFile(): MCGrizzConf { } export async function updateConfFile(newConf: Partial) { - const conf = {...getConfFile(), newConf}; + if (IS_BROWSER) return; + const conf = {...getConfFile(), ...newConf}; await Deno.writeTextFile(confPath, JSON.stringify(conf, null, 2)); } diff --git a/util/download.ts b/util/download.ts new file mode 100644 index 0000000..7cbffe4 --- /dev/null +++ b/util/download.ts @@ -0,0 +1,35 @@ +import { ensureFile } from "$std/fs/ensure_file.ts"; + +/** + * + * @param src url of file + * @param dest destination file. If `useFileName` is true, this should be the destination directory + * @param [useFileName] whether to use the inferred file name from `src` + */ + +export async function downloadFile(src: string, dest: string, useFileName?:boolean) { + if (!(src.startsWith("http://") || src.startsWith("https://"))) { + throw new TypeError("URL must start with be http:// or https://"); + } + + const fileName = src.split('/').at(-1); + + const resp = await fetch(src); + if (!resp.ok) { + throw new Deno.errors.BadResource( + `Request failed with status ${resp.status}`, + ); + } else if (!resp.body) { + throw new Deno.errors.UnexpectedEof( + `The download url ${src} doesn't contain a file to download`, + ); + } else if (resp.status === 404) { + throw new Deno.errors.NotFound( + `The requested url "${src}" could not be found`, + ); + } + + await ensureFile(useFileName ? dest + fileName : dest); + const file = await Deno.open(dest, { truncate: true, write: true }); + resp.body.pipeTo(file.writable); +} \ No newline at end of file diff --git a/util/getNavItems.ts b/util/getNavItems.ts index 87029ae..21639ca 100644 --- a/util/getNavItems.ts +++ b/util/getNavItems.ts @@ -1,3 +1,4 @@ +// deno-lint-ignore-file no-fallthrough import { MCGrizzConf } from "../types/mcgrizzconf.ts"; import { NavItem } from "../types/nav.ts"; import { makeConfFile } from "./confFile.ts"; @@ -13,16 +14,20 @@ export function getNavItems(): NavItem[] { conf = makeConfFile(); } + const items: NavItem[] = []; + switch (conf.loader) { case "unset": - return [{ + items.push({ title: "Setup", href: "/", - }]; + }); + break; case "forge": case "fabric": + items.push({ title: "Mods", href: "/mods" }); case "vanilla": - return [ + items.unshift( { title: "Server Terminal", href: "/terminal", @@ -35,6 +40,8 @@ export function getNavItems(): NavItem[] { title: "Server Properties", href: "/properties", }, - ]; + ); + break; } + return items; } diff --git a/util/players.ts b/util/players.ts index 23ce7d3..e060917 100644 --- a/util/players.ts +++ b/util/players.ts @@ -1,3 +1,4 @@ +import { ensureFile } from "$std/fs/ensure_file.ts"; import { SERVER_STATE } from "../state/serverState.ts"; import { filterTruthy } from "./filters.ts"; @@ -30,7 +31,7 @@ export const getPlayerData = async (username: string) => { username = username.trim(); if (!username) return; const cacheFile = 'players.cache.json' - await Deno.create(cacheFile); + await ensureFile(cacheFile); const cache = JSON.parse(await Deno.readTextFile(cacheFile) || '{}'); if (!cache[username]) { const req = await fetch('https://playerdb.co/api/player/minecraft/' + username, {