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:
@@ -176,6 +176,7 @@ const {
|
|||||||
handleBrowseModpacks,
|
handleBrowseModpacks,
|
||||||
searchModpacks,
|
searchModpacks,
|
||||||
getProjectVersions,
|
getProjectVersions,
|
||||||
|
getLoaderManifest,
|
||||||
setModpackAlreadyInstalledModal,
|
setModpackAlreadyInstalledModal,
|
||||||
handleModpackDuplicateCreateAnyway,
|
handleModpackDuplicateCreateAnyway,
|
||||||
handleModpackDuplicateGoToInstance,
|
handleModpackDuplicateGoToInstance,
|
||||||
@@ -1108,6 +1109,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
|||||||
:fetch-existing-instance-names="fetchExistingInstanceNames"
|
:fetch-existing-instance-names="fetchExistingInstanceNames"
|
||||||
:search-modpacks="searchModpacks"
|
:search-modpacks="searchModpacks"
|
||||||
:get-project-versions="getProjectVersions"
|
:get-project-versions="getProjectVersions"
|
||||||
|
:get-loader-manifest="getLoaderManifest"
|
||||||
@create="handleCreate"
|
@create="handleCreate"
|
||||||
@browse-modpacks="handleBrowseModpacks"
|
@browse-modpacks="handleBrowseModpacks"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -38,6 +38,18 @@
|
|||||||
"app.browse.install-content-to-instance": {
|
"app.browse.install-content-to-instance": {
|
||||||
"message": "Install content to instance"
|
"message": "Install content to instance"
|
||||||
},
|
},
|
||||||
|
"app.browse.project-type.modpacks": {
|
||||||
|
"message": "Modpacks"
|
||||||
|
},
|
||||||
|
"app.browse.server.install": {
|
||||||
|
"message": "Install"
|
||||||
|
},
|
||||||
|
"app.browse.server.installed": {
|
||||||
|
"message": "Installed"
|
||||||
|
},
|
||||||
|
"app.browse.server.installing": {
|
||||||
|
"message": "Installing"
|
||||||
|
},
|
||||||
"app.export-modal.description-placeholder": {
|
"app.export-modal.description-placeholder": {
|
||||||
"message": "Enter modpack description..."
|
"message": "Enter modpack description..."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
|||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
import { get_project_v3, get_search_results_v3 } from '@/helpers/cache.js'
|
import { get_project_v3, get_search_results_v3 } from '@/helpers/cache.js'
|
||||||
import { process_listener } from '@/helpers/events'
|
import { process_listener } from '@/helpers/events'
|
||||||
|
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata'
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
import { get_by_profile_path } from '@/helpers/process'
|
||||||
import {
|
import {
|
||||||
get as getInstance,
|
get as getInstance,
|
||||||
@@ -441,10 +442,26 @@ const messages = defineMessages({
|
|||||||
id: 'app.browse.install-content-to-instance',
|
id: 'app.browse.install-content-to-instance',
|
||||||
defaultMessage: 'Install content to instance',
|
defaultMessage: 'Install content to instance',
|
||||||
},
|
},
|
||||||
|
installToServer: {
|
||||||
|
id: 'app.browse.server.install',
|
||||||
|
defaultMessage: 'Install',
|
||||||
|
},
|
||||||
|
installedToServer: {
|
||||||
|
id: 'app.browse.server.installed',
|
||||||
|
defaultMessage: 'Installed',
|
||||||
|
},
|
||||||
|
installingToServer: {
|
||||||
|
id: 'app.browse.server.installing',
|
||||||
|
defaultMessage: 'Installing',
|
||||||
|
},
|
||||||
modLoaderProvidedByInstance: {
|
modLoaderProvidedByInstance: {
|
||||||
id: 'search.filter.locked.instance-loader.title',
|
id: 'search.filter.locked.instance-loader.title',
|
||||||
defaultMessage: 'Loader is provided by the instance',
|
defaultMessage: 'Loader is provided by the instance',
|
||||||
},
|
},
|
||||||
|
modpacksProjectType: {
|
||||||
|
id: 'app.browse.project-type.modpacks',
|
||||||
|
defaultMessage: 'Modpacks',
|
||||||
|
},
|
||||||
modLoaderProvidedByServer: {
|
modLoaderProvidedByServer: {
|
||||||
id: 'search.filter.locked.server-loader.title',
|
id: 'search.filter.locked.server-loader.title',
|
||||||
defaultMessage: 'Loader is provided by the server',
|
defaultMessage: 'Loader is provided by the server',
|
||||||
@@ -550,7 +567,9 @@ const selectableProjectTypes = computed(() => {
|
|||||||
const suffix = queryString ? `?${queryString}` : ''
|
const suffix = queryString ? `?${queryString}` : ''
|
||||||
|
|
||||||
if (isSetupServerContext.value) {
|
if (isSetupServerContext.value) {
|
||||||
return [{ label: 'Modpacks', href: `/browse/modpack${suffix}` }]
|
return [
|
||||||
|
{ label: formatMessage(messages.modpacksProjectType), href: `/browse/modpack${suffix}` },
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFromWorlds.value) {
|
if (isFromWorlds.value) {
|
||||||
@@ -730,7 +749,13 @@ function getCardActions(
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: 'install',
|
key: 'install',
|
||||||
label: isInstalling ? 'Installing' : isInstalled ? 'Installed' : 'Install',
|
label: formatMessage(
|
||||||
|
isInstalling
|
||||||
|
? messages.installingToServer
|
||||||
|
: isInstalled
|
||||||
|
? messages.installedToServer
|
||||||
|
: messages.installToServer,
|
||||||
|
),
|
||||||
icon: isInstalled ? CheckIcon : PlusIcon,
|
icon: isInstalled ? CheckIcon : PlusIcon,
|
||||||
disabled: isInstalled || isInstalling,
|
disabled: isInstalled || isInstalling,
|
||||||
color: 'brand',
|
color: 'brand',
|
||||||
@@ -972,6 +997,7 @@ provideBrowseManager({
|
|||||||
:on-back="onServerFlowBack"
|
:on-back="onServerFlowBack"
|
||||||
:search-modpacks="searchServerModpacks"
|
:search-modpacks="searchServerModpacks"
|
||||||
:get-project-versions="getServerProjectVersions"
|
:get-project-versions="getServerProjectVersions"
|
||||||
|
:get-loader-manifest="getLoaderManifest"
|
||||||
@hide="() => {}"
|
@hide="() => {}"
|
||||||
@browse-modpacks="() => {}"
|
@browse-modpacks="() => {}"
|
||||||
@create="handleServerModpackFlowCreate"
|
@create="handleServerModpackFlowCreate"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlre
|
|||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { get_project_versions, get_search_results } from '@/helpers/cache.js'
|
import { get_project_versions, get_search_results } from '@/helpers/cache.js'
|
||||||
import { import_instance } from '@/helpers/import.js'
|
import { import_instance } from '@/helpers/import.js'
|
||||||
|
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata.js'
|
||||||
import { create_profile_and_install, create_profile_and_install_from_file } from '@/helpers/pack'
|
import { create_profile_and_install, create_profile_and_install_from_file } from '@/helpers/pack'
|
||||||
import { create, list } from '@/helpers/profile.js'
|
import { create, list } from '@/helpers/profile.js'
|
||||||
import type { InstanceLoader } from '@/helpers/types'
|
import type { InstanceLoader } from '@/helpers/types'
|
||||||
@@ -165,6 +166,7 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
|
|||||||
handleBrowseModpacks,
|
handleBrowseModpacks,
|
||||||
searchModpacks,
|
searchModpacks,
|
||||||
getProjectVersions,
|
getProjectVersions,
|
||||||
|
getLoaderManifest,
|
||||||
setModpackAlreadyInstalledModal,
|
setModpackAlreadyInstalledModal,
|
||||||
handleModpackDuplicateCreateAnyway,
|
handleModpackDuplicateCreateAnyway,
|
||||||
handleModpackDuplicateGoToInstance,
|
handleModpackDuplicateGoToInstance,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CopyIcon, LibraryIcon, PlayIcon, SearchIcon } from '@modrinth/assets'
|
import { CopyIcon, LibraryIcon, PlayIcon, SearchIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, Card, StyledInput } from '@modrinth/ui'
|
import { ButtonStyled, Card, NewModal, StyledInput } from '@modrinth/ui'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import emails from '~/templates/emails'
|
import emails from '~/templates/emails'
|
||||||
@@ -14,7 +14,7 @@ const filtered = computed(() =>
|
|||||||
function openAll() {
|
function openAll() {
|
||||||
let offset = 0
|
let offset = 0
|
||||||
for (const id of filtered.value) {
|
for (const id of filtered.value) {
|
||||||
openPreview(id, offset)
|
openPopupPreview(id, offset)
|
||||||
offset++
|
offset++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,81 @@ function copy(id: string) {
|
|||||||
navigator.clipboard?.writeText(`/_internal/templates/email/${id}`).catch(() => {})
|
navigator.clipboard?.writeText(`/_internal/templates/email/${id}`).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPreview(id: string, offset = 0) {
|
const previewModal = ref<{ hide: () => void; show: () => void } | null>(null)
|
||||||
|
const previewTemplate = ref<string | null>(null)
|
||||||
|
const previewLoading = ref(false)
|
||||||
|
const previewError = ref<string | null>(null)
|
||||||
|
const previewHtml = ref('')
|
||||||
|
const previewVariables = ref<string[]>([])
|
||||||
|
const variableValues = ref<Record<string, string>>({})
|
||||||
|
|
||||||
|
function extractVariables(html: string): string[] {
|
||||||
|
const tokens = new Set<string>()
|
||||||
|
const regex = /\{([a-zA-Z0-9_.-]+)\}/g
|
||||||
|
let match = regex.exec(html)
|
||||||
|
|
||||||
|
while (match !== null) {
|
||||||
|
tokens.add(match[1])
|
||||||
|
match = regex.exec(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...tokens]
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedPreview = computed(() => {
|
||||||
|
let html = previewHtml.value
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(variableValues.value)) {
|
||||||
|
if (!value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = new RegExp(`\\{${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\}`, 'g')
|
||||||
|
html = html.replace(pattern, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return html
|
||||||
|
})
|
||||||
|
|
||||||
|
async function openPreview(id: string, event?: MouseEvent) {
|
||||||
|
if (event?.shiftKey) {
|
||||||
|
openPopupPreview(id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
previewTemplate.value = id
|
||||||
|
previewLoading.value = true
|
||||||
|
previewError.value = null
|
||||||
|
previewHtml.value = ''
|
||||||
|
previewVariables.value = []
|
||||||
|
variableValues.value = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/_internal/templates/email/${id}`)
|
||||||
|
previewHtml.value = await response.text()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load template ${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const variables = extractVariables(previewHtml.value)
|
||||||
|
previewVariables.value = variables
|
||||||
|
variableValues.value = Object.fromEntries(variables.map((value) => [value, '']))
|
||||||
|
previewModal.value?.show()
|
||||||
|
} catch (error) {
|
||||||
|
previewError.value = 'Failed to load email preview.'
|
||||||
|
console.error(error)
|
||||||
|
previewModal.value?.show()
|
||||||
|
} finally {
|
||||||
|
previewLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePreview() {
|
||||||
|
previewModal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPopupPreview(id: string, offset = 0) {
|
||||||
const width = 600
|
const width = 600
|
||||||
const height = 850
|
const height = 850
|
||||||
const left = window.screenX + (window.outerWidth - width) / 2 + ((offset * 28) % 320)
|
const left = window.screenX + (window.outerWidth - width) / 2 + ((offset * 28) % 320)
|
||||||
@@ -48,6 +122,69 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="normal-page no-sidebar">
|
<div class="normal-page no-sidebar">
|
||||||
<h1 class="mb-4 text-3xl font-extrabold text-heading">Email templates</h1>
|
<h1 class="mb-4 text-3xl font-extrabold text-heading">Email templates</h1>
|
||||||
|
<NewModal
|
||||||
|
ref="previewModal"
|
||||||
|
header="Preview email"
|
||||||
|
width="min(92vw, 1000px)"
|
||||||
|
:max-content-height="'88vh'"
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<p class="label__title text-base">Template: {{ previewTemplate }}</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="previewError"
|
||||||
|
class="border-danger bg-danger/10 text-danger my-2 rounded border px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
{{ previewError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="previewLoading" class="my-4 text-sm text-secondary">Loading preview…</div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="previewVariables.length" class="mt-2 grid gap-3 md:grid-cols-2">
|
||||||
|
<label
|
||||||
|
v-for="variable in previewVariables"
|
||||||
|
:key="variable"
|
||||||
|
:for="`preview-${variable}`"
|
||||||
|
class="flex flex-col"
|
||||||
|
>
|
||||||
|
<span class="label__title">{{ variable }}</span>
|
||||||
|
<StyledInput
|
||||||
|
:id="`preview-${variable}`"
|
||||||
|
v-model="variableValues[variable]"
|
||||||
|
type="text"
|
||||||
|
:placeholder="`Enter ${variable}`"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p v-else class="mt-2 text-xs text-secondary">
|
||||||
|
No template variables were detected; preview shown using default values.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="label__title mb-2">Rendered template</div>
|
||||||
|
<iframe
|
||||||
|
v-if="!previewError"
|
||||||
|
:srcdoc="renderedPreview"
|
||||||
|
class="h-[60vh] w-full rounded border border-divider bg-white"
|
||||||
|
sandbox="allow-same-origin"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="rounded border border-divider bg-white px-4 py-3 text-sm text-secondary"
|
||||||
|
>
|
||||||
|
Could not render template preview.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group mt-4">
|
||||||
|
<button class="iconified-button transparent" type="button" @click="closePreview">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NewModal>
|
||||||
<div class="normal-page__content">
|
<div class="normal-page__content">
|
||||||
<Card class="mb-6 flex flex-col gap-4">
|
<Card class="mb-6 flex flex-col gap-4">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
@@ -97,7 +234,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div class="mt-auto flex gap-2">
|
<div class="mt-auto flex gap-2">
|
||||||
<ButtonStyled color="brand" class="flex-1">
|
<ButtonStyled color="brand" class="flex-1">
|
||||||
<button class="w-full justify-center" @click="openPreview(id)">
|
<button class="w-full justify-center" @click="openPreview(id, $event)">
|
||||||
<PlayIcon class="h-4 w-4" aria-hidden="true" />
|
<PlayIcon class="h-4 w-4" aria-hidden="true" />
|
||||||
Preview
|
Preview
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>
|
>
|
||||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> New sign-in method added </Heading>
|
<Heading as="h1" class="mb-2 text-2xl font-bold"> New sign-in method added </Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
Your {authprovider.name} account has been connected and you can now use it to sign in to your
|
Your {authprovider.name} account has been connected and you can now use it to sign in to your
|
||||||
Modrinth account.
|
Modrinth account.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>
|
>
|
||||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Sign-in method removed</Heading>
|
<Heading as="h1" class="mb-2 text-2xl font-bold"> Sign-in method removed</Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
Your <b>{authprovider.name}</b> account has been disconnected and you can no longer use it to
|
Your <b>{authprovider.name}</b> account has been disconnected and you can no longer use it to
|
||||||
sign in to your Modrinth account.
|
sign in to your Modrinth account.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>
|
>
|
||||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Your email has been changed </Heading>
|
<Heading as="h1" class="mb-2 text-2xl font-bold"> Your email has been changed </Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
At your request, we've successfully updated your Modrinth account's email to
|
At your request, we've successfully updated your Modrinth account's email to
|
||||||
{emailchanged.new_email}.
|
{emailchanged.new_email}.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>
|
>
|
||||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Sign in from new device </Heading>
|
<Heading as="h1" class="mb-2 text-2xl font-bold"> Sign in from new device </Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
We noticed that your account was just signed into from a new device or location. If this was
|
We noticed that your account was just signed into from a new device or location. If this was
|
||||||
you, you can safely ignore this email.
|
you, you can safely ignore this email.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
A new personal access token has been created
|
A new personal access token has been created
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
A new personal access token, <b>{newpat.token_name}</b>, has been added to your account.
|
A new personal access token, <b>{newpat.token_name}</b>, has been added to your account.
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>
|
>
|
||||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Your password has been changed </Heading>
|
<Heading as="h1" class="mb-2 text-2xl font-bold"> Your password has been changed </Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base"> Your password has been changed on your account. </Text>
|
<Text class="text-muted text-base"> Your password has been changed on your account. </Text>
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
If you did not make this change, please contact us immediately through our
|
If you did not make this change, please contact us immediately through our
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>
|
>
|
||||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Your password has been removed </Heading>
|
<Heading as="h1" class="mb-2 text-2xl font-bold"> Your password has been removed </Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
At your request, your password has been removed from your account. You must now use a linked
|
At your request, your password has been removed from your account. You must now use a linked
|
||||||
authentication provider (such as your {passremoved.provider} account) to log into your
|
authentication provider (such as your {passremoved.provider} account) to log into your
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
Payment failed for {paymentfailed.service}
|
Payment failed for {paymentfailed.service}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
Our attempt to collect payment for {paymentfailed.amount} from the payment card on file was
|
Our attempt to collect payment for {paymentfailed.amount} from the payment card on file was
|
||||||
unsuccessful. Please update your billing settings to avoid suspension of your service.
|
unsuccessful. Please update your billing settings to avoid suspension of your service.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>
|
>
|
||||||
<Heading as="h1" class="mb-2 text-2xl font-bold">Revenue available to withdraw!</Heading>
|
<Heading as="h1" class="mb-2 text-2xl font-bold">Revenue available to withdraw!</Heading>
|
||||||
|
|
||||||
<Text class="text-base">Hi {user.name},</Text>
|
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
|
|
||||||
<Text class="text-base">
|
<Text class="text-base">
|
||||||
The {payout.amount} earned during {payout.period} has been processed and is now available to
|
The {payout.amount} earned during {payout.period} has been processed and is now available to
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>
|
>
|
||||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Reset your password </Heading>
|
<Heading as="h1" class="mb-2 text-2xl font-bold"> Reset your password </Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
Please visit the link below to reset your password. If you did not request for your password
|
Please visit the link below to reset your password. If you did not request for your password
|
||||||
to be reset, you can safely ignore this email.
|
to be reset, you can safely ignore this email.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
<StyledEmail title="We’ve added time to your server">
|
<StyledEmail title="We’ve added time to your server">
|
||||||
<Heading as="h1" class="mb-2 text-2xl font-bold">We’ve added time to your server</Heading>
|
<Heading as="h1" class="mb-2 text-2xl font-bold">We’ve added time to your server</Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base">{credit.header_message}</Text>
|
<Text class="text-muted text-base">{credit.header_message}</Text>
|
||||||
|
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
Price change for {taxnotification.service}
|
Price change for {taxnotification.service}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
We're writing to let you know about an update to your {taxnotification.service} subscription.
|
We're writing to let you know about an update to your {taxnotification.service} subscription.
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>
|
>
|
||||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Two-factor authentication enabled </Heading>
|
<Heading as="h1" class="mb-2 text-2xl font-bold"> Two-factor authentication enabled </Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
You've secured your account with two-factor authentication. Now, when signing in, you will
|
You've secured your account with two-factor authentication. Now, when signing in, you will
|
||||||
need to submit the code generated by your authenticator app.
|
need to submit the code generated by your authenticator app.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
You've disabled two-factor authentication security on your account.
|
You've disabled two-factor authentication security on your account.
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
At your request, we've removed two-factor authentication from your Modrinth account.
|
At your request, we've removed two-factor authentication from your Modrinth account.
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>
|
>
|
||||||
<Heading as="h1" class="mb-2 text-2xl font-bold"> Verify your email </Heading>
|
<Heading as="h1" class="mb-2 text-2xl font-bold"> Verify your email </Heading>
|
||||||
|
|
||||||
<Text class="text-muted text-base">Hi {user.name},</Text>
|
<Text class="text-muted text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
<Text class="text-muted text-base">
|
<Text class="text-muted text-base">
|
||||||
Please visit the link below to verify your email. If the button does not work, you can copy
|
Please visit the link below to verify your email. If the button does not work, you can copy
|
||||||
the link and paste it into your browser. This link expires in 24 hours.
|
the link and paste it into your browser. This link expires in 24 hours.
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>New message from moderators on {project.name}</Heading
|
>New message from moderators on {project.name}</Heading
|
||||||
>
|
>
|
||||||
|
|
||||||
<Text class="text-base">Hi {user.name},</Text>
|
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
|
|
||||||
<Text class="text-base">
|
<Text class="text-base">
|
||||||
Modrinth's moderation team has left a message on your project,
|
Modrinth's moderation team has left a message on your project,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>Report of '{report.title}' has been updated</Heading
|
>Report of '{report.title}' has been updated</Heading
|
||||||
>
|
>
|
||||||
|
|
||||||
<Text class="text-base">Hi {user.name},</Text>
|
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
|
|
||||||
<Text class="text-base"
|
<Text class="text-base"
|
||||||
>Your report of {report.title} from {report.date} has been updated by our moderation
|
>Your report of {report.title} from {report.date} has been updated by our moderation
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>Report of {report.title} has been submitted</Heading
|
>Report of {report.title} has been submitted</Heading
|
||||||
>
|
>
|
||||||
|
|
||||||
<Text class="text-base">Hi {user.name},</Text>
|
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
|
|
||||||
<Text class="text-base">
|
<Text class="text-base">
|
||||||
We've received your report of {report.title} and our moderation team will review it shortly.
|
We've received your report of {report.title} and our moderation team will review it shortly.
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>You've been invited to an organization</Heading
|
>You've been invited to an organization</Heading
|
||||||
>
|
>
|
||||||
|
|
||||||
<Text class="text-base">Hi {user.name},</Text>
|
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
|
|
||||||
<Text class="text-base"
|
<Text class="text-base"
|
||||||
>Modrinth user
|
>Modrinth user
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
</Section>
|
</Section>
|
||||||
<Heading as="h1" class="mb-2 text-2xl font-bold">You've been invited to a project</Heading>
|
<Heading as="h1" class="mb-2 text-2xl font-bold">You've been invited to a project</Heading>
|
||||||
|
|
||||||
<Text class="text-base">Hi {user.name},</Text>
|
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
|
|
||||||
<Text class="text-base">
|
<Text class="text-base">
|
||||||
Modrinth user
|
Modrinth user
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>Your project, {project.name}, has been approved 🎉</Heading
|
>Your project, {project.name}, has been approved 🎉</Heading
|
||||||
>
|
>
|
||||||
|
|
||||||
<Text class="text-base">Congratulations {user.name},</Text>
|
<Text class="text-base">Congratulations <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
|
|
||||||
<Text class="text-base">
|
<Text class="text-base">
|
||||||
Your project
|
Your project
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
>Your project, {project.name}, status has been updated</Heading
|
>Your project, {project.name}, status has been updated</Heading
|
||||||
>
|
>
|
||||||
|
|
||||||
<Text class="text-base">Hi {user.name},</Text>
|
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
|
|
||||||
<Text class="text-base">
|
<Text class="text-base">
|
||||||
Your project's status has been changed from <b>{project.oldstatus}</b> to
|
Your project's status has been changed from <b>{project.oldstatus}</b> to
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
|
|||||||
</Section>
|
</Section>
|
||||||
<Heading as="h1" class="mb-2 text-2xl font-bold">Project ownership transferred</Heading>
|
<Heading as="h1" class="mb-2 text-2xl font-bold">Project ownership transferred</Heading>
|
||||||
|
|
||||||
<Text class="text-base">Hi {user.name},</Text>
|
<Text class="text-base">Hi <span class="no-auto-link">{user.name}</span>,</Text>
|
||||||
|
|
||||||
<Text class="text-base">
|
<Text class="text-base">
|
||||||
The ownership of
|
The ownership of
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ const tailwindConfig = {
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no" />
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css?family=Inter:700,400"
|
href="https://fonts.googleapis.com/css?family=Inter:700,400"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
@@ -78,6 +79,9 @@ const tailwindConfig = {
|
|||||||
line-height:100%; } table { border-collapse:separate; } a, a:link, a:visited {
|
line-height:100%; } table { border-collapse:separate; } a, a:link, a:visited {
|
||||||
text-decoration:none; color:#1f68c0; } a:hover { text-decoration:underline; }
|
text-decoration:none; color:#1f68c0; } a:hover { text-decoration:underline; }
|
||||||
h1,h2,h3,h4,h5,h6 { color:#000 !important; margin:0; mso-line-height-rule:exactly; }
|
h1,h2,h3,h4,h5,h6 { color:#000 !important; margin:0; mso-line-height-rule:exactly; }
|
||||||
|
.no-auto-link, .no-auto-link a, .no-auto-link a:link, .no-auto-link a:visited, .no-auto-link
|
||||||
|
a[x-apple-data-detectors] { color:inherit !important; text-decoration:none !important;
|
||||||
|
cursor:default !important; pointer-events:none !important; }
|
||||||
</Style>
|
</Style>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
<ButtonStyled type="outlined">
|
<ButtonStyled type="outlined">
|
||||||
<button class="!border-surface-5" @click="triggerIconInput">
|
<button class="!border-surface-5" @click="triggerIconInput">
|
||||||
<UploadIcon />
|
<UploadIcon />
|
||||||
Select icon
|
{{ formatMessage(messages.selectIcon) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled type="outlined">
|
<ButtonStyled type="outlined">
|
||||||
<button class="!border-surface-5" :disabled="!ctx.instanceIcon.value" @click="removeIcon">
|
<button class="!border-surface-5" :disabled="!ctx.instanceIcon.value" @click="removeIcon">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
Remove icon
|
{{ formatMessage(messages.removeIcon) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,17 +21,19 @@
|
|||||||
|
|
||||||
<!-- Instance-specific: Name field -->
|
<!-- Instance-specific: Name field -->
|
||||||
<div v-if="ctx.flowType === 'instance'" class="flex flex-col gap-2">
|
<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
|
<StyledInput
|
||||||
v-model="ctx.instanceName.value"
|
v-model="ctx.instanceName.value"
|
||||||
:placeholder="ctx.autoInstanceName.value || 'Enter instance name'"
|
:placeholder="ctx.autoInstanceName.value || formatMessage(messages.instanceNamePlaceholder)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loader chips -->
|
<!-- Loader chips -->
|
||||||
<div v-if="!hideLoaderChips" class="flex flex-col gap-2">
|
<div v-if="!hideLoaderChips" class="flex flex-col gap-2">
|
||||||
<span class="font-semibold text-contrast">{{
|
<span class="font-semibold text-contrast">{{
|
||||||
ctx.flowType === 'instance' ? 'Loader' : 'Content loader'
|
ctx.flowType === 'instance'
|
||||||
|
? formatMessage(messages.loaderLabel)
|
||||||
|
: formatMessage(messages.contentLoaderLabel)
|
||||||
}}</span>
|
}}</span>
|
||||||
<Chips
|
<Chips
|
||||||
v-model="selectedLoader"
|
v-model="selectedLoader"
|
||||||
@@ -43,14 +45,21 @@
|
|||||||
|
|
||||||
<!-- Game version -->
|
<!-- Game version -->
|
||||||
<div class="flex flex-col gap-2">
|
<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
|
<Combobox
|
||||||
v-model="selectedGameVersion"
|
v-model="selectedGameVersion"
|
||||||
:options="gameVersionOptions"
|
:options="gameVersionOptions"
|
||||||
|
:no-options-message="
|
||||||
|
gameVersionsLoading
|
||||||
|
? formatMessage(commonMessages.loadingLabel)
|
||||||
|
: formatMessage(messages.noVersionsAvailable)
|
||||||
|
"
|
||||||
searchable
|
searchable
|
||||||
sync-with-selection
|
sync-with-selection
|
||||||
placeholder="Select game version"
|
:placeholder="formatMessage(messages.selectGameVersion)"
|
||||||
search-placeholder="Search game version..."
|
:search-placeholder="formatMessage(messages.searchGameVersion)"
|
||||||
@option-hover="handleGameVersionHover"
|
@option-hover="handleGameVersionHover"
|
||||||
>
|
>
|
||||||
<template v-if="ctx.showSnapshotToggle" #dropdown-footer>
|
<template v-if="ctx.showSnapshotToggle" #dropdown-footer>
|
||||||
@@ -61,7 +70,11 @@
|
|||||||
>
|
>
|
||||||
<EyeOffIcon v-if="ctx.showSnapshots.value" class="size-4" />
|
<EyeOffIcon v-if="ctx.showSnapshots.value" class="size-4" />
|
||||||
<EyeIcon v-else 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>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
@@ -72,24 +85,36 @@
|
|||||||
<Collapsible :collapsed="!selectedLoader || !selectedGameVersion" overflow-visible>
|
<Collapsible :collapsed="!selectedLoader || !selectedGameVersion" overflow-visible>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<span class="font-semibold text-contrast">{{
|
<span class="font-semibold text-contrast">{{
|
||||||
isPaperLike ? 'Build number' : 'Loader version'
|
isPaperLike
|
||||||
|
? formatMessage(messages.buildNumberLabel)
|
||||||
|
: formatMessage(messages.loaderVersionLabel)
|
||||||
}}</span>
|
}}</span>
|
||||||
<Chips
|
<Chips
|
||||||
v-if="!isPaperLike"
|
v-if="!isPaperLike"
|
||||||
v-model="loaderVersionType"
|
v-model="loaderVersionType"
|
||||||
:items="loaderVersionTypeItems"
|
:items="loaderVersionTypeItems"
|
||||||
:format-label="capitalize"
|
:format-label="formatLoaderVersionTypeLabel"
|
||||||
/>
|
/>
|
||||||
<div v-if="isPaperLike || loaderVersionType === 'other'">
|
<div v-if="isPaperLike || loaderVersionType === 'other'">
|
||||||
<Combobox
|
<Combobox
|
||||||
v-model="selectedLoaderVersion"
|
v-model="selectedLoaderVersion"
|
||||||
:options="loaderVersionOptions"
|
:options="loaderVersionOptions"
|
||||||
:no-options-message="loaderVersionsLoading ? 'Loading...' : 'No versions available'"
|
:no-options-message="
|
||||||
|
loaderVersionsLoading
|
||||||
|
? formatMessage(commonMessages.loadingLabel)
|
||||||
|
: formatMessage(messages.noVersionsAvailable)
|
||||||
|
"
|
||||||
searchable
|
searchable
|
||||||
sync-with-selection
|
sync-with-selection
|
||||||
:placeholder="isPaperLike ? 'Select build number' : 'Select loader version'"
|
:placeholder="
|
||||||
|
isPaperLike
|
||||||
|
? formatMessage(messages.selectBuildNumber)
|
||||||
|
: formatMessage(messages.selectLoaderVersion)
|
||||||
|
"
|
||||||
:search-placeholder="
|
: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. -->
|
<!-- When not Paper, this scoped slot is omitted and Combobox uses default option markup. -->
|
||||||
@@ -123,6 +148,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Paper } from '@modrinth/api-client'
|
import type { Paper } from '@modrinth/api-client'
|
||||||
import { EyeIcon, EyeOffIcon, UploadIcon, XIcon } from '@modrinth/assets'
|
import { EyeIcon, EyeOffIcon, UploadIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import { commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
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 Combobox, { type ComboboxOption } from '../../../base/Combobox.vue'
|
||||||
import PaperChannelBadge from '../../../base/PaperChannelBadge.vue'
|
import PaperChannelBadge from '../../../base/PaperChannelBadge.vue'
|
||||||
import StyledInput from '../../../base/StyledInput.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 { injectCreationFlowContext } from '../creation-flow-context'
|
||||||
import { capitalize, formatLoaderLabel } from '../shared'
|
import { formatLoaderLabel } from '../shared'
|
||||||
|
|
||||||
const debug = useDebugLogger('CustomSetupStage')
|
const debug = useDebugLogger('CustomSetupStage')
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const ctx = injectCreationFlowContext()
|
const ctx = injectCreationFlowContext()
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
const {
|
const {
|
||||||
selectedLoader,
|
selectedLoader,
|
||||||
selectedGameVersion,
|
selectedGameVersion,
|
||||||
@@ -151,6 +178,92 @@ const {
|
|||||||
hideLoaderVersion,
|
hideLoaderVersion,
|
||||||
} = ctx
|
} = 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 instance flow, prepend 'vanilla' to available loaders.
|
||||||
// For server flows, vanilla is a separate option in the setup type stage, so exclude it here.
|
// For server flows, vanilla is a separate option in the setup type stage, so exclude it here.
|
||||||
const effectiveLoaders = computed(() => {
|
const effectiveLoaders = computed(() => {
|
||||||
@@ -205,23 +318,24 @@ function removeIcon() {
|
|||||||
ctx.instanceIconPath.value = null
|
ctx.instanceIconPath.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loader versions fetched from launcher-meta
|
|
||||||
interface LoaderVersionEntry {
|
|
||||||
id: string
|
|
||||||
stable: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const loaderVersionsLoading = ref(false)
|
const loaderVersionsLoading = ref(false)
|
||||||
const loaderVersionsData = ref<LoaderVersionEntry[]>([])
|
const loaderVersionsData = ref<LoaderVersionEntry[]>([])
|
||||||
const loaderVersionsCache = ref<Record<string, { id: string; loaders: LoaderVersionEntry[] }[]>>({})
|
|
||||||
|
|
||||||
// Paper/Purpur build caches
|
// Paper/Purpur build caches
|
||||||
const paperVersions = ref<Record<string, Paper.Versions.v3.Build[]>>({})
|
const paperVersions = ref<Record<string, Paper.Versions.v3.Build[]>>({})
|
||||||
const purpurVersions = ref<Record<string, string[]>>({})
|
const purpurVersions = ref<Record<string, string[]>>({})
|
||||||
|
|
||||||
// Paper/Purpur supported game version sets (for filtering the game version combobox)
|
function toApiLoaderName(loader: string): string {
|
||||||
const paperSupportedVersions = ref<Set<string> | null>(null)
|
return loader === 'neoforge' ? 'neo' : loader
|
||||||
const purpurSupportedVersions = ref<Set<string> | null>(null)
|
}
|
||||||
|
|
||||||
|
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
|
// Game versions from tags provider, filtered by loader support
|
||||||
const gameVersionOptions = computed<ComboboxOption<string>[]>(() => {
|
const gameVersionOptions = computed<ComboboxOption<string>[]>(() => {
|
||||||
@@ -231,34 +345,36 @@ const gameVersionOptions = computed<ComboboxOption<string>[]>(() => {
|
|||||||
|
|
||||||
// For loaders with per-version data, only show game versions that have builds
|
// For loaders with per-version data, only show game versions that have builds
|
||||||
if (selectedLoader.value && selectedLoader.value !== 'vanilla') {
|
if (selectedLoader.value && selectedLoader.value !== 'vanilla') {
|
||||||
if (selectedLoader.value === 'paper' && paperSupportedVersions.value) {
|
if (selectedLoader.value === 'paper') {
|
||||||
|
if (!ctx.paperSupportedVersions.value) return []
|
||||||
return versions
|
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 }))
|
.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
|
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 }))
|
.map((v) => ({ value: v.version, label: v.version }))
|
||||||
}
|
}
|
||||||
|
|
||||||
let apiLoader = selectedLoader.value
|
const apiLoader = toApiLoaderName(selectedLoader.value)
|
||||||
if (apiLoader === 'neoforge') apiLoader = 'neo'
|
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}')
|
const hasPlaceholder = manifest.some((x) => x.id === '${modrinth.gameVersion}')
|
||||||
if (!hasPlaceholder) {
|
|
||||||
const supportedVersions = new Set(
|
const supportedVersions = new Set(
|
||||||
manifest.filter((x) => x.loaders.length > 0).map((x) => x.id),
|
manifest
|
||||||
|
.filter(
|
||||||
|
(x) => x.id !== '${modrinth.gameVersion}' && (hasPlaceholder || x.loaders.length > 0),
|
||||||
|
)
|
||||||
|
.map((x) => x.id),
|
||||||
)
|
)
|
||||||
return versions
|
return versions
|
||||||
.filter((v) => supportedVersions.has(v.version))
|
.filter((v) => supportedVersions.has(v.version))
|
||||||
.map((v) => ({ value: v.version, label: v.version }))
|
.map((v) => ({ value: v.version, label: v.version }))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return versions.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(
|
watch(
|
||||||
gameVersionOptions,
|
gameVersionOptions,
|
||||||
(options) => {
|
(options) => {
|
||||||
if (options.length === 0) return
|
if (options.length === 0) {
|
||||||
|
selectedGameVersion.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!selectedGameVersion.value || !options.some((o) => o.value === selectedGameVersion.value)) {
|
if (!selectedGameVersion.value || !options.some((o) => o.value === selectedGameVersion.value)) {
|
||||||
selectedGameVersion.value = options[0].value
|
selectedGameVersion.value = options[0].value
|
||||||
}
|
}
|
||||||
@@ -276,50 +395,20 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
async function fetchLoaderManifest(loader: string) {
|
async function fetchLoaderManifest(loader: string) {
|
||||||
let apiLoader = loader
|
const apiLoader = toApiLoaderName(loader)
|
||||||
if (apiLoader === 'neoforge') apiLoader = 'neo'
|
|
||||||
|
|
||||||
debug(
|
debug(
|
||||||
'fetchLoaderManifest:',
|
'fetchLoaderManifest:',
|
||||||
loader,
|
loader,
|
||||||
'apiLoader:',
|
'apiLoader:',
|
||||||
apiLoader,
|
apiLoader,
|
||||||
'cached:',
|
'cached:',
|
||||||
!!loaderVersionsCache.value[apiLoader],
|
!!ctx.loaderVersionsCache.value[apiLoader],
|
||||||
)
|
)
|
||||||
if (loaderVersionsCache.value[apiLoader]) return
|
await ctx.fetchLoaderMetadata(loader)
|
||||||
|
|
||||||
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] = []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPaperSupportedVersions() {
|
async function fetchLoaderMetadata(loader?: string | null) {
|
||||||
if (paperSupportedVersions.value) return
|
await ctx.fetchLoaderMetadata(loader)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function paperBuildChannelTag(buildId: string): 'ALPHA' | 'BETA' | null {
|
function paperBuildChannelTag(buildId: string): 'ALPHA' | 'BETA' | null {
|
||||||
@@ -363,10 +452,8 @@ function getLoaderVersionsForGameVersion(
|
|||||||
loader: string,
|
loader: string,
|
||||||
gameVersion: string,
|
gameVersion: string,
|
||||||
): LoaderVersionEntry[] {
|
): LoaderVersionEntry[] {
|
||||||
let apiLoader = loader
|
const apiLoader = toApiLoaderName(loader)
|
||||||
if (apiLoader === 'neoforge') apiLoader = 'neo'
|
const manifest = ctx.loaderVersionsCache.value[apiLoader]
|
||||||
|
|
||||||
const manifest = loaderVersionsCache.value[apiLoader]
|
|
||||||
debug('getLoaderVersionsForGameVersion:', {
|
debug('getLoaderVersionsForGameVersion:', {
|
||||||
loader,
|
loader,
|
||||||
apiLoader,
|
apiLoader,
|
||||||
@@ -379,6 +466,7 @@ function getLoaderVersionsForGameVersion(
|
|||||||
// Some loaders (e.g. Fabric) list all versions under a placeholder entry
|
// Some loaders (e.g. Fabric) list all versions under a placeholder entry
|
||||||
const placeholder = manifest.find((x) => x.id === '${modrinth.gameVersion}')
|
const placeholder = manifest.find((x) => x.id === '${modrinth.gameVersion}')
|
||||||
if (placeholder) {
|
if (placeholder) {
|
||||||
|
if (!manifest.some((x) => x.id === gameVersion)) return []
|
||||||
debug(
|
debug(
|
||||||
'getLoaderVersionsForGameVersion: using placeholder, loaders:',
|
'getLoaderVersionsForGameVersion: using placeholder, loaders:',
|
||||||
placeholder.loaders.length,
|
placeholder.loaders.length,
|
||||||
@@ -400,16 +488,7 @@ function getLoaderVersionsForGameVersion(
|
|||||||
watch(
|
watch(
|
||||||
() => selectedLoader.value,
|
() => selectedLoader.value,
|
||||||
async (loader) => {
|
async (loader) => {
|
||||||
if (!loader || loader === 'vanilla') return
|
await fetchLoaderMetadata(loader)
|
||||||
if (loader === 'paper') {
|
|
||||||
await fetchPaperSupportedVersions()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (loader === 'purpur') {
|
|
||||||
await fetchPurpurSupportedVersions()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await fetchLoaderManifest(loader)
|
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,18 +4,23 @@
|
|||||||
v-if="ctx.flowType !== 'server-onboarding' && ctx.flowType !== 'reset-server'"
|
v-if="ctx.flowType !== 'server-onboarding' && ctx.flowType !== 'reset-server'"
|
||||||
class="flex flex-col gap-2"
|
class="flex flex-col gap-2"
|
||||||
>
|
>
|
||||||
<span class="font-semibold text-contrast">World name</span>
|
<span class="font-semibold text-contrast">{{ formatMessage(messages.worldNameLabel) }}</span>
|
||||||
<StyledInput v-model="worldName" placeholder="Enter world name" />
|
<StyledInput
|
||||||
|
v-model="worldName"
|
||||||
|
:placeholder="formatMessage(messages.worldNamePlaceholder)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="ctx.setupType.value === 'vanilla'" class="flex flex-col gap-2">
|
<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
|
<Combobox
|
||||||
v-model="selectedGameVersion"
|
v-model="selectedGameVersion"
|
||||||
:options="gameVersionOptions"
|
:options="gameVersionOptions"
|
||||||
searchable
|
searchable
|
||||||
sync-with-selection
|
sync-with-selection
|
||||||
placeholder="Select game version"
|
:placeholder="formatMessage(messages.gameVersionPlaceholder)"
|
||||||
>
|
>
|
||||||
<template v-if="ctx.showSnapshotToggle" #dropdown-footer>
|
<template v-if="ctx.showSnapshotToggle" #dropdown-footer>
|
||||||
<button
|
<button
|
||||||
@@ -25,37 +30,50 @@
|
|||||||
>
|
>
|
||||||
<EyeOffIcon v-if="ctx.showSnapshots.value" class="size-4" />
|
<EyeOffIcon v-if="ctx.showSnapshots.value" class="size-4" />
|
||||||
<EyeIcon v-else 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>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<span class="font-semibold text-contrast">Gamemode</span>
|
<span class="font-semibold text-contrast">{{ formatMessage(messages.gamemodeLabel) }}</span>
|
||||||
<Chips v-model="gamemode" :items="gamemodeItems" :format-label="capitalize" />
|
<Chips v-model="gamemode" :items="gamemodeItems" :format-label="formatGamemodeLabel" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="gamemode !== 'hardcore'" class="flex flex-col gap-2">
|
<div v-if="gamemode !== 'hardcore'" class="flex flex-col gap-2">
|
||||||
<span class="font-semibold text-contrast">Difficulty</span>
|
<span class="font-semibold text-contrast">{{ formatMessage(messages.difficultyLabel) }}</span>
|
||||||
<Chips v-model="difficulty" :items="difficultyItems" :format-label="capitalize" />
|
<Chips v-model="difficulty" :items="difficultyItems" :format-label="formatDifficultyLabel" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<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
|
<Combobox
|
||||||
v-model="worldTypeOption"
|
v-model="worldTypeOption"
|
||||||
:options="worldTypeOptions"
|
:options="worldTypeOptions"
|
||||||
placeholder="Select world type"
|
:placeholder="formatMessage(messages.worldTypePlaceholder)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<span class="font-semibold text-contrast"
|
<span class="font-semibold text-contrast">
|
||||||
>World seed <span class="text-secondary font-normal">(Optional)</span></span
|
<IntlFormatted :message-id="messages.worldSeedLabelWithOptional">
|
||||||
>
|
<template #optional="{ children }">
|
||||||
<StyledInput v-model="worldSeed" placeholder="Enter world seed" />
|
<span class="text-secondary font-normal">
|
||||||
<span class="text-sm text-secondary">Leave blank for a random seed.</span>
|
<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>
|
||||||
|
|
||||||
<div class="h-px w-full bg-surface-5" />
|
<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">
|
<Accordion overflow-visible button-class="w-full bg-transparent m-0 p-0 border-none">
|
||||||
<template #title>
|
<template #title>
|
||||||
<SettingsIcon class="size-4 shrink-0 text-primary" />
|
<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>
|
</template>
|
||||||
<div class="flex flex-col gap-4 pt-4">
|
<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 w-full flex-row items-center justify-between gap-4">
|
||||||
<div class="flex flex-col gap-1">
|
<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">
|
<span class="text-sm text-secondary">
|
||||||
Controls whether villages, strongholds, and other structures generate in new chunks.
|
{{ formatMessage(messages.generateStructuresDescription) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Toggle v-model="generateStructures" small class="shrink-0" />
|
<Toggle v-model="generateStructures" small class="shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<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
|
<Combobox
|
||||||
v-model="generatorSettingsMode"
|
v-model="generatorSettingsMode"
|
||||||
:options="generatorSettingsOptions"
|
:options="generatorSettingsOptions"
|
||||||
placeholder="Select generator settings"
|
:placeholder="formatMessage(messages.generatorSettingsPlaceholder)"
|
||||||
/>
|
/>
|
||||||
<StyledInput
|
<StyledInput
|
||||||
v-if="generatorSettingsMode === 'custom'"
|
v-if="generatorSettingsMode === 'custom'"
|
||||||
v-model="generatorSettingsCustom"
|
v-model="generatorSettingsCustom"
|
||||||
multiline
|
multiline
|
||||||
:rows="4"
|
:rows="4"
|
||||||
placeholder="Enter generator settings JSON"
|
:placeholder="formatMessage(messages.generatorSettingsJsonPlaceholder)"
|
||||||
input-class="font-mono"
|
input-class="font-mono"
|
||||||
/>
|
/>
|
||||||
<span class="text-sm text-secondary">
|
<span class="text-sm text-secondary">
|
||||||
Used for advanced world customization such as custom Superflat layers.
|
{{ formatMessage(messages.generatorSettingsDescription) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,7 +125,7 @@
|
|||||||
<InlineBackupCreator
|
<InlineBackupCreator
|
||||||
v-if="ctx.flowType === 'reset-server'"
|
v-if="ctx.flowType === 'reset-server'"
|
||||||
ref="backupCreator"
|
ref="backupCreator"
|
||||||
backup-name="Before reset server"
|
:backup-name="formatMessage(messages.beforeResetServerBackupName)"
|
||||||
hide-shift-click-hint
|
hide-shift-click-hint
|
||||||
@update:buttons-disabled="ctx.isBackingUp.value = $event"
|
@update:buttons-disabled="ctx.isBackingUp.value = $event"
|
||||||
/>
|
/>
|
||||||
@@ -110,6 +134,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { EyeIcon, EyeOffIcon, SettingsIcon } from '@modrinth/assets'
|
import { EyeIcon, EyeOffIcon, SettingsIcon } from '@modrinth/assets'
|
||||||
|
import { commonMessages, defineMessages, IntlFormatted, useVIntl } from '@modrinth/ui'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||||
@@ -123,10 +148,146 @@ import StyledInput from '../../../base/StyledInput.vue'
|
|||||||
import Toggle from '../../../base/Toggle.vue'
|
import Toggle from '../../../base/Toggle.vue'
|
||||||
import type { Difficulty, Gamemode, GeneratorSettingsMode } from '../creation-flow-context'
|
import type { Difficulty, Gamemode, GeneratorSettingsMode } from '../creation-flow-context'
|
||||||
import { injectCreationFlowContext } from '../creation-flow-context'
|
import { injectCreationFlowContext } from '../creation-flow-context'
|
||||||
import { capitalize } from '../shared'
|
|
||||||
|
|
||||||
const debug = useDebugLogger('FinalConfigStage')
|
const debug = useDebugLogger('FinalConfigStage')
|
||||||
const ctx = injectCreationFlowContext()
|
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)
|
const backupCreator = ref<InstanceType<typeof InlineBackupCreator> | null>(null)
|
||||||
watch(backupCreator, (creator) => {
|
watch(backupCreator, (creator) => {
|
||||||
@@ -189,17 +350,41 @@ watch(gamemode, (mode) => {
|
|||||||
const gamemodeItems: Gamemode[] = ['survival', 'creative', 'hardcore']
|
const gamemodeItems: Gamemode[] = ['survival', 'creative', 'hardcore']
|
||||||
const difficultyItems: Difficulty[] = ['peaceful', 'easy', 'normal', 'hard']
|
const difficultyItems: Difficulty[] = ['peaceful', 'easy', 'normal', 'hard']
|
||||||
|
|
||||||
const worldTypeOptions: ComboboxOption<string>[] = [
|
function formatGamemodeLabel(mode: Gamemode): string {
|
||||||
{ value: 'minecraft:normal', label: 'Default' },
|
switch (mode) {
|
||||||
{ value: 'minecraft:flat', label: 'Superflat' },
|
case 'survival':
|
||||||
{ value: 'minecraft:large_biomes', label: 'Large Biomes' },
|
return formatMessage(messages.gamemodeSurvival)
|
||||||
{ value: 'minecraft:amplified', label: 'Amplified' },
|
case 'creative':
|
||||||
{ value: 'minecraft:single_biome_surface', label: 'Single Biome' },
|
return formatMessage(messages.gamemodeCreative)
|
||||||
]
|
case 'hardcore':
|
||||||
|
return formatMessage(messages.gamemodeHardcore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const generatorSettingsOptions: ComboboxOption<GeneratorSettingsMode>[] = [
|
function formatDifficultyLabel(value: Difficulty): string {
|
||||||
{ value: 'default', label: 'Default' },
|
switch (value) {
|
||||||
{ value: 'flat', label: 'Flat' },
|
case 'peaceful':
|
||||||
{ value: 'custom', label: 'Custom' },
|
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>
|
</script>
|
||||||
|
|||||||
@@ -2,19 +2,21 @@
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between">
|
<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
|
<ButtonStyled
|
||||||
type="transparent"
|
type="transparent"
|
||||||
size="small"
|
size="small"
|
||||||
:class="{ invisible: totalSelectedCount === 0 }"
|
:class="{ invisible: totalSelectedCount === 0 }"
|
||||||
>
|
>
|
||||||
<button @click="clearAll">Clear all</button>
|
<button @click="clearAll">{{ formatMessage(messages.clearAll) }}</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="loading">
|
<template v-if="loading">
|
||||||
<div class="flex items-center justify-center py-8 text-secondary text-sm">
|
<div class="flex items-center justify-center py-8 text-secondary text-sm">
|
||||||
Detecting launcher instances...
|
{{ formatMessage(messages.detectingLauncherInstances) }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -23,7 +25,7 @@
|
|||||||
v-if="ctx.importLaunchers.value.length > 0"
|
v-if="ctx.importLaunchers.value.length > 0"
|
||||||
v-model="ctx.importSearchQuery.value"
|
v-model="ctx.importSearchQuery.value"
|
||||||
:icon="SearchIcon"
|
:icon="SearchIcon"
|
||||||
placeholder="Search instance name"
|
:placeholder="formatMessage(messages.searchInstanceNamePlaceholder)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Launcher sections -->
|
<!-- Launcher sections -->
|
||||||
@@ -75,7 +77,9 @@
|
|||||||
<!-- Add launcher path -->
|
<!-- Add launcher path -->
|
||||||
<div v-if="!showAddPath">
|
<div v-if="!showAddPath">
|
||||||
<ButtonStyled>
|
<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>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex items-center gap-2">
|
<div v-else class="flex items-center gap-2">
|
||||||
@@ -83,10 +87,14 @@
|
|||||||
><button class="!shadow-none" @click="browseForLauncherPath">
|
><button class="!shadow-none" @click="browseForLauncherPath">
|
||||||
<FolderSearchIcon class="size-5" /></button
|
<FolderSearchIcon class="size-5" /></button
|
||||||
></ButtonStyled>
|
></ButtonStyled>
|
||||||
<StyledInput v-model="newLauncherPath" placeholder="Path to launcher..." class="flex-1" />
|
<StyledInput
|
||||||
|
v-model="newLauncherPath"
|
||||||
|
:placeholder="formatMessage(messages.launcherPathPlaceholder)"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button class="!shadow-none" :disabled="!newLauncherPath.trim()" @click="addLauncherPath">
|
<button class="!shadow-none" :disabled="!newLauncherPath.trim()" @click="addLauncherPath">
|
||||||
Add
|
{{ formatMessage(messages.add) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,6 +104,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronRightIcon, FolderSearchIcon, SearchIcon } from '@modrinth/assets'
|
import { ChevronRightIcon, FolderSearchIcon, SearchIcon } from '@modrinth/assets'
|
||||||
|
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { injectInstanceImport, injectNotificationManager } from '../../../../providers'
|
import { injectInstanceImport, injectNotificationManager } from '../../../../providers'
|
||||||
@@ -109,6 +118,7 @@ import { injectCreationFlowContext } from '../creation-flow-context'
|
|||||||
const ctx = injectCreationFlowContext()
|
const ctx = injectCreationFlowContext()
|
||||||
const importProvider = injectInstanceImport()
|
const importProvider = injectInstanceImport()
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const expandedLaunchers = ref(new Set<string>())
|
const expandedLaunchers = ref(new Set<string>())
|
||||||
@@ -116,6 +126,49 @@ const expandedBeforeSearch = ref<Set<string> | null>(null)
|
|||||||
const showAddPath = ref(false)
|
const showAddPath = ref(false)
|
||||||
const newLauncherPath = ref('')
|
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
|
// Load detected launchers on mount
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (ctx.importLaunchers.value.length > 0) return // Already loaded
|
if (ctx.importLaunchers.value.length > 0) return // Already loaded
|
||||||
@@ -261,13 +314,15 @@ async function addLauncherPath() {
|
|||||||
if (instances.length === 0) {
|
if (instances.length === 0) {
|
||||||
addNotification({
|
addNotification({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'No instances found',
|
title: formatMessage(messages.noInstancesFoundTitle),
|
||||||
text: `No importable instances were found at the specified path.`,
|
text: formatMessage(messages.noInstancesFoundText),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const launcher: ImportableLauncher = {
|
const launcher: ImportableLauncher = {
|
||||||
name: `Custom (${path.split(/[\\/]/).pop() || path})`,
|
name: formatMessage(messages.customLauncherName, {
|
||||||
|
pathName: path.split(/[\\/]/).pop() || path,
|
||||||
|
}),
|
||||||
path,
|
path,
|
||||||
instances,
|
instances,
|
||||||
}
|
}
|
||||||
@@ -277,8 +332,8 @@ async function addLauncherPath() {
|
|||||||
} catch {
|
} catch {
|
||||||
addNotification({
|
addNotification({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'No instances found',
|
title: formatMessage(messages.noInstancesFoundTitle),
|
||||||
text: `No importable instances were found at the specified path.`,
|
text: formatMessage(messages.noInstancesFoundText),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4">
|
<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
|
<Combobox
|
||||||
v-model="ctx.modpackSearchProjectId.value"
|
v-model="ctx.modpackSearchProjectId.value"
|
||||||
:options="ctx.modpackSearchOptions.value"
|
:options="ctx.modpackSearchOptions.value"
|
||||||
searchable
|
searchable
|
||||||
search-placeholder="Search for modpack"
|
:search-placeholder="formatMessage(messages.searchModpackPlaceholder)"
|
||||||
:no-options-message="searchLoading ? 'Loading...' : 'No results found'"
|
:no-options-message="
|
||||||
|
searchLoading
|
||||||
|
? formatMessage(commonMessages.loadingLabel)
|
||||||
|
: formatMessage(messages.noResultsFound)
|
||||||
|
"
|
||||||
:disable-search-filter="true"
|
:disable-search-filter="true"
|
||||||
@search-input="(query) => handleSearch(query)"
|
@search-input="(query) => handleSearch(query)"
|
||||||
>
|
>
|
||||||
@@ -18,20 +24,20 @@
|
|||||||
</Combobox>
|
</Combobox>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="h-[1px] w-full flex-1 bg-surface-5" />
|
<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 class="h-[1px] w-full flex-1 bg-surface-5" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<ButtonStyled type="outlined">
|
<ButtonStyled type="outlined">
|
||||||
<button class="flex-1 !border-surface-4" @click="triggerFileInput">
|
<button class="flex-1 !border-surface-4" @click="triggerFileInput">
|
||||||
<ImportIcon />
|
<ImportIcon />
|
||||||
Import modpack
|
{{ formatMessage(messages.importModpack) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button class="flex-1" @click="ctx.browseModpacks()">
|
<button class="flex-1" @click="ctx.browseModpacks()">
|
||||||
<CompassIcon />
|
<CompassIcon />
|
||||||
Browse modpacks
|
{{ formatMessage(messages.browseModpacks) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,6 +46,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CompassIcon, ImportIcon, RightArrowIcon } from '@modrinth/assets'
|
import { CompassIcon, ImportIcon, RightArrowIcon } from '@modrinth/assets'
|
||||||
|
import { commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
|
||||||
import { defineAsyncComponent, h, onMounted, ref, watch } from 'vue'
|
import { defineAsyncComponent, h, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||||
@@ -52,9 +59,33 @@ import { injectCreationFlowContext } from '../creation-flow-context'
|
|||||||
const debug = useDebugLogger('ModpackStage')
|
const debug = useDebugLogger('ModpackStage')
|
||||||
const ctx = injectCreationFlowContext()
|
const ctx = injectCreationFlowContext()
|
||||||
const filePicker = injectFilePicker()
|
const filePicker = injectFilePicker()
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const searchLoading = ref(false)
|
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() {
|
function proceedWithModpack() {
|
||||||
debug('proceedWithModpack:', {
|
debug('proceedWithModpack:', {
|
||||||
flowType: ctx.flowType,
|
flowType: ctx.flowType,
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<span class="font-semibold text-contrast">
|
<span class="font-semibold text-contrast">
|
||||||
{{
|
{{ setupTypeTitle }}
|
||||||
ctx.flowType === 'instance'
|
|
||||||
? 'Choose instance type'
|
|
||||||
: ctx.flowType === 'server-onboarding' || ctx.flowType === 'reset-server'
|
|
||||||
? 'Select installation type'
|
|
||||||
: 'Select world type'
|
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Instance flow options -->
|
<!-- Instance flow options -->
|
||||||
@@ -15,25 +9,25 @@
|
|||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<BigOptionButton
|
<BigOptionButton
|
||||||
:icon="BoxesIcon"
|
:icon="BoxesIcon"
|
||||||
title="Custom setup"
|
:title="formatMessage(messages.customSetupTitle)"
|
||||||
description="Start from scratch by picking a loader and game version."
|
:description="formatMessage(messages.customSetupDescription)"
|
||||||
@click="setSetupType('custom')"
|
@click="setSetupType('custom')"
|
||||||
/>
|
/>
|
||||||
<BigOptionButton
|
<BigOptionButton
|
||||||
:icon="PackageIcon"
|
:icon="PackageIcon"
|
||||||
title="Modpack base"
|
:title="formatMessage(messages.modpackBaseTitle)"
|
||||||
description="Use a popular modpack as your starting point."
|
:description="formatMessage(messages.modpackBaseDescription)"
|
||||||
@click="setSetupType('modpack')"
|
@click="setSetupType('modpack')"
|
||||||
/>
|
/>
|
||||||
<BigOptionButton
|
<BigOptionButton
|
||||||
:icon="BoxImportIcon"
|
:icon="BoxImportIcon"
|
||||||
title="Import instance"
|
:title="formatMessage(messages.importInstanceTitle)"
|
||||||
description="Import an instance from Prism, CurseForge, or similar."
|
:description="formatMessage(messages.importInstanceDescription)"
|
||||||
@click="ctx.setImportMode()"
|
@click="ctx.setImportMode()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm text-secondary">
|
<span class="text-sm text-secondary">
|
||||||
An instance is a Minecraft setup with a specific loader, version, and mods.
|
{{ formatMessage(messages.instanceDescription) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -42,20 +36,20 @@
|
|||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<BigOptionButton
|
<BigOptionButton
|
||||||
:icon="PackageIcon"
|
:icon="PackageIcon"
|
||||||
title="Modpack base"
|
:title="formatMessage(messages.modpackBaseTitle)"
|
||||||
description="Use a popular modpack as your starting point."
|
:description="formatMessage(messages.modpackBaseDescription)"
|
||||||
@click="setSetupType('modpack')"
|
@click="setSetupType('modpack')"
|
||||||
/>
|
/>
|
||||||
<BigOptionButton
|
<BigOptionButton
|
||||||
:icon="BoxesIcon"
|
:icon="BoxesIcon"
|
||||||
title="Custom setup"
|
:title="formatMessage(messages.customSetupTitle)"
|
||||||
description="Start from scratch by picking a loader and game version."
|
:description="formatMessage(messages.customSetupDescription)"
|
||||||
@click="setSetupType('custom')"
|
@click="setSetupType('custom')"
|
||||||
/>
|
/>
|
||||||
<BigOptionButton
|
<BigOptionButton
|
||||||
:icon="BoxIcon"
|
:icon="BoxIcon"
|
||||||
title="Vanilla Minecraft"
|
:title="formatMessage(messages.vanillaMinecraftTitle)"
|
||||||
description="Classic Minecraft with no mods or plugins."
|
:description="formatMessage(messages.vanillaMinecraftDescription)"
|
||||||
@click="setSetupType('vanilla')"
|
@click="setSetupType('vanilla')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,6 +59,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { BoxesIcon, BoxIcon, BoxImportIcon, PackageIcon } from '@modrinth/assets'
|
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'
|
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||||
|
|
||||||
@@ -74,6 +70,68 @@ import { injectCreationFlowContext } from '../creation-flow-context'
|
|||||||
const debug = useDebugLogger('SetupTypeStage')
|
const debug = useDebugLogger('SetupTypeStage')
|
||||||
const ctx = injectCreationFlowContext()
|
const ctx = injectCreationFlowContext()
|
||||||
const { setSetupType: _setSetupType } = ctx
|
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') {
|
function setSetupType(type: 'modpack' | 'custom' | 'vanilla') {
|
||||||
debug('selected:', type)
|
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 { computed, type ComputedRef, type Ref, ref, type ShallowRef, watch } from 'vue'
|
||||||
import type { ComponentExposed } from 'vue-component-type-helpers'
|
import type { ComponentExposed } from 'vue-component-type-helpers'
|
||||||
|
|
||||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
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 { formatLoaderLabel } from '#ui/utils/loaders'
|
||||||
|
|
||||||
import { createContext } from '../../../providers'
|
import { createContext, injectModrinthClient } from '../../../providers'
|
||||||
import type { ImportableLauncher } from '../../../providers/instance-import'
|
import type { ImportableLauncher } from '../../../providers/instance-import'
|
||||||
import type { MultiStageModal, StageConfigInput } from '../../base'
|
import type { MultiStageModal, StageConfigInput } from '../../base'
|
||||||
import type { ComboboxOption } from '../../base/Combobox.vue'
|
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 Difficulty = 'peaceful' | 'easy' | 'normal' | 'hard'
|
||||||
export type LoaderVersionType = 'stable' | 'latest' | 'other'
|
export type LoaderVersionType = 'stable' | 'latest' | 'other'
|
||||||
export type GeneratorSettingsMode = 'default' | 'flat' | 'custom'
|
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 {
|
export interface ModpackSelection {
|
||||||
projectId: string
|
projectId: string
|
||||||
@@ -43,16 +118,10 @@ export interface ModpackSearchResult {
|
|||||||
limit: number
|
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 {
|
export interface CreationFlowContextValue {
|
||||||
// Flow
|
// Flow
|
||||||
flowType: FlowType
|
flowType: FlowType
|
||||||
|
formatMessage: VIntlFormatters['formatMessage']
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
availableLoaders: string[]
|
availableLoaders: string[]
|
||||||
@@ -91,6 +160,9 @@ export interface CreationFlowContextValue {
|
|||||||
hideLoaderChips: ComputedRef<boolean>
|
hideLoaderChips: ComputedRef<boolean>
|
||||||
hideLoaderVersion: ComputedRef<boolean>
|
hideLoaderVersion: ComputedRef<boolean>
|
||||||
showSnapshots: Ref<boolean>
|
showSnapshots: Ref<boolean>
|
||||||
|
loaderVersionsCache: Ref<Record<string, { id: string; loaders: LoaderVersionEntry[] }[]>>
|
||||||
|
paperSupportedVersions: Ref<Set<string> | null>
|
||||||
|
purpurSupportedVersions: Ref<Set<string> | null>
|
||||||
|
|
||||||
// Modpack state
|
// Modpack state
|
||||||
modpackSelection: Ref<ModpackSelection | null>
|
modpackSelection: Ref<ModpackSelection | null>
|
||||||
@@ -133,10 +205,13 @@ export interface CreationFlowContextValue {
|
|||||||
browseModpacks: () => void
|
browseModpacks: () => void
|
||||||
finish: () => void
|
finish: () => void
|
||||||
buildProperties: () => Archon.Content.v1.PropertiesFields
|
buildProperties: () => Archon.Content.v1.PropertiesFields
|
||||||
|
fetchLoaderMetadata: (loader?: string | null) => Promise<void>
|
||||||
|
prefetchLoaderMetadata: () => Promise<void>
|
||||||
|
|
||||||
// Platform-provided search
|
// Platform-provided search
|
||||||
searchModpacks: (query: string, limit?: number) => Promise<ModpackSearchResult>
|
searchModpacks: (query: string, limit?: number) => Promise<ModpackSearchResult>
|
||||||
getProjectVersions: (projectId: string) => Promise<{ id: string }[]>
|
getProjectVersions: (projectId: string) => Promise<{ id: string }[]>
|
||||||
|
getLoaderManifest: LoaderManifestResolver | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const [injectCreationFlowContext, provideCreationFlowContext] =
|
export const [injectCreationFlowContext, provideCreationFlowContext] =
|
||||||
@@ -156,6 +231,7 @@ export interface CreationFlowOptions {
|
|||||||
onBack?: () => void
|
onBack?: () => void
|
||||||
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
|
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
|
||||||
getProjectVersions?: (projectId: string) => Promise<{ id: string }[]>
|
getProjectVersions?: (projectId: string) => Promise<{ id: string }[]>
|
||||||
|
getLoaderManifest?: LoaderManifestResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCreationFlowContext(
|
export function createCreationFlowContext(
|
||||||
@@ -168,6 +244,9 @@ export function createCreationFlowContext(
|
|||||||
options: CreationFlowOptions = {},
|
options: CreationFlowOptions = {},
|
||||||
): CreationFlowContextValue {
|
): CreationFlowContextValue {
|
||||||
const debug = useDebugLogger('CreationFlow')
|
const debug = useDebugLogger('CreationFlow')
|
||||||
|
const client = injectModrinthClient()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
const availableLoaders = options.availableLoaders ?? ['fabric', 'neoforge', 'forge', 'quilt']
|
const availableLoaders = options.availableLoaders ?? ['fabric', 'neoforge', 'forge', 'quilt']
|
||||||
const showSnapshotToggle = options.showSnapshotToggle ?? false
|
const showSnapshotToggle = options.showSnapshotToggle ?? false
|
||||||
const disableClose = options.disableClose ?? false
|
const disableClose = options.disableClose ?? false
|
||||||
@@ -175,6 +254,9 @@ export function createCreationFlowContext(
|
|||||||
const initialLoader = options.initialLoader ?? null
|
const initialLoader = options.initialLoader ?? null
|
||||||
const initialGameVersion = options.initialGameVersion ?? null
|
const initialGameVersion = options.initialGameVersion ?? null
|
||||||
const onBack = options.onBack ?? 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 setupType = ref<SetupType | null>(null)
|
||||||
const isImportMode = ref(false)
|
const isImportMode = ref(false)
|
||||||
@@ -207,6 +289,11 @@ export function createCreationFlowContext(
|
|||||||
const loaderVersionType = ref<LoaderVersionType>('stable')
|
const loaderVersionType = ref<LoaderVersionType>('stable')
|
||||||
const selectedLoaderVersion = ref<string | null>(null)
|
const selectedLoaderVersion = ref<string | null>(null)
|
||||||
const showSnapshots = ref(false)
|
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 autoInstanceName = computed(() => {
|
||||||
const loader = selectedLoader.value
|
const loader = selectedLoader.value
|
||||||
@@ -255,6 +342,83 @@ export function createCreationFlowContext(
|
|||||||
() => setupType.value === 'vanilla' || selectedLoader.value === 'vanilla',
|
() => 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() {
|
async function reset() {
|
||||||
if (fetchExistingInstanceNames) {
|
if (fetchExistingInstanceNames) {
|
||||||
existingInstanceNames.value = await fetchExistingInstanceNames()
|
existingInstanceNames.value = await fetchExistingInstanceNames()
|
||||||
@@ -370,15 +534,13 @@ export function createCreationFlowContext(
|
|||||||
return { known }
|
return { known }
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchModpacks = options.searchModpacks!
|
|
||||||
const getProjectVersions = options.getProjectVersions!
|
|
||||||
|
|
||||||
const resolvedStageConfigs = disableClose
|
const resolvedStageConfigs = disableClose
|
||||||
? stageConfigs.map((stage) => ({ ...stage, disableClose: true }))
|
? stageConfigs.map((stage) => ({ ...stage, disableClose: true }))
|
||||||
: stageConfigs
|
: stageConfigs
|
||||||
|
|
||||||
const contextValue: CreationFlowContextValue = {
|
const contextValue: CreationFlowContextValue = {
|
||||||
flowType,
|
flowType,
|
||||||
|
formatMessage,
|
||||||
availableLoaders,
|
availableLoaders,
|
||||||
showSnapshotToggle,
|
showSnapshotToggle,
|
||||||
disableClose,
|
disableClose,
|
||||||
@@ -407,6 +569,9 @@ export function createCreationFlowContext(
|
|||||||
hideLoaderChips,
|
hideLoaderChips,
|
||||||
hideLoaderVersion,
|
hideLoaderVersion,
|
||||||
showSnapshots,
|
showSnapshots,
|
||||||
|
loaderVersionsCache,
|
||||||
|
paperSupportedVersions,
|
||||||
|
purpurSupportedVersions,
|
||||||
modpackSelection,
|
modpackSelection,
|
||||||
modpackFile,
|
modpackFile,
|
||||||
modpackFilePath,
|
modpackFilePath,
|
||||||
@@ -431,8 +596,11 @@ export function createCreationFlowContext(
|
|||||||
browseModpacks,
|
browseModpacks,
|
||||||
finish,
|
finish,
|
||||||
buildProperties,
|
buildProperties,
|
||||||
|
fetchLoaderMetadata,
|
||||||
|
prefetchLoaderMetadata,
|
||||||
searchModpacks,
|
searchModpacks,
|
||||||
getProjectVersions,
|
getProjectVersions,
|
||||||
|
getLoaderManifest,
|
||||||
}
|
}
|
||||||
|
|
||||||
return contextValue
|
return contextValue
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
createCreationFlowContext,
|
createCreationFlowContext,
|
||||||
type CreationFlowContextValue,
|
type CreationFlowContextValue,
|
||||||
type FlowType,
|
type FlowType,
|
||||||
|
type LoaderManifestResolver,
|
||||||
type ModpackSearchResult,
|
type ModpackSearchResult,
|
||||||
provideCreationFlowContext,
|
provideCreationFlowContext,
|
||||||
} from './creation-flow-context'
|
} from './creation-flow-context'
|
||||||
@@ -36,6 +37,7 @@ const props = withDefaults(
|
|||||||
fade?: 'standard' | 'warning' | 'danger'
|
fade?: 'standard' | 'warning' | 'danger'
|
||||||
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
|
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
|
||||||
getProjectVersions?: (projectId: string) => Promise<{ id: string }[]>
|
getProjectVersions?: (projectId: string) => Promise<{ id: string }[]>
|
||||||
|
getLoaderManifest?: LoaderManifestResolver
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
type: 'world',
|
type: 'world',
|
||||||
@@ -75,12 +77,14 @@ const ctx = createCreationFlowContext(
|
|||||||
onBack: props.onBack ?? undefined,
|
onBack: props.onBack ?? undefined,
|
||||||
searchModpacks: props.searchModpacks,
|
searchModpacks: props.searchModpacks,
|
||||||
getProjectVersions: props.getProjectVersions,
|
getProjectVersions: props.getProjectVersions,
|
||||||
|
getLoaderManifest: props.getLoaderManifest,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
provideCreationFlowContext(ctx)
|
provideCreationFlowContext(ctx)
|
||||||
|
|
||||||
async function show() {
|
async function show() {
|
||||||
await ctx.reset()
|
await ctx.reset()
|
||||||
|
void ctx.prefetchLoaderMetadata()
|
||||||
modal.value?.setStage(0)
|
modal.value?.setStage(0)
|
||||||
modal.value?.show()
|
modal.value?.show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
import { LeftArrowIcon, PlusIcon, RightArrowIcon } from '@modrinth/assets'
|
import { LeftArrowIcon, PlusIcon, RightArrowIcon } from '@modrinth/assets'
|
||||||
import { markRaw } from 'vue'
|
import { markRaw } from 'vue'
|
||||||
|
|
||||||
|
import { commonMessages } from '#ui/utils/common-messages'
|
||||||
|
|
||||||
import type { StageConfigInput } from '../../../base'
|
import type { StageConfigInput } from '../../../base'
|
||||||
import CustomSetupStage from '../components/CustomSetupStage.vue'
|
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 {
|
function isForwardBlocked(ctx: CreationFlowContextValue): boolean {
|
||||||
if (!ctx.selectedGameVersion.value) return true
|
if (!ctx.selectedGameVersion.value) return true
|
||||||
if (!ctx.hideLoaderChips.value && !ctx.selectedLoader.value) return true
|
if (!ctx.hideLoaderChips.value && !ctx.selectedLoader.value) return true
|
||||||
if (
|
if (!ctx.hideLoaderVersion.value && !ctx.selectedLoaderVersion.value) return true
|
||||||
!ctx.hideLoaderVersion.value &&
|
|
||||||
ctx.loaderVersionType.value === 'other' &&
|
|
||||||
!ctx.selectedLoaderVersion.value
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||||
id: 'custom-setup',
|
id: 'custom-setup',
|
||||||
title: (ctx) => flowTypeHeadings[ctx.flowType],
|
title: (ctx) => ctx.formatMessage(flowTypeHeadingMessages[ctx.flowType]),
|
||||||
stageContent: markRaw(CustomSetupStage),
|
stageContent: markRaw(CustomSetupStage),
|
||||||
skip: (ctx) =>
|
skip: (ctx) =>
|
||||||
ctx.setupType.value === 'modpack' ||
|
ctx.setupType.value === 'modpack' ||
|
||||||
@@ -27,7 +28,7 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
|||||||
ctx.isImportMode.value,
|
ctx.isImportMode.value,
|
||||||
cannotNavigateForward: isForwardBlocked,
|
cannotNavigateForward: isForwardBlocked,
|
||||||
leftButtonConfig: (ctx) => ({
|
leftButtonConfig: (ctx) => ({
|
||||||
label: 'Back',
|
label: ctx.formatMessage(commonMessages.backButton),
|
||||||
icon: LeftArrowIcon,
|
icon: LeftArrowIcon,
|
||||||
onClick: () => ctx.modal.value?.setStage('setup-type'),
|
onClick: () => ctx.modal.value?.setStage('setup-type'),
|
||||||
}),
|
}),
|
||||||
@@ -41,7 +42,7 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
|||||||
|
|
||||||
if (isInstance) {
|
if (isInstance) {
|
||||||
return {
|
return {
|
||||||
label: 'Create instance',
|
label: ctx.formatMessage(creationFlowMessages.createInstanceButton),
|
||||||
icon: PlusIcon,
|
icon: PlusIcon,
|
||||||
iconPosition: 'before' as const,
|
iconPosition: 'before' as const,
|
||||||
color: 'brand' as const,
|
color: 'brand' as const,
|
||||||
@@ -52,7 +53,9 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: goesToNextStage ? 'Continue' : 'Finish',
|
label: ctx.formatMessage(
|
||||||
|
goesToNextStage ? commonMessages.continueButton : creationFlowMessages.finishButton,
|
||||||
|
),
|
||||||
icon: goesToNextStage ? RightArrowIcon : null,
|
icon: goesToNextStage ? RightArrowIcon : null,
|
||||||
iconPosition: 'after' as const,
|
iconPosition: 'after' as const,
|
||||||
color: goesToNextStage ? undefined : ('brand' as const),
|
color: goesToNextStage ? undefined : ('brand' as const),
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { LeftArrowIcon, PlusIcon, RightArrowIcon } from '@modrinth/assets'
|
import { LeftArrowIcon, PlusIcon, RightArrowIcon } from '@modrinth/assets'
|
||||||
import { markRaw } from 'vue'
|
import { markRaw } from 'vue'
|
||||||
|
|
||||||
|
import { commonMessages } from '#ui/utils/common-messages'
|
||||||
|
|
||||||
import type { StageConfigInput } from '../../../base'
|
import type { StageConfigInput } from '../../../base'
|
||||||
import FinalConfigStage from '../components/FinalConfigStage.vue'
|
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 {
|
function isForwardBlocked(ctx: CreationFlowContextValue): boolean {
|
||||||
if (ctx.flowType === 'world' && !ctx.worldName.value.trim()) return true
|
if (ctx.flowType === 'world' && !ctx.worldName.value.trim()) return true
|
||||||
@@ -13,12 +19,12 @@ function isForwardBlocked(ctx: CreationFlowContextValue): boolean {
|
|||||||
|
|
||||||
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||||
id: 'final-config',
|
id: 'final-config',
|
||||||
title: (ctx) => flowTypeHeadings[ctx.flowType],
|
title: (ctx) => ctx.formatMessage(flowTypeHeadingMessages[ctx.flowType]),
|
||||||
stageContent: markRaw(FinalConfigStage),
|
stageContent: markRaw(FinalConfigStage),
|
||||||
skip: (ctx) => ctx.flowType === 'instance' || ctx.isImportMode.value,
|
skip: (ctx) => ctx.flowType === 'instance' || ctx.isImportMode.value,
|
||||||
cannotNavigateForward: isForwardBlocked,
|
cannotNavigateForward: isForwardBlocked,
|
||||||
leftButtonConfig: (ctx) => ({
|
leftButtonConfig: (ctx) => ({
|
||||||
label: 'Back',
|
label: ctx.formatMessage(commonMessages.backButton),
|
||||||
icon: LeftArrowIcon,
|
icon: LeftArrowIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (ctx.onBack) {
|
if (ctx.onBack) {
|
||||||
@@ -33,14 +39,15 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
|||||||
const isOnboarding = ctx.flowType === 'server-onboarding'
|
const isOnboarding = ctx.flowType === 'server-onboarding'
|
||||||
const isReset = ctx.flowType === 'reset-server'
|
const isReset = ctx.flowType === 'reset-server'
|
||||||
const isFinish = isWorld || isOnboarding || isReset
|
const isFinish = isWorld || isOnboarding || isReset
|
||||||
return {
|
const label = isWorld
|
||||||
label: isWorld
|
? ctx.formatMessage(creationFlowMessages.createWorldButton)
|
||||||
? 'Create world'
|
|
||||||
: isReset
|
: isReset
|
||||||
? 'Reset server'
|
? ctx.formatMessage(commonMessages.resetServerButton)
|
||||||
: isOnboarding
|
: isOnboarding
|
||||||
? 'Setup server'
|
? ctx.formatMessage(creationFlowMessages.setupServerButton)
|
||||||
: 'Continue',
|
: ctx.formatMessage(commonMessages.continueButton)
|
||||||
|
return {
|
||||||
|
label,
|
||||||
icon: isFinish ? PlusIcon : RightArrowIcon,
|
icon: isFinish ? PlusIcon : RightArrowIcon,
|
||||||
iconPosition: isFinish ? ('before' as const) : ('after' as const),
|
iconPosition: isFinish ? ('before' as const) : ('after' as const),
|
||||||
color: isReset ? ('red' as const) : isFinish ? ('brand' as const) : undefined,
|
color: isReset ? ('red' as const) : isFinish ? ('brand' as const) : undefined,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { DownloadIcon, LeftArrowIcon } from '@modrinth/assets'
|
import { DownloadIcon, LeftArrowIcon } from '@modrinth/assets'
|
||||||
import { markRaw } from 'vue'
|
import { markRaw } from 'vue'
|
||||||
|
|
||||||
|
import { commonMessages } from '#ui/utils/common-messages'
|
||||||
|
|
||||||
import type { StageConfigInput } from '../../../base'
|
import type { StageConfigInput } from '../../../base'
|
||||||
import ImportInstanceStage from '../components/ImportInstanceStage.vue'
|
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 {
|
function getSelectedCount(ctx: CreationFlowContextValue): number {
|
||||||
let count = 0
|
let count = 0
|
||||||
@@ -15,11 +17,11 @@ function getSelectedCount(ctx: CreationFlowContextValue): number {
|
|||||||
|
|
||||||
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||||
id: 'import-instance',
|
id: 'import-instance',
|
||||||
title: 'Import instance',
|
title: (ctx) => ctx.formatMessage(creationFlowMessages.importInstanceTitle),
|
||||||
stageContent: markRaw(ImportInstanceStage),
|
stageContent: markRaw(ImportInstanceStage),
|
||||||
skip: (ctx) => !ctx.isImportMode.value,
|
skip: (ctx) => !ctx.isImportMode.value,
|
||||||
leftButtonConfig: (ctx) => ({
|
leftButtonConfig: (ctx) => ({
|
||||||
label: 'Back',
|
label: ctx.formatMessage(commonMessages.backButton),
|
||||||
icon: LeftArrowIcon,
|
icon: LeftArrowIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
ctx.isImportMode.value = false
|
ctx.isImportMode.value = false
|
||||||
@@ -29,7 +31,10 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
|||||||
rightButtonConfig: (ctx) => {
|
rightButtonConfig: (ctx) => {
|
||||||
const count = getSelectedCount(ctx)
|
const count = getSelectedCount(ctx)
|
||||||
return {
|
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,
|
icon: DownloadIcon,
|
||||||
iconPosition: 'before' as const,
|
iconPosition: 'before' as const,
|
||||||
color: 'brand' as const,
|
color: 'brand' as const,
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { LeftArrowIcon } from '@modrinth/assets'
|
import { LeftArrowIcon } from '@modrinth/assets'
|
||||||
import { markRaw } from 'vue'
|
import { markRaw } from 'vue'
|
||||||
|
|
||||||
|
import { commonMessages } from '#ui/utils/common-messages'
|
||||||
|
|
||||||
import type { StageConfigInput } from '../../../base'
|
import type { StageConfigInput } from '../../../base'
|
||||||
import ModpackStage from '../components/ModpackStage.vue'
|
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> = {
|
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||||
id: 'modpack',
|
id: 'modpack',
|
||||||
title: 'Choose modpack',
|
title: (ctx) => ctx.formatMessage(creationFlowMessages.chooseModpackTitle),
|
||||||
stageContent: markRaw(ModpackStage),
|
stageContent: markRaw(ModpackStage),
|
||||||
skip: (ctx) => ctx.setupType.value !== 'modpack' || ctx.isImportMode.value,
|
skip: (ctx) => ctx.setupType.value !== 'modpack' || ctx.isImportMode.value,
|
||||||
leftButtonConfig: (ctx) => ({
|
leftButtonConfig: (ctx) => ({
|
||||||
label: 'Back',
|
label: ctx.formatMessage(commonMessages.backButton),
|
||||||
icon: LeftArrowIcon,
|
icon: LeftArrowIcon,
|
||||||
onClick: () => ctx.modal.value?.setStage('setup-type'),
|
onClick: () => ctx.modal.value?.setStage('setup-type'),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { markRaw } from 'vue'
|
|||||||
|
|
||||||
import type { StageConfigInput } from '../../../base'
|
import type { StageConfigInput } from '../../../base'
|
||||||
import SetupTypeStage from '../components/SetupTypeStage.vue'
|
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> = {
|
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
|
||||||
id: 'setup-type',
|
id: 'setup-type',
|
||||||
title: (ctx) => flowTypeHeadings[ctx.flowType],
|
title: (ctx) => ctx.formatMessage(flowTypeHeadingMessages[ctx.flowType]),
|
||||||
stageContent: markRaw(SetupTypeStage),
|
stageContent: markRaw(SetupTypeStage),
|
||||||
leftButtonConfig: null,
|
leftButtonConfig: null,
|
||||||
rightButtonConfig: null,
|
rightButtonConfig: null,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { computed } from 'vue'
|
|||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const { currentMember, projectV2, projectV3, refreshProject } = injectProjectPageContext()
|
const { currentMember, projectV2, projectV3, invalidate } = injectProjectPageContext()
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ const { saved, current, saving, reset, save } = useSavable(
|
|||||||
environment,
|
environment,
|
||||||
side_types_migration_review_status: 'reviewed',
|
side_types_migration_review_status: 'reviewed',
|
||||||
})
|
})
|
||||||
await refreshProject()
|
await invalidate()
|
||||||
reset()
|
reset()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError(err as Error)
|
handleError(err as Error)
|
||||||
|
|||||||
@@ -207,6 +207,8 @@ function emitReinstall(args?: { loader: string; lVersion: string; mVersion: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
|
void creationFlowRef.value?.ctx?.fetchLoaderMetadata('paper')
|
||||||
|
void creationFlowRef.value?.ctx?.fetchLoaderMetadata('purpur')
|
||||||
creationFlowRef.value?.show()
|
creationFlowRef.value?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mx-auto flex w-fit flex-col items-start gap-4 mt-16 max-w-[500px]">
|
<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">
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<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="rounded-[20px] border border-solid border-surface-5 bg-surface-3 p-5">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
@@ -41,11 +45,13 @@
|
|||||||
<ButtonStyled v-if="uploading" size="large">
|
<ButtonStyled v-if="uploading" size="large">
|
||||||
<button class="ml-auto" disabled>
|
<button class="ml-auto" disabled>
|
||||||
<SpinnerIcon class="animate-spin" />
|
<SpinnerIcon class="animate-spin" />
|
||||||
Uploading ({{ uploadPercent }}%)
|
{{ formatMessage(messages.uploadingProgress, { percent: uploadPercent }) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-else color="brand" size="large">
|
<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>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -66,7 +72,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Archon } from '@modrinth/api-client'
|
import type { Archon } from '@modrinth/api-client'
|
||||||
import { GlobeIcon, PackageIcon, RightArrowIcon, SpinnerIcon, UsersIcon } from '@modrinth/assets'
|
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 { useQueryClient } from '@tanstack/vue-query'
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
@@ -77,6 +89,73 @@ import { injectModrinthServerContext } from '#ui/providers'
|
|||||||
|
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const { addNotification } = injectNotificationManager()
|
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) {
|
async function searchModpacks(query: string, limit: number = 10) {
|
||||||
return client.labrinth.projects_v2.search({
|
return client.labrinth.projects_v2.search({
|
||||||
@@ -209,8 +288,8 @@ const onCreate = async (config: CreationFlowContextValue) => {
|
|||||||
await finalizeSetup()
|
await finalizeSetup()
|
||||||
} catch {
|
} catch {
|
||||||
addNotification({
|
addNotification({
|
||||||
title: 'Modpack upload failed',
|
title: formatMessage(messages.modpackUploadFailedTitle),
|
||||||
text: 'An unexpected error occurred while uploading. Please try again later.',
|
text: formatMessage(messages.modpackUploadFailedText),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
config.loading.value = false
|
config.loading.value = false
|
||||||
@@ -252,31 +331,29 @@ const onCreate = async (config: CreationFlowContextValue) => {
|
|||||||
await finalizeSetup()
|
await finalizeSetup()
|
||||||
} catch {
|
} catch {
|
||||||
addNotification({
|
addNotification({
|
||||||
title: 'Installation failed',
|
title: formatMessage(messages.installationFailedTitle),
|
||||||
text: 'An unexpected error occurred while installing. Please try again later.',
|
text: formatMessage(messages.installationFailedText),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
config.loading.value = false
|
config.loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const steps = [
|
const steps = computed(() => [
|
||||||
{
|
{
|
||||||
icon: PackageIcon,
|
icon: PackageIcon,
|
||||||
title: 'Choose what to play',
|
title: formatMessage(messages.chooseWhatToPlayTitle),
|
||||||
description:
|
description: formatMessage(messages.chooseWhatToPlayDescription),
|
||||||
'Pick your favorite modpack from Modrinth, or choose a loader and add the mods you want.',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: GlobeIcon,
|
icon: GlobeIcon,
|
||||||
title: 'Configure your world',
|
title: formatMessage(messages.configureWorldTitle),
|
||||||
description: 'Set up your world just like singleplayer. Choose your gamemode and world seed.',
|
description: formatMessage(messages.configureWorldDescription),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: UsersIcon,
|
icon: UsersIcon,
|
||||||
title: 'Invite your friends',
|
title: formatMessage(messages.inviteFriendsTitle),
|
||||||
description:
|
description: formatMessage(messages.inviteFriendsDescription),
|
||||||
"Share your server with friends by copying the address and letting them know which mods they'll need to join.",
|
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -491,6 +491,276 @@
|
|||||||
"content.selection-bar.selected-count-simple": {
|
"content.selection-bar.selected-count-simple": {
|
||||||
"defaultMessage": "{count, number} selected"
|
"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": {
|
"files.conflict-modal.header": {
|
||||||
"defaultMessage": "Extract summary"
|
"defaultMessage": "Extract summary"
|
||||||
},
|
},
|
||||||
@@ -3062,6 +3332,51 @@
|
|||||||
"servers.region.western-europe": {
|
"servers.region.western-europe": {
|
||||||
"defaultMessage": "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": {
|
"servers.setup.rate-limit.text": {
|
||||||
"defaultMessage": "You are being rate limited. Please try again later."
|
"defaultMessage": "You are being rate limited. Please try again later."
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user