mirror of
https://github.com/modrinth/code.git
synced 2026-09-02 04:56:52 +00:00
chore: i18n pass on server panel before worlds proj
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { SearchIcon } from '@modrinth/assets'
|
||||
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed, toValue } from 'vue'
|
||||
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
@@ -17,9 +18,51 @@ import type { SortType } from '#ui/utils/search'
|
||||
import BrowseInstallHeader from './header.vue'
|
||||
import { injectBrowseManager } from './providers/browse-manager'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
searchPlaceholder: {
|
||||
id: 'browse-tab.search-placeholder',
|
||||
defaultMessage:
|
||||
'{projectType, select, mod {Search mods…} modpack {Search modpacks…} resourcepack {Search resource packs…} shader {Search shaders…} datapack {Search datapacks…} plugin {Search plugins…} server {Search servers…} project {Search projects…} other {Search…}}',
|
||||
},
|
||||
sortByPlaceholder: {
|
||||
id: 'browse-tab.sort-by-placeholder',
|
||||
defaultMessage: 'Sort by',
|
||||
},
|
||||
sortByPrefix: {
|
||||
id: 'browse-tab.sort-by-prefix',
|
||||
defaultMessage: 'Sort by:',
|
||||
},
|
||||
viewPlaceholder: {
|
||||
id: 'browse-tab.view-placeholder',
|
||||
defaultMessage: 'View',
|
||||
},
|
||||
viewPrefix: {
|
||||
id: 'browse-tab.view-prefix',
|
||||
defaultMessage: 'View:',
|
||||
},
|
||||
filterResultsButton: {
|
||||
id: 'browse-tab.filter-results',
|
||||
defaultMessage: 'Filter results…',
|
||||
},
|
||||
offlineMessage: {
|
||||
id: 'browse-tab.offline',
|
||||
defaultMessage: 'You are currently offline. Connect to the internet to browse Modrinth!',
|
||||
},
|
||||
noResultsMessage: {
|
||||
id: 'browse-tab.no-results',
|
||||
defaultMessage: 'No results found for your query!',
|
||||
},
|
||||
})
|
||||
|
||||
const ctx = injectBrowseManager()
|
||||
const lockedMessages = computed(() => toValue(ctx.lockedFilterMessages))
|
||||
|
||||
const searchPlaceholderText = computed(() =>
|
||||
formatMessage(messages.searchPlaceholder, { projectType: ctx.projectType.value }),
|
||||
)
|
||||
|
||||
const sortOptions = computed<ComboboxOption<SortType>[]>(() =>
|
||||
ctx.effectiveSortTypes.value.map((st) => ({
|
||||
value: st,
|
||||
@@ -47,7 +90,7 @@ const maxResultsOptions = computed<ComboboxOption<number>[]>(() =>
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:placeholder="`Search ${ctx.projectType.value}s...`"
|
||||
:placeholder="searchPlaceholderText"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
:input-class="ctx.variant === 'web' ? '!h-12' : 'h-12'"
|
||||
@@ -59,11 +102,11 @@ const maxResultsOptions = computed<ComboboxOption<number>[]>(() =>
|
||||
:model-value="ctx.effectiveCurrentSortType.value"
|
||||
:options="sortOptions"
|
||||
:class="ctx.variant === 'web' ? '!w-auto flex-grow md:flex-grow-0' : 'max-w-[16rem]'"
|
||||
placeholder="Sort by"
|
||||
:placeholder="formatMessage(messages.sortByPlaceholder)"
|
||||
@update:model-value="(val: SortType) => (ctx.effectiveCurrentSortType.value = val)"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="font-semibold text-primary">Sort by:</span>
|
||||
<span class="font-semibold text-primary">{{ formatMessage(messages.sortByPrefix) }}</span>
|
||||
</template>
|
||||
</Combobox>
|
||||
|
||||
@@ -71,17 +114,19 @@ const maxResultsOptions = computed<ComboboxOption<number>[]>(() =>
|
||||
:model-value="ctx.maxResults.value"
|
||||
:options="maxResultsOptions"
|
||||
:class="ctx.variant === 'web' ? '!w-auto flex-grow md:flex-grow-0' : 'max-w-[9rem]'"
|
||||
placeholder="View"
|
||||
:placeholder="formatMessage(messages.viewPlaceholder)"
|
||||
@update:model-value="(val: number) => (ctx.maxResults.value = val)"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="font-semibold text-primary">View:</span>
|
||||
<span class="font-semibold text-primary">{{ formatMessage(messages.viewPrefix) }}</span>
|
||||
</template>
|
||||
</Combobox>
|
||||
|
||||
<div v-if="ctx.filtersMenuOpen && !ctx.filtersMenuOpen.value" class="lg:hidden">
|
||||
<ButtonStyled>
|
||||
<button @click="ctx.filtersMenuOpen.value = true">Filter results...</button>
|
||||
<button @click="ctx.filtersMenuOpen.value = true">
|
||||
{{ formatMessage(messages.filterResultsButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
@@ -120,7 +165,7 @@ const maxResultsOptions = computed<ComboboxOption<number>[]>(() =>
|
||||
<component :is="ctx.loadingComponent ?? LoadingIndicator" />
|
||||
</section>
|
||||
<section v-else-if="ctx.offline?.value && ctx.totalHits.value === 0" class="offline">
|
||||
You are currently offline. Connect to the internet to browse Modrinth!
|
||||
{{ formatMessage(messages.offlineMessage) }}
|
||||
</section>
|
||||
<section
|
||||
v-else-if="
|
||||
@@ -130,7 +175,7 @@ const maxResultsOptions = computed<ComboboxOption<number>[]>(() =>
|
||||
"
|
||||
class="offline"
|
||||
>
|
||||
<p>No results found for your query!</p>
|
||||
<p>{{ formatMessage(messages.noResultsMessage) }}</p>
|
||||
</section>
|
||||
|
||||
<ProjectCardList v-else :layout="ctx.effectiveLayout.value">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { InfoIcon, XIcon } from '@modrinth/assets'
|
||||
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed, toValue } from 'vue'
|
||||
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
@@ -8,6 +9,19 @@ import SearchSidebarFilter from '#ui/components/search/SearchSidebarFilter.vue'
|
||||
|
||||
import { injectBrowseManager } from './providers/browse-manager'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
filtersHeading: {
|
||||
id: 'browse-tab.filters-heading',
|
||||
defaultMessage: 'Filters',
|
||||
},
|
||||
hideInstalledDefault: {
|
||||
id: 'browse-tab.hide-installed-default',
|
||||
defaultMessage: 'Hide installed content',
|
||||
},
|
||||
})
|
||||
|
||||
const ctx = injectBrowseManager()
|
||||
|
||||
const isApp = computed(() => ctx.variant === 'app')
|
||||
@@ -80,7 +94,7 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
v-if="ctx.filtersMenuOpen?.value"
|
||||
class="sticky top-0 z-10 mx-1 flex items-center justify-between gap-3 border-0 border-b-[1px] border-solid border-divider bg-bg-raised px-6 py-4"
|
||||
>
|
||||
<h3 class="m-0 text-lg text-contrast">Filters</h3>
|
||||
<h3 class="m-0 text-lg text-contrast">{{ formatMessage(messages.filtersHeading) }}</h3>
|
||||
<ButtonStyled circular>
|
||||
<button @click="closeFiltersMenu">
|
||||
<XIcon />
|
||||
@@ -98,7 +112,7 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
>
|
||||
<Checkbox
|
||||
v-model="ctx.hideInstalled!.value"
|
||||
:label="ctx.hideInstalledLabel?.value ?? 'Hide installed content'"
|
||||
:label="ctx.hideInstalledLabel?.value ?? formatMessage(messages.hideInstalledDefault)"
|
||||
class="filter-checkbox"
|
||||
@update:model-value="ctx.onFilterChange()"
|
||||
@click.prevent.stop
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ButtonStyled v-if="showClear && hasLogs" type="transparent">
|
||||
<button @click="emit('clear')">
|
||||
<XIcon />
|
||||
Clear
|
||||
{{ formatMessage(commonMessages.clearButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="showDelete" type="transparent" hover-color-fill="background" color="red">
|
||||
@@ -13,7 +13,7 @@
|
||||
@click="emit('delete')"
|
||||
>
|
||||
<TrashIcon />
|
||||
Delete
|
||||
{{ formatMessage(commonMessages.deleteLabel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="hasLogs" type="transparent">
|
||||
@@ -24,14 +24,14 @@
|
||||
>
|
||||
<SpinnerIcon v-if="sharing" class="animate-spin" />
|
||||
<ShareIcon v-else />
|
||||
Share
|
||||
{{ formatMessage(messages.share) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent">
|
||||
<button @click="emit('toggle-fullscreen')">
|
||||
<ContractIcon v-if="fullscreen" />
|
||||
<ExpandIcon v-else />
|
||||
{{ fullscreen ? 'Collapse' : 'Expand' }}
|
||||
{{ fullscreen ? formatMessage(messages.collapse) : formatMessage(messages.expand) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -48,6 +48,25 @@ import {
|
||||
} from '@modrinth/assets'
|
||||
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
share: {
|
||||
id: 'servers.console.action.share',
|
||||
defaultMessage: 'Share',
|
||||
},
|
||||
expand: {
|
||||
id: 'servers.console.action.expand',
|
||||
defaultMessage: 'Expand',
|
||||
},
|
||||
collapse: {
|
||||
id: 'servers.console.action.collapse',
|
||||
defaultMessage: 'Collapse',
|
||||
},
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
showClear?: boolean
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<FilterPills v-model="selectedFilters" :options="visibleOptions">
|
||||
<template #all> All </template>
|
||||
<template #all>{{ formatMessage(commonMessages.consoleFilterAllLevels) }}</template>
|
||||
</FilterPills>
|
||||
</template>
|
||||
|
||||
@@ -8,22 +8,42 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import FilterPills from '#ui/components/base/FilterPills.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import type { ConditionalLevel } from '../composables/console-filtering'
|
||||
import type { LogLevel } from '../types'
|
||||
|
||||
type FilterValue = LogLevel | 'all'
|
||||
|
||||
const ALWAYS_VISIBLE: Array<{ id: LogLevel; label: string }> = [
|
||||
{ id: 'error', label: 'Error' },
|
||||
{ id: 'warn', label: 'Warn' },
|
||||
{ id: 'info', label: 'Info' },
|
||||
]
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const CONDITIONAL_OPTIONS: Array<{ id: ConditionalLevel; label: string }> = [
|
||||
{ id: 'debug', label: 'Debug' },
|
||||
{ id: 'trace', label: 'Trace' },
|
||||
]
|
||||
const logLevelLabels = defineMessages({
|
||||
error: {
|
||||
id: 'servers.console.filter.log-level.error',
|
||||
defaultMessage: 'Error',
|
||||
},
|
||||
warn: {
|
||||
id: 'servers.console.filter.log-level.warn',
|
||||
defaultMessage: 'Warn',
|
||||
},
|
||||
info: {
|
||||
id: 'servers.console.filter.log-level.info',
|
||||
defaultMessage: 'Info',
|
||||
},
|
||||
debug: {
|
||||
id: 'servers.console.filter.log-level.debug',
|
||||
defaultMessage: 'Debug',
|
||||
},
|
||||
trace: {
|
||||
id: 'servers.console.filter.log-level.trace',
|
||||
defaultMessage: 'Trace',
|
||||
},
|
||||
})
|
||||
|
||||
const ALWAYS_VISIBLE: LogLevel[] = ['error', 'warn', 'info']
|
||||
|
||||
const CONDITIONAL_LEVELS: ConditionalLevel[] = ['debug', 'trace']
|
||||
|
||||
const props = defineProps<{
|
||||
presentLevels: Set<ConditionalLevel>
|
||||
@@ -36,8 +56,11 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const visibleOptions = computed(() => [
|
||||
...ALWAYS_VISIBLE,
|
||||
...CONDITIONAL_OPTIONS.filter((opt) => props.presentLevels.has(opt.id)),
|
||||
...ALWAYS_VISIBLE.map((id) => ({ id, label: formatMessage(logLevelLabels[id]) })),
|
||||
...CONDITIONAL_LEVELS.filter((id) => props.presentLevels.has(id)).map((id) => ({
|
||||
id,
|
||||
label: formatMessage(logLevelLabels[id]),
|
||||
})),
|
||||
])
|
||||
|
||||
const selectedFilters = computed({
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<StyledInput
|
||||
v-model="searchQuery"
|
||||
:icon="SearchIcon"
|
||||
placeholder="Search logs"
|
||||
:placeholder="formatMessage(messages.searchLogsPlaceholder)"
|
||||
wrapper-class="flex-1"
|
||||
input-class="!h-10"
|
||||
clearable
|
||||
@@ -65,11 +65,21 @@
|
||||
@ready="handleTerminalReady"
|
||||
/>
|
||||
</div>
|
||||
<ShareModal ref="shareModal" header="Share Logs" link :social-buttons="false" />
|
||||
<NewModal ref="deleteModal" header="Delete log file" :fade="'danger'" max-width="500px">
|
||||
<ShareModal
|
||||
ref="shareModal"
|
||||
:header="formatMessage(messages.shareLogsHeader)"
|
||||
link
|
||||
:social-buttons="false"
|
||||
/>
|
||||
<NewModal
|
||||
ref="deleteModal"
|
||||
:header="formatMessage(messages.deleteLogFileHeader)"
|
||||
:fade="'danger'"
|
||||
max-width="500px"
|
||||
>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Admonition type="critical" header="This is irreversible">
|
||||
Deleting this log file cannot be undone. Are you sure you want to continue?
|
||||
<Admonition type="critical" :header="formatMessage(messages.deleteLogIrreversibleHeader)">
|
||||
{{ formatMessage(messages.deleteLogIrreversibleBody) }}
|
||||
</Admonition>
|
||||
</div>
|
||||
<template #actions>
|
||||
@@ -77,13 +87,13 @@
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border !border-surface-4" @click="deleteModal?.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button :disabled="isDeleting" @click="confirmDelete">
|
||||
<TrashIcon />
|
||||
Delete
|
||||
{{ formatMessage(commonMessages.deleteLabel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -105,9 +115,11 @@ import Combobox from '#ui/components/base/Combobox.vue'
|
||||
import StyledInput from '#ui/components/base/StyledInput.vue'
|
||||
import NewModal from '#ui/components/modal/NewModal.vue'
|
||||
import ShareModal from '#ui/components/modal/ShareModal.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { injectModrinthClient } from '#ui/providers'
|
||||
import { injectModalBehavior } from '#ui/providers/modal-behavior'
|
||||
import { injectNotificationManager } from '#ui/providers/web-notifications.ts'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import ConsoleActionButtons from './components/ConsoleActionButtons.vue'
|
||||
import ConsoleFilterPills from './components/ConsoleFilterPills.vue'
|
||||
@@ -128,10 +140,51 @@ const client = injectModrinthClient()
|
||||
const modalBehavior = injectModalBehavior()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
searchLogsPlaceholder: {
|
||||
id: 'servers.console.search.logs.placeholder',
|
||||
defaultMessage: 'Search logs',
|
||||
},
|
||||
crashProblemsDetected: {
|
||||
id: 'servers.console.crash.problems-detected',
|
||||
defaultMessage: '{count, plural, one {# problem detected} other {# problems detected}}',
|
||||
},
|
||||
shareLogsHeader: {
|
||||
id: 'servers.console.share.logs.header',
|
||||
defaultMessage: 'Share Logs',
|
||||
},
|
||||
deleteLogFileHeader: {
|
||||
id: 'servers.console.delete-log-file.header',
|
||||
defaultMessage: 'Delete log file',
|
||||
},
|
||||
deleteLogIrreversibleHeader: {
|
||||
id: 'servers.console.delete-log-file.irreversible.header',
|
||||
defaultMessage: 'This is irreversible',
|
||||
},
|
||||
deleteLogIrreversibleBody: {
|
||||
id: 'servers.console.delete-log-file.irreversible.body',
|
||||
defaultMessage: 'Deleting this log file cannot be undone. Are you sure you want to continue?',
|
||||
},
|
||||
failedDeleteLogTitle: {
|
||||
id: 'servers.console.delete-log-file.error.title',
|
||||
defaultMessage: 'Failed to delete log file',
|
||||
},
|
||||
failedShareLogsTitle: {
|
||||
id: 'servers.console.share.logs.error.title',
|
||||
defaultMessage: 'Failed to share logs',
|
||||
},
|
||||
unknownErrorDetail: {
|
||||
id: 'servers.console.error.unknown-detail',
|
||||
defaultMessage: 'Unknown error.',
|
||||
},
|
||||
})
|
||||
|
||||
const crashHeader = computed(() => {
|
||||
const problems = ctx.crashAnalysis?.value?.analysis.problems ?? []
|
||||
const count = problems.length
|
||||
return `${count} problem${count !== 1 ? 's' : ''} detected`
|
||||
return formatMessage(messages.crashProblemsDetected, { count })
|
||||
})
|
||||
|
||||
const crashItems = computed<CollapsibleAdmonitionItem[]>(() => {
|
||||
@@ -338,8 +391,8 @@ async function confirmDelete() {
|
||||
console.error('Failed to delete log file:', err)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to delete log file',
|
||||
text: typeof err === 'string' ? err : 'Unknown error.',
|
||||
title: formatMessage(messages.failedDeleteLogTitle),
|
||||
text: typeof err === 'string' ? err : formatMessage(messages.unknownErrorDetail),
|
||||
})
|
||||
} finally {
|
||||
isDeleting.value = false
|
||||
@@ -361,8 +414,8 @@ async function handleShare() {
|
||||
console.error('Failed to share logs:', err)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to share logs',
|
||||
text: typeof err === 'string' ? err : 'Unknown error.',
|
||||
title: formatMessage(messages.failedShareLogsTitle),
|
||||
text: typeof err === 'string' ? err : formatMessage(messages.unknownErrorDetail),
|
||||
})
|
||||
} finally {
|
||||
isSharing.value = false
|
||||
|
||||
@@ -33,7 +33,10 @@
|
||||
<span class="text-secondary">
|
||||
{{
|
||||
formatMessage(messages.extracted, {
|
||||
size: 'bytes_processed' in op ? formatBytes(op.bytes_processed ?? 0) : '0 B',
|
||||
size:
|
||||
'bytes_processed' in op
|
||||
? formatBinaryIecSize(op.bytes_processed ?? 0)
|
||||
: formatBinaryIecSize(0),
|
||||
})
|
||||
}}
|
||||
<template v-if="'current_file' in op && op.current_file">
|
||||
@@ -77,7 +80,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PackageOpenIcon, XIcon } from '@modrinth/assets'
|
||||
import { formatBytes } from '@modrinth/utils'
|
||||
|
||||
import Admonition from '#ui/components/base/Admonition.vue'
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
@@ -86,7 +88,10 @@ import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { injectModrinthServerContext } from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import { useFormatFileSizeI18n } from '../composables/format-file-size-i18n'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { formatBinaryIecSize } = useFormatFileSizeI18n()
|
||||
|
||||
const messages = defineMessages({
|
||||
extracting: {
|
||||
|
||||
@@ -122,10 +122,12 @@ import {
|
||||
startFileDrag,
|
||||
wasRecentDrag,
|
||||
} from '../composables/file-drag-state'
|
||||
import { useFormatFileSizeI18n } from '../composables/format-file-size-i18n'
|
||||
import { injectFileManager } from '../providers/file-manager'
|
||||
import type { FileItem } from '../types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { formatTableRowSize } = useFormatFileSizeI18n()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const ctx = injectFileManager()
|
||||
|
||||
@@ -164,8 +166,6 @@ const isDropTarget = computed(
|
||||
)
|
||||
const isDragSource = computed(() => fileDragActive.value && fileDragData.value?.path === props.path)
|
||||
|
||||
const units = Object.freeze(['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'])
|
||||
|
||||
const formatDateTime = useFormatDateTime({
|
||||
year: '2-digit',
|
||||
month: '2-digit',
|
||||
@@ -307,12 +307,7 @@ const formattedSize = computed(() => {
|
||||
}
|
||||
|
||||
if (props.size === undefined) return ''
|
||||
const bytes = props.size
|
||||
if (bytes === 0) return '0 B'
|
||||
|
||||
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
||||
const size = (bytes / Math.pow(1024, exponent)).toFixed(2)
|
||||
return `${size} ${units[exponent]}`
|
||||
return formatTableRowSize(props.size)
|
||||
})
|
||||
|
||||
function openContextMenu(event: MouseEvent) {
|
||||
|
||||
+5
-5
@@ -39,11 +39,7 @@
|
||||
v-model="url"
|
||||
:icon="LinkIcon"
|
||||
type="url"
|
||||
:placeholder="
|
||||
cf
|
||||
? 'https://www.curseforge.com/minecraft/modpacks/.../files/6412259'
|
||||
: 'https://www.example.com/.../modpack-name-1.0.2.zip'
|
||||
"
|
||||
:placeholder="cf ? CF_URL_PLACEHOLDER : ZIP_URL_PLACEHOLDER"
|
||||
:disabled="submitted"
|
||||
:error="touched && !!error"
|
||||
autocomplete="off"
|
||||
@@ -114,6 +110,10 @@ import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import InlineBackupCreator from '../../../content-tab/components/modals/InlineBackupCreator.vue'
|
||||
|
||||
// Language-invariant example URLs for the input placeholder.
|
||||
const CF_URL_PLACEHOLDER = 'https://www.curseforge.com/minecraft/modpacks/.../files/6412259'
|
||||
const ZIP_URL_PLACEHOLDER = 'https://www.example.com/.../modpack-name-1.0.2.zip'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -120,7 +120,10 @@ import { injectModrinthClient } from '#ui/providers/api-client'
|
||||
import { injectNotificationManager } from '#ui/providers/web-notifications'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import { useFormatFileSizeI18n } from '../../composables/format-file-size-i18n'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { formatUploadQueueSize } = useFormatFileSizeI18n()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
|
||||
@@ -161,10 +164,6 @@ const messages = defineMessages({
|
||||
id: 'files.upload-dropdown.incorrect-file-type',
|
||||
defaultMessage: 'Upload had incorrect file type',
|
||||
},
|
||||
failedToUpload: {
|
||||
id: 'files.upload-dropdown.failed-to-upload',
|
||||
defaultMessage: 'Failed to upload {fileName}',
|
||||
},
|
||||
})
|
||||
|
||||
interface UploadItem {
|
||||
@@ -240,13 +239,6 @@ watch(
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + ' KB'
|
||||
if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + ' MB'
|
||||
return (bytes / 1024 ** 3).toFixed(1) + ' GB'
|
||||
}
|
||||
|
||||
const cancelUpload = (item: UploadItem) => {
|
||||
if (item.uploader && item.status === 'uploading') {
|
||||
item.uploader.cancel()
|
||||
@@ -269,7 +261,7 @@ const uploadFile = async (file: File) => {
|
||||
file,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
size: formatFileSize(file.size),
|
||||
size: formatUploadQueueSize(file.size),
|
||||
}
|
||||
|
||||
uploadQueue.value.push(uploadItem)
|
||||
@@ -343,7 +335,7 @@ const uploadFile = async (file: File) => {
|
||||
if (error instanceof Error && error.message !== 'Upload cancelled') {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.uploadFailedLabel),
|
||||
text: formatMessage(messages.failedToUpload, { fileName: file.name }),
|
||||
text: formatMessage(commonMessages.uploadFailedFileDetail, { fileName: file.name }),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const TABLE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'] as const
|
||||
|
||||
/** Localized file sizes for the shared files tab (table, upload queue, extraction progress). */
|
||||
export function useFormatFileSizeI18n() {
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
/** Match FileTableRow byte formatting. */
|
||||
function formatTableRowSize(bytes: number): string {
|
||||
if (bytes === 0) {
|
||||
return formatMessage(commonMessages.fileSizeFormatted, { value: '0', unit: 'B' })
|
||||
}
|
||||
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), TABLE_UNITS.length - 1)
|
||||
const size = (bytes / Math.pow(1024, exponent)).toFixed(2)
|
||||
return formatMessage(commonMessages.fileSizeFormatted, {
|
||||
value: size,
|
||||
unit: TABLE_UNITS[exponent],
|
||||
})
|
||||
}
|
||||
|
||||
/** Match FileUploadDropdown queue item sizes. */
|
||||
function formatUploadQueueSize(bytes: number): string {
|
||||
if (bytes < 1024) {
|
||||
return formatMessage(commonMessages.fileSizeFormatted, { value: String(bytes), unit: 'B' })
|
||||
}
|
||||
if (bytes < 1024 ** 2) {
|
||||
return formatMessage(commonMessages.fileSizeFormatted, {
|
||||
value: (bytes / 1024).toFixed(1),
|
||||
unit: 'KB',
|
||||
})
|
||||
}
|
||||
if (bytes < 1024 ** 3) {
|
||||
return formatMessage(commonMessages.fileSizeFormatted, {
|
||||
value: (bytes / 1024 ** 2).toFixed(1),
|
||||
unit: 'MB',
|
||||
})
|
||||
}
|
||||
return formatMessage(commonMessages.fileSizeFormatted, {
|
||||
value: (bytes / 1024 ** 3).toFixed(1),
|
||||
unit: 'GB',
|
||||
})
|
||||
}
|
||||
|
||||
/** Match @modrinth/utils formatBytes (KiB / MiB / GiB). */
|
||||
function formatBinaryIecSize(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) {
|
||||
return formatMessage(commonMessages.fileSizeFormatted, { value: '0', unit: 'Bytes' })
|
||||
}
|
||||
const k = 1024
|
||||
const dm = decimals < 0 ? 0 : decimals
|
||||
const units = ['Bytes', 'KiB', 'MiB', 'GiB'] as const
|
||||
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), units.length - 1)
|
||||
const value = parseFloat((bytes / Math.pow(k, i)).toFixed(dm))
|
||||
return formatMessage(commonMessages.fileSizeFormatted, {
|
||||
value: String(value),
|
||||
unit: units[i],
|
||||
})
|
||||
}
|
||||
|
||||
return { formatTableRowSize, formatUploadQueueSize, formatBinaryIecSize }
|
||||
}
|
||||
@@ -5,26 +5,32 @@
|
||||
<!-- SFTP section -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col items-center justify-between gap-0.5 sm:flex-row">
|
||||
<span class="text-lg font-semibold text-contrast">SFTP</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.sftpSectionTitle)
|
||||
}}</span>
|
||||
<ButtonStyled>
|
||||
<a
|
||||
v-tooltip="'This button only works with compatible SFTP clients (e.g. WinSCP)'"
|
||||
v-tooltip="formatMessage(messages.sftpLaunchTooltip)"
|
||||
class="!w-full sm:!w-auto"
|
||||
:href="sftpUrl"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalIcon class="h-5 w-5" />
|
||||
Launch SFTP
|
||||
{{ formatMessage(messages.launchSftpButton) }}
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5 rounded-2xl bg-surface-2 p-4">
|
||||
<span class="text-lg font-semibold text-contrast">Server Address</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.serverAddressLabel)
|
||||
}}</span>
|
||||
<div
|
||||
v-tooltip="'Copy SFTP server address'"
|
||||
v-tooltip="formatMessage(messages.copySftpAddressTooltip)"
|
||||
class="copy-field hover:bg-button-bg-hover"
|
||||
@click="copyToClipboard('Server address', server?.sftp_host)"
|
||||
@click="
|
||||
copyToClipboard(formatMessage(messages.serverAddressLabel), server?.sftp_host)
|
||||
"
|
||||
>
|
||||
<span class="cursor-pointer font-semibold text-primary">
|
||||
{{ server?.sftp_host }}
|
||||
@@ -35,11 +41,18 @@
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 sm:mt-0 sm:flex-row">
|
||||
<div class="flex w-full flex-col justify-center gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">Username</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(commonMessages.usernameLabel)
|
||||
}}</span>
|
||||
<div
|
||||
v-tooltip="'Copy SFTP username'"
|
||||
v-tooltip="formatMessage(messages.copySftpUsernameTooltip)"
|
||||
class="copy-field hover:bg-button-bg-hover"
|
||||
@click="copyToClipboard('Username', server?.sftp_username)"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
formatMessage(commonMessages.usernameLabel),
|
||||
server?.sftp_username,
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="truncate font-semibold">
|
||||
{{ server?.sftp_username }}
|
||||
@@ -50,14 +63,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-col justify-center gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">Password</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(commonMessages.passwordLabel)
|
||||
}}</span>
|
||||
<div
|
||||
class="copy-field-has-button [&:hover:not(:has(button:hover))]:bg-button-bg-hover"
|
||||
@click="copyToClipboard('Password', server?.sftp_password)"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
formatMessage(commonMessages.passwordLabel),
|
||||
server?.sftp_password,
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="flex items-center gap-1.5 h-full w-full">
|
||||
<div
|
||||
v-tooltip="'Copy SFTP Password'"
|
||||
v-tooltip="formatMessage(messages.copySftpPasswordTooltip)"
|
||||
class="h-full flex justify-between grow items-center"
|
||||
>
|
||||
<div class="truncate font-semibold">
|
||||
@@ -72,7 +92,11 @@
|
||||
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button
|
||||
v-tooltip="showPassword ? 'Hide password' : 'Show password'"
|
||||
v-tooltip="
|
||||
showPassword
|
||||
? formatMessage(messages.hidePasswordTooltip)
|
||||
: formatMessage(messages.showPasswordTooltip)
|
||||
"
|
||||
class="hover:bg-button-bg-hover grid h-10 w-10 place-content-center rounded-lg"
|
||||
@click.stop="showPassword = !showPassword"
|
||||
>
|
||||
@@ -92,7 +116,9 @@
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="flex h-10 flex-col items-end justify-between gap-4 sm:flex-row">
|
||||
<label for="startup-command-field" class="mb-0.5 flex flex-col gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">Startup command</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.startupCommandLabel)
|
||||
}}</span>
|
||||
</label>
|
||||
<ButtonStyled v-if="startupCommand !== defaultStartupCommand" type="transparent">
|
||||
<button
|
||||
@@ -101,7 +127,7 @@
|
||||
@click="resetToDefault"
|
||||
>
|
||||
<UpdatedIcon class="h-5 w-5" />
|
||||
Default
|
||||
{{ formatMessage(messages.defaultStartupButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -121,13 +147,15 @@
|
||||
<SpinnerIcon class="h-6 w-6 animate-spin text-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
<span> The command that runs when your server is started. </span>
|
||||
<span>{{ formatMessage(messages.startupCommandDescription) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Java version section -->
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">Java version</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.javaVersionLabel)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="relative max-w-xs">
|
||||
<Combobox
|
||||
@@ -135,7 +163,9 @@
|
||||
v-model="javaVersion"
|
||||
name="java-version"
|
||||
:options="displayedJavaVersions"
|
||||
:display-value="javaVersionLabel ?? 'Java Version'"
|
||||
:display-value="
|
||||
javaVersionLabel ?? formatMessage(messages.javaVersionComboboxFallback)
|
||||
"
|
||||
:disabled="isStartupLoading"
|
||||
>
|
||||
<template #dropdown-footer>
|
||||
@@ -146,7 +176,11 @@
|
||||
>
|
||||
<EyeOffIcon v-if="showAllVersions" class="size-4" />
|
||||
<EyeIcon v-else class="size-4" />
|
||||
{{ showAllVersions ? 'Hide extra versions' : 'Show all versions' }}
|
||||
{{
|
||||
showAllVersions
|
||||
? formatMessage(messages.hideExtraJavaVersions)
|
||||
: formatMessage(messages.showAllJavaVersions)
|
||||
}}
|
||||
</button>
|
||||
</template>
|
||||
</Combobox>
|
||||
@@ -157,21 +191,23 @@
|
||||
<SpinnerIcon class="h-5 w-5 animate-spin text-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
<span> The Java version your server runs on. </span>
|
||||
<span>{{ formatMessage(messages.javaVersionDescription) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Java runtime section -->
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">Java runtime</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.javaRuntimeLabel)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="relative max-w-xs">
|
||||
<Combobox
|
||||
:id="'runtime-field'"
|
||||
v-model="jreVendor"
|
||||
name="runtime"
|
||||
:options="JRE_VENDORS"
|
||||
:display-value="jreVendorLabel ?? 'Runtime'"
|
||||
:options="JRE_VENDOR_OPTIONS"
|
||||
:display-value="jreVendorLabel ?? formatMessage(messages.javaRuntimeComboboxFallback)"
|
||||
:disabled="isStartupLoading"
|
||||
/>
|
||||
<div
|
||||
@@ -181,7 +217,7 @@
|
||||
<SpinnerIcon class="h-5 w-5 animate-spin text-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
<span> The Java runtime your server will use. </span>
|
||||
<span>{{ formatMessage(messages.javaRuntimeDescription) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,13 +246,111 @@ import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { ButtonStyled, Combobox, StyledInput } from '#ui/components'
|
||||
import SaveBanner from '#ui/components/servers/SaveBanner.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
} from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const messages = defineMessages({
|
||||
sftpSectionTitle: {
|
||||
id: 'server.settings.advanced.sftp.title',
|
||||
defaultMessage: 'SFTP',
|
||||
},
|
||||
sftpLaunchTooltip: {
|
||||
id: 'server.settings.advanced.sftp.launch-tooltip',
|
||||
defaultMessage: 'This button only works with compatible SFTP clients (e.g. WinSCP)',
|
||||
},
|
||||
launchSftpButton: {
|
||||
id: 'server.settings.advanced.sftp.launch',
|
||||
defaultMessage: 'Launch SFTP',
|
||||
},
|
||||
serverAddressLabel: {
|
||||
id: 'server.settings.advanced.sftp.server-address',
|
||||
defaultMessage: 'Server Address',
|
||||
},
|
||||
copySftpAddressTooltip: {
|
||||
id: 'server.settings.advanced.sftp.copy-address-tooltip',
|
||||
defaultMessage: 'Copy SFTP server address',
|
||||
},
|
||||
copySftpUsernameTooltip: {
|
||||
id: 'server.settings.advanced.sftp.copy-username-tooltip',
|
||||
defaultMessage: 'Copy SFTP username',
|
||||
},
|
||||
copySftpPasswordTooltip: {
|
||||
id: 'server.settings.advanced.sftp.copy-password-tooltip',
|
||||
defaultMessage: 'Copy SFTP password',
|
||||
},
|
||||
showPasswordTooltip: {
|
||||
id: 'server.settings.advanced.sftp.show-password-tooltip',
|
||||
defaultMessage: 'Show password',
|
||||
},
|
||||
hidePasswordTooltip: {
|
||||
id: 'server.settings.advanced.sftp.hide-password-tooltip',
|
||||
defaultMessage: 'Hide password',
|
||||
},
|
||||
startupCommandLabel: {
|
||||
id: 'server.settings.advanced.startup-command.title',
|
||||
defaultMessage: 'Startup command',
|
||||
},
|
||||
defaultStartupButton: {
|
||||
id: 'server.settings.advanced.startup-command.default',
|
||||
defaultMessage: 'Default',
|
||||
},
|
||||
startupCommandDescription: {
|
||||
id: 'server.settings.advanced.startup-command.description',
|
||||
defaultMessage: 'The command that runs when your server is started.',
|
||||
},
|
||||
javaVersionLabel: {
|
||||
id: 'server.settings.advanced.java-version.title',
|
||||
defaultMessage: 'Java version',
|
||||
},
|
||||
javaVersionComboboxFallback: {
|
||||
id: 'server.settings.advanced.java-version.fallback',
|
||||
defaultMessage: 'Java version',
|
||||
},
|
||||
javaVersionDescription: {
|
||||
id: 'server.settings.advanced.java-version.description',
|
||||
defaultMessage: 'The Java version your server runs on.',
|
||||
},
|
||||
showAllJavaVersions: {
|
||||
id: 'server.settings.advanced.java-version.show-all',
|
||||
defaultMessage: 'Show all versions',
|
||||
},
|
||||
hideExtraJavaVersions: {
|
||||
id: 'server.settings.advanced.java-version.hide-extra',
|
||||
defaultMessage: 'Hide extra versions',
|
||||
},
|
||||
javaRuntimeLabel: {
|
||||
id: 'server.settings.advanced.java-runtime.title',
|
||||
defaultMessage: 'Java runtime',
|
||||
},
|
||||
javaRuntimeComboboxFallback: {
|
||||
id: 'server.settings.advanced.java-runtime.fallback',
|
||||
defaultMessage: 'Runtime',
|
||||
},
|
||||
javaRuntimeDescription: {
|
||||
id: 'server.settings.advanced.java-runtime.description',
|
||||
defaultMessage: 'The Java runtime your server will use.',
|
||||
},
|
||||
clipboardCopiedTitle: {
|
||||
id: 'server.settings.advanced.clipboard.copied.title',
|
||||
defaultMessage: '{label} copied to clipboard!',
|
||||
},
|
||||
startupUpdateFailedTitle: {
|
||||
id: 'server.settings.advanced.error.startup.title',
|
||||
defaultMessage: 'Failed to update server arguments',
|
||||
},
|
||||
startupUpdateFailedText: {
|
||||
id: 'server.settings.advanced.error.startup.text',
|
||||
defaultMessage: 'Please try again later.',
|
||||
},
|
||||
})
|
||||
const { server, serverId, worldId } = injectModrinthServerContext()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -225,11 +359,11 @@ const queryClient = useQueryClient()
|
||||
const showPassword = ref(false)
|
||||
const sftpUrl = computed(() => `sftp://${server.value?.sftp_username}@${server.value?.sftp_host}`)
|
||||
|
||||
const copyToClipboard = (name: string, textToCopy?: string) => {
|
||||
const copyToClipboard = (label: string, textToCopy?: string) => {
|
||||
navigator.clipboard.writeText(textToCopy || '')
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: `${name} copied to clipboard!`,
|
||||
title: formatMessage(messages.clipboardCopiedTitle, { label }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -242,7 +376,7 @@ const { data: startupData, isLoading: isStartupLoading } = useQuery({
|
||||
enabled: computed(() => worldId.value !== null),
|
||||
})
|
||||
|
||||
const JAVA_VERSIONS = [
|
||||
const JAVA_VERSION_OPTIONS: { value: number; label: string }[] = [
|
||||
{ value: 8, label: 'Java 8' },
|
||||
{ value: 11, label: 'Java 11' },
|
||||
{ value: 17, label: 'Java 17' },
|
||||
@@ -271,24 +405,24 @@ function parseMinecraftReleaseVersion(version: string): MinecraftReleaseVersion
|
||||
}
|
||||
|
||||
function filterJavaVersions(compatibleVersions: number[]) {
|
||||
return JAVA_VERSIONS.filter((version) => compatibleVersions.includes(version.value))
|
||||
return JAVA_VERSION_OPTIONS.filter((version) => compatibleVersions.includes(version.value))
|
||||
}
|
||||
|
||||
const displayedJavaVersions = computed(() => {
|
||||
if (showAllVersions.value) return JAVA_VERSIONS
|
||||
if (showAllVersions.value) return JAVA_VERSION_OPTIONS
|
||||
|
||||
const mcVersion = server.value?.mc_version ?? ''
|
||||
if (!mcVersion) return JAVA_VERSIONS
|
||||
if (!mcVersion) return JAVA_VERSION_OPTIONS
|
||||
|
||||
const releaseVersion = parseMinecraftReleaseVersion(mcVersion)
|
||||
if (!releaseVersion) return JAVA_VERSIONS
|
||||
if (!releaseVersion) return JAVA_VERSION_OPTIONS
|
||||
|
||||
if (releaseVersion.major > 1) {
|
||||
if (releaseVersion.major >= 26) {
|
||||
return filterJavaVersions([25])
|
||||
}
|
||||
|
||||
return JAVA_VERSIONS
|
||||
return JAVA_VERSION_OPTIONS
|
||||
}
|
||||
|
||||
if (releaseVersion.minor >= 20) return filterJavaVersions([21])
|
||||
@@ -298,7 +432,7 @@ const displayedJavaVersions = computed(() => {
|
||||
return filterJavaVersions([8])
|
||||
})
|
||||
|
||||
const JRE_VENDORS: { value: Archon.Content.v1.JreVendor; label: string }[] = [
|
||||
const JRE_VENDOR_OPTIONS: { value: Archon.Content.v1.JreVendor; label: string }[] = [
|
||||
{ value: 'corretto', label: 'Corretto' },
|
||||
{ value: 'temurin', label: 'Temurin' },
|
||||
{ value: 'graal', label: 'GraalVM' },
|
||||
@@ -316,9 +450,11 @@ const javaVersion = ref<number>()
|
||||
const jreVendor = ref<Archon.Content.v1.JreVendor>()
|
||||
|
||||
const javaVersionLabel = computed(
|
||||
() => JAVA_VERSIONS.find((v) => v.value === javaVersion.value)?.label,
|
||||
() => JAVA_VERSION_OPTIONS.find((v) => v.value === javaVersion.value)?.label,
|
||||
)
|
||||
const jreVendorLabel = computed(
|
||||
() => JRE_VENDOR_OPTIONS.find((v) => v.value === jreVendor.value)?.label,
|
||||
)
|
||||
const jreVendorLabel = computed(() => JRE_VENDORS.find((v) => v.value === jreVendor.value)?.label)
|
||||
|
||||
function syncFormFromData() {
|
||||
startupCommand.value = savedStartupCommand.value
|
||||
@@ -355,16 +491,16 @@ const { mutate: saveStartup, isPending } = useMutation({
|
||||
syncFormFromData()
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Server settings updated',
|
||||
text: 'Your server settings were successfully changed.',
|
||||
title: formatMessage(commonMessages.serverSettingsUpdatedTitle),
|
||||
text: formatMessage(commonMessages.serverSettingsUpdatedText),
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to update server arguments',
|
||||
text: 'Please try again later.',
|
||||
title: formatMessage(messages.startupUpdateFailedTitle),
|
||||
text: formatMessage(messages.startupUpdateFailedText),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
<!-- Server name -->
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label for="server-name-field" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">Server name</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.serverNameLabel)
|
||||
}}</span>
|
||||
</label>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<StyledInput
|
||||
@@ -17,9 +19,11 @@
|
||||
:maxlength="48"
|
||||
@keyup.enter="!serverName && saveGeneral"
|
||||
/>
|
||||
<span>This name is only visible on Modrinth.</span>
|
||||
<span>{{ formatMessage(messages.serverNameDescription) }}</span>
|
||||
<div class="text-red font-medium">
|
||||
<span v-if="!isValidServerName"> Server name cannot be empty. </span>
|
||||
<span v-if="!isValidServerName">
|
||||
{{ formatMessage(messages.serverNameEmptyError) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,7 +31,9 @@
|
||||
<!-- Hostname -->
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label for="server-subdomain" class="flex flex-col gap-2.5">
|
||||
<span class="text-lg font-semibold text-contrast">Hostname</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.hostnameLabel)
|
||||
}}</span>
|
||||
<div
|
||||
class="flex w-full overflow-hidden rounded-xl bg-button-bg px-3 [box-shadow:var(--shadow-inset-sm)] transition-[box-shadow] duration-100 ease-in-out focus-within:[box-shadow:0_0_0_0.25rem_var(--color-brand-shadow)]"
|
||||
>
|
||||
@@ -35,12 +41,12 @@
|
||||
<span
|
||||
class="pointer-events-none invisible whitespace-pre px-px text-base font-medium"
|
||||
aria-hidden="true"
|
||||
>{{ serverSubdomain || 'Enter subdomain...' }}</span
|
||||
>{{ serverSubdomain || formatMessage(messages.subdomainPlaceholder) }}</span
|
||||
>
|
||||
<input
|
||||
id="server-subdomain"
|
||||
:value="serverSubdomain"
|
||||
placeholder="Enter subdomain..."
|
||||
:placeholder="formatMessage(messages.subdomainPlaceholder)"
|
||||
:maxlength="32"
|
||||
class="absolute left-px inset-0 bg-transparent !p-0 text-base font-medium text-primary !shadow-none transition-colors placeholder:text-secondary focus:text-contrast"
|
||||
autocomplete="off"
|
||||
@@ -56,13 +62,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<span>Your friends can connect to your server using this address.</span>
|
||||
<span>{{ formatMessage(messages.hostnameDescription) }}</span>
|
||||
<div v-if="!isValidSubdomain" class="text-red font-medium">
|
||||
<span v-if="!isValidLengthSubdomain">
|
||||
Subdomain must be at least 5 characters long.
|
||||
{{ formatMessage(messages.subdomainLengthError) }}
|
||||
</span>
|
||||
<span v-if="!isValidCharsSubdomain">
|
||||
Subdomain can only contain alphanumeric characters and dashes.
|
||||
{{ formatMessage(messages.subdomainCharsError) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,15 +85,17 @@
|
||||
>
|
||||
<label :for="`pref-${key}`" class="flex flex-col gap-1">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">{{ prefConfig.displayName }}</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(prefConfig.title)
|
||||
}}</span>
|
||||
<div
|
||||
v-if="!prefConfig.implemented"
|
||||
class="hidden items-center gap-1 rounded-full bg-surface-2 p-1 px-1.5 text-xs font-semibold sm:flex"
|
||||
>
|
||||
Coming Soon
|
||||
{{ formatMessage(messages.comingSoonBadge) }}
|
||||
</div>
|
||||
</div>
|
||||
<span>{{ prefConfig.description }}</span>
|
||||
<span>{{ formatMessage(prefConfig.description) }}</span>
|
||||
</label>
|
||||
<div v-tooltip="getPreferenceTooltip(key)">
|
||||
<Toggle
|
||||
@@ -102,14 +110,16 @@
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex flex-col gap-2.5 pb-10">
|
||||
<div class="text-lg m-0 font-semibold text-contrast">Info</div>
|
||||
<div class="text-lg m-0 font-semibold text-contrast">
|
||||
{{ formatMessage(messages.infoSectionTitle) }}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2.5 rounded-xl bg-surface-2 p-4">
|
||||
<div
|
||||
v-for="property in infoProperties"
|
||||
:key="property.name"
|
||||
class="flex items-start justify-between gap-4"
|
||||
>
|
||||
<template v-if="property.value !== 'Unknown'">
|
||||
<template v-if="property.value !== unknownLabelResolved">
|
||||
<span class="mt-1">{{ property.name }}</span>
|
||||
<CopyCode v-if="property.type === 'copy'" :text="property.value" />
|
||||
<div
|
||||
@@ -145,14 +155,142 @@ import { computed, ref, watch } from 'vue'
|
||||
import { CopyCode, StyledInput, Toggle } from '#ui/components'
|
||||
import EditServerIcon from '#ui/components/servers/edit-server-icon/EditServerIcon.vue'
|
||||
import SaveBanner from '#ui/components/servers/SaveBanner.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
injectPageContext,
|
||||
} from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const messages = defineMessages({
|
||||
serverNameLabel: {
|
||||
id: 'server.settings.general.server-name',
|
||||
defaultMessage: 'Server name',
|
||||
},
|
||||
serverNameDescription: {
|
||||
id: 'server.settings.general.server-name-description',
|
||||
defaultMessage: 'This name is only visible on Modrinth.',
|
||||
},
|
||||
serverNameEmptyError: {
|
||||
id: 'server.settings.general.server-name-empty',
|
||||
defaultMessage: 'Server name cannot be empty.',
|
||||
},
|
||||
hostnameLabel: {
|
||||
id: 'server.settings.general.hostname',
|
||||
defaultMessage: 'Hostname',
|
||||
},
|
||||
subdomainPlaceholder: {
|
||||
id: 'server.settings.general.subdomain-placeholder',
|
||||
defaultMessage: 'Enter subdomain…',
|
||||
},
|
||||
hostnameDescription: {
|
||||
id: 'server.settings.general.hostname-description',
|
||||
defaultMessage: 'Your friends can connect to your server using this address.',
|
||||
},
|
||||
subdomainLengthError: {
|
||||
id: 'server.settings.general.subdomain-length',
|
||||
defaultMessage: 'Subdomain must be at least 5 characters long.',
|
||||
},
|
||||
subdomainCharsError: {
|
||||
id: 'server.settings.general.subdomain-chars',
|
||||
defaultMessage: 'Subdomain can only contain alphanumeric characters and dashes.',
|
||||
},
|
||||
prefHideSubdomainTitle: {
|
||||
id: 'server.settings.general.pref.hide-subdomain.title',
|
||||
defaultMessage: 'Hide subdomain label',
|
||||
},
|
||||
prefHideSubdomainDescription: {
|
||||
id: 'server.settings.general.pref.hide-subdomain.description',
|
||||
defaultMessage: 'When enabled, the subdomain label will be hidden from the server header.',
|
||||
},
|
||||
prefRamAsBytesTitle: {
|
||||
id: 'server.settings.general.pref.ram-bytes.title',
|
||||
defaultMessage: 'RAM as bytes',
|
||||
},
|
||||
prefRamAsBytesDescription: {
|
||||
id: 'server.settings.general.pref.ram-bytes.description',
|
||||
defaultMessage: 'Show RAM usage in bytes instead of a percentage.',
|
||||
},
|
||||
prefRamAsBytesForcedTooltip: {
|
||||
id: 'server.settings.general.pref.ram-bytes.forced-tooltip',
|
||||
defaultMessage: 'Feature flag enabled to always show RAM as bytes.',
|
||||
},
|
||||
comingSoonBadge: {
|
||||
id: 'server.settings.general.coming-soon',
|
||||
defaultMessage: 'Coming soon',
|
||||
},
|
||||
infoSectionTitle: {
|
||||
id: 'server.settings.general.info.title',
|
||||
defaultMessage: 'Info',
|
||||
},
|
||||
infoServerId: {
|
||||
id: 'server.settings.general.info.server-id',
|
||||
defaultMessage: 'Server ID',
|
||||
},
|
||||
infoNode: {
|
||||
id: 'server.settings.general.info.node',
|
||||
defaultMessage: 'Node',
|
||||
},
|
||||
infoHostname: {
|
||||
id: 'server.settings.general.info.hostname',
|
||||
defaultMessage: 'Hostname',
|
||||
},
|
||||
infoServerSpecs: {
|
||||
id: 'server.settings.general.info.server-specs',
|
||||
defaultMessage: 'Server specs',
|
||||
},
|
||||
specsAvailable: {
|
||||
id: 'server.settings.general.info.specs-available',
|
||||
defaultMessage: 'Available',
|
||||
},
|
||||
specsCpuRamLine: {
|
||||
id: 'server.settings.general.info.specs-cpu-ram-line',
|
||||
defaultMessage:
|
||||
'{shared} {sharedNum, plural, one {Shared CPU} other {Shared CPUs}} (Bursts up to {burst} CPUs)',
|
||||
},
|
||||
specsRamGb: {
|
||||
id: 'server.settings.general.info.specs-ram-gb',
|
||||
defaultMessage: '{gb} GB RAM',
|
||||
},
|
||||
specsSwapGb: {
|
||||
id: 'server.settings.general.info.specs-swap-gb',
|
||||
defaultMessage: '{gb} GB Swap',
|
||||
},
|
||||
specsStorageGb: {
|
||||
id: 'server.settings.general.info.specs-storage-gb',
|
||||
defaultMessage: '{gb} GB SSD',
|
||||
},
|
||||
subdomainUnavailableTitle: {
|
||||
id: 'server.settings.general.error.subdomain-taken.title',
|
||||
defaultMessage: 'Subdomain not available',
|
||||
},
|
||||
subdomainUnavailableText: {
|
||||
id: 'server.settings.general.error.subdomain-taken.text',
|
||||
defaultMessage: 'The subdomain you entered is already in use.',
|
||||
},
|
||||
subdomainCheckFailedTitle: {
|
||||
id: 'server.settings.general.error.subdomain-check.title',
|
||||
defaultMessage: 'Error checking availability',
|
||||
},
|
||||
subdomainCheckFailedText: {
|
||||
id: 'server.settings.general.error.subdomain-check.text',
|
||||
defaultMessage: 'Failed to verify if the subdomain is available.',
|
||||
},
|
||||
settingsUpdateFailedTitle: {
|
||||
id: 'server.settings.general.error.update-failed.title',
|
||||
defaultMessage: 'Failed to update server settings',
|
||||
},
|
||||
settingsUpdateFailedText: {
|
||||
id: 'server.settings.general.error.update-failed.text',
|
||||
defaultMessage: 'An error occurred while attempting to update your server settings.',
|
||||
},
|
||||
})
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { server: data, serverId, busyReasons } = injectModrinthServerContext()
|
||||
const { featureFlags } = injectPageContext()
|
||||
@@ -185,18 +323,13 @@ watch(serverName, (newValue, oldValue) => {
|
||||
// Preferences
|
||||
const preferences = {
|
||||
hideSubdomainLabel: {
|
||||
displayName: 'Hide subdomain label',
|
||||
description: 'When enabled, the subdomain label will be hidden from the server header.',
|
||||
title: messages.prefHideSubdomainTitle,
|
||||
description: messages.prefHideSubdomainDescription,
|
||||
implemented: true,
|
||||
},
|
||||
// autoRestart: {
|
||||
// displayName: 'Auto restarts',
|
||||
// description: 'Automatically restart the server if it crashes.',
|
||||
// implemented: false,
|
||||
// },
|
||||
ramAsNumber: {
|
||||
displayName: 'RAM as bytes',
|
||||
description: 'Show RAM usage in bytes instead of a percentage.',
|
||||
title: messages.prefRamAsBytesTitle,
|
||||
description: messages.prefRamAsBytesDescription,
|
||||
implemented: true,
|
||||
},
|
||||
} as const
|
||||
@@ -229,7 +362,7 @@ const isPreferenceForcedByFeatureFlag = (key: string) =>
|
||||
|
||||
const getPreferenceTooltip = (key: string) =>
|
||||
isPreferenceForcedByFeatureFlag(key)
|
||||
? 'Feature flag enabled to always show RAM as bytes.'
|
||||
? formatMessage(messages.prefRamAsBytesForcedTooltip)
|
||||
: undefined
|
||||
|
||||
const getPreferenceValue = (key: string) =>
|
||||
@@ -289,8 +422,10 @@ const getServerSpecs = (product?: Labrinth.Billing.Internal.Product | null) => {
|
||||
}
|
||||
}
|
||||
|
||||
const unknownLabelResolved = computed(() => formatMessage(commonMessages.unknownLabel))
|
||||
|
||||
const serverHostname = computed(() =>
|
||||
serverSubdomain.value ? `${serverSubdomain.value}.modrinth.gg` : 'Unknown',
|
||||
serverSubdomain.value ? `${serverSubdomain.value}.modrinth.gg` : unknownLabelResolved.value,
|
||||
)
|
||||
|
||||
const serverSpecs = computed(() => getServerSpecs(serverProduct.value))
|
||||
@@ -314,24 +449,36 @@ type InfoProperty =
|
||||
}
|
||||
|
||||
// Info properties
|
||||
const infoProperties = computed<InfoProperty[]>(() => [
|
||||
{ name: 'Server ID', value: serverId ?? 'Unknown', type: 'copy' },
|
||||
{ name: 'Node', value: data.value?.node?.instance ?? 'Unknown', type: 'copy' },
|
||||
{ name: 'Hostname', value: serverHostname.value, type: 'copy' },
|
||||
{
|
||||
name: 'Server specs',
|
||||
value: serverSpecs.value ? 'Available' : 'Unknown',
|
||||
type: 'specs',
|
||||
lines: serverSpecs.value
|
||||
? [
|
||||
`${serverSpecs.value.sharedCpus} Shared CPU${Number(serverSpecs.value.sharedCpus) > 1 ? 's' : ''} (Bursts up to ${serverSpecs.value.burstCpus} CPUs)`,
|
||||
`${serverSpecs.value.ramGb} GB RAM`,
|
||||
`${serverSpecs.value.swapGb} GB Swap`,
|
||||
`${serverSpecs.value.storageGb} GB SSD`,
|
||||
]
|
||||
: [],
|
||||
},
|
||||
])
|
||||
const infoProperties = computed<InfoProperty[]>(() => {
|
||||
const u = unknownLabelResolved.value
|
||||
const specs = serverSpecs.value
|
||||
return [
|
||||
{ name: formatMessage(messages.infoServerId), value: serverId ?? u, type: 'copy' },
|
||||
{
|
||||
name: formatMessage(messages.infoNode),
|
||||
value: data.value?.node?.instance ?? u,
|
||||
type: 'copy',
|
||||
},
|
||||
{ name: formatMessage(messages.infoHostname), value: serverHostname.value, type: 'copy' },
|
||||
{
|
||||
name: formatMessage(messages.infoServerSpecs),
|
||||
value: specs ? formatMessage(messages.specsAvailable) : u,
|
||||
type: 'specs',
|
||||
lines: specs
|
||||
? [
|
||||
formatMessage(messages.specsCpuRamLine, {
|
||||
shared: specs.sharedCpus,
|
||||
sharedNum: Number(specs.sharedCpus),
|
||||
burst: specs.burstCpus,
|
||||
}),
|
||||
formatMessage(messages.specsRamGb, { gb: specs.ramGb }),
|
||||
formatMessage(messages.specsSwapGb, { gb: specs.swapGb }),
|
||||
formatMessage(messages.specsStorageGb, { gb: specs.storageGb }),
|
||||
]
|
||||
: [],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// Unsaved changes tracking (API fields + preferences)
|
||||
const hasUnsavedChanges = computed(
|
||||
@@ -359,8 +506,8 @@ const saveGeneral = async () => {
|
||||
if (!available) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Subdomain not available',
|
||||
text: 'The subdomain you entered is already in use.',
|
||||
title: formatMessage(messages.subdomainUnavailableTitle),
|
||||
text: formatMessage(messages.subdomainUnavailableText),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -370,8 +517,8 @@ const saveGeneral = async () => {
|
||||
console.error('Error checking subdomain availability:', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error checking availability',
|
||||
text: 'Failed to verify if the subdomain is available.',
|
||||
title: formatMessage(messages.subdomainCheckFailedTitle),
|
||||
text: formatMessage(messages.subdomainCheckFailedText),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -385,15 +532,15 @@ const saveGeneral = async () => {
|
||||
})
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Server settings updated',
|
||||
text: 'Your server settings were successfully changed.',
|
||||
title: formatMessage(commonMessages.serverSettingsUpdatedTitle),
|
||||
text: formatMessage(commonMessages.serverSettingsUpdatedText),
|
||||
})
|
||||
} 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.',
|
||||
title: formatMessage(messages.settingsUpdateFailedTitle),
|
||||
text: formatMessage(messages.settingsUpdateFailedText),
|
||||
})
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
|
||||
@@ -2,24 +2,32 @@
|
||||
<div>
|
||||
<Teleport to="body">
|
||||
<div class="relative z-[100]">
|
||||
<NewModal ref="editAllocationModal" header="Edit allocation" width="550px">
|
||||
<NewModal
|
||||
ref="editAllocationModal"
|
||||
:header="formatMessage(messages.editAllocationHeader)"
|
||||
width="550px"
|
||||
>
|
||||
<form class="flex w-full flex-col gap-2" @submit.prevent="editAllocation">
|
||||
<label for="edit-allocation-name" class="font-semibold text-contrast"> Name </label>
|
||||
<label for="edit-allocation-name" class="font-semibold text-contrast">
|
||||
{{ formatMessage(messages.allocationNameLabel) }}
|
||||
</label>
|
||||
<StyledInput
|
||||
id="edit-allocation-name"
|
||||
ref="editAllocationInput"
|
||||
v-model="editAllocationName"
|
||||
wrapper-class="w-full"
|
||||
:maxlength="32"
|
||||
placeholder="e.g. Secondary allocation"
|
||||
:placeholder="formatMessage(messages.allocationNamePlaceholder)"
|
||||
/>
|
||||
<div class="mb-1 mt-4 flex justify-end gap-2.5">
|
||||
<ButtonStyled>
|
||||
<button @click="editAllocationModal?.hide()">Cancel</button>
|
||||
<button @click="editAllocationModal?.hide()">
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!editAllocationName || creatingAllocation" type="submit">
|
||||
<SaveIcon /> Update allocation
|
||||
<SaveIcon /> {{ formatMessage(messages.updateAllocationButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -28,9 +36,9 @@
|
||||
|
||||
<ConfirmModal
|
||||
ref="confirmDeleteModal"
|
||||
title="Deleting allocation"
|
||||
:description="`You are deleting the allocation ${allocationToDelete}. This cannot be reserved again. Are you sure you want to proceed?`"
|
||||
proceed-label="Delete"
|
||||
:title="formatMessage(messages.deleteAllocationTitle)"
|
||||
:description="deleteAllocationDescriptionText"
|
||||
:proceed-label="formatMessage(commonMessages.deleteLabel)"
|
||||
@proceed="confirmDeleteAllocation"
|
||||
/>
|
||||
</div>
|
||||
@@ -47,16 +55,20 @@
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<IssuesIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-semibold">Failed to load network settings</h1>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-semibold">
|
||||
{{ formatMessage(messages.loadNetworkErrorTitle) }}
|
||||
</h1>
|
||||
</div>
|
||||
<p class="text-md text-secondary">
|
||||
We couldn't load your server's network settings. Here's what we know:
|
||||
{{ formatMessage(messages.loadNetworkErrorDescription) }}
|
||||
<span class="break-all font-mono">{{
|
||||
allocationsError?.message ?? 'Unknown error'
|
||||
allocationsError?.message ?? formatMessage(commonMessages.unknownLabel)
|
||||
}}</span>
|
||||
</p>
|
||||
<ButtonStyled size="large" color="brand" @click="() => refetchAllocations()">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
<button class="mt-6 !w-full">
|
||||
{{ formatMessage(commonMessages.retryButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,31 +77,37 @@
|
||||
<div class="flex h-full flex-col gap-6">
|
||||
<!-- Allocations section -->
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<span class="text-lg font-semibold text-contrast">Allocations</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.allocationsSectionTitle)
|
||||
}}</span>
|
||||
|
||||
<div class="flex w-full flex-col items-center justify-start gap-2 sm:flex-row">
|
||||
<StyledInput
|
||||
v-model="createAllocationName"
|
||||
wrapper-class="grow max-w-[400px]"
|
||||
:maxlength="32"
|
||||
placeholder="e.g. Secondary allocation"
|
||||
:placeholder="formatMessage(messages.allocationNamePlaceholder)"
|
||||
/>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
v-tooltip="!createAllocationName ? 'Enter a name to create an allocation' : ''"
|
||||
v-tooltip="
|
||||
!createAllocationName ? formatMessage(messages.createAllocationTooltip) : ''
|
||||
"
|
||||
:disabled="!createAllocationName || creatingAllocation"
|
||||
@click="addNewAllocation"
|
||||
>
|
||||
<PlusIcon />
|
||||
<span>Create allocation</span>
|
||||
<span>{{ formatMessage(messages.createAllocationButton) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<Table :columns="allocationColumns" :data="allocationRows" row-key="port">
|
||||
<template #cell-name="{ row }">
|
||||
<TagItem v-if="row.primary" class="!font-medium">Primary</TagItem>
|
||||
<TagItem v-if="row.primary" class="!font-medium">{{
|
||||
formatMessage(messages.primaryAllocationLabel)
|
||||
}}</TagItem>
|
||||
<span v-else class="font-semibold">{{ row.name }}</span>
|
||||
</template>
|
||||
<template #cell-port="{ row }">
|
||||
@@ -117,16 +135,15 @@
|
||||
</div>
|
||||
</template>
|
||||
</Table>
|
||||
<span>
|
||||
Create additional ports for internet-facing features like map viewers or voice chat
|
||||
mods.
|
||||
</span>
|
||||
<span>{{ formatMessage(messages.allocationsHelpText) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- DNS records section -->
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label for="user-domain" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">DNS records</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.dnsRecordsTitle)
|
||||
}}</span>
|
||||
</label>
|
||||
<div class="flex w-full flex-col items-center justify-start gap-2 sm:flex-row">
|
||||
<StyledInput
|
||||
@@ -144,7 +161,7 @@
|
||||
@click="exportDnsRecords"
|
||||
>
|
||||
<UploadIcon />
|
||||
<span>Export</span>
|
||||
<span>{{ formatMessage(messages.exportDnsButton) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -184,9 +201,7 @@
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<span>
|
||||
Set up your personal domain to connect to your server via custom DNS records.
|
||||
</span>
|
||||
<span>{{ formatMessage(messages.dnsRecordsHelpText) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,13 +224,145 @@ import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import { ButtonStyled, ConfirmModal, NewModal, StyledInput, Table, TagItem } from '#ui/components'
|
||||
import type { TableColumn } from '#ui/components/base'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
} from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const messages = defineMessages({
|
||||
editAllocationHeader: {
|
||||
id: 'server.settings.network.edit-allocation.title',
|
||||
defaultMessage: 'Edit allocation',
|
||||
},
|
||||
allocationNameLabel: {
|
||||
id: 'server.settings.network.allocation-name.label',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
allocationNamePlaceholder: {
|
||||
id: 'server.settings.network.allocation-name.placeholder',
|
||||
defaultMessage: 'e.g. Secondary allocation',
|
||||
},
|
||||
updateAllocationButton: {
|
||||
id: 'server.settings.network.update-allocation',
|
||||
defaultMessage: 'Update allocation',
|
||||
},
|
||||
deleteAllocationTitle: {
|
||||
id: 'server.settings.network.delete-allocation.title',
|
||||
defaultMessage: 'Deleting allocation',
|
||||
},
|
||||
deleteAllocationDescription: {
|
||||
id: 'server.settings.network.delete-allocation.description',
|
||||
defaultMessage:
|
||||
'You are deleting the allocation on port {port}. This cannot be reserved again. Are you sure you want to proceed?',
|
||||
},
|
||||
loadNetworkErrorTitle: {
|
||||
id: 'server.settings.network.error.load.title',
|
||||
defaultMessage: 'Failed to load network settings',
|
||||
},
|
||||
loadNetworkErrorDescription: {
|
||||
id: 'server.settings.network.error.load.description',
|
||||
defaultMessage: "We couldn't load your server's network settings. Here's what we know:",
|
||||
},
|
||||
allocationsSectionTitle: {
|
||||
id: 'server.settings.network.allocations.title',
|
||||
defaultMessage: 'Allocations',
|
||||
},
|
||||
createAllocationTooltip: {
|
||||
id: 'server.settings.network.create-allocation.tooltip',
|
||||
defaultMessage: 'Enter a name to create an allocation',
|
||||
},
|
||||
createAllocationButton: {
|
||||
id: 'server.settings.network.create-allocation',
|
||||
defaultMessage: 'Create allocation',
|
||||
},
|
||||
primaryAllocationLabel: {
|
||||
id: 'server.settings.network.primary-allocation',
|
||||
defaultMessage: 'Primary',
|
||||
},
|
||||
primaryAllocationRowName: {
|
||||
id: 'server.settings.network.primary-allocation-row-name',
|
||||
defaultMessage: 'Primary allocation',
|
||||
},
|
||||
allocationsHelpText: {
|
||||
id: 'server.settings.network.allocations.help',
|
||||
defaultMessage:
|
||||
'Create additional ports for internet-facing features like map viewers or voice chat mods.',
|
||||
},
|
||||
dnsRecordsTitle: {
|
||||
id: 'server.settings.network.dns.title',
|
||||
defaultMessage: 'DNS records',
|
||||
},
|
||||
exportDnsButton: {
|
||||
id: 'server.settings.network.dns.export',
|
||||
defaultMessage: 'Export',
|
||||
},
|
||||
dnsRecordsHelpText: {
|
||||
id: 'server.settings.network.dns.help',
|
||||
defaultMessage: 'Set up your personal domain to connect to your server via custom DNS records.',
|
||||
},
|
||||
columnName: {
|
||||
id: 'server.settings.network.column.name',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
columnPort: {
|
||||
id: 'server.settings.network.column.port',
|
||||
defaultMessage: 'Port',
|
||||
},
|
||||
columnActions: {
|
||||
id: 'server.settings.network.column.actions',
|
||||
defaultMessage: 'Actions',
|
||||
},
|
||||
columnRecordType: {
|
||||
id: 'server.settings.network.column.record-type',
|
||||
defaultMessage: 'Type',
|
||||
},
|
||||
columnRecordName: {
|
||||
id: 'server.settings.network.column.record-name',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
columnRecordContent: {
|
||||
id: 'server.settings.network.column.record-content',
|
||||
defaultMessage: 'Content',
|
||||
},
|
||||
allocationReservedTitle: {
|
||||
id: 'server.settings.network.success.allocation-reserved.title',
|
||||
defaultMessage: 'Allocation reserved',
|
||||
},
|
||||
allocationReservedText: {
|
||||
id: 'server.settings.network.success.allocation-reserved.text',
|
||||
defaultMessage: 'Your allocation has been reserved.',
|
||||
},
|
||||
allocationRemovedTitle: {
|
||||
id: 'server.settings.network.success.allocation-removed.title',
|
||||
defaultMessage: 'Allocation removed',
|
||||
},
|
||||
allocationRemovedText: {
|
||||
id: 'server.settings.network.success.allocation-removed.text',
|
||||
defaultMessage: 'Your allocation has been removed.',
|
||||
},
|
||||
allocationUpdatedTitle: {
|
||||
id: 'server.settings.network.success.allocation-updated.title',
|
||||
defaultMessage: 'Allocation updated',
|
||||
},
|
||||
allocationUpdatedText: {
|
||||
id: 'server.settings.network.success.allocation-updated.text',
|
||||
defaultMessage: 'Your allocation has been updated.',
|
||||
},
|
||||
textCopiedTitle: {
|
||||
id: 'server.settings.network.success.text-copied.title',
|
||||
defaultMessage: 'Text copied',
|
||||
},
|
||||
textCopiedText: {
|
||||
id: 'server.settings.network.success.text-copied.text',
|
||||
defaultMessage: '{text} has been copied to your clipboard',
|
||||
},
|
||||
})
|
||||
const { server, serverId } = injectModrinthServerContext()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -237,15 +384,15 @@ const {
|
||||
})
|
||||
const allocations = allocationsData
|
||||
|
||||
const allocationColumns: TableColumn[] = [
|
||||
{ key: 'name', label: 'Name', width: '40%' },
|
||||
{ key: 'port', label: 'Port' },
|
||||
{ key: 'actions', label: 'Actions', width: '33%', align: 'right' },
|
||||
]
|
||||
const allocationColumns = computed<TableColumn[]>(() => [
|
||||
{ key: 'name', label: formatMessage(messages.columnName), width: '40%' },
|
||||
{ key: 'port', label: formatMessage(messages.columnPort) },
|
||||
{ key: 'actions', label: formatMessage(messages.columnActions), width: '33%', align: 'right' },
|
||||
])
|
||||
|
||||
const allocationRows = computed(() => {
|
||||
const primary = {
|
||||
name: 'Primary allocation',
|
||||
name: formatMessage(messages.primaryAllocationRowName),
|
||||
port: serverPrimaryPort.value,
|
||||
primary: true,
|
||||
}
|
||||
@@ -257,11 +404,17 @@ const allocationRows = computed(() => {
|
||||
return [primary, ...extra]
|
||||
})
|
||||
|
||||
const dnsColumns: TableColumn[] = [
|
||||
{ key: 'type', label: 'Type', width: '20%' },
|
||||
{ key: 'name', label: 'Name', width: '35%' },
|
||||
{ key: 'content', label: 'Content' },
|
||||
]
|
||||
const dnsColumns = computed<TableColumn[]>(() => [
|
||||
{ key: 'type', label: formatMessage(messages.columnRecordType), width: '20%' },
|
||||
{ key: 'name', label: formatMessage(messages.columnRecordName), width: '35%' },
|
||||
{ key: 'content', label: formatMessage(messages.columnRecordContent) },
|
||||
])
|
||||
|
||||
const deleteAllocationDescriptionText = computed(() =>
|
||||
allocationToDelete.value != null
|
||||
? formatMessage(messages.deleteAllocationDescription, { port: allocationToDelete.value })
|
||||
: '',
|
||||
)
|
||||
|
||||
const editAllocationModal = ref<typeof NewModal>()
|
||||
const confirmDeleteModal = ref<typeof ConfirmModal>()
|
||||
@@ -284,8 +437,8 @@ const addNewAllocation = async () => {
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Allocation reserved',
|
||||
text: 'Your allocation has been reserved.',
|
||||
title: formatMessage(messages.allocationReservedTitle),
|
||||
text: formatMessage(messages.allocationReservedText),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to reserve new allocation:', error)
|
||||
@@ -318,8 +471,8 @@ const confirmDeleteAllocation = async () => {
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Allocation removed',
|
||||
text: 'Your allocation has been removed.',
|
||||
title: formatMessage(messages.allocationRemovedTitle),
|
||||
text: formatMessage(messages.allocationRemovedText),
|
||||
})
|
||||
|
||||
allocationToDelete.value = null
|
||||
@@ -342,8 +495,8 @@ const editAllocation = async () => {
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Allocation updated',
|
||||
text: 'Your allocation has been updated.',
|
||||
title: formatMessage(messages.allocationUpdatedTitle),
|
||||
text: formatMessage(messages.allocationUpdatedText),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to reserve new allocation:', error)
|
||||
@@ -404,8 +557,8 @@ const copyText = (text: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Text copied',
|
||||
text: `${text} has been copied to your clipboard`,
|
||||
title: formatMessage(messages.textCopiedTitle),
|
||||
text: formatMessage(messages.textCopiedText, { text }),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,32 +5,37 @@
|
||||
<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."
|
||||
:body="formatMessage(messages.missingPropertiesWarning)"
|
||||
/>
|
||||
<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.
|
||||
<IntlFormatted :message-id="messages.introParagraph">
|
||||
<template #files-link="{ children }">
|
||||
<AutoLink
|
||||
class="goto-link !inline-block"
|
||||
:to="filesTabLink"
|
||||
@click="onFilesTabLinkClick"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</AutoLink>
|
||||
</template>
|
||||
<template #wiki-link="{ children }">
|
||||
<AutoLink
|
||||
class="goto-link !inline-block"
|
||||
to="https://minecraft.wiki/w/Server.properties"
|
||||
target="_blank"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</AutoLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full text-sm">
|
||||
<label for="search-server-properties" class="sr-only"> Search server properties </label>
|
||||
<label for="search-server-properties" class="sr-only">
|
||||
{{ formatMessage(messages.searchPropertiesAriaLabel) }}
|
||||
</label>
|
||||
<StyledInput
|
||||
id="search-server-properties"
|
||||
v-model="searchInput"
|
||||
@@ -39,7 +44,7 @@
|
||||
:icon="SearchIcon"
|
||||
name="search"
|
||||
autocomplete="off"
|
||||
placeholder="Search server properties..."
|
||||
:placeholder="formatMessage(messages.searchPropertiesPlaceholder)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 pb-2">
|
||||
@@ -51,7 +56,9 @@
|
||||
>
|
||||
<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>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelGamemode)
|
||||
}}</span>
|
||||
<Chips
|
||||
v-model="combinedGamemode"
|
||||
:items="gamemodeItems"
|
||||
@@ -63,7 +70,9 @@
|
||||
v-if="combinedGamemode !== 'hardcore' && isPropertyVisible('difficulty')"
|
||||
class="flex flex-col gap-2.5 my-1"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Difficulty</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelDifficulty)
|
||||
}}</span>
|
||||
<Chips
|
||||
v-model="selectedDifficulty"
|
||||
:items="difficultyItems"
|
||||
@@ -72,23 +81,27 @@
|
||||
</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>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelMaxPlayers)
|
||||
}}</span>
|
||||
<StyledInput
|
||||
id="server-property-max-players"
|
||||
:model-value="liveProperties.max_players"
|
||||
type="number"
|
||||
placeholder="20"
|
||||
:placeholder="formatMessage(messages.placeholderDefaultMaxPlayers)"
|
||||
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>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelMotd)
|
||||
}}</span>
|
||||
<StyledInput
|
||||
id="server-property-motd"
|
||||
v-model="liveProperties.motd"
|
||||
placeholder="A Minecraft Server"
|
||||
:placeholder="formatMessage(messages.placeholderDefaultMotd)"
|
||||
wrapper-class="w-full max-w-[450px]"
|
||||
/>
|
||||
</div>
|
||||
@@ -97,7 +110,9 @@
|
||||
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>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelAllowFlight)
|
||||
}}</span>
|
||||
<Toggle
|
||||
id="server-property-allow-flight"
|
||||
:model-value="liveProperties.allow_flight === 'true'"
|
||||
@@ -109,7 +124,9 @@
|
||||
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>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelAllowCheats)
|
||||
}}</span>
|
||||
<Toggle
|
||||
id="server-property-allow-cheats"
|
||||
:model-value="liveProperties.allow_cheats === 'true'"
|
||||
@@ -121,7 +138,9 @@
|
||||
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>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelEnableWhitelist)
|
||||
}}</span>
|
||||
<Toggle id="server-property-whitelist" v-model="whitelistEnabled" />
|
||||
</div>
|
||||
|
||||
@@ -129,7 +148,9 @@
|
||||
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>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelEnableSpawnProtection)
|
||||
}}</span>
|
||||
<Toggle
|
||||
id="server-property-spawn-protection-toggle"
|
||||
v-model="spawnProtectionEnabled"
|
||||
@@ -140,7 +161,9 @@
|
||||
v-if="spawnProtectionEnabled && isPropertyVisible('spawn_protection')"
|
||||
class="flex items-center justify-between h-10"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Protection radius</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelProtectionRadius)
|
||||
}}</span>
|
||||
<StyledInput
|
||||
id="server-property-spawn-protection-radius"
|
||||
:model-value="liveProperties.spawn_protection"
|
||||
@@ -161,7 +184,9 @@
|
||||
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>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.advancedPropertiesTitle)
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col gap-6 pt-4">
|
||||
@@ -201,7 +226,7 @@
|
||||
:id="`server-property-${key}`"
|
||||
:model-value="liveProperties[key]"
|
||||
type="number"
|
||||
placeholder="Type here..."
|
||||
:placeholder="formatMessage(messages.propertyValuePlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
:aria-labelledby="`property-label-${key}`"
|
||||
@update:model-value="liveProperties[key] = String($event)"
|
||||
@@ -211,7 +236,7 @@
|
||||
<StyledInput
|
||||
:id="`server-property-${key}`"
|
||||
v-model="liveProperties[key]"
|
||||
placeholder="Type here..."
|
||||
:placeholder="formatMessage(messages.propertyValuePlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
:aria-labelledby="`property-label-${key}`"
|
||||
/>
|
||||
@@ -222,14 +247,17 @@
|
||||
</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
|
||||
>.
|
||||
<IntlFormatted :message-id="messages.footerParagraph">
|
||||
<template #files-link="{ children }">
|
||||
<AutoLink
|
||||
class="goto-link !inline-block"
|
||||
:to="filesTabLink"
|
||||
@click="onFilesTabLinkClick"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</AutoLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
@@ -239,8 +267,12 @@
|
||||
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>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.noSearchResultsTitle)
|
||||
}}</span>
|
||||
<span>{{
|
||||
formatMessage(messages.noSearchResultsDescription, { query: searchInput })
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -272,15 +304,230 @@ import Fuse from 'fuse.js'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { Accordion, Admonition, AutoLink, Chips, StyledInput, Toggle } from '#ui/components'
|
||||
import IntlFormatted from '#ui/components/base/IntlFormatted.vue'
|
||||
import SaveBanner from '#ui/components/servers/SaveBanner.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { injectServerSettings } from '#ui/layouts/shared/server-settings'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
} from '#ui/providers'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const messages = defineMessages({
|
||||
missingPropertiesWarning: {
|
||||
id: 'server.settings.properties.warning.missing',
|
||||
defaultMessage:
|
||||
"Some expected properties are missing from your server.properties — this usually means the server hasn't completed its first startup yet.",
|
||||
},
|
||||
introParagraph: {
|
||||
id: 'server.settings.properties.intro',
|
||||
defaultMessage:
|
||||
"Edit the Minecraft server properties file here, or use the <files-link>Files tab</files-link> to edit the full file. If you're unsure about a setting, the <wiki-link>Minecraft Wiki</wiki-link> has more details.",
|
||||
},
|
||||
searchPropertiesAriaLabel: {
|
||||
id: 'server.settings.properties.search.aria',
|
||||
defaultMessage: 'Search server properties',
|
||||
},
|
||||
searchPropertiesPlaceholder: {
|
||||
id: 'server.settings.properties.search.placeholder',
|
||||
defaultMessage: 'Search server properties…',
|
||||
},
|
||||
labelGamemode: {
|
||||
id: 'server.settings.properties.label.gamemode',
|
||||
defaultMessage: 'Gamemode',
|
||||
},
|
||||
labelDifficulty: {
|
||||
id: 'server.settings.properties.label.difficulty',
|
||||
defaultMessage: 'Difficulty',
|
||||
},
|
||||
labelMaxPlayers: {
|
||||
id: 'server.settings.properties.label.max-players',
|
||||
defaultMessage: 'Max players',
|
||||
},
|
||||
labelMotd: {
|
||||
id: 'server.settings.properties.label.motd',
|
||||
defaultMessage: 'MOTD',
|
||||
},
|
||||
labelAllowFlight: {
|
||||
id: 'server.settings.properties.label.allow-flight',
|
||||
defaultMessage: 'Allow flight',
|
||||
},
|
||||
labelAllowCheats: {
|
||||
id: 'server.settings.properties.label.allow-cheats',
|
||||
defaultMessage: 'Allow cheats',
|
||||
},
|
||||
labelEnableWhitelist: {
|
||||
id: 'server.settings.properties.label.enable-whitelist',
|
||||
defaultMessage: 'Enable whitelist',
|
||||
},
|
||||
labelEnableSpawnProtection: {
|
||||
id: 'server.settings.properties.label.enable-spawn-protection',
|
||||
defaultMessage: 'Enable spawn protection',
|
||||
},
|
||||
labelProtectionRadius: {
|
||||
id: 'server.settings.properties.label.protection-radius',
|
||||
defaultMessage: 'Protection radius',
|
||||
},
|
||||
advancedPropertiesTitle: {
|
||||
id: 'server.settings.properties.advanced.title',
|
||||
defaultMessage: 'Advanced properties',
|
||||
},
|
||||
groupPerformance: {
|
||||
id: 'server.settings.properties.group.performance',
|
||||
defaultMessage: 'Performance',
|
||||
},
|
||||
groupResourcePack: {
|
||||
id: 'server.settings.properties.group.resource-pack',
|
||||
defaultMessage: 'Resource pack',
|
||||
},
|
||||
propertyValuePlaceholder: {
|
||||
id: 'server.settings.properties.placeholder.value',
|
||||
defaultMessage: 'Type here…',
|
||||
},
|
||||
placeholderDefaultMaxPlayers: {
|
||||
id: 'server.settings.properties.placeholder.max-players',
|
||||
defaultMessage: '20',
|
||||
},
|
||||
placeholderDefaultMotd: {
|
||||
id: 'server.settings.properties.placeholder.motd',
|
||||
defaultMessage: 'A Minecraft Server',
|
||||
},
|
||||
footerParagraph: {
|
||||
id: 'server.settings.properties.footer',
|
||||
defaultMessage:
|
||||
'All other properties can be edited in server.properties via the <files-link>Files tab</files-link>.',
|
||||
},
|
||||
noSearchResultsTitle: {
|
||||
id: 'server.settings.properties.search.no-results.title',
|
||||
defaultMessage: 'No properties found',
|
||||
},
|
||||
noSearchResultsDescription: {
|
||||
id: 'server.settings.properties.search.no-results.description',
|
||||
defaultMessage: 'No properties match "{query}".',
|
||||
},
|
||||
propertiesUpdatedTitle: {
|
||||
id: 'server.settings.properties.success.updated.title',
|
||||
defaultMessage: 'Server properties updated',
|
||||
},
|
||||
propertiesUpdatedText: {
|
||||
id: 'server.settings.properties.success.updated.text',
|
||||
defaultMessage: 'Your server properties were successfully changed.',
|
||||
},
|
||||
propertiesUpdateFailedTitle: {
|
||||
id: 'server.settings.properties.error.update.title',
|
||||
defaultMessage: 'Failed to update server properties',
|
||||
},
|
||||
propertiesUpdateFailedFallback: {
|
||||
id: 'server.settings.properties.error.update.fallback',
|
||||
defaultMessage: 'An error occurred.',
|
||||
},
|
||||
})
|
||||
|
||||
const propertyFieldMessages = defineMessages({
|
||||
allow_cheats: {
|
||||
id: 'server.settings.properties.field.allow_cheats',
|
||||
defaultMessage: 'Allow cheats',
|
||||
},
|
||||
allow_flight: {
|
||||
id: 'server.settings.properties.field.allow_flight',
|
||||
defaultMessage: 'Allow flight',
|
||||
},
|
||||
difficulty: {
|
||||
id: 'server.settings.properties.field.difficulty',
|
||||
defaultMessage: 'Difficulty',
|
||||
},
|
||||
enforce_whitelist: {
|
||||
id: 'server.settings.properties.field.enforce_whitelist',
|
||||
defaultMessage: 'Enforce whitelist',
|
||||
},
|
||||
force_gamemode: {
|
||||
id: 'server.settings.properties.field.force_gamemode',
|
||||
defaultMessage: 'Force gamemode',
|
||||
},
|
||||
gamemode: {
|
||||
id: 'server.settings.properties.field.gamemode',
|
||||
defaultMessage: 'Gamemode',
|
||||
},
|
||||
generate_structures: {
|
||||
id: 'server.settings.properties.field.generate_structures',
|
||||
defaultMessage: 'Generate structures',
|
||||
},
|
||||
generator_settings: {
|
||||
id: 'server.settings.properties.field.generator_settings',
|
||||
defaultMessage: 'Generator settings',
|
||||
},
|
||||
hardcore: {
|
||||
id: 'server.settings.properties.field.hardcore',
|
||||
defaultMessage: 'Hardcore',
|
||||
},
|
||||
level_seed: {
|
||||
id: 'server.settings.properties.field.level_seed',
|
||||
defaultMessage: 'Level seed',
|
||||
},
|
||||
level_type: {
|
||||
id: 'server.settings.properties.field.level_type',
|
||||
defaultMessage: 'Level type',
|
||||
},
|
||||
max_players: {
|
||||
id: 'server.settings.properties.field.max_players',
|
||||
defaultMessage: 'Max players',
|
||||
},
|
||||
max_tick_time: {
|
||||
id: 'server.settings.properties.field.max_tick_time',
|
||||
defaultMessage: 'Max tick time',
|
||||
},
|
||||
motd: {
|
||||
id: 'server.settings.properties.field.motd',
|
||||
defaultMessage: 'MOTD',
|
||||
},
|
||||
pause_when_empty_seconds: {
|
||||
id: 'server.settings.properties.field.pause_when_empty_seconds',
|
||||
defaultMessage: 'Pause when empty (seconds)',
|
||||
},
|
||||
player_idle_timeout: {
|
||||
id: 'server.settings.properties.field.player_idle_timeout',
|
||||
defaultMessage: 'Player idle timeout',
|
||||
},
|
||||
require_resource_pack: {
|
||||
id: 'server.settings.properties.field.require_resource_pack',
|
||||
defaultMessage: 'Require resource pack',
|
||||
},
|
||||
resource_pack: {
|
||||
id: 'server.settings.properties.field.resource_pack',
|
||||
defaultMessage: 'Resource pack',
|
||||
},
|
||||
resource_pack_id: {
|
||||
id: 'server.settings.properties.field.resource_pack_id',
|
||||
defaultMessage: 'Resource pack ID',
|
||||
},
|
||||
resource_pack_sha1: {
|
||||
id: 'server.settings.properties.field.resource_pack_sha1',
|
||||
defaultMessage: 'Resource pack SHA-1',
|
||||
},
|
||||
simulation_distance: {
|
||||
id: 'server.settings.properties.field.simulation_distance',
|
||||
defaultMessage: 'Simulation distance',
|
||||
},
|
||||
spawn_protection: {
|
||||
id: 'server.settings.properties.field.spawn_protection',
|
||||
defaultMessage: 'Spawn protection',
|
||||
},
|
||||
sync_chunk_writes: {
|
||||
id: 'server.settings.properties.field.sync_chunk_writes',
|
||||
defaultMessage: 'Sync chunk writes',
|
||||
},
|
||||
view_distance: {
|
||||
id: 'server.settings.properties.field.view_distance',
|
||||
defaultMessage: 'View distance',
|
||||
},
|
||||
white_list: {
|
||||
id: 'server.settings.properties.field.white_list',
|
||||
defaultMessage: 'Whitelist',
|
||||
},
|
||||
})
|
||||
const client = injectModrinthClient()
|
||||
const { serverId, worldId, powerState, busyReasons } = injectModrinthServerContext()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -329,9 +576,9 @@ function getPropertyDef(key: string): PropertyDef {
|
||||
return KNOWN_PROPERTIES[key] ?? { type: 'text' }
|
||||
}
|
||||
|
||||
const ADVANCED_GROUPS = [
|
||||
const ADVANCED_GROUP_DEFS = [
|
||||
{
|
||||
label: 'Performance',
|
||||
labelMessage: messages.groupPerformance,
|
||||
keys: [
|
||||
'view_distance',
|
||||
'simulation_distance',
|
||||
@@ -342,10 +589,10 @@ const ADVANCED_GROUPS = [
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Resource Pack',
|
||||
labelMessage: messages.groupResourcePack,
|
||||
keys: ['resource_pack', 'resource_pack_id', 'resource_pack_sha1', 'require_resource_pack'],
|
||||
},
|
||||
]
|
||||
] as const
|
||||
|
||||
type CombinedGamemode = 'survival' | 'creative' | 'hardcore'
|
||||
const gamemodeItems: CombinedGamemode[] = ['survival', 'creative', 'hardcore']
|
||||
@@ -494,15 +741,18 @@ const { mutateAsync: saveProperties, isPending: isUpdating } = useMutation({
|
||||
syncFormFromData()
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Server properties updated',
|
||||
text: 'Your server properties were successfully changed.',
|
||||
title: formatMessage(messages.propertiesUpdatedTitle),
|
||||
text: formatMessage(messages.propertiesUpdatedText),
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to update server properties',
|
||||
text: error instanceof Error ? error.message : 'An error occurred.',
|
||||
title: formatMessage(messages.propertiesUpdateFailedTitle),
|
||||
text:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: formatMessage(messages.propertiesUpdateFailedFallback),
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -512,8 +762,8 @@ function resetProperties() {
|
||||
}
|
||||
|
||||
const advancedGroupedProperties = computed(() =>
|
||||
ADVANCED_GROUPS.map((group) => ({
|
||||
label: group.label,
|
||||
ADVANCED_GROUP_DEFS.map((group) => ({
|
||||
label: formatMessage(group.labelMessage),
|
||||
properties: group.keys.filter((key) => key in liveProperties.value),
|
||||
})).filter((g) => g.properties.length > 0),
|
||||
)
|
||||
@@ -551,6 +801,10 @@ const hasVisibleAdvancedProperties = computed(() =>
|
||||
)
|
||||
|
||||
function formatPropertyName(name: string): string {
|
||||
const known = propertyFieldMessages[name as keyof typeof propertyFieldMessages]
|
||||
if (known) {
|
||||
return formatMessage(known)
|
||||
}
|
||||
return name
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
|
||||
@@ -29,7 +29,6 @@ export interface ServerSettingsTabContext {
|
||||
|
||||
export interface ServerSettingsTabDefinition {
|
||||
id: ServerSettingsTabId
|
||||
label: string
|
||||
icon: Component
|
||||
href?: (ctx: ServerSettingsTabContext) => string
|
||||
external?: boolean
|
||||
@@ -39,33 +38,27 @@ export interface ServerSettingsTabDefinition {
|
||||
export const serverSettingsTabDefinitions: ServerSettingsTabDefinition[] = [
|
||||
{
|
||||
id: 'general',
|
||||
label: 'General',
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
id: 'installation',
|
||||
label: 'Installation',
|
||||
icon: WrenchIcon,
|
||||
},
|
||||
{
|
||||
id: 'network',
|
||||
label: 'Network',
|
||||
icon: VersionIcon,
|
||||
},
|
||||
{
|
||||
id: 'properties',
|
||||
label: 'Properties',
|
||||
icon: ListIcon,
|
||||
shown: ({ serverStatus }) => serverStatus !== 'installing',
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
label: 'Advanced',
|
||||
icon: TextQuoteIcon,
|
||||
},
|
||||
{
|
||||
id: 'billing',
|
||||
label: 'Billing',
|
||||
icon: CardIcon,
|
||||
href: ({ serverId }) => `/settings/billing#server-${serverId}`,
|
||||
external: true,
|
||||
@@ -73,7 +66,6 @@ export const serverSettingsTabDefinitions: ServerSettingsTabDefinition[] = [
|
||||
},
|
||||
{
|
||||
id: 'admin-billing',
|
||||
label: 'Admin Billing',
|
||||
icon: ModrinthIcon,
|
||||
href: ({ ownerId }) => `/admin/billing/${ownerId}`,
|
||||
external: true,
|
||||
|
||||
Reference in New Issue
Block a user