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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 952 additions and 248 deletions

View File

@ -44,7 +44,7 @@ import { ButtonStyled, NewModal } from "@modrinth/ui";
import { PlusIcon, XIcon, InfoIcon } from "@modrinth/assets"; import { PlusIcon, XIcon, InfoIcon } from "@modrinth/assets";
const props = defineProps<{ const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>; server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>(); }>();
const emit = defineEmits(["backupCreated"]); const emit = defineEmits(["backupCreated"]);

View File

@ -104,7 +104,7 @@ const modal = ref<InstanceType<typeof NewModal>>();
const initialSettings = ref<{ interval: number; enabled: boolean } | null>(null); const initialSettings = ref<{ interval: number; enabled: boolean } | null>(null);
const autoBackupEnabled = ref(false); const autoBackupEnabled = ref(false);
const autoBackupInterval = ref(1); const autoBackupInterval = ref(6);
const isLoadingSettings = ref(true); const isLoadingSettings = ref(true);
const isSaving = ref(false); const isSaving = ref(false);
@ -134,7 +134,7 @@ const fetchSettings = async () => {
const settings = await props.server.backups?.getAutoBackup(); const settings = await props.server.backups?.getAutoBackup();
initialSettings.value = settings as { interval: number; enabled: boolean }; initialSettings.value = settings as { interval: number; enabled: boolean };
autoBackupEnabled.value = settings?.enabled ?? false; autoBackupEnabled.value = settings?.enabled ?? false;
autoBackupInterval.value = settings?.interval || 1; autoBackupInterval.value = settings?.interval || 6;
} catch (error) { } catch (error) {
console.error("Error fetching backup settings:", error); console.error("Error fetching backup settings:", error);
addNotification({ addNotification({

View File

@ -1,52 +1,60 @@
<template> <template>
<div <div class="flex w-full flex-col gap-1 rounded-2xl bg-table-alternateRow p-2">
v-for="loader in loaders" <div
:key="loader.name" v-for="loader in vanillaLoaders"
class="group relative flex items-center justify-between rounded-2xl p-2 pr-2.5 hover:bg-bg" :key="loader.name"
> class="group relative flex items-center justify-between rounded-2xl p-2 pr-2.5 hover:bg-bg"
<div class="flex items-center gap-4"> >
<UiServersLoaderSelectorCard
:loader="loader"
:is-current="isCurrentLoader(loader.name)"
:loader-version="data.loader_version"
:current-loader="data.loader"
@select="selectLoader"
/>
</div>
</div>
<div class="mt-4">
<h2 class="mb-2 px-2 text-lg font-bold text-contrast">Mod loaders</h2>
<div class="flex w-full flex-col gap-1 rounded-2xl bg-table-alternateRow p-2">
<div <div
class="grid size-10 place-content-center rounded-xl border-[1px] border-solid border-button-border bg-button-bg shadow-sm" v-for="loader in modLoaders"
:class="isCurrentLoader(loader.name) ? '[&&]:bg-bg-green' : ''" :key="loader.name"
class="group relative flex items-center justify-between rounded-2xl p-2 pr-2.5 hover:bg-bg"
> >
<UiServersIconsLoaderIcon <UiServersLoaderSelectorCard
:loader="loader.name" :loader="loader"
class="[&&]:size-6" :is-current="isCurrentLoader(loader.name)"
:class="isCurrentLoader(loader.name) ? 'text-brand' : ''" :loader-version="data.loader_version"
:current-loader="data.loader"
@select="selectLoader"
/> />
</div> </div>
<div class="flex flex-col gap-0.5"> </div>
<div class="flex flex-row items-center gap-2"> </div>
<h1 class="m-0 text-xl font-bold leading-none text-contrast">
{{ loader.displayName }} <div class="mt-4">
</h1> <h2 class="mb-2 px-2 text-lg font-bold text-contrast">Plugin loaders</h2>
<span <div class="flex w-full flex-col gap-1 rounded-2xl bg-table-alternateRow p-2">
v-if="isCurrentLoader(loader.name)" <div
class="hidden items-center gap-1 rounded-full bg-bg-green p-1 px-1.5 text-xs font-semibold text-brand sm:flex" v-for="loader in pluginLoaders"
> :key="loader.name"
<CheckIcon class="h-4 w-4" /> class="group relative flex items-center justify-between rounded-2xl p-2 pr-2.5 hover:bg-bg"
Current >
</span> <UiServersLoaderSelectorCard
</div> :loader="loader"
<p v-if="isCurrentLoader(loader.name)" class="m-0 text-xs text-secondary"> :is-current="isCurrentLoader(loader.name)"
{{ data.loader_version }} :loader-version="data.loader_version"
</p> :current-loader="data.loader"
@select="selectLoader"
/>
</div> </div>
</div> </div>
<ButtonStyled>
<button @click="selectLoader(loader.name)">
<DownloadIcon class="h-5 w-5" />
{{ isCurrentLoader(loader.name) ? "Reinstall" : "Install" }}
</button>
</ButtonStyled>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { CheckIcon, DownloadIcon } from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui";
const props = defineProps<{ const props = defineProps<{
data: { data: {
loader: string | null; loader: string | null;
@ -58,14 +66,20 @@ const emit = defineEmits<{
(e: "selectLoader", loader: string): void; (e: "selectLoader", loader: string): void;
}>(); }>();
const loaders = [ const vanillaLoaders = [{ name: "Vanilla" as const, displayName: "Vanilla" }];
{ name: "Vanilla" as const, displayName: "Vanilla" },
const modLoaders = [
{ name: "Fabric" as const, displayName: "Fabric" }, { name: "Fabric" as const, displayName: "Fabric" },
{ name: "Quilt" as const, displayName: "Quilt" }, { name: "Quilt" as const, displayName: "Quilt" },
{ name: "Forge" as const, displayName: "Forge" }, { name: "Forge" as const, displayName: "Forge" },
{ name: "NeoForge" as const, displayName: "NeoForge" }, { name: "NeoForge" as const, displayName: "NeoForge" },
]; ];
const pluginLoaders = [
{ name: "Paper" as const, displayName: "Paper" },
{ name: "Purpur" as const, displayName: "Purpur" },
];
const isCurrentLoader = (loaderName: string) => { const isCurrentLoader = (loaderName: string) => {
return props.data.loader?.toLowerCase() === loaderName.toLowerCase(); return props.data.loader?.toLowerCase() === loaderName.toLowerCase();
}; };

View File

@ -0,0 +1,70 @@
<template>
<div class="flex w-full items-center justify-between">
<div class="flex items-center gap-4">
<div
class="grid size-10 place-content-center rounded-xl border-[1px] border-solid border-button-border bg-button-bg shadow-sm"
:class="isCurrentLoader ? '[&&]:bg-bg-green' : ''"
>
<UiServersIconsLoaderIcon
:loader="loader.name"
class="[&&]:size-6"
:class="isCurrentLoader ? 'text-brand' : ''"
/>
</div>
<div class="flex flex-col gap-0.5">
<div class="flex flex-row items-center gap-2">
<h1 class="m-0 text-xl font-bold leading-none text-contrast">
{{ loader.displayName }}
</h1>
<span
v-if="isCurrentLoader"
class="hidden items-center gap-1 rounded-full bg-bg-green p-1 px-1.5 text-xs font-semibold text-brand sm:flex"
>
<CheckIcon class="h-4 w-4" />
Current
</span>
</div>
<p v-if="isCurrentLoader" class="m-0 text-xs text-secondary">
{{ loaderVersion }}
</p>
</div>
</div>
<ButtonStyled>
<button @click="onSelect">
<DownloadIcon class="h-5 w-5" />
{{ isCurrentLoader ? "Reinstall" : "Install" }}
</button>
</ButtonStyled>
</div>
</template>
<script setup lang="ts">
import { CheckIcon, DownloadIcon } from "@modrinth/assets";
import { ButtonStyled } from "@modrinth/ui";
interface LoaderInfo {
name: "Vanilla" | "Fabric" | "Forge" | "Quilt" | "Paper" | "NeoForge" | "Purpur";
displayName: string;
}
interface Props {
loader: LoaderInfo;
currentLoader: string | null;
loaderVersion: string | null;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "select", loader: string): void;
}>();
const isCurrentLoader = computed(() => {
return props.currentLoader?.toLowerCase() === props.loader.name.toLowerCase();
});
const onSelect = () => {
emit("select", props.loader.name);
};
</script>

View File

@ -1,11 +1,26 @@
<template> <template>
<NuxtLink class="contents" :to="`/servers/manage/${props.server_id}`"> <NuxtLink
class="contents"
:to="status === 'suspended' ? '' : `/servers/manage/${props.server_id}`"
>
<div <div
class="flex cursor-pointer flex-row items-center overflow-x-hidden rounded-3xl bg-bg-raised p-4 transition-transform duration-100 active:scale-95" v-tooltip="
status === 'suspended'
? `This server is suspended visit the billing page to learn more`
: ''
"
class="flex cursor-pointer flex-row items-center overflow-x-hidden rounded-3xl bg-bg-raised p-4 transition-transform duration-100"
:class="status === 'suspended' ? 'opacity-50' : 'active:scale-95'"
data-pyro-server-listing data-pyro-server-listing
:data-pyro-server-listing-id="server_id" :data-pyro-server-listing-id="server_id"
> >
<UiServersServerIcon :image="image" /> <UiServersServerIcon v-if="status !== 'suspended'" :image="image" />
<div
v-else
class="bg-bg-secondary flex size-24 items-center justify-center rounded-xl border-[1px] border-solid border-button-border bg-button-bg shadow-sm"
>
<LockIcon class="size-20 text-secondary" />
</div>
<div class="ml-8 flex flex-col gap-2.5"> <div class="ml-8 flex flex-col gap-2.5">
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<h2 class="m-0 text-xl font-bold text-contrast">{{ name }}</h2> <h2 class="m-0 text-xl font-bold text-contrast">{{ name }}</h2>
@ -40,7 +55,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ChevronRightIcon } from "@modrinth/assets"; import { ChevronRightIcon, LockIcon } from "@modrinth/assets";
import type { Project, Server } from "~/types/servers"; import type { Project, Server } from "~/types/servers";
const props = defineProps<Partial<Server>>(); const props = defineProps<Partial<Server>>();

View File

@ -153,6 +153,65 @@
/> />
</g> </g>
</svg> </svg>
<svg
v-else-if="loader === 'Purpur'"
xml:space="preserve"
fill-rule="evenodd"
stroke-linecap="round"
stroke-linejoin="round"
stroke-miterlimit="1.5"
clip-rule="evenodd"
viewBox="0 0 24 24"
>
<defs>
<path
id="purpur"
fill="none"
stroke="currentColor"
stroke-width="1.68"
d="m264 41.95 8-4v8l-8 4v-8Z"
></path>
</defs>
<path fill="none" d="M0 0h24v24H0z"></path>
<path
fill="none"
stroke="currentColor"
stroke-width="1.77"
d="m264 29.95-8 4 8 4.42 8-4.42-8-4Z"
transform="matrix(1.125 0 0 1.1372 -285 -31.69)"
></path>
<path
fill="none"
stroke="currentColor"
stroke-width="1.77"
d="m272 38.37-8 4.42-8-4.42"
transform="matrix(1.125 0 0 1.1372 -285 -31.69)"
></path>
<path
fill="none"
stroke="currentColor"
stroke-width="1.77"
d="m260 31.95 8 4.21V45"
transform="matrix(1.125 0 0 1.1372 -285 -31.69)"
></path>
<path
fill="none"
stroke="currentColor"
stroke-width="1.77"
d="M260 45v-8.84l8-4.21"
transform="matrix(1.125 0 0 1.1372 -285 -31.69)"
></path>
<use
xlink:href="#purpur"
stroke-width="1.68"
transform="matrix(1.125 0 0 1.2569 -285 -40.78)"
></use>
<use
xlink:href="#purpur"
stroke-width="1.68"
transform="matrix(-1.125 0 0 1.2569 309 -40.78)"
></use>
</svg>
<svg v-else-if="loader === 'Vanilla'" viewBox="0 0 20 20" fill="currentColor"> <svg v-else-if="loader === 'Vanilla'" viewBox="0 0 20 20" fill="currentColor">
<path <path
fill-rule="evenodd" fill-rule="evenodd"
@ -165,8 +224,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { LoaderIcon } from "@modrinth/assets"; import { LoaderIcon } from "@modrinth/assets";
import type { Loaders } from "~/types/servers";
defineProps<{ defineProps<{
loader: "Fabric" | "Quilt" | "Forge" | "NeoForge" | "Paper" | "Spigot" | "Bukkit" | "Vanilla"; loader: Loaders;
}>(); }>();
</script> </script>

View File

@ -213,6 +213,7 @@ interface Backup {
name: string; name: string;
created_at: string; created_at: string;
ongoing: boolean; ongoing: boolean;
locked: boolean;
} }
interface AutoBackupSettings { interface AutoBackupSettings {
@ -225,6 +226,8 @@ interface JWTAuth {
token: string; token: string;
} }
type ContentType = "Mod" | "Plugin";
const constructServerProperties = (properties: any): string => { const constructServerProperties = (properties: any): string => {
let fileContent = `#Minecraft server properties\n#${new Date().toUTCString()}\n`; let fileContent = `#Minecraft server properties\n#${new Date().toUTCString()}\n`;
@ -483,13 +486,16 @@ const setMotd = async (motd: string) => {
} }
}; };
// ------------------ MODS ------------------ // // ------------------ CONTENT ------------------ //
const installMod = async (projectId: string, versionId: string) => { const installContent = async (contentType: ContentType, projectId: string, versionId: string) => {
try { try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/mods`, { await PyroFetch(`servers/${internalServerRefrence.value.serverId}/mods`, {
method: "POST", method: "POST",
body: { rinth_ids: { project_id: projectId, version_id: versionId } }, body: {
install_as: contentType,
rinth_ids: { project_id: projectId, version_id: versionId },
},
}); });
} catch (error) { } catch (error) {
console.error("Error installing mod:", error); console.error("Error installing mod:", error);
@ -497,12 +503,13 @@ const installMod = async (projectId: string, versionId: string) => {
} }
}; };
const removeMod = async (modId: string) => { const removeContent = async (contentType: ContentType, contentId: string) => {
try { try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/deleteMod`, { await PyroFetch(`servers/${internalServerRefrence.value.serverId}/deleteMod`, {
method: "POST", method: "POST",
body: { body: {
path: modId, install_as: contentType,
path: contentId,
}, },
}); });
} catch (error) { } catch (error) {
@ -511,11 +518,15 @@ const removeMod = async (modId: string) => {
} }
}; };
const reinstallMod = async (modId: string, versionId: string) => { const reinstallContent = async (
contentType: ContentType,
contentId: string,
newContentId: string,
) => {
try { try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/mods/${modId}`, { await PyroFetch(`servers/${internalServerRefrence.value.serverId}/mods/${contentId}`, {
method: "PUT", method: "PUT",
body: { version_id: versionId }, body: { install_as: contentType, version_id: newContentId },
}); });
} catch (error) { } catch (error) {
console.error("Error reinstalling mod:", error); console.error("Error reinstalling mod:", error);
@ -527,10 +538,11 @@ const reinstallMod = async (modId: string, versionId: string) => {
const createBackup = async (backupName: string) => { const createBackup = async (backupName: string) => {
try { try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups`, { const response = (await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups`, {
method: "POST", method: "POST",
body: { name: backupName }, body: { name: backupName },
}); })) as { id: string };
return response.id;
} catch (error) { } catch (error) {
console.error("Error creating backup:", error); console.error("Error creating backup:", error);
throw error; throw error;
@ -604,6 +616,34 @@ const getAutoBackup = async () => {
} }
}; };
const lockBackup = async (backupId: string) => {
try {
return await PyroFetch(
`servers/${internalServerRefrence.value.serverId}/backups/${backupId}/lock`,
{
method: "POST",
},
);
} catch (error) {
console.error("Error locking backup:", error);
throw error;
}
};
const unlockBackup = async (backupId: string) => {
try {
return await PyroFetch(
`servers/${internalServerRefrence.value.serverId}/backups/${backupId}/unlock`,
{
method: "POST",
},
);
} catch (error) {
console.error("Error locking backup:", error);
throw error;
}
};
// ------------------ NETWORK ------------------ // // ------------------ NETWORK ------------------ //
const reserveAllocation = async (name: string): Promise<Allocation> => { const reserveAllocation = async (name: string): Promise<Allocation> => {
@ -858,7 +898,7 @@ const modules: any = {
setMotd, setMotd,
fetchConfigFile, fetchConfigFile,
}, },
mods: { content: {
get: async (serverId: string) => { get: async (serverId: string) => {
try { try {
const mods = await PyroFetch<Mod[]>(`servers/${serverId}/mods`); const mods = await PyroFetch<Mod[]>(`servers/${serverId}/mods`);
@ -873,9 +913,9 @@ const modules: any = {
return undefined; return undefined;
} }
}, },
install: installMod, install: installContent,
remove: removeMod, remove: removeContent,
reinstall: reinstallMod, reinstall: reinstallContent,
}, },
backups: { backups: {
get: async (serverId: string) => { get: async (serverId: string) => {
@ -893,6 +933,8 @@ const modules: any = {
download: downloadBackup, download: downloadBackup,
updateAutoBackup, updateAutoBackup,
getAutoBackup, getAutoBackup,
lock: lockBackup,
unlock: unlockBackup,
}, },
network: { network: {
get: async (serverId: string) => { get: async (serverId: string) => {
@ -1018,9 +1060,9 @@ type GeneralFunctions = {
fetchConfigFile: (fileName: string) => Promise<any>; fetchConfigFile: (fileName: string) => Promise<any>;
}; };
type ModFunctions = { type ContentFunctions = {
/** /**
* INTERNAL: Gets the mods of a server. * INTERNAL: Gets the list content of a server.
* @param serverId - The ID of the server. * @param serverId - The ID of the server.
* @returns * @returns
*/ */
@ -1028,23 +1070,26 @@ type ModFunctions = {
/** /**
* Installs a mod to a server. * Installs a mod to a server.
* @param contentType - The type of content to install.
* @param projectId - The ID of the project. * @param projectId - The ID of the project.
* @param versionId - The ID of the version. * @param versionId - The ID of the version.
*/ */
install: (projectId: string, versionId: string) => Promise<void>; install: (contentType: ContentType, projectId: string, versionId: string) => Promise<void>;
/** /**
* Removes a mod from a server. * Removes a mod from a server.
* @param modId - The ID of the mod. * @param contentType - The type of content to remove.
* @param contentId - The ID of the content.
*/ */
remove: (modId: string) => Promise<void>; remove: (contentType: ContentType, contentId: string) => Promise<void>;
/** /**
* Reinstalls a mod to a server. * Reinstalls a mod to a server.
* @param modId - The ID of the mod. * @param contentType - The type of content to reinstall.
* @param versionId - The ID of the version. * @param contentId - The ID of the content.
* @param newContentId - The ID of the new version.
*/ */
reinstall: (modId: string, versionId: string) => Promise<void>; reinstall: (contentType: ContentType, contentId: string, newContentId: string) => Promise<void>;
}; };
type BackupFunctions = { type BackupFunctions = {
@ -1058,6 +1103,7 @@ type BackupFunctions = {
/** /**
* Creates a new backup for the server. * Creates a new backup for the server.
* @param backupName - The name of the backup. * @param backupName - The name of the backup.
* @returns The ID of the backup.
*/ */
create: (backupName: string) => Promise<void>; create: (backupName: string) => Promise<void>;
@ -1098,6 +1144,18 @@ type BackupFunctions = {
* Gets the auto backup settings of the server. * Gets the auto backup settings of the server.
*/ */
getAutoBackup: () => Promise<AutoBackupSettings>; getAutoBackup: () => Promise<AutoBackupSettings>;
/**
* Locks a backup for the server.
* @param backupId - The ID of the backup.
*/
lock: (backupId: string) => Promise<void>;
/**
* Unlocks a backup for the server.
* @param backupId - The ID of the backup.
*/
unlock: (backupId: string) => Promise<void>;
}; };
type NetworkFunctions = { type NetworkFunctions = {
@ -1231,7 +1289,7 @@ type FSFunctions = {
}; };
type GeneralModule = General & GeneralFunctions; type GeneralModule = General & GeneralFunctions;
type ModsModule = { data: Mod[] } & ModFunctions; type ContentModule = { data: Mod[] } & ContentFunctions;
type BackupsModule = { data: Backup[] } & BackupFunctions; type BackupsModule = { data: Backup[] } & BackupFunctions;
type NetworkModule = { allocations: Allocation[] } & NetworkFunctions; type NetworkModule = { allocations: Allocation[] } & NetworkFunctions;
type StartupModule = Startup & StartupFunctions; type StartupModule = Startup & StartupFunctions;
@ -1239,7 +1297,7 @@ type FSModule = { auth: JWTAuth } & FSFunctions;
type ModulesMap = { type ModulesMap = {
general: GeneralModule; general: GeneralModule;
mods: ModsModule; content: ContentModule;
backups: BackupsModule; backups: BackupsModule;
network: NetworkModule; network: NetworkModule;
startup: StartupModule; startup: StartupModule;
@ -1247,7 +1305,7 @@ type ModulesMap = {
fs: FSModule; fs: FSModule;
}; };
type avaliableModules = ("general" | "mods" | "backups" | "network" | "startup" | "ws" | "fs")[]; type avaliableModules = ("general" | "content" | "backups" | "network" | "startup" | "ws" | "fs")[];
export type Server<T extends avaliableModules> = { export type Server<T extends avaliableModules> = {
[K in T[number]]?: ModulesMap[K]; [K in T[number]]?: ModulesMap[K];

View File

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

View File

@ -1,7 +1,35 @@
<template> <template>
<div class="contents"> <div class="contents">
<div <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" 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 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" 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"> <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 flex-col gap-2 leading-[150%]">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<IssuesIcon class="block h-8 w-8 text-red sm:hidden" /> <IssuesIcon class="flex h-8 w-8 shrink-0 text-red sm:hidden" />
<div class="flex gap-2 text-xl font-bold">{{ errorTitle }}</div> <div class="flex gap-2 text-2xl font-bold">{{ errorTitle }}</div>
</div> </div>
<div <div
@ -175,6 +203,14 @@
reinstalling your server, and if the problem persists, please contact Modrinth reinstalling your server, and if the problem persists, please contact Modrinth
support with your server's debug information. support with your server's debug information.
</div> </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 <div
v-if="errorTitle === 'Installation error'" v-if="errorTitle === 'Installation error'"
@ -262,6 +298,7 @@ import {
CheckIcon, CheckIcon,
FileIcon, FileIcon,
TransferIcon, TransferIcon,
LockIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { ButtonStyled } from "@modrinth/ui"; import { ButtonStyled } from "@modrinth/ui";
@ -294,7 +331,7 @@ const router = useRouter();
const serverId = route.params.id as string; const serverId = route.params.id as string;
const server = await usePyroServer(serverId, [ const server = await usePyroServer(serverId, [
"general", "general",
"mods", "content",
"backups", "backups",
"network", "network",
"startup", "startup",
@ -305,6 +342,7 @@ const server = await usePyroServer(serverId, [
watch( watch(
() => server.error, () => server.error,
(newError) => { (newError) => {
if (server.general?.status === "suspended") return;
if (newError && !newError.message.includes("Forbidden")) { if (newError && !newError.message.includes("Forbidden")) {
startPolling(); startPolling();
} }
@ -518,7 +556,7 @@ const handleWebSocketMessage = (data: WSEvent) => {
handleInstallationResult(data); handleInstallationResult(data);
break; break;
case "new-mod": case "new-mod":
server.refresh(["mods"]); server.refresh(["content"]);
console.log("New mod:", data); console.log("New mod:", data);
break; break;
case "auth-ok": case "auth-ok":

View File

@ -107,6 +107,7 @@
v-tooltip="'Backup in progress'" v-tooltip="'Backup in progress'"
class="size-6 animate-spin" class="size-6 animate-spin"
/> />
<LockIcon v-else-if="backup.locked" class="size-8" />
<BoxIcon v-else class="size-8" /> <BoxIcon v-else class="size-8" />
</div> </div>
<div class="flex min-w-0 flex-col gap-2"> <div class="flex min-w-0 flex-col gap-2">
@ -159,6 +160,16 @@
}, },
}, },
{ id: 'download', action: () => initiateDownload(backup.id) }, { id: 'download', action: () => initiateDownload(backup.id) },
{
id: 'lock',
action: () => {
if (backup.locked) {
unlockBackup(backup.id);
} else {
lockBackup(backup.id);
}
},
},
{ {
id: 'delete', id: 'delete',
action: () => { action: () => {
@ -172,6 +183,8 @@
<MoreHorizontalIcon class="h-5 w-5 bg-transparent" /> <MoreHorizontalIcon class="h-5 w-5 bg-transparent" />
<template #rename> <EditIcon /> Rename </template> <template #rename> <EditIcon /> Rename </template>
<template #restore> <ClipboardCopyIcon /> Restore </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 #download> <DownloadIcon /> Download </template>
<template #delete> <TrashIcon /> Delete </template> <template #delete> <TrashIcon /> Delete </template>
</UiServersTeleportOverflowMenu> </UiServersTeleportOverflowMenu>
@ -217,6 +230,8 @@ import {
TrashIcon, TrashIcon,
SettingsIcon, SettingsIcon,
BoxIcon, BoxIcon,
LockIcon,
OpenLockIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import type { Server } from "~/composables/pyroServers"; 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(() => { onMounted(() => {
watchEffect(() => { watchEffect(() => {
const hasOngoingBackups = backups.value.some((backup) => backup.ongoing); const hasOngoingBackups = backups.value.some((backup) => backup.ongoing);

View File

@ -10,7 +10,7 @@ import type { Server } from "~/composables/pyroServers";
const route = useNativeRoute(); const route = useNativeRoute();
const props = defineProps<{ 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); const data = computed(() => props.server.general);

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@
<div class="card flex flex-col gap-4"> <div class="card flex flex-col gap-4">
<label for="server-name-field" class="flex flex-col gap-2"> <label for="server-name-field" class="flex flex-col gap-2">
<span class="text-lg font-bold text-contrast">Server name</span> <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> </label>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<input <input
@ -51,7 +51,7 @@
/> />
.modrinth.gg .modrinth.gg
</div> </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"> <span v-if="!isValidLengthSubdomain">
Subdomain must be at least 5 characters long. Subdomain must be at least 5 characters long.
</span> </span>
@ -134,7 +134,7 @@ import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
import type { Server } from "~/composables/pyroServers"; import type { Server } from "~/composables/pyroServers";
const props = defineProps<{ 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); const data = computed(() => props.server.general);

View File

@ -123,7 +123,7 @@ const route = useNativeRoute();
const serverId = route.params.id as string; const serverId = route.params.id as string;
const props = defineProps<{ 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); const data = computed(() => props.server.general);

View File

@ -1,51 +1,152 @@
<template> <template>
<NewModal ref="editModal" header="Select modpack">
<UiServersProjectSelect type="modpack" @select="reinstallNew" />
</NewModal>
<NewModal <NewModal
ref="versionSelectModal" 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" @hide="onHide"
@show="onShow" @show="onShow"
> >
<div class="flex flex-col gap-4 md:w-[600px]"> <div class="flex flex-col gap-4 md:w-[600px]">
<p <p
v-if="isSecondPhase"
:style="{ :style="{
lineHeight: isSecondPhase ? '1.5' : undefined, lineHeight: isSecondPhase ? '1.5' : undefined,
marginBottom: isSecondPhase ? '-12px' : '0', marginBottom: isSecondPhase ? '-12px' : '0',
marginTop: isSecondPhase ? '-4px' : '-2px', marginTop: isSecondPhase ? '-4px' : '-2px',
}" }"
> >
{{ This will reinstall your server and erase all data. You may want to back up your server
isSecondPhase before proceeding. Are you sure you want to continue?
? "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."
}}
</p> </p>
<div v-if="!isSecondPhase" class="flex flex-col gap-2"> <div v-if="!isSecondPhase" class="flex flex-col gap-4">
<UiServersTeleportDropdownMenu <div class="mx-auto flex flex-row items-center gap-4">
v-model="selectedMCVersion" <div
name="mcVersion" class="grid size-16 place-content-center rounded-2xl border-[2px] border-solid border-button-border bg-button-bg shadow-sm"
:options="mcVersions" >
placeholder="Select Minecraft version..." <UiServersIconsLoaderIcon class="size-10" :loader="selectedLoader" />
/> </div>
<UiServersTeleportDropdownMenu <svg
v-if="selectedMCVersion && selectedLoader.toLowerCase() !== 'vanilla'" xmlns="http://www.w3.org/2000/svg"
v-model="selectedLoaderVersion" width="24"
name="loaderVersion" height="24"
:options="selectedLoaderVersions" viewBox="0 0 24 24"
placeholder="Select loader version..." fill="none"
/> stroke="currentColor"
<div class="mt-2 flex items-center gap-2"> stroke-width="2"
<input stroke-linecap="round"
id="hard-reset" stroke-linejoin="round"
:checked="hardReset" class="size-10"
class="switch stylized-toggle" >
type="checkbox" <path d="M5 9v6" />
@change="hardReset = ($event.target as HTMLInputElement).checked" <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> </div>
<div class="mt-4 flex justify-start gap-4"> <div class="mt-4 flex justify-start gap-4">
@ -53,13 +154,15 @@
<button :disabled="canInstall" @click="handleReinstall"> <button :disabled="canInstall" @click="handleReinstall">
<RightArrowIcon /> <RightArrowIcon />
{{ {{
isSecondPhase isBackingUp
? "Erase and install" ? "Backing up..."
: loadingServerCheck : isSecondPhase
? "Loading..." ? "Erase and install"
: isDangerous : loadingServerCheck
? "Erase and install" ? "Loading..."
: "Install" : isDangerous
? "Erase and install"
: "Install"
}} }}
</button> </button>
</ButtonStyled> </ButtonStyled>
@ -75,51 +178,130 @@
" "
> >
<XIcon /> <XIcon />
{{ isSecondPhase ? "No" : "Cancel" }} {{ isSecondPhase ? "Go back" : "Cancel" }}
</button> </button>
</ButtonStyled> </ButtonStyled>
</div> </div>
</div> </div>
</NewModal> </NewModal>
<NewModal ref="mrpackModal" header="Upload mrpack" @hide="onHide" @show="onShow"> <NewModal ref="mrpackModal" header="Uploading mrpack" @hide="onHide" @show="onShow">
<div> <div class="flex flex-col gap-4 md:w-[600px]">
<div class="mt-2 flex items-center gap-2"> <p
<input v-if="isSecondPhase"
id="hard-reset" :style="{
:checked="hardReset" lineHeight: isSecondPhase ? '1.5' : undefined,
class="switch stylized-toggle" marginBottom: isSecondPhase ? '-12px' : '0',
type="checkbox" marginTop: isSecondPhase ? '-4px' : '-2px',
@change="hardReset = ($event.target as HTMLInputElement).checked" }"
/> >
<label for="hard-reset">Clean reinstall</label> 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> </div>
<input
type="file"
accept=".mrpack"
class="mt-4"
:disabled="isLoading"
@change="uploadMrpack"
/>
<div class="mt-4 flex justify-start gap-4"> <div class="mt-4 flex justify-start gap-4">
<ButtonStyled :color="isDangerous ? 'red' : 'brand'"> <ButtonStyled :color="isDangerous ? 'red' : 'brand'">
<button :disabled="!mrpackFile || isLoading" @click="reinstallMrpack"> <button :disabled="canInstallUpload" @click="handleReinstallUpload">
<RightArrowIcon /> <RightArrowIcon />
{{ {{
isSecondPhase isBackingUp
? "Erase and install" ? "Backing up..."
: loadingServerCheck : isSecondPhase
? "Loading..." ? "Erase and install"
: isDangerous : loadingServerCheck
? "Erase and install" ? "Loading..."
: "Install" : isDangerous
? "Erase and install"
: "Install"
}} }}
</button> </button>
</ButtonStyled> </ButtonStyled>
<ButtonStyled> <ButtonStyled>
<button :disabled="isLoading" @click="mrpackModal?.hide"> <button
:disabled="isLoading"
@click="
if (isSecondPhase) {
isSecondPhase = false;
} else {
mrpackModal?.hide();
}
"
>
<XIcon /> <XIcon />
Cancel {{ isSecondPhase ? "Go back" : "Cancel" }}
</button> </button>
</ButtonStyled> </ButtonStyled>
</div> </div>
@ -199,7 +381,7 @@
<div v-else class="flex w-full flex-col items-center gap-2 sm:w-fit sm:flex-row"> <div v-else class="flex w-full flex-col items-center gap-2 sm:w-fit sm:flex-row">
<ButtonStyled> <ButtonStyled>
<nuxt-link class="!w-full sm:!w-auto" :to="`/modpacks?sid=${props.server.serverId}`"> <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> </nuxt-link>
</ButtonStyled> </ButtonStyled>
<span class="hidden sm:block">or</span> <span class="hidden sm:block">or</span>
@ -213,18 +395,21 @@
<div class="card flex flex-col gap-4"> <div class="card flex flex-col gap-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<h2 class="m-0 text-lg font-bold text-contrast">Mod loader</h2> <h2 class="m-0 text-lg font-bold text-contrast">Platform</h2>
<p class="m-0">Mod loaders allow you to run mods on your server.</p> <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"> <div v-if="data.upstream" class="mt-2 flex items-center gap-2">
<InfoIcon class="hidden sm:block" /> <InfoIcon class="hidden sm:block" />
<span class="text-sm text-secondary"> <span class="text-sm text-secondary">
Your server was installed from a modpack, which automatically chooses the appropriate Your server was installed from a modpack, which automatically chooses the appropriate
mod loader. platform.
</span> </span>
</div> </div>
</div> </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="{ :class="{
'pointer-events-none cursor-not-allowed select-none opacity-50': 'pointer-events-none cursor-not-allowed select-none opacity-50':
props.server.general?.status === 'installing' && isError, props.server.general?.status === 'installing' && isError,
@ -236,7 +421,7 @@
</div> </div>
</div> </div>
<UiServersPyroLoading v-else /> <div v-else />
</div> </div>
</template> </template>
@ -249,14 +434,18 @@ import {
InfoIcon, InfoIcon,
RightArrowIcon, RightArrowIcon,
XIcon, XIcon,
CompassIcon,
DropdownIcon,
ServerIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import type { Server } from "~/composables/pyroServers"; import type { Server } from "~/composables/pyroServers";
import type { Loaders } from "~/types/servers";
const route = useNativeRoute(); const route = useNativeRoute();
const serverId = route.params.id as string; const serverId = route.params.id as string;
const props = defineProps<{ const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>; server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -274,6 +463,7 @@ const isError = computed(() => props.server.general?.status === "error");
const isDangerous = computed(() => hardReset.value); const isDangerous = computed(() => hardReset.value);
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const isBackupLimited = computed(() => (props.server.backups?.data?.length || 0) >= 15); const isBackupLimited = computed(() => (props.server.backups?.data?.length || 0) >= 15);
const isBackingUp = ref(false);
const versionStrings = ["forge", "fabric", "quilt", "neo"] as const; const versionStrings = ["forge", "fabric", "quilt", "neo"] as const;
@ -312,7 +502,6 @@ const loaderVersions = (await Promise.all(
}[] }[]
>; >;
const editModal = ref();
const versionSelectModal = ref(); const versionSelectModal = ref();
const mrpackModal = ref(); const mrpackModal = ref();
@ -330,6 +519,16 @@ const canInstall = computed(() => {
return conds || !selectedLoaderVersion.value; 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 const mcVersions = tags.value.gameVersions
.filter((x) => x.version_type === "release") .filter((x) => x.version_type === "release")
.map((x) => x.version) .map((x) => x.version)
@ -343,27 +542,37 @@ const mcVersions = tags.value.gameVersions
}); });
const selectedLoaderVersions = computed(() => { const selectedLoaderVersions = computed(() => {
/* const loader = selectedLoader.value.toLowerCase();
loaderVersions[
selectedLoader.value.toLowerCase() === "neoforge" ? "neo" : selectedLoader.toLowerCase() if (loader === "paper") {
] return paperVersions.value[selectedMCVersion.value] || [];
.find((x) => x.id === selectedMCVersion)
?.loaders.map((x) => x.id) || []
*/
let loader = selectedLoader.value.toLowerCase();
if (loader === "neoforge") {
loader = "neo";
} }
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 // eslint-disable-next-line no-template-curly-in-string
(x) => x.id === "${modrinth.gameVersion}", (x) => x.id === "${modrinth.gameVersion}",
); );
if (backwardsCompatibleVersion) { if (backwardsCompatibleVersion) {
return backwardsCompatibleVersion.loaders.map((x) => x.id); return backwardsCompatibleVersion.loaders.map((x) => x.id);
} }
return ( return (
loaderVersions[loader] loaderVersions[apiLoader]
.find((x) => x.id === selectedMCVersion.value) ?.find((x) => x.id === selectedMCVersion.value)
?.loaders.map((x) => x.id) || [] ?.loaders.map((x) => x.id) || []
); );
}); });
@ -395,7 +604,7 @@ const versionIds = computed(() =>
const version = ref(); const version = ref();
const currentVersion = ref(); const currentVersion = ref();
const selectedLoader = ref(""); const selectedLoader = ref<Loaders>("Vanilla");
const selectedMCVersion = ref(""); const selectedMCVersion = ref("");
const selectedLoaderVersion = ref(""); const selectedLoaderVersion = ref("");
const isSecondPhase = ref(false); const isSecondPhase = ref(false);
@ -409,8 +618,35 @@ const updateData = async () => {
}; };
updateData(); 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) => { const selectLoader = (loader: string) => {
selectedLoader.value = loader; selectedLoader.value = loader as Loaders;
versionSelectModal.value.show(); versionSelectModal.value.show();
}; };
@ -421,24 +657,48 @@ const cachedVersions: Record<string, any> = {};
watch(selectedMCVersion, async () => { watch(selectedMCVersion, async () => {
if (selectedMCVersion.value.trim().length < 3) return; 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; 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 = ""; serverCheckError.value = "";
} else { } catch (error) {
serverCheckError.value = console.error(error);
"We couldn't find a server.jar for this version. Please pick another one."; 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}`; const backupName = `Reinstallation - ${format}`;
isLoading.value = true; 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 { } catch {
addNotification({ addNotification({
group: "server", 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 mrpackFile = ref<File | null>(null);
const uploadMrpack = (event: Event) => { const uploadMrpack = (event: Event) => {
@ -579,19 +850,86 @@ const uploadMrpack = (event: Event) => {
mrpackFile.value = target.files[0]; mrpackFile.value = target.files[0];
}; };
const reinstallMrpack = async () => { const handleReinstallUpload = async () => {
if (!mrpackFile.value) { if (hardReset.value && !backupServer.value && !isSecondPhase.value) {
isSecondPhase.value = true;
return; return;
} }
const mrpack = new File([mrpackFile.value], mrpackFile.value.name, { if (backupServer.value) {
type: mrpackFile.value.type, 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 { 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); await props.server.general?.reinstallFromMrpack(mrpack, hardReset.value);
emit("reinstall");
emit("reinstall", {
loader: "mrpack",
lVersion: "",
mVersion: "",
});
await nextTick(); await nextTick();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} catch (error) { } catch (error) {

View File

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

View File

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

View File

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

View File

@ -90,7 +90,7 @@ import { ButtonStyled } from "@modrinth/ui";
import type { Server } from "~/composables/pyroServers"; import type { Server } from "~/composables/pyroServers";
const props = defineProps<{ 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); const data = computed(() => props.server.general);

View File

@ -149,6 +149,17 @@ export type ServerState = "running" | "stopped" | "crashed";
// state: ServerState; // state: ServerState;
// } // }
export type Loaders =
| "Fabric"
| "Quilt"
| "Forge"
| "NeoForge"
| "Paper"
| "Spigot"
| "Bukkit"
| "Vanilla"
| "Purpur";
export interface WSLogEvent { export interface WSLogEvent {
event: "log"; event: "log";
message: string; message: string;

View File

@ -0,0 +1 @@
<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="lucide lucide-lock-open"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@ -99,6 +99,7 @@ import _LinkIcon from './icons/link.svg?component'
import _ListIcon from './icons/list.svg?component' import _ListIcon from './icons/list.svg?component'
import _ListEndIcon from './icons/list-end.svg?component' import _ListEndIcon from './icons/list-end.svg?component'
import _LockIcon from './icons/lock.svg?component' import _LockIcon from './icons/lock.svg?component'
import _OpenLockIcon from './icons/lock-open.svg?component'
import _LogInIcon from './icons/log-in.svg?component' import _LogInIcon from './icons/log-in.svg?component'
import _LogOutIcon from './icons/log-out.svg?component' import _LogOutIcon from './icons/log-out.svg?component'
import _MailIcon from './icons/mail.svg?component' import _MailIcon from './icons/mail.svg?component'
@ -279,6 +280,7 @@ export const LinkIcon = _LinkIcon
export const ListIcon = _ListIcon export const ListIcon = _ListIcon
export const ListEndIcon = _ListEndIcon export const ListEndIcon = _ListEndIcon
export const LockIcon = _LockIcon export const LockIcon = _LockIcon
export const OpenLockIcon = _OpenLockIcon
export const LogInIcon = _LogInIcon export const LogInIcon = _LogInIcon
export const LogOutIcon = _LogOutIcon export const LogOutIcon = _LogOutIcon
export const MailIcon = _MailIcon export const MailIcon = _MailIcon

View File

@ -75,7 +75,15 @@
/> --> /> -->
<div class="grid lg:grid-cols-5 grid-cols-3 gap-4"> <div class="grid lg:grid-cols-5 grid-cols-3 gap-4">
<button <button
v-for="loader in ['Vanilla', 'Fabric', 'Forge', 'Quilt', 'NeoForge']" v-for="loader in [
'Vanilla',
'Fabric',
'Forge',
'Quilt',
'NeoForge',
'Paper',
'Purpur',
]"
:key="loader" :key="loader"
class="!h-24 btn flex !flex-col !items-center !justify-between !pt-4 !pb-3 !w-full" class="!h-24 btn flex !flex-col !items-center !justify-between !pt-4 !pb-3 !w-full"
:style="{ :style="{