You've already forked AstralRinth
forked from didirus/AstralRinth
refactor: migrate to common eslint+prettier configs (#4168)
* refactor: migrate to common eslint+prettier configs * fix: prettier frontend * feat: config changes * fix: lint issues * fix: lint * fix: type imports * fix: cyclical import issue * fix: lockfile * fix: missing dep * fix: switch to tabs * fix: continue switch to tabs * fix: rustfmt parity * fix: moderation lint issue * fix: lint issues * fix: ui intl * fix: lint issues * Revert "fix: rustfmt parity" This reverts commit cb99d2376c321d813d4b7fc7e2a213bb30a54711. * feat: revert last rs
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="universal-card">
|
||||
<p>You can manage your server's billing from Settings > Billing and subscriptions.</p>
|
||||
<ButtonStyled>
|
||||
<NuxtLink to="/settings/billing">Go to Billing</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="universal-card">
|
||||
<p>You can manage your server's billing from Settings > Billing and subscriptions.</p>
|
||||
<ButtonStyled>
|
||||
<NuxtLink to="/settings/billing">Go to Billing</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
</script>
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
<template>
|
||||
<div class="relative h-full w-full overflow-y-auto">
|
||||
<div v-if="data" class="flex h-full w-full flex-col">
|
||||
<div class="gap-2">
|
||||
<div class="card flex flex-col gap-4">
|
||||
<label for="server-name-field" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Server name</span>
|
||||
<span> This name is only visible on Modrinth.</span>
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
id="server-name-field"
|
||||
v-model="serverName"
|
||||
class="w-full md:w-[50%]"
|
||||
maxlength="48"
|
||||
minlength="1"
|
||||
@keyup.enter="!serverName && saveGeneral"
|
||||
/>
|
||||
<span v-if="!serverName" class="text-sm text-rose-400">
|
||||
Server name must be at least 1 character long.
|
||||
</span>
|
||||
<span v-if="!isValidServerName" class="text-sm text-rose-400">
|
||||
Server name can contain any character.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- WIP - disable for now
|
||||
<div class="relative h-full w-full overflow-y-auto">
|
||||
<div v-if="data" class="flex h-full w-full flex-col">
|
||||
<div class="gap-2">
|
||||
<div class="card flex flex-col gap-4">
|
||||
<label for="server-name-field" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Server name</span>
|
||||
<span> This name is only visible on Modrinth.</span>
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
id="server-name-field"
|
||||
v-model="serverName"
|
||||
class="w-full md:w-[50%]"
|
||||
maxlength="48"
|
||||
minlength="1"
|
||||
@keyup.enter="!serverName && saveGeneral"
|
||||
/>
|
||||
<span v-if="!serverName" class="text-sm text-rose-400">
|
||||
Server name must be at least 1 character long.
|
||||
</span>
|
||||
<span v-if="!isValidServerName" class="text-sm text-rose-400">
|
||||
Server name can contain any character.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- WIP - disable for now
|
||||
<div class="card flex flex-col gap-4">
|
||||
<label for="server-motd-field" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Server MOTD</span>
|
||||
@@ -36,294 +36,293 @@
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div class="card flex flex-col gap-4">
|
||||
<label for="server-subdomain" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Custom URL</span>
|
||||
<span> Your friends can connect to your server using this URL. </span>
|
||||
</label>
|
||||
<div class="flex w-full items-center gap-2 md:w-[60%]">
|
||||
<input
|
||||
id="server-subdomain"
|
||||
v-model="serverSubdomain"
|
||||
class="h-[50%] w-[63%]"
|
||||
maxlength="32"
|
||||
@keyup.enter="saveGeneral"
|
||||
/>
|
||||
.modrinth.gg
|
||||
</div>
|
||||
<div v-if="!isValidSubdomain" class="flex flex-col text-sm text-rose-400">
|
||||
<span v-if="!isValidLengthSubdomain">
|
||||
Subdomain must be at least 5 characters long.
|
||||
</span>
|
||||
<span v-if="!isValidCharsSubdomain">
|
||||
Subdomain can only contain alphanumeric characters and dashes.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card flex flex-col gap-4">
|
||||
<label for="server-subdomain" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Custom URL</span>
|
||||
<span> Your friends can connect to your server using this URL. </span>
|
||||
</label>
|
||||
<div class="flex w-full items-center gap-2 md:w-[60%]">
|
||||
<input
|
||||
id="server-subdomain"
|
||||
v-model="serverSubdomain"
|
||||
class="h-[50%] w-[63%]"
|
||||
maxlength="32"
|
||||
@keyup.enter="saveGeneral"
|
||||
/>
|
||||
.modrinth.gg
|
||||
</div>
|
||||
<div v-if="!isValidSubdomain" class="flex flex-col text-sm text-rose-400">
|
||||
<span v-if="!isValidLengthSubdomain">
|
||||
Subdomain must be at least 5 characters long.
|
||||
</span>
|
||||
<span v-if="!isValidCharsSubdomain">
|
||||
Subdomain can only contain alphanumeric characters and dashes.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col gap-4">
|
||||
<label for="server-icon-field" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Server icon</span>
|
||||
<span> This icon will be visible on the Minecraft server list and on Modrinth. </span>
|
||||
</label>
|
||||
<div class="flex gap-4">
|
||||
<div
|
||||
v-tooltip="'Upload a custom Icon'"
|
||||
class="group relative flex w-fit cursor-pointer items-center gap-2 rounded-xl bg-table-alternateRow"
|
||||
@dragover.prevent="onDragOver"
|
||||
@dragleave.prevent="onDragLeave"
|
||||
@drop.prevent="onDrop"
|
||||
@click="triggerFileInput"
|
||||
>
|
||||
<input
|
||||
v-if="icon"
|
||||
id="server-icon-field"
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
hidden
|
||||
@change="uploadFile"
|
||||
/>
|
||||
<div
|
||||
class="absolute top-0 hidden size-[6rem] flex-col items-center justify-center rounded-xl bg-button-bg p-2 opacity-80 group-hover:flex"
|
||||
>
|
||||
<EditIcon class="h-8 w-8 text-contrast" />
|
||||
</div>
|
||||
<UiServersServerIcon :image="icon" />
|
||||
</div>
|
||||
<ButtonStyled>
|
||||
<button v-tooltip="'Synchronize icon with installed modpack'" @click="resetIcon">
|
||||
<TransferIcon class="h-6 w-6" />
|
||||
<span>Sync icon</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else />
|
||||
<UiServersSaveBanner
|
||||
:is-visible="!!hasUnsavedChanges && !!isValidServerName"
|
||||
:server="props.server"
|
||||
:is-updating="isUpdating"
|
||||
:save="saveGeneral"
|
||||
:reset="resetGeneral"
|
||||
/>
|
||||
</div>
|
||||
<div class="card flex flex-col gap-4">
|
||||
<label for="server-icon-field" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Server icon</span>
|
||||
<span> This icon will be visible on the Minecraft server list and on Modrinth. </span>
|
||||
</label>
|
||||
<div class="flex gap-4">
|
||||
<div
|
||||
v-tooltip="'Upload a custom Icon'"
|
||||
class="group relative flex w-fit cursor-pointer items-center gap-2 rounded-xl bg-table-alternateRow"
|
||||
@dragover.prevent="onDragOver"
|
||||
@dragleave.prevent="onDragLeave"
|
||||
@drop.prevent="onDrop"
|
||||
@click="triggerFileInput"
|
||||
>
|
||||
<input
|
||||
v-if="icon"
|
||||
id="server-icon-field"
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
hidden
|
||||
@change="uploadFile"
|
||||
/>
|
||||
<div
|
||||
class="absolute top-0 hidden size-[6rem] flex-col items-center justify-center rounded-xl bg-button-bg p-2 opacity-80 group-hover:flex"
|
||||
>
|
||||
<EditIcon class="h-8 w-8 text-contrast" />
|
||||
</div>
|
||||
<UiServersServerIcon :image="icon" />
|
||||
</div>
|
||||
<ButtonStyled>
|
||||
<button v-tooltip="'Synchronize icon with installed modpack'" @click="resetIcon">
|
||||
<TransferIcon class="h-6 w-6" />
|
||||
<span>Sync icon</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else />
|
||||
<UiServersSaveBanner
|
||||
:is-visible="!!hasUnsavedChanges && !!isValidServerName"
|
||||
:server="props.server"
|
||||
:is-updating="isUpdating"
|
||||
:save="saveGeneral"
|
||||
:reset="resetGeneral"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { EditIcon, TransferIcon } from "@modrinth/assets";
|
||||
import { injectNotificationManager } from "@modrinth/ui";
|
||||
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import { EditIcon, TransferIcon } from '@modrinth/assets'
|
||||
import { injectNotificationManager } from '@modrinth/ui'
|
||||
import ButtonStyled from '@modrinth/ui/src/components/base/ButtonStyled.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager();
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const data = computed(() => props.server.general);
|
||||
const serverName = ref(data.value?.name);
|
||||
const serverSubdomain = ref(data.value?.net?.domain ?? "");
|
||||
const isValidLengthSubdomain = computed(() => serverSubdomain.value.length >= 5);
|
||||
const isValidCharsSubdomain = computed(() => /^[a-zA-Z0-9-]+$/.test(serverSubdomain.value));
|
||||
const isValidSubdomain = computed(
|
||||
() => isValidLengthSubdomain.value && isValidCharsSubdomain.value,
|
||||
);
|
||||
const icon = computed(() => data.value?.image);
|
||||
const data = computed(() => props.server.general)
|
||||
const serverName = ref(data.value?.name)
|
||||
const serverSubdomain = ref(data.value?.net?.domain ?? '')
|
||||
const isValidLengthSubdomain = computed(() => serverSubdomain.value.length >= 5)
|
||||
const isValidCharsSubdomain = computed(() => /^[a-zA-Z0-9-]+$/.test(serverSubdomain.value))
|
||||
const isValidSubdomain = computed(() => isValidLengthSubdomain.value && isValidCharsSubdomain.value)
|
||||
const icon = computed(() => data.value?.image)
|
||||
|
||||
const isUpdating = ref(false);
|
||||
const isUpdating = ref(false)
|
||||
const hasUnsavedChanges = computed(
|
||||
() =>
|
||||
(serverName.value && serverName.value !== data.value?.name) ||
|
||||
serverSubdomain.value !== data.value?.net?.domain,
|
||||
);
|
||||
const isValidServerName = computed(() => (serverName.value?.length ?? 0) > 0);
|
||||
() =>
|
||||
(serverName.value && serverName.value !== data.value?.name) ||
|
||||
serverSubdomain.value !== data.value?.net?.domain,
|
||||
)
|
||||
const isValidServerName = computed(() => (serverName.value?.length ?? 0) > 0)
|
||||
|
||||
watch(serverName, (oldValue) => {
|
||||
if (!isValidServerName.value) {
|
||||
serverName.value = oldValue;
|
||||
}
|
||||
});
|
||||
if (!isValidServerName.value) {
|
||||
serverName.value = oldValue
|
||||
}
|
||||
})
|
||||
|
||||
const saveGeneral = async () => {
|
||||
if (!isValidServerName.value || !isValidSubdomain.value) return;
|
||||
if (!isValidServerName.value || !isValidSubdomain.value) return
|
||||
|
||||
try {
|
||||
isUpdating.value = true;
|
||||
if (serverName.value !== data.value?.name) {
|
||||
await data.value?.updateName(serverName.value ?? "");
|
||||
}
|
||||
if (serverSubdomain.value !== data.value?.net?.domain) {
|
||||
try {
|
||||
// type shit backend makes me do
|
||||
const available = await props.server.network?.checkSubdomainAvailability(
|
||||
serverSubdomain.value,
|
||||
);
|
||||
try {
|
||||
isUpdating.value = true
|
||||
if (serverName.value !== data.value?.name) {
|
||||
await data.value?.updateName(serverName.value ?? '')
|
||||
}
|
||||
if (serverSubdomain.value !== data.value?.net?.domain) {
|
||||
try {
|
||||
// type shit backend makes me do
|
||||
const available = await props.server.network?.checkSubdomainAvailability(
|
||||
serverSubdomain.value,
|
||||
)
|
||||
|
||||
if (!available) {
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Subdomain not available",
|
||||
text: "The subdomain you entered is already in use.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!available) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Subdomain not available',
|
||||
text: 'The subdomain you entered is already in use.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await props.server.network?.changeSubdomain(serverSubdomain.value);
|
||||
} catch (error) {
|
||||
console.error("Error checking subdomain availability:", error);
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Error checking availability",
|
||||
text: "Failed to verify if the subdomain is available.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await props.server.refresh();
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: "Server settings updated",
|
||||
text: "Your server settings were successfully changed.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Failed to update server settings",
|
||||
text: "An error occurred while attempting to update your server settings.",
|
||||
});
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
};
|
||||
await props.server.network?.changeSubdomain(serverSubdomain.value)
|
||||
} catch (error) {
|
||||
console.error('Error checking subdomain availability:', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error checking availability',
|
||||
text: 'Failed to verify if the subdomain is available.',
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
await props.server.refresh()
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Server settings updated',
|
||||
text: 'Your server settings were successfully changed.',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to update server settings',
|
||||
text: 'An error occurred while attempting to update your server settings.',
|
||||
})
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetGeneral = () => {
|
||||
serverName.value = data.value?.name || "";
|
||||
serverSubdomain.value = data.value?.net?.domain ?? "";
|
||||
};
|
||||
serverName.value = data.value?.name || ''
|
||||
serverSubdomain.value = data.value?.net?.domain ?? ''
|
||||
}
|
||||
|
||||
const uploadFile = async (e: Event) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (!file) {
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "No file selected",
|
||||
text: "Please select a file to upload.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (!file) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'No file selected',
|
||||
text: 'Please select a file to upload.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const scaledFile = await new Promise<File>((resolve, reject) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
ctx?.drawImage(img, 0, 0, 64, 64);
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(new File([blob], "server-icon.png", { type: "image/png" }));
|
||||
} else {
|
||||
reject(new Error("Canvas toBlob failed"));
|
||||
}
|
||||
}, "image/png");
|
||||
URL.revokeObjectURL(img.src);
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
const scaledFile = await new Promise<File>((resolve, reject) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
canvas.width = 64
|
||||
canvas.height = 64
|
||||
ctx?.drawImage(img, 0, 0, 64, 64)
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(new File([blob], 'server-icon.png', { type: 'image/png' }))
|
||||
} else {
|
||||
reject(new Error('Canvas toBlob failed'))
|
||||
}
|
||||
}, 'image/png')
|
||||
URL.revokeObjectURL(img.src)
|
||||
}
|
||||
img.onerror = reject
|
||||
img.src = URL.createObjectURL(file)
|
||||
})
|
||||
|
||||
try {
|
||||
if (data.value?.image) {
|
||||
await props.server.fs?.deleteFileOrFolder("/server-icon.png", false);
|
||||
await props.server.fs?.deleteFileOrFolder("/server-icon-original.png", false);
|
||||
}
|
||||
try {
|
||||
if (data.value?.image) {
|
||||
await props.server.fs?.deleteFileOrFolder('/server-icon.png', false)
|
||||
await props.server.fs?.deleteFileOrFolder('/server-icon-original.png', false)
|
||||
}
|
||||
|
||||
await props.server.fs?.uploadFile("/server-icon.png", scaledFile);
|
||||
await props.server.fs?.uploadFile("/server-icon-original.png", file);
|
||||
await props.server.fs?.uploadFile('/server-icon.png', scaledFile)
|
||||
await props.server.fs?.uploadFile('/server-icon-original.png', file)
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const img = new Image();
|
||||
await new Promise<void>((resolve) => {
|
||||
img.onload = () => {
|
||||
canvas.width = 512;
|
||||
canvas.height = 512;
|
||||
ctx?.drawImage(img, 0, 0, 512, 512);
|
||||
const dataURL = canvas.toDataURL("image/png");
|
||||
useState(`server-icon-${props.server.serverId}`).value = dataURL;
|
||||
if (data.value) data.value.image = dataURL;
|
||||
resolve();
|
||||
URL.revokeObjectURL(img.src);
|
||||
};
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
const img = new Image()
|
||||
await new Promise<void>((resolve) => {
|
||||
img.onload = () => {
|
||||
canvas.width = 512
|
||||
canvas.height = 512
|
||||
ctx?.drawImage(img, 0, 0, 512, 512)
|
||||
const dataURL = canvas.toDataURL('image/png')
|
||||
useState(`server-icon-${props.server.serverId}`).value = dataURL
|
||||
if (data.value) data.value.image = dataURL
|
||||
resolve()
|
||||
URL.revokeObjectURL(img.src)
|
||||
}
|
||||
img.src = URL.createObjectURL(file)
|
||||
})
|
||||
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: "Server icon updated",
|
||||
text: "Your server icon was successfully changed.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error uploading icon:", error);
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Upload failed",
|
||||
text: "Failed to upload server icon.",
|
||||
});
|
||||
}
|
||||
};
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Server icon updated',
|
||||
text: 'Your server icon was successfully changed.',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error uploading icon:', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Upload failed',
|
||||
text: 'Failed to upload server icon.',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resetIcon = async () => {
|
||||
if (data.value?.image) {
|
||||
try {
|
||||
await props.server.fs?.deleteFileOrFolder("/server-icon.png", false);
|
||||
await props.server.fs?.deleteFileOrFolder("/server-icon-original.png", false);
|
||||
if (data.value?.image) {
|
||||
try {
|
||||
await props.server.fs?.deleteFileOrFolder('/server-icon.png', false)
|
||||
await props.server.fs?.deleteFileOrFolder('/server-icon-original.png', false)
|
||||
|
||||
useState(`server-icon-${props.server.serverId}`).value = undefined;
|
||||
if (data.value) data.value.image = undefined;
|
||||
useState(`server-icon-${props.server.serverId}`).value = undefined
|
||||
if (data.value) data.value.image = undefined
|
||||
|
||||
await props.server.refresh(["general"]);
|
||||
await props.server.refresh(['general'])
|
||||
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: "Server icon reset",
|
||||
text: "Your server icon was successfully reset.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error resetting icon:", error);
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Reset failed",
|
||||
text: "Failed to reset server icon.",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Server icon reset',
|
||||
text: 'Your server icon was successfully reset.',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error resetting icon:', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Reset failed',
|
||||
text: 'Failed to reset server icon.',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const onDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const onDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
uploadFile(e);
|
||||
};
|
||||
e.preventDefault()
|
||||
uploadFile(e)
|
||||
}
|
||||
|
||||
const triggerFileInput = () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.id = "server-icon-field";
|
||||
input.accept = "image/png,image/jpeg,image/gif,image/webp";
|
||||
input.onchange = uploadFile;
|
||||
input.click();
|
||||
};
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.id = 'server-icon-field'
|
||||
input.accept = 'image/png,image/jpeg,image/gif,image/webp'
|
||||
input.onchange = uploadFile
|
||||
input.click()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,154 +1,157 @@
|
||||
<template>
|
||||
<div class="h-full w-full gap-2 overflow-y-auto">
|
||||
<div class="card">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col justify-between gap-4 sm:flex-row">
|
||||
<label class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">SFTP</span>
|
||||
<span> SFTP allows you to access your server's files from outside of Modrinth. </span>
|
||||
</label>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
v-tooltip="'This button only works with compatible SFTP clients (e.g. WinSCP)'"
|
||||
class="!w-full sm:!w-auto"
|
||||
@click="openSftp"
|
||||
>
|
||||
<ExternalIcon class="h-5 w-5" />
|
||||
Launch SFTP
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="h-full w-full gap-2 overflow-y-auto">
|
||||
<div class="card">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col justify-between gap-4 sm:flex-row">
|
||||
<label class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">SFTP</span>
|
||||
<span> SFTP allows you to access your server's files from outside of Modrinth. </span>
|
||||
</label>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
v-tooltip="'This button only works with compatible SFTP clients (e.g. WinSCP)'"
|
||||
class="!w-full sm:!w-auto"
|
||||
@click="openSftp"
|
||||
>
|
||||
<ExternalIcon class="h-5 w-5" />
|
||||
Launch SFTP
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex w-full flex-row justify-between gap-2 rounded-xl bg-table-alternateRow p-4"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="cursor-pointer font-bold text-contrast">
|
||||
{{ data?.sftp_host }}
|
||||
</span>
|
||||
<div
|
||||
class="flex w-full flex-row justify-between gap-2 rounded-xl bg-table-alternateRow p-4"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="cursor-pointer font-bold text-contrast">
|
||||
{{ data?.sftp_host }}
|
||||
</span>
|
||||
|
||||
<span class="text-xs text-secondary">Server Address</span>
|
||||
</div>
|
||||
<span class="text-xs text-secondary">Server Address</span>
|
||||
</div>
|
||||
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
v-tooltip="'Copy SFTP server address'"
|
||||
@click="copyToClipboard('Server address', data?.sftp_host)"
|
||||
>
|
||||
<CopyIcon class="h-5 w-5 hover:cursor-pointer" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="-mt-2 flex flex-col gap-2 sm:mt-0 sm:flex-row">
|
||||
<div
|
||||
class="flex w-full flex-col justify-center gap-2 rounded-xl bg-table-alternateRow px-4 py-2"
|
||||
>
|
||||
<div class="flex h-8 items-center justify-between">
|
||||
<span class="font-bold text-contrast">
|
||||
{{ data?.sftp_username }}
|
||||
</span>
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
v-tooltip="'Copy SFTP server address'"
|
||||
@click="copyToClipboard('Server address', data?.sftp_host)"
|
||||
>
|
||||
<CopyIcon class="h-5 w-5 hover:cursor-pointer" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="-mt-2 flex flex-col gap-2 sm:mt-0 sm:flex-row">
|
||||
<div
|
||||
class="flex w-full flex-col justify-center gap-2 rounded-xl bg-table-alternateRow px-4 py-2"
|
||||
>
|
||||
<div class="flex h-8 items-center justify-between">
|
||||
<span class="font-bold text-contrast">
|
||||
{{ data?.sftp_username }}
|
||||
</span>
|
||||
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
v-tooltip="'Copy SFTP username'"
|
||||
@click="copyToClipboard('Username', data?.sftp_username)"
|
||||
>
|
||||
<CopyIcon class="h-5 w-5 hover:cursor-pointer" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<span class="text-xs text-secondary">Username</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex w-full flex-col justify-center gap-2 rounded-xl bg-table-alternateRow p-4"
|
||||
>
|
||||
<div class="flex h-8 items-center justify-between">
|
||||
<span class="font-bold text-contrast">
|
||||
{{
|
||||
showPassword ? data?.sftp_password : "*".repeat(data?.sftp_password?.length ?? 0)
|
||||
}}
|
||||
</span>
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
v-tooltip="'Copy SFTP username'"
|
||||
@click="copyToClipboard('Username', data?.sftp_username)"
|
||||
>
|
||||
<CopyIcon class="h-5 w-5 hover:cursor-pointer" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<span class="text-xs text-secondary">Username</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex w-full flex-col justify-center gap-2 rounded-xl bg-table-alternateRow p-4"
|
||||
>
|
||||
<div class="flex h-8 items-center justify-between">
|
||||
<span class="font-bold text-contrast">
|
||||
{{
|
||||
showPassword ? data?.sftp_password : '*'.repeat(data?.sftp_password?.length ?? 0)
|
||||
}}
|
||||
</span>
|
||||
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
v-tooltip="'Copy SFTP password'"
|
||||
@click="copyToClipboard('Password', data?.sftp_password)"
|
||||
>
|
||||
<CopyIcon class="h-5 w-5 hover:cursor-pointer" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
v-tooltip="showPassword ? 'Hide password' : 'Show password'"
|
||||
@click="togglePassword"
|
||||
>
|
||||
<EyeIcon v-if="showPassword" class="h-5 w-5 hover:cursor-pointer" />
|
||||
<EyeOffIcon v-else class="h-5 w-5 hover:cursor-pointer" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-secondary">Password</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="text-xl font-bold">Info</h2>
|
||||
<div class="rounded-xl bg-table-alternateRow p-4">
|
||||
<table
|
||||
class="min-w-full border-collapse overflow-hidden rounded-lg border-2 border-gray-300"
|
||||
>
|
||||
<tbody>
|
||||
<tr v-for="property in properties" :key="property.name">
|
||||
<td v-if="property.value !== 'Unknown'" class="py-3">{{ property.name }}</td>
|
||||
<td v-if="property.value !== 'Unknown'" class="px-4">
|
||||
<CopyCode :text="property.value" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
v-tooltip="'Copy SFTP password'"
|
||||
@click="copyToClipboard('Password', data?.sftp_password)"
|
||||
>
|
||||
<CopyIcon class="h-5 w-5 hover:cursor-pointer" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
v-tooltip="showPassword ? 'Hide password' : 'Show password'"
|
||||
@click="togglePassword"
|
||||
>
|
||||
<EyeIcon v-if="showPassword" class="h-5 w-5 hover:cursor-pointer" />
|
||||
<EyeOffIcon v-else class="h-5 w-5 hover:cursor-pointer" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-secondary">Password</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="text-xl font-bold">Info</h2>
|
||||
<div class="rounded-xl bg-table-alternateRow p-4">
|
||||
<table
|
||||
class="min-w-full border-collapse overflow-hidden rounded-lg border-2 border-gray-300"
|
||||
>
|
||||
<tbody>
|
||||
<tr v-for="property in properties" :key="property.name">
|
||||
<td v-if="property.value !== 'Unknown'" class="py-3">
|
||||
{{ property.name }}
|
||||
</td>
|
||||
<td v-if="property.value !== 'Unknown'" class="px-4">
|
||||
<CopyCode :text="property.value" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CopyIcon, ExternalIcon, EyeIcon, EyeOffIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, CopyCode, injectNotificationManager } from "@modrinth/ui";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import { CopyIcon, ExternalIcon, EyeIcon, EyeOffIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, CopyCode, injectNotificationManager } from '@modrinth/ui'
|
||||
|
||||
const { addNotification } = injectNotificationManager();
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const data = computed(() => props.server.general);
|
||||
const showPassword = ref(false);
|
||||
const data = computed(() => props.server.general)
|
||||
const showPassword = ref(false)
|
||||
|
||||
const openSftp = () => {
|
||||
const sftpUrl = `sftp://${data.value?.sftp_username}@${data.value?.sftp_host}`;
|
||||
window.open(sftpUrl, "_blank");
|
||||
};
|
||||
const sftpUrl = `sftp://${data.value?.sftp_username}@${data.value?.sftp_host}`
|
||||
window.open(sftpUrl, '_blank')
|
||||
}
|
||||
|
||||
const togglePassword = () => {
|
||||
showPassword.value = !showPassword.value;
|
||||
};
|
||||
showPassword.value = !showPassword.value
|
||||
}
|
||||
|
||||
const copyToClipboard = (name: string, textToCopy?: string) => {
|
||||
navigator.clipboard.writeText(textToCopy || "");
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: `${name} copied to clipboard!`,
|
||||
});
|
||||
};
|
||||
navigator.clipboard.writeText(textToCopy || '')
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: `${name} copied to clipboard!`,
|
||||
})
|
||||
}
|
||||
|
||||
const properties = [
|
||||
{ name: "Server ID", value: props.server.serverId ?? "Unknown" },
|
||||
{ name: "Node", value: data.value?.node?.instance ?? "Unknown" },
|
||||
{ name: "Kind", value: data.value?.upstream?.kind ?? data.value?.loader ?? "Unknown" },
|
||||
{ name: "Project ID", value: data.value?.upstream?.project_id ?? "Unknown" },
|
||||
{ name: "Version ID", value: data.value?.upstream?.version_id ?? "Unknown" },
|
||||
];
|
||||
{ name: 'Server ID', value: props.server.serverId ?? 'Unknown' },
|
||||
{ name: 'Node', value: data.value?.node?.instance ?? 'Unknown' },
|
||||
{ name: 'Kind', value: data.value?.upstream?.kind ?? data.value?.loader ?? 'Unknown' },
|
||||
{ name: 'Project ID', value: data.value?.upstream?.project_id ?? 'Unknown' },
|
||||
{ name: 'Version ID', value: data.value?.upstream?.version_id ?? 'Unknown' },
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<template>
|
||||
<ServerInstallation
|
||||
:server="props.server"
|
||||
:backup-in-progress="props.backupInProgress"
|
||||
@reinstall="emit('reinstall')"
|
||||
/>
|
||||
<ServerInstallation
|
||||
:server="props.server"
|
||||
:backup-in-progress="props.backupInProgress"
|
||||
@reinstall="emit('reinstall')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import ServerInstallation from '~/components/ui/servers/ServerInstallation.vue'
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
import type { BackupInProgressReason } from '~/pages/servers/manage/[id].vue'
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
backupInProgress?: BackupInProgressReason
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
reinstall: [any?];
|
||||
}>();
|
||||
reinstall: [any?]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -1,492 +1,490 @@
|
||||
<template>
|
||||
<div class="contents">
|
||||
<NewModal ref="newAllocationModal" header="New allocation">
|
||||
<form class="flex flex-col gap-2 md:w-[600px]" @submit.prevent="addNewAllocation">
|
||||
<label for="new-allocation-name" class="font-semibold text-contrast"> Name </label>
|
||||
<input
|
||||
id="new-allocation-name"
|
||||
ref="newAllocationInput"
|
||||
v-model="newAllocationName"
|
||||
type="text"
|
||||
class="bg-bg-input w-full rounded-lg p-4"
|
||||
maxlength="32"
|
||||
placeholder="e.g. Secondary allocation"
|
||||
/>
|
||||
<div class="mb-1 mt-4 flex justify-start gap-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!newAllocationName" type="submit">
|
||||
<PlusIcon /> Create allocation
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="newAllocationModal?.hide()">Cancel</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</form>
|
||||
</NewModal>
|
||||
<div class="contents">
|
||||
<NewModal ref="newAllocationModal" header="New allocation">
|
||||
<form class="flex flex-col gap-2 md:w-[600px]" @submit.prevent="addNewAllocation">
|
||||
<label for="new-allocation-name" class="font-semibold text-contrast"> Name </label>
|
||||
<input
|
||||
id="new-allocation-name"
|
||||
ref="newAllocationInput"
|
||||
v-model="newAllocationName"
|
||||
type="text"
|
||||
class="bg-bg-input w-full rounded-lg p-4"
|
||||
maxlength="32"
|
||||
placeholder="e.g. Secondary allocation"
|
||||
/>
|
||||
<div class="mb-1 mt-4 flex justify-start gap-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!newAllocationName" type="submit">
|
||||
<PlusIcon /> Create allocation
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="newAllocationModal?.hide()">Cancel</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</form>
|
||||
</NewModal>
|
||||
|
||||
<NewModal ref="editAllocationModal" header="Edit allocation">
|
||||
<form class="flex flex-col gap-2 md:w-[600px]" @submit.prevent="editAllocation">
|
||||
<label for="edit-allocation-name" class="font-semibold text-contrast"> Name </label>
|
||||
<input
|
||||
id="edit-allocation-name"
|
||||
ref="editAllocationInput"
|
||||
v-model="newAllocationName"
|
||||
type="text"
|
||||
class="bg-bg-input w-full rounded-lg p-4"
|
||||
maxlength="32"
|
||||
placeholder="e.g. Secondary allocation"
|
||||
/>
|
||||
<div class="mb-1 mt-4 flex justify-start gap-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!newAllocationName" type="submit">
|
||||
<SaveIcon /> Update allocation
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="editAllocationModal?.hide()">Cancel</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</form>
|
||||
</NewModal>
|
||||
<NewModal ref="editAllocationModal" header="Edit allocation">
|
||||
<form class="flex flex-col gap-2 md:w-[600px]" @submit.prevent="editAllocation">
|
||||
<label for="edit-allocation-name" class="font-semibold text-contrast"> Name </label>
|
||||
<input
|
||||
id="edit-allocation-name"
|
||||
ref="editAllocationInput"
|
||||
v-model="newAllocationName"
|
||||
type="text"
|
||||
class="bg-bg-input w-full rounded-lg p-4"
|
||||
maxlength="32"
|
||||
placeholder="e.g. Secondary allocation"
|
||||
/>
|
||||
<div class="mb-1 mt-4 flex justify-start gap-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!newAllocationName" type="submit">
|
||||
<SaveIcon /> Update allocation
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="editAllocationModal?.hide()">Cancel</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</form>
|
||||
</NewModal>
|
||||
|
||||
<ConfirmModal
|
||||
ref="confirmDeleteModal"
|
||||
title="Deleting allocation"
|
||||
:description="`You are deleting the allocation ${allocationToDelete}. This cannot be reserved again. Are you sure you want to proceed?`"
|
||||
proceed-label="Delete"
|
||||
@proceed="confirmDeleteAllocation"
|
||||
/>
|
||||
<ConfirmModal
|
||||
ref="confirmDeleteModal"
|
||||
title="Deleting allocation"
|
||||
:description="`You are deleting the allocation ${allocationToDelete}. This cannot be reserved again. Are you sure you want to proceed?`"
|
||||
proceed-label="Delete"
|
||||
@proceed="confirmDeleteAllocation"
|
||||
/>
|
||||
|
||||
<div class="relative h-full w-full overflow-y-auto">
|
||||
<div
|
||||
v-if="server.moduleErrors.network"
|
||||
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<IssuesIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Failed to load network settings</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
We couldn't load your server's network settings. Here's what we know:
|
||||
<span class="break-all font-mono">{{
|
||||
JSON.stringify(server.moduleErrors.network.error)
|
||||
}}</span>
|
||||
</p>
|
||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['network'])">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="data" class="flex h-full w-full flex-col justify-between gap-4">
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Subdomain section -->
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="flex w-full flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
<label for="user-domain" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Generated DNS records</span>
|
||||
<span>
|
||||
Set up your personal domain to connect to your server via custom DNS records.
|
||||
</span>
|
||||
</label>
|
||||
<div class="relative h-full w-full overflow-y-auto">
|
||||
<div
|
||||
v-if="server.moduleErrors.network"
|
||||
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<IssuesIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Failed to load network settings</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
We couldn't load your server's network settings. Here's what we know:
|
||||
<span class="break-all font-mono">{{
|
||||
JSON.stringify(server.moduleErrors.network.error)
|
||||
}}</span>
|
||||
</p>
|
||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['network'])">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="data" class="flex h-full w-full flex-col justify-between gap-4">
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Subdomain section -->
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="flex w-full flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
<label for="user-domain" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Generated DNS records</span>
|
||||
<span>
|
||||
Set up your personal domain to connect to your server via custom DNS records.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<ButtonStyled>
|
||||
<button
|
||||
class="!w-full sm:!w-auto"
|
||||
:disabled="userDomain == ''"
|
||||
@click="exportDnsRecords"
|
||||
>
|
||||
<UploadIcon />
|
||||
<span>Export DNS records</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
class="!w-full sm:!w-auto"
|
||||
:disabled="userDomain == ''"
|
||||
@click="exportDnsRecords"
|
||||
>
|
||||
<UploadIcon />
|
||||
<span>Export DNS records</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<input
|
||||
id="user-domain"
|
||||
v-model="userDomain"
|
||||
class="w-full md:w-[50%]"
|
||||
maxlength="64"
|
||||
minlength="1"
|
||||
type="text"
|
||||
:placeholder="exampleDomain"
|
||||
/>
|
||||
<input
|
||||
id="user-domain"
|
||||
v-model="userDomain"
|
||||
class="w-full md:w-[50%]"
|
||||
maxlength="64"
|
||||
minlength="1"
|
||||
type="text"
|
||||
:placeholder="exampleDomain"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex max-w-full flex-none overflow-auto rounded-xl bg-table-alternateRow px-4 py-2"
|
||||
>
|
||||
<table
|
||||
class="w-full flex-none border-collapse truncate rounded-lg border-2 border-gray-300"
|
||||
>
|
||||
<tbody class="w-full">
|
||||
<tr v-for="record in dnsRecords" :key="record.content" class="w-full">
|
||||
<td class="w-1/6 py-3 pr-4 md:w-1/5 md:pr-8 lg:w-1/4 lg:pr-12">
|
||||
<div class="flex flex-col gap-1" @click="copyText(record.type)">
|
||||
<span
|
||||
class="text-md font-bold tracking-wide text-contrast hover:cursor-pointer"
|
||||
>
|
||||
{{ record.type }}
|
||||
</span>
|
||||
<span class="text-xs text-secondary">Type</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="w-2/6 py-3 md:w-1/3">
|
||||
<div class="flex flex-col gap-1" @click="copyText(record.name)">
|
||||
<span
|
||||
class="text-md truncate font-bold tracking-wide text-contrast hover:cursor-pointer"
|
||||
>
|
||||
{{ record.name }}
|
||||
</span>
|
||||
<span class="text-xs text-secondary">Name</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="w-3/6 py-3 pl-4 md:w-5/12 lg:w-5/12">
|
||||
<div class="flex flex-col gap-1" @click="copyText(record.content)">
|
||||
<span
|
||||
class="text-md w-fit truncate font-bold tracking-wide text-contrast hover:cursor-pointer"
|
||||
>
|
||||
{{ record.content }}
|
||||
</span>
|
||||
<span class="text-xs text-secondary">Content</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
class="flex max-w-full flex-none overflow-auto rounded-xl bg-table-alternateRow px-4 py-2"
|
||||
>
|
||||
<table
|
||||
class="w-full flex-none border-collapse truncate rounded-lg border-2 border-gray-300"
|
||||
>
|
||||
<tbody class="w-full">
|
||||
<tr v-for="record in dnsRecords" :key="record.content" class="w-full">
|
||||
<td class="w-1/6 py-3 pr-4 md:w-1/5 md:pr-8 lg:w-1/4 lg:pr-12">
|
||||
<div class="flex flex-col gap-1" @click="copyText(record.type)">
|
||||
<span
|
||||
class="text-md font-bold tracking-wide text-contrast hover:cursor-pointer"
|
||||
>
|
||||
{{ record.type }}
|
||||
</span>
|
||||
<span class="text-xs text-secondary">Type</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="w-2/6 py-3 md:w-1/3">
|
||||
<div class="flex flex-col gap-1" @click="copyText(record.name)">
|
||||
<span
|
||||
class="text-md truncate font-bold tracking-wide text-contrast hover:cursor-pointer"
|
||||
>
|
||||
{{ record.name }}
|
||||
</span>
|
||||
<span class="text-xs text-secondary">Name</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="w-3/6 py-3 pl-4 md:w-5/12 lg:w-5/12">
|
||||
<div class="flex flex-col gap-1" @click="copyText(record.content)">
|
||||
<span
|
||||
class="text-md w-fit truncate font-bold tracking-wide text-contrast hover:cursor-pointer"
|
||||
>
|
||||
{{ record.content }}
|
||||
</span>
|
||||
<span class="text-xs text-secondary">Content</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<InfoIcon class="hidden sm:block" />
|
||||
<span class="text-sm text-secondary">
|
||||
You must own your own domain to use this feature.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<InfoIcon class="hidden sm:block" />
|
||||
<span class="text-sm text-secondary">
|
||||
You must own your own domain to use this feature.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Allocations section -->
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="flex w-full flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Allocations</span>
|
||||
<span>
|
||||
Configure additional ports for internet-facing features like map viewers or voice
|
||||
chat mods.
|
||||
</span>
|
||||
</div>
|
||||
<!-- Allocations section -->
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="flex w-full flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Allocations</span>
|
||||
<span>
|
||||
Configure additional ports for internet-facing features like map viewers or voice
|
||||
chat mods.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ButtonStyled type="standard" @click="showNewAllocationModal">
|
||||
<button class="!w-full sm:!w-auto">
|
||||
<PlusIcon />
|
||||
<span>New allocation</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<ButtonStyled type="standard" @click="showNewAllocationModal">
|
||||
<button class="!w-full sm:!w-auto">
|
||||
<PlusIcon />
|
||||
<span>New allocation</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col overflow-hidden rounded-xl bg-table-alternateRow p-4">
|
||||
<!-- Primary allocation -->
|
||||
<div class="flex flex-col justify-between gap-2 sm:flex-row sm:items-center">
|
||||
<span class="text-md font-bold tracking-wide text-contrast">
|
||||
Primary allocation
|
||||
</span>
|
||||
<div class="flex w-full flex-col overflow-hidden rounded-xl bg-table-alternateRow p-4">
|
||||
<!-- Primary allocation -->
|
||||
<div class="flex flex-col justify-between gap-2 sm:flex-row sm:items-center">
|
||||
<span class="text-md font-bold tracking-wide text-contrast">
|
||||
Primary allocation
|
||||
</span>
|
||||
|
||||
<CopyCode :text="`${serverIP}:${serverPrimaryPort}`" />
|
||||
</div>
|
||||
</div>
|
||||
<CopyCode :text="`${serverIP}:${serverPrimaryPort}`" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="allocations?.[0]"
|
||||
class="flex w-full flex-col gap-4 overflow-hidden rounded-xl bg-table-alternateRow p-4"
|
||||
>
|
||||
<div
|
||||
v-for="allocation in allocations"
|
||||
:key="allocation.port"
|
||||
class="border-border flex flex-col justify-between gap-4 sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<VersionIcon class="h-7 w-7 flex-none rotate-90" />
|
||||
<div class="flex w-[20rem] flex-col justify-between sm:flex-row sm:items-center">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-md font-bold tracking-wide text-contrast">
|
||||
{{ allocation.name }}
|
||||
</span>
|
||||
<span class="hidden text-xs text-secondary sm:block">Name</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span
|
||||
class="text-md w-10 tracking-wide text-secondary sm:font-bold sm:text-contrast"
|
||||
>
|
||||
{{ allocation.port }}
|
||||
</span>
|
||||
<span class="hidden text-xs text-secondary sm:block">Port</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="allocations?.[0]"
|
||||
class="flex w-full flex-col gap-4 overflow-hidden rounded-xl bg-table-alternateRow p-4"
|
||||
>
|
||||
<div
|
||||
v-for="allocation in allocations"
|
||||
:key="allocation.port"
|
||||
class="border-border flex flex-col justify-between gap-4 sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<VersionIcon class="h-7 w-7 flex-none rotate-90" />
|
||||
<div class="flex w-[20rem] flex-col justify-between sm:flex-row sm:items-center">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-md font-bold tracking-wide text-contrast">
|
||||
{{ allocation.name }}
|
||||
</span>
|
||||
<span class="hidden text-xs text-secondary sm:block">Name</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span
|
||||
class="text-md w-10 tracking-wide text-secondary sm:font-bold sm:text-contrast"
|
||||
>
|
||||
{{ allocation.port }}
|
||||
</span>
|
||||
<span class="hidden text-xs text-secondary sm:block">Port</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-row items-center gap-2 sm:w-auto">
|
||||
<CopyCode :text="`${serverIP}:${allocation.port}`" />
|
||||
<ButtonStyled icon-only>
|
||||
<button
|
||||
class="!w-full sm:!w-auto"
|
||||
@click="showEditAllocationModal(allocation.port)"
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled icon-only color="red">
|
||||
<button
|
||||
class="!w-full sm:!w-auto"
|
||||
@click="showConfirmDeleteModal(allocation.port)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UiServersSaveBanner
|
||||
:is-visible="!!hasUnsavedChanges && !!isValidSubdomain"
|
||||
:server="props.server"
|
||||
:is-updating="isUpdating"
|
||||
:save="saveNetwork"
|
||||
:reset="resetNetwork"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-row items-center gap-2 sm:w-auto">
|
||||
<CopyCode :text="`${serverIP}:${allocation.port}`" />
|
||||
<ButtonStyled icon-only>
|
||||
<button
|
||||
class="!w-full sm:!w-auto"
|
||||
@click="showEditAllocationModal(allocation.port)"
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled icon-only color="red">
|
||||
<button
|
||||
class="!w-full sm:!w-auto"
|
||||
@click="showConfirmDeleteModal(allocation.port)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UiServersSaveBanner
|
||||
:is-visible="!!hasUnsavedChanges && !!isValidSubdomain"
|
||||
:server="props.server"
|
||||
:is-updating="isUpdating"
|
||||
:save="saveNetwork"
|
||||
:reset="resetNetwork"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
EditIcon,
|
||||
InfoIcon,
|
||||
IssuesIcon,
|
||||
PlusIcon,
|
||||
SaveIcon,
|
||||
TrashIcon,
|
||||
UploadIcon,
|
||||
VersionIcon,
|
||||
} from "@modrinth/assets";
|
||||
EditIcon,
|
||||
InfoIcon,
|
||||
IssuesIcon,
|
||||
PlusIcon,
|
||||
SaveIcon,
|
||||
TrashIcon,
|
||||
UploadIcon,
|
||||
VersionIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
ConfirmModal,
|
||||
CopyCode,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
} from "@modrinth/ui";
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
ButtonStyled,
|
||||
ConfirmModal,
|
||||
CopyCode,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager();
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const isUpdating = ref(false);
|
||||
const data = computed(() => props.server.general);
|
||||
const isUpdating = ref(false)
|
||||
const data = computed(() => props.server.general)
|
||||
|
||||
const serverIP = ref(data?.value?.net?.ip ?? "");
|
||||
const serverSubdomain = ref(data?.value?.net?.domain ?? "");
|
||||
const serverPrimaryPort = ref(data?.value?.net?.port ?? 0);
|
||||
const userDomain = ref("");
|
||||
const exampleDomain = "play.example.com";
|
||||
const serverIP = ref(data?.value?.net?.ip ?? '')
|
||||
const serverSubdomain = ref(data?.value?.net?.domain ?? '')
|
||||
const serverPrimaryPort = ref(data?.value?.net?.port ?? 0)
|
||||
const userDomain = ref('')
|
||||
const exampleDomain = 'play.example.com'
|
||||
|
||||
const network = computed(() => props.server.network);
|
||||
const allocations = computed(() => network.value?.allocations);
|
||||
const network = computed(() => props.server.network)
|
||||
const allocations = computed(() => network.value?.allocations)
|
||||
|
||||
const newAllocationModal = ref<typeof NewModal>();
|
||||
const editAllocationModal = ref<typeof NewModal>();
|
||||
const confirmDeleteModal = ref<typeof ConfirmModal>();
|
||||
const newAllocationInput = ref<HTMLInputElement | null>(null);
|
||||
const editAllocationInput = ref<HTMLInputElement | null>(null);
|
||||
const newAllocationName = ref("");
|
||||
const newAllocationPort = ref(0);
|
||||
const allocationToDelete = ref<number | null>(null);
|
||||
const newAllocationModal = ref<typeof NewModal>()
|
||||
const editAllocationModal = ref<typeof NewModal>()
|
||||
const confirmDeleteModal = ref<typeof ConfirmModal>()
|
||||
const newAllocationInput = ref<HTMLInputElement | null>(null)
|
||||
const editAllocationInput = ref<HTMLInputElement | null>(null)
|
||||
const newAllocationName = ref('')
|
||||
const newAllocationPort = ref(0)
|
||||
const allocationToDelete = ref<number | null>(null)
|
||||
|
||||
const hasUnsavedChanges = computed(() => serverSubdomain.value !== data?.value?.net?.domain);
|
||||
const hasUnsavedChanges = computed(() => serverSubdomain.value !== data?.value?.net?.domain)
|
||||
|
||||
const isValidSubdomain = computed(() => /^[a-zA-Z0-9-]{5,}$/.test(serverSubdomain.value));
|
||||
const isValidSubdomain = computed(() => /^[a-zA-Z0-9-]{5,}$/.test(serverSubdomain.value))
|
||||
|
||||
const addNewAllocation = async () => {
|
||||
if (!newAllocationName.value) return;
|
||||
if (!newAllocationName.value) return
|
||||
|
||||
try {
|
||||
await props.server.network?.reserveAllocation(newAllocationName.value);
|
||||
await props.server.refresh(["network"]);
|
||||
try {
|
||||
await props.server.network?.reserveAllocation(newAllocationName.value)
|
||||
await props.server.refresh(['network'])
|
||||
|
||||
newAllocationModal.value?.hide();
|
||||
newAllocationName.value = "";
|
||||
newAllocationModal.value?.hide()
|
||||
newAllocationName.value = ''
|
||||
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: "Allocation reserved",
|
||||
text: "Your allocation has been reserved.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to reserve new allocation:", error);
|
||||
}
|
||||
};
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Allocation reserved',
|
||||
text: 'Your allocation has been reserved.',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to reserve new allocation:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const showNewAllocationModal = () => {
|
||||
newAllocationName.value = "";
|
||||
newAllocationModal.value?.show();
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
newAllocationInput.value?.focus();
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
newAllocationName.value = ''
|
||||
newAllocationModal.value?.show()
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
newAllocationInput.value?.focus()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
const showEditAllocationModal = (port: number) => {
|
||||
newAllocationPort.value = port;
|
||||
editAllocationModal.value?.show();
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
editAllocationInput.value?.focus();
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
newAllocationPort.value = port
|
||||
editAllocationModal.value?.show()
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
editAllocationInput.value?.focus()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
const showConfirmDeleteModal = (port: number) => {
|
||||
allocationToDelete.value = port;
|
||||
confirmDeleteModal.value?.show();
|
||||
};
|
||||
allocationToDelete.value = port
|
||||
confirmDeleteModal.value?.show()
|
||||
}
|
||||
|
||||
const confirmDeleteAllocation = async () => {
|
||||
if (allocationToDelete.value === null) return;
|
||||
if (allocationToDelete.value === null) return
|
||||
|
||||
await props.server.network?.deleteAllocation(allocationToDelete.value);
|
||||
await props.server.refresh(["network"]);
|
||||
await props.server.network?.deleteAllocation(allocationToDelete.value)
|
||||
await props.server.refresh(['network'])
|
||||
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: "Allocation removed",
|
||||
text: "Your allocation has been removed.",
|
||||
});
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Allocation removed',
|
||||
text: 'Your allocation has been removed.',
|
||||
})
|
||||
|
||||
allocationToDelete.value = null;
|
||||
};
|
||||
allocationToDelete.value = null
|
||||
}
|
||||
|
||||
const editAllocation = async () => {
|
||||
if (!newAllocationName.value) return;
|
||||
if (!newAllocationName.value) return
|
||||
|
||||
try {
|
||||
await props.server.network?.updateAllocation(newAllocationPort.value, newAllocationName.value);
|
||||
await props.server.refresh(["network"]);
|
||||
try {
|
||||
await props.server.network?.updateAllocation(newAllocationPort.value, newAllocationName.value)
|
||||
await props.server.refresh(['network'])
|
||||
|
||||
editAllocationModal.value?.hide();
|
||||
newAllocationName.value = "";
|
||||
editAllocationModal.value?.hide()
|
||||
newAllocationName.value = ''
|
||||
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: "Allocation updated",
|
||||
text: "Your allocation has been updated.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to reserve new allocation:", error);
|
||||
}
|
||||
};
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Allocation updated',
|
||||
text: 'Your allocation has been updated.',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to reserve new allocation:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const saveNetwork = async () => {
|
||||
if (!isValidSubdomain.value) return;
|
||||
if (!isValidSubdomain.value) return
|
||||
|
||||
try {
|
||||
isUpdating.value = true;
|
||||
const available = await props.server.network?.checkSubdomainAvailability(serverSubdomain.value);
|
||||
if (!available) {
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Subdomain not available",
|
||||
text: "The subdomain you entered is already in use.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (serverSubdomain.value !== data?.value?.net?.domain) {
|
||||
await props.server.network?.changeSubdomain(serverSubdomain.value);
|
||||
}
|
||||
if (serverPrimaryPort.value !== data?.value?.net?.port) {
|
||||
await props.server.network?.updateAllocation(
|
||||
serverPrimaryPort.value,
|
||||
newAllocationName.value,
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await props.server.refresh();
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: "Server settings updated",
|
||||
text: "Your server settings were successfully changed.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Failed to update server settings",
|
||||
text: "An error occurred while attempting to update your server settings.",
|
||||
});
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
};
|
||||
try {
|
||||
isUpdating.value = true
|
||||
const available = await props.server.network?.checkSubdomainAvailability(serverSubdomain.value)
|
||||
if (!available) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Subdomain not available',
|
||||
text: 'The subdomain you entered is already in use.',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (serverSubdomain.value !== data?.value?.net?.domain) {
|
||||
await props.server.network?.changeSubdomain(serverSubdomain.value)
|
||||
}
|
||||
if (serverPrimaryPort.value !== data?.value?.net?.port) {
|
||||
await props.server.network?.updateAllocation(serverPrimaryPort.value, newAllocationName.value)
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
await props.server.refresh()
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Server settings updated',
|
||||
text: 'Your server settings were successfully changed.',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to update server settings',
|
||||
text: 'An error occurred while attempting to update your server settings.',
|
||||
})
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetNetwork = () => {
|
||||
serverSubdomain.value = data?.value?.net?.domain ?? "";
|
||||
};
|
||||
serverSubdomain.value = data?.value?.net?.domain ?? ''
|
||||
}
|
||||
|
||||
const dnsRecords = computed(() => {
|
||||
const domain = userDomain.value === "" ? exampleDomain : userDomain.value;
|
||||
return [
|
||||
{
|
||||
type: "A",
|
||||
name: `${domain}`,
|
||||
content: data.value?.net?.ip ?? "",
|
||||
},
|
||||
{
|
||||
type: "SRV",
|
||||
name: `_minecraft._tcp.${domain}`,
|
||||
content: `0 10 ${data.value?.net?.port} ${domain}`,
|
||||
},
|
||||
];
|
||||
});
|
||||
const domain = userDomain.value === '' ? exampleDomain : userDomain.value
|
||||
return [
|
||||
{
|
||||
type: 'A',
|
||||
name: `${domain}`,
|
||||
content: data.value?.net?.ip ?? '',
|
||||
},
|
||||
{
|
||||
type: 'SRV',
|
||||
name: `_minecraft._tcp.${domain}`,
|
||||
content: `0 10 ${data.value?.net?.port} ${domain}`,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const exportDnsRecords = () => {
|
||||
const records = dnsRecords.value.reduce(
|
||||
(acc, record) => {
|
||||
const type = record.type;
|
||||
if (!acc[type]) {
|
||||
acc[type] = [];
|
||||
}
|
||||
acc[type].push(record);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any[]>,
|
||||
);
|
||||
const records = dnsRecords.value.reduce(
|
||||
(acc, record) => {
|
||||
const type = record.type
|
||||
if (!acc[type]) {
|
||||
acc[type] = []
|
||||
}
|
||||
acc[type].push(record)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any[]>,
|
||||
)
|
||||
|
||||
const text = Object.entries(records)
|
||||
.map(([type, records]) => {
|
||||
return `; ${type} Records\n${records.map((record) => `${record.name}. 1 IN ${record.type} ${record.content}${record.type === "SRV" ? "." : ""}`).join("\n")}\n`;
|
||||
})
|
||||
.join("\n");
|
||||
const blob = new Blob([text], { type: "text/plain" });
|
||||
const a = document.createElement("a");
|
||||
a.href = window.URL.createObjectURL(blob);
|
||||
a.download = `${userDomain.value}.txt`;
|
||||
a.click();
|
||||
a.remove();
|
||||
};
|
||||
const text = Object.entries(records)
|
||||
.map(([type, records]) => {
|
||||
return `; ${type} Records\n${records.map((record) => `${record.name}. 1 IN ${record.type} ${record.content}${record.type === 'SRV' ? '.' : ''}`).join('\n')}\n`
|
||||
})
|
||||
.join('\n')
|
||||
const blob = new Blob([text], { type: 'text/plain' })
|
||||
const a = document.createElement('a')
|
||||
a.href = window.URL.createObjectURL(blob)
|
||||
a.download = `${userDomain.value}.txt`
|
||||
a.click()
|
||||
a.remove()
|
||||
}
|
||||
|
||||
const copyText = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: "Text copied",
|
||||
text: `${text} has been copied to your clipboard`,
|
||||
});
|
||||
};
|
||||
navigator.clipboard.writeText(text)
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Text copied',
|
||||
text: `${text} has been copied to your clipboard`,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,129 +1,130 @@
|
||||
<template>
|
||||
<div class="h-full w-full">
|
||||
<div class="h-full w-full gap-2 overflow-y-auto">
|
||||
<div class="card flex flex-col gap-4">
|
||||
<h1 class="m-0 text-lg font-bold text-contrast">Server preferences</h1>
|
||||
<p class="m-0">Preferences apply per server and changes are only saved in your browser.</p>
|
||||
<div
|
||||
v-for="(prefConfig, key) in preferences"
|
||||
:key="key"
|
||||
class="flex items-center justify-between gap-2"
|
||||
>
|
||||
<label :for="`pref-${key}`" class="flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-2">
|
||||
<span class="text-lg font-bold text-contrast">{{ prefConfig.displayName }}</span>
|
||||
<div
|
||||
v-if="prefConfig.implemented === false"
|
||||
class="hidden items-center gap-1 rounded-full bg-table-alternateRow p-1 px-1.5 text-xs font-semibold sm:flex"
|
||||
>
|
||||
Coming Soon
|
||||
</div>
|
||||
</div>
|
||||
<span>{{ prefConfig.description }}</span>
|
||||
</label>
|
||||
<input
|
||||
:id="`pref-${key}`"
|
||||
v-model="newUserPreferences[key]"
|
||||
class="switch stylized-toggle flex-none"
|
||||
type="checkbox"
|
||||
:disabled="prefConfig.implemented === false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UiServersSaveBanner
|
||||
:is-visible="hasUnsavedChanges"
|
||||
:server="props.server"
|
||||
:is-updating="false"
|
||||
:save="savePreferences"
|
||||
:reset="resetPreferences"
|
||||
/>
|
||||
</div>
|
||||
<div class="h-full w-full">
|
||||
<div class="h-full w-full gap-2 overflow-y-auto">
|
||||
<div class="card flex flex-col gap-4">
|
||||
<h1 class="m-0 text-lg font-bold text-contrast">Server preferences</h1>
|
||||
<p class="m-0">Preferences apply per server and changes are only saved in your browser.</p>
|
||||
<div
|
||||
v-for="(prefConfig, key) in preferences"
|
||||
:key="key"
|
||||
class="flex items-center justify-between gap-2"
|
||||
>
|
||||
<label :for="`pref-${key}`" class="flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-2">
|
||||
<span class="text-lg font-bold text-contrast">{{ prefConfig.displayName }}</span>
|
||||
<div
|
||||
v-if="prefConfig.implemented === false"
|
||||
class="hidden items-center gap-1 rounded-full bg-table-alternateRow p-1 px-1.5 text-xs font-semibold sm:flex"
|
||||
>
|
||||
Coming Soon
|
||||
</div>
|
||||
</div>
|
||||
<span>{{ prefConfig.description }}</span>
|
||||
</label>
|
||||
<input
|
||||
:id="`pref-${key}`"
|
||||
v-model="newUserPreferences[key]"
|
||||
class="switch stylized-toggle flex-none"
|
||||
type="checkbox"
|
||||
:disabled="prefConfig.implemented === false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UiServersSaveBanner
|
||||
:is-visible="hasUnsavedChanges"
|
||||
:server="props.server"
|
||||
:is-updating="false"
|
||||
:save="savePreferences"
|
||||
:reset="resetPreferences"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { injectNotificationManager } from "@modrinth/ui";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import { injectNotificationManager } from '@modrinth/ui'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const { addNotification } = injectNotificationManager();
|
||||
const route = useNativeRoute();
|
||||
const serverId = route.params.id as string;
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const route = useNativeRoute()
|
||||
const serverId = route.params.id as string
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const preferences = {
|
||||
ramAsNumber: {
|
||||
displayName: "RAM as bytes",
|
||||
description:
|
||||
"When enabled, RAM will be displayed as bytes instead of a percentage in your server's Overview.",
|
||||
implemented: true,
|
||||
},
|
||||
hideSubdomainLabel: {
|
||||
displayName: "Hide subdomain label",
|
||||
description: "When enabled, the subdomain label will be hidden from the server header.",
|
||||
implemented: true,
|
||||
},
|
||||
autoRestart: {
|
||||
displayName: "Auto restart",
|
||||
description: "When enabled, your server will automatically restart if it crashes.",
|
||||
implemented: false,
|
||||
},
|
||||
powerDontAskAgain: {
|
||||
displayName: "Power actions confirmation",
|
||||
description: "When enabled, you will be prompted before stopping and restarting your server.",
|
||||
implemented: true,
|
||||
},
|
||||
backupWhileRunning: {
|
||||
displayName: "Create backups while running",
|
||||
description: "When enabled, backups will be created even if the server is running.",
|
||||
implemented: true,
|
||||
},
|
||||
} as const;
|
||||
ramAsNumber: {
|
||||
displayName: 'RAM as bytes',
|
||||
description:
|
||||
"When enabled, RAM will be displayed as bytes instead of a percentage in your server's Overview.",
|
||||
implemented: true,
|
||||
},
|
||||
hideSubdomainLabel: {
|
||||
displayName: 'Hide subdomain label',
|
||||
description: 'When enabled, the subdomain label will be hidden from the server header.',
|
||||
implemented: true,
|
||||
},
|
||||
autoRestart: {
|
||||
displayName: 'Auto restart',
|
||||
description: 'When enabled, your server will automatically restart if it crashes.',
|
||||
implemented: false,
|
||||
},
|
||||
powerDontAskAgain: {
|
||||
displayName: 'Power actions confirmation',
|
||||
description: 'When enabled, you will be prompted before stopping and restarting your server.',
|
||||
implemented: true,
|
||||
},
|
||||
backupWhileRunning: {
|
||||
displayName: 'Create backups while running',
|
||||
description: 'When enabled, backups will be created even if the server is running.',
|
||||
implemented: true,
|
||||
},
|
||||
} as const
|
||||
|
||||
type PreferenceKeys = keyof typeof preferences;
|
||||
type PreferenceKeys = keyof typeof preferences
|
||||
|
||||
type UserPreferences = {
|
||||
[K in PreferenceKeys]: boolean;
|
||||
};
|
||||
[K in PreferenceKeys]: boolean
|
||||
}
|
||||
|
||||
const defaultPreferences: UserPreferences = {
|
||||
ramAsNumber: false,
|
||||
hideSubdomainLabel: false,
|
||||
autoRestart: false,
|
||||
powerDontAskAgain: false,
|
||||
backupWhileRunning: false,
|
||||
};
|
||||
ramAsNumber: false,
|
||||
hideSubdomainLabel: false,
|
||||
autoRestart: false,
|
||||
powerDontAskAgain: false,
|
||||
backupWhileRunning: false,
|
||||
}
|
||||
|
||||
const userPreferences = useStorage<UserPreferences>(
|
||||
`pyro-server-${serverId}-preferences`,
|
||||
defaultPreferences,
|
||||
);
|
||||
`pyro-server-${serverId}-preferences`,
|
||||
defaultPreferences,
|
||||
)
|
||||
|
||||
const newUserPreferences = ref<UserPreferences>(JSON.parse(JSON.stringify(userPreferences.value)));
|
||||
const newUserPreferences = ref<UserPreferences>(JSON.parse(JSON.stringify(userPreferences.value)))
|
||||
|
||||
const hasUnsavedChanges = computed(() => {
|
||||
return JSON.stringify(newUserPreferences.value) !== JSON.stringify(userPreferences.value);
|
||||
});
|
||||
return JSON.stringify(newUserPreferences.value) !== JSON.stringify(userPreferences.value)
|
||||
})
|
||||
|
||||
const savePreferences = () => {
|
||||
userPreferences.value = { ...newUserPreferences.value };
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: "Preferences saved",
|
||||
text: "Your preferences have been saved.",
|
||||
});
|
||||
};
|
||||
userPreferences.value = { ...newUserPreferences.value }
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Preferences saved',
|
||||
text: 'Your preferences have been saved.',
|
||||
})
|
||||
}
|
||||
|
||||
const resetPreferences = () => {
|
||||
newUserPreferences.value = { ...userPreferences.value };
|
||||
};
|
||||
newUserPreferences.value = { ...userPreferences.value }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stylized-toggle:checked::after {
|
||||
background: var(--color-accent-contrast) !important;
|
||||
background: var(--color-accent-contrast) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,332 +1,333 @@
|
||||
<template>
|
||||
<div class="relative h-full w-full select-none overflow-y-auto">
|
||||
<div
|
||||
v-if="server.moduleErrors.fs"
|
||||
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<IssuesIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Failed to load properties</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
We couldn't access your server's properties. Here's what we know:
|
||||
<span class="break-all font-mono">{{
|
||||
JSON.stringify(server.moduleErrors.fs.error)
|
||||
}}</span>
|
||||
</p>
|
||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['fs'])">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-full w-full select-none overflow-y-auto">
|
||||
<div
|
||||
v-if="server.moduleErrors.fs"
|
||||
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<IssuesIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Failed to load properties</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
We couldn't access your server's properties. Here's what we know:
|
||||
<span class="break-all font-mono">{{
|
||||
JSON.stringify(server.moduleErrors.fs.error)
|
||||
}}</span>
|
||||
</p>
|
||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['fs'])">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="propsData && status === 'success'"
|
||||
class="flex h-full w-full flex-col justify-between gap-6 overflow-y-auto"
|
||||
>
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 class="m-0 text-lg font-bold text-contrast">Server properties</h2>
|
||||
<div class="m-0">
|
||||
Edit the Minecraft server properties file. If you're unsure about a specific property,
|
||||
the
|
||||
<NuxtLink
|
||||
class="goto-link !inline-block"
|
||||
to="https://minecraft.wiki/w/Server.properties"
|
||||
external
|
||||
>
|
||||
Minecraft Wiki
|
||||
</NuxtLink>
|
||||
has more detailed information.
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 rounded-2xl bg-table-alternateRow p-4">
|
||||
<div class="relative w-full text-sm">
|
||||
<label for="search-server-properties" class="sr-only">Search server properties</label>
|
||||
<SearchIcon
|
||||
class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<input
|
||||
id="search-server-properties"
|
||||
v-model="searchInput"
|
||||
class="w-full pl-9"
|
||||
type="search"
|
||||
name="search"
|
||||
autocomplete="off"
|
||||
placeholder="Search server properties..."
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="(property, index) in filteredProperties"
|
||||
:key="index"
|
||||
class="flex flex-row flex-wrap items-center justify-between py-2"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span :id="`property-label-${index}`">{{ formatPropertyName(index) }}</span>
|
||||
<span v-if="overrides[index] && overrides[index].info" class="ml-2">
|
||||
<EyeIcon v-tooltip="overrides[index].info" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="overrides[index] && overrides[index].type === 'dropdown'"
|
||||
class="mt-2 flex w-full sm:w-[320px] sm:justify-end"
|
||||
>
|
||||
<UiServersTeleportDropdownMenu
|
||||
:id="`server-property-${index}`"
|
||||
v-model="liveProperties[index]"
|
||||
:name="formatPropertyName(index)"
|
||||
:options="overrides[index].options || []"
|
||||
:aria-labelledby="`property-label-${index}`"
|
||||
placeholder="Select..."
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="typeof property === 'boolean'" class="flex justify-end">
|
||||
<input
|
||||
:id="`server-property-${index}`"
|
||||
v-model="liveProperties[index]"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
:aria-labelledby="`property-label-${index}`"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="typeof property === 'number'" class="mt-2 w-full sm:w-[320px]">
|
||||
<input
|
||||
:id="`server-property-${index}`"
|
||||
v-model.number="liveProperties[index]"
|
||||
type="number"
|
||||
class="w-full border p-2"
|
||||
:aria-labelledby="`property-label-${index}`"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="isComplexProperty(property)" class="mt-2 w-full sm:w-[320px]">
|
||||
<textarea
|
||||
:id="`server-property-${index}`"
|
||||
v-model="liveProperties[index]"
|
||||
class="w-full resize-y rounded-xl border p-2"
|
||||
:aria-labelledby="`property-label-${index}`"
|
||||
></textarea>
|
||||
</div>
|
||||
<div v-else class="mt-2 flex w-full justify-end sm:w-[320px]">
|
||||
<input
|
||||
:id="`server-property-${index}`"
|
||||
v-model="liveProperties[index]"
|
||||
type="text"
|
||||
class="w-full rounded-xl border p-2"
|
||||
:aria-labelledby="`property-label-${index}`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="card flex h-full w-full items-center justify-center">
|
||||
<p class="text-contrast">
|
||||
The server properties file has not been generated yet. Start up your server to generate it.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="propsData && status === 'success'"
|
||||
class="flex h-full w-full flex-col justify-between gap-6 overflow-y-auto"
|
||||
>
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 class="m-0 text-lg font-bold text-contrast">Server properties</h2>
|
||||
<div class="m-0">
|
||||
Edit the Minecraft server properties file. If you're unsure about a specific property,
|
||||
the
|
||||
<NuxtLink
|
||||
class="goto-link !inline-block"
|
||||
to="https://minecraft.wiki/w/Server.properties"
|
||||
external
|
||||
>
|
||||
Minecraft Wiki
|
||||
</NuxtLink>
|
||||
has more detailed information.
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 rounded-2xl bg-table-alternateRow p-4">
|
||||
<div class="relative w-full text-sm">
|
||||
<label for="search-server-properties" class="sr-only">Search server properties</label>
|
||||
<SearchIcon
|
||||
class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<input
|
||||
id="search-server-properties"
|
||||
v-model="searchInput"
|
||||
class="w-full pl-9"
|
||||
type="search"
|
||||
name="search"
|
||||
autocomplete="off"
|
||||
placeholder="Search server properties..."
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="(property, index) in filteredProperties"
|
||||
:key="index"
|
||||
class="flex flex-row flex-wrap items-center justify-between py-2"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span :id="`property-label-${index}`">{{ formatPropertyName(index) }}</span>
|
||||
<span v-if="overrides[index] && overrides[index].info" class="ml-2">
|
||||
<EyeIcon v-tooltip="overrides[index].info" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="overrides[index] && overrides[index].type === 'dropdown'"
|
||||
class="mt-2 flex w-full sm:w-[320px] sm:justify-end"
|
||||
>
|
||||
<UiServersTeleportDropdownMenu
|
||||
:id="`server-property-${index}`"
|
||||
v-model="liveProperties[index]"
|
||||
:name="formatPropertyName(index)"
|
||||
:options="overrides[index].options || []"
|
||||
:aria-labelledby="`property-label-${index}`"
|
||||
placeholder="Select..."
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="typeof property === 'boolean'" class="flex justify-end">
|
||||
<input
|
||||
:id="`server-property-${index}`"
|
||||
v-model="liveProperties[index]"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
:aria-labelledby="`property-label-${index}`"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="typeof property === 'number'" class="mt-2 w-full sm:w-[320px]">
|
||||
<input
|
||||
:id="`server-property-${index}`"
|
||||
v-model.number="liveProperties[index]"
|
||||
type="number"
|
||||
class="w-full border p-2"
|
||||
:aria-labelledby="`property-label-${index}`"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="isComplexProperty(property)" class="mt-2 w-full sm:w-[320px]">
|
||||
<textarea
|
||||
:id="`server-property-${index}`"
|
||||
v-model="liveProperties[index]"
|
||||
class="w-full resize-y rounded-xl border p-2"
|
||||
:aria-labelledby="`property-label-${index}`"
|
||||
></textarea>
|
||||
</div>
|
||||
<div v-else class="mt-2 flex w-full justify-end sm:w-[320px]">
|
||||
<input
|
||||
:id="`server-property-${index}`"
|
||||
v-model="liveProperties[index]"
|
||||
type="text"
|
||||
class="w-full rounded-xl border p-2"
|
||||
:aria-labelledby="`property-label-${index}`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="card flex h-full w-full items-center justify-center">
|
||||
<p class="text-contrast">
|
||||
The server properties file has not been generated yet. Start up your server to generate it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UiServersSaveBanner
|
||||
:is-visible="hasUnsavedChanges"
|
||||
:server="props.server"
|
||||
:is-updating="isUpdating"
|
||||
restart
|
||||
:save="saveProperties"
|
||||
:reset="resetProperties"
|
||||
/>
|
||||
</div>
|
||||
<UiServersSaveBanner
|
||||
:is-visible="hasUnsavedChanges"
|
||||
:server="props.server"
|
||||
:is-updating="isUpdating"
|
||||
restart
|
||||
:save="saveProperties"
|
||||
:reset="resetProperties"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { EyeIcon, IssuesIcon, SearchIcon } from "@modrinth/assets";
|
||||
import { injectNotificationManager } from "@modrinth/ui";
|
||||
import Fuse from "fuse.js";
|
||||
import { computed, inject, ref, watch } from "vue";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import { EyeIcon, IssuesIcon, SearchIcon } from '@modrinth/assets'
|
||||
import { injectNotificationManager } from '@modrinth/ui'
|
||||
import Fuse from 'fuse.js'
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager();
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
const tags = useTags();
|
||||
const tags = useTags()
|
||||
|
||||
const isUpdating = ref(false);
|
||||
const isUpdating = ref(false)
|
||||
|
||||
const searchInput = ref("");
|
||||
const searchInput = ref('')
|
||||
|
||||
const data = computed(() => props.server.general);
|
||||
const modulesLoaded = inject<Promise<void>>("modulesLoaded");
|
||||
const { data: propsData, status } = await useAsyncData("ServerProperties", async () => {
|
||||
await modulesLoaded;
|
||||
const rawProps = await props.server.fs?.downloadFile("server.properties");
|
||||
if (!rawProps) return null;
|
||||
const data = computed(() => props.server.general)
|
||||
const modulesLoaded = inject<Promise<void>>('modulesLoaded')
|
||||
const { data: propsData, status } = await useAsyncData('ServerProperties', async () => {
|
||||
await modulesLoaded
|
||||
const rawProps = await props.server.fs?.downloadFile('server.properties')
|
||||
if (!rawProps) return null
|
||||
|
||||
const properties: Record<string, any> = {};
|
||||
const lines = rawProps.split("\n");
|
||||
const properties: Record<string, any> = {}
|
||||
const lines = rawProps.split('\n')
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("#") || !line.includes("=")) continue;
|
||||
const [key, ...valueParts] = line.split("=");
|
||||
let value = valueParts.join("=");
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('#') || !line.includes('=')) continue
|
||||
const [key, ...valueParts] = line.split('=')
|
||||
let value = valueParts.join('=')
|
||||
|
||||
if (value.toLowerCase() === "true" || value.toLowerCase() === "false") {
|
||||
value = value.toLowerCase() === "true";
|
||||
} else if (!isNaN(value as any) && value !== "") {
|
||||
value = Number(value);
|
||||
}
|
||||
if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
|
||||
value = value.toLowerCase() === 'true'
|
||||
} else if (!isNaN(value as any) && value !== '') {
|
||||
value = Number(value)
|
||||
}
|
||||
|
||||
properties[key.trim()] = value;
|
||||
}
|
||||
properties[key.trim()] = value
|
||||
}
|
||||
|
||||
return properties;
|
||||
});
|
||||
return properties
|
||||
})
|
||||
|
||||
const liveProperties = ref<Record<string, any>>({});
|
||||
const originalProperties = ref<Record<string, any>>({});
|
||||
const liveProperties = ref<Record<string, any>>({})
|
||||
const originalProperties = ref<Record<string, any>>({})
|
||||
|
||||
watch(
|
||||
propsData,
|
||||
(newPropsData) => {
|
||||
if (newPropsData) {
|
||||
liveProperties.value = JSON.parse(JSON.stringify(newPropsData));
|
||||
originalProperties.value = JSON.parse(JSON.stringify(newPropsData));
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
propsData,
|
||||
(newPropsData) => {
|
||||
if (newPropsData) {
|
||||
liveProperties.value = JSON.parse(JSON.stringify(newPropsData))
|
||||
originalProperties.value = JSON.parse(JSON.stringify(newPropsData))
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const hasUnsavedChanges = computed(() => {
|
||||
return Object.keys(liveProperties.value).some(
|
||||
(key) =>
|
||||
JSON.stringify(liveProperties.value[key]) !== JSON.stringify(originalProperties.value[key]),
|
||||
);
|
||||
});
|
||||
return Object.keys(liveProperties.value).some(
|
||||
(key) =>
|
||||
JSON.stringify(liveProperties.value[key]) !== JSON.stringify(originalProperties.value[key]),
|
||||
)
|
||||
})
|
||||
|
||||
const getDifficultyOptions = () => {
|
||||
const pre113Versions = tags.value.gameVersions
|
||||
.filter((v) => {
|
||||
const versionNumbers = v.version.split(".").map(Number);
|
||||
return versionNumbers[0] === 1 && versionNumbers[1] < 13;
|
||||
})
|
||||
.map((v) => v.version);
|
||||
if (data.value?.mc_version && pre113Versions.includes(data.value.mc_version)) {
|
||||
return ["0", "1", "2", "3"];
|
||||
} else {
|
||||
return ["peaceful", "easy", "normal", "hard"];
|
||||
}
|
||||
};
|
||||
const pre113Versions = tags.value.gameVersions
|
||||
.filter((v) => {
|
||||
const versionNumbers = v.version.split('.').map(Number)
|
||||
return versionNumbers[0] === 1 && versionNumbers[1] < 13
|
||||
})
|
||||
.map((v) => v.version)
|
||||
if (data.value?.mc_version && pre113Versions.includes(data.value.mc_version)) {
|
||||
return ['0', '1', '2', '3']
|
||||
} else {
|
||||
return ['peaceful', 'easy', 'normal', 'hard']
|
||||
}
|
||||
}
|
||||
|
||||
const overrides: { [key: string]: { type: string; options?: string[]; info?: string } } = {
|
||||
difficulty: {
|
||||
type: "dropdown",
|
||||
options: getDifficultyOptions(),
|
||||
},
|
||||
gamemode: {
|
||||
type: "dropdown",
|
||||
options: ["survival", "creative", "adventure", "spectator"],
|
||||
},
|
||||
};
|
||||
difficulty: {
|
||||
type: 'dropdown',
|
||||
options: getDifficultyOptions(),
|
||||
},
|
||||
gamemode: {
|
||||
type: 'dropdown',
|
||||
options: ['survival', 'creative', 'adventure', 'spectator'],
|
||||
},
|
||||
}
|
||||
|
||||
const fuse = computed(() => {
|
||||
if (!liveProperties.value) return null;
|
||||
if (!liveProperties.value) return null
|
||||
|
||||
const propertiesToFuse = Object.entries(liveProperties.value).map(([key, value]) => ({
|
||||
key,
|
||||
value: String(value),
|
||||
}));
|
||||
const propertiesToFuse = Object.entries(liveProperties.value).map(([key, value]) => ({
|
||||
key,
|
||||
value: String(value),
|
||||
}))
|
||||
|
||||
return new Fuse(propertiesToFuse, {
|
||||
keys: ["key", "value"],
|
||||
threshold: 0.2,
|
||||
});
|
||||
});
|
||||
return new Fuse(propertiesToFuse, {
|
||||
keys: ['key', 'value'],
|
||||
threshold: 0.2,
|
||||
})
|
||||
})
|
||||
|
||||
const filteredProperties = computed(() => {
|
||||
if (!searchInput.value?.trim()) {
|
||||
return liveProperties.value;
|
||||
}
|
||||
if (!searchInput.value?.trim()) {
|
||||
return liveProperties.value
|
||||
}
|
||||
|
||||
const results = fuse.value?.search(searchInput.value) ?? [];
|
||||
const results = fuse.value?.search(searchInput.value) ?? []
|
||||
|
||||
return Object.fromEntries(results.map(({ item }) => [item.key, liveProperties.value[item.key]]));
|
||||
});
|
||||
return Object.fromEntries(results.map(({ item }) => [item.key, liveProperties.value[item.key]]))
|
||||
})
|
||||
|
||||
const constructServerProperties = (): string => {
|
||||
const properties = liveProperties.value;
|
||||
const properties = liveProperties.value
|
||||
|
||||
let fileContent = `#Minecraft server properties\n#${new Date().toUTCString()}\n`;
|
||||
let fileContent = `#Minecraft server properties\n#${new Date().toUTCString()}\n`
|
||||
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
if (typeof value === "object") {
|
||||
fileContent += `${key}=${JSON.stringify(value)}\n`;
|
||||
} else if (typeof value === "boolean") {
|
||||
fileContent += `${key}=${value ? "true" : "false"}\n`;
|
||||
} else {
|
||||
fileContent += `${key}=${value}\n`;
|
||||
}
|
||||
}
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
if (typeof value === 'object') {
|
||||
fileContent += `${key}=${JSON.stringify(value)}\n`
|
||||
} else if (typeof value === 'boolean') {
|
||||
fileContent += `${key}=${value ? 'true' : 'false'}\n`
|
||||
} else {
|
||||
fileContent += `${key}=${value}\n`
|
||||
}
|
||||
}
|
||||
|
||||
return fileContent;
|
||||
};
|
||||
return fileContent
|
||||
}
|
||||
|
||||
const saveProperties = async () => {
|
||||
try {
|
||||
isUpdating.value = true;
|
||||
await props.server.fs?.updateFile("server.properties", constructServerProperties());
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
originalProperties.value = JSON.parse(JSON.stringify(liveProperties.value));
|
||||
await props.server.refresh();
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: "Server properties updated",
|
||||
text: "Your server properties were successfully changed.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating server properties:", error);
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Failed to update server properties",
|
||||
text: "An error occurred while attempting to update your server properties.",
|
||||
});
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
};
|
||||
try {
|
||||
isUpdating.value = true
|
||||
await props.server.fs?.updateFile('server.properties', constructServerProperties())
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
originalProperties.value = JSON.parse(JSON.stringify(liveProperties.value))
|
||||
await props.server.refresh()
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Server properties updated',
|
||||
text: 'Your server properties were successfully changed.',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error updating server properties:', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to update server properties',
|
||||
text: 'An error occurred while attempting to update your server properties.',
|
||||
})
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetProperties = async () => {
|
||||
liveProperties.value = JSON.parse(JSON.stringify(originalProperties.value));
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
};
|
||||
liveProperties.value = JSON.parse(JSON.stringify(originalProperties.value))
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
}
|
||||
|
||||
const formatPropertyName = (propertyName: string): string => {
|
||||
return propertyName
|
||||
.split(/[-.]/)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
};
|
||||
return propertyName
|
||||
.split(/[-.]/)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
const isComplexProperty = (property: any): boolean => {
|
||||
return (
|
||||
typeof property === "object" ||
|
||||
(typeof property === "string" &&
|
||||
(property.includes(",") ||
|
||||
property.includes("{") ||
|
||||
property.includes("}") ||
|
||||
property.includes("[") ||
|
||||
property.includes("]") ||
|
||||
property.length > 30))
|
||||
);
|
||||
};
|
||||
return (
|
||||
typeof property === 'object' ||
|
||||
(typeof property === 'string' &&
|
||||
(property.includes(',') ||
|
||||
property.includes('{') ||
|
||||
property.includes('}') ||
|
||||
property.includes('[') ||
|
||||
property.includes(']') ||
|
||||
property.length > 30))
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stylized-toggle:checked::after {
|
||||
background: var(--color-accent-contrast) !important;
|
||||
background: var(--color-accent-contrast) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,234 +1,235 @@
|
||||
<template>
|
||||
<div class="relative h-full w-full">
|
||||
<div
|
||||
v-if="server.moduleErrors.startup"
|
||||
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<IssuesIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Failed to load startup settings</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
We couldn't load your server's startup settings. Here's what we know:
|
||||
</p>
|
||||
<p>
|
||||
<span class="break-all font-mono">{{
|
||||
JSON.stringify(server.moduleErrors.startup.error)
|
||||
}}</span>
|
||||
</p>
|
||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['startup'])">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="data" class="flex h-full w-full flex-col gap-4">
|
||||
<div
|
||||
class="rounded-2xl border-[1px] border-solid border-orange bg-bg-orange p-4 text-contrast"
|
||||
>
|
||||
These settings are for advanced users. Changing them can break your server.
|
||||
</div>
|
||||
<div class="relative h-full w-full">
|
||||
<div
|
||||
v-if="server.moduleErrors.startup"
|
||||
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<IssuesIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Failed to load startup settings</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
We couldn't load your server's startup settings. Here's what we know:
|
||||
</p>
|
||||
<p>
|
||||
<span class="break-all font-mono">{{
|
||||
JSON.stringify(server.moduleErrors.startup.error)
|
||||
}}</span>
|
||||
</p>
|
||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['startup'])">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="data" class="flex h-full w-full flex-col gap-4">
|
||||
<div
|
||||
class="rounded-2xl border-[1px] border-solid border-orange bg-bg-orange p-4 text-contrast"
|
||||
>
|
||||
These settings are for advanced users. Changing them can break your server.
|
||||
</div>
|
||||
|
||||
<div class="gap-2">
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="flex flex-col justify-between gap-4 sm:flex-row">
|
||||
<label for="startup-command-field" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Startup command</span>
|
||||
<span> The command that runs when your server is started. </span>
|
||||
</label>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
:disabled="invocation === originalInvocation"
|
||||
class="!w-full sm:!w-auto"
|
||||
@click="resetToDefault"
|
||||
>
|
||||
<UpdatedIcon class="h-5 w-5" />
|
||||
Restore default command
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<textarea
|
||||
id="startup-command-field"
|
||||
v-model="invocation"
|
||||
class="min-h-[270px] w-full resize-y font-[family-name:var(--mono-font)]"
|
||||
/>
|
||||
</div>
|
||||
<div class="gap-2">
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="flex flex-col justify-between gap-4 sm:flex-row">
|
||||
<label for="startup-command-field" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Startup command</span>
|
||||
<span> The command that runs when your server is started. </span>
|
||||
</label>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
:disabled="invocation === originalInvocation"
|
||||
class="!w-full sm:!w-auto"
|
||||
@click="resetToDefault"
|
||||
>
|
||||
<UpdatedIcon class="h-5 w-5" />
|
||||
Restore default command
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<textarea
|
||||
id="startup-command-field"
|
||||
v-model="invocation"
|
||||
class="min-h-[270px] w-full resize-y font-[family-name:var(--mono-font)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col gap-8">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Java version</span>
|
||||
<span>
|
||||
The version of Java that your server will run on. By default, only the Java versions
|
||||
compatible with this version of Minecraft are shown. Some mods may require a
|
||||
different Java version to work properly.
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
id="show-all-versions"
|
||||
v-model="showAllVersions"
|
||||
class="switch stylized-toggle flex-none"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label for="show-all-versions" class="text-sm">Show all Java versions</label>
|
||||
</div>
|
||||
<UiServersTeleportDropdownMenu
|
||||
:id="'java-version-field'"
|
||||
v-model="jdkVersion"
|
||||
name="java-version"
|
||||
:options="displayedJavaVersions"
|
||||
placeholder="Java Version"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Runtime</span>
|
||||
<span> The Java runtime your server will use. </span>
|
||||
</div>
|
||||
<UiServersTeleportDropdownMenu
|
||||
:id="'runtime-field'"
|
||||
v-model="jdkBuild"
|
||||
name="runtime"
|
||||
:options="['Corretto', 'Temurin', 'GraalVM']"
|
||||
placeholder="Runtime"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UiServersSaveBanner
|
||||
:is-visible="!!hasUnsavedChanges"
|
||||
:server="props.server"
|
||||
:is-updating="isUpdating"
|
||||
:save="saveStartup"
|
||||
:reset="resetStartup"
|
||||
/>
|
||||
</div>
|
||||
<div class="card flex flex-col gap-8">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Java version</span>
|
||||
<span>
|
||||
The version of Java that your server will run on. By default, only the Java versions
|
||||
compatible with this version of Minecraft are shown. Some mods may require a
|
||||
different Java version to work properly.
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
id="show-all-versions"
|
||||
v-model="showAllVersions"
|
||||
class="switch stylized-toggle flex-none"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label for="show-all-versions" class="text-sm">Show all Java versions</label>
|
||||
</div>
|
||||
<UiServersTeleportDropdownMenu
|
||||
:id="'java-version-field'"
|
||||
v-model="jdkVersion"
|
||||
name="java-version"
|
||||
:options="displayedJavaVersions"
|
||||
placeholder="Java Version"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-lg font-bold text-contrast">Runtime</span>
|
||||
<span> The Java runtime your server will use. </span>
|
||||
</div>
|
||||
<UiServersTeleportDropdownMenu
|
||||
:id="'runtime-field'"
|
||||
v-model="jdkBuild"
|
||||
name="runtime"
|
||||
:options="['Corretto', 'Temurin', 'GraalVM']"
|
||||
placeholder="Runtime"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UiServersSaveBanner
|
||||
:is-visible="!!hasUnsavedChanges"
|
||||
:server="props.server"
|
||||
:is-updating="isUpdating"
|
||||
:save="saveStartup"
|
||||
:reset="resetStartup"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { IssuesIcon, UpdatedIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled, injectNotificationManager } from "@modrinth/ui";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import { IssuesIcon, UpdatedIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
|
||||
|
||||
const { addNotification } = injectNotificationManager();
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
server: ModrinthServer
|
||||
}>()
|
||||
|
||||
await props.server.startup.fetch();
|
||||
await props.server.startup.fetch()
|
||||
|
||||
const data = computed(() => props.server.general);
|
||||
const showAllVersions = ref(false);
|
||||
const data = computed(() => props.server.general)
|
||||
const showAllVersions = ref(false)
|
||||
|
||||
const jdkVersionMap = [
|
||||
{ value: "lts8", label: "Java 8" },
|
||||
{ value: "lts11", label: "Java 11" },
|
||||
{ value: "lts17", label: "Java 17" },
|
||||
{ value: "lts21", label: "Java 21" },
|
||||
];
|
||||
{ value: 'lts8', label: 'Java 8' },
|
||||
{ value: 'lts11', label: 'Java 11' },
|
||||
{ value: 'lts17', label: 'Java 17' },
|
||||
{ value: 'lts21', label: 'Java 21' },
|
||||
]
|
||||
|
||||
const jdkBuildMap = [
|
||||
{ value: "corretto", label: "Corretto" },
|
||||
{ value: "temurin", label: "Temurin" },
|
||||
{ value: "graal", label: "GraalVM" },
|
||||
];
|
||||
{ value: 'corretto', label: 'Corretto' },
|
||||
{ value: 'temurin', label: 'Temurin' },
|
||||
{ value: 'graal', label: 'GraalVM' },
|
||||
]
|
||||
|
||||
const invocation = ref(props.server.startup.invocation);
|
||||
const invocation = ref(props.server.startup.invocation)
|
||||
const jdkVersion = ref(
|
||||
jdkVersionMap.find((v) => v.value === props.server.startup.jdk_version)?.label,
|
||||
);
|
||||
const jdkBuild = ref(jdkBuildMap.find((v) => v.value === props.server.startup.jdk_build)?.label);
|
||||
jdkVersionMap.find((v) => v.value === props.server.startup.jdk_version)?.label,
|
||||
)
|
||||
const jdkBuild = ref(jdkBuildMap.find((v) => v.value === props.server.startup.jdk_build)?.label)
|
||||
|
||||
const originalInvocation = ref(invocation.value);
|
||||
const originalJdkVersion = ref(jdkVersion.value);
|
||||
const originalJdkBuild = ref(jdkBuild.value);
|
||||
const originalInvocation = ref(invocation.value)
|
||||
const originalJdkVersion = ref(jdkVersion.value)
|
||||
const originalJdkBuild = ref(jdkBuild.value)
|
||||
|
||||
const hasUnsavedChanges = computed(
|
||||
() =>
|
||||
invocation.value !== originalInvocation.value ||
|
||||
jdkVersion.value !== originalJdkVersion.value ||
|
||||
jdkBuild.value !== originalJdkBuild.value,
|
||||
);
|
||||
() =>
|
||||
invocation.value !== originalInvocation.value ||
|
||||
jdkVersion.value !== originalJdkVersion.value ||
|
||||
jdkBuild.value !== originalJdkBuild.value,
|
||||
)
|
||||
|
||||
const isUpdating = ref(false);
|
||||
const isUpdating = ref(false)
|
||||
|
||||
const compatibleJavaVersions = computed(() => {
|
||||
const mcVersion = data.value?.mc_version ?? "";
|
||||
if (!mcVersion) return jdkVersionMap.map((v) => v.label);
|
||||
const mcVersion = data.value?.mc_version ?? ''
|
||||
if (!mcVersion) return jdkVersionMap.map((v) => v.label)
|
||||
|
||||
const [major, minor] = mcVersion.split(".").map(Number);
|
||||
const [major, minor] = mcVersion.split('.').map(Number)
|
||||
|
||||
if (major >= 1) {
|
||||
if (minor >= 20) return ["Java 21"];
|
||||
if (minor >= 18) return ["Java 17", "Java 21"];
|
||||
if (minor >= 17) return ["Java 16", "Java 17", "Java 21"];
|
||||
if (minor >= 12) return ["Java 8", "Java 11", "Java 17", "Java 21"];
|
||||
if (minor >= 6) return ["Java 8", "Java 11"];
|
||||
}
|
||||
if (major >= 1) {
|
||||
if (minor >= 20) return ['Java 21']
|
||||
if (minor >= 18) return ['Java 17', 'Java 21']
|
||||
if (minor >= 17) return ['Java 16', 'Java 17', 'Java 21']
|
||||
if (minor >= 12) return ['Java 8', 'Java 11', 'Java 17', 'Java 21']
|
||||
if (minor >= 6) return ['Java 8', 'Java 11']
|
||||
}
|
||||
|
||||
return ["Java 8"];
|
||||
});
|
||||
return ['Java 8']
|
||||
})
|
||||
|
||||
const displayedJavaVersions = computed(() => {
|
||||
return showAllVersions.value ? jdkVersionMap.map((v) => v.label) : compatibleJavaVersions.value;
|
||||
});
|
||||
return showAllVersions.value ? jdkVersionMap.map((v) => v.label) : compatibleJavaVersions.value
|
||||
})
|
||||
|
||||
async function saveStartup() {
|
||||
try {
|
||||
isUpdating.value = true;
|
||||
const invocationValue = invocation.value ?? "";
|
||||
const jdkVersionKey = jdkVersionMap.find((v) => v.label === jdkVersion.value)?.value;
|
||||
const jdkBuildKey = jdkBuildMap.find((v) => v.label === jdkBuild.value)?.value;
|
||||
await props.server.startup?.update(invocationValue, jdkVersionKey as any, jdkBuildKey as any);
|
||||
try {
|
||||
isUpdating.value = true
|
||||
const invocationValue = invocation.value ?? ''
|
||||
const jdkVersionKey = jdkVersionMap.find((v) => v.label === jdkVersion.value)?.value
|
||||
const jdkBuildKey = jdkBuildMap.find((v) => v.label === jdkBuild.value)?.value
|
||||
await props.server.startup?.update(invocationValue, jdkVersionKey as any, jdkBuildKey as any)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
await props.server.refresh(["startup"]);
|
||||
await props.server.refresh(['startup'])
|
||||
|
||||
if (props.server.startup) {
|
||||
invocation.value = props.server.startup.invocation;
|
||||
jdkVersion.value =
|
||||
jdkVersionMap.find((v) => v.value === props.server.startup?.jdk_version)?.label || "";
|
||||
jdkBuild.value =
|
||||
jdkBuildMap.find((v) => v.value === props.server.startup?.jdk_build)?.label || "";
|
||||
}
|
||||
if (props.server.startup) {
|
||||
invocation.value = props.server.startup.invocation
|
||||
jdkVersion.value =
|
||||
jdkVersionMap.find((v) => v.value === props.server.startup?.jdk_version)?.label || ''
|
||||
jdkBuild.value =
|
||||
jdkBuildMap.find((v) => v.value === props.server.startup?.jdk_build)?.label || ''
|
||||
}
|
||||
|
||||
addNotification({
|
||||
type: "success",
|
||||
title: "Server settings updated",
|
||||
text: "Your server settings were successfully changed.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Failed to update server arguments",
|
||||
text: "Please try again later.",
|
||||
});
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Server settings updated',
|
||||
text: 'Your server settings were successfully changed.',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to update server arguments',
|
||||
text: 'Please try again later.',
|
||||
})
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetStartup() {
|
||||
invocation.value = originalInvocation.value;
|
||||
jdkVersion.value = originalJdkVersion.value;
|
||||
jdkBuild.value = originalJdkBuild.value;
|
||||
invocation.value = originalInvocation.value
|
||||
jdkVersion.value = originalJdkVersion.value
|
||||
jdkBuild.value = originalJdkBuild.value
|
||||
}
|
||||
|
||||
function resetToDefault() {
|
||||
invocation.value = originalInvocation.value ?? "";
|
||||
invocation.value = originalInvocation.value ?? ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stylized-toggle:checked::after {
|
||||
background: var(--color-accent-contrast) !important;
|
||||
background: var(--color-accent-contrast) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user