From 003249e981ca1bebb99dea4ffc710346815797aa Mon Sep 17 00:00:00 2001 From: tdgao Date: Fri, 20 Mar 2026 23:11:53 -0600 Subject: [PATCH 01/23] start new server settings tabs --- .../src/pages/hosting/manage/[id]/options.vue | 12 +- .../hosting/manage/[id]/options/advanced.vue | 359 ++++++++++++++++++ .../hosting/manage/[id]/options/index.vue | 146 +++++-- .../hosting/manage/[id]/options/info.vue | 154 -------- .../hosting/manage/[id]/options/network.vue | 234 +++++------- .../manage/[id]/options/preferences.vue | 113 ------ .../hosting/manage/[id]/options/startup.vue | 255 ------------- 7 files changed, 569 insertions(+), 704 deletions(-) create mode 100644 apps/frontend/src/pages/hosting/manage/[id]/options/advanced.vue delete mode 100644 apps/frontend/src/pages/hosting/manage/[id]/options/info.vue delete mode 100644 apps/frontend/src/pages/hosting/manage/[id]/options/preferences.vue delete mode 100644 apps/frontend/src/pages/hosting/manage/[id]/options/startup.vue diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options.vue b/apps/frontend/src/pages/hosting/manage/[id]/options.vue index 9d4697de05..c3ad86a742 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/options.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/options.vue @@ -6,12 +6,10 @@ diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/advanced.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/advanced.vue new file mode 100644 index 0000000000..20e4d7013e --- /dev/null +++ b/apps/frontend/src/pages/hosting/manage/[id]/options/advanced.vue @@ -0,0 +1,359 @@ + + + diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/index.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/index.vue index 09d45c96bc..094dbb4bf5 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/index.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/options/index.vue @@ -1,8 +1,9 @@ @@ -280,20 +273,16 @@ import { StyledInput, } from '@modrinth/ui' import { useQuery, useQueryClient } from '@tanstack/vue-query' -import { computed, nextTick, ref } from 'vue' - -import SaveBanner from '~/components/ui/servers/SaveBanner.vue' +import { nextTick, ref } from 'vue' const { addNotification } = injectNotificationManager() const { server, serverId } = injectModrinthServerContext() const client = injectModrinthClient() const queryClient = useQueryClient() -const isUpdating = ref(false) const data = server const serverIP = ref(data?.value?.net?.ip ?? '') -const serverSubdomain = ref(data?.value?.net?.domain ?? '') const serverPrimaryPort = ref(data?.value?.net?.port ?? 0) const userDomain = ref('') const exampleDomain = 'play.example.com' @@ -317,10 +306,6 @@ const newAllocationName = ref('') const newAllocationPort = ref(0) const allocationToDelete = ref(null) -const hasUnsavedChanges = computed(() => serverSubdomain.value !== data?.value?.net?.domain) - -const isValidSubdomain = computed(() => /^[a-zA-Z0-9-]{5,}$/.test(serverSubdomain.value)) - const addNewAllocation = async () => { if (!newAllocationName.value) return @@ -405,55 +390,6 @@ const editAllocation = async () => { } } -const saveNetwork = async () => { - if (!isValidSubdomain.value) return - - try { - isUpdating.value = true - const result = await client.archon.servers_v0.checkSubdomainAvailability(serverSubdomain.value) - const available = result.available - if (!available) { - addNotification({ - type: 'error', - title: 'Subdomain not available', - text: 'The subdomain you entered is already in use.', - }) - return - } - if (serverSubdomain.value !== data?.value?.net?.domain) { - await client.archon.servers_v0.changeSubdomain(serverId, serverSubdomain.value) - } - if (serverPrimaryPort.value !== data?.value?.net?.port) { - await client.archon.servers_v0.updateAllocation( - serverId, - serverPrimaryPort.value, - newAllocationName.value, - ) - } - await new Promise((resolve) => setTimeout(resolve, 500)) - await queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] }) - await queryClient.invalidateQueries({ queryKey: ['servers', 'allocations', serverId] }) - addNotification({ - type: 'success', - title: 'Server settings updated', - text: 'Your server settings were successfully changed.', - }) - } catch (error) { - console.error(error) - addNotification({ - type: 'error', - title: 'Failed to update server settings', - text: 'An error occurred while attempting to update your server settings.', - }) - } finally { - isUpdating.value = false - } -} - -const resetNetwork = () => { - serverSubdomain.value = data?.value?.net?.domain ?? '' -} - const dnsRecords = computed(() => { const domain = userDomain.value === '' ? exampleDomain : userDomain.value return [ @@ -485,7 +421,7 @@ const exportDnsRecords = () => { const text = Object.entries(records) .map(([type, records]) => { - return `; ${type} Records\n${records.map((record) => `${record.name}. 1 IN ${record.type} ${record.content}${record.type === 'SRV' ? '.' : ''}`).join('\n')}\n` + return `; ${type} Records\n${records.map((record) => `${record.name}.\t1\tIN\t${record.type} ${record.content}${record.type === 'SRV' ? '.' : ''}`).join('\n')}\n` }) .join('\n') const blob = new Blob([text], { type: 'text/plain' }) diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/preferences.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/preferences.vue deleted file mode 100644 index d3c84bf5b0..0000000000 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/preferences.vue +++ /dev/null @@ -1,113 +0,0 @@ - - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/startup.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/startup.vue deleted file mode 100644 index c48452832c..0000000000 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/startup.vue +++ /dev/null @@ -1,255 +0,0 @@ - - - From ef452a2255c55793d723d78899a59ea1911cf429 Mon Sep 17 00:00:00 2001 From: tdgao Date: Fri, 20 Mar 2026 23:41:24 -0600 Subject: [PATCH 02/23] update properties tab to match design --- .../manage/[id]/options/properties.vue | 365 ++++++++++++++---- 1 file changed, 296 insertions(+), 69 deletions(-) diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue index c074e1373e..dce35259f4 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue @@ -6,6 +6,7 @@ type="warning" body="Some expected properties are missing from your server.properties - this usually means the server hasn't completed its first startup yet." /> +

Server properties

@@ -22,72 +23,178 @@ has more detailed information.
-
-
- - -
-
- {{ formatPropertyName(key) }} - -
- -
-
- -
-
- -
-
- + +
+ + +
+ +
+ +
+
+
+ Gamemode + +
+ +
+ Difficulty + +
+ +
+ Max players + +
+ +
+ MOTD + +
+ +
+ Enable whitelist + +
+ +
+
+ Enable spawn protection + +
+
+ Protection radius + +
+
+ + + + +
+ + +
+

Custom properties

+
+
+ + {{ formatPropertyName(key) }} + +
+ +
+
+
+
+
+
@@ -110,8 +217,9 @@ import type { Archon } from '@modrinth/api-client' import { SearchIcon, SpinnerIcon } from '@modrinth/assets' import { + Accordion, Admonition, - Combobox, + Chips, injectModrinthClient, injectModrinthServerContext, injectNotificationManager, @@ -131,16 +239,15 @@ const queryClient = useQueryClient() const searchInput = ref('') -type DropdownPropertyDef = { type: 'dropdown'; options: string[] } -type PropertyDef = { type: 'toggle' } | { type: 'number' } | { type: 'text' } | DropdownPropertyDef +type PropertyDef = { type: 'toggle' } | { type: 'number' } | { type: 'text' } const KNOWN_PROPERTIES: Record = { allow_cheats: { type: 'toggle' }, allow_flight: { type: 'toggle' }, - difficulty: { type: 'dropdown', options: ['peaceful', 'easy', 'normal', 'hard'] }, + difficulty: { type: 'text' }, enforce_whitelist: { type: 'toggle' }, force_gamemode: { type: 'toggle' }, - gamemode: { type: 'dropdown', options: ['survival', 'creative', 'adventure', 'spectator'] }, + gamemode: { type: 'text' }, generate_structures: { type: 'toggle' }, generator_settings: { type: 'text' }, hardcore: { type: 'toggle' }, @@ -166,6 +273,44 @@ 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'], + }, + { + label: 'Other', + keys: [ + 'allow_cheats', + 'allow_flight', + 'force_gamemode', + 'generate_structures', + 'generator_settings', + 'level_seed', + 'level_type', + ], + }, +] + +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({ @@ -197,6 +342,10 @@ function syncFormFromData() { const flat = flattenProperties(propsData.value) liveProperties.value = { ...flat } originalProperties.value = { ...flat } + const sp = flat.spawn_protection + if (sp && sp !== '0') { + previousSpawnProtection = sp + } } watch( @@ -217,6 +366,56 @@ const missingKnownProperties = computed(() => Object.keys(KNOWN_PROPERTIES).filter((key) => !(key in liveProperties.value)), ) +let previousSpawnProtection = '16' + +const combinedGamemode = computed({ + 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' + } + }, +}) + const hasUnsavedChanges = computed(() => Object.keys(liveProperties.value).some( (key) => liveProperties.value[key] !== originalProperties.value[key], @@ -271,6 +470,18 @@ 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 customProperties = computed(() => { + const knownKeys = new Set(Object.keys(KNOWN_PROPERTIES)) + return Object.keys(liveProperties.value).filter((key) => !knownKeys.has(key)) +}) + const fuse = computed(() => { const entries = Object.entries(liveProperties.value).map(([key, value]) => ({ key, @@ -285,6 +496,22 @@ const filteredProperties = computed(() => { return Object.fromEntries(results.map(({ item }) => [item.key, liveProperties.value[item.key]])) }) +const isSearchActive = computed(() => !!searchInput.value?.trim()) + +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 visibleCustomProperties = computed(() => { + if (!isSearchActive.value) return customProperties.value + return customProperties.value.filter((key) => isPropertyVisible(key)) +}) + function formatPropertyName(name: string): string { return name .split('_') From 05995c3c9b40f919677fee6a2fdfc2f4eb94e02b Mon Sep 17 00:00:00 2001 From: tdgao Date: Mon, 23 Mar 2026 20:40:53 -0600 Subject: [PATCH 03/23] better stying in general tab --- .../hosting/manage/[id]/options/index.vue | 169 +++++++++--------- 1 file changed, 81 insertions(+), 88 deletions(-) diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/index.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/index.vue index 094dbb4bf5..45120b3f42 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/index.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/options/index.vue @@ -2,62 +2,62 @@
- -
- -
- - - Server name must be at least 1 character long. - - - Server name can contain any character. - -
-
+
+
+ +
+ +
+ + + Server name must be at least 1 character long. + + + Server name can contain any character. + +
+ This name is only visible on Modrinth. +
- -
- -
- - .modrinth.gg -
-
- - Subdomain must be at least 5 characters long. - - - Subdomain can only contain alphanumeric characters and dashes. - + +
+ +
+
+ + Subdomain must be at least 5 characters long. + + + Subdomain can only contain alphanumeric characters and dashes. + +
+ + .modrinth.gg +
+ Your friends can connect to your server using this URL. +
-
- -
- -
+ +
+
- -
- -
-

Preferences

-
-