Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🧩 Updated the plugins page #18

Merged
merged 12 commits into from
Jul 2, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;

import java.io.File;

public class PluginListRoute extends DefaultHandler {

@Override
Expand All @@ -26,11 +28,17 @@ public void get(Request request, ResponseController response) throws Exception {
ArrayBuilder builder = new ArrayBuilder();

for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) {
if (plugin.getClass().getProtectionDomain().getCodeSource() == null) continue;
String pluginJar = plugin.getClass().getProtectionDomain().getCodeSource().getLocation().getPath();
String pluginJarNoPath = pluginJar.substring(pluginJar.lastIndexOf(File.separator) + 1);

if (!new File("./plugins/" + pluginJarNoPath).exists()) continue;

builder.addNode()
.add("name", plugin.getName())
.add("author", plugin.getDescription().getAuthors().size() == 0 ? null : plugin.getDescription().getAuthors().get(0))
.add("description", plugin.getDescription().getDescription())
.add("path", plugin.getClass().getProtectionDomain().getCodeSource().getLocation().getFile())
.add("path", pluginJarNoPath)
.add("enabled", plugin.isEnabled())
.add("version", plugin.getDescription().getVersion())
.register();
Expand Down
28 changes: 15 additions & 13 deletions src/main/java/de/gnmyt/mcdash/panel/routes/store/StoreRoute.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ public void get(Request request, ResponseController response) throws Exception {
int page = request.getQuery().containsKey("page") ? getIntegerFromQuery(request, "page") : 1;

String base = Objects.equals(query, "") ? "resources" : "search/resources/"
+ URLEncoder.encode(query, StandardCharsets.UTF_8.toString());
+ URLEncoder.encode(query, StandardCharsets.UTF_8.toString()).replace("+", "%20");

HttpUrl url = HttpUrl.parse(ROOT_URL + base).newBuilder()
.addQueryParameter("size", "35")
.addQueryParameter("page", String.valueOf(page))
.addQueryParameter("sort", "-downloads")
.build();

okhttp3.Response httpResponse = client.newCall(new okhttp3.Request.Builder().url(url).build()).execute();
Expand All @@ -52,13 +53,14 @@ public void get(Request request, ResponseController response) throws Exception {
ArrayBuilder items = new ArrayBuilder();

mapper.readTree(httpResponse.body().string()).forEach(item -> {
if (!item.get("external").asBoolean()) new NodeBuilder(items)
.add("id", item.get("id").asInt())
.add("name", item.get("name").asText())
.add("description", item.get("tag").asText())
.add("icon", !item.get("icon").get("data").asText().isEmpty() ? item.get("icon").get("data").asText() : null)
.add("downloads", item.get("downloads").asInt())
.register();
if (!item.get("external").asBoolean() && item.get("file").get("type").asText().equals(".jar"))
new NodeBuilder(items).add("id", item.get("id").asInt())
.add("name", item.get("name").asText())
.add("description", item.get("tag").asText())
.add("icon", !item.get("icon").get("data").asText().isEmpty()
? item.get("icon").get("data").asText() : null)
.add("downloads", item.get("downloads").asInt())
.register();
});

response.type(ContentType.JSON).text(items.toJSON());
Expand All @@ -69,8 +71,7 @@ public void put(Request request, ResponseController response) throws Exception {
if (!isStringInQuery(request, response, "id")) return;

HttpUrl url = HttpUrl.parse(ROOT_URL + "resources/" + URLEncoder.encode(request.getQuery()
.get("id"), UTF_8.toString())).newBuilder().addQueryParameter("game_versions",
"[\"" + Bukkit.getServer().getBukkitVersion().split("-")[0] + "\"]").build();
.get("id"), UTF_8.toString())).newBuilder().build();

okhttp3.Response httpResponse = client.newCall(new okhttp3.Request.Builder().url(url).build()).execute();

Expand All @@ -87,7 +88,7 @@ public void put(Request request, ResponseController response) throws Exception {
return;
}

String fileUrl = ROOT_URL + "resources/"+ URLEncoder.encode(request.getQuery().get("id"), UTF_8.toString())
String fileUrl = ROOT_URL + "resources/" + URLEncoder.encode(request.getQuery().get("id"), UTF_8.toString())
+ "/download";

if (new File("plugins//Managed-" + projectId + ".jar").exists()) {
Expand All @@ -101,9 +102,10 @@ public void put(Request request, ResponseController response) throws Exception {
runSync(() -> {
try {
Bukkit.getPluginManager().loadPlugin(new File("plugins//Managed-" + projectId + ".jar"));
} catch (InvalidPluginException | InvalidDescriptionException e) {
} catch (Exception e) {
FileUtils.deleteQuietly(new File("plugins//Managed-" + projectId + ".jar"));
response.code(400).message("The item with the id '" + projectId + "' is not a valid plugin");
response.code(400).json("message=\"The item with the id '" + projectId
+ "' is not a valid plugin\"", "error=\"" + e.getMessage() + "\"");
return;
}
response.message("The item with the id '" + projectId + "' has been installed");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ export const ActionConfirmDialog = ({open, setOpen, title, description, buttonTe

return (
<>
<Snackbar open={snackbarOpen} autoHideDuration={3000} onClose={() => setSnackbarOpen(false)}
anchorOrigin={{vertical: "bottom", horizontal: "right"}}>
<Alert onClose={() => setSnackbarOpen(false)} severity={actionFailed ? "error" : "success"}
sx={{width: '100%'}}>
{actionFailed ? "Could not execute action" : (successMessage || "Action executed successfully")}
</Alert>
</Snackbar>
{successMessage !== "none" &&
<Snackbar open={snackbarOpen} autoHideDuration={3000} onClose={() => setSnackbarOpen(false)}
anchorOrigin={{vertical: "bottom", horizontal: "right"}}>
<Alert onClose={() => setSnackbarOpen(false)} severity={actionFailed ? "error" : "success"}
sx={{width: '100%'}}>
{actionFailed ? "Could not execute action" : (successMessage || "Action executed successfully")}
</Alert>
</Snackbar>}
<Dialog open={open} onClose={() => setOpen(false)} aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description">
<DialogTitle id="alert-dialog-title">
Expand Down
7 changes: 4 additions & 3 deletions webui/src/states/Root/pages/Plugins/Plugins.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Box, Button, Stack, TextField, Typography} from "@mui/material";
import {Box, Button, Chip, Stack, TextField, Typography} from "@mui/material";
import React, {useContext, useState} from "react";
import {PluginsContext} from "@/states/Root/pages/Plugins/contexts/Plugins";
import {PluginItem} from "@/states/Root/pages/Plugins/components/PluginItem/PluginItem.jsx";
Expand All @@ -15,7 +15,8 @@ export const Plugins = () => {
return (
<>
<Box sx={{display: "flex", alignItems: "center", justifyContent: "space-between", mt: 2, mb: 2}}>
<Typography variant="h5" fontWeight={500}>Plugins</Typography>
<Typography variant="h5" fontWeight={500}>Plugins <Chip label={plugins.length}
color="secondary"/></Typography>

<Stack direction="row" spacing={1}>
{storeOpen && <TextField label="Search" variant="outlined" size={"small"} sx={{width: {xs: 150, lg: 300}}}
Expand All @@ -28,7 +29,7 @@ export const Plugins = () => {
</Box>

{!storeOpen && <Stack direction="row" sx={{my: 1, alignItems: "baseline"}} flexWrap="wrap">
{plugins.map((plugin) => <PluginItem key={plugin.name} {...plugin} />)}
{plugins.map((plugin) => <PluginItem key={plugin.path} {...plugin} />)}
</Stack>}

{storeOpen && <PluginStore search={currentSearch} closeStore={() => setStoreOpen(false)} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
import {Box, Button, Chip, Stack, Typography} from "@mui/material";
import React, {useContext} from "react";
import {Box, Button, Chip, IconButton, Stack, Tooltip, Typography} from "@mui/material";
import React, {useContext, useState} from "react";
import {deleteRequest, postRequest} from "@/common/utils/RequestUtil.js";
import {PluginsContext} from "@/states/Root/pages/Plugins/contexts/Plugins";
import {Delete} from "@mui/icons-material";
import ActionConfirmDialog from "@components/ActionConfirmDialog/index.js";

export const PluginItem = ({name, version, author, description, enabled}) => {
export const PluginItem = ({name, version, author, description, enabled, path}) => {
const {plugins, updatePlugins} = useContext(PluginsContext);

const togglePlugin = (pluginName) => {
const plugin = plugins.find((p) => p.name === pluginName);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);

(!plugin?.enabled ? postRequest("plugin/", {name: pluginName}) : deleteRequest("plugin/", {name: pluginName}))
const togglePlugin = () => {
const plugin = plugins.find((p) => p.name === name);

(!plugin?.enabled ? postRequest("plugin/", {name: name}) : deleteRequest("plugin/", {name: name}))
.then(() => updatePlugins());

return true;
}

const deletePlugin = () => {
if (enabled) togglePlugin();

deleteRequest("filebrowser/file", {path: `plugins/${path}`})
.then(() => updatePlugins());

return true;
Expand All @@ -21,9 +34,19 @@ export const PluginItem = ({name, version, author, description, enabled}) => {
<Typography variant="body2" color="text.secondary">by {author || "Unknown"}</Typography>
<Typography variant="body1">{description || "No description provided"}</Typography>

<Stack direction="row" justifyContent="flex-end" sx={{mt: 1}}>
<ActionConfirmDialog title={`Delete ${name}`} description={`Are you sure you want to delete ${name}? You need to restart your server to apply the changes after deleting.`}
onClick={deletePlugin} successMessage="none"
open={deleteDialogOpen} setOpen={setDeleteDialogOpen} />

<Stack direction="row" justifyContent="flex-end" sx={{mt: 1, alignItems: "center"}} gap={1}>

{name !== "MinecraftDashboard" && <Tooltip title="Delete plugin">
<IconButton size="small" color="error" onClick={() => setDeleteDialogOpen(true)}>
<Delete/>
</IconButton></Tooltip>}

<Button variant="contained" size="small" color={enabled ? "error" : "success"} disabled={name === "MinecraftDashboard"}
onClick={() => togglePlugin(name)}>{enabled ? "Disable" : "Enable"}</Button>
onClick={togglePlugin}>{enabled ? "Disable" : "Enable"}</Button>
</Stack>
</Box>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const PluginStore = ({search, closeStore}) => {
<Stack direction="row" sx={{my: 1, alignItems: "baseline"}} flexWrap="wrap">
{plugins.map((plugin) => <StoreItem {...plugin} key={plugin.id} closeStore={closeStore}
installed={currentPlugins.find((p) => p.path
?.includes("/Managed-" + plugin.id + ".jar"))} />)}
?.startsWith("Managed-" + plugin.id + ".jar"))} />)}

{plugins.length === 0 && <Typography sx={{width: "100%"}} textAlign="center">No plugins found</Typography>}
</Stack>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import {Box, Button, CircularProgress, Stack, Tooltip, Typography} from "@mui/material";
import {Box, Button, CircularProgress, Link, Stack, Tooltip, Typography} from "@mui/material";
import React, {useContext, useState} from "react";
import {Download, Error, Warning} from "@mui/icons-material";
import {request} from "@/common/utils/RequestUtil.js";
import {PluginsContext} from "@/states/Root/pages/Plugins/contexts/Plugins";
import ResourceIcon from "@/common/assets/images/resource.webp";
import {prettyDownloadCount} from "@/states/Root/pages/Plugins/components/PluginStore/components/StoreItem/utils.js";

export const StoreItem = ({id, name, description, icon, downloads, closeStore, installed}) => {
const {updatePlugins} = useContext(PluginsContext);
const [installing, setInstalling] = useState(false);
const [error, setError] = useState(false);
const [error, setError] = useState("");
const [alreadyInstalled, setAlreadyInstalled] = useState(installed);

const install = () => {
setInstalling(true);
request("store/?id=" + id, "PUT", {}, {}, false).then((r) => {
request("store/?id=" + id, "PUT", {}, {}, false).then(async (r) => {
setInstalling(false);

if (r.status === 409) return setAlreadyInstalled(true);
if (!r.ok) return setError(true);
if (!r.ok) return setError((await r.json()).error || "Plugin not supported");

updatePlugins();
closeStore();
Expand All @@ -28,20 +29,26 @@ export const StoreItem = ({id, name, description, icon, downloads, closeStore, i
<Box backgroundColor="background.darker" borderRadius={2} padding={2} sx={{mr: 1, mt: 1, width: {xs: "100%", lg: 300}}}>
<Box sx={{display: "flex", alignItems: "center", justifyContent: "space-between"}}>
<Typography variant="h6" fontWeight={500}>{name}</Typography>
<Box component="img" sx={{width: 40, height: 40, borderRadius: 50}}
src={icon ? ("data:image/png;base64," + icon) : ResourceIcon} alt="icon"/>

<Tooltip title="View resource">
<Link href={"https://www.spigotmc.org/resources/" + id} alt="icon" target="_blank">
<Box component="img" sx={{width: 40, height: 40, borderRadius: 50}}
src={icon ? ("data:image/png;base64," + icon) : ResourceIcon} rel="noreferrer"/>
</Link>
</Tooltip>
</Box>

<Typography variant="body1">{description || "No description provided"}</Typography>

<Stack direction="row" justifyContent="space-between" sx={{mt: 1}}>
<Stack direction="row" alignItems="center" gap={0.5}>
<Download color="secondary"/>
<Typography variant="h6" fontWeight={500}>{downloads}</Typography>
<Typography variant="h6" fontWeight={500}>{prettyDownloadCount(downloads)}</Typography>
</Stack>
<Stack direction="row" alignItems="center" gap={1.5}>
{installing && <CircularProgress size={20} color="secondary" />}
{error && <Tooltip title="Plugin not supported"><Warning color="error" /></Tooltip>}
{error !== "" && <Tooltip title={error}><Warning color="error" /></Tooltip>}

{alreadyInstalled && <Tooltip title="Plugin already installed"><Error color="warning" /></Tooltip>}

<Button variant="contained" color="secondary" size="small" onClick={install}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const prettyDownloadCount = (count) => {
if (count < 1000) return count;
if (count < 1000000) return (count / 1000).toFixed(1) + "K";
return (count / 1000000).toFixed(1) + "M";
}