fix: various smaller fixes (#5917)
* fix: try fix email templates rendering links for variables * fix: b is not a function * fix: wording on modpack btn on setup type stage * fix: respect launcher-meta info * feat: i18n pass on creation flow modal * fix: prefetch loader manifests * fix: lint
This commit is contained in:
@@ -7,13 +7,13 @@
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border-surface-5" @click="triggerIconInput">
|
||||
<UploadIcon />
|
||||
Select icon
|
||||
{{ formatMessage(messages.selectIcon) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border-surface-5" :disabled="!ctx.instanceIcon.value" @click="removeIcon">
|
||||
<XIcon />
|
||||
Remove icon
|
||||
{{ formatMessage(messages.removeIcon) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -21,17 +21,19 @@
|
||||
|
||||
<!-- Instance-specific: Name field -->
|
||||
<div v-if="ctx.flowType === 'instance'" class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">Name</span>
|
||||
<span class="font-semibold text-contrast">{{ formatMessage(messages.nameLabel) }}</span>
|
||||
<StyledInput
|
||||
v-model="ctx.instanceName.value"
|
||||
:placeholder="ctx.autoInstanceName.value || 'Enter instance name'"
|
||||
:placeholder="ctx.autoInstanceName.value || formatMessage(messages.instanceNamePlaceholder)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Loader chips -->
|
||||
<div v-if="!hideLoaderChips" class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">{{
|
||||
ctx.flowType === 'instance' ? 'Loader' : 'Content loader'
|
||||
ctx.flowType === 'instance'
|
||||
? formatMessage(messages.loaderLabel)
|
||||
: formatMessage(messages.contentLoaderLabel)
|
||||
}}</span>
|
||||
<Chips
|
||||
v-model="selectedLoader"
|
||||
@@ -43,14 +45,21 @@
|
||||
|
||||
<!-- Game version -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">Game version</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(commonMessages.gameVersionLabel)
|
||||
}}</span>
|
||||
<Combobox
|
||||
v-model="selectedGameVersion"
|
||||
:options="gameVersionOptions"
|
||||
:no-options-message="
|
||||
gameVersionsLoading
|
||||
? formatMessage(commonMessages.loadingLabel)
|
||||
: formatMessage(messages.noVersionsAvailable)
|
||||
"
|
||||
searchable
|
||||
sync-with-selection
|
||||
placeholder="Select game version"
|
||||
search-placeholder="Search game version..."
|
||||
:placeholder="formatMessage(messages.selectGameVersion)"
|
||||
:search-placeholder="formatMessage(messages.searchGameVersion)"
|
||||
@option-hover="handleGameVersionHover"
|
||||
>
|
||||
<template v-if="ctx.showSnapshotToggle" #dropdown-footer>
|
||||
@@ -61,7 +70,11 @@
|
||||
>
|
||||
<EyeOffIcon v-if="ctx.showSnapshots.value" class="size-4" />
|
||||
<EyeIcon v-else class="size-4" />
|
||||
{{ ctx.showSnapshots.value ? 'Hide snapshots' : 'Show all versions' }}
|
||||
{{
|
||||
ctx.showSnapshots.value
|
||||
? formatMessage(commonMessages.hideSnapshotsButton)
|
||||
: formatMessage(commonMessages.showAllVersionsButton)
|
||||
}}
|
||||
</button>
|
||||
</template>
|
||||
</Combobox>
|
||||
@@ -72,24 +85,36 @@
|
||||
<Collapsible :collapsed="!selectedLoader || !selectedGameVersion" overflow-visible>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">{{
|
||||
isPaperLike ? 'Build number' : 'Loader version'
|
||||
isPaperLike
|
||||
? formatMessage(messages.buildNumberLabel)
|
||||
: formatMessage(messages.loaderVersionLabel)
|
||||
}}</span>
|
||||
<Chips
|
||||
v-if="!isPaperLike"
|
||||
v-model="loaderVersionType"
|
||||
:items="loaderVersionTypeItems"
|
||||
:format-label="capitalize"
|
||||
:format-label="formatLoaderVersionTypeLabel"
|
||||
/>
|
||||
<div v-if="isPaperLike || loaderVersionType === 'other'">
|
||||
<Combobox
|
||||
v-model="selectedLoaderVersion"
|
||||
:options="loaderVersionOptions"
|
||||
:no-options-message="loaderVersionsLoading ? 'Loading...' : 'No versions available'"
|
||||
:no-options-message="
|
||||
loaderVersionsLoading
|
||||
? formatMessage(commonMessages.loadingLabel)
|
||||
: formatMessage(messages.noVersionsAvailable)
|
||||
"
|
||||
searchable
|
||||
sync-with-selection
|
||||
:placeholder="isPaperLike ? 'Select build number' : 'Select loader version'"
|
||||
:placeholder="
|
||||
isPaperLike
|
||||
? formatMessage(messages.selectBuildNumber)
|
||||
: formatMessage(messages.selectLoaderVersion)
|
||||
"
|
||||
:search-placeholder="
|
||||
isPaperLike ? 'Search build number...' : 'Search loader version...'
|
||||
isPaperLike
|
||||
? formatMessage(messages.searchBuildNumber)
|
||||
: formatMessage(messages.searchLoaderVersion)
|
||||
"
|
||||
>
|
||||
<!-- When not Paper, this scoped slot is omitted and Combobox uses default option markup. -->
|
||||
@@ -123,6 +148,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Paper } from '@modrinth/api-client'
|
||||
import { EyeIcon, EyeOffIcon, UploadIcon, XIcon } from '@modrinth/assets'
|
||||
import { commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
@@ -135,13 +161,14 @@ import Collapsible from '../../../base/Collapsible.vue'
|
||||
import Combobox, { type ComboboxOption } from '../../../base/Combobox.vue'
|
||||
import PaperChannelBadge from '../../../base/PaperChannelBadge.vue'
|
||||
import StyledInput from '../../../base/StyledInput.vue'
|
||||
import type { LoaderVersionType } from '../creation-flow-context'
|
||||
import type { LoaderVersionEntry, LoaderVersionType } from '../creation-flow-context'
|
||||
import { injectCreationFlowContext } from '../creation-flow-context'
|
||||
import { capitalize, formatLoaderLabel } from '../shared'
|
||||
import { formatLoaderLabel } from '../shared'
|
||||
|
||||
const debug = useDebugLogger('CustomSetupStage')
|
||||
const client = injectModrinthClient()
|
||||
const ctx = injectCreationFlowContext()
|
||||
const { formatMessage } = useVIntl()
|
||||
const {
|
||||
selectedLoader,
|
||||
selectedGameVersion,
|
||||
@@ -151,6 +178,92 @@ const {
|
||||
hideLoaderVersion,
|
||||
} = ctx
|
||||
|
||||
const messages = defineMessages({
|
||||
selectIcon: {
|
||||
id: 'creation-flow.modal.custom-setup.icon.select',
|
||||
defaultMessage: 'Select icon',
|
||||
},
|
||||
removeIcon: {
|
||||
id: 'creation-flow.modal.custom-setup.icon.remove',
|
||||
defaultMessage: 'Remove icon',
|
||||
},
|
||||
nameLabel: {
|
||||
id: 'creation-flow.modal.custom-setup.name.label',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
instanceNamePlaceholder: {
|
||||
id: 'creation-flow.modal.custom-setup.name.placeholder',
|
||||
defaultMessage: 'Enter instance name',
|
||||
},
|
||||
loaderLabel: {
|
||||
id: 'creation-flow.modal.custom-setup.loader.label',
|
||||
defaultMessage: 'Loader',
|
||||
},
|
||||
contentLoaderLabel: {
|
||||
id: 'creation-flow.modal.custom-setup.content-loader.label',
|
||||
defaultMessage: 'Content loader',
|
||||
},
|
||||
noVersionsAvailable: {
|
||||
id: 'creation-flow.modal.custom-setup.options.no-versions-available',
|
||||
defaultMessage: 'No versions available',
|
||||
},
|
||||
selectGameVersion: {
|
||||
id: 'creation-flow.modal.custom-setup.game-version.placeholder',
|
||||
defaultMessage: 'Select game version',
|
||||
},
|
||||
searchGameVersion: {
|
||||
id: 'creation-flow.modal.custom-setup.game-version.search-placeholder',
|
||||
defaultMessage: 'Search game version...',
|
||||
},
|
||||
buildNumberLabel: {
|
||||
id: 'creation-flow.modal.custom-setup.build-number.label',
|
||||
defaultMessage: 'Build number',
|
||||
},
|
||||
loaderVersionLabel: {
|
||||
id: 'creation-flow.modal.custom-setup.loader-version.label',
|
||||
defaultMessage: 'Loader version',
|
||||
},
|
||||
selectBuildNumber: {
|
||||
id: 'creation-flow.modal.custom-setup.build-number.placeholder',
|
||||
defaultMessage: 'Select build number',
|
||||
},
|
||||
selectLoaderVersion: {
|
||||
id: 'creation-flow.modal.custom-setup.loader-version.placeholder',
|
||||
defaultMessage: 'Select loader version',
|
||||
},
|
||||
searchBuildNumber: {
|
||||
id: 'creation-flow.modal.custom-setup.build-number.search-placeholder',
|
||||
defaultMessage: 'Search build number...',
|
||||
},
|
||||
searchLoaderVersion: {
|
||||
id: 'creation-flow.modal.custom-setup.loader-version.search-placeholder',
|
||||
defaultMessage: 'Search loader version...',
|
||||
},
|
||||
stableLoaderVersionType: {
|
||||
id: 'creation-flow.modal.custom-setup.loader-version-type.stable',
|
||||
defaultMessage: 'Stable',
|
||||
},
|
||||
latestLoaderVersionType: {
|
||||
id: 'creation-flow.modal.custom-setup.loader-version-type.latest',
|
||||
defaultMessage: 'Latest',
|
||||
},
|
||||
otherLoaderVersionType: {
|
||||
id: 'creation-flow.modal.custom-setup.loader-version-type.other',
|
||||
defaultMessage: 'Other',
|
||||
},
|
||||
})
|
||||
|
||||
function formatLoaderVersionTypeLabel(type: LoaderVersionType): string {
|
||||
switch (type) {
|
||||
case 'stable':
|
||||
return formatMessage(messages.stableLoaderVersionType)
|
||||
case 'latest':
|
||||
return formatMessage(messages.latestLoaderVersionType)
|
||||
case 'other':
|
||||
return formatMessage(messages.otherLoaderVersionType)
|
||||
}
|
||||
}
|
||||
|
||||
// For instance flow, prepend 'vanilla' to available loaders.
|
||||
// For server flows, vanilla is a separate option in the setup type stage, so exclude it here.
|
||||
const effectiveLoaders = computed(() => {
|
||||
@@ -205,23 +318,24 @@ function removeIcon() {
|
||||
ctx.instanceIconPath.value = null
|
||||
}
|
||||
|
||||
// Loader versions fetched from launcher-meta
|
||||
interface LoaderVersionEntry {
|
||||
id: string
|
||||
stable: boolean
|
||||
}
|
||||
|
||||
const loaderVersionsLoading = ref(false)
|
||||
const loaderVersionsData = ref<LoaderVersionEntry[]>([])
|
||||
const loaderVersionsCache = ref<Record<string, { id: string; loaders: LoaderVersionEntry[] }[]>>({})
|
||||
|
||||
// Paper/Purpur build caches
|
||||
const paperVersions = ref<Record<string, Paper.Versions.v3.Build[]>>({})
|
||||
const purpurVersions = ref<Record<string, string[]>>({})
|
||||
|
||||
// Paper/Purpur supported game version sets (for filtering the game version combobox)
|
||||
const paperSupportedVersions = ref<Set<string> | null>(null)
|
||||
const purpurSupportedVersions = ref<Set<string> | null>(null)
|
||||
function toApiLoaderName(loader: string): string {
|
||||
return loader === 'neoforge' ? 'neo' : loader
|
||||
}
|
||||
|
||||
const gameVersionsLoading = computed(() => {
|
||||
const loader = selectedLoader.value
|
||||
if (!loader || loader === 'vanilla') return false
|
||||
if (loader === 'paper') return ctx.paperSupportedVersions.value === null
|
||||
if (loader === 'purpur') return ctx.purpurSupportedVersions.value === null
|
||||
return ctx.loaderVersionsCache.value[toApiLoaderName(loader)] === undefined
|
||||
})
|
||||
|
||||
// Game versions from tags provider, filtered by loader support
|
||||
const gameVersionOptions = computed<ComboboxOption<string>[]>(() => {
|
||||
@@ -231,33 +345,35 @@ const gameVersionOptions = computed<ComboboxOption<string>[]>(() => {
|
||||
|
||||
// For loaders with per-version data, only show game versions that have builds
|
||||
if (selectedLoader.value && selectedLoader.value !== 'vanilla') {
|
||||
if (selectedLoader.value === 'paper' && paperSupportedVersions.value) {
|
||||
if (selectedLoader.value === 'paper') {
|
||||
if (!ctx.paperSupportedVersions.value) return []
|
||||
return versions
|
||||
.filter((v) => paperSupportedVersions.value!.has(v.version))
|
||||
.filter((v) => ctx.paperSupportedVersions.value!.has(v.version))
|
||||
.map((v) => ({ value: v.version, label: v.version }))
|
||||
}
|
||||
|
||||
if (selectedLoader.value === 'purpur' && purpurSupportedVersions.value) {
|
||||
if (selectedLoader.value === 'purpur') {
|
||||
if (!ctx.purpurSupportedVersions.value) return []
|
||||
return versions
|
||||
.filter((v) => purpurSupportedVersions.value!.has(v.version))
|
||||
.filter((v) => ctx.purpurSupportedVersions.value!.has(v.version))
|
||||
.map((v) => ({ value: v.version, label: v.version }))
|
||||
}
|
||||
|
||||
let apiLoader = selectedLoader.value
|
||||
if (apiLoader === 'neoforge') apiLoader = 'neo'
|
||||
const apiLoader = toApiLoaderName(selectedLoader.value)
|
||||
const manifest = ctx.loaderVersionsCache.value[apiLoader]
|
||||
if (!manifest) return []
|
||||
|
||||
const manifest = loaderVersionsCache.value[apiLoader]
|
||||
if (manifest) {
|
||||
const hasPlaceholder = manifest.some((x) => x.id === '${modrinth.gameVersion}')
|
||||
if (!hasPlaceholder) {
|
||||
const supportedVersions = new Set(
|
||||
manifest.filter((x) => x.loaders.length > 0).map((x) => x.id),
|
||||
const hasPlaceholder = manifest.some((x) => x.id === '${modrinth.gameVersion}')
|
||||
const supportedVersions = new Set(
|
||||
manifest
|
||||
.filter(
|
||||
(x) => x.id !== '${modrinth.gameVersion}' && (hasPlaceholder || x.loaders.length > 0),
|
||||
)
|
||||
return versions
|
||||
.filter((v) => supportedVersions.has(v.version))
|
||||
.map((v) => ({ value: v.version, label: v.version }))
|
||||
}
|
||||
}
|
||||
.map((x) => x.id),
|
||||
)
|
||||
return versions
|
||||
.filter((v) => supportedVersions.has(v.version))
|
||||
.map((v) => ({ value: v.version, label: v.version }))
|
||||
}
|
||||
|
||||
return versions.map((v) => ({ value: v.version, label: v.version }))
|
||||
@@ -267,7 +383,10 @@ const gameVersionOptions = computed<ComboboxOption<string>[]>(() => {
|
||||
watch(
|
||||
gameVersionOptions,
|
||||
(options) => {
|
||||
if (options.length === 0) return
|
||||
if (options.length === 0) {
|
||||
selectedGameVersion.value = null
|
||||
return
|
||||
}
|
||||
if (!selectedGameVersion.value || !options.some((o) => o.value === selectedGameVersion.value)) {
|
||||
selectedGameVersion.value = options[0].value
|
||||
}
|
||||
@@ -276,50 +395,20 @@ watch(
|
||||
)
|
||||
|
||||
async function fetchLoaderManifest(loader: string) {
|
||||
let apiLoader = loader
|
||||
if (apiLoader === 'neoforge') apiLoader = 'neo'
|
||||
|
||||
const apiLoader = toApiLoaderName(loader)
|
||||
debug(
|
||||
'fetchLoaderManifest:',
|
||||
loader,
|
||||
'apiLoader:',
|
||||
apiLoader,
|
||||
'cached:',
|
||||
!!loaderVersionsCache.value[apiLoader],
|
||||
!!ctx.loaderVersionsCache.value[apiLoader],
|
||||
)
|
||||
if (loaderVersionsCache.value[apiLoader]) return
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://launcher-meta.modrinth.com/${apiLoader}/v0/manifest.json`)
|
||||
const data = (await res.json()) as {
|
||||
gameVersions: { id: string; loaders: LoaderVersionEntry[] }[]
|
||||
}
|
||||
loaderVersionsCache.value[apiLoader] = data.gameVersions
|
||||
debug('fetchLoaderManifest: loaded', apiLoader, 'gameVersions:', data.gameVersions.length)
|
||||
} catch (e) {
|
||||
debug('fetchLoaderManifest: FAILED', apiLoader, e)
|
||||
loaderVersionsCache.value[apiLoader] = []
|
||||
}
|
||||
await ctx.fetchLoaderMetadata(loader)
|
||||
}
|
||||
|
||||
async function fetchPaperSupportedVersions() {
|
||||
if (paperSupportedVersions.value) return
|
||||
try {
|
||||
const project = await client.paper.versions_v3.getProject()
|
||||
paperSupportedVersions.value = new Set(Object.values(project.versions).flat())
|
||||
} catch {
|
||||
paperSupportedVersions.value = new Set()
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPurpurSupportedVersions() {
|
||||
if (purpurSupportedVersions.value) return
|
||||
try {
|
||||
const project = await client.purpur.versions_v2.getProject()
|
||||
purpurSupportedVersions.value = new Set(project.versions)
|
||||
} catch {
|
||||
purpurSupportedVersions.value = new Set()
|
||||
}
|
||||
async function fetchLoaderMetadata(loader?: string | null) {
|
||||
await ctx.fetchLoaderMetadata(loader)
|
||||
}
|
||||
|
||||
function paperBuildChannelTag(buildId: string): 'ALPHA' | 'BETA' | null {
|
||||
@@ -363,10 +452,8 @@ function getLoaderVersionsForGameVersion(
|
||||
loader: string,
|
||||
gameVersion: string,
|
||||
): LoaderVersionEntry[] {
|
||||
let apiLoader = loader
|
||||
if (apiLoader === 'neoforge') apiLoader = 'neo'
|
||||
|
||||
const manifest = loaderVersionsCache.value[apiLoader]
|
||||
const apiLoader = toApiLoaderName(loader)
|
||||
const manifest = ctx.loaderVersionsCache.value[apiLoader]
|
||||
debug('getLoaderVersionsForGameVersion:', {
|
||||
loader,
|
||||
apiLoader,
|
||||
@@ -379,6 +466,7 @@ function getLoaderVersionsForGameVersion(
|
||||
// Some loaders (e.g. Fabric) list all versions under a placeholder entry
|
||||
const placeholder = manifest.find((x) => x.id === '${modrinth.gameVersion}')
|
||||
if (placeholder) {
|
||||
if (!manifest.some((x) => x.id === gameVersion)) return []
|
||||
debug(
|
||||
'getLoaderVersionsForGameVersion: using placeholder, loaders:',
|
||||
placeholder.loaders.length,
|
||||
@@ -400,16 +488,7 @@ function getLoaderVersionsForGameVersion(
|
||||
watch(
|
||||
() => selectedLoader.value,
|
||||
async (loader) => {
|
||||
if (!loader || loader === 'vanilla') return
|
||||
if (loader === 'paper') {
|
||||
await fetchPaperSupportedVersions()
|
||||
return
|
||||
}
|
||||
if (loader === 'purpur') {
|
||||
await fetchPurpurSupportedVersions()
|
||||
return
|
||||
}
|
||||
await fetchLoaderManifest(loader)
|
||||
await fetchLoaderMetadata(loader)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
@@ -4,18 +4,23 @@
|
||||
v-if="ctx.flowType !== 'server-onboarding' && ctx.flowType !== 'reset-server'"
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<span class="font-semibold text-contrast">World name</span>
|
||||
<StyledInput v-model="worldName" placeholder="Enter world name" />
|
||||
<span class="font-semibold text-contrast">{{ formatMessage(messages.worldNameLabel) }}</span>
|
||||
<StyledInput
|
||||
v-model="worldName"
|
||||
:placeholder="formatMessage(messages.worldNamePlaceholder)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="ctx.setupType.value === 'vanilla'" class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">Game version</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(commonMessages.gameVersionLabel)
|
||||
}}</span>
|
||||
<Combobox
|
||||
v-model="selectedGameVersion"
|
||||
:options="gameVersionOptions"
|
||||
searchable
|
||||
sync-with-selection
|
||||
placeholder="Select game version"
|
||||
:placeholder="formatMessage(messages.gameVersionPlaceholder)"
|
||||
>
|
||||
<template v-if="ctx.showSnapshotToggle" #dropdown-footer>
|
||||
<button
|
||||
@@ -25,37 +30,50 @@
|
||||
>
|
||||
<EyeOffIcon v-if="ctx.showSnapshots.value" class="size-4" />
|
||||
<EyeIcon v-else class="size-4" />
|
||||
{{ ctx.showSnapshots.value ? 'Hide snapshots' : 'Show all versions' }}
|
||||
{{
|
||||
ctx.showSnapshots.value
|
||||
? formatMessage(commonMessages.hideSnapshotsButton)
|
||||
: formatMessage(commonMessages.showAllVersionsButton)
|
||||
}}
|
||||
</button>
|
||||
</template>
|
||||
</Combobox>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">Gamemode</span>
|
||||
<Chips v-model="gamemode" :items="gamemodeItems" :format-label="capitalize" />
|
||||
<span class="font-semibold text-contrast">{{ formatMessage(messages.gamemodeLabel) }}</span>
|
||||
<Chips v-model="gamemode" :items="gamemodeItems" :format-label="formatGamemodeLabel" />
|
||||
</div>
|
||||
|
||||
<div v-if="gamemode !== 'hardcore'" class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">Difficulty</span>
|
||||
<Chips v-model="difficulty" :items="difficultyItems" :format-label="capitalize" />
|
||||
<span class="font-semibold text-contrast">{{ formatMessage(messages.difficultyLabel) }}</span>
|
||||
<Chips v-model="difficulty" :items="difficultyItems" :format-label="formatDifficultyLabel" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">World type</span>
|
||||
<span class="font-semibold text-contrast">{{ formatMessage(messages.worldTypeLabel) }}</span>
|
||||
<Combobox
|
||||
v-model="worldTypeOption"
|
||||
:options="worldTypeOptions"
|
||||
placeholder="Select world type"
|
||||
:placeholder="formatMessage(messages.worldTypePlaceholder)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast"
|
||||
>World seed <span class="text-secondary font-normal">(Optional)</span></span
|
||||
>
|
||||
<StyledInput v-model="worldSeed" placeholder="Enter world seed" />
|
||||
<span class="text-sm text-secondary">Leave blank for a random seed.</span>
|
||||
<span class="font-semibold text-contrast">
|
||||
<IntlFormatted :message-id="messages.worldSeedLabelWithOptional">
|
||||
<template #optional="{ children }">
|
||||
<span class="text-secondary font-normal">
|
||||
<component :is="() => children" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
<StyledInput
|
||||
v-model="worldSeed"
|
||||
:placeholder="formatMessage(messages.worldSeedPlaceholder)"
|
||||
/>
|
||||
<span class="text-sm text-secondary">{{ formatMessage(messages.worldSeedDescription) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="h-px w-full bg-surface-5" />
|
||||
@@ -63,36 +81,42 @@
|
||||
<Accordion overflow-visible button-class="w-full bg-transparent m-0 p-0 border-none">
|
||||
<template #title>
|
||||
<SettingsIcon class="size-4 shrink-0 text-primary" />
|
||||
<span class="font-semibold text-contrast text-lg">Additional settings</span>
|
||||
<span class="font-semibold text-contrast text-lg">{{
|
||||
formatMessage(messages.additionalSettingsTitle)
|
||||
}}</span>
|
||||
</template>
|
||||
<div class="flex flex-col gap-4 pt-4">
|
||||
<div class="flex w-full flex-row items-center justify-between gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="font-semibold text-contrast">Generate structures</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.generateStructuresLabel)
|
||||
}}</span>
|
||||
<span class="text-sm text-secondary">
|
||||
Controls whether villages, strongholds, and other structures generate in new chunks.
|
||||
{{ formatMessage(messages.generateStructuresDescription) }}
|
||||
</span>
|
||||
</div>
|
||||
<Toggle v-model="generateStructures" small class="shrink-0" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">Generator settings</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.generatorSettingsLabel)
|
||||
}}</span>
|
||||
<Combobox
|
||||
v-model="generatorSettingsMode"
|
||||
:options="generatorSettingsOptions"
|
||||
placeholder="Select generator settings"
|
||||
:placeholder="formatMessage(messages.generatorSettingsPlaceholder)"
|
||||
/>
|
||||
<StyledInput
|
||||
v-if="generatorSettingsMode === 'custom'"
|
||||
v-model="generatorSettingsCustom"
|
||||
multiline
|
||||
:rows="4"
|
||||
placeholder="Enter generator settings JSON"
|
||||
:placeholder="formatMessage(messages.generatorSettingsJsonPlaceholder)"
|
||||
input-class="font-mono"
|
||||
/>
|
||||
<span class="text-sm text-secondary">
|
||||
Used for advanced world customization such as custom Superflat layers.
|
||||
{{ formatMessage(messages.generatorSettingsDescription) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,7 +125,7 @@
|
||||
<InlineBackupCreator
|
||||
v-if="ctx.flowType === 'reset-server'"
|
||||
ref="backupCreator"
|
||||
backup-name="Before reset server"
|
||||
:backup-name="formatMessage(messages.beforeResetServerBackupName)"
|
||||
hide-shift-click-hint
|
||||
@update:buttons-disabled="ctx.isBackingUp.value = $event"
|
||||
/>
|
||||
@@ -110,6 +134,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { EyeIcon, EyeOffIcon, SettingsIcon } from '@modrinth/assets'
|
||||
import { commonMessages, defineMessages, IntlFormatted, useVIntl } from '@modrinth/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
@@ -123,10 +148,146 @@ import StyledInput from '../../../base/StyledInput.vue'
|
||||
import Toggle from '../../../base/Toggle.vue'
|
||||
import type { Difficulty, Gamemode, GeneratorSettingsMode } from '../creation-flow-context'
|
||||
import { injectCreationFlowContext } from '../creation-flow-context'
|
||||
import { capitalize } from '../shared'
|
||||
|
||||
const debug = useDebugLogger('FinalConfigStage')
|
||||
const ctx = injectCreationFlowContext()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
worldNameLabel: {
|
||||
id: 'creation-flow.modal.final-config.world-name.label',
|
||||
defaultMessage: 'World name',
|
||||
},
|
||||
worldNamePlaceholder: {
|
||||
id: 'creation-flow.modal.final-config.world-name.placeholder',
|
||||
defaultMessage: 'Enter world name',
|
||||
},
|
||||
gameVersionPlaceholder: {
|
||||
id: 'creation-flow.modal.final-config.game-version.placeholder',
|
||||
defaultMessage: 'Select game version',
|
||||
},
|
||||
gamemodeLabel: {
|
||||
id: 'creation-flow.modal.final-config.gamemode.label',
|
||||
defaultMessage: 'Gamemode',
|
||||
},
|
||||
gamemodeSurvival: {
|
||||
id: 'creation-flow.modal.final-config.gamemode.survival',
|
||||
defaultMessage: 'Survival',
|
||||
},
|
||||
gamemodeCreative: {
|
||||
id: 'creation-flow.modal.final-config.gamemode.creative',
|
||||
defaultMessage: 'Creative',
|
||||
},
|
||||
gamemodeHardcore: {
|
||||
id: 'creation-flow.modal.final-config.gamemode.hardcore',
|
||||
defaultMessage: 'Hardcore',
|
||||
},
|
||||
difficultyLabel: {
|
||||
id: 'creation-flow.modal.final-config.difficulty.label',
|
||||
defaultMessage: 'Difficulty',
|
||||
},
|
||||
difficultyPeaceful: {
|
||||
id: 'creation-flow.modal.final-config.difficulty.peaceful',
|
||||
defaultMessage: 'Peaceful',
|
||||
},
|
||||
difficultyEasy: {
|
||||
id: 'creation-flow.modal.final-config.difficulty.easy',
|
||||
defaultMessage: 'Easy',
|
||||
},
|
||||
difficultyNormal: {
|
||||
id: 'creation-flow.modal.final-config.difficulty.normal',
|
||||
defaultMessage: 'Normal',
|
||||
},
|
||||
difficultyHard: {
|
||||
id: 'creation-flow.modal.final-config.difficulty.hard',
|
||||
defaultMessage: 'Hard',
|
||||
},
|
||||
worldTypeLabel: {
|
||||
id: 'creation-flow.modal.final-config.world-type.label',
|
||||
defaultMessage: 'World type',
|
||||
},
|
||||
worldTypePlaceholder: {
|
||||
id: 'creation-flow.modal.final-config.world-type.placeholder',
|
||||
defaultMessage: 'Select world type',
|
||||
},
|
||||
worldTypeDefault: {
|
||||
id: 'creation-flow.modal.final-config.world-type.default',
|
||||
defaultMessage: 'Default',
|
||||
},
|
||||
worldTypeSuperflat: {
|
||||
id: 'creation-flow.modal.final-config.world-type.superflat',
|
||||
defaultMessage: 'Superflat',
|
||||
},
|
||||
worldTypeLargeBiomes: {
|
||||
id: 'creation-flow.modal.final-config.world-type.large-biomes',
|
||||
defaultMessage: 'Large Biomes',
|
||||
},
|
||||
worldTypeAmplified: {
|
||||
id: 'creation-flow.modal.final-config.world-type.amplified',
|
||||
defaultMessage: 'Amplified',
|
||||
},
|
||||
worldTypeSingleBiome: {
|
||||
id: 'creation-flow.modal.final-config.world-type.single-biome',
|
||||
defaultMessage: 'Single Biome',
|
||||
},
|
||||
worldSeedLabelWithOptional: {
|
||||
id: 'creation-flow.modal.final-config.world-seed.label-with-optional',
|
||||
defaultMessage: 'World seed <optional>(Optional)</optional>',
|
||||
},
|
||||
worldSeedPlaceholder: {
|
||||
id: 'creation-flow.modal.final-config.world-seed.placeholder',
|
||||
defaultMessage: 'Enter world seed',
|
||||
},
|
||||
worldSeedDescription: {
|
||||
id: 'creation-flow.modal.final-config.world-seed.description',
|
||||
defaultMessage: 'Leave blank for a random seed.',
|
||||
},
|
||||
additionalSettingsTitle: {
|
||||
id: 'creation-flow.modal.final-config.additional-settings.title',
|
||||
defaultMessage: 'Additional settings',
|
||||
},
|
||||
generateStructuresLabel: {
|
||||
id: 'creation-flow.modal.final-config.generate-structures.label',
|
||||
defaultMessage: 'Generate structures',
|
||||
},
|
||||
generateStructuresDescription: {
|
||||
id: 'creation-flow.modal.final-config.generate-structures.description',
|
||||
defaultMessage:
|
||||
'Controls whether villages, strongholds, and other structures generate in new chunks.',
|
||||
},
|
||||
generatorSettingsLabel: {
|
||||
id: 'creation-flow.modal.final-config.generator-settings.label',
|
||||
defaultMessage: 'Generator settings',
|
||||
},
|
||||
generatorSettingsPlaceholder: {
|
||||
id: 'creation-flow.modal.final-config.generator-settings.placeholder',
|
||||
defaultMessage: 'Select generator settings',
|
||||
},
|
||||
generatorSettingsDefault: {
|
||||
id: 'creation-flow.modal.final-config.generator-settings.default',
|
||||
defaultMessage: 'Default',
|
||||
},
|
||||
generatorSettingsFlat: {
|
||||
id: 'creation-flow.modal.final-config.generator-settings.flat',
|
||||
defaultMessage: 'Flat',
|
||||
},
|
||||
generatorSettingsCustom: {
|
||||
id: 'creation-flow.modal.final-config.generator-settings.custom',
|
||||
defaultMessage: 'Custom',
|
||||
},
|
||||
generatorSettingsJsonPlaceholder: {
|
||||
id: 'creation-flow.modal.final-config.generator-settings-json.placeholder',
|
||||
defaultMessage: 'Enter generator settings JSON',
|
||||
},
|
||||
generatorSettingsDescription: {
|
||||
id: 'creation-flow.modal.final-config.generator-settings.description',
|
||||
defaultMessage: 'Used for advanced world customization such as custom Superflat layers.',
|
||||
},
|
||||
beforeResetServerBackupName: {
|
||||
id: 'creation-flow.modal.final-config.backup.before-reset-server.name',
|
||||
defaultMessage: 'Before reset server',
|
||||
},
|
||||
})
|
||||
|
||||
const backupCreator = ref<InstanceType<typeof InlineBackupCreator> | null>(null)
|
||||
watch(backupCreator, (creator) => {
|
||||
@@ -189,17 +350,41 @@ watch(gamemode, (mode) => {
|
||||
const gamemodeItems: Gamemode[] = ['survival', 'creative', 'hardcore']
|
||||
const difficultyItems: Difficulty[] = ['peaceful', 'easy', 'normal', 'hard']
|
||||
|
||||
const worldTypeOptions: ComboboxOption<string>[] = [
|
||||
{ value: 'minecraft:normal', label: 'Default' },
|
||||
{ value: 'minecraft:flat', label: 'Superflat' },
|
||||
{ value: 'minecraft:large_biomes', label: 'Large Biomes' },
|
||||
{ value: 'minecraft:amplified', label: 'Amplified' },
|
||||
{ value: 'minecraft:single_biome_surface', label: 'Single Biome' },
|
||||
]
|
||||
function formatGamemodeLabel(mode: Gamemode): string {
|
||||
switch (mode) {
|
||||
case 'survival':
|
||||
return formatMessage(messages.gamemodeSurvival)
|
||||
case 'creative':
|
||||
return formatMessage(messages.gamemodeCreative)
|
||||
case 'hardcore':
|
||||
return formatMessage(messages.gamemodeHardcore)
|
||||
}
|
||||
}
|
||||
|
||||
const generatorSettingsOptions: ComboboxOption<GeneratorSettingsMode>[] = [
|
||||
{ value: 'default', label: 'Default' },
|
||||
{ value: 'flat', label: 'Flat' },
|
||||
{ value: 'custom', label: 'Custom' },
|
||||
]
|
||||
function formatDifficultyLabel(value: Difficulty): string {
|
||||
switch (value) {
|
||||
case 'peaceful':
|
||||
return formatMessage(messages.difficultyPeaceful)
|
||||
case 'easy':
|
||||
return formatMessage(messages.difficultyEasy)
|
||||
case 'normal':
|
||||
return formatMessage(messages.difficultyNormal)
|
||||
case 'hard':
|
||||
return formatMessage(messages.difficultyHard)
|
||||
}
|
||||
}
|
||||
|
||||
const worldTypeOptions = computed<ComboboxOption<string>[]>(() => [
|
||||
{ value: 'minecraft:normal', label: formatMessage(messages.worldTypeDefault) },
|
||||
{ value: 'minecraft:flat', label: formatMessage(messages.worldTypeSuperflat) },
|
||||
{ value: 'minecraft:large_biomes', label: formatMessage(messages.worldTypeLargeBiomes) },
|
||||
{ value: 'minecraft:amplified', label: formatMessage(messages.worldTypeAmplified) },
|
||||
{ value: 'minecraft:single_biome_surface', label: formatMessage(messages.worldTypeSingleBiome) },
|
||||
])
|
||||
|
||||
const generatorSettingsOptions = computed<ComboboxOption<GeneratorSettingsMode>[]>(() => [
|
||||
{ value: 'default', label: formatMessage(messages.generatorSettingsDefault) },
|
||||
{ value: 'flat', label: formatMessage(messages.generatorSettingsFlat) },
|
||||
{ value: 'custom', label: formatMessage(messages.generatorSettingsCustom) },
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast">Launcher instances</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.launcherInstancesTitle)
|
||||
}}</span>
|
||||
<ButtonStyled
|
||||
type="transparent"
|
||||
size="small"
|
||||
:class="{ invisible: totalSelectedCount === 0 }"
|
||||
>
|
||||
<button @click="clearAll">Clear all</button>
|
||||
<button @click="clearAll">{{ formatMessage(messages.clearAll) }}</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<template v-if="loading">
|
||||
<div class="flex items-center justify-center py-8 text-secondary text-sm">
|
||||
Detecting launcher instances...
|
||||
{{ formatMessage(messages.detectingLauncherInstances) }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -23,7 +25,7 @@
|
||||
v-if="ctx.importLaunchers.value.length > 0"
|
||||
v-model="ctx.importSearchQuery.value"
|
||||
:icon="SearchIcon"
|
||||
placeholder="Search instance name"
|
||||
:placeholder="formatMessage(messages.searchInstanceNamePlaceholder)"
|
||||
/>
|
||||
|
||||
<!-- Launcher sections -->
|
||||
@@ -75,7 +77,9 @@
|
||||
<!-- Add launcher path -->
|
||||
<div v-if="!showAddPath">
|
||||
<ButtonStyled>
|
||||
<button class="w-full !shadow-none" @click="showAddPath = true">Add launcher path</button>
|
||||
<button class="w-full !shadow-none" @click="showAddPath = true">
|
||||
{{ formatMessage(messages.addLauncherPath) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-2">
|
||||
@@ -83,10 +87,14 @@
|
||||
><button class="!shadow-none" @click="browseForLauncherPath">
|
||||
<FolderSearchIcon class="size-5" /></button
|
||||
></ButtonStyled>
|
||||
<StyledInput v-model="newLauncherPath" placeholder="Path to launcher..." class="flex-1" />
|
||||
<StyledInput
|
||||
v-model="newLauncherPath"
|
||||
:placeholder="formatMessage(messages.launcherPathPlaceholder)"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ButtonStyled>
|
||||
<button class="!shadow-none" :disabled="!newLauncherPath.trim()" @click="addLauncherPath">
|
||||
Add
|
||||
{{ formatMessage(messages.add) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -96,6 +104,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon, FolderSearchIcon, SearchIcon } from '@modrinth/assets'
|
||||
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { injectInstanceImport, injectNotificationManager } from '../../../../providers'
|
||||
@@ -109,6 +118,7 @@ import { injectCreationFlowContext } from '../creation-flow-context'
|
||||
const ctx = injectCreationFlowContext()
|
||||
const importProvider = injectInstanceImport()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const loading = ref(false)
|
||||
const expandedLaunchers = ref(new Set<string>())
|
||||
@@ -116,6 +126,49 @@ const expandedBeforeSearch = ref<Set<string> | null>(null)
|
||||
const showAddPath = ref(false)
|
||||
const newLauncherPath = ref('')
|
||||
|
||||
const messages = defineMessages({
|
||||
launcherInstancesTitle: {
|
||||
id: 'creation-flow.modal.import-instance.launcher-instances.title',
|
||||
defaultMessage: 'Launcher instances',
|
||||
},
|
||||
clearAll: {
|
||||
id: 'creation-flow.modal.import-instance.selection.clear-all',
|
||||
defaultMessage: 'Clear all',
|
||||
},
|
||||
detectingLauncherInstances: {
|
||||
id: 'creation-flow.modal.import-instance.detecting-launcher-instances',
|
||||
defaultMessage: 'Detecting launcher instances...',
|
||||
},
|
||||
searchInstanceNamePlaceholder: {
|
||||
id: 'creation-flow.modal.import-instance.search.placeholder',
|
||||
defaultMessage: 'Search instance name',
|
||||
},
|
||||
addLauncherPath: {
|
||||
id: 'creation-flow.modal.import-instance.launcher-path.add',
|
||||
defaultMessage: 'Add launcher path',
|
||||
},
|
||||
launcherPathPlaceholder: {
|
||||
id: 'creation-flow.modal.import-instance.launcher-path.placeholder',
|
||||
defaultMessage: 'Path to launcher...',
|
||||
},
|
||||
add: {
|
||||
id: 'creation-flow.modal.import-instance.action.add',
|
||||
defaultMessage: 'Add',
|
||||
},
|
||||
noInstancesFoundTitle: {
|
||||
id: 'creation-flow.modal.import-instance.notification.no-instances-found.title',
|
||||
defaultMessage: 'No instances found',
|
||||
},
|
||||
noInstancesFoundText: {
|
||||
id: 'creation-flow.modal.import-instance.notification.no-instances-found.text',
|
||||
defaultMessage: 'No importable instances were found at the specified path.',
|
||||
},
|
||||
customLauncherName: {
|
||||
id: 'creation-flow.modal.import-instance.custom-launcher.name',
|
||||
defaultMessage: 'Custom ({pathName})',
|
||||
},
|
||||
})
|
||||
|
||||
// Load detected launchers on mount
|
||||
onMounted(async () => {
|
||||
if (ctx.importLaunchers.value.length > 0) return // Already loaded
|
||||
@@ -261,13 +314,15 @@ async function addLauncherPath() {
|
||||
if (instances.length === 0) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'No instances found',
|
||||
text: `No importable instances were found at the specified path.`,
|
||||
title: formatMessage(messages.noInstancesFoundTitle),
|
||||
text: formatMessage(messages.noInstancesFoundText),
|
||||
})
|
||||
return
|
||||
}
|
||||
const launcher: ImportableLauncher = {
|
||||
name: `Custom (${path.split(/[\\/]/).pop() || path})`,
|
||||
name: formatMessage(messages.customLauncherName, {
|
||||
pathName: path.split(/[\\/]/).pop() || path,
|
||||
}),
|
||||
path,
|
||||
instances,
|
||||
}
|
||||
@@ -277,8 +332,8 @@ async function addLauncherPath() {
|
||||
} catch {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'No instances found',
|
||||
text: `No importable instances were found at the specified path.`,
|
||||
title: formatMessage(messages.noInstancesFoundTitle),
|
||||
text: formatMessage(messages.noInstancesFoundText),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<span class="font-semibold text-contrast">Already know the modpack you want to install?</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.knownModpackPrompt)
|
||||
}}</span>
|
||||
<Combobox
|
||||
v-model="ctx.modpackSearchProjectId.value"
|
||||
:options="ctx.modpackSearchOptions.value"
|
||||
searchable
|
||||
search-placeholder="Search for modpack"
|
||||
:no-options-message="searchLoading ? 'Loading...' : 'No results found'"
|
||||
:search-placeholder="formatMessage(messages.searchModpackPlaceholder)"
|
||||
:no-options-message="
|
||||
searchLoading
|
||||
? formatMessage(commonMessages.loadingLabel)
|
||||
: formatMessage(messages.noResultsFound)
|
||||
"
|
||||
:disable-search-filter="true"
|
||||
@search-input="(query) => handleSearch(query)"
|
||||
>
|
||||
@@ -18,20 +24,20 @@
|
||||
</Combobox>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-[1px] w-full flex-1 bg-surface-5" />
|
||||
<span class="text-sm text-secondary">or</span>
|
||||
<span class="text-sm text-secondary">{{ formatMessage(commonMessages.orLabel) }}</span>
|
||||
<div class="h-[1px] w-full flex-1 bg-surface-5" />
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="flex-1 !border-surface-4" @click="triggerFileInput">
|
||||
<ImportIcon />
|
||||
Import modpack
|
||||
{{ formatMessage(messages.importModpack) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button class="flex-1" @click="ctx.browseModpacks()">
|
||||
<CompassIcon />
|
||||
Browse modpacks
|
||||
{{ formatMessage(messages.browseModpacks) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -40,6 +46,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CompassIcon, ImportIcon, RightArrowIcon } from '@modrinth/assets'
|
||||
import { commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { defineAsyncComponent, h, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
@@ -52,9 +59,33 @@ import { injectCreationFlowContext } from '../creation-flow-context'
|
||||
const debug = useDebugLogger('ModpackStage')
|
||||
const ctx = injectCreationFlowContext()
|
||||
const filePicker = injectFilePicker()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const searchLoading = ref(false)
|
||||
|
||||
const messages = defineMessages({
|
||||
knownModpackPrompt: {
|
||||
id: 'creation-flow.modal.modpack.known-modpack.prompt',
|
||||
defaultMessage: 'Already know the modpack you want to install?',
|
||||
},
|
||||
searchModpackPlaceholder: {
|
||||
id: 'creation-flow.modal.modpack.search.placeholder',
|
||||
defaultMessage: 'Search for modpack',
|
||||
},
|
||||
noResultsFound: {
|
||||
id: 'creation-flow.modal.modpack.search.no-results',
|
||||
defaultMessage: 'No results found',
|
||||
},
|
||||
importModpack: {
|
||||
id: 'creation-flow.modal.modpack.action.import',
|
||||
defaultMessage: 'Import modpack',
|
||||
},
|
||||
browseModpacks: {
|
||||
id: 'creation-flow.modal.modpack.action.browse',
|
||||
defaultMessage: 'Browse modpacks',
|
||||
},
|
||||
})
|
||||
|
||||
function proceedWithModpack() {
|
||||
debug('proceedWithModpack:', {
|
||||
flowType: ctx.flowType,
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<span class="font-semibold text-contrast">
|
||||
{{
|
||||
ctx.flowType === 'instance'
|
||||
? 'Choose instance type'
|
||||
: ctx.flowType === 'server-onboarding' || ctx.flowType === 'reset-server'
|
||||
? 'Select installation type'
|
||||
: 'Select world type'
|
||||
}}
|
||||
{{ setupTypeTitle }}
|
||||
</span>
|
||||
|
||||
<!-- Instance flow options -->
|
||||
@@ -15,25 +9,25 @@
|
||||
<div class="flex flex-col gap-3">
|
||||
<BigOptionButton
|
||||
:icon="BoxesIcon"
|
||||
title="Custom setup"
|
||||
description="Start from scratch by picking a loader and game version."
|
||||
:title="formatMessage(messages.customSetupTitle)"
|
||||
:description="formatMessage(messages.customSetupDescription)"
|
||||
@click="setSetupType('custom')"
|
||||
/>
|
||||
<BigOptionButton
|
||||
:icon="PackageIcon"
|
||||
title="Modpack base"
|
||||
description="Use a popular modpack as your starting point."
|
||||
:title="formatMessage(messages.modpackBaseTitle)"
|
||||
:description="formatMessage(messages.modpackBaseDescription)"
|
||||
@click="setSetupType('modpack')"
|
||||
/>
|
||||
<BigOptionButton
|
||||
:icon="BoxImportIcon"
|
||||
title="Import instance"
|
||||
description="Import an instance from Prism, CurseForge, or similar."
|
||||
:title="formatMessage(messages.importInstanceTitle)"
|
||||
:description="formatMessage(messages.importInstanceDescription)"
|
||||
@click="ctx.setImportMode()"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm text-secondary">
|
||||
An instance is a Minecraft setup with a specific loader, version, and mods.
|
||||
{{ formatMessage(messages.instanceDescription) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -42,20 +36,20 @@
|
||||
<div class="flex flex-col gap-3">
|
||||
<BigOptionButton
|
||||
:icon="PackageIcon"
|
||||
title="Modpack base"
|
||||
description="Use a popular modpack as your starting point."
|
||||
:title="formatMessage(messages.modpackBaseTitle)"
|
||||
:description="formatMessage(messages.modpackBaseDescription)"
|
||||
@click="setSetupType('modpack')"
|
||||
/>
|
||||
<BigOptionButton
|
||||
:icon="BoxesIcon"
|
||||
title="Custom setup"
|
||||
description="Start from scratch by picking a loader and game version."
|
||||
:title="formatMessage(messages.customSetupTitle)"
|
||||
:description="formatMessage(messages.customSetupDescription)"
|
||||
@click="setSetupType('custom')"
|
||||
/>
|
||||
<BigOptionButton
|
||||
:icon="BoxIcon"
|
||||
title="Vanilla Minecraft"
|
||||
description="Classic Minecraft with no mods or plugins."
|
||||
:title="formatMessage(messages.vanillaMinecraftTitle)"
|
||||
:description="formatMessage(messages.vanillaMinecraftDescription)"
|
||||
@click="setSetupType('vanilla')"
|
||||
/>
|
||||
</div>
|
||||
@@ -65,6 +59,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BoxesIcon, BoxIcon, BoxImportIcon, PackageIcon } from '@modrinth/assets'
|
||||
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
|
||||
@@ -74,6 +70,68 @@ import { injectCreationFlowContext } from '../creation-flow-context'
|
||||
const debug = useDebugLogger('SetupTypeStage')
|
||||
const ctx = injectCreationFlowContext()
|
||||
const { setSetupType: _setSetupType } = ctx
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
instanceTypeTitle: {
|
||||
id: 'creation-flow.modal.setup-type.title.instance',
|
||||
defaultMessage: 'Choose instance type',
|
||||
},
|
||||
installationTypeTitle: {
|
||||
id: 'creation-flow.modal.setup-type.title.installation',
|
||||
defaultMessage: 'Select installation type',
|
||||
},
|
||||
worldTypeTitle: {
|
||||
id: 'creation-flow.modal.setup-type.title.world',
|
||||
defaultMessage: 'Select world type',
|
||||
},
|
||||
customSetupTitle: {
|
||||
id: 'creation-flow.modal.setup-type.option.custom-setup.title',
|
||||
defaultMessage: 'Custom setup',
|
||||
},
|
||||
customSetupDescription: {
|
||||
id: 'creation-flow.modal.setup-type.option.custom-setup.description',
|
||||
defaultMessage: 'Start from scratch by picking a loader and game version.',
|
||||
},
|
||||
modpackBaseTitle: {
|
||||
id: 'creation-flow.modal.setup-type.option.modpack-base.title',
|
||||
defaultMessage: 'Modpack base',
|
||||
},
|
||||
modpackBaseDescription: {
|
||||
id: 'creation-flow.modal.setup-type.option.modpack-base.description',
|
||||
defaultMessage: 'Use a popular modpack or upload one as your starting point.',
|
||||
},
|
||||
importInstanceTitle: {
|
||||
id: 'creation-flow.modal.setup-type.option.import-instance.title',
|
||||
defaultMessage: 'Import instance',
|
||||
},
|
||||
importInstanceDescription: {
|
||||
id: 'creation-flow.modal.setup-type.option.import-instance.description',
|
||||
defaultMessage: 'Import an instance from Prism, CurseForge, or similar.',
|
||||
},
|
||||
instanceDescription: {
|
||||
id: 'creation-flow.modal.setup-type.instance.description',
|
||||
defaultMessage: 'An instance is a Minecraft setup with a specific loader, version, and mods.',
|
||||
},
|
||||
vanillaMinecraftTitle: {
|
||||
id: 'creation-flow.modal.setup-type.option.vanilla-minecraft.title',
|
||||
defaultMessage: 'Vanilla Minecraft',
|
||||
},
|
||||
vanillaMinecraftDescription: {
|
||||
id: 'creation-flow.modal.setup-type.option.vanilla-minecraft.description',
|
||||
defaultMessage: 'Classic Minecraft with no mods or plugins.',
|
||||
},
|
||||
})
|
||||
|
||||
const setupTypeTitle = computed(() => {
|
||||
if (ctx.flowType === 'instance') {
|
||||
return formatMessage(messages.instanceTypeTitle)
|
||||
}
|
||||
if (ctx.flowType === 'server-onboarding' || ctx.flowType === 'reset-server') {
|
||||
return formatMessage(messages.installationTypeTitle)
|
||||
}
|
||||
return formatMessage(messages.worldTypeTitle)
|
||||
})
|
||||
|
||||
function setSetupType(type: 'modpack' | 'custom' | 'vanilla') {
|
||||
debug('selected:', type)
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import type { Archon, LauncherMeta } from '@modrinth/api-client'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, type ComputedRef, type Ref, ref, type ShallowRef, watch } from 'vue'
|
||||
import type { ComponentExposed } from 'vue-component-type-helpers'
|
||||
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
import {
|
||||
defineMessages,
|
||||
type MessageDescriptor,
|
||||
useVIntl,
|
||||
type VIntlFormatters,
|
||||
} from '#ui/composables/i18n'
|
||||
import { formatLoaderLabel } from '#ui/utils/loaders'
|
||||
|
||||
import { createContext } from '../../../providers'
|
||||
import { createContext, injectModrinthClient } from '../../../providers'
|
||||
import type { ImportableLauncher } from '../../../providers/instance-import'
|
||||
import type { MultiStageModal, StageConfigInput } from '../../base'
|
||||
import type { ComboboxOption } from '../../base/Combobox.vue'
|
||||
@@ -17,6 +24,74 @@ export type Gamemode = 'survival' | 'creative' | 'hardcore'
|
||||
export type Difficulty = 'peaceful' | 'easy' | 'normal' | 'hard'
|
||||
export type LoaderVersionType = 'stable' | 'latest' | 'other'
|
||||
export type GeneratorSettingsMode = 'default' | 'flat' | 'custom'
|
||||
export type LoaderManifestResolver = (loader: string) => Promise<LauncherMeta.Manifest.v0.Manifest>
|
||||
export interface LoaderVersionEntry {
|
||||
id: string
|
||||
stable: boolean
|
||||
}
|
||||
|
||||
const loaderManifestQueryKey = (loader: string) =>
|
||||
['creation-flow', 'loader-manifest', loader] as const
|
||||
const paperSupportedVersionsQueryKey = ['creation-flow', 'paper', 'supported-versions'] as const
|
||||
const purpurSupportedVersionsQueryKey = ['creation-flow', 'purpur', 'supported-versions'] as const
|
||||
|
||||
export const creationFlowMessages = defineMessages({
|
||||
createWorldTitle: {
|
||||
id: 'creation-flow.title.create-world',
|
||||
defaultMessage: 'Create world',
|
||||
},
|
||||
setUpServerTitle: {
|
||||
id: 'creation-flow.title.set-up-server',
|
||||
defaultMessage: 'Set up server',
|
||||
},
|
||||
resetServerTitle: {
|
||||
id: 'creation-flow.title.reset-server',
|
||||
defaultMessage: 'Reset server',
|
||||
},
|
||||
createInstanceTitle: {
|
||||
id: 'creation-flow.title.create-instance',
|
||||
defaultMessage: 'Create instance',
|
||||
},
|
||||
createWorldButton: {
|
||||
id: 'creation-flow.button.create-world',
|
||||
defaultMessage: 'Create world',
|
||||
},
|
||||
createInstanceButton: {
|
||||
id: 'creation-flow.button.create-instance',
|
||||
defaultMessage: 'Create instance',
|
||||
},
|
||||
setupServerButton: {
|
||||
id: 'creation-flow.button.setup-server',
|
||||
defaultMessage: 'Setup server',
|
||||
},
|
||||
finishButton: {
|
||||
id: 'creation-flow.button.finish',
|
||||
defaultMessage: 'Finish',
|
||||
},
|
||||
importInstanceTitle: {
|
||||
id: 'creation-flow.title.import-instance',
|
||||
defaultMessage: 'Import instance',
|
||||
},
|
||||
importButton: {
|
||||
id: 'creation-flow.button.import',
|
||||
defaultMessage: 'Import',
|
||||
},
|
||||
importInstancesButton: {
|
||||
id: 'creation-flow.button.import-instances',
|
||||
defaultMessage: 'Import {count, plural, one {# instance} other {# instances}}',
|
||||
},
|
||||
chooseModpackTitle: {
|
||||
id: 'creation-flow.title.choose-modpack',
|
||||
defaultMessage: 'Choose modpack',
|
||||
},
|
||||
})
|
||||
|
||||
export const flowTypeHeadingMessages: Record<FlowType, MessageDescriptor> = {
|
||||
world: creationFlowMessages.createWorldTitle,
|
||||
'server-onboarding': creationFlowMessages.setUpServerTitle,
|
||||
'reset-server': creationFlowMessages.resetServerTitle,
|
||||
instance: creationFlowMessages.createInstanceTitle,
|
||||
}
|
||||
|
||||
export interface ModpackSelection {
|
||||
projectId: string
|
||||
@@ -43,16 +118,10 @@ export interface ModpackSearchResult {
|
||||
limit: number
|
||||
}
|
||||
|
||||
export const flowTypeHeadings: Record<FlowType, string> = {
|
||||
world: 'Create world',
|
||||
'server-onboarding': 'Set up server',
|
||||
'reset-server': 'Reset server',
|
||||
instance: 'Create instance',
|
||||
}
|
||||
|
||||
export interface CreationFlowContextValue {
|
||||
// Flow
|
||||
flowType: FlowType
|
||||
formatMessage: VIntlFormatters['formatMessage']
|
||||
|
||||
// Configuration
|
||||
availableLoaders: string[]
|
||||
@@ -91,6 +160,9 @@ export interface CreationFlowContextValue {
|
||||
hideLoaderChips: ComputedRef<boolean>
|
||||
hideLoaderVersion: ComputedRef<boolean>
|
||||
showSnapshots: Ref<boolean>
|
||||
loaderVersionsCache: Ref<Record<string, { id: string; loaders: LoaderVersionEntry[] }[]>>
|
||||
paperSupportedVersions: Ref<Set<string> | null>
|
||||
purpurSupportedVersions: Ref<Set<string> | null>
|
||||
|
||||
// Modpack state
|
||||
modpackSelection: Ref<ModpackSelection | null>
|
||||
@@ -133,10 +205,13 @@ export interface CreationFlowContextValue {
|
||||
browseModpacks: () => void
|
||||
finish: () => void
|
||||
buildProperties: () => Archon.Content.v1.PropertiesFields
|
||||
fetchLoaderMetadata: (loader?: string | null) => Promise<void>
|
||||
prefetchLoaderMetadata: () => Promise<void>
|
||||
|
||||
// Platform-provided search
|
||||
searchModpacks: (query: string, limit?: number) => Promise<ModpackSearchResult>
|
||||
getProjectVersions: (projectId: string) => Promise<{ id: string }[]>
|
||||
getLoaderManifest: LoaderManifestResolver | null
|
||||
}
|
||||
|
||||
export const [injectCreationFlowContext, provideCreationFlowContext] =
|
||||
@@ -156,6 +231,7 @@ export interface CreationFlowOptions {
|
||||
onBack?: () => void
|
||||
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
|
||||
getProjectVersions?: (projectId: string) => Promise<{ id: string }[]>
|
||||
getLoaderManifest?: LoaderManifestResolver
|
||||
}
|
||||
|
||||
export function createCreationFlowContext(
|
||||
@@ -168,6 +244,9 @@ export function createCreationFlowContext(
|
||||
options: CreationFlowOptions = {},
|
||||
): CreationFlowContextValue {
|
||||
const debug = useDebugLogger('CreationFlow')
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const { formatMessage } = useVIntl()
|
||||
const availableLoaders = options.availableLoaders ?? ['fabric', 'neoforge', 'forge', 'quilt']
|
||||
const showSnapshotToggle = options.showSnapshotToggle ?? false
|
||||
const disableClose = options.disableClose ?? false
|
||||
@@ -175,6 +254,9 @@ export function createCreationFlowContext(
|
||||
const initialLoader = options.initialLoader ?? null
|
||||
const initialGameVersion = options.initialGameVersion ?? null
|
||||
const onBack = options.onBack ?? null
|
||||
const searchModpacks = options.searchModpacks!
|
||||
const getProjectVersions = options.getProjectVersions!
|
||||
const getLoaderManifest = options.getLoaderManifest ?? null
|
||||
|
||||
const setupType = ref<SetupType | null>(null)
|
||||
const isImportMode = ref(false)
|
||||
@@ -207,6 +289,11 @@ export function createCreationFlowContext(
|
||||
const loaderVersionType = ref<LoaderVersionType>('stable')
|
||||
const selectedLoaderVersion = ref<string | null>(null)
|
||||
const showSnapshots = ref(false)
|
||||
const loaderVersionsCache = ref<Record<string, { id: string; loaders: LoaderVersionEntry[] }[]>>(
|
||||
{},
|
||||
)
|
||||
const paperSupportedVersions = ref<Set<string> | null>(null)
|
||||
const purpurSupportedVersions = ref<Set<string> | null>(null)
|
||||
|
||||
const autoInstanceName = computed(() => {
|
||||
const loader = selectedLoader.value
|
||||
@@ -255,6 +342,83 @@ export function createCreationFlowContext(
|
||||
() => setupType.value === 'vanilla' || selectedLoader.value === 'vanilla',
|
||||
)
|
||||
|
||||
function toApiLoaderName(loader: string): string {
|
||||
return loader === 'neoforge' ? 'neo' : loader
|
||||
}
|
||||
|
||||
async function fetchLoaderManifest(loader: string) {
|
||||
const apiLoader = toApiLoaderName(loader)
|
||||
if (loaderVersionsCache.value[apiLoader]) return
|
||||
|
||||
try {
|
||||
const data = await queryClient.fetchQuery({
|
||||
queryKey: loaderManifestQueryKey(apiLoader),
|
||||
queryFn: async () =>
|
||||
(await getLoaderManifest?.(apiLoader)) ??
|
||||
(await client.launchermeta.manifest_v0.getManifest(apiLoader)),
|
||||
staleTime: Infinity,
|
||||
})
|
||||
loaderVersionsCache.value[apiLoader] = data.gameVersions
|
||||
debug('fetchLoaderManifest: loaded', apiLoader, 'gameVersions:', data.gameVersions.length)
|
||||
} catch (error) {
|
||||
debug('fetchLoaderManifest: failed', apiLoader, error)
|
||||
loaderVersionsCache.value[apiLoader] = []
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPaperSupportedVersions() {
|
||||
if (paperSupportedVersions.value) return
|
||||
try {
|
||||
paperSupportedVersions.value = await queryClient.fetchQuery({
|
||||
queryKey: paperSupportedVersionsQueryKey,
|
||||
queryFn: async () => {
|
||||
const project = await client.paper.versions_v3.getProject()
|
||||
return new Set(Object.values(project.versions).flat())
|
||||
},
|
||||
staleTime: Infinity,
|
||||
})
|
||||
} catch {
|
||||
paperSupportedVersions.value = new Set()
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPurpurSupportedVersions() {
|
||||
if (purpurSupportedVersions.value) return
|
||||
try {
|
||||
purpurSupportedVersions.value = await queryClient.fetchQuery({
|
||||
queryKey: purpurSupportedVersionsQueryKey,
|
||||
queryFn: async () => {
|
||||
const project = await client.purpur.versions_v2.getProject()
|
||||
return new Set(project.versions)
|
||||
},
|
||||
staleTime: Infinity,
|
||||
})
|
||||
} catch {
|
||||
purpurSupportedVersions.value = new Set()
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLoaderMetadata(loader?: string | null) {
|
||||
if (!loader || loader === 'vanilla') return
|
||||
if (loader === 'paper') {
|
||||
await fetchPaperSupportedVersions()
|
||||
return
|
||||
}
|
||||
if (loader === 'purpur') {
|
||||
await fetchPurpurSupportedVersions()
|
||||
return
|
||||
}
|
||||
await fetchLoaderManifest(loader)
|
||||
}
|
||||
|
||||
async function prefetchLoaderMetadata() {
|
||||
await Promise.allSettled(
|
||||
availableLoaders
|
||||
.filter((loader) => loader !== 'vanilla')
|
||||
.map((loader) => fetchLoaderMetadata(loader)),
|
||||
)
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
if (fetchExistingInstanceNames) {
|
||||
existingInstanceNames.value = await fetchExistingInstanceNames()
|
||||
@@ -370,15 +534,13 @@ export function createCreationFlowContext(
|
||||
return { known }
|
||||
}
|
||||
|
||||
const searchModpacks = options.searchModpacks!
|
||||
const getProjectVersions = options.getProjectVersions!
|
||||
|
||||
const resolvedStageConfigs = disableClose
|
||||
? stageConfigs.map((stage) => ({ ...stage, disableClose: true }))
|
||||
: stageConfigs
|
||||
|
||||
const contextValue: CreationFlowContextValue = {
|
||||
flowType,
|
||||
formatMessage,
|
||||
availableLoaders,
|
||||
showSnapshotToggle,
|
||||
disableClose,
|
||||
@@ -407,6 +569,9 @@ export function createCreationFlowContext(
|
||||
hideLoaderChips,
|
||||
hideLoaderVersion,
|
||||
showSnapshots,
|
||||
loaderVersionsCache,
|
||||
paperSupportedVersions,
|
||||
purpurSupportedVersions,
|
||||
modpackSelection,
|
||||
modpackFile,
|
||||
modpackFilePath,
|
||||
@@ -431,8 +596,11 @@ export function createCreationFlowContext(
|
||||
browseModpacks,
|
||||
finish,
|
||||
buildProperties,
|
||||
fetchLoaderMetadata,
|
||||
prefetchLoaderMetadata,
|
||||
searchModpacks,
|
||||
getProjectVersions,
|
||||
getLoaderManifest,
|
||||
}
|
||||
|
||||
return contextValue
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
createCreationFlowContext,
|
||||
type CreationFlowContextValue,
|
||||
type FlowType,
|
||||
type LoaderManifestResolver,
|
||||
type ModpackSearchResult,
|
||||
provideCreationFlowContext,
|
||||
} from './creation-flow-context'
|
||||
@@ -36,6 +37,7 @@ const props = withDefaults(
|
||||
fade?: 'standard' | 'warning' | 'danger'
|
||||
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
|
||||
getProjectVersions?: (projectId: string) => Promise<{ id: string }[]>
|
||||
getLoaderManifest?: LoaderManifestResolver
|
||||
}>(),
|
||||
{
|
||||
type: 'world',
|
||||
@@ -75,12 +77,14 @@ const ctx = createCreationFlowContext(
|
||||
onBack: props.onBack ?? undefined,
|
||||
searchModpacks: props.searchModpacks,
|
||||
getProjectVersions: props.getProjectVersions,
|
||||
getLoaderManifest: props.getLoaderManifest,
|
||||
},
|
||||
)
|
||||
provideCreationFlowContext(ctx)
|
||||
|
||||
async function show() {
|
||||
await ctx.reset()
|
||||
void ctx.prefetchLoaderMetadata()
|
||||
modal.value?.setStage(0)
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import { LeftArrowIcon, PlusIcon, RightArrowIcon } from '@modrinth/assets'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import type { StageConfigInput } from '../../../base'
|
||||
import CustomSetupStage from '../components/CustomSetupStage.vue'
|
||||
import { type CreationFlowContextValue, flowTypeHeadings } from '../creation-flow-context'
|
||||
import {
|
||||
type CreationFlowContextValue,
|
||||
creationFlowMessages,
|
||||
flowTypeHeadingMessages,
|
||||
} from '../creation-flow-context'
|
||||
|
||||
function isForwardBlocked(ctx: CreationFlowContextValue): boolean {
|
||||
if (!ctx.selectedGameVersion.value) return true
|
||||
if (!ctx.hideLoaderChips.value && !ctx.selectedLoader.value) return true
|
||||
if (
|
||||
!ctx.hideLoaderVersion.value &&
|
||||
ctx.loaderVersionType.value === 'other' &&
|
||||
!ctx.selectedLoaderVersion.value
|
||||
)
|
||||
return true
|
||||
if (!ctx.hideLoaderVersion.value && !ctx.selectedLoaderVersion.value) return true
|
||||
return false
|
||||
}
|
||||
|
||||
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||
id: 'custom-setup',
|
||||
title: (ctx) => flowTypeHeadings[ctx.flowType],
|
||||
title: (ctx) => ctx.formatMessage(flowTypeHeadingMessages[ctx.flowType]),
|
||||
stageContent: markRaw(CustomSetupStage),
|
||||
skip: (ctx) =>
|
||||
ctx.setupType.value === 'modpack' ||
|
||||
@@ -27,7 +28,7 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||
ctx.isImportMode.value,
|
||||
cannotNavigateForward: isForwardBlocked,
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Back',
|
||||
label: ctx.formatMessage(commonMessages.backButton),
|
||||
icon: LeftArrowIcon,
|
||||
onClick: () => ctx.modal.value?.setStage('setup-type'),
|
||||
}),
|
||||
@@ -41,7 +42,7 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||
|
||||
if (isInstance) {
|
||||
return {
|
||||
label: 'Create instance',
|
||||
label: ctx.formatMessage(creationFlowMessages.createInstanceButton),
|
||||
icon: PlusIcon,
|
||||
iconPosition: 'before' as const,
|
||||
color: 'brand' as const,
|
||||
@@ -52,7 +53,9 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||
}
|
||||
|
||||
return {
|
||||
label: goesToNextStage ? 'Continue' : 'Finish',
|
||||
label: ctx.formatMessage(
|
||||
goesToNextStage ? commonMessages.continueButton : creationFlowMessages.finishButton,
|
||||
),
|
||||
icon: goesToNextStage ? RightArrowIcon : null,
|
||||
iconPosition: 'after' as const,
|
||||
color: goesToNextStage ? undefined : ('brand' as const),
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { LeftArrowIcon, PlusIcon, RightArrowIcon } from '@modrinth/assets'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import type { StageConfigInput } from '../../../base'
|
||||
import FinalConfigStage from '../components/FinalConfigStage.vue'
|
||||
import { type CreationFlowContextValue, flowTypeHeadings } from '../creation-flow-context'
|
||||
import {
|
||||
type CreationFlowContextValue,
|
||||
creationFlowMessages,
|
||||
flowTypeHeadingMessages,
|
||||
} from '../creation-flow-context'
|
||||
|
||||
function isForwardBlocked(ctx: CreationFlowContextValue): boolean {
|
||||
if (ctx.flowType === 'world' && !ctx.worldName.value.trim()) return true
|
||||
@@ -13,12 +19,12 @@ function isForwardBlocked(ctx: CreationFlowContextValue): boolean {
|
||||
|
||||
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||
id: 'final-config',
|
||||
title: (ctx) => flowTypeHeadings[ctx.flowType],
|
||||
title: (ctx) => ctx.formatMessage(flowTypeHeadingMessages[ctx.flowType]),
|
||||
stageContent: markRaw(FinalConfigStage),
|
||||
skip: (ctx) => ctx.flowType === 'instance' || ctx.isImportMode.value,
|
||||
cannotNavigateForward: isForwardBlocked,
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Back',
|
||||
label: ctx.formatMessage(commonMessages.backButton),
|
||||
icon: LeftArrowIcon,
|
||||
onClick: () => {
|
||||
if (ctx.onBack) {
|
||||
@@ -33,14 +39,15 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||
const isOnboarding = ctx.flowType === 'server-onboarding'
|
||||
const isReset = ctx.flowType === 'reset-server'
|
||||
const isFinish = isWorld || isOnboarding || isReset
|
||||
const label = isWorld
|
||||
? ctx.formatMessage(creationFlowMessages.createWorldButton)
|
||||
: isReset
|
||||
? ctx.formatMessage(commonMessages.resetServerButton)
|
||||
: isOnboarding
|
||||
? ctx.formatMessage(creationFlowMessages.setupServerButton)
|
||||
: ctx.formatMessage(commonMessages.continueButton)
|
||||
return {
|
||||
label: isWorld
|
||||
? 'Create world'
|
||||
: isReset
|
||||
? 'Reset server'
|
||||
: isOnboarding
|
||||
? 'Setup server'
|
||||
: 'Continue',
|
||||
label,
|
||||
icon: isFinish ? PlusIcon : RightArrowIcon,
|
||||
iconPosition: isFinish ? ('before' as const) : ('after' as const),
|
||||
color: isReset ? ('red' as const) : isFinish ? ('brand' as const) : undefined,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { DownloadIcon, LeftArrowIcon } from '@modrinth/assets'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import type { StageConfigInput } from '../../../base'
|
||||
import ImportInstanceStage from '../components/ImportInstanceStage.vue'
|
||||
import type { CreationFlowContextValue } from '../creation-flow-context'
|
||||
import { type CreationFlowContextValue, creationFlowMessages } from '../creation-flow-context'
|
||||
|
||||
function getSelectedCount(ctx: CreationFlowContextValue): number {
|
||||
let count = 0
|
||||
@@ -15,11 +17,11 @@ function getSelectedCount(ctx: CreationFlowContextValue): number {
|
||||
|
||||
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||
id: 'import-instance',
|
||||
title: 'Import instance',
|
||||
title: (ctx) => ctx.formatMessage(creationFlowMessages.importInstanceTitle),
|
||||
stageContent: markRaw(ImportInstanceStage),
|
||||
skip: (ctx) => !ctx.isImportMode.value,
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Back',
|
||||
label: ctx.formatMessage(commonMessages.backButton),
|
||||
icon: LeftArrowIcon,
|
||||
onClick: () => {
|
||||
ctx.isImportMode.value = false
|
||||
@@ -29,7 +31,10 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||
rightButtonConfig: (ctx) => {
|
||||
const count = getSelectedCount(ctx)
|
||||
return {
|
||||
label: count > 0 ? `Import ${count} instance${count !== 1 ? 's' : ''}` : 'Import',
|
||||
label:
|
||||
count > 0
|
||||
? ctx.formatMessage(creationFlowMessages.importInstancesButton, { count })
|
||||
: ctx.formatMessage(creationFlowMessages.importButton),
|
||||
icon: DownloadIcon,
|
||||
iconPosition: 'before' as const,
|
||||
color: 'brand' as const,
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { LeftArrowIcon } from '@modrinth/assets'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import type { StageConfigInput } from '../../../base'
|
||||
import ModpackStage from '../components/ModpackStage.vue'
|
||||
import type { CreationFlowContextValue } from '../creation-flow-context'
|
||||
import { type CreationFlowContextValue, creationFlowMessages } from '../creation-flow-context'
|
||||
|
||||
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||
id: 'modpack',
|
||||
title: 'Choose modpack',
|
||||
title: (ctx) => ctx.formatMessage(creationFlowMessages.chooseModpackTitle),
|
||||
stageContent: markRaw(ModpackStage),
|
||||
skip: (ctx) => ctx.setupType.value !== 'modpack' || ctx.isImportMode.value,
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Back',
|
||||
label: ctx.formatMessage(commonMessages.backButton),
|
||||
icon: LeftArrowIcon,
|
||||
onClick: () => ctx.modal.value?.setStage('setup-type'),
|
||||
}),
|
||||
|
||||
@@ -2,11 +2,11 @@ import { markRaw } from 'vue'
|
||||
|
||||
import type { StageConfigInput } from '../../../base'
|
||||
import SetupTypeStage from '../components/SetupTypeStage.vue'
|
||||
import { type CreationFlowContextValue, flowTypeHeadings } from '../creation-flow-context'
|
||||
import { type CreationFlowContextValue, flowTypeHeadingMessages } from '../creation-flow-context'
|
||||
|
||||
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||
id: 'setup-type',
|
||||
title: (ctx) => flowTypeHeadings[ctx.flowType],
|
||||
title: (ctx) => ctx.formatMessage(flowTypeHeadingMessages[ctx.flowType]),
|
||||
stageContent: markRaw(SetupTypeStage),
|
||||
leftButtonConfig: null,
|
||||
rightButtonConfig: null,
|
||||
|
||||
@@ -16,7 +16,7 @@ import { computed } from 'vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const { currentMember, projectV2, projectV3, refreshProject } = injectProjectPageContext()
|
||||
const { currentMember, projectV2, projectV3, invalidate } = injectProjectPageContext()
|
||||
const { handleError } = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
|
||||
@@ -53,7 +53,7 @@ const { saved, current, saving, reset, save } = useSavable(
|
||||
environment,
|
||||
side_types_migration_review_status: 'reviewed',
|
||||
})
|
||||
await refreshProject()
|
||||
await invalidate()
|
||||
reset()
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
|
||||
@@ -207,6 +207,8 @@ function emitReinstall(args?: { loader: string; lVersion: string; mVersion: stri
|
||||
}
|
||||
|
||||
function show() {
|
||||
void creationFlowRef.value?.ctx?.fetchLoaderMetadata('paper')
|
||||
void creationFlowRef.value?.ctx?.fetchLoaderMetadata('purpur')
|
||||
creationFlowRef.value?.show()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<template>
|
||||
<div class="mx-auto flex w-fit flex-col items-start gap-4 mt-16 max-w-[500px]">
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<h2 class="m-0 text-2xl font-semibold text-contrast">Welcome to Modrinth Hosting</h2>
|
||||
<h2 class="m-0 text-2xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.welcomeTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 text-base text-secondary">
|
||||
Your server is ready. Here's what you need to do to start playing!
|
||||
{{ formatMessage(messages.welcomeDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<span class="text-base font-medium text-secondary"> Setup your server (~2mins) </span>
|
||||
<span class="text-base font-medium text-secondary">
|
||||
{{ formatMessage(messages.setupStepsHeading) }}
|
||||
</span>
|
||||
|
||||
<div class="rounded-[20px] border border-solid border-surface-5 bg-surface-3 p-5">
|
||||
<div class="flex flex-col">
|
||||
@@ -41,11 +45,13 @@
|
||||
<ButtonStyled v-if="uploading" size="large">
|
||||
<button class="ml-auto" disabled>
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
Uploading ({{ uploadPercent }}%)
|
||||
{{ formatMessage(messages.uploadingProgress, { percent: uploadPercent }) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else color="brand" size="large">
|
||||
<button class="ml-auto" @click="openModal">Setup server <RightArrowIcon /></button>
|
||||
<button class="ml-auto" @click="openModal">
|
||||
{{ formatMessage(messages.setupServerButton) }} <RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +72,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import { GlobeIcon, PackageIcon, RightArrowIcon, SpinnerIcon, UsersIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectModrinthClient, injectNotificationManager } from '@modrinth/ui'
|
||||
import {
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -77,6 +89,73 @@ import { injectModrinthServerContext } from '#ui/providers'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
welcomeTitle: {
|
||||
id: 'servers.setup.onboarding.welcome.title',
|
||||
defaultMessage: 'Welcome to Modrinth Hosting',
|
||||
},
|
||||
welcomeDescription: {
|
||||
id: 'servers.setup.onboarding.welcome.description',
|
||||
defaultMessage: "Your server is ready. Here's what you need to do to start playing!",
|
||||
},
|
||||
setupStepsHeading: {
|
||||
id: 'servers.setup.onboarding.steps.heading',
|
||||
defaultMessage: 'Setup your server (~2mins)',
|
||||
},
|
||||
uploadingProgress: {
|
||||
id: 'servers.setup.onboarding.uploading.progress',
|
||||
defaultMessage: 'Uploading ({percent, number}%)',
|
||||
},
|
||||
setupServerButton: {
|
||||
id: 'servers.setup.onboarding.setup-server.button',
|
||||
defaultMessage: 'Setup server',
|
||||
},
|
||||
modpackUploadFailedTitle: {
|
||||
id: 'servers.setup.onboarding.modpack-upload-failed.title',
|
||||
defaultMessage: 'Modpack upload failed',
|
||||
},
|
||||
modpackUploadFailedText: {
|
||||
id: 'servers.setup.onboarding.modpack-upload-failed.text',
|
||||
defaultMessage: 'An unexpected error occurred while uploading. Please try again later.',
|
||||
},
|
||||
installationFailedTitle: {
|
||||
id: 'servers.setup.onboarding.installation-failed.title',
|
||||
defaultMessage: 'Installation failed',
|
||||
},
|
||||
installationFailedText: {
|
||||
id: 'servers.setup.onboarding.installation-failed.text',
|
||||
defaultMessage: 'An unexpected error occurred while installing. Please try again later.',
|
||||
},
|
||||
chooseWhatToPlayTitle: {
|
||||
id: 'servers.setup.onboarding.step.choose.title',
|
||||
defaultMessage: 'Choose what to play',
|
||||
},
|
||||
chooseWhatToPlayDescription: {
|
||||
id: 'servers.setup.onboarding.step.choose.description',
|
||||
defaultMessage:
|
||||
'Pick your favorite modpack from Modrinth, or choose a loader and add the mods you want.',
|
||||
},
|
||||
configureWorldTitle: {
|
||||
id: 'servers.setup.onboarding.step.configure-world.title',
|
||||
defaultMessage: 'Configure your world',
|
||||
},
|
||||
configureWorldDescription: {
|
||||
id: 'servers.setup.onboarding.step.configure-world.description',
|
||||
defaultMessage:
|
||||
'Set up your world just like singleplayer. Choose your gamemode and world seed.',
|
||||
},
|
||||
inviteFriendsTitle: {
|
||||
id: 'servers.setup.onboarding.step.invite-friends.title',
|
||||
defaultMessage: 'Invite your friends',
|
||||
},
|
||||
inviteFriendsDescription: {
|
||||
id: 'servers.setup.onboarding.step.invite-friends.description',
|
||||
defaultMessage:
|
||||
"Share your server with friends by copying the address and letting them know which mods they'll need to join.",
|
||||
},
|
||||
})
|
||||
|
||||
async function searchModpacks(query: string, limit: number = 10) {
|
||||
return client.labrinth.projects_v2.search({
|
||||
@@ -209,8 +288,8 @@ const onCreate = async (config: CreationFlowContextValue) => {
|
||||
await finalizeSetup()
|
||||
} catch {
|
||||
addNotification({
|
||||
title: 'Modpack upload failed',
|
||||
text: 'An unexpected error occurred while uploading. Please try again later.',
|
||||
title: formatMessage(messages.modpackUploadFailedTitle),
|
||||
text: formatMessage(messages.modpackUploadFailedText),
|
||||
type: 'error',
|
||||
})
|
||||
config.loading.value = false
|
||||
@@ -252,31 +331,29 @@ const onCreate = async (config: CreationFlowContextValue) => {
|
||||
await finalizeSetup()
|
||||
} catch {
|
||||
addNotification({
|
||||
title: 'Installation failed',
|
||||
text: 'An unexpected error occurred while installing. Please try again later.',
|
||||
title: formatMessage(messages.installationFailedTitle),
|
||||
text: formatMessage(messages.installationFailedText),
|
||||
type: 'error',
|
||||
})
|
||||
config.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const steps = [
|
||||
const steps = computed(() => [
|
||||
{
|
||||
icon: PackageIcon,
|
||||
title: 'Choose what to play',
|
||||
description:
|
||||
'Pick your favorite modpack from Modrinth, or choose a loader and add the mods you want.',
|
||||
title: formatMessage(messages.chooseWhatToPlayTitle),
|
||||
description: formatMessage(messages.chooseWhatToPlayDescription),
|
||||
},
|
||||
{
|
||||
icon: GlobeIcon,
|
||||
title: 'Configure your world',
|
||||
description: 'Set up your world just like singleplayer. Choose your gamemode and world seed.',
|
||||
title: formatMessage(messages.configureWorldTitle),
|
||||
description: formatMessage(messages.configureWorldDescription),
|
||||
},
|
||||
{
|
||||
icon: UsersIcon,
|
||||
title: 'Invite your friends',
|
||||
description:
|
||||
"Share your server with friends by copying the address and letting them know which mods they'll need to join.",
|
||||
title: formatMessage(messages.inviteFriendsTitle),
|
||||
description: formatMessage(messages.inviteFriendsDescription),
|
||||
},
|
||||
]
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -491,6 +491,276 @@
|
||||
"content.selection-bar.selected-count-simple": {
|
||||
"defaultMessage": "{count, number} selected"
|
||||
},
|
||||
"creation-flow.button.create-instance": {
|
||||
"defaultMessage": "Create instance"
|
||||
},
|
||||
"creation-flow.button.create-world": {
|
||||
"defaultMessage": "Create world"
|
||||
},
|
||||
"creation-flow.button.finish": {
|
||||
"defaultMessage": "Finish"
|
||||
},
|
||||
"creation-flow.button.import": {
|
||||
"defaultMessage": "Import"
|
||||
},
|
||||
"creation-flow.button.import-instances": {
|
||||
"defaultMessage": "Import {count, plural, one {# instance} other {# instances}}"
|
||||
},
|
||||
"creation-flow.button.setup-server": {
|
||||
"defaultMessage": "Setup server"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.build-number.label": {
|
||||
"defaultMessage": "Build number"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.build-number.placeholder": {
|
||||
"defaultMessage": "Select build number"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.build-number.search-placeholder": {
|
||||
"defaultMessage": "Search build number..."
|
||||
},
|
||||
"creation-flow.modal.custom-setup.content-loader.label": {
|
||||
"defaultMessage": "Content loader"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.game-version.placeholder": {
|
||||
"defaultMessage": "Select game version"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
|
||||
"defaultMessage": "Search game version..."
|
||||
},
|
||||
"creation-flow.modal.custom-setup.icon.remove": {
|
||||
"defaultMessage": "Remove icon"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.icon.select": {
|
||||
"defaultMessage": "Select icon"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.loader-version-type.latest": {
|
||||
"defaultMessage": "Latest"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.loader-version-type.other": {
|
||||
"defaultMessage": "Other"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.loader-version-type.stable": {
|
||||
"defaultMessage": "Stable"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.loader-version.label": {
|
||||
"defaultMessage": "Loader version"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.loader-version.placeholder": {
|
||||
"defaultMessage": "Select loader version"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.loader-version.search-placeholder": {
|
||||
"defaultMessage": "Search loader version..."
|
||||
},
|
||||
"creation-flow.modal.custom-setup.loader.label": {
|
||||
"defaultMessage": "Loader"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.name.label": {
|
||||
"defaultMessage": "Name"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.name.placeholder": {
|
||||
"defaultMessage": "Enter instance name"
|
||||
},
|
||||
"creation-flow.modal.custom-setup.options.no-versions-available": {
|
||||
"defaultMessage": "No versions available"
|
||||
},
|
||||
"creation-flow.modal.final-config.additional-settings.title": {
|
||||
"defaultMessage": "Additional settings"
|
||||
},
|
||||
"creation-flow.modal.final-config.backup.before-reset-server.name": {
|
||||
"defaultMessage": "Before reset server"
|
||||
},
|
||||
"creation-flow.modal.final-config.difficulty.easy": {
|
||||
"defaultMessage": "Easy"
|
||||
},
|
||||
"creation-flow.modal.final-config.difficulty.hard": {
|
||||
"defaultMessage": "Hard"
|
||||
},
|
||||
"creation-flow.modal.final-config.difficulty.label": {
|
||||
"defaultMessage": "Difficulty"
|
||||
},
|
||||
"creation-flow.modal.final-config.difficulty.normal": {
|
||||
"defaultMessage": "Normal"
|
||||
},
|
||||
"creation-flow.modal.final-config.difficulty.peaceful": {
|
||||
"defaultMessage": "Peaceful"
|
||||
},
|
||||
"creation-flow.modal.final-config.game-version.placeholder": {
|
||||
"defaultMessage": "Select game version"
|
||||
},
|
||||
"creation-flow.modal.final-config.gamemode.creative": {
|
||||
"defaultMessage": "Creative"
|
||||
},
|
||||
"creation-flow.modal.final-config.gamemode.hardcore": {
|
||||
"defaultMessage": "Hardcore"
|
||||
},
|
||||
"creation-flow.modal.final-config.gamemode.label": {
|
||||
"defaultMessage": "Gamemode"
|
||||
},
|
||||
"creation-flow.modal.final-config.gamemode.survival": {
|
||||
"defaultMessage": "Survival"
|
||||
},
|
||||
"creation-flow.modal.final-config.generate-structures.description": {
|
||||
"defaultMessage": "Controls whether villages, strongholds, and other structures generate in new chunks."
|
||||
},
|
||||
"creation-flow.modal.final-config.generate-structures.label": {
|
||||
"defaultMessage": "Generate structures"
|
||||
},
|
||||
"creation-flow.modal.final-config.generator-settings-json.placeholder": {
|
||||
"defaultMessage": "Enter generator settings JSON"
|
||||
},
|
||||
"creation-flow.modal.final-config.generator-settings.custom": {
|
||||
"defaultMessage": "Custom"
|
||||
},
|
||||
"creation-flow.modal.final-config.generator-settings.default": {
|
||||
"defaultMessage": "Default"
|
||||
},
|
||||
"creation-flow.modal.final-config.generator-settings.description": {
|
||||
"defaultMessage": "Used for advanced world customization such as custom Superflat layers."
|
||||
},
|
||||
"creation-flow.modal.final-config.generator-settings.flat": {
|
||||
"defaultMessage": "Flat"
|
||||
},
|
||||
"creation-flow.modal.final-config.generator-settings.label": {
|
||||
"defaultMessage": "Generator settings"
|
||||
},
|
||||
"creation-flow.modal.final-config.generator-settings.placeholder": {
|
||||
"defaultMessage": "Select generator settings"
|
||||
},
|
||||
"creation-flow.modal.final-config.world-name.label": {
|
||||
"defaultMessage": "World name"
|
||||
},
|
||||
"creation-flow.modal.final-config.world-name.placeholder": {
|
||||
"defaultMessage": "Enter world name"
|
||||
},
|
||||
"creation-flow.modal.final-config.world-seed.description": {
|
||||
"defaultMessage": "Leave blank for a random seed."
|
||||
},
|
||||
"creation-flow.modal.final-config.world-seed.label-with-optional": {
|
||||
"defaultMessage": "World seed <optional>(Optional)</optional>"
|
||||
},
|
||||
"creation-flow.modal.final-config.world-seed.placeholder": {
|
||||
"defaultMessage": "Enter world seed"
|
||||
},
|
||||
"creation-flow.modal.final-config.world-type.amplified": {
|
||||
"defaultMessage": "Amplified"
|
||||
},
|
||||
"creation-flow.modal.final-config.world-type.default": {
|
||||
"defaultMessage": "Default"
|
||||
},
|
||||
"creation-flow.modal.final-config.world-type.label": {
|
||||
"defaultMessage": "World type"
|
||||
},
|
||||
"creation-flow.modal.final-config.world-type.large-biomes": {
|
||||
"defaultMessage": "Large Biomes"
|
||||
},
|
||||
"creation-flow.modal.final-config.world-type.placeholder": {
|
||||
"defaultMessage": "Select world type"
|
||||
},
|
||||
"creation-flow.modal.final-config.world-type.single-biome": {
|
||||
"defaultMessage": "Single Biome"
|
||||
},
|
||||
"creation-flow.modal.final-config.world-type.superflat": {
|
||||
"defaultMessage": "Superflat"
|
||||
},
|
||||
"creation-flow.modal.import-instance.action.add": {
|
||||
"defaultMessage": "Add"
|
||||
},
|
||||
"creation-flow.modal.import-instance.custom-launcher.name": {
|
||||
"defaultMessage": "Custom ({pathName})"
|
||||
},
|
||||
"creation-flow.modal.import-instance.detecting-launcher-instances": {
|
||||
"defaultMessage": "Detecting launcher instances..."
|
||||
},
|
||||
"creation-flow.modal.import-instance.launcher-instances.title": {
|
||||
"defaultMessage": "Launcher instances"
|
||||
},
|
||||
"creation-flow.modal.import-instance.launcher-path.add": {
|
||||
"defaultMessage": "Add launcher path"
|
||||
},
|
||||
"creation-flow.modal.import-instance.launcher-path.placeholder": {
|
||||
"defaultMessage": "Path to launcher..."
|
||||
},
|
||||
"creation-flow.modal.import-instance.notification.no-instances-found.text": {
|
||||
"defaultMessage": "No importable instances were found at the specified path."
|
||||
},
|
||||
"creation-flow.modal.import-instance.notification.no-instances-found.title": {
|
||||
"defaultMessage": "No instances found"
|
||||
},
|
||||
"creation-flow.modal.import-instance.search.placeholder": {
|
||||
"defaultMessage": "Search instance name"
|
||||
},
|
||||
"creation-flow.modal.import-instance.selection.clear-all": {
|
||||
"defaultMessage": "Clear all"
|
||||
},
|
||||
"creation-flow.modal.modpack.action.browse": {
|
||||
"defaultMessage": "Browse modpacks"
|
||||
},
|
||||
"creation-flow.modal.modpack.action.import": {
|
||||
"defaultMessage": "Import modpack"
|
||||
},
|
||||
"creation-flow.modal.modpack.known-modpack.prompt": {
|
||||
"defaultMessage": "Already know the modpack you want to install?"
|
||||
},
|
||||
"creation-flow.modal.modpack.search.no-results": {
|
||||
"defaultMessage": "No results found"
|
||||
},
|
||||
"creation-flow.modal.modpack.search.placeholder": {
|
||||
"defaultMessage": "Search for modpack"
|
||||
},
|
||||
"creation-flow.modal.setup-type.instance.description": {
|
||||
"defaultMessage": "An instance is a Minecraft setup with a specific loader, version, and mods."
|
||||
},
|
||||
"creation-flow.modal.setup-type.option.custom-setup.description": {
|
||||
"defaultMessage": "Start from scratch by picking a loader and game version."
|
||||
},
|
||||
"creation-flow.modal.setup-type.option.custom-setup.title": {
|
||||
"defaultMessage": "Custom setup"
|
||||
},
|
||||
"creation-flow.modal.setup-type.option.import-instance.description": {
|
||||
"defaultMessage": "Import an instance from Prism, CurseForge, or similar."
|
||||
},
|
||||
"creation-flow.modal.setup-type.option.import-instance.title": {
|
||||
"defaultMessage": "Import instance"
|
||||
},
|
||||
"creation-flow.modal.setup-type.option.modpack-base.description": {
|
||||
"defaultMessage": "Use a popular modpack or upload one as your starting point."
|
||||
},
|
||||
"creation-flow.modal.setup-type.option.modpack-base.title": {
|
||||
"defaultMessage": "Modpack base"
|
||||
},
|
||||
"creation-flow.modal.setup-type.option.vanilla-minecraft.description": {
|
||||
"defaultMessage": "Classic Minecraft with no mods or plugins."
|
||||
},
|
||||
"creation-flow.modal.setup-type.option.vanilla-minecraft.title": {
|
||||
"defaultMessage": "Vanilla Minecraft"
|
||||
},
|
||||
"creation-flow.modal.setup-type.title.installation": {
|
||||
"defaultMessage": "Select installation type"
|
||||
},
|
||||
"creation-flow.modal.setup-type.title.instance": {
|
||||
"defaultMessage": "Choose instance type"
|
||||
},
|
||||
"creation-flow.modal.setup-type.title.world": {
|
||||
"defaultMessage": "Select world type"
|
||||
},
|
||||
"creation-flow.title.choose-modpack": {
|
||||
"defaultMessage": "Choose modpack"
|
||||
},
|
||||
"creation-flow.title.create-instance": {
|
||||
"defaultMessage": "Create instance"
|
||||
},
|
||||
"creation-flow.title.create-world": {
|
||||
"defaultMessage": "Create world"
|
||||
},
|
||||
"creation-flow.title.import-instance": {
|
||||
"defaultMessage": "Import instance"
|
||||
},
|
||||
"creation-flow.title.reset-server": {
|
||||
"defaultMessage": "Reset server"
|
||||
},
|
||||
"creation-flow.title.set-up-server": {
|
||||
"defaultMessage": "Set up server"
|
||||
},
|
||||
"files.conflict-modal.header": {
|
||||
"defaultMessage": "Extract summary"
|
||||
},
|
||||
@@ -3062,6 +3332,51 @@
|
||||
"servers.region.western-europe": {
|
||||
"defaultMessage": "Western Europe"
|
||||
},
|
||||
"servers.setup.onboarding.installation-failed.text": {
|
||||
"defaultMessage": "An unexpected error occurred while installing. Please try again later."
|
||||
},
|
||||
"servers.setup.onboarding.installation-failed.title": {
|
||||
"defaultMessage": "Installation failed"
|
||||
},
|
||||
"servers.setup.onboarding.modpack-upload-failed.text": {
|
||||
"defaultMessage": "An unexpected error occurred while uploading. Please try again later."
|
||||
},
|
||||
"servers.setup.onboarding.modpack-upload-failed.title": {
|
||||
"defaultMessage": "Modpack upload failed"
|
||||
},
|
||||
"servers.setup.onboarding.setup-server.button": {
|
||||
"defaultMessage": "Setup server"
|
||||
},
|
||||
"servers.setup.onboarding.step.choose.description": {
|
||||
"defaultMessage": "Pick your favorite modpack from Modrinth, or choose a loader and add the mods you want."
|
||||
},
|
||||
"servers.setup.onboarding.step.choose.title": {
|
||||
"defaultMessage": "Choose what to play"
|
||||
},
|
||||
"servers.setup.onboarding.step.configure-world.description": {
|
||||
"defaultMessage": "Set up your world just like singleplayer. Choose your gamemode and world seed."
|
||||
},
|
||||
"servers.setup.onboarding.step.configure-world.title": {
|
||||
"defaultMessage": "Configure your world"
|
||||
},
|
||||
"servers.setup.onboarding.step.invite-friends.description": {
|
||||
"defaultMessage": "Share your server with friends by copying the address and letting them know which mods they'll need to join."
|
||||
},
|
||||
"servers.setup.onboarding.step.invite-friends.title": {
|
||||
"defaultMessage": "Invite your friends"
|
||||
},
|
||||
"servers.setup.onboarding.steps.heading": {
|
||||
"defaultMessage": "Setup your server (~2mins)"
|
||||
},
|
||||
"servers.setup.onboarding.uploading.progress": {
|
||||
"defaultMessage": "Uploading ({percent, number}%)"
|
||||
},
|
||||
"servers.setup.onboarding.welcome.description": {
|
||||
"defaultMessage": "Your server is ready. Here's what you need to do to start playing!"
|
||||
},
|
||||
"servers.setup.onboarding.welcome.title": {
|
||||
"defaultMessage": "Welcome to Modrinth Hosting"
|
||||
},
|
||||
"servers.setup.rate-limit.text": {
|
||||
"defaultMessage": "You are being rate limited. Please try again later."
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user