Basic mod manager

This commit is contained in:
Emmaline Autumn 2023-10-05 12:55:50 -06:00
parent 5779cd9efc
commit 1e9868348d
5 changed files with 185 additions and 37 deletions

View File

@ -3,37 +3,43 @@ import { useEffect, useRef, useState } from "preact/hooks";
import { JSX } from "preact/jsx-runtime"; import { JSX } from "preact/jsx-runtime";
import { Sockpuppet } from "puppet/client"; import { Sockpuppet } from "puppet/client";
export function Terminal(props: { channelId: string }) { export function Terminal(props: { channelId: string }) {
const puppet = useRef(new Sockpuppet("ws://sockpuppet.cyborggrizzly.com")); const puppet = useRef(new Sockpuppet("ws://sockpuppet.cyborggrizzly.com"));
const [lines, setLines] = useState<string[]>([]); const [lines, setLines] = useState<string[]>([]);
const divRef = useRef<HTMLDivElement>(null); const divRef = useRef<HTMLDivElement>(null);
const storeKey = 'commandHistory'; const storeKey = "commandHistory";
const [commandHistory, setCommandHistory] = useState<string[]>(JSON.parse(localStorage.getItem(storeKey) || '[]')); const [commandHistory, setCommandHistory] = useState<string[]>(
JSON.parse(localStorage.getItem(storeKey) || "[]"),
);
const [historyIndex, setHistoryIndex] = useState(commandHistory.length); const [historyIndex, setHistoryIndex] = useState(commandHistory.length);
const [command, setCommand] = useState(""); const [command, setCommand] = useState("");
const changeHistoryIndex = (by: number) => setHistoryIndex(i => i + by); const changeHistoryIndex = (by: number) => setHistoryIndex((i) => i + by);
useEffect(() => { useEffect(() => {
if (!IS_BROWSER) return; if (!IS_BROWSER) return;
puppet.current.joinChannel(props.channelId, (line) => { puppet.current.joinChannel(props.channelId, (line) => {
setLines((l) => [...l, line]); setLines((l) => [...l, line]);
}); });
setTimeout(() => {
const channel = puppet.current.getChannel(props.channelId)
// console.log(channel)
channel?.send("log");
}, 200);
document.addEventListener('keyup', (e) => { document.addEventListener("keyup", (e) => {
switch (e.key) { switch (e.key) {
case 'Up': case "Up":
case 'ArrowUp': case "ArrowUp":
changeHistoryIndex(-1); changeHistoryIndex(-1);
break; break;
case 'Down': case "Down":
case 'ArrowDown': case "ArrowDown":
changeHistoryIndex(1); changeHistoryIndex(1);
break; break;
} }
}) });
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -44,25 +50,34 @@ export function Terminal(props: { channelId: string }) {
const sendCommand = (e: Event) => { const sendCommand = (e: Event) => {
e.preventDefault(); e.preventDefault();
puppet.current.getChannel(props.channelId)?.send(historyIndex === commandHistory.length ? command : commandHistory[historyIndex]); puppet.current.getChannel(props.channelId)?.send(
setCommandHistory(c => [...c, command]); historyIndex === commandHistory.length
setHistoryIndex(commandHistory.length + 1) ? command
: commandHistory[historyIndex],
);
setCommandHistory((c) => [...c, command]);
setHistoryIndex(commandHistory.length + 1);
setCommand(""); setCommand("");
}; };
const handleCommandUpdate = (e: JSX.TargetedEvent<HTMLInputElement, Event>) => { const handleCommandUpdate = (
e: JSX.TargetedEvent<HTMLInputElement, Event>,
) => {
const value = e.currentTarget.value; const value = e.currentTarget.value;
if (historyIndex !== commandHistory.length) { if (historyIndex !== commandHistory.length) {
setHistoryIndex(commandHistory.length); setHistoryIndex(commandHistory.length);
} }
setCommand(value || '') setCommand(value || "");
} };
useEffect(() => { useEffect(() => {
localStorage.setItem(storeKey, JSON.stringify(commandHistory.slice(0,100))); localStorage.setItem(
}, [commandHistory]) storeKey,
JSON.stringify(commandHistory.slice(0, 100)),
);
}, [commandHistory]);
return ( return (
<div> <div>

View File

@ -1,5 +1,6 @@
import { AppProps } from "$fresh/server.ts"; import { AppProps } from "$fresh/server.ts";
import { Nav } from "../components/nav/index.tsx"; import { Nav } from "../components/nav/index.tsx";
import { StatusManager } from "../islands/statusManager.tsx";
export default function App({ Component }: AppProps) { export default function App({ Component }: AppProps) {
return ( return (
@ -44,6 +45,10 @@ export default function App({ Component }: AppProps) {
<Component /> <Component />
</div> </div>
</div> </div>
<div class="fixed bottom-0 right-0 rounded-tl-3xl border-t-4 border-l-4 border-sky px-4 py-2 bg-smoke-600">
<StatusManager />
</div>
</body> </body>
</html> </html>
); );

View File

@ -2,34 +2,158 @@ import { FunctionComponent } from "preact";
import { Content } from "../../components/Content.tsx"; import { Content } from "../../components/Content.tsx";
import { SERVER_STATE } from "../../state/serverState.ts"; import { SERVER_STATE } from "../../state/serverState.ts";
import { FileUploader } from "../../islands/fileUploader.tsx"; import { FileUploader } from "../../islands/fileUploader.tsx";
import { Handlers, PageProps } from "$fresh/server.ts";
import { ensureFile } from "$std/fs/ensure_file.ts";
import { ensureDir } from "$std/fs/ensure_dir.ts";
import { Button } from "../../components/Button.tsx";
export default async function ModsFolder() { export const handler: Handlers = {
const files: string[] = []; async POST(req, _ctx) {
const formData = await req.formData();
const filePath = formData.get("filePath") as string;
if (typeof filePath !== "string") {
throw "File path not included in mod enable/disable";
}
if (formData.get("delete")) {
await Deno.remove(filePath);
return Response.redirect(req.url);
}
const fileName = filePath.split("/").at(-1);
if (!fileName) throw "Unable to infer filename in mod enable/disable";
try {
await Deno.lstat(filePath);
} catch {
return Response.redirect(req.url);
}
if (filePath.includes("disabled")) {
await ensureFile("./server/mods/" + fileName);
await Deno.rename(filePath, "./server/mods/" + fileName);
} else {
await ensureFile("./server/disabled-mods/" + fileName);
await Deno.rename(filePath, "./server/disabled-mods/" + fileName);
}
return Response.redirect(req.url);
},
};
export default async function ModsFolder({ url }: PageProps) {
const activeMods: string[] = [];
const disabledMods: string[] = [];
if ( if (
SERVER_STATE.serverType !== "unset" && SERVER_STATE.serverType !== "vanilla" SERVER_STATE.serverType !== "unset" && SERVER_STATE.serverType !== "vanilla"
) { ) {
for await (const fileEntry of Deno.readDir("./server/mods")) { for await (const fileEntry of Deno.readDir("./server/mods")) {
if (fileEntry.isFile) { if (fileEntry.isFile) {
files.push(fileEntry.name); activeMods.push(fileEntry.name);
}
}
ensureDir("./server/disabled-mods");
for await (const fileEntry of Deno.readDir("./server/disabled-mods")) {
if (fileEntry.isFile) {
disabledMods.push(fileEntry.name);
} }
} }
} }
return ( return (
<div className="container p-8"> <div className="container p-8 flex flex-col gap-8">
<Content> <Content>
<h2 class="font-pixel text-xl">Active Mods</h2> <h2 class="font-pixel text-xl">Active Mods</h2>
<FileUploader path="./server/mods"> <FileUploader path="./server/mods">
<div className="relative grid lg:grid-cols-3 min-h-[100px]"> <div class="min-h-[100px]">
{!files.length && ( <div className="relative grid lg:grid-cols-3 gap-8">
<div class="absolute place-self-center">Drop files here to upload</div> {!activeMods.length && (
)} <div class="absolute place-self-center">
{files.map((f) => ( Drop files here to upload
<div class="flex gap-2 items-center"> </div>
<FileIcon fileName={f} /> )}
{f} {activeMods.map((f) => (
</div> <div class="flex justify-between">
))} <div class="flex gap-2 items-center">
<FileIcon fileName={f} />
{f}
</div>
<div class="flex gap-2">
<form action={url.pathname} method="POST">
<input
type="hidden"
name="filePath"
value={"./server/mods/" + f}
/>
<Button type="submit">Disable</Button>
</form>
<form action={url.pathname} method="POST">
<input
type="hidden"
name="filePath"
value={"./server/disabled-mods/" + f}
/>
<input
type="hidden"
name="delete"
value="true"
/>
<Button color="fire" type="submit">
<i class="fas fa-trash"></i>
</Button>
</form>
</div>
</div>
))}
</div>
</div>
</FileUploader>
</Content>
<Content>
<h2 class="font-pixel text-xl">Disabled Mods</h2>
<FileUploader path="./server/disabled-mods">
<div class="min-h-[100px]">
<div className="relative grid lg:grid-cols-3 gap-8">
{!disabledMods.length && (
<div class="absolute place-self-center">
Drop files here to upload
</div>
)}
{disabledMods.map((f) => (
<div class="flex justify-between">
<div class="flex gap-2 items-center">
<FileIcon fileName={f} />
{f}
</div>
<div class="flex gap-2">
<form action={url.pathname} method="POST">
<input
type="hidden"
name="filePath"
value={"./server/disabled-mods/" + f}
/>
<Button type="submit">Enable</Button>
</form>
<form action={url.pathname} method="POST">
<input
type="hidden"
name="filePath"
value={"./server/disabled-mods/" + f}
/>
<input
type="hidden"
name="delete"
value="true"
/>
<Button color="fire" type="submit">
<i class="fas fa-trash"></i>
</Button>
</form>
</div>
</div>
))}
</div>
</div> </div>
</FileUploader> </FileUploader>
</Content> </Content>
@ -39,9 +163,9 @@ export default async function ModsFolder() {
const FileIcon: FunctionComponent<{ fileName: string }> = ({ fileName }) => { const FileIcon: FunctionComponent<{ fileName: string }> = ({ fileName }) => {
let icon; let icon;
switch (fileName.split(".")[1]) { switch (fileName.split(".").at(-1)) {
case "jar": case "jar":
icon = "fa-brand fa-java"; icon = "fab fa-java";
break; break;
case "tmp": case "tmp":
case "temp": case "temp":

View File

@ -56,7 +56,11 @@ class ServerState {
this.sockpuppet = new Sockpuppet( this.sockpuppet = new Sockpuppet(
"ws://sockpuppet.cyborggrizzly.com", "ws://sockpuppet.cyborggrizzly.com",
() => { () => {
this.sockpuppet.joinChannel(this.channelId, (msg) => { this.sockpuppet.joinChannel(this.channelId, async (msg) => {
if (msg === 'log' && !IS_BROWSER) {
const log = await Deno.readTextFile('./server/logs/latest.log');
this.channel?.send(log);
} else
this.sendStdIn(msg); this.sendStdIn(msg);
}); });
}, },

File diff suppressed because one or more lines are too long