Skip to content

Commit

Permalink
feat(frontend): wip player managment
Browse files Browse the repository at this point in the history
  • Loading branch information
thewander02 committed Oct 16, 2024
1 parent 33e09c6 commit 9d99b69
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 11 deletions.
89 changes: 89 additions & 0 deletions apps/frontend/src/components/ui/servers/PanelPlayerList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<template>
<div class="flex h-full w-full flex-col gap-4">
<h1 class="m-0 text-2xl font-bold leading-none text-contrast">Players</h1>

<div class="grid grid-cols-3 gap-2 rounded-xl">
<div
v-for="player in players"
:key="player.id"
class="w-full items-center justify-between rounded-lg border border-solid border-divider p-2"
>
<div class="flex w-full items-center justify-between gap-2">
<div class="flex items-center gap-2">
<UiAvatar :src="player.avatar" size="sm" />
<div class="flex flex-col gap-1">
<h1 class="m-0 text-sm font-semibold leading-none text-primary">
{{ player.name }}
</h1>
{{ player.created_at }}
</div>
</div>
<ButtonStyled type="transparent">
<OverflowMenu
:options="[
{
id: 'op',
action: () => {
emit('sendCommand', `op ${player.name}`);
},
},
{
id: 'deop',
action: () => {
emit('sendCommand', `deop ${player.name}`);
},
},
{
id: 'whitelist',
action: () => {
emit('sendCommand', `whitelist add ${player.name}`);
},
},
{
id: 'dewhitelist',
action: () => {
emit('sendCommand', `whitelist remove ${player.name}`);
},
},
{
id: 'kick',
action: () => {
emit('sendCommand', `kick ${player.name}`);
},
},
{
id: 'ban',
action: () => {
emit('sendCommand', `ban ${player.name}`);
},
},
]"
position="bottom"
>
<MoreHorizontalIcon class="h-5 w-5 bg-transparent text-contrast" />

