diff --git a/README.md b/README.md index 12e5c20..3ad92e4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -![MultiServer](assets/banner.jpg) +![MultiServer](img/banner.jpg) > The most pragmatic and efficient way to orchestrate multiple Minecraft servers. diff --git a/assets/banner.jpg b/img/banner.jpg similarity index 100% rename from assets/banner.jpg rename to img/banner.jpg diff --git a/img/fabric_logo.png b/img/fabric_logo.png new file mode 100644 index 0000000..4ab8370 Binary files /dev/null and b/img/fabric_logo.png differ diff --git a/img/paper_logo.png b/img/paper_logo.png new file mode 100644 index 0000000..c494760 Binary files /dev/null and b/img/paper_logo.png differ diff --git a/img/vanilla_logo.png b/img/vanilla_logo.png new file mode 100644 index 0000000..4136007 Binary files /dev/null and b/img/vanilla_logo.png differ diff --git a/package.json b/package.json index 8322fb5..f25a2a5 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "packagerConfig": { "extraResource": [ "./resources/server.properties", - "./resources/fabric-installer-0.9.0.jar" + "./resources/fabric-installer.jar" ] }, "makers": [ @@ -94,10 +94,12 @@ "electron": "16.0.1", "eslint": "^7.6.0", "eslint-plugin-import": "^2.20.0", + "file-loader": "^6.2.0", "fork-ts-checker-webpack-plugin": "^6.0.1", "node-loader": "^2.0.0", "postcss": "^8.3.11", "postcss-loader": "^6.2.0", + "prettier": "^2.5.0", "style-loader": "^3.0.0", "tailwindcss": "^2.2.19", "ts-loader": "^9.2.2", @@ -109,6 +111,7 @@ "node-fetch": "^3.1.0", "react": "^17.0.2", "react-dom": "^17.0.2", + "sanitize-filename": "^1.6.3", "update-electron-app": "^2.0.1" } } diff --git a/src/components/Instance.tsx b/src/components/Instance.tsx new file mode 100644 index 0000000..c2c9c8c --- /dev/null +++ b/src/components/Instance.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { InstanceOptions } from "../types"; + +import fabricLogo from "../../img/fabric_logo.png"; +import paperLogo from "../../img/paper_logo.png"; +import vanillaLogo from "../../img/vanilla_logo.png"; + +const images = { + fabric: fabricLogo, + paper: paperLogo, + vanilla: vanillaLogo, +} as const; + +interface InstanceProps { + info: InstanceOptions; +} + +const Instance = ({ info }: InstanceProps): JSX.Element => { + return ( +
+ +

{info.name}

+
+ ); +}; + +export default Instance; diff --git a/src/global.d.ts b/src/global.d.ts index 75cc72b..08d68ff 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,6 +1,10 @@ import type { IpcChannels } from "./types"; declare global { + declare module "*.png" { + const src: string; + export default src; + } interface Window { ipc: IpcChannels; log: import("electron-log").LogFunctions; diff --git a/src/index.ts b/src/index.ts index fc57d2d..bb1ff37 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import updater from "update-electron-app"; import log from "electron-log"; import createInstance from "./lib/instances/createInstance"; +import { getInstances } from "./lib/instances/getInstances"; // declarations for webpack magic constants for built react code declare const MAIN_WINDOW_WEBPACK_ENTRY: string; @@ -19,9 +20,11 @@ if (require("electron-squirrel-startup")) { updater(); -const createWindow = (): void => { +let mainWindow: BrowserWindow; + +const createWindow = () => { // Create the browser window. - const mainWindow = new BrowserWindow({ + mainWindow = new BrowserWindow({ height: 600, width: 800, webPreferences: { @@ -72,8 +75,10 @@ ipcMain.on("newInstanceWindow", () => { ipcMain.on("closeWindow", (e) => { const sender = BrowserWindow.fromWebContents(e.sender); - log.debug(sender); sender?.close(); }); ipcMain.handle("createInstance", createInstance); +ipcMain.handle("getInstances", getInstances); + +export const getMainWindow = (): BrowserWindow => mainWindow; diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000..0adba8a --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,9 @@ +import { app } from "electron"; + +import path from "path"; + +export const resourcesPath = app.isPackaged + ? process.resourcesPath + : path.join(process.cwd(), "resources"); + +export const instancesPath = path.join(app.getPath("userData"), "instances"); diff --git a/src/lib/instances/createInstance.ts b/src/lib/instances/createInstance.ts index b045642..073739a 100644 --- a/src/lib/instances/createInstance.ts +++ b/src/lib/instances/createInstance.ts @@ -1,6 +1,7 @@ -import { app, IpcMainInvokeEvent } from "electron"; +import type { IpcMainInvokeEvent } from "electron"; import fetch from "node-fetch"; import log from "electron-log"; +import sanitize from "sanitize-filename"; import https from "https"; import fs from "fs/promises"; @@ -8,6 +9,8 @@ import { createWriteStream } from "fs"; import path from "path"; import cp from "child_process"; +import { getMainWindow } from "../../index"; +import { instancesPath, resourcesPath } from "../constants"; import type { InstanceOptions } from "../../types"; /** @@ -19,17 +22,20 @@ export default async function create( _event: IpcMainInvokeEvent, opts: InstanceOptions ): Promise { - const instanceRoot = path.join( - app.getPath("userData"), - "instances", - opts.name - ); + try { + const sanitizedName = sanitize( + opts.name.toLowerCase().replace(/\s/g, "_") + ); - const resourcesPath = app.isPackaged - ? process.resourcesPath - : path.join(process.cwd(), "resources"); + if (sanitizedName === "") { + log.error(`Invalid instance name ${opts.name}`); + throw new Error(`Invalid instance name ${opts.name}`); + } + + const instanceRoot = path.join(instancesPath, sanitizedName); + + // TODO: check if instance already exists - try { log.info( `Creating directory for ${opts.type} server instance ${opts.name}` ); @@ -38,7 +44,7 @@ export default async function create( log.silly("Writing configuration file"); await fs.writeFile( path.join(instanceRoot, "multiserver.config.json"), - JSON.stringify(opts, (v) => v, 4) + JSON.stringify(opts, undefined, 4) ); log.silly("Writing eula.txt"); @@ -50,9 +56,6 @@ export default async function create( path.join(instanceRoot, "server.properties") ); - // TODO: check if instance already exists - // TODO: fabric - if (opts.type === "fabric") { log.debug("Calling fabric installer"); @@ -122,6 +125,9 @@ export default async function create( } catch (e) { log.error(e); return false; + } finally { + // way less work than having to wire up an IPC event to let the main window know that theres an new instance + setTimeout(() => getMainWindow().reload(), 500); } } diff --git a/src/lib/instances/getInstances.ts b/src/lib/instances/getInstances.ts new file mode 100644 index 0000000..609e5c8 --- /dev/null +++ b/src/lib/instances/getInstances.ts @@ -0,0 +1,23 @@ +import fs from "fs/promises"; +import path from "path"; + +import type { InstanceOptions, InstanceInfo } from "../../types"; +import { instancesPath } from "../constants"; + +export async function getInstances(): Promise { + const instances = await fs.readdir(instancesPath); + + return Promise.all( + instances.map(async (instance) => { + const conf = await fs.readFile( + path.join(instancesPath, instance, "multiserver.config.json"), + "utf8" + ); + + return { + path: path.join(instancesPath, instance), + info: JSON.parse(conf) as InstanceOptions, + }; + }) + ); +} diff --git a/src/preload.ts b/src/preload.ts index ff7811e..9139cb9 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -9,6 +9,7 @@ contextBridge.exposeInMainWorld("ipc", { newInstanceWindow: () => ipcRenderer.send("newInstanceWindow"), createInstance: (opts) => ipcRenderer.invoke("createInstance", opts), closeWindow: () => ipcRenderer.send("closeWindow"), + getInstances: () => ipcRenderer.invoke("getInstances"), } as IpcChannels); contextBridge.exposeInMainWorld("log", log.functions); diff --git a/src/types.ts b/src/types.ts index 4b27560..5515735 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,22 +1,19 @@ export interface InstanceOptions { name: string; - type: - | "vanilla" - | "spigot" - | "bukkit" - | "paper" - | "fabric" - | "purpur" - | "tuinity" - | "fabric"; - + type: "vanilla" | "paper" | "fabric"; version: string; javaPath?: string; jvmArgs?: string; } +export interface InstanceInfo { + path: string; + info: InstanceOptions; +} + export type IpcChannels = { newInstanceWindow: () => void; createInstance: (opts: InstanceOptions) => Promise; closeWindow: () => void; + getInstances: () => Promise; }; diff --git a/src/windows/MainWIndow.tsx b/src/windows/MainWIndow.tsx index e8fb467..e73edf6 100644 --- a/src/windows/MainWIndow.tsx +++ b/src/windows/MainWIndow.tsx @@ -1,17 +1,36 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import ReactDOM from "react-dom"; -import "tailwindcss/tailwind.css"; +import type { InstanceInfo } from "../types"; import "../app.global.css"; +import Instance from "../components/Instance"; const MainWindow = () => { + const [instances, setInstances] = useState([]); + + useEffect(() => { + const fetchInstances = () => + void ipc + .getInstances() + .then(setInstances) + .catch((err) => log.error(err)); + + fetchInstances(); + }, []); + return (

Welcome to MultiServer

- {/* TODO: add instances view */} + +
+ {instances.map((instance) => ( + + ))} +
+ diff --git a/webpack.rules.js b/webpack.rules.js index 8a34116..ce5d91b 100644 --- a/webpack.rules.js +++ b/webpack.rules.js @@ -69,4 +69,11 @@ module.exports = [ }, ], }, + { + test: /\.(png|jpe?g|gif|jp2|webp)$/, + loader: "file-loader", + options: { + name: "[name].[ext]", + }, + }, ]; diff --git a/yarn.lock b/yarn.lock index 9a11f06..ab72547 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2749,6 +2749,14 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-loader@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + filename-reserved-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" @@ -5243,6 +5251,11 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= +prettier@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.0.tgz#a6370e2d4594e093270419d9cc47f7670488f893" + integrity sha512-FM/zAKgWTxj40rH03VxzIPdXmj39SwSjwG0heUcNFwI+EMZJnY93yAiKXM3dObIKAM5TA88werc8T/EwhB45eg== + pretty-bytes@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" @@ -5747,6 +5760,13 @@ safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sanitize-filename@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" + integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg== + dependencies: + truncate-utf8-bytes "^1.0.0" + scheduler@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" @@ -5764,7 +5784,7 @@ schema-utils@2.7.0: ajv "^6.12.2" ajv-keywords "^3.4.1" -schema-utils@^3.1.0, schema-utils@^3.1.1: +schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== @@ -6469,6 +6489,13 @@ trim-repeated@^1.0.0: dependencies: escape-string-regexp "^1.0.2" +truncate-utf8-bytes@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" + integrity sha1-QFkjkJWS1W94pYGENLC3hInKXys= + dependencies: + utf8-byte-length "^1.0.1" + ts-loader@^9.2.2: version "9.2.6" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.6.tgz#9937c4dd0a1e3dbbb5e433f8102a6601c6615d74" @@ -6647,6 +6674,11 @@ username@^5.1.0: execa "^1.0.0" mem "^4.3.0" +utf8-byte-length@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" + integrity sha1-9F8VDExm7uloGGUFq5P8u4rWv2E= + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"