|
| 1 | +<script setup lang="ts"> |
| 2 | +import { |
| 3 | + type Archon, |
| 4 | + clearNodeAuthState, |
| 5 | + type Labrinth, |
| 6 | + setNodeAuthState, |
| 7 | +} from '@modrinth/api-client' |
| 8 | +import { ChevronRightIcon } from '@modrinth/assets' |
| 9 | +import { |
| 10 | + type BusyReason, |
| 11 | + commonMessages, |
| 12 | + defineMessage, |
| 13 | + defineMessages, |
| 14 | + injectModrinthClient, |
| 15 | + injectNotificationManager, |
| 16 | + provideModrinthServerContext, |
| 17 | + provideServerSettings, |
| 18 | + ServerSettingsAdvancedPage, |
| 19 | + ServerSettingsGeneralPage, |
| 20 | + ServerSettingsInstallationPage, |
| 21 | + ServerSettingsNetworkPage, |
| 22 | + ServerSettingsPropertiesPage, |
| 23 | + serverSettingsTabDefinitions, |
| 24 | + TabbedModal, |
| 25 | + type TabbedModalTab, |
| 26 | + useVIntl, |
| 27 | +} from '@modrinth/ui' |
| 28 | +import { useQueryClient } from '@tanstack/vue-query' |
| 29 | +import { computed, nextTick, onUnmounted, reactive, ref } from 'vue' |
| 30 | +
|
| 31 | +import { get_user } from '@/helpers/cache' |
| 32 | +import { get as getCreds } from '@/helpers/mr_auth' |
| 33 | +
|
| 34 | +type ShowOptions = { |
| 35 | + serverId: string |
| 36 | + tabIndex?: number |
| 37 | +} |
| 38 | +
|
| 39 | +const { formatMessage } = useVIntl() |
| 40 | +const queryClient = useQueryClient() |
| 41 | +const client = injectModrinthClient() |
| 42 | +const { addNotification } = injectNotificationManager() |
| 43 | +
|
| 44 | +const messages = defineMessages({ |
| 45 | + failedToLoadServer: { |
| 46 | + id: 'app.server-settings.failed-to-load-server', |
| 47 | + defaultMessage: 'Failed to load server settings', |
| 48 | + }, |
| 49 | +}) |
| 50 | +
|
| 51 | +const modal = ref<InstanceType<typeof TabbedModal> | null>(null) |
| 52 | +
|
| 53 | +const currentServerId = ref('') |
| 54 | +const worldId = ref<string | null>(null) |
| 55 | +const server = ref<Archon.Servers.v0.Server>({} as Archon.Servers.v0.Server) |
| 56 | +
|
| 57 | +const currentUserId = ref<string | null>(null) |
| 58 | +const currentUserRole = ref<string | null>(null) |
| 59 | +
|
| 60 | +const isApp = ref(true) |
| 61 | +
|
| 62 | +function browseModpacks() { |
| 63 | + // Stub for app browse-modpacks flow. Intentionally no-op for now. |
| 64 | +} |
| 65 | +
|
| 66 | +const isConnected = ref(true) |
| 67 | +const powerState = ref<Archon.Websocket.v0.PowerState>('stopped') |
| 68 | +const isServerRunning = computed(() => powerState.value === 'running') |
| 69 | +const backupsState = reactive(new Map()) |
| 70 | +const isSyncingContent = ref(false) |
| 71 | +
|
| 72 | +const busyReasons = computed<BusyReason[]>(() => { |
| 73 | + const reasons: BusyReason[] = [] |
| 74 | + if (server.value?.status === 'installing') { |
| 75 | + reasons.push({ |
| 76 | + reason: defineMessage({ |
| 77 | + id: 'servers.busy.installing', |
| 78 | + defaultMessage: 'Server is installing', |
| 79 | + }), |
| 80 | + }) |
| 81 | + } |
| 82 | + if (isSyncingContent.value) { |
| 83 | + reasons.push({ |
| 84 | + reason: defineMessage({ |
| 85 | + id: 'servers.busy.syncing-content', |
| 86 | + defaultMessage: 'Content sync in progress', |
| 87 | + }), |
| 88 | + }) |
| 89 | + } |
| 90 | + return reasons |
| 91 | +}) |
| 92 | +
|
| 93 | +const fsAuth = ref<{ url: string; token: string } | null>(null) |
| 94 | +const fsOps = ref<Archon.Websocket.v0.FilesystemOperation[]>([]) |
| 95 | +const fsQueuedOps = ref<Archon.Websocket.v0.QueuedFilesystemOp[]>([]) |
| 96 | +
|
| 97 | +async function refreshFsAuth() { |
| 98 | + if (!currentServerId.value) { |
| 99 | + fsAuth.value = null |
| 100 | + return |
| 101 | + } |
| 102 | + fsAuth.value = await queryClient.fetchQuery({ |
| 103 | + queryKey: ['servers', 'filesystem-auth', currentServerId.value], |
| 104 | + queryFn: () => client.archon.servers_v0.getFilesystemAuth(currentServerId.value), |
| 105 | + }) |
| 106 | +} |
| 107 | +
|
| 108 | +function markBackupCancelled(_backupId: string) {} |
| 109 | +
|
| 110 | +const serverSettingsTabComponentMap = { |
| 111 | + general: ServerSettingsGeneralPage, |
| 112 | + installation: ServerSettingsInstallationPage, |
| 113 | + network: ServerSettingsNetworkPage, |
| 114 | + properties: ServerSettingsPropertiesPage, |
| 115 | + advanced: ServerSettingsAdvancedPage, |
| 116 | +} as const |
| 117 | +
|
| 118 | +provideServerSettings({ |
| 119 | + isApp, |
| 120 | + currentUserId, |
| 121 | + currentUserRole, |
| 122 | + browseModpacks, |
| 123 | +}) |
| 124 | +
|
| 125 | +provideModrinthServerContext({ |
| 126 | + get serverId() { |
| 127 | + return currentServerId.value |
| 128 | + }, |
| 129 | + worldId, |
| 130 | + server, |
| 131 | + isConnected, |
| 132 | + powerState, |
| 133 | + isServerRunning, |
| 134 | + backupsState, |
| 135 | + markBackupCancelled, |
| 136 | + isSyncingContent, |
| 137 | + busyReasons, |
| 138 | + fsAuth, |
| 139 | + fsOps, |
| 140 | + fsQueuedOps, |
| 141 | + refreshFsAuth, |
| 142 | +}) |
| 143 | +
|
| 144 | +const ownerId = computed(() => server.value?.owner_id ?? 'Ghost') |
| 145 | +const isOwner = computed(() => currentUserId.value != null && currentUserId.value === ownerId.value) |
| 146 | +const isAdmin = computed(() => currentUserRole.value === 'admin') |
| 147 | +
|
| 148 | +const tabs = computed<TabbedModalTab[]>(() => |
| 149 | + serverSettingsTabDefinitions.map((tab) => { |
| 150 | + const ctx = { |
| 151 | + serverId: currentServerId.value, |
| 152 | + ownerId: ownerId.value, |
| 153 | + serverStatus: server.value?.status, |
| 154 | + isOwner: isOwner.value, |
| 155 | + isAdmin: isAdmin.value, |
| 156 | + } |
| 157 | + const name = defineMessage({ |
| 158 | + id: `server.settings.tabs.${tab.id}`, |
| 159 | + defaultMessage: tab.label, |
| 160 | + }) |
| 161 | + const shown = tab.shown ? tab.shown(ctx) : true |
| 162 | +
|
| 163 | + if (tab.external) { |
| 164 | + return { |
| 165 | + name, |
| 166 | + icon: tab.icon, |
| 167 | + href: `https://modrinth.com${tab.href(ctx)}`, |
| 168 | + shown, |
| 169 | + } |
| 170 | + } |
| 171 | +
|
| 172 | + return { |
| 173 | + name, |
| 174 | + icon: tab.icon, |
| 175 | + content: serverSettingsTabComponentMap[tab.id as keyof typeof serverSettingsTabComponentMap], |
| 176 | + shown, |
| 177 | + } |
| 178 | + }), |
| 179 | +) |
| 180 | +
|
| 181 | +async function resolveViewer() { |
| 182 | + currentUserId.value = null |
| 183 | + currentUserRole.value = null |
| 184 | +
|
| 185 | + const credentials = await getCreds().catch(() => null) |
| 186 | + if (!credentials?.user_id) { |
| 187 | + return |
| 188 | + } |
| 189 | +
|
| 190 | + currentUserId.value = credentials.user_id |
| 191 | +
|
| 192 | + const user = await get_user(credentials.user_id, 'bypass').catch(() => null) |
| 193 | + const typedUser = user as Labrinth.Users.v2.User | null |
| 194 | + currentUserRole.value = typedUser?.role ?? null |
| 195 | +} |
| 196 | +
|
| 197 | +async function show({ serverId, tabIndex }: ShowOptions) { |
| 198 | + try { |
| 199 | + const [serverData, serverFull] = await Promise.all([ |
| 200 | + queryClient.fetchQuery({ |
| 201 | + queryKey: ['servers', 'detail', serverId], |
| 202 | + queryFn: () => client.archon.servers_v0.get(serverId), |
| 203 | + }), |
| 204 | + queryClient.fetchQuery({ |
| 205 | + queryKey: ['servers', 'v1', 'detail', serverId], |
| 206 | + queryFn: () => client.archon.servers_v1.get(serverId), |
| 207 | + }), |
| 208 | + resolveViewer(), |
| 209 | + ]) |
| 210 | +
|
| 211 | + currentServerId.value = serverId |
| 212 | + server.value = serverData |
| 213 | + const activeWorld = serverFull.worlds.find((world) => world.is_active) |
| 214 | + worldId.value = activeWorld?.id ?? serverFull.worlds[0]?.id ?? null |
| 215 | +
|
| 216 | + setNodeAuthState(() => fsAuth.value, refreshFsAuth) |
| 217 | + await refreshFsAuth().catch(() => {}) |
| 218 | +
|
| 219 | + modal.value?.show() |
| 220 | + const visibleTabsCount = tabs.value.filter((tab) => tab.shown !== false).length |
| 221 | + const requestedTab = tabIndex ?? 0 |
| 222 | + const clampedTab = Math.min(Math.max(requestedTab, 0), Math.max(visibleTabsCount - 1, 0)) |
| 223 | + nextTick(() => modal.value?.setTab(clampedTab)) |
| 224 | + } catch (error) { |
| 225 | + console.error(error) |
| 226 | + addNotification({ |
| 227 | + type: 'error', |
| 228 | + title: formatMessage(messages.failedToLoadServer), |
| 229 | + }) |
| 230 | + } |
| 231 | +} |
| 232 | +
|
| 233 | +function hide() { |
| 234 | + modal.value?.hide() |
| 235 | +} |
| 236 | +
|
| 237 | +function onHide() { |
| 238 | + clearNodeAuthState() |
| 239 | +} |
| 240 | +
|
| 241 | +onUnmounted(() => { |
| 242 | + clearNodeAuthState() |
| 243 | +}) |
| 244 | +
|
| 245 | +defineExpose({ show, hide }) |
| 246 | +</script> |
| 247 | + |
| 248 | +<template> |
| 249 | + <TabbedModal |
| 250 | + ref="modal" |
| 251 | + :tabs="tabs" |
| 252 | + :on-hide="onHide" |
| 253 | + :max-width="'min(980px, calc(95vw - 10rem))'" |
| 254 | + :width="'min(980px, calc(95vw - 10rem))'" |
| 255 | + > |
| 256 | + <template #title> |
| 257 | + <span class="flex items-center gap-2 text-lg font-semibold text-primary"> |
| 258 | + {{ server.name || 'Server' }} <ChevronRightIcon /> |
| 259 | + <span class="font-extrabold text-contrast">{{ |
| 260 | + formatMessage(commonMessages.settingsLabel) |
| 261 | + }}</span> |
| 262 | + </span> |
| 263 | + </template> |
| 264 | + </TabbedModal> |
| 265 | +</template> |
0 commit comments