<template #op> Op </template>
<template #deop> Deop </template>
<template #whitelist> Whitelist </template>
<template #dewhitelist> Dewhitelist </template>
<template #kick> Kick </template>
<template #ban> Ban </template>
</OverflowMenu>
</ButtonStyled>
</div>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { MoreHorizontalIcon } from "@modrinth/assets";
import { OverflowMenu } from "@modrinth/ui";
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
const emit = defineEmits(["sendCommand"]);
defineProps<{
players: any[];
}>();
</script>
4 changes: 3 additions & 1 deletion apps/frontend/src/composables/pyroServers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,9 @@ const modules: any = {
data.image = (await processImage(data.project?.icon_url)) ?? undefined;
const motd = await getMotd();
if (motd === "A Minecraft Server") {
await setMotd(`§b${data.project?.title} §f♦ §aModrinth Servers`);
await setMotd(
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
);
}
data.motd = motd;
return data;
Expand Down
56 changes: 55 additions & 1 deletion apps/frontend/src/pages/servers/manage/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
:console-output="consoleOutput"
:socket="socket"
:server="server"
:players="players"
@reinstall="onReinstall"
/>
</div>
Expand Down Expand Up @@ -121,6 +122,9 @@ const ramData = ref<number[]>([]);
const isActioning = ref(false);
const isServerRunning = computed(() => serverPowerState.value === "running");
const serverPowerState = ref<ServerState>("stopped");
const players = ref<string[]>([]);
const isInitialListCommand = ref(true);
const firstOk = ref(true);
const stats = ref<Stats>({
current: {
Expand Down Expand Up @@ -194,7 +198,53 @@ const connectWebSocket = () => {
const handleWebSocketMessage = (data: WSEvent) => {
switch (data.event) {
case "log":
consoleOutput.value.push(...data.message.split("\n").filter((l) => l.trim()));
// eslint-disable-next-line no-case-declarations
let log = data.message.split("\n").filter((l) => l.trim());
// eslint-disable-next-line no-case-declarations
const joinRegex = /(.+) joined the game/;
// eslint-disable-next-line no-case-declarations
const leaveRegex = /(.+) left the game/;
// eslint-disable-next-line no-case-declarations
const playerListRegex = /There are \d+ of a max of \d+ players online: (.+)/;
log.forEach((line) => {
const joinMatch = line.match(joinRegex);
const leaveMatch = line.match(leaveRegex);
const playerListMatch = line.match(playerListRegex);
if (joinMatch && joinMatch[1]) {
const player = joinMatch[1].split(" ")[3];
console.log("Adding player", player);
if (!players.value.includes(player)) {
players.value.push(player);
}
}
if (leaveMatch && leaveMatch[1]) {
const player = leaveMatch[1].split(" ")[3];
console.log("Removing player", player);
players.value = players.value.filter((p) => p !== player);
}
if (playerListMatch && playerListMatch[1]) {
players.value = playerListMatch[1].split(", ");
}
});
console.log(players.value);
if (isInitialListCommand.value) {
log = log.filter((line) => {
if (line.includes("There are") && line.includes("players online")) {
isInitialListCommand.value = false;
return false;
}
return true;
});
}
consoleOutput.value.push(...log);
break;
case "stats":
updateStats(data);
Expand All @@ -210,6 +260,10 @@ const handleWebSocketMessage = (data: WSEvent) => {
handleInstallationResult(data);
break;
case "auth-ok":
if (firstOk.value) {
socket.value?.send(JSON.stringify({ event: "command", cmd: "list" }));
firstOk.value = false;
}
break;
default:
console.warn("Unhandled WebSocket event:", data);
Expand Down
48 changes: 39 additions & 9 deletions apps/frontend/src/pages/servers/manage/[id]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
v-if="suggestions.length"
id="command-suggestions"
ref="suggestionsList"
class="z-20 mt-1 max-h-60 w-full list-none overflow-auto rounded-md border border-divider bg-bg-raised p-0 shadow-lg"
class="mt-1 max-h-60 w-full list-none overflow-auto rounded-md border border-divider bg-bg-raised p-0 shadow-lg"
role="listbox"
>
<li
Expand All @@ -45,32 +45,30 @@
<div class="relative flex items-center">
<span
v-if="bestSuggestion"
class="pointer-events-none absolute left-[26px] z-50 transform select-none text-gray-400"
class="pointer-events-none absolute left-[26px] transform select-none text-gray-400"
>
<span class="ml-[23.5px] whitespace-pre">{{
" ".repeat(commandInput.length - 1)
}}</span>
<span> {{ bestSuggestion }} </span>
<button
class="text pointer-events-auto z-50 ml-2 cursor-pointer rounded-md border-none bg-white text-sm focus:outline-none dark:bg-highlight"
class="text pointer-events-auto ml-2 cursor-pointer rounded-md border-none bg-white text-sm focus:outline-none dark:bg-highlight"
aria-label="Accept suggestion"
style="transform: translateY(-1px)"
@click="acceptSuggestion"
>
TAB
</button>
</span>
<div
class="pointer-events-none absolute left-0 top-0 z-30 flex h-full w-full items-center"
>
<div class="pointer-events-none absolute left-0 top-0 flex h-full w-full items-center">
<TerminalSquareIcon class="ml-3 h-5 w-5" />
</div>
<input
v-if="isServerRunning"
v-model="commandInput"
type="text"
placeholder="Send a command"
class="z-20 w-full rounded-md !pl-10 pt-4 focus:border-none [&&]:border-[1px] [&&]:border-solid [&&]:border-bg-raised [&&]:bg-bg"
class="w-full rounded-md !pl-10 pt-4 focus:border-none [&&]:border-[1px] [&&]:border-solid [&&]:border-bg-raised [&&]:bg-bg"
aria-autocomplete="list"
aria-controls="command-suggestions"
spellcheck="false"
Expand All @@ -85,12 +83,15 @@
disabled
type="text"
placeholder="Send a command"
class="z-50 w-full rounded-md !pl-10 focus:border-none [&&]:border-[1px] [&&]:border-solid [&&]:border-bg-raised [&&]:bg-bg"
class="w-full rounded-md !pl-10 focus:border-none [&&]:border-[1px] [&&]:border-solid [&&]:border-bg-raised [&&]:bg-bg"
/>
</div>
</div>
</UiServersPanelTerminal>
</div>
<div v-if="playerList && playerList.length > 0" class="card">
<UiServersPanelPlayerList :players="playerList" @send-command="sendConsoleCommand" />
</div>
</div>
<UiServersPanelOverviewLoading v-else-if="!isConnected && !isWsAuthIncorrect" />
<UiServersPyroError
Expand All @@ -107,6 +108,7 @@

<script setup lang="ts">
import { TerminalSquareIcon } from "@modrinth/assets";
import { asyncComputed } from "@vueuse/core";
import type { ServerState, Stats } from "~/types/servers";
import type { Server } from "~/composables/pyroServers";
Expand All @@ -119,8 +121,28 @@ const props = defineProps<{
serverPowerState: ServerState;
isServerRunning: boolean;
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
players: string[];
}>();
const playerList = asyncComputed(async () => {
const results = await Promise.all(
props.players.map(async (name) => {
const ply = await $fetch<any>(`https://api.ashcon.app/mojang/v2/user/${name}`, {
method: "GET",
retry: false,
});
return {
name,
id: ply.uuid,
avatar: `https://crafatar.com/avatars/${ply.uuid}`,
name_history: ply.username_history,
created_at: ply.created_at,
};
}),
);
return results;
});
const socket = ref(props.socket);
watch(props, (newAttrs) => {
Expand Down Expand Up @@ -490,7 +512,7 @@ const sendCommand = () => {
const cmd = commandInput.value.trim();
if (!socket || !cmd) return;
try {
socket.value?.send(JSON.stringify({ event: "command", cmd }));
sendConsoleCommand(cmd);
commandInput.value = "";
suggestions.value = [];
selectedSuggestionIndex.value = 0;
Expand All @@ -499,6 +521,14 @@ const sendCommand = () => {
}
};
const sendConsoleCommand = (cmd: string) => {
try {
socket.value?.send(JSON.stringify({ event: "command", cmd }));
} catch (error) {
console.error("Error sending command:", error);
}
};
watch(
() => selectedSuggestionIndex.value,
(newVal) => {
Expand Down

0 comments on commit 9d99b69

Please sign in to comment.