feat: server management in app (#5628)
* start new server settings tabs * update properties tab to match design * better stying in general tab * feat: add suffix input for hostname field * implement tables for allocations and DNS records * add tags for dns record type * small gap adjustment * polish advanced page * adjust properties page hierarchy * fix searching properties, empty state and projection radius appearing * pnpm prepr * update copy to match designs * fix suffix input component * style fixes and match heading size * small fix * fix search allocations placeholder * adjust table styles * move all installation settings helper text to below input * update icon to use overflow menu buttons * fix modal to be consistent * open advanced properties when search * remove other and custom properties, and update styles * remove hide/show all java versions * handle mc 26 * refactor: move server settings pages into /ui and add app ServerSettingsModal * hook up server pages for app * add server page header to app * hook up server settings modal * use large size * fix card box shadow style * fix hostname input for app * fix app/website card containers * implement external tabs for billing and admin billing * fix save banner fixed to parent instead of page body * remove unused prop to FriendsList causing warning in app * fix client-only not available for app * fix bottom cut off * wire node auth * implement full copy buttons * dedup copy button tailwind styles * fix hover class not working in @apply * fix spacing * fix error validation styles * apply consistent styles and spacing * feat: update hosting server card (#5609) * fix type errors * fix some stylesheets not imported for storybook * add server listing stories * add fix for frontend stylesheet imports * remove props. * convert copy code to use tailwind * update server listing component styles * update server info label styles * start status/player count info label, more style updates and fixes * add new server card buttons * hook up server cards and implement updated styles * hook up on download button * fix tauri throwing error when api returns 204 No Content * hook up purchase server modal in app * fix upgrading state loading icon * pnpm prepr * filter out servers past 30 days after cancellation * do not apply opacity on lock or spiner icons * fix disabled server icon background * update pending change stage * handle known suspension states * refactor: reduce code duplication for server listing * update disabled state text color * fix loading icon color * clean up copy * fix disabled opacity for server card * update server listing files kept to be countdown * implement resubscribe modal * implement proper provisioning state for resubscribe * fix duplicate attribute and pnpm prepr * feat: add shared UI package auth DI * feat: update purchase server flow (#5714) * implement server list empty state component * fix stories and adjust spacing * implement select plan design refresh * implement auth for empty server list * use refs instead of reactive * pnpm prepr * fix auth usage for empty servers list * move app auth provider setup to src/providers/setup * pnpm prepr * fix max height * style fix * fix getCreds no auth is blocking api client * implement servers guest plan modal and signin which redirects back to modal's next step * refactor guest plan select logic into provider * implement sign in or create account popup * remove force empty serverList * add download button for suspended mod and generic * add handling for when user logs out * QA pass style fixes * more consistent page styles * fix duplicate export * refactor: remove all fallback stuff from resubscribe modal * implement shared download latest backup util * i18n pass * pnpm prepr * fix region being selected if ping failed * pnpm prepr * feat: servers in app finalization (#5744) * feat: start on shared console implementation into logs and overview pages * fix: terminal gap issues * feat: swap word wrap for full screen * fix: stats cards alignment * fix: stats * feat: fix console clear + remove copy * fix: lint * fix: use reset not clear * feat: shared server header & overview page for app and website (#5736) * feat: implement shared server header for app and website * feat: implement wrapped overview page with shared composable and hook it up * pnpm prepr * fix: bugs * qa: cleanup * feat: root.vue shared layout * feat: delete old options pages + fix discovery frontend * fix: discovery * fix: misc style/layout issues * fix page padding * fix: modal height jankiness * feat: implement server install content in app and server setup modal with DI * fix: spacing * remove servers in app feature flag * Revert "remove servers in app feature flag" This reverts commit 86e284c4bdd6fa42c3c8fbaf1efbec41f4d1c6d2. * fix: qa * feat: remove legacy components from apps/frontend/src/components/ui/servers --------- Co-authored-by: Calum H. (IMB11) <contact@cal.engineer> * qa pass (#5738) * fix: qa * feat: qa * fix: server icon fetch fails due to global node auth race condition overriding each other * fix: lint * fix: server icon upload/sync and centralize logic * fix: server settings modal not closing for server reset * fix: better server sorting * feat: copy address in server listing card * fix: notification panel in modal and when overlapping with action bar * fix: empty server list empty state flashing when refresh, fixed by adding isReady auth flag * feat: use floating action bar for save banner * fix: saving state in save bar * fix: edit server icon styling * fix: confirm modal to have consistent buttons * feat: loading animation for server panel + caching improvements for app * pnpm prepr * feat: search page deduplication (#5754) * fix: action bar behind modal * fix: remove warning modal for stopping * fix: server cards states * we hate webkit we hate webkit * fix: update allocation creation to not use modal * fix: properties tab spacing and styles * feat: add files tab copy * fix: advanced properties icon * fix: remove back to all servers link * feat: add files tab link in copy * fix: server header styles to be consistent with instance * fix: add header icons back * feat: update instance settings icon to be consistent * fix: icon container * feat: upload state persistence across tabs * fix: server labels text wrapping * fix: use surface-5 border * fix: loading spinner showing with onboarding below * feat: new server button shows purchase modal in website * fix: billing page not showing quarterly interval * fix: server downgrade not showing updated subscription notification * fix: server settings invalidate saved state and remove server context provider since its already provided in the page * pnpm prepr * add stripe publishable key to app build * feat: console highlighting * fix: rename servers title to modrinth hosting * feat: search fix * fix: qa/styles * fix: ip click active and remove power dont ask again * fix: qa * feat: highlighting fix console * fix: disable conflicts action * fix: error dismiss bug * feat: modal clarification * fix: files perms issue * fix: lint * feat: modal fix * enable show uptime * fix: add loading state to edit server icon * fix: notification panel take in has sidebar from settings * fix: consistency pass on app settings * fix: consistency pass on instance settings * pnpm prepr * fix: nagivate to billing button in app to go to website * fix: stripe return url in app causing app to open modrinth.com in tauri * refactor: better show polling UI code * fix: new server polling comparison to use server ids instead of length * fix: buttonstyled story * fix: button styling * fix: content.vue regression * feat: project url redirects * fix: breadcrumbs * fix: purchase with newly added card * fix: console ordering problems * fix: app-frontend missing env config and staging environment * fix: log syncing for instances and server panel accidentally * fix: QA issues * fix: server page loading state * fix: stats card logic * fix: lint * fix: qa * fix: console height padding * fix: terminal padding + loading indicator * feat: update medal server listing styling * fix: no upgrade button for medal server listing in app * fix: go to overview instead of content tab after onboarding * fix: qa * fix: teleport modals to body * fix: logs tab + qa * fix: local storage for user preferences * fix: qa loading indic * feat: considitonal debug and trace * fix: jump to top on install bug * feat: swap out server hard drive icon to server stack icon * feat: servers in app feature flag default true * fix: highlight row ufll * fix: webkit thing onto a tag * fix: input field * fix: clear fix * fix: lint * fix: fmt * feat: improve share modal and bring it back for sharing log * pnpm prepr * fix: menu overflowing * feat: remove servers in app feature flag * fix: server stat charts no longer showing color * fix: library nav no primary state * fix: better modal height and width * fix: highlighting bugs * fix: empty states * fix: delay import to fix overview page slow load on MacOS * fix: medal server listing too bright on light mode * fix: admon analysis + fix logs * fix: bug * fix: clear purchase intent from sign-in after closing modal * performance: improve server manage stats loading by splitting reactivity * fix: deploy + admon + disable highlighting * fix: clippy --------- Co-authored-by: tdgao <mr.trumgao@gmail.com> Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com> * feat: temp wrangler * fix: lint * fix: logs upload * fix: console empty state and admon regressions * fix: fields * feat: log deleting + prefetch for Logs.vue * feat: move delete before share * feat: clear endpoint * feat: we ball! --------- Co-authored-by: Calum H. <calum@modrinth.com> Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
@@ -0,0 +1,560 @@
|
||||
<template>
|
||||
<div class="relative h-screen w-full select-none max-h-[min(70vh,750px)]">
|
||||
<div v-if="propsData" class="flex h-full w-full flex-col justify-between gap-4">
|
||||
<Admonition
|
||||
v-if="hasNoProperties"
|
||||
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="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="m-0">
|
||||
Edit the Minecraft server properties file here, or use the
|
||||
<AutoLink
|
||||
class="goto-link !inline-block"
|
||||
:to="filesTabLink"
|
||||
@click="onFilesTabLinkClick"
|
||||
>
|
||||
Files tab
|
||||
</AutoLink>
|
||||
to edit the full file. If you're unsure about a setting, the
|
||||
<AutoLink
|
||||
class="goto-link !inline-block"
|
||||
to="https://minecraft.wiki/w/Server.properties"
|
||||
target="_blank"
|
||||
>
|
||||
Minecraft Wiki
|
||||
</AutoLink>
|
||||
has more details.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 class="flex flex-col gap-3 pb-2">
|
||||
<div class="flex flex-col gap-6">
|
||||
<!-- Basic Properties -->
|
||||
<!-- [&:not(:has(*:not(:empty)))]:hidden is to hide parent if all children are empty -->
|
||||
<div
|
||||
class="rounded-2xl border border-solid border-surface-5 p-4 pb-2 [&:not(:has(*:not(:empty)))]:hidden"
|
||||
>
|
||||
<div class="flex w-full flex-col gap-1.5">
|
||||
<div v-if="isPropertyVisible('gamemode')" class="flex flex-col gap-2.5 my-1">
|
||||
<span class="font-semibold text-contrast">Gamemode</span>
|
||||
<Chips
|
||||
v-model="combinedGamemode"
|
||||
:items="gamemodeItems"
|
||||
:format-label="capitalize"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="combinedGamemode !== 'hardcore' && isPropertyVisible('difficulty')"
|
||||
class="flex flex-col gap-2.5 my-1"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Difficulty</span>
|
||||
<Chips
|
||||
v-model="selectedDifficulty"
|
||||
:items="difficultyItems"
|
||||
:format-label="capitalize"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isPropertyVisible('max_players')" class="flex flex-col gap-2.5 my-1">
|
||||
<span class="font-semibold text-contrast">Max players</span>
|
||||
<StyledInput
|
||||
id="server-property-max-players"
|
||||
:model-value="liveProperties.max_players"
|
||||
type="number"
|
||||
placeholder="20"
|
||||
wrapper-class="w-full max-w-[450px]"
|
||||
@update:model-value="liveProperties.max_players = String($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isPropertyVisible('motd')" class="flex flex-col gap-2.5 my-1">
|
||||
<span class="font-semibold text-contrast">MOTD</span>
|
||||
<StyledInput
|
||||
id="server-property-motd"
|
||||
v-model="liveProperties.motd"
|
||||
placeholder="A Minecraft Server"
|
||||
wrapper-class="w-full max-w-[450px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isPropertyVisible('allow_flight')"
|
||||
class="flex flex-row items-center justify-between gap-4 h-10"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Allow flight</span>
|
||||
<Toggle
|
||||
id="server-property-allow-flight"
|
||||
:model-value="liveProperties.allow_flight === 'true'"
|
||||
@update:model-value="liveProperties.allow_flight = $event ? 'true' : 'false'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isPropertyVisible('allow_cheats')"
|
||||
class="flex flex-row items-center justify-between gap-4 h-10"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Allow cheats</span>
|
||||
<Toggle
|
||||
id="server-property-allow-cheats"
|
||||
:model-value="liveProperties.allow_cheats === 'true'"
|
||||
@update:model-value="liveProperties.allow_cheats = $event ? 'true' : 'false'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isPropertyVisible('white_list')"
|
||||
class="flex flex-row items-center justify-between gap-4 h-10"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Enable whitelist</span>
|
||||
<Toggle id="server-property-whitelist" v-model="whitelistEnabled" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isPropertyVisible('spawn_protection')"
|
||||
class="flex flex-row items-center justify-between gap-4 h-10"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Enable spawn protection</span>
|
||||
<Toggle
|
||||
id="server-property-spawn-protection-toggle"
|
||||
v-model="spawnProtectionEnabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="spawnProtectionEnabled && isPropertyVisible('spawn_protection')"
|
||||
class="flex items-center justify-between h-10"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Protection radius</span>
|
||||
<StyledInput
|
||||
id="server-property-spawn-protection-radius"
|
||||
:model-value="liveProperties.spawn_protection"
|
||||
type="number"
|
||||
wrapper-class="w-full sm:w-[100px]"
|
||||
input-class="text-right"
|
||||
@update:model-value="liveProperties.spawn_protection = String($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Advanced Properties -->
|
||||
<Accordion
|
||||
v-if="hasVisibleAdvancedProperties"
|
||||
overflow-visible
|
||||
:force-open="isSearchActive"
|
||||
button-class="flex w-full flex-col gap-2 bg-transparent m-0 p-0 border-none"
|
||||
>
|
||||
<template #title>
|
||||
<span class="text-lg font-semibold text-contrast">Advanced properties</span>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col gap-6 pt-4">
|
||||
<template v-for="group in advancedGroupedProperties" :key="group.label">
|
||||
<div v-if="hasVisibleProperties(group)" class="flex flex-col gap-2.5">
|
||||
<h3 class="m-0 text-base font-semibold text-contrast">
|
||||
{{ group.label }}
|
||||
</h3>
|
||||
<div
|
||||
class="flex flex-col gap-2 rounded-2xl border border-solid border-surface-5 p-4"
|
||||
>
|
||||
<template v-for="key in group.properties" :key="key">
|
||||
<div
|
||||
v-if="isPropertyVisible(key)"
|
||||
class="flex flex-row flex-wrap items-center justify-between h-10"
|
||||
>
|
||||
<span :id="`property-label-${key}`" class="font-semibold text-contrast">
|
||||
{{ formatPropertyName(key) }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-if="getPropertyDef(key).type === 'toggle'"
|
||||
class="flex w-full justify-end sm:w-[320px]"
|
||||
>
|
||||
<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="w-full sm:w-[320px]"
|
||||
>
|
||||
<StyledInput
|
||||
:id="`server-property-${key}`"
|
||||
:model-value="liveProperties[key]"
|
||||
type="number"
|
||||
placeholder="Type here..."
|
||||
wrapper-class="w-full"
|
||||
:aria-labelledby="`property-label-${key}`"
|
||||
@update:model-value="liveProperties[key] = String($event)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex w-full justify-end sm:w-[320px]">
|
||||
<StyledInput
|
||||
:id="`server-property-${key}`"
|
||||
v-model="liveProperties[key]"
|
||||
placeholder="Type here..."
|
||||
wrapper-class="w-full"
|
||||
:aria-labelledby="`property-label-${key}`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
All other properties can be edited in server.properties via the
|
||||
<AutoLink
|
||||
class="goto-link !inline-block"
|
||||
:to="filesTabLink"
|
||||
@click="onFilesTabLinkClick"
|
||||
>
|
||||
Files tab </AutoLink
|
||||
>.
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
|
||||
<div
|
||||
v-if="hasNoResults"
|
||||
class="flex flex-col items-center gap-2 py-8 text-center text-secondary"
|
||||
>
|
||||
<SearchIcon class="size-10" />
|
||||
<span class="text-lg font-semibold text-contrast">No properties found</span>
|
||||
<span>No properties match "{{ searchInput }}".</span>
|
||||
</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 || isUpdating"
|
||||
:server-id="serverId"
|
||||
:is-updating="isUpdating || busyReasons.length > 0"
|
||||
restart
|
||||
:save="
|
||||
async () => {
|
||||
await saveProperties()
|
||||
}
|
||||
"
|
||||
:reset="resetProperties"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import { SearchIcon, SpinnerIcon } from '@modrinth/assets'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import Fuse from 'fuse.js'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { Accordion, Admonition, AutoLink, Chips, StyledInput, Toggle } from '#ui/components'
|
||||
import SaveBanner from '#ui/components/servers/SaveBanner.vue'
|
||||
import { injectServerSettings } from '#ui/layouts/shared/server-settings'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
} from '#ui/providers'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
const { serverId, worldId, powerState, busyReasons } = injectModrinthServerContext()
|
||||
const queryClient = useQueryClient()
|
||||
const filesTabLink = computed(
|
||||
() => `/hosting/manage/${encodeURIComponent(serverId)}/files?path=/&editing=server.properties`,
|
||||
)
|
||||
const serverSettings = injectServerSettings(null)
|
||||
|
||||
const searchInput = ref('')
|
||||
|
||||
function onFilesTabLinkClick() {
|
||||
serverSettings?.closeModal?.()
|
||||
}
|
||||
|
||||
type PropertyDef = { type: 'toggle' } | { type: 'number' } | { type: 'text' }
|
||||
|
||||
const KNOWN_PROPERTIES: Record<string, PropertyDef> = {
|
||||
allow_cheats: { type: 'toggle' },
|
||||
allow_flight: { type: 'toggle' },
|
||||
difficulty: { type: 'text' },
|
||||
enforce_whitelist: { type: 'toggle' },
|
||||
force_gamemode: { type: 'toggle' },
|
||||
gamemode: { type: 'text' },
|
||||
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 ADVANCED_GROUPS = [
|
||||
{
|
||||
label: 'Performance',
|
||||
keys: [
|
||||
'view_distance',
|
||||
'simulation_distance',
|
||||
'sync_chunk_writes',
|
||||
'max_tick_time',
|
||||
'player_idle_timeout',
|
||||
'pause_when_empty_seconds',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Resource Pack',
|
||||
keys: ['resource_pack', 'resource_pack_id', 'resource_pack_sha1', 'require_resource_pack'],
|
||||
},
|
||||
]
|
||||
|
||||
type CombinedGamemode = 'survival' | 'creative' | 'hardcore'
|
||||
const gamemodeItems: CombinedGamemode[] = ['survival', 'creative', 'hardcore']
|
||||
const difficultyItems = ['peaceful', 'easy', 'normal', 'hard']
|
||||
|
||||
function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
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>>({})
|
||||
let previousSpawnProtection = '16'
|
||||
|
||||
function syncFormFromData() {
|
||||
if (!propsData.value) return
|
||||
const flat = flattenProperties(propsData.value)
|
||||
liveProperties.value = { ...flat }
|
||||
originalProperties.value = { ...flat }
|
||||
const sp = flat.spawn_protection
|
||||
if (sp && sp !== '0') {
|
||||
previousSpawnProtection = sp
|
||||
}
|
||||
}
|
||||
|
||||
const hasNoProperties = computed(() => Object.keys(liveProperties.value).length === 0)
|
||||
|
||||
const hasUnsavedChanges = computed(() =>
|
||||
Object.keys(liveProperties.value).some(
|
||||
(key) => liveProperties.value[key] !== originalProperties.value[key],
|
||||
),
|
||||
)
|
||||
|
||||
watch(
|
||||
propsData,
|
||||
(newData) => {
|
||||
if (newData && !hasUnsavedChanges.value) {
|
||||
syncFormFromData()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(powerState, () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||
})
|
||||
|
||||
const combinedGamemode = computed<CombinedGamemode>({
|
||||
get() {
|
||||
if (liveProperties.value.hardcore === 'true') return 'hardcore'
|
||||
if (liveProperties.value.gamemode === 'creative') return 'creative'
|
||||
return 'survival'
|
||||
},
|
||||
set(value) {
|
||||
if (value === 'hardcore') {
|
||||
liveProperties.value.gamemode = 'survival'
|
||||
liveProperties.value.hardcore = 'true'
|
||||
liveProperties.value.difficulty = 'hard'
|
||||
} else {
|
||||
liveProperties.value.gamemode = value
|
||||
liveProperties.value.hardcore = 'false'
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const selectedDifficulty = computed({
|
||||
get: () => liveProperties.value.difficulty ?? 'normal',
|
||||
set: (v: string) => {
|
||||
liveProperties.value.difficulty = v
|
||||
},
|
||||
})
|
||||
|
||||
const whitelistEnabled = computed({
|
||||
get: () => liveProperties.value.white_list === 'true',
|
||||
set: (v: boolean) => {
|
||||
liveProperties.value.white_list = v ? 'true' : 'false'
|
||||
liveProperties.value.enforce_whitelist = v ? 'true' : 'false'
|
||||
},
|
||||
})
|
||||
|
||||
const spawnProtectionEnabled = computed({
|
||||
get: () => {
|
||||
const val = liveProperties.value.spawn_protection
|
||||
return val !== undefined && val !== '0'
|
||||
},
|
||||
set: (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
liveProperties.value.spawn_protection = previousSpawnProtection || '16'
|
||||
} else {
|
||||
previousSpawnProtection = liveProperties.value.spawn_protection || '16'
|
||||
liveProperties.value.spawn_protection = '0'
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
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 { mutateAsync: 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 advancedGroupedProperties = computed(() =>
|
||||
ADVANCED_GROUPS.map((group) => ({
|
||||
label: group.label,
|
||||
properties: group.keys.filter((key) => key in liveProperties.value),
|
||||
})).filter((g) => g.properties.length > 0),
|
||||
)
|
||||
|
||||
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]]))
|
||||
})
|
||||
|
||||
const isSearchActive = computed(() => !!searchInput.value?.trim())
|
||||
const hasNoResults = computed(
|
||||
() => isSearchActive.value && Object.keys(filteredProperties.value).length === 0,
|
||||
)
|
||||
|
||||
function isPropertyVisible(key: string): boolean {
|
||||
if (!isSearchActive.value) return true
|
||||
return key in filteredProperties.value
|
||||
}
|
||||
|
||||
function hasVisibleProperties(group: { properties: string[] }): boolean {
|
||||
return group.properties.some((key) => isPropertyVisible(key))
|
||||
}
|
||||
|
||||
const hasVisibleAdvancedProperties = computed(() =>
|
||||
advancedGroupedProperties.value.some((group) => hasVisibleProperties(group)),
|
||||
)
|
||||
|
||||
function formatPropertyName(name: string): string {
|
||||
return name
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user