Paper and Purpur + Backups (#3004)

* feat: init selecting paper+purpur on purchase flow

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: properly implement Paper/Purpur in Platform

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: correct wording

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: redo platform modal

Signed-off-by: Evan Song <theevansong@gmail.com>

* Switch to HCaptcha for Auth-related captchas (#2945)

* Switch to HCaptcha for Auth-related captchas

* run fmt

* fix hcaptcha not loading

* fix: more robust loader dropdown logic

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: handle "not yet supported" install err

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: fix icon kerfuffles

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: improve vanilla install modal title

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: spacing

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: improve no loader state

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: type error

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust mod version modal title

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust modpack warning copy

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: vanilla empty state in content page

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust copy

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: update icon

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: loader type

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: loader type

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: always show dropdown if possible

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: improve spacing

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: appear disabled

Signed-off-by: Evan Song <theevansong@gmail.com>

* h

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: if reinstalling, show it on the modal title

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: put it in the dropdown, they said

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust style

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: sort paper-purpur versions desc

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: do not consider backup limit in reinstall prompt

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: backup locking, plugin support

* fix: content type error

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: casing

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: plugins pt 2

* feat: backups, mrpack

* fix: type errors come on

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: spacing

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: type maxing

* chore: show copy button on allocation rows

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: suspend improvement

---------

Signed-off-by: Evan Song <theevansong@gmail.com>
Co-authored-by: Evan Song <theevansong@gmail.com>
Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Evan Song <52982404+ferothefox@users.noreply.github.com>
This commit is contained in:
TheWander02
2024-12-10 23:49:50 -07:00
committed by GitHub
parent eff3189ded
commit 742c0edd9e
26 changed files with 952 additions and 248 deletions

View File

@@ -274,7 +274,7 @@
<button
v-if="
result.installed ||
server.mods.data.find((x) => x.project_id === result.project_id) ||
server.content.data.find((x) => x.project_id === result.project_id) ||
server.general?.project?.id === result.project_id
"
disabled
@@ -430,7 +430,7 @@ const serverOverrideGameVersions = ref(false);
const serverOverrideLoaders = ref(false);
if (route.query.sid) {
server.value = await usePyroServer(route.query.sid, ["general", "mods"]);
server.value = await usePyroServer(route.query.sid, ["general", "content"]);
}
if (route.query.shi && projectType.value.id !== "modpack") {
@@ -462,8 +462,12 @@ async function serverInstall(project) {
project.installed = true;
navigateTo(`/servers/manage/${route.query.sid}/options/loader`);
} else if (projectType.value.id === "mod") {
await server.value.mods.install(version.project_id, version.id);
await server.value.refresh(["mods"]);
await server.value.content.install("mod", version.project_id, version.id);
await server.value.refresh(["content"]);
project.installed = true;
} else if (projectType.value.id === "plugin") {
await server.value.content.install("plugin", version.project_id, version.id);
await server.value.refresh(["content"]);
project.installed = true;
}
} catch (e) {
@@ -509,7 +513,7 @@ const {
}
if (server.value && serverHideInstalled.value) {
const installedMods = server.value.mods.data
const installedMods = server.value.content.data
.filter((x) => x.project_id)
.map((x) => x.project_id);

View File

@@ -1,7 +1,35 @@
<template>
<div class="contents">
<div
v-if="server.error && server.error.message.includes('Forbidden')"
v-if="serverData?.status === 'suspended'"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
>
<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">
<LockIcon class="size-12 text-orange" />
</div>
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server Suspended</h1>
</div>
<p class="text-lg text-secondary">
{{
serverData.suspension_reason
? `Your server has been suspended: ${serverData.suspension_reason}`
: "Your server has been suspended."
}}
<br />
This is most likely due to a billing issue. Please check your billing information and
contact Modrinth support if you believe this is an error.
</p>
</div>
<ButtonStyled size="large" color="brand" @click="() => router.push('/settings/billing')">
<button class="mt-6 !w-full">Go to billing</button>
</ButtonStyled>
</div>
</div>
<div
v-else-if="server.error && server.error.message.includes('Forbidden')"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
@@ -128,11 +156,11 @@
class="mx-auto mb-4 flex justify-between gap-2 rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
>
<div class="flex flex-row gap-4">
<IssuesIcon class="hidden h-8 w-8 text-red sm:block" />
<IssuesIcon class="hidden h-8 w-8 shrink-0 text-red sm:block" />
<div class="flex flex-col gap-2 leading-[150%]">
<div class="flex items-center gap-3">
<IssuesIcon class="block h-8 w-8 text-red sm:hidden" />
<div class="flex gap-2 text-xl font-bold">{{ errorTitle }}</div>
<IssuesIcon class="flex h-8 w-8 shrink-0 text-red sm:hidden" />
<div class="flex gap-2 text-2xl font-bold">{{ errorTitle }}</div>
</div>
<div
@@ -175,6 +203,14 @@
reinstalling your server, and if the problem persists, please contact Modrinth
support with your server's debug information.
</div>
<div
v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'"
>
An error occurred while installing your server because Modrinth Servers does not
support the version of Minecraft or the loader you specified. Try reinstalling
your server with a different version or loader, and if the problem persists,
please contact Modrinth support with your server's debug information.
</div>
<div
v-if="errorTitle === 'Installation error'"
@@ -262,6 +298,7 @@ import {
CheckIcon,
FileIcon,
TransferIcon,
LockIcon,
} from "@modrinth/assets";
import DOMPurify from "dompurify";
import { ButtonStyled } from "@modrinth/ui";
@@ -294,7 +331,7 @@ const router = useRouter();
const serverId = route.params.id as string;
const server = await usePyroServer(serverId, [
"general",
"mods",
"content",
"backups",
"network",
"startup",
@@ -305,6 +342,7 @@ const server = await usePyroServer(serverId, [
watch(
() => server.error,
(newError) => {
if (server.general?.status === "suspended") return;
if (newError && !newError.message.includes("Forbidden")) {
startPolling();
}
@@ -518,7 +556,7 @@ const handleWebSocketMessage = (data: WSEvent) => {
handleInstallationResult(data);
break;
case "new-mod":
server.refresh(["mods"]);
server.refresh(["content"]);
console.log("New mod:", data);
break;
case "auth-ok":

View File

@@ -107,6 +107,7 @@
v-tooltip="'Backup in progress'"
class="size-6 animate-spin"
/>
<LockIcon v-else-if="backup.locked" class="size-8" />
<BoxIcon v-else class="size-8" />
</div>
<div class="flex min-w-0 flex-col gap-2">
@@ -159,6 +160,16 @@
},
},
{ id: 'download', action: () => initiateDownload(backup.id) },
{
id: 'lock',
action: () => {
if (backup.locked) {
unlockBackup(backup.id);
} else {
lockBackup(backup.id);
}
},
},
{
id: 'delete',
action: () => {
@@ -172,6 +183,8 @@
<MoreHorizontalIcon class="h-5 w-5 bg-transparent" />
<template #rename> <EditIcon /> Rename </template>
<template #restore> <ClipboardCopyIcon /> Restore </template>
<template v-if="backup.locked" #lock> <OpenLockIcon /> Unlock </template>
<template v-else #lock> <LockIcon /> Lock </template>
<template #download> <DownloadIcon /> Download </template>
<template #delete> <TrashIcon /> Delete </template>
</UiServersTeleportOverflowMenu>
@@ -217,6 +230,8 @@ import {
TrashIcon,
SettingsIcon,
BoxIcon,
LockIcon,
OpenLockIcon,
} from "@modrinth/assets";
import { ref, computed } from "vue";
import type { Server } from "~/composables/pyroServers";
@@ -335,6 +350,24 @@ const initiateDownload = async (backupId: string) => {
}
};
const lockBackup = async (backupId: string) => {
try {
await props.server.backups?.lock(backupId);
await props.server.refresh(["backups"]);
} catch (error) {
console.error("Failed to toggle lock:", error);
}
};
const unlockBackup = async (backupId: string) => {
try {
await props.server.backups?.unlock(backupId);
await props.server.refresh(["backups"]);
} catch (error) {
console.error("Failed to toggle lock:", error);
}
};
onMounted(() => {
watchEffect(() => {
const hasOngoingBackups = backups.value.some((backup) => backup.ongoing);

View File

@@ -10,7 +10,7 @@ import type { Server } from "~/composables/pyroServers";
const route = useNativeRoute();
const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>();
const data = computed(() => props.server.general);

View File

@@ -1,5 +1,5 @@
<template>
<NewModal ref="modModal" header="Edit mod version">
<NewModal ref="modModal" header="Editing mod version">
<div>
<div class="mb-4 flex flex-col gap-4">
<div class="inline-flex flex-wrap items-center">
@@ -19,7 +19,8 @@
<InfoIcon class="hidden sm:block" />
<span class="text-sm text-secondary">
Your server was created from a modpack. Changing the mod version may cause unexpected
issues.
issues. You can update the modpack version in your server's Options > Platform
settings.
</span>
</div>
</div>
@@ -72,7 +73,7 @@
type="search"
name="search"
autocomplete="off"
placeholder="Search mods..."
:placeholder="`Search ${type}s...`"
@input="debouncedSearch"
/>
</div>
@@ -80,7 +81,7 @@
<UiServersTeleportOverflowMenu
position="bottom"
direction="left"
aria-label="Filter mods"
:aria-label="`Filter ${type}s`"
:options="[
{ id: 'all', action: () => (filterMethod = 'all') },
{ id: 'enabled', action: () => (filterMethod = 'enabled') },
@@ -92,7 +93,7 @@
</span>
<FilterIcon aria-hidden="true" />
<DropdownIcon aria-hidden="true" class="h-5 w-5 text-secondary" />
<template #all> All mods </template>
<template #all> All {{ type }}s </template>
<template #enabled> Only enabled </template>
<template #disabled> Only disabled </template>
</UiServersTeleportOverflowMenu>
@@ -101,10 +102,10 @@
<ButtonStyled v-if="hasMods" color="brand" type="outlined">
<nuxt-link
class="w-full text-nowrap sm:w-fit"
:to="`/mods?sid=${props.server.serverId}`"
:to="`/${type}s?sid=${props.server.serverId}`"
>
<PlusIcon />
Add content
Add {{ type }}
</nuxt-link>
</ButtonStyled>
</div>
@@ -227,14 +228,48 @@
</div>
</div>
</div>
<div v-else class="mt-4 flex h-full flex-col items-center justify-center text-center">
<PackageClosedIcon class="size-24 text-neutral-500" />
<p class="m-0 pb-2 pt-3 text-neutral-200">No mods found!</p>
<p class="m-0 pb-3 text-neutral-400">Add some mods to your server to manage them here.</p>
<ButtonStyled color="brand" class="mt-8">
<nuxt-link :to="`/mods?sid=${props.server.serverId}`">Add content</nuxt-link>
<!-- no mods has platform -->
<div
v-else-if="
!hasMods &&
props.server.general?.loader &&
props.server.general?.loader.toLocaleLowerCase() !== 'vanilla'
"
class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center"
>
<PackageClosedIcon class="size-24" />
<p class="m-0 font-bold text-contrast">No {{ type }}s found!</p>
<p class="m-0">Add some {{ type }}s to your server to manage them here.</p>
<ButtonStyled color="brand">
<NuxtLink :to="`/${type}s?sid=${props.server.serverId}`">
<PlusIcon />
Add {{ type }}
</NuxtLink>
</ButtonStyled>
</div>
<div v-else class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center">
<UiServersIconsLoaderIcon loader="Vanilla" class="size-24" />
<p class="m-0 pt-3 font-bold text-contrast">Your server is running Vanilla Minecraft</p>
<p class="m-0">
Add content to your server by installing a modpack or choosing a different platform that
supports {{ type }}s.
</p>
<div class="flex flex-row items-center gap-4">
<ButtonStyled class="mt-8">
<NuxtLink :to="`/modpacks?sid=${props.server.serverId}`">
<CompassIcon />
Find a modpack
</NuxtLink>
</ButtonStyled>
<div>or</div>
<ButtonStyled class="mt-8">
<NuxtLink :to="`/${type}s?sid=${props.server.serverId}`">
<WrenchIcon />
Change platform
</NuxtLink>
</ButtonStyled>
</div>
</div>
</div>
</div>
</template>
@@ -251,15 +286,22 @@ import {
XIcon,
PlusIcon,
MoreVerticalIcon,
CompassIcon,
WrenchIcon,
} from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import type { Server } from "~/composables/pyroServers";
const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>();
const type = computed(() => {
const loader = props.server.general?.loader?.toLowerCase();
return loader === "paper" || loader === "purpur" ? "plugin" : "mod";
});
interface Mod {
name?: string;
filename: string;
@@ -291,7 +333,7 @@ const filterMethodLabel = computed(() => {
case "enabled":
return "Only enabled";
default:
return "All mods";
return `All ${type.value}s`;
}
});
@@ -350,7 +392,7 @@ onUnmounted(() => {
});
watch(
() => props.server.mods?.data,
() => props.server.content?.data,
(newMods) => {
if (newMods) {
localMods.value = [...newMods];
@@ -399,7 +441,7 @@ async function toggleMod(mod: Mod) {
await props.server.fs?.moveFileOrFolder(sourcePath, destinationPath);
await props.server.refresh(["general", "mods"]);
await props.server.refresh(["general", "content"]);
} catch (error) {
mod.filename = originalFilename;
mod.disabled = originalFilename.endsWith(".disabled");
@@ -418,8 +460,8 @@ async function removeMod(mod: Mod) {
mod.changing = true;
try {
await props.server.mods?.remove(`/mods/${mod.filename}`);
await props.server.refresh(["general", "mods"]);
await props.server.content?.remove(type.value, `/${type.value}s/${mod.filename}`);
await props.server.refresh(["general", "content"]);
} catch (error) {
console.error("Error removing mod:", error);
@@ -439,7 +481,12 @@ const currentVersion = ref();
async function beginChangeModVersion(mod: Mod) {
currentMod.value = mod;
currentVersions.value = await useBaseFetch(`project/${mod.project_id}/version`, {}, false, true);
currentVersions.value = await useBaseFetch(`project/${mod.project_id}/version`, {}, false);
currentVersions.value = currentVersions.value.filter((version: any) =>
version.loaders.includes(props.server.general?.loader?.toLowerCase()),
);
currentVersion.value = currentVersions.value.find(
(version: any) => version.id === mod.version_id,
);
@@ -450,9 +497,12 @@ async function changeModVersion() {
currentMod.value.changing = true;
try {
modModal.value.hide();
await props.server.mods?.remove(`/mods/${currentMod.value.filename}`);
await props.server.mods?.install(currentMod.value.project_id, currentVersion.value.id);
await props.server.refresh(["general", "mods"]);
await props.server.content?.reinstall(
type.value,
currentMod.value.version_id,
currentVersion.value.id,
);
await props.server.refresh(["general", "content"]);
} catch (error) {
console.error("Error changing mod version:", error);
}

View File

@@ -144,7 +144,7 @@ import { UploadIcon, FolderOpenIcon } from "@modrinth/assets";
import type { Server } from "~/composables/pyroServers";
const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>();
const route = useRoute();

View File

@@ -254,6 +254,7 @@ const inspectError = async () => {
mcError.value = response;
// @ts-ignore
const analysis = (await $fetch(`https://api.mclo.gs/1/insights/${response.id}`, {
method: "POST",
headers: {

View File

@@ -19,7 +19,7 @@ const route = useRoute();
const serverId = route.params.id as string;
const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>();
useHead({

View File

@@ -5,7 +5,7 @@
<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> Change the name of your server. This name is only visible on Modrinth.</span>
<span> Change your server's name. This name is only visible on Modrinth.</span>
</label>
<div class="flex flex-col gap-2">
<input
@@ -51,7 +51,7 @@
/>
.modrinth.gg
</div>
<div class="flex flex-col text-sm text-rose-400">
<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>
@@ -134,7 +134,7 @@ import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
import type { Server } from "~/composables/pyroServers";
const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>();
const data = computed(() => props.server.general);

View File

@@ -123,7 +123,7 @@ const route = useNativeRoute();
const serverId = route.params.id as string;
const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>();
const data = computed(() => props.server.general);

View File

@@ -1,51 +1,152 @@
<template>
<NewModal ref="editModal" header="Select modpack">
<UiServersProjectSelect type="modpack" @select="reinstallNew" />
</NewModal>
<NewModal
ref="versionSelectModal"
:header="isSecondPhase ? 'Confirm reinstallation' : 'Select version'"
:header="
isSecondPhase
? 'Confirming reinstallation'
: `${data?.loader === selectedLoader ? 'Reinstalling' : 'Installing'}
${selectedLoader.toLowerCase() === 'vanilla' ? 'Vanilla Minecraft' : selectedLoader}`
"
@hide="onHide"
@show="onShow"
>
<div class="flex flex-col gap-4 md:w-[600px]">
<p
v-if="isSecondPhase"
:style="{
lineHeight: isSecondPhase ? '1.5' : undefined,
marginBottom: isSecondPhase ? '-12px' : '0',
marginTop: isSecondPhase ? '-4px' : '-2px',
}"
>
{{
isSecondPhase
? "This will reinstall your server and erase all data. You may want to back up your server before proceeding. Are you sure you want to continue?"
: "Choose the version of Minecraft you want to use for this server."
}}
This will reinstall your server and erase all data. You may want to back up your server
before proceeding. Are you sure you want to continue?
</p>
<div v-if="!isSecondPhase" class="flex flex-col gap-2">
<UiServersTeleportDropdownMenu
v-model="selectedMCVersion"
name="mcVersion"
:options="mcVersions"
placeholder="Select Minecraft version..."
/>
<UiServersTeleportDropdownMenu
v-if="selectedMCVersion && selectedLoader.toLowerCase() !== 'vanilla'"
v-model="selectedLoaderVersion"
name="loaderVersion"
:options="selectedLoaderVersions"
placeholder="Select loader version..."
/>
<div class="mt-2 flex items-center gap-2">
<input
id="hard-reset"
:checked="hardReset"
class="switch stylized-toggle"
type="checkbox"
@change="hardReset = ($event.target as HTMLInputElement).checked"
<div v-if="!isSecondPhase" class="flex flex-col gap-4">
<div class="mx-auto flex flex-row items-center gap-4">
<div
class="grid size-16 place-content-center rounded-2xl border-[2px] border-solid border-button-border bg-button-bg shadow-sm"
>
<UiServersIconsLoaderIcon class="size-10" :loader="selectedLoader" />
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-10"
>
<path d="M5 9v6" />
<path d="M9 9h3V5l7 7-7 7v-4H9V9z" />
</svg>
<div
class="grid size-16 place-content-center rounded-2xl border-[2px] border-solid border-button-border bg-table-alternateRow shadow-sm"
>
<ServerIcon class="size-10" />
</div>
</div>
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
<div class="text-sm font-bold text-contrast">Minecraft version</div>
<UiServersTeleportDropdownMenu
v-model="selectedMCVersion"
name="mcVersion"
:options="mcVersions"
class="w-full max-w-[100%]"
placeholder="Select Minecraft version..."
/>
<label for="hard-reset">Clean reinstall</label>
</div>
<div
v-if="selectedLoader.toLowerCase() !== 'vanilla'"
class="flex w-full flex-col gap-2 rounded-2xl p-4"
:class="{
'bg-table-alternateRow':
!selectedMCVersion || isLoading || selectedLoaderVersions.length > 0,
'bg-highlight-red':
selectedMCVersion && !isLoading && selectedLoaderVersions.length === 0,
}"
>
<div class="flex flex-col gap-2">
<div class="text-sm font-bold text-contrast">{{ selectedLoader }} version</div>
<template v-if="!selectedMCVersion">
<div
class="relative flex h-9 w-full select-none items-center rounded-xl bg-button-bg px-4 opacity-50"
>
Select a Minecraft version to see available versions
<DropdownIcon class="absolute right-4" />
</div>
</template>
<template v-else-if="isLoading">
<div
class="relative flex h-9 w-full items-center rounded-xl bg-button-bg px-4 opacity-50"
>
<UiServersIconsLoadingIcon class="mr-2 animate-spin" />
Loading versions...
<DropdownIcon class="absolute right-4" />
</div>
</template>
<template v-else-if="selectedLoaderVersions.length > 0">
<UiServersTeleportDropdownMenu
v-model="selectedLoaderVersion"
name="loaderVersion"
:options="selectedLoaderVersions"
class="w-full max-w-[100%]"
:placeholder="
selectedLoader.toLowerCase() === 'paper' ||
selectedLoader.toLowerCase() === 'purpur'
? `Select build number...`
: `Select loader version...`
"
/>
</template>
<template v-else>
<div>No versions available for Minecraft {{ selectedMCVersion }}.</div>
</template>
</div>
</div>
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
<div class="flex w-full flex-row items-center justify-between">
<label class="w-full text-lg font-bold text-contrast" for="hard-reset">
Erase all data
</label>
<input
id="hard-reset"
:checked="hardReset"
class="switch stylized-toggle shrink-0"
type="checkbox"
@change="hardReset = ($event.target as HTMLInputElement).checked"
/>
</div>
<div>
Removes all data on your server, including your worlds, mods, and configuration files,
then reinstalls it with the selected version.
</div>
</div>
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
<div class="flex w-full flex-row items-center justify-between">
<label class="w-full text-lg font-bold text-contrast" for="backup-server">
Backup server
</label>
<input
id="backup-server"
:checked="backupServer"
class="switch stylized-toggle shrink-0"
type="checkbox"
@change="backupServer = ($event.target as HTMLInputElement).checked"
/>
</div>
<div>
Creates a backup of your server before proceeding with the installation or
reinstallation.
</div>
</div>
</div>
<div class="mt-4 flex justify-start gap-4">
@@ -53,13 +154,15 @@
<button :disabled="canInstall" @click="handleReinstall">
<RightArrowIcon />
{{
isSecondPhase
? "Erase and install"
: loadingServerCheck
? "Loading..."
: isDangerous
? "Erase and install"
: "Install"
isBackingUp
? "Backing up..."
: isSecondPhase
? "Erase and install"
: loadingServerCheck
? "Loading..."
: isDangerous
? "Erase and install"
: "Install"
}}
</button>
</ButtonStyled>
@@ -75,51 +178,130 @@
"
>
<XIcon />
{{ isSecondPhase ? "No" : "Cancel" }}
{{ isSecondPhase ? "Go back" : "Cancel" }}
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<NewModal ref="mrpackModal" header="Upload mrpack" @hide="onHide" @show="onShow">
<div>
<div class="mt-2 flex items-center gap-2">
<input
id="hard-reset"
:checked="hardReset"
class="switch stylized-toggle"
type="checkbox"
@change="hardReset = ($event.target as HTMLInputElement).checked"
/>
<label for="hard-reset">Clean reinstall</label>
<NewModal ref="mrpackModal" header="Uploading mrpack" @hide="onHide" @show="onShow">
<div class="flex flex-col gap-4 md:w-[600px]">
<p
v-if="isSecondPhase"
:style="{
lineHeight: isSecondPhase ? '1.5' : undefined,
marginBottom: isSecondPhase ? '-12px' : '0',
marginTop: isSecondPhase ? '-4px' : '-2px',
}"
>
This will reinstall your server and erase all data. You may want to back up your server
before proceeding. Are you sure you want to continue?
</p>
<div v-if="!isSecondPhase" class="flex flex-col gap-4">
<div class="mx-auto flex flex-row items-center gap-4">
<div
class="grid size-16 place-content-center rounded-2xl border-[2px] border-solid border-button-border bg-button-bg shadow-sm"
>
<UploadIcon class="size-10" />
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-10"
>
<path d="M5 9v6" />
<path d="M9 9h3V5l7 7-7 7v-4H9V9z" />
</svg>
<div
class="grid size-16 place-content-center rounded-2xl border-[2px] border-solid border-button-border bg-table-alternateRow shadow-sm"
>
<ServerIcon class="size-10" />
</div>
</div>
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
<div class="text-sm font-bold text-contrast">Upload mrpack</div>
<input
type="file"
accept=".mrpack"
class=""
:disabled="isLoading"
@change="uploadMrpack"
/>
</div>
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
<div class="flex w-full flex-row items-center justify-between">
<label class="w-full text-lg font-bold text-contrast" for="hard-reset">
Erase all data
</label>
<input
id="hard-reset"
:checked="hardReset"
class="switch stylized-toggle shrink-0"
type="checkbox"
@change="hardReset = ($event.target as HTMLInputElement).checked"
/>
</div>
<div>
Removes all data on your server, including your worlds, mods, and configuration files,
then reinstalls it with the selected version.
</div>
</div>
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
<div class="flex w-full flex-row items-center justify-between">
<label class="w-full text-lg font-bold text-contrast" for="backup-server-mrpack">
Backup server
</label>
<input
id="backup-server-mrpack"
:checked="backupServer"
class="switch stylized-toggle shrink-0"
type="checkbox"
@change="backupServer = ($event.target as HTMLInputElement).checked"
/>
</div>
<div>Creates a backup of your server before proceeding.</div>
</div>
</div>
<input
type="file"
accept=".mrpack"
class="mt-4"
:disabled="isLoading"
@change="uploadMrpack"
/>
<div class="mt-4 flex justify-start gap-4">
<ButtonStyled :color="isDangerous ? 'red' : 'brand'">
<button :disabled="!mrpackFile || isLoading" @click="reinstallMrpack">
<button :disabled="canInstallUpload" @click="handleReinstallUpload">
<RightArrowIcon />
{{
isSecondPhase
? "Erase and install"
: loadingServerCheck
? "Loading..."
: isDangerous
? "Erase and install"
: "Install"
isBackingUp
? "Backing up..."
: isSecondPhase
? "Erase and install"
: loadingServerCheck
? "Loading..."
: isDangerous
? "Erase and install"
: "Install"
}}
</button>
</ButtonStyled>
<ButtonStyled>
<button :disabled="isLoading" @click="mrpackModal?.hide">
<button
:disabled="isLoading"
@click="
if (isSecondPhase) {
isSecondPhase = false;
} else {
mrpackModal?.hide();
}
"
>
<XIcon />
Cancel
{{ isSecondPhase ? "Go back" : "Cancel" }}
</button>
</ButtonStyled>
</div>
@@ -199,7 +381,7 @@
<div v-else class="flex w-full flex-col items-center gap-2 sm:w-fit sm:flex-row">
<ButtonStyled>
<nuxt-link class="!w-full sm:!w-auto" :to="`/modpacks?sid=${props.server.serverId}`">
<DownloadIcon class="size-4" /> Install a modpack
<CompassIcon class="size-4" /> Find a modpack
</nuxt-link>
</ButtonStyled>
<span class="hidden sm:block">or</span>
@@ -213,18 +395,21 @@
<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">Mod loader</h2>
<p class="m-0">Mod loaders allow you to run mods on your server.</p>
<h2 class="m-0 text-lg font-bold text-contrast">Platform</h2>
<p class="m-0">
Your server's platform is the software that runs your server. Different platforms
support different mods and plugins.
</p>
<div v-if="data.upstream" class="mt-2 flex items-center gap-2">
<InfoIcon class="hidden sm:block" />
<span class="text-sm text-secondary">
Your server was installed from a modpack, which automatically chooses the appropriate
mod loader.
platform.
</span>
</div>
</div>
<div
class="flex w-full flex-col gap-1 rounded-2xl bg-table-alternateRow p-2"
class="flex w-full flex-col gap-1 rounded-2xl"
:class="{
'pointer-events-none cursor-not-allowed select-none opacity-50':
props.server.general?.status === 'installing' && isError,
@@ -236,7 +421,7 @@
</div>
</div>
<UiServersPyroLoading v-else />
<div v-else />
</div>
</template>
@@ -249,14 +434,18 @@ import {
InfoIcon,
RightArrowIcon,
XIcon,
CompassIcon,
DropdownIcon,
ServerIcon,
} from "@modrinth/assets";
import type { Server } from "~/composables/pyroServers";
import type { Loaders } from "~/types/servers";
const route = useNativeRoute();
const serverId = route.params.id as string;
const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>();
const emit = defineEmits<{
@@ -274,6 +463,7 @@ const isError = computed(() => props.server.general?.status === "error");
const isDangerous = computed(() => hardReset.value);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const isBackupLimited = computed(() => (props.server.backups?.data?.length || 0) >= 15);
const isBackingUp = ref(false);
const versionStrings = ["forge", "fabric", "quilt", "neo"] as const;
@@ -312,7 +502,6 @@ const loaderVersions = (await Promise.all(
}[]
>;
const editModal = ref();
const versionSelectModal = ref();
const mrpackModal = ref();
@@ -330,6 +519,16 @@ const canInstall = computed(() => {
return conds || !selectedLoaderVersion.value;
});
const canInstallUpload = computed(() => {
const conds =
!mrpackFile.value ||
isLoading.value ||
loadingServerCheck.value ||
serverCheckError.value.trim().length > 0;
return conds;
});
const mcVersions = tags.value.gameVersions
.filter((x) => x.version_type === "release")
.map((x) => x.version)
@@ -343,27 +542,37 @@ const mcVersions = tags.value.gameVersions
});
const selectedLoaderVersions = computed(() => {
/*
loaderVersions[
selectedLoader.value.toLowerCase() === "neoforge" ? "neo" : selectedLoader.toLowerCase()
]
.find((x) => x.id === selectedMCVersion)
?.loaders.map((x) => x.id) || []
*/
let loader = selectedLoader.value.toLowerCase();
if (loader === "neoforge") {
loader = "neo";
const loader = selectedLoader.value.toLowerCase();
if (loader === "paper") {
return paperVersions.value[selectedMCVersion.value] || [];
}
const backwardsCompatibleVersion = loaderVersions[loader].find(
if (loader === "purpur") {
return purpurVersions.value[selectedMCVersion.value] || [];
}
if (loader === "vanilla") {
return [];
}
let apiLoader = loader;
if (loader === "neoforge") {
apiLoader = "neo";
}
const backwardsCompatibleVersion = loaderVersions[apiLoader]?.find(
// eslint-disable-next-line no-template-curly-in-string
(x) => x.id === "${modrinth.gameVersion}",
);
if (backwardsCompatibleVersion) {
return backwardsCompatibleVersion.loaders.map((x) => x.id);
}
return (
loaderVersions[loader]
.find((x) => x.id === selectedMCVersion.value)
loaderVersions[apiLoader]
?.find((x) => x.id === selectedMCVersion.value)
?.loaders.map((x) => x.id) || []
);
});
@@ -395,7 +604,7 @@ const versionIds = computed(() =>
const version = ref();
const currentVersion = ref();
const selectedLoader = ref("");
const selectedLoader = ref<Loaders>("Vanilla");
const selectedMCVersion = ref("");
const selectedLoaderVersion = ref("");
const isSecondPhase = ref(false);
@@ -409,8 +618,35 @@ const updateData = async () => {
};
updateData();
const paperVersions = ref<Record<string, number[]>>({});
const purpurVersions = ref<Record<string, string[]>>({});
const fetchPaperVersions = async (mcVersion: string) => {
try {
const res = await $fetch(`https://api.papermc.io/v2/projects/paper/versions/${mcVersion}`);
paperVersions.value[mcVersion] = (res as any).builds.sort((a: number, b: number) => b - a);
return res;
} catch (e) {
console.error(e);
return null;
}
};
const fetchPurpurVersions = async (mcVersion: string) => {
try {
const res = await $fetch(`https://api.purpurmc.org/v2/purpur/${mcVersion}`);
purpurVersions.value[mcVersion] = (res as any).builds.all.sort(
(a: string, b: string) => parseInt(b) - parseInt(a),
);
return res;
} catch (e) {
console.error(e);
return null;
}
};
const selectLoader = (loader: string) => {
selectedLoader.value = loader;
selectedLoader.value = loader as Loaders;
versionSelectModal.value.show();
};
@@ -421,24 +657,48 @@ const cachedVersions: Record<string, any> = {};
watch(selectedMCVersion, async () => {
if (selectedMCVersion.value.trim().length < 3) return;
// const res = await fetch(
// `/loader-versions?loader=minecraft&version=${selectedMCVersion.value}`,
// ).then((r) => r.json());
isLoading.value = true;
loadingServerCheck.value = true;
const res =
cachedVersions[selectedMCVersion.value] ||
(await $fetch(`/loader-versions?loader=minecraft&version=${selectedMCVersion.value}`));
cachedVersions[selectedMCVersion.value] = res;
try {
// Check if Minecraft version exists
const mcRes =
cachedVersions[selectedMCVersion.value] ||
(await $fetch(`/loader-versions?loader=minecraft&version=${selectedMCVersion.value}`));
loadingServerCheck.value = false;
cachedVersions[selectedMCVersion.value] = mcRes;
if (!mcRes.downloads?.server) {
serverCheckError.value =
"We couldn't find a server.jar for this version. Please pick another one.";
return;
}
// Fetch Paper/Purpur versions if needed
if (selectedLoader.value.toLowerCase() === "paper") {
const paperRes = await fetchPaperVersions(selectedMCVersion.value);
if (!paperRes) {
serverCheckError.value = "This Minecraft version is not supported by Paper.";
return;
}
}
if (selectedLoader.value.toLowerCase() === "purpur") {
const purpurRes = await fetchPurpurVersions(selectedMCVersion.value);
if (!purpurRes) {
serverCheckError.value = "This Minecraft version is not supported by Purpur.";
return;
}
}
if (res.downloads.server) {
serverCheckError.value = "";
} else {
serverCheckError.value =
"We couldn't find a server.jar for this version. Please pick another one.";
} catch (error) {
console.error(error);
serverCheckError.value = "Failed to fetch versions. Please try again.";
} finally {
loadingServerCheck.value = false;
isLoading.value = false;
}
});
@@ -510,7 +770,36 @@ const handleReinstall = async () => {
});
const backupName = `Reinstallation - ${format}`;
isLoading.value = true;
await props.server.backups?.create(backupName);
const backupId = (await props.server.backups?.create(backupName)) as unknown as string;
isBackingUp.value = true;
let attempts = 0;
while (true) {
attempts += 1;
if (attempts > 100) {
addNotification({
group: "server",
title: "Backup Failed",
text: "An unexpected error occurred while backing up. Please try again later.",
type: "error",
});
isLoading.value = false;
return;
}
await props.server.refresh(["backups"]);
const backups = await props.server.backups?.data;
const backup = backupId ? backups?.find((x) => x.id === backupId) : undefined;
if (backup && !backup.ongoing) {
console.log("Backup Finished");
isBackingUp.value = false;
break;
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}
} catch {
addNotification({
group: "server",
@@ -551,24 +840,6 @@ const handleReinstall = async () => {
}
};
const reinstallNew = async (project: any, versionNumber: string) => {
editModal.value.hide();
try {
const versions = (await useBaseFetch(`project/${project.project_id}/version`)) as any;
const version = versions.find((x: any) => x.version_number === versionNumber);
if (!version?.id) {
throw new Error("Version not found");
}
await props.server.general?.reinstall(serverId, false, project.project_id, version.id);
emit("reinstall");
await nextTick();
window.scrollTo(0, 0);
} catch (error) {
handleReinstallError(error);
}
};
const mrpackFile = ref<File | null>(null);
const uploadMrpack = (event: Event) => {
@@ -579,19 +850,86 @@ const uploadMrpack = (event: Event) => {
mrpackFile.value = target.files[0];
};
const reinstallMrpack = async () => {
if (!mrpackFile.value) {
const handleReinstallUpload = async () => {
if (hardReset.value && !backupServer.value && !isSecondPhase.value) {
isSecondPhase.value = true;
return;
}
const mrpack = new File([mrpackFile.value], mrpackFile.value.name, {
type: mrpackFile.value.type,
});
if (backupServer.value) {
try {
const date = new Date();
const format = date.toLocaleString(navigator.language || "en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
timeZoneName: "short",
});
const backupName = `Reinstallation - ${format}`;
isLoading.value = true;
const backupId = (await props.server.backups?.create(backupName)) as unknown as string;
isBackingUp.value = true;
let attempts = 0;
while (true) {
attempts += 1;
if (attempts > 100) {
addNotification({
group: "server",
title: "Backup Failed",
text: "An unexpected error occurred while backing up. Please try again later.",
type: "error",
});
isLoading.value = false;
return;
}
await props.server.refresh(["backups"]);
const backups = await props.server.backups?.data;
const backup = backupId ? backups?.find((x) => x.id === backupId) : undefined;
if (backup && !backup.ongoing) {
console.log("Backup Finished");
isBackingUp.value = false;
break;
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}
} catch {
addNotification({
group: "server",
title: "Backup Failed",
text: "An unexpected error occurred while backing up. Please try again later.",
type: "error",
});
isLoading.value = false;
return;
}
}
isLoading.value = true;
try {
isLoading.value = true;
if (!mrpackFile.value) {
throw new Error("No mrpack file selected");
}
const mrpack = new File([mrpackFile.value], mrpackFile.value.name, {
type: mrpackFile.value.type,
});
await props.server.general?.reinstallFromMrpack(mrpack, hardReset.value);
emit("reinstall");
emit("reinstall", {
loader: "mrpack",
lVersion: "",
mVersion: "",
});
await nextTick();
window.scrollTo(0, 0);
} catch (error) {

View File

@@ -204,6 +204,7 @@
</div>
<div class="flex w-full flex-row items-center gap-2 sm:w-auto">
<UiCopyCode :text="`${serverIP}:${allocation.port}`" />
<ButtonStyled icon-only>
<button
class="!w-full sm:!w-auto"
@@ -252,7 +253,7 @@ import { ref, computed, nextTick } from "vue";
import type { Server } from "~/composables/pyroServers";
const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>();
const isUpdating = ref(false);

View File

@@ -49,7 +49,7 @@ const route = useNativeRoute();
const serverId = route.params.id as string;
const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>();
const preferences = {

View File

@@ -124,7 +124,7 @@ import Fuse from "fuse.js";
import type { Server } from "~/composables/pyroServers";
const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>();
const tags = useTags();

View File

@@ -90,7 +90,7 @@ import { ButtonStyled } from "@modrinth/ui";
import type { Server } from "~/composables/pyroServers";
const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>();
const data = computed(() => props.server.general);