feat: app onboarding and library (#6923)

* refactor: remove unused legacy onboarded flag

* sqlx prepare

* feat: add onboarding checklist app db and add vue provider

* feat: add welcome page

* feat: add onboarding checklist component

* feat: move checklist visibility source of truth in app db

* feat: split up import modpack button

* feat-small: style tweak

* feat: remove discover section from homepage and add library

* refactor: move welcome screen to component

* refactor: move grid display into /library

* refactor: rename existing components

* refactor: split up the current library page files

* feat: add create group modal

* feat: add instance group definitions

* feat: update accordian for instance groups

* feat: add delete group

* feat: implement rename instance groups

* refactor: move instance group api out

* feat: add context menu for groups

* feat: style instance items in library

* feat: add selected instance item state

* feat: implement action bar for selection

* feat: drag and drop to move between groups

* feat: show ungrouped with header

* feat: improve edit group name

* small fix

* small style fixes

* feat: improve selection behaviour

* fix: dont toggle group when context menu open

* feat: drag and drop multiple selected

* small

* feat: add remove from group in action bar

* feat: add delay on icon hiding in inline input

* feat: improve icon hiding delay

* fix: bad routes.js

* feat: implement sort and filter options

* refactor: dont prefix filenames

* feat: change open/collapse group target

* feat: improve ungrouped behaviour

* feat: add shift to select multiple instances

* fix: icon disappear transition

* feat: add esc while dragging to cancel

* feat: update sorting and moved label

* feat: update context menu

* fix: multiple context menus can be opened

* refactor: instance groups to be identified by ID instead of name, names become display data

* feat: creating new group always adds instance

* feat: create group button, fix instance selection, and increase group name max length

* feat: strokes

* fix: show getting started checklist in all sidebar

* feat: set size on microsoft login screen

* fix: use ButtonStyled in Chips component

* feat: creating groups with selected instances and fix some moving selection and duplication bugs

* pnpm prepr

* feat: add tooltip for instance already in group

* feat: create instance with content in creation flow modal

- search modpack becomes search projects
- creation flow modal context receives prepareProjectInstall and createProjectInstall callbacks, from content-install.ts (shared with ContentInstallModal)

* feat: update instance card styles

* refactor: separate out instance card content

* update styles

* refactor: instance card

* fix: width of instance

* feat: update empty state

* feat: update sort and grouping dropdowns

* feat: better context menu options

* feat: add group action buttons

* feat: add/remove favourites group

* remove manual sort

* feat: improve the "New group" jank

* feat: add group by instance type and change group sorting

* feat: welcome screen styles match figma

* feat: not signed in skins demo

* feat: new hosting empty state

* feat: new instance card style

* feat: add tooltip and spice up animation more

* feat: improve the transitions for groups after dropping many instances

* feat: set instance group membership for bulk moving

* feat: remove fade out

* fix: allow dragging from buttons inside instance card

* fix: library page load in behaviour

* small fix

* feat: add group ordering and reordering

* feat: add drag to reorder groups

* fix: switching page not clearing selected instances

* style fixes

* style fix

* fix spacing

* feat: jump in changes

* feat: add styled newly created instances

* feat: improve jump in cards styles

* fix: welcome screen style

* fix: empty state styles

* feat: max 5 jump in items

* feat: max new instances to 3

* fix: search icon

* feat: clear selection after move

* feat: skins list use check icon styles

* feat: list will be deleted instances and default sort last played

* fix: stroke

* remove: no more group settings in instance general settings

* fix: resolving modpack server install

* feat: icon editor

* feat: add three hue shifted variants

* fix: surprise me doesnt pick symbol

* feat: add customize icon to create instance modal

* feat: skins loading state style

* fix: welcome logo

* fix: copy

* fix: left over usage of ButtonStyled from main's refactor

* feat: some leaked merge stuff

* feat: server empty state styles

* remove: instance helper text

* remove: auto link in set up stage

* feat: auto focus on search when modal open

* fix: delete instance modal style

* remove: modpack meta repeating

* fix: icon

* fix: font weight

* fix: dropdown filter preview heights

* pnpm prepr

* fix: sign into modrinth flashing change in position

* feat: add avatar transparent corner gets padding

* feat: drag and hold over group opens group

* feat: add random icon for custom instance

* feat: update loading icon

* polish: empty state for search

* polish: jump in cards

* fix: avatar could skip checking for image if tauri loads it first

* feat: add icon customize and randomizer in content install modal for add to new instances

* fix: avatar cannot read image pixels due to cors

* refactor: popup notifications toast type for instance sharing notifications

* feat: apply random icons modal and add popup notification for it

* polish: style

* fix: set_webview_visible

* QA

* qa

* feat: add inner white 15 opacity stroke around avatar

* add to catalog

* pnpm prepr

* fix: app event import

* feat: add new icons and backgrounds for icon editor

* feat: recents list generate from randomized icons and save icon in editor + icon editor modal fixes

* qa

* refactor: recipe -> config

* fix: event bus

* polish

* feat: add safe guard against randomizing empty icons

* feat: navigate to instance after creation

* fix: icon file names

* i18n on welcome screen

* i18n pass on PR

* fix: inconsistent max group name length

* refactor: use sqlx macro

* feat: add instance icon background selection drag

* feat: update catalog order

* fix: remove gradient for welcome screen

* remove: frog

* fix: min width

* feat: update corner size

* fix: context menu styles

* prepr

* fix: instance name trimming

* reorder

* fix: instance card loading state stayed until hovering

* feat: add icon randomizer blacklist

* blog

* prepr

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Truman Gao
2026-08-18 01:08:09 +00:00
committed by GitHub
co-authored by Prospector
parent d5556271f9
commit e79e0d024b
347 changed files with 12456 additions and 4505 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 KiB

@@ -72,9 +72,6 @@ const props = withDefaults(
{
type: 'standard',
openByDefault: false,
buttonClass: null,
contentClass: null,
titleWrapperClass: null,
forceOpen: false,
overflowVisible: false,
divider: false,
+152 -5
View File
@@ -6,14 +6,16 @@
:style="`--_size: ${cssSize}`"
:class="{
circle: circle,
detecting: !hasDetectedCorners,
'no-shadow': noShadow,
padded: hasTransparentCorners,
raised: raised,
pixelated: pixelated,
}"
:src="src"
:alt="alt"
:loading="loading"
@load="updatePixelated"
@load="onLoad"
@error="onError"
/>
<svg
@@ -46,11 +48,22 @@
</template>
<script setup lang="ts">
import { computed, ref, useTemplateRef, watch } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
const pixelated = ref(false)
const hasTransparentCorners = ref(false)
const hasDetectedCorners = ref(false)
const img = useTemplateRef<HTMLImageElement>('img')
const failed = ref(false)
let detectionTimeout: number | undefined
let detectingSource: string | undefined
const DETECTION_TIMEOUT_MS = 2000
const CORNER_SAMPLE_SIZE = 8
defineExpose({
failed,
})
const props = withDefaults(
defineProps<{
@@ -88,21 +101,146 @@ const cssSize = computed(() => LEGACY_PRESETS[props.size] ?? props.size)
watch(
() => props.src,
() => {
clearDetectionTimeout()
detectingSource = undefined
failed.value = false
hasTransparentCorners.value = false
hasDetectedCorners.value = false
},
)
onMounted(() => {
const image = img.value
if (!image?.complete || hasDetectedCorners.value) return
if (image.naturalWidth > 0) {
onLoad()
} else {
failed.value = true
}
})
onBeforeUnmount(clearDetectionTimeout)
function clearDetectionTimeout() {
if (detectionTimeout !== undefined) {
window.clearTimeout(detectionTimeout)
detectionTimeout = undefined
}
}
function onError(e) {
clearDetectionTimeout()
detectingSource = undefined
console.log('Avatar image failed to load:', props.src, e)
failed.value = true
}
function updatePixelated() {
if (img.value && img.value.naturalWidth && img.value.naturalWidth < 32) {
function onLoad() {
const image = img.value
if (!image) return
const source = image.currentSrc
if (detectingSource === source) return
detectingSource = source
clearDetectionTimeout()
if (image.naturalWidth && image.naturalWidth < 32) {
pixelated.value = true
} else {
pixelated.value = false
}
if (canReadImagePixels(source)) {
const transparentCorners = detectTransparentCorners(image)
if (transparentCorners !== null) {
detectingSource = undefined
hasTransparentCorners.value = transparentCorners
hasDetectedCorners.value = true
return
}
}
const probe = new Image()
let detectionFinished = false
probe.crossOrigin = 'anonymous'
probe.onload = () => {
if (detectionFinished || img.value?.currentSrc !== source) return
detectionFinished = true
detectingSource = undefined
clearDetectionTimeout()
hasTransparentCorners.value = detectTransparentCorners(probe) ?? false
hasDetectedCorners.value = true
}
probe.onerror = () => {
if (detectionFinished || img.value?.currentSrc !== source) return
detectionFinished = true
detectingSource = undefined
clearDetectionTimeout()
hasDetectedCorners.value = true
}
detectionTimeout = window.setTimeout(() => {
if (img.value?.currentSrc !== source) return
detectionFinished = true
detectingSource = undefined
detectionTimeout = undefined
hasDetectedCorners.value = true
}, DETECTION_TIMEOUT_MS)
probe.src = source
}
function canReadImagePixels(source: string): boolean {
try {
const url = new URL(source, window.location.href)
return url.protocol === 'data:' || url.origin === window.location.origin
} catch {
return false
}
}
function detectTransparentCorners(image: HTMLImageElement): boolean | null {
if (image.naturalWidth < CORNER_SAMPLE_SIZE || image.naturalHeight < CORNER_SAMPLE_SIZE) {
return false
}
const canvas = document.createElement('canvas')
canvas.width = CORNER_SAMPLE_SIZE * 2
canvas.height = CORNER_SAMPLE_SIZE * 2
const context = canvas.getContext('2d', { willReadFrequently: true })
if (!context) return false
const right = image.naturalWidth - CORNER_SAMPLE_SIZE
const bottom = image.naturalHeight - CORNER_SAMPLE_SIZE
const drawCorner = (
sourceX: number,
sourceY: number,
destinationX: number,
destinationY: number,
) =>
context.drawImage(
image,
sourceX,
sourceY,
CORNER_SAMPLE_SIZE,
CORNER_SAMPLE_SIZE,
destinationX,
destinationY,
CORNER_SAMPLE_SIZE,
CORNER_SAMPLE_SIZE,
)
try {
drawCorner(0, 0, 0, 0)
drawCorner(right, 0, CORNER_SAMPLE_SIZE, 0)
drawCorner(0, bottom, 0, CORNER_SAMPLE_SIZE)
drawCorner(right, bottom, CORNER_SAMPLE_SIZE, CORNER_SAMPLE_SIZE)
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data
for (let alpha = 3; alpha < pixels.length; alpha += 4) {
if (pixels[alpha] !== 0) return false
}
return true
} catch {
return null
}
}
const tint = computed(() => {
@@ -128,7 +266,8 @@ function hash(str: string): number {
.avatar {
--_size: 2rem;
border: 1px solid var(--surface-5);
outline: 1px solid rgb(255 255 255 / 15%);
outline-offset: -1px;
background-color: var(--color-button-bg);
object-fit: contain;
border-radius: calc(16 / 96 * var(--_override-size, var(--_size)));
@@ -154,6 +293,14 @@ function hash(str: string): number {
image-rendering: pixelated;
}
&.detecting {
visibility: hidden;
}
&.padded {
padding: calc(6 / 96 * var(--_override-size, var(--_size)));
}
&.raised {
background-color: var(--color-raised-bg);
}
@@ -2,10 +2,10 @@
<span
v-if="showLabel"
class="flex h-10 items-center text-nowrap text-base font-medium text-primary"
:aria-label="useFilterIcon ? label : undefined"
:aria-label="useFilterIcon ? effectiveLabel : undefined"
>
<FilterIcon v-if="useFilterIcon" class="size-5 text-primary" aria-hidden="true" />
<template v-else>{{ label }}</template>
<template v-else>{{ effectiveLabel }}</template>
</span>
<template v-for="preview in appliedFilterPreviews" :key="preview.key">
@@ -15,7 +15,7 @@
native-type="button"
size="lg"
:class="effectivePreviewTriggerClass"
:aria-label="`Clear ${preview.label} filter`"
:aria-label="formatMessage(messages.clearNamedFilter, { name: preview.label })"
@click="clearFilterCategory(preview.key)"
>
<span class="font-semibold">{{ preview.label }}</span>
@@ -84,7 +84,7 @@
<button
type="button"
class="flex cursor-pointer items-center justify-center rounded border-none bg-transparent p-0.5 text-secondary transition-colors hover:text-contrast"
:aria-label="`Clear ${preview.label} filter`"
:aria-label="formatMessage(messages.clearNamedFilter, { name: preview.label })"
@click.stop="clearFilterCategory(preview.key)"
>
<XIcon class="size-4 text-primary" />
@@ -142,11 +142,11 @@
@keydown="handleAddMenuTriggerKeydown"
>
<PlusIcon />
{{ addLabel }}
{{ effectiveAddLabel }}
</Button>
<Button v-if="shouldShowClear" type="quiet" native-type="button" @click="clearAllFilters">{{
clearLabel
effectiveClearLabel
}}</Button>
</div>
@@ -226,7 +226,9 @@
v-model="categorySearchQuery"
:icon="SearchIcon"
type="text"
:placeholder="activeCategory.searchPlaceholder ?? 'Search...'"
:placeholder="
activeCategory.searchPlaceholder ?? formatMessage(messages.searchPlaceholder)
"
wrapper-class="grow bg-surface-4 mx-1"
input-class="ps-9 mx-1.5"
/>
@@ -327,7 +329,11 @@
class="border-0 bg-transparent p-0 text-sm font-semibold text-secondary shadow-none transition-colors hover:text-contrast"
@click="toggleSectionHeaderOptions(item)"
>
{{ areSectionHeaderOptionsSelected(item) ? 'Clear' : 'Select all' }}
{{
formatMessage(
areSectionHeaderOptionsSelected(item) ? messages.clear : messages.selectAll,
)
}}
</button>
</div>
<button
@@ -423,6 +429,7 @@ import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { Button, type ButtonElementHandle, type ButtonSize } from '#ui/components/base/buttons'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { useVirtualScroll } from '../../composables/virtual-scroll'
import MultiSelect, { type MultiSelectItem } from './MultiSelect.vue'
import StyledInput from './StyledInput.vue'
@@ -539,6 +546,21 @@ const TAILWIND_WIDTH_CLASS_SIZE: Record<string, string> = {
[DEFAULT_SUBMENU_CLASS]: DEFAULT_SUBMENU_WIDTH,
}
const { formatMessage } = useVIntl()
const messages = defineMessages({
filteredBy: { id: 'filter-bar.filtered-by', defaultMessage: 'Filtered by' },
add: { id: 'filter-bar.add', defaultMessage: 'Add' },
clear: { id: 'filter-bar.clear', defaultMessage: 'Clear' },
selectAll: { id: 'filter-bar.select-all', defaultMessage: 'Select all' },
noOptions: { id: 'filter-bar.no-options', defaultMessage: 'No options available.' },
noSearchResults: { id: 'filter-bar.no-search-results', defaultMessage: 'No options found.' },
searchPlaceholder: { id: 'filter-bar.search-placeholder', defaultMessage: 'Search...' },
clearNamedFilter: {
id: 'filter-bar.clear-named-filter',
defaultMessage: 'Clear {name} filter',
},
})
const props = withDefaults(
defineProps<{
modelValue: DropdownFilterBarValue
@@ -559,21 +581,26 @@ const props = withDefaults(
checkboxPosition?: 'left' | 'right'
}>(),
{
label: 'Filtered by',
addLabel: 'Add',
clearLabel: 'Clear',
showClear: false,
showLabel: true,
useFilterIcon: false,
addButtonSize: 'md',
applyImmediately: false,
showPreviewFilterIcon: false,
emptyOptionsLabel: 'No options available.',
emptySearchLabel: 'No options found.',
checkboxPosition: 'left',
},
)
const effectiveLabel = computed(() => props.label ?? formatMessage(messages.filteredBy))
const effectiveAddLabel = computed(() => props.addLabel ?? formatMessage(messages.add))
const effectiveClearLabel = computed(() => props.clearLabel ?? formatMessage(messages.clear))
const effectiveEmptyOptionsLabel = computed(
() => props.emptyOptionsLabel ?? formatMessage(messages.noOptions),
)
const effectiveEmptySearchLabel = computed(
() => props.emptySearchLabel ?? formatMessage(messages.noSearchResults),
)
const emit = defineEmits<{
'update:modelValue': [value: DropdownFilterBarValue]
clear: []
@@ -737,12 +764,12 @@ const activeCategoryOptionsListStyle = computed<CSSProperties | undefined>(() =>
const activeCategoryEmptyStateLabel = computed(() => {
const category = activeCategory.value
if (!category) {
return props.emptyOptionsLabel
return effectiveEmptyOptionsLabel.value
}
return category.searchable && categorySearchQuery.value.trim().length > 0
? (category.emptySearchLabel ?? props.emptySearchLabel)
: (category.emptyOptionsLabel ?? props.emptyOptionsLabel)
? (category.emptySearchLabel ?? effectiveEmptySearchLabel.value)
: (category.emptyOptionsLabel ?? effectiveEmptyOptionsLabel.value)
})
const submenuStyle = computed<CSSProperties>(() => {
@@ -788,9 +815,9 @@ const appliedFilterPreviews = computed(() =>
const hasAppliedFilters = computed(() => appliedFilterPreviews.value.length > 0)
const shouldShowClear = computed(() => hasAppliedFilters.value || props.showClear)
const DEFAULT_PREVIEW_TRIGGER_CLASS = 'max-w-[16rem]'
const DEFAULT_PREVIEW_TRIGGER_CLASS = 'max-w-[16rem] !rounded-xl'
const effectivePreviewTriggerClass = computed(
() => props.previewTriggerClass ?? DEFAULT_PREVIEW_TRIGGER_CLASS,
() => (props.previewTriggerClass ?? DEFAULT_PREVIEW_TRIGGER_CLASS) + ' h-9',
)
function cloneSelectedFilters(filters: DropdownFilterBarValue): DropdownFilterBarValue {
@@ -0,0 +1,248 @@
<script setup lang="ts">
import { EditIcon } from '@modrinth/assets'
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
const model = defineModel<string>({ default: '' })
const props = withDefaults(
defineProps<{
placeholder?: string
defaultValue?: string
maxWidth?: string
maxLength?: number
editLabel?: string
activationMode?: 'text' | 'icon' | 'manual'
inputClass?: string
buttonClass?: string
iconTextClass?: string
validate?: (value: string) => boolean
onChange?: (value: string) => boolean | void | Promise<boolean | void>
}>(),
{
placeholder: '',
defaultValue: '',
maxWidth: '100%',
maxLength: undefined,
editLabel: 'Edit',
activationMode: 'text',
validate: undefined,
onChange: undefined,
},
)
const isEditing = ref(false)
const isSaving = ref(false)
const isInvalid = ref(false)
const input = ref<HTMLInputElement>()
const isEditIconVisible = ref(false)
const draft = ref(model.value)
const originalValue = ref('')
let editIconHideTimeout: ReturnType<typeof setTimeout> | undefined
const displayValue = computed(() => model.value || props.defaultValue || props.placeholder)
const sizingValue = computed(() => {
if (isEditing.value) {
return draft.value || props.placeholder || ' '
}
return displayValue.value || ' '
})
watch(model, (value) => {
if (!isEditing.value) {
draft.value = value
}
})
function clearEditIconHideTimeout() {
if (editIconHideTimeout) {
clearTimeout(editIconHideTimeout)
editIconHideTimeout = undefined
}
}
function showEditIcon() {
isEditIconVisible.value = true
}
function keepEditIconVisible() {
clearEditIconHideTimeout()
isEditIconVisible.value = true
}
function hideEditIcon() {
if (!editIconHideTimeout) {
isEditIconVisible.value = false
}
}
function handleEditIconMouseleave() {
clearEditIconHideTimeout()
editIconHideTimeout = setTimeout(() => {
isEditIconVisible.value = false
editIconHideTimeout = undefined
}, 1000)
}
async function startEditing() {
if (isEditing.value) return
clearEditIconHideTimeout()
originalValue.value = model.value
draft.value = model.value
isInvalid.value = false
isEditing.value = true
await nextTick()
input.value?.focus()
input.value?.select()
}
async function applyValue() {
if (!isEditing.value || isSaving.value) return
const nextValue = (draft.value || props.defaultValue).trim()
if (nextValue === originalValue.value) {
model.value = nextValue
isEditing.value = false
isInvalid.value = false
return
}
if (props.validate && !props.validate(nextValue)) {
isInvalid.value = true
await nextTick()
input.value?.focus()
return
}
isSaving.value = true
isInvalid.value = false
try {
const accepted = await props.onChange?.(nextValue)
if (accepted === false) {
isInvalid.value = true
isSaving.value = false
await nextTick()
input.value?.focus()
return
}
model.value = nextValue
isEditing.value = false
} catch {
isInvalid.value = true
isSaving.value = false
await nextTick()
input.value?.focus()
} finally {
isSaving.value = false
}
}
function cancelEditing() {
if (isSaving.value) return
draft.value = originalValue.value
isInvalid.value = false
isEditing.value = false
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
event.preventDefault()
void applyValue()
return
}
if (event.key === 'Escape') {
event.preventDefault()
event.stopPropagation()
cancelEditing()
}
}
onUnmounted(clearEditIconHideTimeout)
defineExpose({
isEditing,
startEditing,
})
</script>
<template>
<div
:data-value="sizingValue"
class="relative flex h-6 min-w-3 min-h-0 max-w-full flex-col justify-center border-b font-medium"
:class="
isEditing
? [
'after:invisible after:block after:w-full after:whitespace-pre after:content-[attr(data-value)]',
isInvalid ? 'border-red' : 'border-contrast',
]
: 'border-transparent'
"
:style="{ maxWidth }"
>
<input
v-if="isEditing"
ref="input"
v-model="draft"
type="text"
:aria-invalid="isInvalid"
:aria-label="editLabel"
:disabled="isSaving"
:maxlength="maxLength"
:placeholder="placeholder"
class="absolute inset-0 top-px w-full !h-full !min-h-0 min-w-0 truncate bg-transparent !p-0 text-inherit !border-b-2 border-0 !border-brand !border-solid !shadow-none [font:inherit] !outline-none"
:class="inputClass"
@blur="applyValue"
@click.stop
@keydown="handleKeydown"
/>
<div
v-else-if="activationMode === 'icon'"
class="group/edit-icon flex min-w-0 max-w-full items-center"
@mouseenter="showEditIcon"
@mouseleave="hideEditIcon"
>
<span class="min-w-0 truncate" :class="iconTextClass" :title="model || displayValue">
{{ displayValue }}
</span>
<span
class="flex shrink-0 overflow-hidden transition-all duration-150"
:class="
isEditIconVisible
? 'max-w-6 translate-x-0 opacity-100'
: 'max-w-0 translate-x-1 opacity-0'
"
@mouseenter="keepEditIconVisible"
@mouseleave="handleEditIconMouseleave"
>
<button
type="button"
class="ml-1 flex size-5 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-secondary transition-colors hover:text-brand focus-visible:text-contrast"
:aria-label="`${editLabel}: ${displayValue}`"
@click.stop="startEditing"
>
<EditIcon class="size-5" aria-hidden="true" />
</button>
</span>
</div>
<button
v-else-if="activationMode === 'text'"
type="button"
class="flex w-full max-w-full items-center gap-2 truncate border-0 bg-transparent p-0 text-left text-inherit transition-colors hover:text-brand focus-visible:text-contrast [font:inherit]"
:class="buttonClass"
:aria-label="`${editLabel}: ${displayValue}`"
:title="model || displayValue"
@click="startEditing"
>
<span class="min-w-0 truncate">{{ displayValue }}</span>
</button>
<span v-else class="min-w-0 truncate" :title="model || displayValue">
{{ displayValue }}
</span>
</div>
</template>
@@ -83,7 +83,7 @@ defineOptions({
// When clickable is being clicked, give contents an effect
:first-child:active + .smart-clickable__contents:not(.smart-clickable__contents--disabled) {
scale: 0.98;
scale: var(--active-scale, 0.98);
}
}
</style>
@@ -90,7 +90,7 @@
v-if="!multiline && clearable && model && !disabled && !readonly && variant === 'filled'"
type="button"
class="absolute right-0.5 z-[1] p-2 touch-manipulation bg-transparent border-none text-secondary hover:text-contrast transition-colors cursor-pointer select-none"
aria-label="Clear input"
:aria-label="formatMessage(messages.clearInput)"
@click="clear"
>
<XIcon class="h-5 w-5" />
@@ -101,7 +101,7 @@
v-if="!multiline && variant === 'outlined'"
type="button"
class="flex touch-manipulation items-center justify-center px-2 bg-transparent border border-solid border-button-bg rounded-r-xl text-secondary hover:text-contrast transition-colors shrink-0"
:aria-label="clearable && model ? 'Clear input' : 'Search'"
:aria-label="formatMessage(clearable && model ? messages.clearInput : messages.search)"
:tabindex="clearable && model ? undefined : -1"
@click="clearable && model ? clear() : undefined"
>
@@ -119,6 +119,14 @@
import { SearchIcon, XIcon } from '@modrinth/assets'
import { type Component, computed, ref } from 'vue'
import { defineMessages, useVIntl } from '../../composables/i18n'
const { formatMessage } = useVIntl()
const messages = defineMessages({
clearInput: { id: 'input.clear', defaultMessage: 'Clear input' },
search: { id: 'input.search', defaultMessage: 'Search' },
})
const RING = {
error: 'ring-4 ring-red focus:ring-4 focus:ring-red',
none: 'outline-none focus:ring-4 focus:ring-brand-shadow',
@@ -184,6 +192,10 @@ const resizeClass = computed(
defineExpose({
focus: () => inputRef.value?.focus(),
select: () => {
inputRef.value?.focus()
inputRef.value?.select()
},
setValue: (v: string) => {
if (inputRef.value) inputRef.value.value = v
},
+1
View File
@@ -65,6 +65,7 @@ export { default as HeadingLink } from './HeadingLink.vue'
export { default as HorizontalRule } from './HorizontalRule.vue'
export { default as I18nDebugPanel } from './I18nDebugPanel.vue'
export { default as IconSelect } from './IconSelect.vue'
export { default as InlineEditableText } from './InlineEditableText.vue'
export { default as IntlFormatted } from './IntlFormatted.vue'
export { default as LoadingBar } from './LoadingBar.vue'
export { default as LoadingIndicator } from './LoadingIndicator.vue'
@@ -1,16 +1,81 @@
<template>
<div class="space-y-6">
<div
v-if="ctx.projectInstall.value"
class="flex items-center gap-2.5 rounded-[20px] bg-surface-2 p-3"
>
<div class="shrink-0">
<div
class="size-14 shrink-0 overflow-hidden rounded-2xl border border-solid border-surface-5"
>
<Avatar
v-if="ctx.projectInstall.value.iconUrl"
:src="ctx.projectInstall.value.iconUrl"
:alt="ctx.projectInstall.value.title"
size="100%"
class="!rounded-2xl"
no-shadow
/>
</div>
</div>
<div class="flex flex-col gap-1">
<span class="font-semibold text-contrast">
{{ ctx.projectInstall.value.title }}
</span>
<div
v-if="ctx.projectInstall.value.owner"
class="flex items-center gap-2 text-sm text-secondary"
>
<div class="flex items-center gap-1.5 text-inherit">
<Avatar
:src="ctx.projectInstall.value.owner.iconUrl"
:alt="ctx.projectInstall.value.owner.name"
size="1.25rem"
:circle="ctx.projectInstall.value.owner.circle"
no-shadow
/>
<span class="font-medium">{{ ctx.projectInstall.value.owner.name }}</span>
</div>
</div>
</div>
</div>
<!-- Instance-specific: Icon upload -->
<div v-if="ctx.flowType === 'instance'" class="flex items-center gap-4">
<Avatar :src="ctx.instanceIconUrl.value ?? undefined" size="5rem" />
<div class="flex flex-col gap-2">
<div v-if="ctx.flowType === 'instance'" class="flex items-center gap-2.5">
<div class="group relative size-[7.75rem] shrink-0">
<Avatar :src="ctx.instanceIconUrl.value ?? undefined" size="100%" no-shadow />
<div
v-if="ctx.instanceIconUrl.value"
class="pointer-events-none absolute right-1.5 top-1.5 opacity-0 transition-opacity group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100"
>
<Button
size="sm"
class="!p-2"
:aria-label="formatMessage(commonMessages.removeImageButton)"
@click="removeIcon"
>
<XIcon />
</Button>
</div>
</div>
<div class="flex flex-col gap-1.5">
<Button type="outlined" @click="triggerIconInput">
<UploadIcon />
{{ formatMessage(messages.selectIcon) }}
{{ formatMessage(messages.uploadIcon) }}
</Button>
<Button type="outlined" :disabled="!ctx.instanceIcon.value" @click="removeIcon">
<XIcon />
{{ formatMessage(messages.removeIcon) }}
<Button
type="outlined"
:disabled="randomizing"
class="disabled:!cursor-defcaault"
@click="randomizeIcon"
>
<SpinnerIcon v-if="randomizing" class="animate-spin" />
<RefreshCwIcon v-else />
{{ formatMessage(messages.randomizeIcon) }}
</Button>
<Button type="outlined" @click="ctx.customizeInstanceIcon?.()">
<PaletteIcon />
{{ formatMessage(messages.customizeIcon) }}
</Button>
</div>
</div>
@@ -145,7 +210,15 @@
<script setup lang="ts">
import type { Paper } from '@modrinth/api-client'
import { EyeIcon, EyeOffIcon, UploadIcon, XIcon } from '@modrinth/assets'
import {
EyeIcon,
EyeOffIcon,
PaletteIcon,
RefreshCwIcon,
SpinnerIcon,
UploadIcon,
XIcon,
} from '@modrinth/assets'
import { commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
import { computed, onMounted, ref, watch } from 'vue'
@@ -177,13 +250,17 @@ const {
} = ctx
const messages = defineMessages({
selectIcon: {
uploadIcon: {
id: 'creation-flow.modal.custom-setup.icon.select',
defaultMessage: 'Select icon',
defaultMessage: 'Upload',
},
removeIcon: {
id: 'creation-flow.modal.custom-setup.icon.remove',
defaultMessage: 'Remove icon',
randomizeIcon: {
id: 'creation-flow.modal.custom-setup.icon.randomize',
defaultMessage: 'Randomize',
},
customizeIcon: {
id: 'creation-flow.modal.custom-setup.icon.customize',
defaultMessage: 'Customize',
},
nameLabel: {
id: 'creation-flow.modal.custom-setup.name.label',
@@ -265,6 +342,9 @@ function formatLoaderVersionTypeLabel(type: LoaderVersionType): string {
// For instance flow, prepend 'vanilla' to available loaders.
// For server flows, vanilla is a separate option in the setup type stage, so exclude it here.
const effectiveLoaders = computed(() => {
if (ctx.projectInstall.value) {
return ctx.projectInstall.value.compatibleLoaders
}
if (ctx.flowType === 'instance') {
return ['vanilla', ...ctx.availableLoaders.filter((l) => l !== 'vanilla')]
}
@@ -277,6 +357,9 @@ const effectiveLoaders = computed(() => {
// Pre-select loader and game version from initial values
onMounted(() => {
debug('mounted, initialLoader:', ctx.initialLoader, 'initialGameVersion:', ctx.initialGameVersion)
if (ctx.flowType === 'instance') {
void randomizeIcon()
}
if (!selectedLoader.value) {
if (ctx.initialLoader) {
selectedLoader.value = ctx.initialLoader
@@ -321,6 +404,23 @@ function removeIcon() {
ctx.instanceIconPath.value = null
}
const randomizing = ref(false)
async function randomizeIcon() {
if (!ctx.randomizeInstanceIcon || randomizing.value) return
randomizing.value = true
try {
const generated = await ctx.randomizeInstanceIcon()
if (!generated) return
ctx.instanceIcon.value = null
ctx.instanceIconUrl.value = generated.previewUrl
ctx.instanceIconPath.value = generated.path
} finally {
randomizing.value = false
}
}
const loaderVersionsLoading = ref(false)
const loaderVersionsData = ref<LoaderVersionEntry[]>([])
@@ -333,6 +433,7 @@ function toApiLoaderName(loader: string): string {
}
const gameVersionsLoading = computed(() => {
if (ctx.projectInstall.value) return false
const loader = selectedLoader.value
if (!loader || loader === 'vanilla') return false
if (loader === 'paper') return ctx.paperSupportedVersions.value === null
@@ -342,6 +443,16 @@ const gameVersionsLoading = computed(() => {
// Game versions from tags provider, filtered by loader support
const gameVersionOptions = computed<ComboboxOption<string>[]>(() => {
if (ctx.projectInstall.value) {
const versions =
ctx.showSnapshots.value || ctx.projectInstall.value.releaseGameVersions.size === 0
? ctx.projectInstall.value.gameVersions
: ctx.projectInstall.value.gameVersions.filter((version) =>
ctx.projectInstall.value!.releaseGameVersions.has(version),
)
return versions.map((version) => ({ value: version, label: version }))
}
const versions = ctx.showSnapshots.value
? tags.gameVersions.value
: tags.gameVersions.value.filter((v) => v.version_type === 'release')
@@ -506,6 +617,7 @@ function getLoaderVersionsForGameVersion(
watch(
() => selectedLoader.value,
async (loader) => {
if (ctx.projectInstall.value) return
await fetchLoaderMetadata(loader)
},
{ immediate: true },
@@ -521,6 +633,7 @@ watch(
loaderVersionsData.value = []
selectedLoaderVersion.value = null
if (ctx.projectInstall.value) return
if (!loader || !gameVersion || loader === 'vanilla') return
loaderVersionsLoading.value = true
@@ -1,10 +1,53 @@
<template>
<div class="flex flex-col gap-4">
<span class="font-semibold text-contrast">
{{ formatMessage(messages.knownProjectPrompt) }}
</span>
<Combobox
ref="projectSearchCombobox"
v-model="ctx.projectSearchProjectId.value"
v-tooltip="ctx.finishDisabled.value ? ctx.finishDisabledTooltip.value : undefined"
:options="ctx.projectSearchOptions.value"
searchable
show-search-icon
:show-chevron="false"
:disabled="ctx.finishDisabled.value"
:search-placeholder="formatMessage(messages.searchProjectPlaceholder)"
:no-options-message="
searchLoading
? formatMessage(commonMessages.loadingLabel)
: formatMessage(messages.noResultsFound)
"
:disable-search-filter="true"
@search-input="handleSearch"
>
<template #option-suffix="{ item }">
<div
class="flex shrink-0 items-center gap-1.5 text-sm font-semibold text-secondary opacity-0 transition-opacity group-hover/option:opacity-100 group-data-[focused=true]/option:opacity-100"
>
<span>
{{
formatMessage(
isModpackOption(item.value) ? messages.installModpack : messages.createInstance,
)
}}
</span>
<DownloadIcon v-if="isModpackOption(item.value)" class="size-5 shrink-0" />
<RightArrowIcon v-else class="size-5 shrink-0" />
</div>
</template>
</Combobox>
<div class="flex items-center gap-3">
<div class="h-[1px] w-full flex-1 bg-surface-5" />
<span class="text-sm text-secondary">{{ formatMessage(commonMessages.orLabel) }}</span>
<div class="h-[1px] w-full flex-1 bg-surface-5" />
</div>
<span class="font-semibold text-contrast">
{{ setupTypeTitle }}
</span>
<!-- Instance flow options -->
<template v-if="ctx.flowType === 'instance'">
<div class="flex flex-col gap-3">
<BigOptionButton
@@ -14,10 +57,16 @@
@click="setSetupType('custom')"
/>
<BigOptionButton
:icon="PackageIcon"
:icon="CompassIcon"
:title="formatMessage(messages.modpackBaseTitle)"
:description="formatMessage(messages.modpackBaseDescription)"
@click="setSetupType('modpack')"
@click="browseModpacks"
/>
<BigOptionButton
:icon="UploadIcon"
:title="formatMessage(messages.uploadModpackTitle)"
:description="formatMessage(messages.uploadModpackDescription)"
@click="triggerFileInput"
/>
<BigOptionButton
:icon="BoxImportIcon"
@@ -26,19 +75,21 @@
@click="ctx.setImportMode()"
/>
</div>
<span class="text-sm text-secondary">
{{ formatMessage(messages.instanceDescription) }}
</span>
</template>
<!-- World / Server onboarding flow options -->
<template v-else>
<div class="flex flex-col gap-3">
<BigOptionButton
:icon="PackageIcon"
:icon="CompassIcon"
:title="formatMessage(messages.modpackBaseTitle)"
:description="formatMessage(messages.modpackBaseDescription)"
@click="setSetupType('modpack')"
@click="browseModpacks"
/>
<BigOptionButton
:icon="UploadIcon"
:title="formatMessage(messages.uploadModpackTitle)"
:description="formatMessage(messages.uploadModpackDescription)"
@click="triggerFileInput"
/>
<BigOptionButton
:icon="BoxesIcon"
@@ -58,21 +109,55 @@
</template>
<script setup lang="ts">
import { BoxesIcon, BoxIcon, BoxImportIcon, PackageIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@modrinth/ui'
import { computed } from 'vue'
import {
BoxesIcon,
BoxIcon,
BoxImportIcon,
CompassIcon,
DownloadIcon,
RightArrowIcon,
UploadIcon,
} from '@modrinth/assets'
import { commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
import { computed, defineAsyncComponent, h, onMounted, ref, watch } from 'vue'
import { useDebugLogger } from '#ui/composables/debug-logger'
import { injectFilePicker } from '../../../../providers'
import BigOptionButton from '../../../base/BigOptionButton.vue'
import Combobox from '../../../base/Combobox.vue'
import { injectCreationFlowContext } from '../creation-flow-context'
const debug = useDebugLogger('SetupTypeStage')
const ctx = injectCreationFlowContext()
const filePicker = injectFilePicker()
const { setSetupType: _setSetupType } = ctx
const { formatMessage } = useVIntl()
const searchLoading = ref(false)
const projectSearchCombobox = ref<{ $el: HTMLElement }>()
const messages = defineMessages({
knownProjectPrompt: {
id: 'creation-flow.modal.project.known-project.prompt',
defaultMessage: 'Already know what you want to play?',
},
searchProjectPlaceholder: {
id: 'creation-flow.modal.project.search.placeholder',
defaultMessage: 'Search mods, modpacks, and more...',
},
noResultsFound: {
id: 'creation-flow.modal.project.search.no-results',
defaultMessage: 'No results found',
},
installModpack: {
id: 'creation-flow.modal.project.search.install-modpack',
defaultMessage: 'Install modpack',
},
createInstance: {
id: 'creation-flow.modal.project.search.create-instance',
defaultMessage: 'Create instance',
},
instanceTypeTitle: {
id: 'creation-flow.modal.setup-type.title.instance',
defaultMessage: 'Choose instance type',
@@ -95,11 +180,19 @@ const messages = defineMessages({
},
modpackBaseTitle: {
id: 'creation-flow.modal.setup-type.option.modpack-base.title',
defaultMessage: 'Install modpack',
defaultMessage: 'Start from a mod or modpack',
},
modpackBaseDescription: {
id: 'creation-flow.modal.setup-type.option.modpack-base.description',
defaultMessage: 'Browse modpacks on Modrinth or import one from a file.',
defaultMessage: 'Choose a project and well use its latest version.',
},
uploadModpackTitle: {
id: 'creation-flow.modal.setup-type.option.upload-modpack.title',
defaultMessage: 'Upload a modpack',
},
uploadModpackDescription: {
id: 'creation-flow.modal.setup-type.option.upload-modpack.description',
defaultMessage: 'Install a modpack from an .mrpack file on your device.',
},
importInstanceTitle: {
id: 'creation-flow.modal.setup-type.option.import-instance.title',
@@ -109,10 +202,6 @@ const messages = defineMessages({
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',
@@ -133,8 +222,132 @@ const setupTypeTitle = computed(() => {
return formatMessage(messages.worldTypeTitle)
})
function setSetupType(type: 'modpack' | 'custom' | 'vanilla') {
function isModpackOption(projectId: string) {
return ctx.projectSearchHits.value[projectId]?.projectType === 'modpack'
}
function setSetupType(type: 'custom' | 'vanilla') {
debug('selected:', type)
_setSetupType(type)
}
function selectModpack() {
debug('selected: modpack')
_setSetupType('modpack')
}
function proceedWithModpack() {
if (ctx.finishDisabled.value) return
if (ctx.flowType === 'instance') {
ctx.finish()
} else {
ctx.modal.value?.setStage('final-config')
}
}
function browseModpacks() {
if (ctx.finishDisabled.value) return
selectModpack()
ctx.browseModpacks()
}
async function triggerFileInput() {
if (ctx.finishDisabled.value) return
const picked = await filePicker.pickModpackFile({
readFile: ctx.flowType !== 'instance',
})
if (!picked) return
selectModpack()
ctx.modpackFile.value = picked.file ?? null
ctx.modpackFilePath.value = picked.path ?? null
proceedWithModpack()
}
async function search(query: string) {
try {
if (!query.trim()) {
ctx.projectSearchOptions.value = []
return
}
const results = await ctx.searchProjects(query.trim(), 10)
ctx.projectSearchHits.value = {}
for (const hit of results.hits) {
ctx.projectSearchHits.value[hit.project_id] = {
title: hit.title,
iconUrl: hit.icon_url,
latestVersion: hit.latest_version,
projectType: hit.project_type ?? 'modpack',
}
}
ctx.projectSearchOptions.value = results.hits.map((hit) => ({
label: hit.title,
value: hit.project_id,
icon: defineAsyncComponent(() =>
Promise.resolve({
setup: () => () =>
h('img', {
src: hit.icon_url,
alt: hit.title,
class: 'h-5 w-5 rounded',
}),
}),
),
}))
} catch (error) {
debug('project search failed:', error)
ctx.projectSearchOptions.value = []
} finally {
searchLoading.value = false
}
}
async function handleSearch(query: string) {
searchLoading.value = true
await search(query)
}
onMounted(() => {
ctx.projectSearchProjectId.value = undefined
search('')
setTimeout(() => {
projectSearchCombobox.value?.$el.querySelector('input')?.focus()
}, 150)
})
watch(
() => ctx.projectSearchProjectId.value,
async (projectId, oldProjectId) => {
if (projectId === oldProjectId) return
if (!projectId) return
const hit = ctx.projectSearchHits.value[projectId]
if (ctx.flowType === 'instance') {
void ctx.selectProject(projectId, hit?.projectType ?? 'mod')
return
}
try {
const versions = await ctx.getProjectVersions(projectId)
if (ctx.projectSearchProjectId.value !== projectId || versions.length === 0) return
selectModpack()
ctx.modpackSelection.value = {
projectId,
versionId: versions[0].id,
name: hit?.title ?? '',
iconUrl: hit?.iconUrl,
}
proceedWithModpack()
} catch (error) {
debug('failed to load project versions:', error)
}
},
)
</script>
@@ -12,7 +12,7 @@ import {
} from '#ui/composables/i18n'
import { formatLoaderLabel } from '#ui/utils/loaders'
import { createContext, injectModrinthClient } from '../../../providers'
import { createContext, injectModrinthClient, injectNotificationManager } from '../../../providers'
import type { ImportableLauncher } from '../../../providers/instance-import'
import type { MultiStageModal, StageConfigInput } from '../../base'
import type { ComboboxOption } from '../../base/Combobox.vue'
@@ -81,10 +81,6 @@ export const creationFlowMessages = defineMessages({
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> = {
@@ -101,15 +97,17 @@ export interface ModpackSelection {
iconUrl?: string
}
export interface ModpackSearchHit {
export interface ProjectSearchHit {
title: string
iconUrl?: string
latestVersion?: string
projectType: string
}
export interface ModpackSearchResult {
export interface ProjectSearchResult {
hits: {
project_id: string
project_type?: string
title: string
icon_url: string
latest_version?: string
@@ -119,6 +117,35 @@ export interface ModpackSearchResult {
limit: number
}
export interface ProjectInstallSelection {
projectId: string
title: string
iconUrl?: string | null
link: string
owner?: {
name: string
iconUrl?: string
circle?: boolean
link: string | (() => void)
} | null
compatibleLoaders: string[]
gameVersions: string[]
releaseGameVersions: Set<string>
}
export interface ProjectInstallCreateData {
name: string
iconPath: string | null
iconPreviewUrl: string | null
loader: string
gameVersion: string
}
export interface GeneratedInstanceIcon {
path: string
previewUrl: string
}
export interface CreationFlowContextValue {
// Flow
flowType: FlowType
@@ -152,6 +179,8 @@ export interface CreationFlowContextValue {
instanceIcon: Ref<File | null>
instanceIconUrl: Ref<string | null>
instanceIconPath: Ref<string | null>
randomizeInstanceIcon: (() => Promise<GeneratedInstanceIcon | null>) | null
customizeInstanceIcon: (() => void) | null
// Loader/version state (custom setup)
selectedLoader: Ref<string | null>
@@ -169,13 +198,12 @@ export interface CreationFlowContextValue {
modpackSelection: Ref<ModpackSelection | null>
modpackFile: Ref<File | null>
modpackFilePath: Ref<string | null>
projectInstall: Ref<ProjectInstallSelection | null>
// Modpack search state (persisted across stage navigation)
modpackSearchProjectId: Ref<string | undefined>
modpackSearchVersionId: Ref<string | undefined>
modpackSearchOptions: Ref<ComboboxOption<string>[]>
modpackVersionOptions: Ref<ComboboxOption<string>[]>
modpackSearchHits: Ref<Record<string, ModpackSearchHit>>
// Project search state (persisted across stage navigation)
projectSearchProjectId: Ref<string | undefined>
projectSearchOptions: Ref<ComboboxOption<string>[]>
projectSearchHits: Ref<Record<string, ProjectSearchHit>>
// Import state (instance flow only)
importLaunchers: Ref<ImportableLauncher[]>
@@ -206,13 +234,14 @@ export interface CreationFlowContextValue {
setSetupType: (type: SetupType) => void
setImportMode: () => void
browseModpacks: () => void
selectProject: (projectId: string, projectType: string) => Promise<void>
finish: () => void
buildProperties: () => Archon.Content.v1.PropertiesFields
fetchLoaderMetadata: (loader?: string | null) => Promise<void>
prefetchLoaderMetadata: () => Promise<void>
// Platform-provided search
searchModpacks: (query: string, limit?: number) => Promise<ModpackSearchResult>
searchProjects: (query: string, limit?: number) => Promise<ProjectSearchResult>
getProjectVersions: (projectId: string) => Promise<{ id: string }[]>
getLoaderManifest: LoaderManifestResolver | null
}
@@ -232,9 +261,16 @@ export interface CreationFlowOptions {
initialGameVersion?: string
fetchExistingInstanceNames?: () => Promise<string[]>
onBack?: () => void
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
searchProjects?: (query: string, limit?: number) => Promise<ProjectSearchResult>
prepareProjectInstall?: (
projectId: string,
projectType: string,
) => Promise<ProjectInstallSelection | null>
createProjectInstall?: (data: ProjectInstallCreateData) => Promise<void>
getProjectVersions?: (projectId: string) => Promise<{ id: string }[]>
getLoaderManifest?: LoaderManifestResolver
randomizeInstanceIcon?: () => Promise<GeneratedInstanceIcon | null>
customizeInstanceIcon?: () => void
finishDisabled?: ComputedRef<boolean>
finishDisabledTooltip?: ComputedRef<string | undefined>
}
@@ -250,6 +286,7 @@ export function createCreationFlowContext(
): CreationFlowContextValue {
const debug = useDebugLogger('CreationFlow')
const client = injectModrinthClient()
const { handleError } = injectNotificationManager()
const queryClient = useQueryClient()
const { formatMessage } = useVIntl()
const availableLoaders = options.availableLoaders ?? ['fabric', 'neoforge', 'forge', 'quilt']
@@ -259,7 +296,11 @@ export function createCreationFlowContext(
const initialLoader = options.initialLoader ?? null
const initialGameVersion = options.initialGameVersion ?? null
const onBack = options.onBack ?? null
const searchModpacks = options.searchModpacks!
const randomizeInstanceIcon = options.randomizeInstanceIcon ?? null
const customizeInstanceIcon = options.customizeInstanceIcon ?? null
const searchProjects = options.searchProjects!
const prepareProjectInstall = options.prepareProjectInstall
const createProjectInstall = options.createProjectInstall
const getProjectVersions = options.getProjectVersions!
const getLoaderManifest = options.getLoaderManifest ?? null
const finishDisabled = options.finishDisabled ?? computed(() => false)
@@ -321,13 +362,12 @@ export function createCreationFlowContext(
const modpackSelection = ref<ModpackSelection | null>(null)
const modpackFile = ref<File | null>(null)
const modpackFilePath = ref<string | null>(null)
const projectInstall = ref<ProjectInstallSelection | null>(null)
// Modpack search state (persisted across stage navigation)
const modpackSearchProjectId = ref<string | undefined>()
const modpackSearchVersionId = ref<string | undefined>()
const modpackSearchOptions = ref<ComboboxOption<string>[]>([])
const modpackVersionOptions = ref<ComboboxOption<string>[]>([])
const modpackSearchHits = ref<Record<string, ModpackSearchHit>>({})
// Project search state (persisted across stage navigation)
const projectSearchProjectId = ref<string | undefined>()
const projectSearchOptions = ref<ComboboxOption<string>[]>([])
const projectSearchHits = ref<Record<string, ProjectSearchHit>>({})
// Import state (instance flow only)
const importLaunchers = ref<ImportableLauncher[]>([])
@@ -344,7 +384,10 @@ export function createCreationFlowContext(
// hideLoaderVersion: hides the loader version section (vanilla world type OR vanilla selected as loader chip)
const hideLoaderVersion = computed(
() => setupType.value === 'vanilla' || selectedLoader.value === 'vanilla',
() =>
setupType.value === 'vanilla' ||
selectedLoader.value === 'vanilla' ||
projectInstall.value !== null,
)
function toApiLoaderName(loader: string): string {
@@ -454,11 +497,10 @@ export function createCreationFlowContext(
modpackSelection.value = null
modpackFile.value = null
modpackFilePath.value = null
modpackSearchProjectId.value = undefined
modpackSearchVersionId.value = undefined
modpackSearchOptions.value = []
modpackVersionOptions.value = []
modpackSearchHits.value = {}
projectInstall.value = null
projectSearchProjectId.value = undefined
projectSearchOptions.value = []
projectSearchHits.value = {}
// Import state
importLaunchers.value = []
@@ -474,12 +516,12 @@ export function createCreationFlowContext(
function setSetupType(type: SetupType) {
debug('setSetupType:', type)
isImportMode.value = false
projectInstall.value = null
setupType.value = type
if (type === 'modpack') {
selectedLoader.value = null
selectedLoaderVersion.value = null
loaderVersionType.value = 'stable'
modal.value?.setStage('modpack')
} else {
modpackSelection.value = null
modpackFile.value = null
@@ -506,6 +548,37 @@ export function createCreationFlowContext(
emit.browseModpacks()
}
async function selectProject(projectId: string, projectType: string) {
if (!prepareProjectInstall) return
try {
const selection = await prepareProjectInstall(projectId, projectType)
if (selection) {
setProjectInstall(selection)
} else {
modal.value?.hide()
}
} catch (error) {
projectSearchProjectId.value = undefined
handleError(error as Error)
}
}
function setProjectInstall(selection: ProjectInstallSelection) {
projectInstall.value = selection
projectSearchProjectId.value = undefined
setupType.value = 'custom'
isImportMode.value = false
modpackSelection.value = null
instanceName.value = selection.title
selectedLoader.value = selection.compatibleLoaders[0] ?? null
selectedGameVersion.value =
selection.gameVersions.find((version) => selection.releaseGameVersions.has(version)) ??
selection.gameVersions[0] ??
null
modal.value?.setStage('custom-setup')
}
function finish() {
if (finishDisabled.value) return
@@ -518,6 +591,22 @@ export function createCreationFlowContext(
hasModpackFile: !!modpackFile.value,
})
loading.value = true
if (
projectInstall.value &&
createProjectInstall &&
selectedLoader.value &&
selectedGameVersion.value
) {
modal.value?.hide()
void createProjectInstall({
name: instanceName.value.trim() || projectInstall.value.title,
iconPath: instanceIconPath.value,
iconPreviewUrl: instanceIconUrl.value,
loader: selectedLoader.value,
gameVersion: selectedGameVersion.value,
}).catch(handleError)
return
}
emit.create(contextValue)
}
@@ -569,6 +658,8 @@ export function createCreationFlowContext(
instanceIcon,
instanceIconUrl,
instanceIconPath,
randomizeInstanceIcon,
customizeInstanceIcon,
selectedLoader,
selectedGameVersion,
loaderVersionType,
@@ -582,11 +673,10 @@ export function createCreationFlowContext(
modpackSelection,
modpackFile,
modpackFilePath,
modpackSearchProjectId,
modpackSearchVersionId,
modpackSearchOptions,
modpackVersionOptions,
modpackSearchHits,
projectInstall,
projectSearchProjectId,
projectSearchOptions,
projectSearchHits,
importLaunchers,
importSelectedInstances,
importSearchQuery,
@@ -603,11 +693,12 @@ export function createCreationFlowContext(
setSetupType,
setImportMode,
browseModpacks,
selectProject,
finish,
buildProperties,
fetchLoaderMetadata,
prefetchLoaderMetadata,
searchModpacks,
searchProjects,
getProjectVersions,
getLoaderManifest,
}
@@ -19,7 +19,9 @@ import {
type CreationFlowContextValue,
type FlowType,
type LoaderManifestResolver,
type ModpackSearchResult,
type ProjectInstallCreateData,
type ProjectInstallSelection,
type ProjectSearchResult,
provideCreationFlowContext,
} from './creation-flow-context'
@@ -35,9 +37,16 @@ const props = withDefaults(
fetchExistingInstanceNames?: () => Promise<string[]>
onBack?: (() => void) | null
fade?: 'standard' | 'warning' | 'danger'
searchModpacks?: (query: string, limit?: number) => Promise<ModpackSearchResult>
searchProjects?: (query: string, limit?: number) => Promise<ProjectSearchResult>
prepareProjectInstall?: (
projectId: string,
projectType: string,
) => Promise<ProjectInstallSelection | null>
createProjectInstall?: (data: ProjectInstallCreateData) => Promise<void>
getProjectVersions?: (projectId: string) => Promise<{ id: string }[]>
getLoaderManifest?: LoaderManifestResolver
randomizeInstanceIcon?: () => Promise<{ path: string; previewUrl: string } | null>
customizeInstanceIcon?: () => void
finishDisabled?: boolean
finishDisabledTooltip?: string
}>(),
@@ -51,6 +60,8 @@ const props = withDefaults(
initialGameVersion: undefined,
fetchExistingInstanceNames: undefined,
onBack: null,
randomizeInstanceIcon: undefined,
customizeInstanceIcon: undefined,
},
)
@@ -77,9 +88,13 @@ const ctx = createCreationFlowContext(
initialGameVersion: props.initialGameVersion,
fetchExistingInstanceNames: props.fetchExistingInstanceNames,
onBack: props.onBack ?? undefined,
searchModpacks: props.searchModpacks,
searchProjects: props.searchProjects,
prepareProjectInstall: props.prepareProjectInstall,
createProjectInstall: props.createProjectInstall,
getProjectVersions: props.getProjectVersions,
getLoaderManifest: props.getLoaderManifest,
randomizeInstanceIcon: props.randomizeInstanceIcon,
customizeInstanceIcon: props.customizeInstanceIcon,
finishDisabled: computed(() => props.finishDisabled ?? false),
finishDisabledTooltip: computed(() => props.finishDisabledTooltip),
},
@@ -72,5 +72,5 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
},
}
},
maxWidth: '520px',
maxWidth: '560px',
}
@@ -64,5 +64,5 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
},
}
},
maxWidth: '520px',
maxWidth: '560px',
}
@@ -43,5 +43,5 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
onClick: () => ctx.finish(),
}
},
maxWidth: '520px',
maxWidth: '560px',
}
@@ -3,12 +3,10 @@ import type { CreationFlowContextValue } from '../creation-flow-context'
import { stageConfig as customSetupStageConfig } from './custom-setup-stage'
import { stageConfig as finalConfigStageConfig } from './final-config-stage'
import { stageConfig as importInstanceStageConfig } from './import-instance-stage'
import { stageConfig as modpackStageConfig } from './modpack-stage'
import { stageConfig as setupTypeStageConfig } from './setup-type-stage'
export const stageConfigs: StageConfigInput<CreationFlowContextValue>[] = [
setupTypeStageConfig,
modpackStageConfig,
importInstanceStageConfig,
customSetupStageConfig,
finalConfigStageConfig,
@@ -1,22 +0,0 @@
import { LeftArrowIcon } from '@modrinth/assets'
import { markRaw } from 'vue'
import { commonMessages } from '#ui/utils/common-messages'
import type { StageConfigInput } from '../../../base'
import ModpackStage from '../components/ModpackStage.vue'
import { type CreationFlowContextValue, creationFlowMessages } from '../creation-flow-context'
export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
id: 'modpack',
title: (ctx) => ctx.formatMessage(creationFlowMessages.chooseModpackTitle),
stageContent: markRaw(ModpackStage),
skip: (ctx) => ctx.setupType.value !== 'modpack' || ctx.isImportMode.value,
leftButtonConfig: (ctx) => ({
label: ctx.formatMessage(commonMessages.backButton),
icon: LeftArrowIcon,
onClick: () => ctx.modal.value?.setStage('setup-type'),
}),
rightButtonConfig: null,
maxWidth: '520px',
}
@@ -10,5 +10,5 @@ export const stageConfig: StageConfigInput<CreationFlowContextValue> = {
stageContent: markRaw(SetupTypeStage),
leftButtonConfig: null,
rightButtonConfig: null,
maxWidth: '520px',
maxWidth: '560px',
}
@@ -16,27 +16,27 @@
@mouseleave="setNotificationTimer(item)"
>
<NotificationToast
v-if="item.toast"
v-if="item.contentType === 'toast'"
class="min-w-full"
:type="item.toast.type"
:type="item.type"
:action-loading="toastActionLoading(item.id)"
:actor-name="item.toast.actorName"
:actor-avatar-url="item.toast.actorAvatarUrl"
:entity-name="item.toast.entityName"
:entity-icon-url="item.toast.entityIconUrl"
:status-text="item.toast.statusText"
:progress="item.toast.progress"
:waiting="item.toast.waiting"
:show-progress="item.toast.showProgress"
:progress-type="item.toast.progressType"
:progress-current="item.toast.progressCurrent"
:progress-total="item.toast.progressTotal"
@accept="handleToastAccept(item, item.toast.onAccept)"
@decline="handleToastAction(item, item.toast.onDecline)"
@dismiss="handleToastAction(item, item.toast.onDismiss)"
@launch="handleToastAction(item, item.toast.onLaunch)"
@open-actor="item.toast.onOpenActor?.()"
@open-instance="handleToastAction(item, item.toast.onOpenInstance)"
:actor-name="item.actorName"
:actor-avatar-url="item.actorAvatarUrl"
:entity-name="item.entityName"
:entity-icon-url="item.entityIconUrl"
:status-text="item.statusText"
:progress="item.progress"
:waiting="item.waiting"
:show-progress="item.showProgress"
:progress-type="item.progressType"
:progress-current="item.progressCurrent"
:progress-total="item.progressTotal"
@accept="handleToastAccept(item, item.onAccept)"
@decline="handleToastAction(item, item.onDecline)"
@dismiss="handleToastAction(item, item.onDismiss)"
@launch="handleToastAction(item, item.onLaunch)"
@open-actor="item.onOpenActor?.()"
@open-instance="handleToastAction(item, item.onOpenInstance)"
/>
<TransitionGroup
v-else-if="isDownloadNotification(item)"
@@ -65,6 +65,12 @@
/>
</div>
</TransitionGroup>
<component
:is="item.component"
v-else-if="item.contentType === 'custom'"
v-bind="item.componentProps"
@dismiss="popupNotificationManager.removeNotification(item.id)"
/>
<div
v-else
class="flex w-full flex-col gap-3 overflow-hidden rounded-2xl bg-bg-raised shadow-xl border-surface-5 border-solid border p-4"
@@ -105,9 +111,10 @@
<IconButton
v-if="item.dismissible !== false"
type="quiet"
label="Close"
class="-m-1.5"
@click="handleNotificationDismiss(item)"
:label="formatMessage(messages.close)"
size="sm"
class="-m-1.5 -mx-2"
@click="handleStandardNotificationDismiss(item)"
>
<XIcon />
</IconButton>
@@ -115,11 +122,6 @@
<span v-if="item.text" class="text-primary">
{{ item.text }}
</span>
<component
:is="item.bodyComponent"
v-if="item.bodyComponent"
v-bind="item.bodyProps ?? {}"
/>
</div>
<div v-if="item.progressItems?.length" class="flex flex-col gap-3">
<div
@@ -156,20 +158,8 @@
<Button
v-for="(btn, idx) in item.buttons"
:key="idx"
:type="
(btn.color || (idx === 0 ? 'brand' : undefined)) &&
(btn.color || (idx === 0 ? 'brand' : undefined)) !== 'standard'
? 'colored'
: 'base'
"
:color="
(btn.color || (idx === 0 ? 'brand' : undefined)) &&
(btn.color || (idx === 0 ? 'brand' : undefined)) !== 'standard'
? (btn.color || (idx === 0 ? 'brand' : undefined)) === 'medal-promo'
? 'medal_promotion'
: btn.color || (idx === 0 ? 'brand' : undefined)
: undefined
"
:type="notificationButtonColor(btn, idx) ? 'colored' : 'base'"
:color="notificationButtonColor(btn, idx)"
@click="handleButtonClick(item.id, btn)"
>
<component :is="btn.icon" v-if="btn.icon" />
@@ -194,18 +184,25 @@ import {
} from '@modrinth/assets'
import { computed, ref } from 'vue'
import { Button, IconButton } from '#ui/components/base/buttons'
import { Button, type ButtonColor, IconButton } from '#ui/components/base/buttons'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { useModalStack } from '../../composables/modal-stack'
import {
injectPopupNotificationManager,
type PopupNotification,
type PopupNotificationButton,
type PopupNotificationProgressItem,
type PopupNotificationStandard,
} from '../../providers'
import ProgressBar from '../base/ProgressBar.vue'
import NotificationToast from '../notifications/NotificationToast.vue'
const { formatMessage } = useVIntl()
const messages = defineMessages({
close: { id: 'notifications.close', defaultMessage: 'Close' },
})
const popupNotificationManager = injectPopupNotificationManager()
const notifications = computed<PopupNotification[]>(() =>
popupNotificationManager.getVisibleNotifications(),
@@ -222,14 +219,25 @@ const setNotificationTimer = (n: PopupNotification) =>
popupNotificationManager.setNotificationTimer(n)
const toastActionLoading = (id: string | number) => activeToastActions.value[String(id)] ?? null
function isDownloadNotification(item: PopupNotification) {
return (
function notificationButtonColor(
button: PopupNotificationButton,
index: number,
): ButtonColor | undefined {
const color = button.color ?? (index === 0 ? 'brand' : undefined)
return color === 'standard' ? undefined : color
}
function isDownloadNotification(
item: PopupNotification,
): item is PopupNotificationStandard & { type: 'download' } {
return !!(
item.contentType === 'standard' &&
item.type === 'download' &&
(!!item.progressItems?.length || item.progress != null || item.waiting)
)
}
function downloadToastItems(item: PopupNotification): PopupNotificationProgressItem[] {
function downloadToastItems(item: PopupNotificationStandard): PopupNotificationProgressItem[] {
if (item.progressItems?.length) {
return popupNotificationManager.getVisibleDownloadProgressItems(item)
}
@@ -252,7 +260,7 @@ function handleProgressItemDismiss(
item: PopupNotification,
progressItem: PopupNotificationProgressItem,
) {
popupNotificationManager.hideDownloadItem(item.id, progressItem.id)
popupNotificationManager.hideDownloadItem(item.id, downloadProgressItemId(item, progressItem))
}
async function handleProgressItemAction(
@@ -273,10 +281,17 @@ async function handleProgressItemButtonClick(
) {
await btn.action()
if (!btn.keepOpen) {
popupNotificationManager.hideDownloadItem(item.id, progressItem.id)
popupNotificationManager.hideDownloadItem(item.id, downloadProgressItemId(item, progressItem))
}
}
function downloadProgressItemId(
item: PopupNotification,
progressItem: PopupNotificationProgressItem,
): string | undefined {
return item.contentType === 'standard' && item.progressItems?.length ? progressItem.id : undefined
}
async function handleButtonClick(id: string | number, btn: PopupNotificationButton) {
await btn.action()
if (!btn.keepOpen) {
@@ -284,7 +299,7 @@ async function handleButtonClick(id: string | number, btn: PopupNotificationButt
}
}
async function handleNotificationDismiss(item: PopupNotification) {
async function handleStandardNotificationDismiss(item: PopupNotificationStandard) {
await item.onDismiss?.()
popupNotificationManager.removeNotification(item.id)
}
@@ -314,7 +329,7 @@ async function handleToastAccept(item: PopupNotification, action?: () => void |
}
}
function progressColorForType(type: PopupNotification['type']) {
function progressColorForType(type: PopupNotificationStandard['type']) {
if (type === 'error') {
return 'red'
} else if (type === 'warning') {
@@ -15,47 +15,57 @@
<div class="flex min-w-0 flex-1 flex-col gap-2.5">
<div class="flex w-full items-start gap-1">
<p class="m-0 min-w-0 flex-1 break-words text-lg font-normal leading-6 text-contrast/85">
<template v-if="type === 'friend-request'">
<span class="font-semibold text-contrast">{{ actorLabel }}</span>
<span> sent you a friend request.</span>
</template>
<template v-else>
<button
v-if="actorName"
type="button"
class="m-0 inline border-0 bg-transparent p-0 text-lg font-semibold leading-6 text-contrast hover:underline"
@click="$emit('open-actor')"
>
{{ actorName }}
</button>
<span v-else class="font-semibold text-contrast">Someone</span>
<span class="mx-1">{{ inviteActionText }}</span>
<template v-if="type === 'server-invite'">
<span class="font-semibold text-contrast">{{ entityLabel }}</span
>.
<IntlFormatted
:message-id="
type === 'friend-request'
? messages.friendRequest
: type === 'server-invite'
? messages.serverInvite
: messages.instanceInvite
"
:values="{ actor: actorLabel, entity: entityLabel }"
>
<template #actor="{ children }">
<button
v-if="actorName && type !== 'friend-request'"
type="button"
class="m-0 inline border-0 bg-transparent p-0 text-lg font-semibold leading-6 text-contrast hover:underline"
@click="$emit('open-actor')"
>
<component :is="() => children" />
</button>
<span v-else class="font-semibold text-contrast">
<component :is="() => children" />
</span>
</template>
<template v-else>
<Avatar
:src="entityIconUrl"
:alt="entityLabel"
:tint-by="entityLabel"
size="28px"
no-shadow
raised
class="inline-block !rounded-lg align-middle"
/>
<span class="ml-1 font-semibold text-contrast">{{ entityLabel }}</span>
<span> instance.</span>
<template #entity="{ children }">
<template v-if="type === 'instance-invite'">
<Avatar
:src="entityIconUrl"
:alt="entityLabel"
:tint-by="entityLabel"
size="28px"
no-shadow
raised
class="inline-block !rounded-lg align-middle"
/>
</template>
<span
:class="{ 'ml-1': type === 'instance-invite' }"
class="font-semibold text-contrast"
>
<component :is="() => children" />
</span>
</template>
</template>
</IntlFormatted>
</p>
<IconButton
v-if="dismissible"
type="quiet"
size="xs"
label="Dismiss notification"
size="sm"
:label="formatMessage(messages.dismissNotification)"
native-type="button"
class="notification-toast-dismiss !size-6"
class="notification-toast-dismiss -m-1.5"
@click="$emit('dismiss')"
>
<XIcon />
@@ -70,11 +80,11 @@
>
<SpinnerIcon v-if="actionLoading === 'accept'" class="animate-spin" />
<CheckIcon v-else />
Accept
{{ formatMessage(messages.accept) }}
</Button>
<Button type="outlined" :disabled="actionLoading != null" @click="$emit('decline')">
<XIcon />
Decline
{{ formatMessage(messages.decline) }}
</Button>
</div>
</div>
@@ -101,10 +111,10 @@
<IconButton
v-if="dismissible"
type="quiet"
size="xs"
label="Dismiss notification"
size="sm"
:label="formatMessage(messages.dismissNotification)"
native-type="button"
class="notification-toast-dismiss !size-6"
class="notification-toast-dismiss -m-1.5"
@click="$emit('dismiss')"
>
<XIcon />
@@ -133,8 +143,12 @@
class="col-start-1 col-end-3 row-start-3 mt-2 flex min-w-0 items-center justify-between gap-2"
>
<div class="flex min-w-0 items-center gap-2">
<Button type="colored" color="brand" @click="$emit('launch')">Launch game</Button>
<Button type="outlined" @click="$emit('open-instance')">Instance</Button>
<Button type="colored" color="brand" @click="$emit('launch')">
{{ formatMessage(messages.launchGame) }}
</Button>
<Button type="outlined" @click="$emit('open-instance')">
{{ formatMessage(messages.instance) }}
</Button>
</div>
<div v-if="progressLabel" class="notification-inline-progress-label flex-none">
{{ progressLabel }}
@@ -147,20 +161,8 @@
<Button
v-for="(action, index) in actions"
:key="index"
:type="
(action.color || (index === 0 ? 'brand' : undefined)) &&
(action.color || (index === 0 ? 'brand' : undefined)) !== 'standard'
? 'colored'
: 'base'
"
:color="
(action.color || (index === 0 ? 'brand' : undefined)) &&
(action.color || (index === 0 ? 'brand' : undefined)) !== 'standard'
? (action.color || (index === 0 ? 'brand' : undefined)) === 'medal-promo'
? 'medal_promotion'
: action.color || (index === 0 ? 'brand' : undefined)
: undefined
"
:type="notificationButtonColor(action, index) ? 'colored' : 'base'"
:color="notificationButtonColor(action, index)"
@click="$emit('action', index)"
>
<component :is="action.icon" v-if="action.icon" />
@@ -193,12 +195,44 @@
import { CheckIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { computed, ref } from 'vue'
import { Button, IconButton } from '#ui/components/base/buttons'
import { Button, type ButtonColor, IconButton } from '#ui/components/base/buttons'
import { useFormatBytes, useFormatNumber } from '../../composables'
import { defineMessages, useVIntl } from '../../composables/i18n'
import type { PopupNotificationButton, PopupNotificationProgressType } from '../../providers'
import { truncatedTooltip } from '../../utils/truncate'
import Avatar from '../base/Avatar.vue'
import IntlFormatted from '../base/IntlFormatted.vue'
const { formatMessage } = useVIntl()
const messages = defineMessages({
friendRequest: {
id: 'notifications.friend-request.body',
defaultMessage: '<actor>{actor}</actor> sent you a friend request.',
},
serverInvite: {
id: 'notifications.server-invite.body',
defaultMessage:
'<actor>{actor}</actor> invited you to manage the server <entity>{entity}</entity>.',
},
instanceInvite: {
id: 'notifications.instance-invite.body',
defaultMessage: '<actor>{actor}</actor> invited you to <entity>{entity}</entity> instance.',
},
dismissNotification: {
id: 'notifications.dismiss',
defaultMessage: 'Dismiss notification',
},
accept: { id: 'notifications.invite.accept', defaultMessage: 'Accept' },
decline: { id: 'notifications.invite.decline', defaultMessage: 'Decline' },
launchGame: { id: 'notifications.instance-ready.launch-game', defaultMessage: 'Launch game' },
instance: { id: 'notifications.instance-ready.open-instance', defaultMessage: 'Instance' },
someone: { id: 'notifications.actor.unknown', defaultMessage: 'Someone' },
installedReady: {
id: 'notifications.instance-ready.status',
defaultMessage: 'Installed and ready to play.',
},
})
type NotificationToastType =
| 'friend-request'
@@ -258,7 +292,15 @@ const isInviteNotification = computed(
props.type === 'instance-invite',
)
const actorLabel = computed(() => props.actorName || 'Someone')
function notificationButtonColor(
button: PopupNotificationButton,
index: number,
): ButtonColor | undefined {
const color = button.color ?? (index === 0 ? 'brand' : undefined)
return color === 'standard' ? undefined : color
}
const actorLabel = computed(() => props.actorName || formatMessage(messages.someone))
const entityLabel = computed(() => props.entityName || '')
const inviteAvatarUrl = computed(() => props.actorAvatarUrl)
const inviteAvatarLabel = computed(() => actorLabel.value)
@@ -269,17 +311,9 @@ const isWaitingProgress = computed(() => props.type === 'instance-download' && p
const formatBytes = useFormatBytes()
const formatNumber = useFormatNumber()
const inviteActionText = computed(() => {
if (props.type === 'server-invite') {
return 'invited you to manage the server'
}
return 'invited you to'
})
const resolvedStatusText = computed(() => {
if (props.type === 'instance-ready') {
return props.statusText ?? 'Installed and ready to play.'
return props.statusText ?? formatMessage(messages.installedReady)
}
return props.statusText ?? ''
@@ -9,7 +9,7 @@
:initial-loader="initialLoader"
:initial-game-version="initialGameVersion"
:fade="props.initialSetup ? undefined : 'danger'"
:search-modpacks="searchModpacks"
:search-projects="searchModpacks"
:get-project-versions="getProjectVersions"
:finish-disabled="!canCompleteSetup"
:finish-disabled-tooltip="!canCompleteSetup ? permissionDeniedMessage : undefined"
@@ -6,7 +6,7 @@ export type {
Gamemode,
GeneratorSettingsMode,
LoaderVersionType,
ModpackSearchResult,
ProjectSearchResult,
SetupType,
} from '../../flows/creation-flow-modal/creation-flow-context'
export { default as CreationFlowModal } from '../../flows/creation-flow-modal/index.vue'
Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

@@ -1,139 +1,74 @@
<template>
<div class="grid grid-cols-2 gap-8 items-center justify-center py-10 max-w-[760px]">
<!-- Left column -->
<div class="flex flex-col gap-8 items-start pr-8 shrink-0">
<!-- Heading -->
<div class="flex flex-col gap-2 items-start w-[300px]">
<p class="text-3xl leading-9 font-semibold text-contrast">
{{ formatMessage(messages.modrinthHostingLabel) }}
</p>
<p class="text-base font-normal text-primary">
{{ formatMessage(messages.noServersDescription) }}
</p>
</div>
<!-- Feature list -->
<div class="flex flex-col gap-4 items-start w-full">
<div class="flex gap-3 items-start">
<div
class="bg-surface-4 border border-solid border-surface-5 rounded-full shrink-0 size-8 flex items-center justify-center"
>
<PackageOpenIcon class="size-5 text-secondary" aria-hidden="true" />
<div class="flex w-full max-w-[56rem] h-full flex-col items-stretch justify-between gap-8 py-6">
<div class="flex w-full flex-wrap grow items-center justify-center gap-2">
<div class="mx-auto flex w-full max-w-[20rem] flex-col items-start gap-8">
<div class="flex flex-col gap-2">
<div class="flex items-center gap-3">
<h1 class="m-0 text-3xl font-semibold leading-9 text-contrast">
{{ formatMessage(messages.modrinthHostingLabel) }}
</h1>
</div>
<div class="flex flex-col gap-0.5">
<p class="text-base font-semibold text-contrast">
{{ formatMessage(messages.oneClickModInstallsTitle) }}
</p>
<p class="text-base font-normal text-primary">
{{ formatMessage(messages.oneClickModInstallsDescription) }}
</p>
<p class="m-0 text-base text-primary">
{{ formatMessage(messages.noServersDescription) }}
</p>
</div>
<div class="flex w-full flex-col gap-6">
<div v-for="feature in features" :key="feature.key" class="flex items-start gap-4">
<div
class="feature-icon relative flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-[0.875rem] border border-solid bg-surface-1 text-brand"
>
<div class="feature-icon-gradient absolute left-[-1px] top-[-1px] size-[6.25rem]" />
<div class="feature-icon-shade absolute left-[-1px] top-[-1px] size-[6.25rem]" />
<img
:src="iconTexture"
alt=""
class="absolute left-1/2 top-1/2 h-[6.25rem] w-[9.8125rem] max-w-none -translate-x-1/2 -translate-y-1/2 object-cover opacity-40 mix-blend-luminosity"
/>
<component
:is="feature.icon"
class="feature-icon-glyph relative size-5"
aria-hidden="true"
/>
</div>
<div class="flex min-w-0 flex-col gap-0.5">
<p class="m-0 text-lg font-semibold leading-6 text-contrast">
{{ feature.title }}
</p>
<p class="m-0 text-sm leading-5 text-primary">
{{ feature.description }}
</p>
</div>
</div>
</div>
<div class="flex gap-3 items-start">
<div
class="bg-surface-4 border border-solid border-surface-5 rounded-full shrink-0 size-8 flex items-center justify-center overflow-hidden"
>
<GlobeIcon class="size-5 text-secondary" aria-hidden="true" />
</div>
<div class="flex flex-col gap-0.5">
<p class="text-base font-semibold text-contrast">
{{ formatMessage(messages.simpleSetupTitle) }}
</p>
<p class="text-base font-normal text-primary">
{{ formatMessage(messages.simpleSetupDescription) }}
</p>
</div>
</div>
<div class="flex gap-3 items-start">
<div
class="bg-surface-4 border border-solid border-surface-5 rounded-full shrink-0 size-8 flex items-center justify-center overflow-hidden"
>
<UsersIcon class="size-5 text-secondary" aria-hidden="true" />
</div>
<div class="flex flex-col gap-0.5">
<p class="text-base font-semibold text-contrast">
{{ formatMessage(messages.playWithFriendsTitle) }}
</p>
<p class="text-base font-normal text-primary">
{{ formatMessage(messages.playWithFriendsDescription) }}
</p>
</div>
</div>
</div>
<!-- CTA section -->
<div class="flex flex-col gap-6 items-start">
<div class="flex flex-col gap-3 items-start">
<Button type="colored" color="brand" @click="onClickNewServer?.()">
<div class="flex flex-wrap items-center gap-4">
<Button type="colored" color="brand" size="lg" @click="onClickNewServer?.()">
<PlusIcon aria-hidden="true" />
{{ formatMessage(messages.newServerButton) }}
</Button>
<AutoLink
to="https://modrinth.com/hosting"
target="_blank"
class="flex items-center gap-1 hover:brightness-125"
class="flex items-center gap-1 hover:brightness-125 font-semibold"
>
{{ formatMessage(messages.learnMoreLink) }}
<RightArrowIcon class="size-5 shrink-0" aria-hidden="true" />
</AutoLink>
</div>
<template v-if="!loggedIn">
<div class="h-px w-full bg-surface-5" />
<div class="flex gap-3 items-center flex-wrap">
<p class="text-base font-normal text-primary">
{{ formatMessage(messages.alreadyHaveServerLabel) }}
</p>
<Button @click="onClickSignIn?.()">
<LogInIcon aria-hidden="true" />
{{ formatMessage(messages.signInButton) }}
</Button>
</div>
</template>
</div>
<ServerListEmptyPreview />
</div>
<!-- Right column - mod icon grid -->
<div
class="relative flex h-[617px] shrink-0 items-center justify-center overflow-hidden rounded-[40px] pointer-events-none select-none [mask-image:linear-gradient(to_bottom,black_0%,black_35%,transparent_100%)] [-webkit-mask-image:linear-gradient(to_bottom,black_0%,black_35%,transparent_100%)]"
>
<div class="rotate-[15deg]">
<div class="flex flex-col gap-4">
<div
v-for="row in GRID_ROWS"
:key="row"
class="flex gap-4 items-center shrink-0"
:class="animated ? (row % 2 === 1 ? 'drift-left' : 'drift-right relative left-14') : ''"
>
<div class="hidden drift-right drift-left"></div>
<div
v-for="col in GRID_COLS"
:key="col"
class="border border-surface-5 rounded-[20px] shrink-0 size-[112px] bg-surface-4 overflow-hidden"
>
<img :src="getGridImage(row - 1, col - 1)" alt="" class="size-full object-cover" />
</div>
<div
v-for="col in GRID_COLS"
:key="col"
class="border border-surface-5 rounded-[20px] shrink-0 size-[112px] bg-surface-4 overflow-hidden"
>
<img :src="getGridImage(row - 1, col - 1)" alt="" class="size-full object-cover" />
</div>
<div
v-for="col in GRID_COLS"
:key="col"
class="border border-surface-5 rounded-[20px] shrink-0 size-[112px] bg-surface-4 overflow-hidden"
>
<img :src="getGridImage(row - 1, col - 1)" alt="" class="size-full object-cover" />
</div>
</div>
</div>
</div>
<div v-if="!loggedIn" class="flex flex-col items-center gap-4 text-center">
<p class="m-0 text-sm text-secondary">
{{ formatMessage(messages.alreadyHaveServerLabel) }}
</p>
<Button @click="onClickSignIn?.()">
<LogInIcon aria-hidden="true" />
{{ formatMessage(messages.signInButton) }}
</Button>
</div>
</div>
</template>
@@ -147,46 +82,21 @@ import {
RightArrowIcon,
UsersIcon,
} from '@modrinth/assets'
import { AutoLink } from '@modrinth/ui'
import { computed } from 'vue'
import iconTexture from '#ui/assets/welcome/icon-texture.png'
import AutoLink from '#ui/components/base/AutoLink.vue'
import { Button } from '#ui/components/base/buttons'
import { defineMessages, useVIntl } from '../../../composables/i18n'
import imgAircraft from './grid-images/aircraft.png'
import imgAlexs from "./grid-images/alex's.png"
import imgArtifacts from './grid-images/artifacts.png'
import imgBiomes from './grid-images/biomes.png'
import imgCatac from './grid-images/catac.png'
import imgCobble from './grid-images/cobble.png'
import imgComforts from './grid-images/comforts.png'
import imgCreate from './grid-images/create.png'
import imgCreate1 from './grid-images/create1.png'
import imgCreate2 from './grid-images/create2.png'
import imgCreate3 from './grid-images/create3.png'
import imgCreeper from './grid-images/creeper.png'
import imgFriends from './grid-images/friends.png'
import imgGeo from './grid-images/geo.png'
import imgNaturalist from './grid-images/naturalist.png'
import imgSeasons from './grid-images/seasons.png'
import imgTravellers from './grid-images/travellers.png'
import imgTree from './grid-images/tree.png'
import imgYum1 from './grid-images/yum1.png'
import imgYum2 from './grid-images/yum2.png'
import imgYum3 from './grid-images/yum3.png'
import imgYung from './grid-images/yung.png'
import ServerListEmptyPreview from './ServerListEmptyPreview.vue'
withDefaults(
defineProps<{
animated?: boolean
onClickNewServer?: () => void
onClickSignIn?: () => void
loggedIn?: boolean
}>(),
{ animated: false },
)
defineProps<{
onClickNewServer?: () => void
onClickSignIn?: () => void
loggedIn?: boolean
}>()
const GRID_ROWS = 6
const GRID_COLS = 5
const { formatMessage } = useVIntl()
const messages = defineMessages({
@@ -194,10 +104,6 @@ const messages = defineMessages({
id: 'servers.list-empty.modrinth-hosting-label',
defaultMessage: 'Modrinth Hosting',
},
noServersTitle: {
id: 'servers.list-empty.no-servers-title',
defaultMessage: 'No servers yet',
},
noServersDescription: {
id: 'servers.list-empty.no-servers-description',
defaultMessage: 'Install mods, invite friends, and play together all from the Modrinth App.',
@@ -224,7 +130,7 @@ const messages = defineMessages({
},
playWithFriendsDescription: {
id: 'servers.list-empty.play-with-friends-description',
defaultMessage: 'Invite friends and get them set up right in the Modrinth App.',
defaultMessage: 'Invite friends to play your server with one-click play from the Modrinth App.',
},
newServerButton: {
id: 'servers.list-empty.new-server-button',
@@ -232,7 +138,7 @@ const messages = defineMessages({
},
learnMoreLink: {
id: 'servers.list-empty.learn-more-link',
defaultMessage: 'Learn more about Modrinth Hosting',
defaultMessage: 'Learn more',
},
alreadyHaveServerLabel: {
id: 'servers.list-empty.already-have-server-label',
@@ -240,70 +146,55 @@ const messages = defineMessages({
},
signInButton: {
id: 'servers.list-empty.sign-in-button',
defaultMessage: 'Sign in',
defaultMessage: 'Sign in to Modrinth',
},
})
const GRID_IMAGES = [
imgYum1,
imgYum2,
imgYum3,
imgYung,
imgCreeper,
imgFriends,
imgNaturalist,
imgBiomes,
imgCatac,
imgCobble,
imgGeo,
imgCreate,
imgCreate1,
imgCreate2,
imgCreate3,
imgAircraft,
imgArtifacts,
imgComforts,
imgTravellers,
imgAlexs,
imgSeasons,
imgTree,
]
function getGridImage(row: number, col: number): string {
return GRID_IMAGES[(row * GRID_COLS + col) % GRID_IMAGES.length]
}
const features = computed(() => [
{
key: 'one-click-mod-installs',
icon: PackageOpenIcon,
title: formatMessage(messages.oneClickModInstallsTitle),
description: formatMessage(messages.oneClickModInstallsDescription),
},
{
key: 'simple-setup',
icon: GlobeIcon,
title: formatMessage(messages.simpleSetupTitle),
description: formatMessage(messages.simpleSetupDescription),
},
{
key: 'play-with-friends',
icon: UsersIcon,
title: formatMessage(messages.playWithFriendsTitle),
description: formatMessage(messages.playWithFriendsDescription),
},
])
</script>
<style scoped>
p {
margin: 0;
.feature-icon {
border-color: color-mix(in srgb, var(--color-text-primary) 10%, transparent);
box-shadow:
0 0 0 1px color-mix(in srgb, var(--color-brand) 30%, var(--surface-1)),
var(--shadow-card),
0 0 3.75rem color-mix(in srgb, var(--color-brand) 10%, transparent);
}
@keyframes drift-right {
from {
transform: translateX(-33%);
}
to {
transform: translateX(33%);
}
.feature-icon-gradient {
background: linear-gradient(180deg, var(--color-green-800) 0%, var(--color-green-950) 100%);
opacity: 0.5;
}
@keyframes drift-left {
from {
transform: translateX(33%);
}
to {
transform: translateX(-33%);
}
.feature-icon-shade {
background: linear-gradient(
-14deg,
color-mix(in srgb, var(--color-green-950) 37%, transparent) 8%,
transparent 86%
);
}
.drift-left {
animation: drift-left linear infinite alternate;
animation-duration: 400s;
}
.drift-right {
animation: drift-right linear infinite alternate;
animation-duration: 400s;
.feature-icon-glyph {
filter: drop-shadow(0 1px 1px color-mix(in srgb, var(--color-green-950) 12%, transparent));
}
</style>
@@ -0,0 +1,245 @@
<template>
<div
inert
aria-hidden="true"
class="relative mx-auto h-[38rem] w-full max-w-[25rem] select-none -mb-10"
>
<div
class="absolute inset-x-0 top-0 h-full overflow-hidden rounded-2xl border border-solid border-surface-3 bg-surface-2 shadow-card"
>
<div
class="flex items-center justify-between border-0 border-b border-solid border-surface-3 p-4"
>
<div class="flex items-center gap-2 text-contrast">
<UserPlusIcon class="size-5 text-primary" />
<span class="text-lg font-semibold">{{ formatMessage(messages.heading) }}</span>
</div>
<XIcon class="size-5 text-secondary" />
</div>
<div class="flex gap-2 border-0 border-b border-solid border-surface-3 p-4">
<div
class="flex h-9 min-w-0 flex-1 items-center gap-2 rounded-xl bg-surface-3 px-3 text-sm text-secondary"
>
<SearchIcon class="size-4 shrink-0" />
<span class="truncate text-secondary">{{
formatMessage(messages.searchPlaceholder)
}}</span>
</div>
<Button type="colored" color="brand" class="!cursor-default" disabled>
<PlusIcon aria-hidden="true" />
{{ formatMessage(messages.addButton) }}
</Button>
</div>
<div class="bg-surface-1 py-3">
<p class="m-0 pb-2 text-sm font-semibold text-primary px-4">
{{ formatMessage(messages.friendsHeading, { count: totalFriendCount }) }}
</p>
<div
v-for="(friend, index) in friends"
:key="friend.username"
class="relative flex h-11 items-center justify-between gap-3 px-4"
:class="[index > 4 ? 'opacity-40' : '', friend.showPointer ? 'bg-surface-2' : '']"
>
<div class="flex min-w-0 items-center gap-2">
<div class="relative flex shrink-0">
<Avatar :src="friend.avatarUrl" size="1.5rem" class="!border-none" circle no-shadow />
<span
v-if="friend.presence"
class="absolute bottom-0 right-[-1px] size-2 rounded-full border border-solid border-surface-1"
:class="friend.presence === 'online' ? 'bg-brand' : 'bg-blue'"
/>
</div>
<span class="truncate text-base font-medium text-primary">{{ friend.username }}</span>
</div>
<Button
:type="friend.status === 'cancel' ? 'outlined' : 'base'"
class="!cursor-default !font-medium !h-8"
size="md"
:disabled="friend.status === 'added'"
:class="friend.status === 'added' ? '' : 'w-20'"
>
<CheckIcon v-if="friend.status === 'added'" aria-hidden="true" />
{{ friendStatusLabel(friend.status) }}
</Button>
<div
v-if="friend.showPointer"
class="absolute right-[14.25rem] top-9 z-10 flex size-8 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 opacity-75"
>
<img :src="pointerIcon" alt="" class="size-4" />
</div>
</div>
</div>
<div
class="absolute inset-x-0 bottom-0 border-0 border-t border-solid border-surface-3 bg-surface-2 p-4"
>
<p class="m-0 pb-2 text-base font-semibold text-contrast">
{{ formatMessage(messages.inviteLinkHeading) }}
</p>
<div
class="flex h-8 items-center justify-between gap-2 rounded-xl bg-surface-1 px-2.5 text-sm font-medium text-primary"
>
<span class="truncate">https://modrinth.com/server/abc123</span>
<ClipboardCopyIcon class="size-4 shrink-0 text-secondary" />
</div>
</div>
</div>
<div
class="pointer-events-none absolute inset-x-0 bottom-0 h-[28rem] [background:linear-gradient(to_bottom,transparent,var(--surface-1))]"
/>
<div
class="absolute left-[32%] top-[23rem] z-10 flex w-[21rem] max-w-[calc(100%-1rem)] gap-4 !overflow-hidden rounded-2xl border border-solid border-surface-4 bg-surface-2 px-4 py-3 shadow-card"
>
<div
class="absolute top-[16px] left-[16px] size-[20px] rounded-full bg-contrast opacity-75 blur-xl"
/>
<div class="relative h-max">
<Avatar :src="geometricallyAvatar" size="2.25rem" circle no-shadow />
<span
class="absolute bottom-0 right-[-1px] size-3 rounded-full border-2 border-solid border-surface-2 bg-brand"
/>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-start gap-1">
<div class="min-w-0 flex-1 text-base leading-5 text-primary">
<p class="m-0">
<span class="font-medium text-contrast">Geometrically</span>
{{ formatMessage(messages.invitedYouTo) }}
</p>
<p class="m-0 flex items-center gap-1">
<Avatar :src="modrinthSmpIcon" size="1.25rem" no-shadow />
<span class="font-medium text-contrast">Modrinth SMP</span>
<span>{{ formatMessage(messages.serverSuffix) }}</span>
</p>
</div>
<XIcon class="size-5 shrink-0 text-secondary" />
</div>
<div class="mt-2.5 flex gap-2">
<Button type="colored" color="brand" class="!cursor-default !font-medium">
{{ formatMessage(messages.acceptButton) }}
</Button>
<Button type="outlined" class="!cursor-default !font-medium">
{{ formatMessage(messages.declineButton) }}
</Button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
CheckIcon,
ClipboardCopyIcon,
PlusIcon,
SearchIcon,
UserPlusIcon,
XIcon,
} from '@modrinth/assets'
import borisAvatar from '#ui/assets/servers/server-list-empty/boris.png'
import fetchAvatar from '#ui/assets/servers/server-list-empty/fetch.png'
import geometricallyAvatar from '#ui/assets/servers/server-list-empty/geometrically.png'
import imb11Avatar from '#ui/assets/servers/server-list-empty/imb11.png'
import joshAvatar from '#ui/assets/servers/server-list-empty/josh.png'
import michaelAvatar from '#ui/assets/servers/server-list-empty/michael.png'
import modrinthSmpIcon from '#ui/assets/servers/server-list-empty/modrinth-smp.png'
import prospectorAvatar from '#ui/assets/servers/server-list-empty/prospector.png'
import sayaAvatar from '#ui/assets/servers/server-list-empty/saya.png'
import trumanAvatar from '#ui/assets/servers/server-list-empty/truman.png'
import Avatar from '#ui/components/base/Avatar.vue'
import Button from '#ui/components/base/buttons/Button.vue'
import { defineMessages, useVIntl } from '../../../composables/i18n'
import pointerIcon from './Pointer.png'
type FriendStatus = 'added' | 'cancel' | 'invite'
type FriendPresence = 'online' | 'playing'
const { formatMessage } = useVIntl()
const messages = defineMessages({
heading: {
id: 'sharing.invite-players-modal.heading',
defaultMessage: 'Invite players',
},
searchPlaceholder: {
id: 'sharing.invite-players-modal.search-placeholder',
defaultMessage: 'Enter Modrinth username',
},
addButton: {
id: 'sharing.invite-players-modal.add',
defaultMessage: 'Add',
},
friendsHeading: {
id: 'sharing.invite-players-modal.friends-heading',
defaultMessage: 'Your friends - {count}',
},
addedButton: {
id: 'sharing.invite-players-modal.added',
defaultMessage: 'Added',
},
cancelButton: {
id: 'sharing.invite-players-modal.cancel',
defaultMessage: 'Cancel',
},
inviteButton: {
id: 'sharing.invite-players-modal.invite',
defaultMessage: 'Invite',
},
inviteLinkHeading: {
id: 'sharing.invite-players-modal.invite-link-heading',
defaultMessage: 'Or use an invite link',
},
invitedYouTo: {
id: 'sharing.invite-players-toast.invited-you-to',
defaultMessage: 'invited you to',
},
serverSuffix: {
id: 'sharing.invite-players-toast.server-suffix',
defaultMessage: 'server.',
},
acceptButton: {
id: 'sharing.invite-players-toast.accept',
defaultMessage: 'Accept',
},
declineButton: {
id: 'sharing.invite-players-toast.decline',
defaultMessage: 'Decline',
},
})
const friends: Array<{
username: string
avatarUrl: string
status: FriendStatus
presence?: FriendPresence
showPointer?: boolean
}> = [
{ username: 'Josh', avatarUrl: joshAvatar, status: 'added' },
{
username: 'Prospector',
avatarUrl: prospectorAvatar,
status: 'invite',
presence: 'online',
showPointer: true,
},
{ username: 'Fetch', avatarUrl: fetchAvatar, status: 'cancel', presence: 'playing' },
{ username: 'IMB11', avatarUrl: imb11Avatar, status: 'invite' },
{ username: 'Truman', avatarUrl: trumanAvatar, status: 'invite' },
{ username: 'Boris', avatarUrl: borisAvatar, status: 'invite' },
{ username: 'Saya', avatarUrl: sayaAvatar, status: 'invite' },
{ username: 'Michael', avatarUrl: michaelAvatar, status: 'invite' },
]
const totalFriendCount = 11
function friendStatusLabel(status: FriendStatus): string {
if (status === 'added') return formatMessage(messages.addedButton)
if (status === 'cancel') return formatMessage(messages.cancelButton)
return formatMessage(messages.inviteButton)
}
</script>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

+40 -21
View File
@@ -1,6 +1,15 @@
<script setup lang="ts">
import { CheckIcon } from '@modrinth/assets'
import { ref, watch } from 'vue'
import { defineMessages, useVIntl } from '../../composables/i18n'
const { formatMessage } = useVIntl()
const messages = defineMessages({
selectNamedSkin: { id: 'skins.select-named', defaultMessage: 'Select {name}' },
selectSkin: { id: 'skins.select', defaultMessage: 'Select skin' },
})
const emit = defineEmits<{
(e: 'select'): void
(e: 'edit', event: MouseEvent): void
@@ -63,7 +72,11 @@ watch(
<button
class="absolute inset-0 z-10 cursor-pointer border-none bg-transparent p-0 focus-visible:outline-none"
:aria-label="tooltip ? `Select ${tooltip}` : 'Select skin'"
:aria-label="
tooltip
? formatMessage(messages.selectNamedSkin, { name: tooltip })
: formatMessage(messages.selectSkin)
"
:aria-pressed="selected"
:disabled="disabled"
@click="emit('select')"
@@ -74,6 +87,14 @@ watch(
class="pointer-events-none absolute right-3 top-3 z-20 size-3 rounded-full border-2 border-solid border-surface-3 bg-green"
></span>
<span
v-if="selected"
class="pointer-events-none absolute right-3 top-3 z-20 flex size-6 items-center justify-center rounded-full"
>
<span class="absolute inset-0 rounded-full bg-contrast"></span>
<CheckIcon class="relative size-4 invert [stroke-width:3]" />
</span>
<div v-if="!imagesLoaded.forward" class="skeleton-loader h-full w-full">
<div class="skeleton absolute inset-0 aspect-[5/7]"></div>
</div>
@@ -109,22 +130,23 @@ watch(
}
.skeleton {
background: linear-gradient(
90deg,
var(--color-bg) 25%,
var(--color-raised-bg) 50%,
var(--color-bg) 75%
);
background-size: 200% 100%;
animation: wave 1500ms infinite linear;
background: linear-gradient(145deg, var(--surface-2), var(--surface-3));
animation: skeleton-pulse 2700ms ease-in-out infinite;
}
@keyframes wave {
0% {
background-position: -200% 0;
}
@keyframes skeleton-pulse {
0%,
100% {
background-position: 200% 0;
filter: brightness(0.95);
}
50% {
filter: brightness(1.15);
}
}
@media (prefers-reduced-motion: reduce) {
.skeleton {
animation: none;
}
}
@@ -168,13 +190,10 @@ watch(
.skin-button.skin-button--selected:hover,
.skin-button.skin-button--selected:focus-within,
.skin-button.skin-button--selected.skin-button--with-actions:hover,
.skin-button.skin-button--selected.skin-button--with-actions:focus-within,
.skin-button.skin-button--active:hover,
.skin-button.skin-button--active:focus-within,
.skin-button.skin-button--active.skin-button--with-actions:hover,
.skin-button.skin-button--active.skin-button--with-actions:focus-within {
border-color: var(--color-brand);
background: var(--color-brand-highlight);
.skin-button.skin-button--selected.skin-button--with-actions:focus-within {
border-color: color-mix(in srgb, var(--color-text-primary) 40%, transparent);
background: var(--surface-3);
filter: brightness(1.1);
}
.skin-button--disabled {
@@ -74,9 +74,13 @@
class="flex-1"
/>
<IconButton
v-tooltip="`${hideUninstallable ? 'Show' : 'Hide'} unavailable`"
v-tooltip="
formatMessage(hideUninstallable ? messages.showUnavailable : messages.hideUnavailable)
"
type="outlined"
:label="`${hideUninstallable ? 'Show' : 'Hide'} unavailable`"
:label="
formatMessage(hideUninstallable ? messages.showUnavailable : messages.hideUnavailable)
"
@click="hideUninstallable = !hideUninstallable"
>
<EyeOffIcon v-if="hideUninstallable" />
@@ -139,16 +143,42 @@
<!-- New instance tab -->
<div v-else class="flex flex-col gap-6 p-6">
<div class="flex items-center gap-4">
<Avatar :src="iconPreviewUrl ?? undefined" size="5rem" rounded="2xl" />
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2.5">
<div class="group relative size-[7.75rem] shrink-0">
<Avatar :src="iconPreviewUrl ?? undefined" size="100%" no-shadow />
<div
v-if="iconPreviewUrl"
class="pointer-events-none absolute right-1.5 top-1.5 opacity-0 transition-opacity group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100"
>
<Button
size="sm"
class="!p-2"
:aria-label="formatMessage(commonMessages.removeImageButton)"
@click="removeIcon"
>
<XIcon />
</Button>
</div>
</div>
<div class="flex flex-col gap-1.5">
<Button type="outlined" @click="selectIcon">
<UploadIcon />
{{ formatMessage(messages.selectIcon) }}
</Button>
<Button type="outlined" :disabled="!iconPreviewUrl" @click="removeIcon">
<XIcon />
{{ formatMessage(messages.removeIcon) }}
<Button
v-if="props.randomizeIcon"
type="outlined"
:disabled="randomizing"
class="disabled:!cursor-default"
@click="randomizeInstanceIcon"
>
<SpinnerIcon v-if="randomizing" class="animate-spin" />
<RefreshCwIcon v-else />
{{ formatMessage(messages.randomizeIcon) }}
</Button>
<Button v-if="props.customizeIcon" type="outlined" @click="props.customizeIcon?.()">
<PaletteIcon />
{{ formatMessage(messages.customizeIcon) }}
</Button>
</div>
</div>
@@ -245,7 +275,10 @@ import {
DownloadIcon,
EyeIcon,
EyeOffIcon,
PaletteIcon,
RefreshCwIcon,
SearchIcon,
SpinnerIcon,
TriangleAlertIcon,
UploadIcon,
XIcon,
@@ -288,6 +321,14 @@ const messages = defineMessages({
id: 'instances.content-install.search-placeholder',
defaultMessage: 'Search instance',
},
showUnavailable: {
id: 'instances.content-install.show-unavailable',
defaultMessage: 'Show unavailable',
},
hideUnavailable: {
id: 'instances.content-install.hide-unavailable',
defaultMessage: 'Hide unavailable',
},
installedBadge: {
id: 'instances.content-install.installed-badge',
defaultMessage: 'Installed',
@@ -302,12 +343,16 @@ const messages = defineMessages({
'This instance uses a different loader or game version than this project supports.',
},
selectIcon: {
id: 'instances.content-install.select-icon',
defaultMessage: 'Select icon',
id: 'creation-flow.modal.custom-setup.icon.select',
defaultMessage: 'Upload',
},
removeIcon: {
id: 'instances.content-install.remove-icon',
defaultMessage: 'Remove icon',
randomizeIcon: {
id: 'creation-flow.modal.custom-setup.icon.randomize',
defaultMessage: 'Randomize',
},
customizeIcon: {
id: 'creation-flow.modal.custom-setup.icon.customize',
defaultMessage: 'Customize',
},
nameLabel: {
id: 'instances.content-install.name-label',
@@ -368,6 +413,8 @@ const props = defineProps<{
preferredLoader?: string | null
preferredGameVersion?: string | null
projectInfo?: ContentInstallProjectInfo | null
randomizeIcon?: () => Promise<{ path: string; previewUrl: string } | null>
customizeIcon?: () => void
}>()
const emit = defineEmits<{
@@ -424,6 +471,7 @@ const selectedLoader = ref<string | null>(null)
const selectedGameVersion = ref<string | null>(null)
const iconPath = ref<string | null>(null)
const iconPreviewUrl = ref<string | null>(null)
const randomizing = ref(false)
const showSnapshots = ref(false)
const hasReleaseData = computed(
@@ -454,6 +502,20 @@ function removeIcon() {
iconPreviewUrl.value = null
}
async function randomizeInstanceIcon() {
if (!props.randomizeIcon || randomizing.value) return
randomizing.value = true
try {
const generated = await props.randomizeIcon()
if (!generated) return
iconPath.value = generated.path
iconPreviewUrl.value = generated.previewUrl
} finally {
randomizing.value = false
}
}
function resetState() {
tab.value = props.defaultTab ?? 'existing'
searchFilter.value = ''
@@ -461,6 +523,7 @@ function resetState() {
instanceName.value = `New instance (${props.instances.length + 1})`
iconPath.value = null
iconPreviewUrl.value = null
void randomizeInstanceIcon()
selectedLoader.value = props.preferredLoader ?? props.compatibleLoaders[0] ?? null
const preferred = props.preferredGameVersion
@@ -497,6 +560,11 @@ function hide() {
modal.value?.hide()
}
function setIcon(path: string, previewUrl: string) {
iconPath.value = path
iconPreviewUrl.value = previewUrl
}
function handleCreateAndInstall() {
if (!instanceName.value || !selectedLoader.value || !selectedGameVersion.value) return
emit('create-and-install', {
@@ -509,5 +577,5 @@ function handleCreateAndInstall() {
hide()
}
defineExpose({ show, hide })
defineExpose({ show, hide, setIcon })
</script>
@@ -65,7 +65,7 @@
type="server-onboarding"
:available-loaders="['vanilla', 'fabric', 'neoforge', 'forge', 'quilt', 'paper', 'purpur']"
:show-snapshot-toggle="true"
:search-modpacks="searchModpacks"
:search-projects="searchModpacks"
:get-project-versions="getProjectVersions"
:finish-disabled="!canSetup"
:finish-disabled-tooltip="!canSetup ? permissionDeniedMessage : undefined"
@@ -1,8 +1,12 @@
<template>
<div
data-pyro-server-list-root
class="relative mx-auto mb-6 flex w-full flex-col p-6"
:class="serverList.length ? 'min-h-screen' : 'min-h-[calc(100vh-14.5rem)]'"
class="relative mx-auto flex w-full flex-col p-6"
:class="
serverList.length && !showEmptyState
? 'min-h-screen mb-6'
: 'min-h-[calc(100vh-14.5rem)] h-full py-0'
"
>
<ServersGuestPlanModal
ref="guestPlanModal"
@@ -134,7 +138,7 @@
<div
v-else-if="showEmptyState"
key="empty"
class="flex h-full flex-col items-center justify-center gap-8 grow max-h-[1100px]"
class="flex h-full flex-col items-center justify-center gap-8 grow"
>
<ServerListEmpty
:logged-in="loggedIn"
@@ -332,6 +336,10 @@ const messages = defineMessages({
id: 'servers.manage.handle-error.title',
defaultMessage: 'An error occurred',
},
unknownError: {
id: 'servers.manage.error.unknown',
defaultMessage: 'Unknown error',
},
purchaseUnavailableTitle: {
id: 'servers.manage.purchase-unavailable.title',
defaultMessage: 'Purchase unavailable',
@@ -677,7 +685,9 @@ function handleError(err: unknown) {
}
function formatFetchError(error: unknown) {
return error instanceof Error && error.message ? error.message : 'Unknown error'
return error instanceof Error && error.message
? error.message
: formatMessage(messages.unknownError)
}
function handleSignIn() {
-39
View File
@@ -608,9 +608,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Hledat verzi hry..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Odebrat ikonu"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Vybrat ikonu"
},
@@ -767,24 +764,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Zrušit vše"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Procházet modpacky"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Importovat modpack"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Už víš modpack, který chceš nainstalovat?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Nenalezeny žádné výsledky"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Hledat modpack"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Instance je nastavení Minecraftu s konkrétním loaderem, verzí a módy."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Začněte od začátku výběrem loaderu a verze hry."
},
@@ -818,9 +797,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Vyber typ světa"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Výběr modpacku"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Vytvoření instance"
},
@@ -1811,15 +1787,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Nebyly nalezeny žádné kompatibilní instance"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Odebrat ikonu"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Hledat instance"
},
"instances.content-install.select-icon": {
"defaultMessage": "Vybrat ikonu"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Mometálně"
},
@@ -3665,9 +3635,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Už máte server?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Zjistěte víc o Modrinth hostingu"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -3677,9 +3644,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Instalujte mody, zvěte přátele a hrajte společně, to vše z Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Zatím žádné servery"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Vyberte si své oblíbené mody a o zbytek se postaráme."
},
@@ -3695,9 +3659,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Přihlásit se"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Nastavte si server stejně jako svět pro jednoho hráče."
},
"servers.listing.copied-label": {
"defaultMessage": "Zkopírováno"
},
-24
View File
@@ -431,9 +431,6 @@
"creation-flow.button.setup-server": {
"defaultMessage": "Opsæt server"
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Fjern ikon"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Vælg ikon"
},
@@ -500,12 +497,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Ryd alle"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Ingen resultater fundet"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Søg efter modpack"
},
"creation-flow.modal.setup-type.option.modpack-base.title": {
"defaultMessage": "Installer modpack"
},
@@ -791,15 +782,9 @@
"instances.content-install.new-tab": {
"defaultMessage": "Ny instans"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Fjern ikon"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Søg efter instanser"
},
"instances.content-install.select-icon": {
"defaultMessage": "Vælg ikon"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Nuværrende"
},
@@ -1964,9 +1949,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Har du allerede en server?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Lær mere om Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -1976,9 +1958,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Installer mods, inviter venner og spil alle sammen fra Modrinth Appen."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Ingen servere endnu"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Vælg dine favorit mods og vi håndtere resten."
},
@@ -1991,9 +1970,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Log ind"
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Simpel opsætning"
},
"servers.notice.heading.info": {
"defaultMessage": "Information"
},
-42
View File
@@ -707,9 +707,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Spielversionen durchsuchen..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Icon entfernen"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Icon auswählen"
},
@@ -872,24 +869,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Alle entfernen"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Modpacks durchsuchen"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Modpack importieren"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Weisst du schon, welches Modpack du installieren möchtest?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Keine Ergebnisse gefunden"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Nach Modpack suchen"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Eine Instanz ist eine Einrichtung von Minecraft mit einem bestimmten Loader, einer bestimmten Version und Mods."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Beginne von vorne, indem du einen Loader und eine Spielversion auswählst."
},
@@ -923,9 +902,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Welttyp auswählen"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Modpack auswählen"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Instanz erstellen"
},
@@ -2057,15 +2033,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Keine kompatiblen Instanzen gefunden"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Icon entfernen"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Instanz suchen"
},
"instances.content-install.select-icon": {
"defaultMessage": "Icon auswählen"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Aktuell"
},
@@ -4622,9 +4592,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Du hast bereits einen Server?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Erfahre mehr über Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4634,9 +4601,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Installiere Mods, lade Freunde ein und spiel gemeinsam, alles über die Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Noch keine Server"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Wähle deine Lieblingsmods aus, den Rest erledigen wir."
},
@@ -4652,12 +4616,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Anmelden"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Richte deinen Server genauso wie eine Einzelspielerwelt ein."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Einfache Einrichtung"
},
"servers.listing.copied-label": {
"defaultMessage": "Kopiert"
},
-42
View File
@@ -707,9 +707,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Spielversionen durchsuchen..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Icon entfernen"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Icon auswählen"
},
@@ -872,24 +869,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Alle entfernen"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Modpacks durchsuchen"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Modpack importieren"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Weißt du schon, welches Modpack du installieren möchtest?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Keine Ergebnisse gefunden"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Nach Modpack suchen"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Eine Instanz ist eine Einrichtung von Minecraft mit einem bestimmten Loader, einer bestimmten Version und Mods."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Beginne von vorne, indem du einen Loader und eine Spielversion auswählst."
},
@@ -923,9 +902,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Welttyp auswählen"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Modpack auswählen"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Instanz erstellen"
},
@@ -2057,15 +2033,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Keine kompatiblen Instanzen gefunden"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Icon entfernen"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Instanz suchen"
},
"instances.content-install.select-icon": {
"defaultMessage": "Icon auswählen"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Aktuell"
},
@@ -4622,9 +4592,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Du hast bereits einen Server?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Erfahre mehr über Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4634,9 +4601,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Installiere Mods, lade Freunde ein und spiel gemeinsam, alles über die Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Noch keine Server"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Wähle deine Lieblingsmods aus, den Rest erledigen wir."
},
@@ -4652,12 +4616,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Anmelden"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Richte deinen Server genauso wie eine Einzelspielerwelt ein."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Einfache Einrichtung"
},
"servers.listing.copied-label": {
"defaultMessage": "Kopiert"
},
+126 -21
View File
@@ -833,11 +833,14 @@
"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.customize": {
"defaultMessage": "Customize"
},
"creation-flow.modal.custom-setup.icon.randomize": {
"defaultMessage": "Randomize"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Select icon"
"defaultMessage": "Upload"
},
"creation-flow.modal.custom-setup.loader-version-type.latest": {
"defaultMessage": "Latest"
@@ -1013,8 +1016,20 @@
"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.project.known-project.prompt": {
"defaultMessage": "Already know what you want to play?"
},
"creation-flow.modal.project.search.create-instance": {
"defaultMessage": "Create instance"
},
"creation-flow.modal.project.search.install-modpack": {
"defaultMessage": "Install modpack"
},
"creation-flow.modal.project.search.no-results": {
"defaultMessage": "No results found"
},
"creation-flow.modal.project.search.placeholder": {
"defaultMessage": "Search mods, modpacks, and more..."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Start from scratch by picking a loader and game version."
@@ -1029,10 +1044,16 @@
"defaultMessage": "Import instance"
},
"creation-flow.modal.setup-type.option.modpack-base.description": {
"defaultMessage": "Browse modpacks on Modrinth or import one from a file."
"defaultMessage": "Choose a project and well use its latest version."
},
"creation-flow.modal.setup-type.option.modpack-base.title": {
"defaultMessage": "Install modpack"
"defaultMessage": "Start from a mod or modpack"
},
"creation-flow.modal.setup-type.option.upload-modpack.description": {
"defaultMessage": "Install a modpack from an .mrpack file on your device."
},
"creation-flow.modal.setup-type.option.upload-modpack.title": {
"defaultMessage": "Upload a modpack"
},
"creation-flow.modal.setup-type.option.vanilla-minecraft.description": {
"defaultMessage": "Classic Minecraft with no mods or plugins."
@@ -1049,9 +1070,6 @@
"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"
},
@@ -1733,6 +1751,30 @@
"files.zip-url-modal.zip-header": {
"defaultMessage": "Uploading .zip contents from URL"
},
"filter-bar.add": {
"defaultMessage": "Add"
},
"filter-bar.clear": {
"defaultMessage": "Clear"
},
"filter-bar.clear-named-filter": {
"defaultMessage": "Clear {name} filter"
},
"filter-bar.filtered-by": {
"defaultMessage": "Filtered by"
},
"filter-bar.no-options": {
"defaultMessage": "No options available."
},
"filter-bar.no-search-results": {
"defaultMessage": "No options found."
},
"filter-bar.search-placeholder": {
"defaultMessage": "Search..."
},
"filter-bar.select-all": {
"defaultMessage": "Select all"
},
"form.label.address-line": {
"defaultMessage": "Address line"
},
@@ -1970,6 +2012,12 @@
"icon-select.select": {
"defaultMessage": "Select icon"
},
"input.clear": {
"defaultMessage": "Clear input"
},
"input.search": {
"defaultMessage": "Search"
},
"input.search-version.placeholder": {
"defaultMessage": "Search version..."
},
@@ -2162,6 +2210,9 @@
"instances.content-install.header": {
"defaultMessage": "Install project"
},
"instances.content-install.hide-unavailable": {
"defaultMessage": "Hide unavailable"
},
"instances.content-install.incompatible-tooltip": {
"defaultMessage": "This instance uses a different loader or game version than this project supports."
},
@@ -2189,14 +2240,11 @@
"instances.content-install.no-instances": {
"defaultMessage": "No compatible instances found"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Remove icon"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Search instance"
},
"instances.content-install.select-icon": {
"defaultMessage": "Select icon"
"instances.content-install.show-unavailable": {
"defaultMessage": "Show unavailable"
},
"instances.managed-content-modal.empty-description": {
"defaultMessage": "This source does not include any managed content."
@@ -2828,6 +2876,39 @@
"notification.error.title": {
"defaultMessage": "An error occurred"
},
"notifications.actor.unknown": {
"defaultMessage": "Someone"
},
"notifications.close": {
"defaultMessage": "Close"
},
"notifications.dismiss": {
"defaultMessage": "Dismiss notification"
},
"notifications.friend-request.body": {
"defaultMessage": "<actor>{actor}</actor> sent you a friend request."
},
"notifications.instance-invite.body": {
"defaultMessage": "<actor>{actor}</actor> invited you to <entity>{entity}</entity> instance."
},
"notifications.instance-ready.launch-game": {
"defaultMessage": "Launch game"
},
"notifications.instance-ready.open-instance": {
"defaultMessage": "Instance"
},
"notifications.instance-ready.status": {
"defaultMessage": "Installed and ready to play."
},
"notifications.invite.accept": {
"defaultMessage": "Accept"
},
"notifications.invite.decline": {
"defaultMessage": "Decline"
},
"notifications.server-invite.body": {
"defaultMessage": "<actor>{actor}</actor> invited you to manage the server <entity>{entity}</entity>."
},
"omorphia.component.badge.label.accepted": {
"defaultMessage": "Accepted"
},
@@ -4971,7 +5052,7 @@
"defaultMessage": "Already have a server?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Learn more about Modrinth Hosting"
"defaultMessage": "Learn more"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
@@ -4982,9 +5063,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Install mods, invite friends, and play together all from the Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "No servers yet"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Pick your favorite mods and we handle the rest."
},
@@ -4992,13 +5070,13 @@
"defaultMessage": "One-click mod installs"
},
"servers.list-empty.play-with-friends-description": {
"defaultMessage": "Invite friends and get them set up right in the Modrinth App."
"defaultMessage": "Invite friends to play your server with one-click play from the Modrinth App."
},
"servers.list-empty.play-with-friends-title": {
"defaultMessage": "Play with friends"
},
"servers.list-empty.sign-in-button": {
"defaultMessage": "Sign in"
"defaultMessage": "Sign in to Modrinth"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Set up your server just like a single player world."
@@ -5111,6 +5189,9 @@
"servers.manage.error.title": {
"defaultMessage": "Servers could not be loaded"
},
"servers.manage.error.unknown": {
"defaultMessage": "Unknown error"
},
"servers.manage.handle-error.title": {
"defaultMessage": "An error occurred"
},
@@ -5654,6 +5735,9 @@
"sharing.invite-players-modal.avatar-alt": {
"defaultMessage": "{username}'s avatar"
},
"sharing.invite-players-modal.cancel": {
"defaultMessage": "Cancel"
},
"sharing.invite-players-modal.custom-expiry": {
"defaultMessage": "Custom..."
},
@@ -5690,6 +5774,9 @@
"sharing.invite-players-modal.friends-heading": {
"defaultMessage": "Your friends - {count}"
},
"sharing.invite-players-modal.heading": {
"defaultMessage": "Invite players"
},
"sharing.invite-players-modal.invite": {
"defaultMessage": "Invite"
},
@@ -5735,6 +5822,24 @@
"sharing.invite-players-modal.update-invite-link-failed-title": {
"defaultMessage": "Failed to update invite link"
},
"sharing.invite-players-toast.accept": {
"defaultMessage": "Accept"
},
"sharing.invite-players-toast.decline": {
"defaultMessage": "Decline"
},
"sharing.invite-players-toast.invited-you-to": {
"defaultMessage": "invited you to"
},
"sharing.invite-players-toast.server-suffix": {
"defaultMessage": "server."
},
"skins.select": {
"defaultMessage": "Select skin"
},
"skins.select-named": {
"defaultMessage": "Select {name}"
},
"tag.category.128x": {
"defaultMessage": "128x"
},
-42
View File
@@ -707,9 +707,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Buscar versión del juego..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Eliminar ícono"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Seleccionar ícono"
},
@@ -872,24 +869,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Limpiar todo"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Descubrir modpacks"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Importar modpack"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "¿Ya sabes qué modpack quieres instalar?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "No se encontraron resultados"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Buscar un modpack"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Una instancia es una configuración de Minecraft con un loader, una versión y unos mods específicos."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Empieza desde cero eligiendo un loader y una versión del juego."
},
@@ -923,9 +902,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Selecciona el tipo de mundo"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Elige un modpack"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Crear instancia"
},
@@ -2057,15 +2033,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "No se encontraron instancias compatibles"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Eliminar ícono"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Buscar instancia"
},
"instances.content-install.select-icon": {
"defaultMessage": "Seleccionar ícono"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Actual"
},
@@ -4622,9 +4592,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "¿Ya tienes un servidor?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Aprende más sobre Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4634,9 +4601,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Instala mods, invita amigos, y jueguen juntos todo desde la Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Aún no hay servidores"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Escoge tus mods favoritos y nos encargaremos del resto."
},
@@ -4652,12 +4616,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Iniciar sesión"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Configura tu servidor como si fuera un mundo de un solo jugador."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Configuración simple"
},
"servers.listing.copied-label": {
"defaultMessage": "Copiado"
},
-42
View File
@@ -707,9 +707,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Buscar versión de juego..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Eliminar icono"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Seleccionar icono"
},
@@ -872,24 +869,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Borrar todo"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Descubrir modpacks"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Importar modpack"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "¿Ya sabes cuál es el modpack que quieres instalar?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Ningún resultado encontrado"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Buscar por modpack"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Una instancia es una configuración de Minecraft con un específico loader, versión, y mods."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Empieza desde cero primero eligiendo un loader y una versión de juego."
},
@@ -923,9 +902,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Seleccione tipo de mundo"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Elige modpack"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Crear instancia"
},
@@ -2057,15 +2033,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "No se han encontrado instancias compatibles"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Eliminar icono"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Buscar instancia"
},
"instances.content-install.select-icon": {
"defaultMessage": "Seleccionar icono"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Actual"
},
@@ -4619,9 +4589,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "¿Ya tienes un servidor?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Conoce más sobre el alojamiento de servidores de Modrinth"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4631,9 +4598,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Instala mods, invita amigos, y jueguen todos juntos desde la Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "No se han encontrado servidores todavía"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Elige tus mods favoritos y nosotros nos encargamos del resto."
},
@@ -4649,12 +4613,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Inicia sesión"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Prepara tu servidor tal como si fuera un mundo de un solo jugador."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Configuración Simple"
},
"servers.listing.copied-label": {
"defaultMessage": "Copiado"
},
-42
View File
@@ -707,9 +707,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Rechercher une version de jeu..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Retirer l'icône"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Sélectionner l'icône"
},
@@ -872,24 +869,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Effacer tout"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Rechercher un modpack"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Importer un modpack"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Vous avez déjà le modpack en tête ?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Aucun résultat"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Rechercher un modpack"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Une instance est un setup Minecraft avec un mod loader, une version, et des mods spécifiques."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Partez de zéro en choisissant un loader et une version de jeu."
},
@@ -923,9 +902,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Sélectionner le type de monde"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Choisir un modpack"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Créer une instance"
},
@@ -2051,15 +2027,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Aucune instance compatible n'a été trouvée"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Supprimer l'icône"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Rechercher une instance"
},
"instances.content-install.select-icon": {
"defaultMessage": "Sélectionnez l'icône"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Actuel"
},
@@ -4616,9 +4586,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Vous avez déjà un serveur ?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "En savoir plus à propos de Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4628,9 +4595,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Des mods, des potes, et beaucoup de fun, tout depuis Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Aucun serveur pour le moment"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Choisissez vos mods préférés et nous nous occupons du reste."
},
@@ -4646,12 +4610,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Se connecter"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Mettez votre serveur en place tout comme un monde solo."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Setup simple"
},
"servers.listing.copied-label": {
"defaultMessage": "Copié"
},
-18
View File
@@ -620,9 +620,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Játékverzió keresése..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Ikon eltávolítása"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Ikon feltöltése"
},
@@ -797,9 +794,6 @@
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Modcsomag keresése"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "A játékprofil egy külön Minecraft-környezet egy adott modbetöltővel, verzióval és modokkal."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Kezdés alapoktól: válassz betöltőt és játékverziót."
},
@@ -833,9 +827,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Világ típusa"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Modcsomag kiválasztása"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Játékprofil létrehozása"
},
@@ -1709,15 +1700,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Nem található kompatibilis profil"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Ikon eltávolítása"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Profilok keresése"
},
"instances.content-install.select-icon": {
"defaultMessage": "Ikon feltöltése"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Jelenlegi"
},
@@ -3911,9 +3896,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Telepíts modokat, hívd meg a barátaidat, és játsszatok együtt, mindezt a Modrinth Appban."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Még nincsenek szerverek"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Válaszd ki a kedvenc modjaidat, a többit bízd ránk!"
},
-6
View File
@@ -572,9 +572,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Cari versi permainan..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Hapus ikon"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Pilih ikon"
},
@@ -728,9 +725,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Kosongkan semua"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Hasil pencarian tidak ditemukan"
},
"creation-flow.modal.setup-type.option.vanilla-minecraft.description": {
"defaultMessage": "Minecraft klasik tanpa mod atau pasang-masuk."
},
-42
View File
@@ -683,9 +683,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Cerca tra le versioni del gioco..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Rimuovi icona"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Scegli un'icona"
},
@@ -848,24 +845,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Deseleziona tutto"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Sfoglia i pacchetti"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Importa pacchetto"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Sai già quale pacchetto di mod installare?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Nessun risultato trovato"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Cerca tra i pacchetti"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Un'istanza è una combinazione di loader, versione e mod per Minecraft."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Parti da zero scegliendo il loader e la versione del gioco."
},
@@ -899,9 +878,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Seleziona il tipo di mondo"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Scegli pacchetto"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Crea istanza"
},
@@ -2015,15 +1991,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Nessuna istanza compatibile trovata"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Rimuovi icona"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Cerca istanza"
},
"instances.content-install.select-icon": {
"defaultMessage": "Scegli un'icona"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Attuale"
},
@@ -4580,9 +4550,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Hai già un server?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Scopri di più su Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4592,9 +4559,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Crea un server, con o senza mod, e invita chi vuoi tramite la Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Ancora nessun server"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Scegli le tue mod preferite, al resto ci pensiamo noi."
},
@@ -4610,12 +4574,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Accedi"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Configura il tuo server come vuoi tu, senza alcuno sforzo."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Configurazione immediata"
},
"servers.listing.copied-label": {
"defaultMessage": "Copiato"
},
-42
View File
@@ -704,9 +704,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "ゲームバージョンを検索…"
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "アイコンを削除"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "アイコンを選択"
},
@@ -869,24 +866,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "全ての選択を解除"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Modパックを探す"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Modパックをインポート"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "インストールしたいMODパックは既に決まっていますか?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "見つかりません"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Modパックを検索"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "インスタンスとは、特定のローダー、バージョン、およびModを備えたMinecraftの起動構成のことです"
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "ローダーとゲームのバージョンを選択して、最初から始めましょう"
},
@@ -920,9 +899,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "ワールドタイプを選択してください"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Modパックを選択"
},
"creation-flow.title.create-instance": {
"defaultMessage": "インスタンスを作成する"
},
@@ -2054,15 +2030,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "互換性のあるインスタンスが見つかりませんでした"
},
"instances.content-install.remove-icon": {
"defaultMessage": "アイコンを削除"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "インスタンスを検索"
},
"instances.content-install.select-icon": {
"defaultMessage": "アイコンを選択"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "現在"
},
@@ -4466,9 +4436,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "既にサーバーをお持ちですか?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Modrinth Hostingについて詳しくはこちらをご覧ください"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4478,9 +4445,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Modrinth Appを使えば、MODのインストール、友達の招待、そして一緒にプレイすることなど、すべてが可能です。"
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "サーバーはまだありません"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "お好みのModを選んだら、あとはすべて対応いたします。"
},
@@ -4496,12 +4460,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "サインイン"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "サーバーは、シングルプレイヤーワールドと同じように設定してください。"
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "簡単なセットアップ"
},
"servers.listing.copied-label": {
"defaultMessage": "コピーしました"
},
-42
View File
@@ -707,9 +707,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "게임 버전 검색..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "아이콘 제거"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "아이콘 선택"
},
@@ -872,24 +869,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "모두 해제"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "모드팩 둘러보기"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "모드팩 가져오기"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "설치하고 싶은 모드팩을 이미 알고 계신가요?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "결과 없음"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "모드팩 검색"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "인스턴스란 특정 로더, 버전 및 모드가 적용된 Minecraft 환경을 의미합니다."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "로더와 게임 버전을 선택하여 처음부터 시작합니다."
},
@@ -923,9 +902,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "세계 방식 선택"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "모드팩 선택"
},
"creation-flow.title.create-instance": {
"defaultMessage": "인스턴스 생성"
},
@@ -2057,15 +2033,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "호환가능한 인스턴스 없음"
},
"instances.content-install.remove-icon": {
"defaultMessage": "아이콘 제거"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "인스턴스 검색"
},
"instances.content-install.select-icon": {
"defaultMessage": "아이콘 선택"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "현재"
},
@@ -4607,9 +4577,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "서버가 이미 있으신가요?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Modrinth 호스팅에 관하여 알아보기"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth 호스팅"
},
@@ -4619,9 +4586,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Modrinth 앱에서 모드를 설치하고, 친구를 초대해 함께 플레이하세요."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "서버 없음"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "즐겨찾는 모드들을 선택하시고 나머지는 저희에게 맡겨주세요."
},
@@ -4637,12 +4601,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "로그인"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "싱글 플레이어 월드처럼 서버를 설정하세요."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "간단한 설정"
},
"servers.listing.copied-label": {
"defaultMessage": "복사됨"
},
-39
View File
@@ -563,9 +563,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Cari versi permainan..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Alih keluar ikon"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Pilih ikon"
},
@@ -722,21 +719,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Kosongkan semua"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Layari pek mod"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Import pek mod"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Sudah tahu pek mod yang ingin anda pasang?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Tiada hasil dijumpai"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Cari pek mod"
},
"creation-flow.modal.setup-type.option.custom-setup.title": {
"defaultMessage": "Persediaan tersuai"
},
@@ -758,9 +740,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Pilih jenis dunia"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Pilih pek mod"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Cipta pemasangan"
},
@@ -1658,15 +1637,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Tiada pemasangan yang serasi dijumpai"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Alih keluar ikon"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Cari pemasangan"
},
"instances.content-install.select-icon": {
"defaultMessage": "Pilih ikon"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Semasa"
},
@@ -3884,9 +3857,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Sudah mempunyai pelayan?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Ketahui lebih lanjut tentang Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -3896,9 +3866,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Pasang mod, jemput rakan dan main bersama-sama, semuanya dari Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Belum ada pelayan"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Pilih mod kegemaran anda dan kami uruskan selebihnya."
},
@@ -3914,12 +3881,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Daftar masuk"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Sediakan pelayan anda seperti dunia ekapemain."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Persediaan yang mudah"
},
"servers.listing.copied-label": {
"defaultMessage": "Disalin"
},
-42
View File
@@ -707,9 +707,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Spelversie zoeken..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Pictogram verwijderen"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Pictogram selecteren"
},
@@ -872,24 +869,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Alles wissen"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Modpacks bekijken"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Modpack importeren"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Weet je al welk modpack je wilt installeren?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Geen resultaten gevonden"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Modpack zoeken"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Een instantie is een Minecraft-installatie met een specifieke loader, versie en mods."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Begin helemaal opnieuw door een loader en een spelversie te kiezen."
},
@@ -923,9 +902,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Wereldtype selecteren"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Modpack kiezen"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Instantie aanmaken"
},
@@ -2054,15 +2030,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Er zijn geen compatibele instanties gevonden"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Pictogram verwijderen"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Zoek instantie"
},
"instances.content-install.select-icon": {
"defaultMessage": "Selecteer pictogram"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Huidige"
},
@@ -4613,9 +4583,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Heb je al een server?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Meer informatie over Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4625,9 +4592,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Installeer mods, nodig vrienden uit en speel samen allemaal via de Modrinth app."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Nog geen servers"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Kies je favoriete mods en wij regelen de rest."
},
@@ -4643,12 +4607,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Inloggen"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Stel je server in net zoals een eigen wereld."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Eenvoudige set-up"
},
"servers.listing.copied-label": {
"defaultMessage": "Gekopieerd"
},
-15
View File
@@ -437,9 +437,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Søk etter spillversjon..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Fjern ikon"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Velg ikon"
},
@@ -461,12 +458,6 @@
"creation-flow.modal.import-instance.search.placeholder": {
"defaultMessage": "Søk etter instansnavn"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Ingen resultater funnet"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Søk etter modpakke"
},
"external-files.permissions-card.add-files-modal.selected-count": {
"defaultMessage": "{count, plural, one {# fil} other {# filer}} valgt"
},
@@ -851,15 +842,9 @@
"instances.content-install.new-tab": {
"defaultMessage": "Ny instans"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Fjern ikon"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Søk etter instans"
},
"instances.content-install.select-icon": {
"defaultMessage": "Velg ikon"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Nåværende"
},
-42
View File
@@ -707,9 +707,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Szukaj wersję gry..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Usuń ikonę"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Wybierz ikonę"
},
@@ -872,24 +869,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Wyczyść wszystko"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Przeglądaj paczki modów"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Importuj paczkę modów"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Wiesz już którą paczkę modów chcesz zainstalować?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Brak wyników wyszukiwania"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Szukaj paczki modów"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Instancja to instalacja Minecraft w danej wersji, z danym loaderem i modami."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Zacznij od podstaw wybierając loader i wersję gry."
},
@@ -923,9 +902,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Wybierz typ świata"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Wybierz paczkę modów"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Utwórz instancję"
},
@@ -2048,15 +2024,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Nie znaleziono kompatybilnych instancji"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Usuń ikonę"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Szukaj instancji"
},
"instances.content-install.select-icon": {
"defaultMessage": "Wybierz ikonę"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Aktualny"
},
@@ -4589,9 +4559,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Posiadasz już serwer?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Dowiedz się więcej na temat Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4601,9 +4568,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Instaluj modyfikacje, zapraszaj znajomych i graj z nimi przy użyciu Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Brak serwerów"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Wybierz swoje ulubione mody, a my zajmiemy się resztą."
},
@@ -4619,12 +4583,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Zaloguj się"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Ustaw serwer tak, jak zwykły świat w trybie jednoosobowym."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Prosta konfiguracja"
},
"servers.listing.copied-label": {
"defaultMessage": "Skopiowano"
},
-42
View File
@@ -707,9 +707,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Procurar versão de jogo..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Remover ícone"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Selecionar ícone"
},
@@ -872,24 +869,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Limpar tudo"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Navegue pelos pacotes de mods"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Importar pacote de mods"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Já sabe qual pacote de mods deseja instalar?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Nenhum resultado encontrado"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Buscar pacotes de mods"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Uma instância é uma configuração do Minecraft com um loader, versão e mods específicos."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Comece do zero, escolhendo um loader e uma versão do jogo."
},
@@ -923,9 +902,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Selecione o tipo de mundo"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Escolha o pacote de mods"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Criar instância"
},
@@ -2057,15 +2033,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Nenhuma instância compatível encontrada"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Remover ícone"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Buscar instância"
},
"instances.content-install.select-icon": {
"defaultMessage": "Selecionar ícone"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Atual"
},
@@ -4622,9 +4592,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Já possui um servidor?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Saiba mais sobre o Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4634,9 +4601,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Instale mods, convide amigos e joguem juntos, tudo pelo Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Ainda não há servidores"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Escolha seus mods favoritos e nós cuidamos do resto."
},
@@ -4652,12 +4616,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Inicie a sessão"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Configure seu servidor exatamente como um mundo de um jogador."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Configuração simples"
},
"servers.listing.copied-label": {
"defaultMessage": "Copiado"
},
-6
View File
@@ -1001,15 +1001,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Nu s-au găsit instanțe compatibile"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Înlăturare iconiță"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Căutare instanță"
},
"instances.content-install.select-icon": {
"defaultMessage": "Selectează iconița"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Curent"
},
-42
View File
@@ -647,9 +647,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Поиск версии игры..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Удалить иконку"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Выбрать иконку"
},
@@ -812,24 +809,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Очистить всë"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Найти сборки"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Импортировать сборку"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Уже знаете, какую сборку хотите скачать?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Ничего не найдено"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Поиск сборки"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Сборка — это установка Minecraft с определённым загрузчиком модов, версией и модами."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Начать с нуля, выбрав загрузчик и версию игры."
},
@@ -863,9 +842,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Выберите тип мира"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Выберите сборку"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Создание сборки"
},
@@ -1973,15 +1949,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Совместимые сборки не найдены"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Удалить иконку"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Поиск сборки"
},
"instances.content-install.select-icon": {
"defaultMessage": "Выбрать иконку"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Текущая"
},
@@ -4535,9 +4505,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Уже есть сервер?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Подробнее о Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4547,9 +4514,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Играйте в сборки вместе с друзьями через Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Серверов ещё нет"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Выбирайте любимые моды, а мы сделаем всё остальное."
},
@@ -4565,12 +4529,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Войти"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Настраивайте сервер подобно одиночному миру."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Простая настройка"
},
"servers.listing.copied-label": {
"defaultMessage": "Скопировано"
},
-42
View File
@@ -674,9 +674,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Traži verziju igre..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Smakni ikonu"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Izaberi ikonu"
},
@@ -839,24 +836,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Obriši sve"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Pregledaj modpakove"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Uvezi modpack"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Već znaš koji modpak želiš da instaliraš?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Nema pronađenih rezultata"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Potraži modpack"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Instanca je podešavanje Mínecrafta sa određenim programom za učitavanje, verzijom i modovima."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Počni od nule tako što izabereš pokretač i verziju igre."
},
@@ -890,9 +869,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Izaberi tip sveta"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Izaberi modpack"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Kreiraj instancu"
},
@@ -2009,15 +1985,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Nema pronađenih kompatibilnih instanca"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Smakni ikonu"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Pretraži instancu"
},
"instances.content-install.select-icon": {
"defaultMessage": "Izaberi ikonu"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Upravo"
},
@@ -4391,9 +4361,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Već imaš server?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Saznaj više o Modrinth hostiranju"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth hostiranje"
},
@@ -4403,9 +4370,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Instaliraj modove, pozvi prijatelje, i igraj skupa ave u Modrinth aplikaciji."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Nema još servera"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Izaberi svoje omiljene modove, a mi ćemo se pobrinuti za ostalo."
},
@@ -4421,12 +4385,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Prijavi se"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Podesi svoj server baš kao samostalni svet."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Jednostavno podešavanje"
},
"servers.listing.copied-label": {
"defaultMessage": "Kopirano"
},
-33
View File
@@ -701,9 +701,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Sök spelversion..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Ta bort ikon"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Välj ikon"
},
@@ -824,24 +821,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Töm allt"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Bläddra bland modpaket"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Importera modpaket"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Vet du redan modpaketet du vill installera?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Inga resultat hittades"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Sök efter modpaket"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "En instans är ett Minecraft-upplägg med en specifik laddare, version och moddar."
},
"creation-flow.modal.setup-type.option.custom-setup.title": {
"defaultMessage": "Anpassat upplägg"
},
@@ -872,9 +851,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Välj världstyp"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Välj modpaket"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Skapa instans"
},
@@ -1778,15 +1754,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Inga kompatibla instanser hittades"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Radera ikon"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Sök för instans"
},
"instances.content-install.select-icon": {
"defaultMessage": "Välj ikon"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Nuvarande"
},
@@ -4160,9 +4130,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Installera moddar, bjud in vänner och spela tillsammans, allt från Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Inga servrar än"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Välj dina favorit moddar så hanterar vi resten."
},
-6
View File
@@ -308,15 +308,9 @@
"instances.content-install.new-tab": {
"defaultMessage": "อินสแตนซ์อันใหม่"
},
"instances.content-install.remove-icon": {
"defaultMessage": "ลบไอคอน"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "ค้นหาอินสแตนซ์"
},
"instances.content-install.select-icon": {
"defaultMessage": "เลือกไอคอน"
},
"label.available": {
"defaultMessage": "ว่างอยู่ {amount}"
},
-42
View File
@@ -686,9 +686,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Oyun sürümü ara..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Simgeyi kaldır"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Simge seç"
},
@@ -851,24 +848,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Tümünü temizle"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Modpacklere göz atın"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Mod paketini dışa aktar"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Yüklemek istediğiniz mod paketini zaten biliyor musunuz?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Sonuç bulunamadı"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Mod paketi ara"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Her bir 'kurulum', kendine has yükleyicisi (modloader), sürümü ve modları olan bir Minecraft kurulumudur."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Bir yükleyici ve sürüm seçerek başlayın."
},
@@ -902,9 +881,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Dünya Türünü Seçin"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Modpack seçin"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Kurulum oluştur"
},
@@ -2030,15 +2006,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Uyumlu kurulum yok"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Simgeyi kaldır"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Kurulum ara"
},
"instances.content-install.select-icon": {
"defaultMessage": "Simge seç"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Şu anki"
},
@@ -4442,9 +4412,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Zaten sunucunuz var mı?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Modrinth Hosting hakkında daha fazla bilgi edinin"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4454,9 +4421,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Mod kur, arkadaş davet et, birlikte oyna ve hepsini Modrinth App'ten yap."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Daha sunucu yok"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Beğendiğiniz modları seçin ve biz geri kalan işi hallederiz."
},
@@ -4472,12 +4436,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Giriş yap"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Sunucunuzu tıpkı tek oyunculu bir dünya gibi kurun."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Basit kurulum"
},
"servers.listing.copied-label": {
"defaultMessage": "Kopyalandı"
},
-18
View File
@@ -662,9 +662,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Знайти версію гри…"
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Видалити значок"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Вибрати значок"
},
@@ -842,9 +839,6 @@
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Пошук збірок"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Профіль — це налаштування Minecraft із певним завантажувачем, версією та модами."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Почніть з нуля, вибравши завантажувач і версію гри."
},
@@ -878,9 +872,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Виберіть тип світу"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Виберіть збірку"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Створити збірку"
},
@@ -2012,15 +2003,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Не знайдено сумісних профілів"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Прибрати значок"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Пошук профілів"
},
"instances.content-install.select-icon": {
"defaultMessage": "Вибрати значок"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Поточна"
},
@@ -4589,9 +4574,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Установлюйте моди, запрошуйте друзів і грайте разом із Modrinth App."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Серверів ще немає"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Виберіть свої улюблені моди, а ми подбаємо про решту."
},
-42
View File
@@ -608,9 +608,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "Tìm phiên bản Game..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "Xóa biểu tượng"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "Chọn biểu tượng"
},
@@ -773,24 +770,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "Xóa hết"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "Duyệt modpack"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "Thêm modpack"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "Biết modpack bạn định cài là gì rồi?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "Không tìm thấy"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "Tìm modpack"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "Cấu hình là một bản cài Minecraft với mod, phiên bản và loader của riêng nó."
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "Cài từ đầu bằng cách lựa chọn mod loader và phiên bản game."
},
@@ -824,9 +803,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "Chọn loại thế giới"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "Chọn modpack"
},
"creation-flow.title.create-instance": {
"defaultMessage": "Tạo cấu hình"
},
@@ -1688,15 +1664,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "Không tìm thấy phiên chơi tương thích"
},
"instances.content-install.remove-icon": {
"defaultMessage": "Xoá biểu tượng"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "Tìm phiên chơi"
},
"instances.content-install.select-icon": {
"defaultMessage": "Chọn biểu tượng"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "Hiện tại"
},
@@ -3893,9 +3863,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "Đã có máy chủ rồi?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "Tìm hiểu thêm về Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -3905,9 +3872,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "Cài mod, mời bạn bè, và chơi cùng nhau mà chỉ cần Ứng dụng Modrinth."
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "Không máy chủ khả dụng"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "Chọn bản mod mà bạn yêu thích và chúng tôi sẽ làm tất cả phần còn lại."
},
@@ -3923,12 +3887,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "Đăng nhập"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "Thiết lập máy chủ giống như thế giới chơi đơn."
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "Thiết lập đơn giản"
},
"servers.listing.copied-label": {
"defaultMessage": "Đã sao chép"
},
-42
View File
@@ -707,9 +707,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "搜索游戏版本……"
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "移除图标"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "选择图标"
},
@@ -872,24 +869,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "清除所有"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "浏览整合包"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "导入整合包"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "已经知道你想安装哪个整合包?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "未找到结果"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "搜索整合包"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "实例是带有特定加载器、游戏版本和模组的一套 Minecraft 配置。"
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "从头开始,选择一个加载器和游戏版本。"
},
@@ -923,9 +902,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "选择世界类型"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "选择整合包"
},
"creation-flow.title.create-instance": {
"defaultMessage": "创建实例"
},
@@ -2057,15 +2033,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "未找到相关的实例"
},
"instances.content-install.remove-icon": {
"defaultMessage": "移除图标"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "搜索实例"
},
"instances.content-install.select-icon": {
"defaultMessage": "选择图标"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "当前"
},
@@ -4622,9 +4592,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "已经有服务器了吗?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "深入了解 Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4634,9 +4601,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "安装模组、邀请好友并一起畅玩,这一切都能在 Modrinth App 中轻松完成。"
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "目前还没有服务器"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "选出你最喜欢的模组,其余的交给我们来处理。"
},
@@ -4652,12 +4616,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "登录"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "像设置单人游戏世界一样配置你的服务器。"
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "设置简单"
},
"servers.listing.copied-label": {
"defaultMessage": "已复制"
},
-42
View File
@@ -707,9 +707,6 @@
"creation-flow.modal.custom-setup.game-version.search-placeholder": {
"defaultMessage": "搜尋遊戲版本..."
},
"creation-flow.modal.custom-setup.icon.remove": {
"defaultMessage": "移除圖示"
},
"creation-flow.modal.custom-setup.icon.select": {
"defaultMessage": "選擇圖示"
},
@@ -872,24 +869,6 @@
"creation-flow.modal.import-instance.selection.clear-all": {
"defaultMessage": "清除全部"
},
"creation-flow.modal.modpack.action.browse": {
"defaultMessage": "瀏覽模組包"
},
"creation-flow.modal.modpack.action.import": {
"defaultMessage": "匯入模組包"
},
"creation-flow.modal.modpack.known-modpack.prompt": {
"defaultMessage": "已經選好想要安裝的模組包了嗎?"
},
"creation-flow.modal.modpack.search.no-results": {
"defaultMessage": "找不到結果"
},
"creation-flow.modal.modpack.search.placeholder": {
"defaultMessage": "搜尋模組包"
},
"creation-flow.modal.setup-type.instance.description": {
"defaultMessage": "實例是指具有特定載入器、版本及模組的 Minecraft 設定。"
},
"creation-flow.modal.setup-type.option.custom-setup.description": {
"defaultMessage": "從頭開始,先選擇載入器與遊戲版本。"
},
@@ -923,9 +902,6 @@
"creation-flow.modal.setup-type.title.world": {
"defaultMessage": "選擇世界類型"
},
"creation-flow.title.choose-modpack": {
"defaultMessage": "選擇模組包"
},
"creation-flow.title.create-instance": {
"defaultMessage": "建立實例"
},
@@ -2057,15 +2033,9 @@
"instances.content-install.no-instances": {
"defaultMessage": "找不到相容的實例"
},
"instances.content-install.remove-icon": {
"defaultMessage": "移除圖示"
},
"instances.content-install.search-placeholder": {
"defaultMessage": "搜尋實例"
},
"instances.content-install.select-icon": {
"defaultMessage": "選擇圖示"
},
"instances.updater-modal.badge.current": {
"defaultMessage": "目前"
},
@@ -4622,9 +4592,6 @@
"servers.list-empty.already-have-server-label": {
"defaultMessage": "已經有伺服器了嗎?"
},
"servers.list-empty.learn-more-link": {
"defaultMessage": "深入了解 Modrinth Hosting"
},
"servers.list-empty.modrinth-hosting-label": {
"defaultMessage": "Modrinth Hosting"
},
@@ -4634,9 +4601,6 @@
"servers.list-empty.no-servers-description": {
"defaultMessage": "安裝模組、邀請好友並一起暢玩,這一切都能在 Modrinth App 中輕鬆完成。"
},
"servers.list-empty.no-servers-title": {
"defaultMessage": "目前還沒有伺服器"
},
"servers.list-empty.one-click-mod-installs-description": {
"defaultMessage": "挑選你喜愛的模組,剩下的就交給我們處理。"
},
@@ -4652,12 +4616,6 @@
"servers.list-empty.sign-in-button": {
"defaultMessage": "登入"
},
"servers.list-empty.simple-setup-description": {
"defaultMessage": "設定伺服器就像建立單人遊戲世界一樣簡單。"
},
"servers.list-empty.simple-setup-title": {
"defaultMessage": "簡單設定"
},
"servers.listing.copied-label": {
"defaultMessage": "已複製"
},
@@ -1,4 +1,7 @@
import { type Component, type Ref, ref } from 'vue'
import { type Component, markRaw, type Ref, ref } from 'vue'
import type { ComponentProps } from 'vue-component-type-helpers'
import type { ButtonColor } from '#ui/components/base/buttons/types.ts'
import { createContext } from '.'
@@ -6,7 +9,7 @@ export interface PopupNotificationButton {
label: string
action: () => void | Promise<void>
icon?: Component
color?: 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'standard'
color?: ButtonColor | 'standard'
keepOpen?: boolean
}
@@ -35,7 +38,39 @@ export type PopupNotificationToastType =
| 'instance-download'
| 'instance-ready'
export interface PopupNotificationToast {
interface PopupNotificationBase {
id: string | number
autoCloseMs?: number | null
timer?: NodeJS.Timeout
}
export interface PopupNotificationCustom<
TComponent extends Component = Component,
> extends PopupNotificationBase {
contentType: 'custom'
component: TComponent
componentProps?: ComponentProps<TComponent>
}
export interface PopupNotificationStandard extends PopupNotificationBase {
contentType: 'standard'
title: string
type: 'error' | 'warning' | 'success' | 'info' | 'download'
titleLogo?: Component
text?: string
iconUrl?: string | null
hideIcon?: boolean
progress?: number
waiting?: boolean
progressItems?: PopupNotificationProgressItem[]
buttons?: PopupNotificationButton[]
dismissible?: boolean
onDismiss?: () => void | Promise<void>
}
export interface PopupNotificationToast extends PopupNotificationBase {
contentType: 'toast'
title: string
type: PopupNotificationToastType
actorName?: string | null
actorAvatarUrl?: string | null
@@ -56,25 +91,40 @@ export interface PopupNotificationToast {
onOpenInstance?: () => void | Promise<void>
}
export interface PopupNotification {
id: string | number
title: string
titleLogo?: Component
bodyComponent?: Component
bodyProps?: Record<string, unknown>
text?: string
iconUrl?: string | null
hideIcon?: boolean
type?: 'error' | 'warning' | 'success' | 'info' | 'download'
progress?: number
waiting?: boolean
progressItems?: PopupNotificationProgressItem[]
buttons?: PopupNotificationButton[]
toast?: PopupNotificationToast
dismissible?: boolean
onDismiss?: () => void | Promise<void>
autoCloseMs?: number | null
timer?: NodeJS.Timeout
export type PopupNotification =
| PopupNotificationCustom
| PopupNotificationStandard
| PopupNotificationToast
type PopupNotificationDownload =
| (PopupNotificationStandard & { type: 'download' })
| (PopupNotificationToast & { type: 'instance-download' })
type PopupNotificationInput = PopupNotification extends infer Notification
? Notification extends PopupNotification
? Omit<Notification, 'id' | 'timer'>
: never
: never
type PopupNotificationCustomInput<TComponent extends Component> = Omit<
PopupNotificationCustom<TComponent>,
'id' | 'timer'
>
type PopupNotificationNonCustomInput =
| Omit<PopupNotificationStandard, 'id' | 'timer'>
| Omit<PopupNotificationToast, 'id' | 'timer'>
type StoredPopupNotification<Input extends PopupNotificationInput> = Input &
Pick<PopupNotificationBase, 'id' | 'timer'>
interface AddPopupNotification {
<TComponent extends Component>(
notification: PopupNotificationCustomInput<TComponent>,
): StoredPopupNotification<PopupNotificationCustomInput<TComponent>>
<Input extends PopupNotificationNonCustomInput>(
notification: Input,
): StoredPopupNotification<Input>
}
export interface PopupNotificationDownloadState {
@@ -108,7 +158,7 @@ export abstract class AbstractPopupNotificationManager {
)
getVisibleDownloadProgressItems = (
notification: PopupNotification,
notification: PopupNotificationStandard,
): PopupNotificationProgressItem[] =>
(notification.progressItems ?? []).filter(
(progressItem) =>
@@ -121,17 +171,18 @@ export abstract class AbstractPopupNotificationManager {
protected abstract removeNotificationFromStorage(id: string | number): void
protected abstract clearAllNotificationsFromStorage(): void
addPopupNotification = (
notification: Omit<PopupNotification, 'id' | 'timer'>,
): PopupNotification => {
const newNotification: PopupNotification = {
addPopupNotification = ((notification: PopupNotificationInput) => {
const newNotification = {
...notification,
...(notification.contentType === 'custom'
? { component: markRaw(notification.component) }
: {}),
id: Date.now() + Math.random(),
}
} as StoredPopupNotification<PopupNotificationInput>
this.setNotificationTimer(newNotification)
this.addNotificationToStorage(newNotification)
return newNotification
}
}) as AddPopupNotification
removeNotification = (id: string | number): void => {
const notifications = this.getNotifications()
@@ -151,18 +202,18 @@ export abstract class AbstractPopupNotificationManager {
this.hiddenDownloadItemKeys.value.clear()
}
hideDownloadItem = (notificationId: string | number, progressItemId: string): void => {
hideDownloadItem = (notificationId: string | number, progressItemId?: string): void => {
const notification = this.getDownloadNotifications().find(
(candidate) => candidate.id === notificationId,
)
if (!notification?.progressItems?.some((item) => item.id === progressItemId)) return
if (!notification) return
this.hiddenDownloadItemKeys.value.add(this.getDownloadItemKey(notificationId, progressItemId))
if (
this.getDownloadItemKeys(notification).every((key) =>
this.hiddenDownloadItemKeys.value.has(key),
)
) {
const itemKey = this.getDownloadItemKey(notificationId, progressItemId)
const itemKeys = this.getDownloadItemKeys(notification)
if (!itemKeys.includes(itemKey)) return
this.hiddenDownloadItemKeys.value.add(itemKey)
if (itemKeys.every((key) => this.hiddenDownloadItemKeys.value.has(key))) {
this.clearNotificationTimer(notification)
}
}
@@ -212,11 +263,16 @@ export abstract class AbstractPopupNotificationManager {
}
}
private isDownloadNotification(notification: PopupNotification): boolean {
return notification.type === 'download' || notification.toast?.type === 'instance-download'
private isDownloadNotification(
notification: PopupNotification,
): notification is PopupNotificationDownload {
return (
(notification.contentType === 'standard' && notification.type === 'download') ||
(notification.contentType === 'toast' && notification.type === 'instance-download')
)
}
private getDownloadNotifications(): PopupNotification[] {
private getDownloadNotifications(): PopupNotificationDownload[] {
return this.getNotifications().filter((notification) =>
this.isDownloadNotification(notification),
)
@@ -224,7 +280,7 @@ export abstract class AbstractPopupNotificationManager {
private getDownloadItemKeys(notification: PopupNotification): string[] {
if (!this.isDownloadNotification(notification)) return []
if (notification.progressItems?.length) {
if (notification.contentType === 'standard' && notification.progressItems?.length) {
return notification.progressItems.map((progressItem) =>
this.getDownloadItemKey(notification.id, progressItem.id),
)
@@ -0,0 +1,95 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import InlineEditableText from '../../components/base/InlineEditableText.vue'
const meta = {
title: 'Base/InlineEditableText',
component: InlineEditableText,
render: (args) => ({
components: { InlineEditableText },
setup() {
const value = ref('Editable text')
return { args, value }
},
template: '<InlineEditableText v-model="value" v-bind="args" />',
}),
args: {
editLabel: 'Edit text',
},
} satisfies Meta<typeof InlineEditableText>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const IconActivated: Story = {
args: {
activationMode: 'icon',
iconTextClass: '!text-brand',
},
}
export const CustomClasses: Story = {
args: {
buttonClass: '!rounded-lg !bg-surface-3 !px-2',
inputClass: '!text-brand',
},
}
export const ConstrainedWidth: Story = {
args: {
maxWidth: '12rem',
},
render: (args) => ({
components: { InlineEditableText },
setup() {
const value = ref('A long value that truncates within its available width')
return { args, value }
},
template: '<InlineEditableText v-model="value" v-bind="args" />',
}),
}
export const Placeholder: Story = {
args: {
placeholder: 'Click to add a value',
},
render: (args) => ({
components: { InlineEditableText },
setup() {
const value = ref('')
return { args, value }
},
template: '<InlineEditableText v-model="value" v-bind="args" />',
}),
}
export const DefaultValue: Story = {
args: {
defaultValue: 'Default value',
},
render: (args) => ({
components: { InlineEditableText },
setup() {
const value = ref('')
return { args, value }
},
template: '<InlineEditableText v-model="value" v-bind="args" />',
}),
}
export const RejectedValue: Story = {
args: {
validate: (value: string) => value.toLowerCase() !== 'reserved',
},
render: (args) => ({
components: { InlineEditableText },
setup() {
const value = ref('Try changing this to "reserved"')
return { args, value }
},
template: '<InlineEditableText v-model="value" v-bind="args" />',
}),
}
@@ -19,6 +19,7 @@ export const Default: StoryObj = {
const showSuccess = () => {
popupManager.addPopupNotification({
contentType: 'standard',
title: 'Install complete',
text: 'Complex Gaming [Cobblemon] is installed and ready to play.',
type: 'success',
@@ -38,6 +39,7 @@ export const Default: StoryObj = {
const showError = () => {
popupManager.addPopupNotification({
contentType: 'standard',
title: 'Download failed',
text: 'Failed to download the modpack. Please try again.',
type: 'error',
@@ -53,6 +55,7 @@ export const Default: StoryObj = {
const showWarning = () => {
popupManager.addPopupNotification({
contentType: 'standard',
title: 'Update available',
text: "Modrinth App v2.1.0 is available now! Since you're on a metered network, we didn't automatically download it.",
type: 'warning',
@@ -73,6 +76,7 @@ export const Default: StoryObj = {
const showInfo = () => {
popupManager.addPopupNotification({
contentType: 'standard',
title: 'Download complete',
text: 'Modrinth App v2.1.0 has finished downloading. Reload to update now.',
type: 'info',
@@ -92,6 +96,7 @@ export const Default: StoryObj = {
const showNoButtons = () => {
popupManager.addPopupNotification({
contentType: 'standard',
title: 'Heads up',
text: 'This notification has no action buttons and will auto-close in 30 seconds.',
type: 'info',
@@ -100,6 +105,7 @@ export const Default: StoryObj = {
const showPermanent = () => {
popupManager.addPopupNotification({
contentType: 'standard',
title: 'Permanent notification',
text: 'This notification will stay open until manually dismissed.',
type: 'warning',
@@ -109,6 +115,7 @@ export const Default: StoryObj = {
const showBlocking = () => {
popupManager.addPopupNotification({
contentType: 'standard',
title: 'Your privacy and how ads support Modrinth',
text: 'Ads make Modrinth possible and fund creator rewards. Our partners may store unique identifiers to personalize ads and measure performance.',
type: 'info',
@@ -139,6 +146,7 @@ export const Default: StoryObj = {
const showWaitingProgress = () => {
popupManager.addPopupNotification({
contentType: 'standard',
title: 'Installing modpack...',
text: 'example-pack-1.0.0.mrpack',
type: 'info',
@@ -149,6 +157,7 @@ export const Default: StoryObj = {
const showDeterminateProgress = () => {
popupManager.addPopupNotification({
contentType: 'standard',
title: 'Downloading update',
text: 'Downloading files...',
type: 'success',
@@ -159,6 +168,7 @@ export const Default: StoryObj = {
const showGroupedDownloads = () => {
popupManager.addPopupNotification({
contentType: 'standard',
title: 'Downloads',
type: 'download',
autoCloseMs: null,
@@ -2,7 +2,10 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import { Button } from '../../components/base/buttons'
import type { CreationFlowContextValue } from '../../components/flows/creation-flow-modal/creation-flow-context'
import type {
CreationFlowContextValue,
ProjectInstallCreateData,
} from '../../components/flows/creation-flow-modal/creation-flow-context'
import CreationFlowModal from '../../components/flows/creation-flow-modal/index.vue'
const meta = {
@@ -102,7 +105,50 @@ export const Instance: Story = {
const onCreate = (config: CreationFlowContextValue) => {
lastEvent.value = `create emitted — loader: ${config.selectedLoader.value}, version: ${config.selectedGameVersion.value}`
}
return { modalRef, openModal, lastEvent, onCreate }
const prepareProjectInstall = async (projectId: string, projectType: string) => {
lastEvent.value = `project selected — ${projectId}`
if (projectType === 'modpack') return null
return {
projectId,
title: 'Fabric API',
iconUrl: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png',
link: '/project/fabric-api',
compatibleLoaders: ['fabric'],
gameVersions: ['1.21.5', '1.21.4'],
releaseGameVersions: new Set(['1.21.5', '1.21.4']),
}
}
const createProjectInstall = async (data: ProjectInstallCreateData) => {
lastEvent.value = `project instance created — ${data.name}`
}
const searchProjects = async () => ({
hits: [
{
project_id: 'fabric-api',
project_type: 'mod',
title: 'Fabric API',
icon_url: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png',
},
{
project_id: 'adrenaline',
project_type: 'modpack',
title: 'Adrenaline',
icon_url: 'https://cdn.modrinth.com/data/RPYx1T0D/icon.png',
},
],
offset: 0,
limit: 10,
total_hits: 2,
})
return {
modalRef,
openModal,
lastEvent,
onCreate,
prepareProjectInstall,
createProjectInstall,
searchProjects,
}
},
template: /*html*/ `
<div class="flex flex-col gap-4 items-center">
@@ -112,6 +158,9 @@ export const Instance: Story = {
ref="modalRef"
type="instance"
:show-snapshot-toggle="true"
:search-projects="searchProjects"
:prepare-project-install="prepareProjectInstall"
:create-project-install="createProjectInstall"
@hide="() => {}"
@browse-modpacks="() => console.log('browse-modpacks emitted')"
@create="onCreate"
@@ -8,9 +8,34 @@ const meta = {
parameters: {
layout: 'centered',
},
args: {
onClickNewServer: () => undefined,
onClickSignIn: () => undefined,
},
} satisfies Meta<typeof ServerListEmpty>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const SignedOut: Story = {
args: {
loggedIn: false,
},
}
export const LoggedIn: Story = {
args: {
loggedIn: true,
},
}
export const Narrow: Story = {
args: {
loggedIn: false,
},
parameters: {
viewport: {
defaultViewport: 'mobile2',
},
},
}