mirror of
https://github.com/modrinth/code.git
synced 2026-09-04 22:10:15 +00:00
* Reapply "fix: start swapping useBaseFetch usages to api-client"
This reverts commit f4f33db701.
* fix: bugs
* fix: analytics
* fix: lint
295 lines
9.1 KiB
Vue
295 lines
9.1 KiB
Vue
<template>
|
|
<div class="relative h-full w-full select-none overflow-y-auto">
|
|
<div v-if="propsData" class="flex h-full w-full flex-col justify-between gap-4 overflow-y-auto">
|
|
<Admonition
|
|
v-if="missingKnownProperties.length > 0"
|
|
type="warning"
|
|
body="Some expected properties are missing from your server.properties - this usually means the server hasn't completed its first startup yet."
|
|
/>
|
|
<div class="card flex flex-col gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<h2 class="m-0 text-lg font-bold text-contrast">Server properties</h2>
|
|
<div class="m-0">
|
|
Edit the Minecraft server properties file. If you're unsure about a specific property,
|
|
the
|
|
<NuxtLink
|
|
class="goto-link !inline-block"
|
|
to="https://minecraft.wiki/w/Server.properties"
|
|
external
|
|
>
|
|
Minecraft Wiki
|
|
</NuxtLink>
|
|
has more detailed information.
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-col gap-4 rounded-2xl bg-table-alternateRow p-4">
|
|
<div class="w-full text-sm">
|
|
<label for="search-server-properties" class="sr-only"> Search server properties </label>
|
|
<StyledInput
|
|
id="search-server-properties"
|
|
v-model="searchInput"
|
|
wrapper-class="w-full"
|
|
type="search"
|
|
:icon="SearchIcon"
|
|
name="search"
|
|
autocomplete="off"
|
|
placeholder="Search server properties..."
|
|
/>
|
|
</div>
|
|
<div
|
|
v-for="(_value, key) in filteredProperties"
|
|
:key="key"
|
|
class="flex flex-row flex-wrap items-center justify-between py-2"
|
|
>
|
|
<span :id="`property-label-${key}`">{{ formatPropertyName(key) }}</span>
|
|
|
|
<div
|
|
v-if="getPropertyDef(key).type === 'dropdown'"
|
|
class="mt-2 flex w-full sm:w-[320px] sm:justify-end"
|
|
>
|
|
<Combobox
|
|
:id="`server-property-${key}`"
|
|
v-model="liveProperties[key]"
|
|
:name="formatPropertyName(key)"
|
|
:options="
|
|
(getPropertyDef(key) as DropdownPropertyDef).options.map((v) => ({
|
|
value: v,
|
|
label: formatPropertyName(v),
|
|
}))
|
|
"
|
|
:aria-labelledby="`property-label-${key}`"
|
|
:display-value="formatPropertyName(String(liveProperties[key] ?? 'Select...'))"
|
|
/>
|
|
</div>
|
|
<div v-else-if="getPropertyDef(key).type === 'toggle'" class="flex justify-end">
|
|
<Toggle
|
|
:id="`server-property-${key}`"
|
|
:model-value="liveProperties[key] === 'true'"
|
|
:aria-labelledby="`property-label-${key}`"
|
|
@update:model-value="liveProperties[key] = $event ? 'true' : 'false'"
|
|
/>
|
|
</div>
|
|
<div v-else-if="getPropertyDef(key).type === 'number'" class="mt-2 w-full sm:w-[320px]">
|
|
<StyledInput
|
|
:id="`server-property-${key}`"
|
|
:model-value="liveProperties[key]"
|
|
type="number"
|
|
wrapper-class="w-full"
|
|
:aria-labelledby="`property-label-${key}`"
|
|
@update:model-value="liveProperties[key] = String($event)"
|
|
/>
|
|
</div>
|
|
<div v-else class="mt-2 flex w-full justify-end sm:w-[320px]">
|
|
<StyledInput
|
|
:id="`server-property-${key}`"
|
|
v-model="liveProperties[key]"
|
|
wrapper-class="w-full"
|
|
:aria-labelledby="`property-label-${key}`"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="flex h-full w-full items-center justify-center">
|
|
<SpinnerIcon class="animate-spin" />
|
|
</div>
|
|
|
|
<SaveBanner
|
|
:is-visible="hasUnsavedChanges"
|
|
:server-id="serverId"
|
|
:is-updating="isUpdating || busyReasons.length > 0"
|
|
restart
|
|
:save="() => saveProperties()"
|
|
:reset="resetProperties"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Archon } from '@modrinth/api-client'
|
|
import { SearchIcon, SpinnerIcon } from '@modrinth/assets'
|
|
import {
|
|
Admonition,
|
|
Combobox,
|
|
injectModrinthClient,
|
|
injectModrinthServerContext,
|
|
injectNotificationManager,
|
|
StyledInput,
|
|
Toggle,
|
|
} from '@modrinth/ui'
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
|
import Fuse from 'fuse.js'
|
|
import { computed, ref, watch } from 'vue'
|
|
|
|
import SaveBanner from '~/components/ui/servers/SaveBanner.vue'
|
|
|
|
const { addNotification } = injectNotificationManager()
|
|
const client = injectModrinthClient()
|
|
const { serverId, worldId, powerState, busyReasons } = injectModrinthServerContext()
|
|
const queryClient = useQueryClient()
|
|
|
|
const searchInput = ref('')
|
|
|
|
type DropdownPropertyDef = { type: 'dropdown'; options: string[] }
|
|
type PropertyDef = { type: 'toggle' } | { type: 'number' } | { type: 'text' } | DropdownPropertyDef
|
|
|
|
const KNOWN_PROPERTIES: Record<string, PropertyDef> = {
|
|
allow_cheats: { type: 'toggle' },
|
|
allow_flight: { type: 'toggle' },
|
|
difficulty: { type: 'dropdown', options: ['peaceful', 'easy', 'normal', 'hard'] },
|
|
enforce_whitelist: { type: 'toggle' },
|
|
force_gamemode: { type: 'toggle' },
|
|
gamemode: { type: 'dropdown', options: ['survival', 'creative', 'adventure', 'spectator'] },
|
|
generate_structures: { type: 'toggle' },
|
|
generator_settings: { type: 'text' },
|
|
hardcore: { type: 'toggle' },
|
|
level_seed: { type: 'text' },
|
|
level_type: { type: 'text' },
|
|
max_players: { type: 'number' },
|
|
max_tick_time: { type: 'number' },
|
|
motd: { type: 'text' },
|
|
pause_when_empty_seconds: { type: 'number' },
|
|
player_idle_timeout: { type: 'number' },
|
|
require_resource_pack: { type: 'toggle' },
|
|
resource_pack: { type: 'text' },
|
|
resource_pack_id: { type: 'text' },
|
|
resource_pack_sha1: { type: 'text' },
|
|
simulation_distance: { type: 'number' },
|
|
spawn_protection: { type: 'number' },
|
|
sync_chunk_writes: { type: 'toggle' },
|
|
view_distance: { type: 'number' },
|
|
white_list: { type: 'toggle' },
|
|
}
|
|
|
|
function getPropertyDef(key: string): PropertyDef {
|
|
return KNOWN_PROPERTIES[key] ?? { type: 'text' }
|
|
}
|
|
|
|
const queryKey = computed(() => ['servers', 'properties', 'v1', serverId, worldId.value])
|
|
|
|
const { data: propsData } = useQuery({
|
|
queryKey,
|
|
queryFn: () => client.archon.properties_v1.getProperties(serverId, worldId.value!),
|
|
enabled: computed(() => worldId.value !== null),
|
|
})
|
|
|
|
function flattenProperties(data: Archon.Content.v1.PropertiesFields): Record<string, string> {
|
|
const result: Record<string, string> = {}
|
|
if (data.known) {
|
|
for (const [key, value] of Object.entries(data.known)) {
|
|
if (value != null) result[key] = value
|
|
}
|
|
}
|
|
if (data.custom) {
|
|
for (const [key, value] of Object.entries(data.custom)) {
|
|
if (value != null) result[key] = value
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
const liveProperties = ref<Record<string, string>>({})
|
|
const originalProperties = ref<Record<string, string>>({})
|
|
|
|
function syncFormFromData() {
|
|
if (!propsData.value) return
|
|
const flat = flattenProperties(propsData.value)
|
|
liveProperties.value = { ...flat }
|
|
originalProperties.value = { ...flat }
|
|
}
|
|
|
|
watch(
|
|
propsData,
|
|
(newData, oldData) => {
|
|
if (newData && !oldData) {
|
|
syncFormFromData()
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
watch(powerState, () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKey.value })
|
|
})
|
|
|
|
const missingKnownProperties = computed(() =>
|
|
Object.keys(KNOWN_PROPERTIES).filter((key) => !(key in liveProperties.value)),
|
|
)
|
|
|
|
const hasUnsavedChanges = computed(() =>
|
|
Object.keys(liveProperties.value).some(
|
|
(key) => liveProperties.value[key] !== originalProperties.value[key],
|
|
),
|
|
)
|
|
|
|
function buildPatch(): Archon.Content.v1.PatchPropertiesFields {
|
|
const known: Record<string, string> = {}
|
|
const custom: Record<string, string> = {}
|
|
|
|
for (const key of Object.keys(liveProperties.value)) {
|
|
if (liveProperties.value[key] === originalProperties.value[key]) continue
|
|
if (key in KNOWN_PROPERTIES) {
|
|
known[key] = liveProperties.value[key]
|
|
} else {
|
|
custom[key] = liveProperties.value[key]
|
|
}
|
|
}
|
|
|
|
const patch: Archon.Content.v1.PatchPropertiesFields = {}
|
|
if (Object.keys(known).length > 0) {
|
|
patch.known = known as Archon.Content.v1.KnownPropertiesFields
|
|
}
|
|
if (Object.keys(custom).length > 0) {
|
|
patch.custom = custom
|
|
}
|
|
return patch
|
|
}
|
|
|
|
const { mutate: saveProperties, isPending: isUpdating } = useMutation({
|
|
mutationFn: () =>
|
|
client.archon.properties_v1.patchProperties(serverId, worldId.value!, buildPatch()),
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: queryKey.value })
|
|
syncFormFromData()
|
|
addNotification({
|
|
type: 'success',
|
|
title: 'Server properties updated',
|
|
text: 'Your server properties were successfully changed.',
|
|
})
|
|
},
|
|
onError: (error) => {
|
|
addNotification({
|
|
type: 'error',
|
|
title: 'Failed to update server properties',
|
|
text: error instanceof Error ? error.message : 'An error occurred.',
|
|
})
|
|
},
|
|
})
|
|
|
|
function resetProperties() {
|
|
syncFormFromData()
|
|
}
|
|
|
|
const fuse = computed(() => {
|
|
const entries = Object.entries(liveProperties.value).map(([key, value]) => ({
|
|
key,
|
|
value: String(value),
|
|
}))
|
|
return new Fuse(entries, { keys: ['key', 'value'], threshold: 0.2 })
|
|
})
|
|
|
|
const filteredProperties = computed(() => {
|
|
if (!searchInput.value?.trim()) return liveProperties.value
|
|
const results = fuse.value.search(searchInput.value)
|
|
return Object.fromEntries(results.map(({ item }) => [item.key, liveProperties.value[item.key]]))
|
|
})
|
|
|
|
function formatPropertyName(name: string): string {
|
|
return name
|
|
.split('_')
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ')
|
|
}
|
|
</script>
|