Merge branch 'main' into boris/dev-1126

This commit is contained in:
aecsocket
2026-08-04 14:21:03 +01:00
1037 changed files with 95730 additions and 24271 deletions
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

@@ -65,8 +65,7 @@ async function duplicateInstance(p) {
const handleRightClick = (event, instanceId) => {
const item = instanceComponents.value.find((x) => x.instance.id === instanceId)
const baseOptions = [
{ name: 'add_content' },
{ type: 'divider' },
...(item.instance.quarantined ? [] : [{ name: 'add_content' }, { type: 'divider' }]),
{ name: 'edit' },
{ name: 'duplicate' },
{ name: 'open' },
@@ -90,10 +89,14 @@ const handleRightClick = (event, instanceId) => {
...baseOptions,
]
: [
{
name: 'play',
color: 'primary',
},
...(item.instance.quarantined
? []
: [
{
name: 'play',
color: 'primary',
},
]),
...baseOptions,
],
)
@@ -73,8 +73,7 @@ async function duplicateInstance(p) {
const handleInstanceRightClick = async (event, passedInstance) => {
const baseOptions = [
{ name: 'add_content' },
{ type: 'divider' },
...(passedInstance.quarantined ? [] : [{ name: 'add_content' }, { type: 'divider' }]),
{ name: 'edit' },
{ name: 'duplicate' },
{ name: 'open_folder' },
@@ -98,10 +97,14 @@ const handleInstanceRightClick = async (event, passedInstance) => {
...baseOptions,
]
: [
{
name: 'play',
color: 'primary',
},
...(passedInstance.quarantined
? []
: [
{
name: 'play',
color: 'primary',
},
]),
...baseOptions,
]
@@ -2,7 +2,7 @@
<div
ref="outerRef"
data-tauri-drag-region
class="min-w-0 overflow-hidden pl-3"
class="min-w-0 overflow-hidden pl-4"
:class="{ 'breadcrumb-fade-mask': isOverflowing }"
:style="isOverflowing ? { '--scroll-distance': `-${overflowAmount}px` } : undefined"
@mouseenter="onMouseEnter"
@@ -11,30 +11,48 @@
<div
ref="innerRef"
data-tauri-drag-region
class="flex w-fit items-center gap-1"
class="flex w-fit items-center gap-2 pr-4"
:class="{ 'breadcrumbs-scroll': isAnimating }"
@animationiteration="onAnimationIteration"
>
{{ breadcrumbData.resetToNames(breadcrumbs) }}
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
<router-link
v-if="breadcrumb.link"
:to="{
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id as string)),
query: breadcrumb.query,
}"
class="shrink-0 whitespace-nowrap text-primary"
<template v-for="(breadcrumb, index) in breadcrumbs" :key="breadcrumb.slot">
<component
:is="index < breadcrumbs.length - 1 && breadcrumb.to ? RouterLink : 'span'"
v-bind="index < breadcrumbs.length - 1 && breadcrumb.to ? { to: breadcrumb.to } : {}"
:data-tauri-drag-region="index === breadcrumbs.length - 1 ? '' : undefined"
class="flex shrink-0 items-center gap-1.5 whitespace-nowrap text-base font-medium leading-6"
:class="
index === breadcrumbs.length - 1
? 'cursor-default select-none text-contrast'
: 'text-primary hover:text-contrast'
"
:aria-current="index === breadcrumbs.length - 1 ? 'page' : undefined"
>
{{ resolveLabel(breadcrumb.name) }}
</router-link>
<span
v-else
<Avatar
v-if="breadcrumb.visual?.type === 'image'"
:src="breadcrumb.visual.src"
:alt="breadcrumb.visual.alt ?? breadcrumb.label"
:circle="breadcrumb.visual.circle"
:tint-by="breadcrumb.visual.tintBy ?? breadcrumb.id"
size="20px"
no-shadow
raised
class="inline-block shrink-0 align-middle"
:class="{ '!rounded-md': !breadcrumb.visual.circle }"
/>
<component
:is="breadcrumb.visual.component"
v-else-if="breadcrumb.visual?.type === 'icon'"
class="size-5 shrink-0 text-primary"
aria-hidden="true"
/>
<span>{{ breadcrumb.label }}</span>
</component>
<ChevronRightIcon
v-if="index < breadcrumbs.length - 1"
data-tauri-drag-region
class="shrink-0 whitespace-nowrap text-contrast font-semibold cursor-default select-none"
>
{{ resolveLabel(breadcrumb.name) }}
</span>
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5 shrink-0" />
class="size-5 shrink-0 text-primary"
/>
</template>
</div>
</div>
@@ -42,34 +60,13 @@
<script setup lang="ts">
import { ChevronRightIcon } from '@modrinth/assets'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { Avatar } from '@modrinth/ui'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { RouterLink } from 'vue-router'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { injectBreadcrumbManager } from '@/providers/breadcrumbs'
interface Breadcrumb {
name: string
link?: string
query?: Record<string, string>
}
const route = useRoute()
const breadcrumbData = useBreadcrumbs()
const breadcrumbs = computed<Breadcrumb[]>(() => {
const additionalContext =
route.meta.useContext === true
? breadcrumbData.context
: route.meta.useRootContext === true
? breadcrumbData.rootContext
: null
const crumbs = (route.meta.breadcrumb ?? []) as Breadcrumb[]
return additionalContext ? [additionalContext as Breadcrumb, ...crumbs] : crumbs
})
function resolveLabel(name: string): string {
return name.charAt(0) === '?' ? breadcrumbData.getName(name.slice(1)) : name
}
const { entries: breadcrumbs } = injectBreadcrumbManager()
// Overflow detection
const outerRef = ref<HTMLDivElement | null>(null)
@@ -83,9 +80,13 @@ let stopping = false
function checkOverflow() {
if (!outerRef.value || !innerRef.value) return
const overflow = innerRef.value.scrollWidth - outerRef.value.clientWidth
const outerStyles = window.getComputedStyle(outerRef.value)
const horizontalPadding =
Number.parseFloat(outerStyles.paddingLeft) + Number.parseFloat(outerStyles.paddingRight)
const availableWidth = outerRef.value.clientWidth - horizontalPadding
const overflow = innerRef.value.scrollWidth - availableWidth
isOverflowing.value = overflow > 0
overflowAmount.value = overflow + 12
overflowAmount.value = overflow
}
function onMouseEnter() {
@@ -52,10 +52,6 @@ defineExpose({
if (errorVal.message.includes('because the target machine actively refused it')) {
metadata.value.hostsFile = true
}
} else if (errorVal.message && errorVal.message.includes('User is not logged in')) {
title.value = 'Sign in to Minecraft'
errorType.value = 'minecraft_sign_in'
supportLink.value = 'https://support.modrinth.com'
} else if (errorVal.message && errorVal.message.includes('Move directory error:')) {
title.value = 'Could not change app directory'
errorType.value = 'directory_move'
@@ -243,20 +239,6 @@ async function copyToClipboard(text) {
</button>
</div>
</template>
<div v-else-if="errorType === 'minecraft_sign_in'">
<p>
To play this instance, you must sign in through Microsoft below. If you don't have a
Minecraft account, you can purchase the game on the
<a href="https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc"
>Minecraft website</a
>.
</p>
<div class="cta-button">
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
<LogInIcon /> Sign in to Minecraft
</button>
</div>
</div>
<template v-else-if="errorType === 'state_init'">
<p>
Modrinth App failed to load correctly. This may be because of a corrupted file, or
@@ -66,6 +66,7 @@ const checkProcess = async () => {
const play = async (e, context) => {
e?.stopPropagation()
if (props.instance.quarantined) return
loading.value = true
await run(props.instance.id)
.catch((err) => handleSevereError(err, { instanceId: props.instance.id }))
@@ -94,6 +95,7 @@ const stop = async (e, context) => {
const repair = async (e) => {
e?.stopPropagation()
if (props.instance.quarantined) return
if (
props.instance.install_stage !== 'pack_installed' &&
@@ -116,6 +118,7 @@ const openFolder = async () => {
}
const addContent = async () => {
if (props.instance.quarantined) return
await router.push({
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: props.instance.id },
@@ -142,7 +145,9 @@ const unlisten = await process_listener((e) => {
}
})
onMounted(() => checkProcess())
onMounted(() => {
checkProcess()
})
onUnmounted(() => unlisten())
</script>
@@ -173,7 +178,11 @@ onUnmounted(() => unlisten())
<SpinnerIcon class="animate-spin" />
</button>
</ButtonStyled>
<ButtonStyled v-else :color="first ? 'brand' : 'standard'" circular>
<ButtonStyled
v-else-if="!instance.quarantined"
:color="first ? 'brand' : 'standard'"
circular
>
<button
v-tooltip="'Play'"
@click="(e) => play(e, 'InstanceCard')"
@@ -227,7 +236,12 @@ onUnmounted(() => unlisten())
class="animate-spin w-8 h-8"
tabindex="-1"
/>
<ButtonStyled v-else-if="!installed" size="large" color="brand" circular>
<ButtonStyled
v-else-if="!installed && !instance.quarantined"
size="large"
color="brand"
circular
>
<button
v-tooltip="'Repair'"
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
@@ -236,7 +250,7 @@ onUnmounted(() => unlisten())
<DownloadIcon />
</button>
</ButtonStyled>
<ButtonStyled v-else size="large" color="brand" circular>
<ButtonStyled v-else-if="!instance.quarantined" size="large" color="brand" circular>
<button
v-tooltip="'Play'"
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
@@ -1,5 +1,9 @@
<template>
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
<ModalWrapper
ref="detectJavaModal"
:header="formatMessage(messages.title)"
:show-ad-on-close="false"
>
<div class="flex flex-col gap-4">
<Table :columns="javaInstallColumns" :data="chosenInstallOptions" row-key="path">
<template #cell-version="{ value }">
@@ -11,15 +15,23 @@
<template #cell-actions="{ row }">
<div class="flex items-center justify-end">
<ButtonStyled v-if="currentSelected.path === row.path">
<button class="!shadow-none" disabled><CheckIcon /> Selected</button>
<button class="!shadow-none" disabled>
<CheckIcon aria-hidden="true" />
{{ formatMessage(messages.selected) }}
</button>
</ButtonStyled>
<ButtonStyled v-else>
<button class="!shadow-none" @click="setJavaInstall(row)"><PlusIcon /> Select</button>
<button class="!shadow-none" @click="setJavaInstall(row)">
<PlusIcon aria-hidden="true" />
{{ formatMessage(messages.select) }}
</button>
</ButtonStyled>
</div>
</template>
<template #empty-state>
<div class="p-4 text-secondary">No java installations found!</div>
<div class="p-4 text-secondary">
{{ formatMessage(messages.noInstallationsFound) }}
</div>
</template>
</Table>
<div class="flex justify-end">
@@ -28,8 +40,8 @@
class="!shadow-none !border-surface-4 !border"
@click="$refs.detectJavaModal.hide()"
>
<XIcon />
Cancel
<XIcon aria-hidden="true" />
{{ formatMessage(messages.cancel) }}
</button>
</ButtonStyled>
</div>
@@ -38,23 +50,70 @@
</template>
<script setup>
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, Table } from '@modrinth/ui'
import { ref } from 'vue'
import {
ButtonStyled,
defineMessages,
injectNotificationManager,
Table,
useVIntl,
} from '@modrinth/ui'
import { computed, ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { find_filtered_jres } from '@/helpers/jre.js'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const messages = defineMessages({
title: {
id: 'app.java-detection.title',
defaultMessage: 'Select Java installation',
},
versionColumn: {
id: 'app.java-detection.columns.version',
defaultMessage: 'Version',
},
pathColumn: {
id: 'app.java-detection.columns.path',
defaultMessage: 'Path',
},
actionsColumn: {
id: 'app.java-detection.columns.actions',
defaultMessage: 'Actions',
},
selected: {
id: 'app.java-detection.selected',
defaultMessage: 'Selected',
},
select: {
id: 'app.java-detection.select',
defaultMessage: 'Select',
},
noInstallationsFound: {
id: 'app.java-detection.no-installations-found',
defaultMessage: 'No Java installations found.',
},
cancel: {
id: 'app.java-detection.cancel',
defaultMessage: 'Cancel',
},
})
const chosenInstallOptions = ref([])
const detectJavaModal = ref(null)
const currentSelected = ref({})
const javaInstallColumns = [
{ key: 'version', label: 'Version', width: '9rem' },
{ key: 'path', label: 'Path' },
{ key: 'actions', label: 'Actions', align: 'right', width: '10rem' },
]
const javaInstallColumns = computed(() => [
{ key: 'version', label: formatMessage(messages.versionColumn), width: '9rem' },
{ key: 'path', label: formatMessage(messages.pathColumn) },
{
key: 'actions',
label: formatMessage(messages.actionsColumn),
align: 'right',
width: '10rem',
},
])
defineExpose({
show: async (version, currentSelectedJava) => {
@@ -6,7 +6,7 @@
autocomplete="off"
:disabled="props.disabled"
:model-value="props.modelValue ? props.modelValue.path : ''"
:placeholder="placeholder ?? '/path/to/java'"
:placeholder="placeholder ?? formatMessage(messages.pathPlaceholder)"
wrapper-class="installation-input"
@update:model-value="
(val) => {
@@ -28,6 +28,7 @@
color-fill="text"
>
<button
:aria-label="formatMessage(messages.testJavaInstallation)"
class="!shadow-none"
:disabled="testingJava || props.disabled"
@click="runTest(props.modelValue?.path)"
@@ -47,25 +48,31 @@
<span class="installation-buttons">
<ButtonStyled v-if="props.version">
<button
v-tooltip="testingJavaSuccess === true ? 'Already installed' : undefined"
v-tooltip="
testingJavaSuccess === true ? formatMessage(messages.alreadyInstalled) : undefined
"
class="!shadow-none"
:disabled="props.disabled || installingJava || testingJavaSuccess === true"
@click="reinstallJava"
>
<DownloadIcon />
{{ installingJava ? 'Installing...' : 'Install recommended' }}
{{
installingJava
? formatMessage(messages.installing)
: formatMessage(messages.installRecommended)
}}
</button>
</ButtonStyled>
<ButtonStyled>
<button class="!shadow-none" :disabled="props.disabled" @click="autoDetect">
<SearchIcon />
Detect
{{ formatMessage(messages.detect) }}
</button>
</ButtonStyled>
<ButtonStyled>
<button class="!shadow-none" :disabled="props.disabled" @click="handleJavaFileInput()">
<FolderSearchIcon />
Browse
{{ formatMessage(messages.browse) }}
</button>
</ButtonStyled>
</span>
@@ -82,7 +89,13 @@ import {
SpinnerIcon,
XCircleIcon,
} from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, StyledInput } from '@modrinth/ui'
import {
ButtonStyled,
defineMessages,
injectNotificationManager,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref, watch } from 'vue'
@@ -92,6 +105,38 @@ import { trackEvent } from '@/helpers/analytics'
import { auto_install_java, find_filtered_jres, get_jre } from '@/helpers/jre.js'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const messages = defineMessages({
pathPlaceholder: {
id: 'app.java-selector.path.placeholder',
defaultMessage: '/path/to/java',
},
testJavaInstallation: {
id: 'app.java-selector.test-installation',
defaultMessage: 'Test Java installation',
},
alreadyInstalled: {
id: 'app.java-selector.already-installed',
defaultMessage: 'Already installed',
},
installing: {
id: 'app.java-selector.installing',
defaultMessage: 'Installing...',
},
installRecommended: {
id: 'app.java-selector.install-recommended',
defaultMessage: 'Install recommended',
},
detect: {
id: 'app.java-selector.detect',
defaultMessage: 'Detect',
},
browse: {
id: 'app.java-selector.browse',
defaultMessage: 'Browse',
},
})
const props = defineProps({
id: {
@@ -1,41 +1,151 @@
<script setup>
import { SpinnerIcon } from '@modrinth/assets'
import { Avatar, injectNotificationManager } from '@modrinth/ui'
import { Avatar, defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import dayjs from 'dayjs'
import { onUnmounted, ref } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import NavButton from '@/components/ui/NavButton.vue'
import { instance_listener } from '@/helpers/events.js'
import { list } from '@/helpers/instance'
const ITEM_SIZE = 52
const APPROX_USED_VERTICAL_SPACE = 513 // doesn't need to be exact lol just close enough so there's a little gap and no overflow
const STORAGE_KEY = 'modrinth-quick-instance-count'
const { handleError } = injectNotificationManager()
const queryClient = useQueryClient()
const { formatMessage } = useVIntl()
const maxAuto = ref(0)
const allInstances = ref([])
const dragging = ref(false)
const stored = localStorage.getItem(STORAGE_KEY)
const userLimit = ref(stored === null ? null : Number(stored))
const maxVisible = computed(() => Math.min(maxAuto.value, allInstances.value.length))
const visibleCount = computed(() => Math.min(userLimit.value ?? maxVisible.value, maxVisible.value))
const recentInstances = computed(() => allInstances.value.slice(0, visibleCount.value))
const canDrag = computed(() => maxVisible.value > 0)
const showOverdrag = ref(false)
const updateMaxAuto = () => {
maxAuto.value = Math.max(
0,
Math.floor((window.innerHeight - APPROX_USED_VERTICAL_SPACE) / ITEM_SIZE),
)
}
const setLimit = (count) => {
const clamped = Math.max(0, Math.min(count, maxVisible.value))
if (clamped >= maxVisible.value) {
userLimit.value = null
localStorage.removeItem(STORAGE_KEY)
} else {
userLimit.value = clamped
localStorage.setItem(STORAGE_KEY, String(clamped))
}
}
let dragStartY = 0
let dragStartCount = 0
let wasOverdragging = false
let overdragTimeout = null
const clearOverdragFlash = () => {
showOverdrag.value = false
if (overdragTimeout !== null) {
clearTimeout(overdragTimeout)
overdragTimeout = null
}
}
const flashOverdrag = () => {
showOverdrag.value = true
if (overdragTimeout !== null) {
clearTimeout(overdragTimeout)
}
overdragTimeout = setTimeout(() => {
showOverdrag.value = false
overdragTimeout = null
}, 500)
}
const onDividerPointerDown = (event) => {
if (!canDrag.value) {
return
}
event.preventDefault()
dragging.value = true
wasOverdragging = false
clearOverdragFlash()
dragStartY = event.clientY
dragStartCount = visibleCount.value
document.body.classList.add('quick-instance-dragging')
event.currentTarget.setPointerCapture(event.pointerId)
}
const onDividerPointerMove = (event) => {
if (!dragging.value) {
return
}
const delta = event.clientY - dragStartY
const target = dragStartCount + Math.round(delta / ITEM_SIZE)
const isOverdragging = target < 0 || target > maxAuto.value
if (isOverdragging && !wasOverdragging) {
flashOverdrag()
}
wasOverdragging = isOverdragging
setLimit(target)
}
const endDrag = (event) => {
if (!dragging.value) {
return
}
dragging.value = false
wasOverdragging = false
clearOverdragFlash()
document.body.classList.remove('quick-instance-dragging')
if (event?.currentTarget?.hasPointerCapture?.(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId)
}
}
const onDividerPointerUp = (event) => {
endDrag(event)
}
const recentInstances = ref([])
const getInstances = async () => {
const instances = await list().catch(handleError)
recentInstances.value = instances
.sort((a, b) => {
const dateACreated = dayjs(a.created)
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
for (const instance of instances) {
queryClient.setQueryData(['instances', 'summary', instance.id], instance)
}
const dateBCreated = dayjs(b.created)
const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0)
allInstances.value = instances.sort((a, b) => {
const dateACreated = dayjs(a.created)
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed
const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed
const dateBCreated = dayjs(b.created)
const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0)
if (dateA.isSame(dateB)) {
return a.name.localeCompare(b.name)
}
const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed
const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed
return dateB - dateA
})
.slice(0, 3)
if (dateA.isSame(dateB)) {
return a.name.localeCompare(b.name)
}
return dateB - dateA
})
}
await getInstances()
updateMaxAuto()
const unlistenInstance = await instance_listener(async (event) => {
if (event.event !== 'synced') {
@@ -43,29 +153,157 @@ const unlistenInstance = await instance_listener(async (event) => {
}
})
onMounted(() => {
window.addEventListener('resize', updateMaxAuto)
})
onUnmounted(() => {
window.removeEventListener('resize', updateMaxAuto)
document.body.classList.remove('quick-instance-dragging')
clearOverdragFlash()
unlistenInstance()
})
const messages = defineMessages({
dragTooltip: {
id: 'app.quick-instance-switcher.drag-tooltip',
defaultMessage: 'Drag to resize',
},
dragShowTooltip: {
id: 'app.quick-instance-switcher.drag-show-tooltip',
defaultMessage: 'Drag to show recent instances',
},
})
const dividerTooltip = computed(() => {
if (!canDrag.value || dragging.value) {
return null
}
return formatMessage(visibleCount.value === 0 ? messages.dragShowTooltip : messages.dragTooltip)
})
</script>
<template>
<div v-for="instance in recentInstances" :key="instance.id" v-tooltip.right="instance.name">
<NavButton :to="`/instance/${encodeURIComponent(instance.id)}`" class="relative">
<Avatar
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
size="28px"
:tint-by="instance.id"
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
/>
<div
v-if="instance.install_stage !== 'installed'"
class="absolute inset-0 flex items-center justify-center z-10 pointer-events-none"
>
<SpinnerIcon class="animate-spin w-4 h-4" />
</div>
</NavButton>
<Transition name="top-divider">
<div
v-if="recentInstances.length > 0"
class="top-divider flex items-center justify-center overflow-hidden"
>
<div class="h-px w-8 bg-surface-5 shrink-0"></div>
</div>
</Transition>
<TransitionGroup name="quick-instance" tag="div" class="flex flex-col items-center">
<div
v-for="instance in recentInstances"
:key="instance.id"
v-tooltip.right="instance.name"
class="quick-instance-item"
>
<NavButton :to="`/instance/${encodeURIComponent(instance.id)}`" class="relative">
<Avatar
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
size="28px"
:tint-by="instance.id"
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
/>
<div
v-if="instance.install_stage !== 'installed'"
class="absolute inset-0 flex items-center justify-center z-10 pointer-events-none"
>
<SpinnerIcon class="animate-spin w-4 h-4" />
</div>
</NavButton>
</div>
</TransitionGroup>
<div
v-tooltip.right="dividerTooltip"
class="flex items-center justify-center py-2 select-none"
:class="canDrag ? 'cursor-ns-resize touch-none group' : ''"
@pointerdown="onDividerPointerDown"
@pointermove="onDividerPointerMove"
@pointerup="onDividerPointerUp"
@pointercancel="onDividerPointerUp"
>
<div
class="h-px w-8 transition-colors duration-200"
:class="
showOverdrag ? 'bg-red' : canDrag ? 'bg-surface-5 group-hover:bg-secondary' : 'bg-surface-5'
"
></div>
</div>
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-divider"></div>
</template>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.top-divider {
height: calc(1rem + 1px);
}
.top-divider-enter-active,
.top-divider-leave-active {
transition:
opacity 0.25s ease,
height 0.25s ease;
}
.top-divider-enter-from,
.top-divider-leave-to {
opacity: 0;
height: 0;
}
.quick-instance-item {
height: 3rem;
overflow: hidden;
& + & {
margin-top: 0.25rem;
}
}
.quick-instance-enter-active,
.quick-instance-leave-active {
transition:
opacity 0.25s ease,
transform 0.25s ease,
height 0.25s ease,
margin-top 0.25s ease;
}
.quick-instance-enter-from,
.quick-instance-leave-to {
opacity: 0;
transform: scale(0.5);
height: 0;
margin-top: 0 !important;
}
@media (prefers-reduced-motion: reduce) {
.top-divider-enter-active,
.top-divider-leave-active,
.quick-instance-enter-active,
.quick-instance-leave-active {
transition: none;
}
.top-divider-enter-from,
.top-divider-leave-to {
opacity: 1;
height: calc(1rem + 1px);
}
.quick-instance-enter-from,
.quick-instance-leave-to {
opacity: 1;
transform: none;
height: 3rem;
margin-top: unset !important;
}
}
</style>
<style lang="scss">
body.quick-instance-dragging,
body.quick-instance-dragging * {
cursor: ns-resize !important;
}
</style>
@@ -0,0 +1,233 @@
<script setup lang="ts">
import { NotepadTextIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { type } from '@tauri-apps/plugin-os'
import { $fetch } from 'ofetch'
import { onMounted, ref } from 'vue'
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
import { list } from '@/helpers/instance'
import { get as getCreds } from '@/helpers/mr_auth.ts'
type Survey = {
id: string
tally_id: string
type: string
condition?: string
assigned_users?: string[]
dismissed_users?: string[]
}
type TallyApi = {
openPopup: (formId: string, options: object) => void
}
const tallyWindow = window as Window & { Tally?: TallyApi }
const { formatMessage } = useVIntl()
const { handleError } = injectNotificationManager()
const availableSurvey = ref<Survey | null>(null)
const messages = defineMessages({
surveyTitle: {
id: 'app.survey.title',
defaultMessage: 'Hey there Modrinth user!',
},
surveyBody: {
id: 'app.survey.body',
defaultMessage:
'Would you mind answering a few questions about your experience with Modrinth App?',
},
surveyFooter: {
id: 'app.survey.footer',
defaultMessage:
'This feedback will go directly to the Modrinth team and help guide future updates!',
},
takeSurvey: {
id: 'app.survey.take-survey',
defaultMessage: 'Take survey',
},
surveyNoThanks: {
id: 'app.survey.no-thanks',
defaultMessage: 'No thanks',
},
})
function cleanupOldSurveyDisplayData() {
const threeWeeksAgo = new Date()
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21)
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key?.startsWith('survey-') && key.endsWith('-display')) {
const dateValue = new Date(localStorage.getItem(key) ?? '')
if (dateValue < threeWeeksAgo) {
localStorage.removeItem(key)
}
}
}
}
async function openSurvey() {
if (!availableSurvey.value) {
console.error('No survey to open')
return
}
const creds = await getCreds().catch(handleError)
const userId = creds?.user_id
const formId = availableSurvey.value.tally_id
const popupOptions = {
layout: 'modal',
width: 700,
autoClose: 2000,
hideTitle: true,
hiddenFields: {
user_id: userId,
},
onOpen: () => console.info('Opened user survey'),
onClose: () => {
console.info('Closed user survey')
show_ads_window()
},
onSubmit: () => console.info('Active user survey submitted'),
}
try {
hide_ads_window()
if (tallyWindow.Tally?.openPopup) {
console.info(`Opening Tally popup for user survey (form ID: ${formId})`)
dismissSurvey()
tallyWindow.Tally.openPopup(formId, popupOptions)
} else {
console.warn('Tally script not yet loaded')
show_ads_window()
}
} catch (e) {
console.error('Error opening Tally popup:', e)
show_ads_window()
}
console.info(`Found user survey to show with tally_id: ${formId}`)
tallyWindow.Tally?.openPopup(formId, popupOptions)
}
function dismissSurvey() {
if (!availableSurvey.value) return
localStorage.setItem(`survey-${availableSurvey.value.id}-display`, String(new Date()))
availableSurvey.value = null
}
async function processPendingSurveys() {
function isWithinLastTwoWeeks(date: string | Date | null | undefined) {
if (!date) return false
const twoWeeksAgo = new Date()
twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14)
return new Date(date) >= twoWeeksAgo
}
cleanupOldSurveyDisplayData()
const creds = await getCreds().catch(handleError)
const userId = creds?.user_id
const instances = (await list().catch(handleError)) ?? []
const isActivePlayer = instances.some(
(instance) =>
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
)
let surveys: Survey[] = []
try {
surveys = await $fetch('https://api.modrinth.com/v2/surveys')
} catch (e) {
console.error('Error fetching surveys:', e)
}
const surveyToShow = surveys.find(
(survey) =>
!!(
localStorage.getItem(`survey-${survey.id}-display`) === null &&
survey.type === 'tally_app' &&
((survey.condition === 'active_player' && isActivePlayer) ||
(!!userId &&
survey.assigned_users?.includes(userId) &&
!survey.dismissed_users?.includes(userId)))
),
)
if (surveyToShow) {
availableSurvey.value = surveyToShow
} else {
console.info('No user survey to show')
}
}
onMounted(async () => {
const osType = await type()
if (osType === 'windows') {
await processPendingSurveys()
} else {
console.info('Skipping user surveys on non-Windows platforms')
}
})
</script>
<template>
<transition name="popup-survey">
<div
v-if="availableSurvey"
class="w-[400px] z-20 fixed -bottom-12 pb-16 right-[--right-bar-width] mr-4 rounded-t-2xl card-shadow bg-bg-raised border-surface-5 border-[1px] border-solid border-b-0 p-4"
>
<h2 class="text-lg font-extrabold mt-0 mb-2">
{{ formatMessage(messages.surveyTitle) }}
</h2>
<p class="m-0 leading-tight">
{{ formatMessage(messages.surveyBody) }}
</p>
<p class="mt-3 mb-4 leading-tight">
{{ formatMessage(messages.surveyFooter) }}
</p>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button @click="openSurvey">
<NotepadTextIcon />
{{ formatMessage(messages.takeSurvey) }}
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="dismissSurvey">
<XIcon />
{{ formatMessage(messages.surveyNoThanks) }}
</button>
</ButtonStyled>
</div>
</div>
</transition>
</template>
<style scoped>
.popup-survey-enter-active {
transition:
opacity 0.25s ease,
transform 0.25s cubic-bezier(0.51, 1.08, 0.35, 1.15);
transform-origin: top center;
}
.popup-survey-leave-active {
transition:
opacity 0.25s ease,
transform 0.25s cubic-bezier(0.68, -0.17, 0.23, 0.11);
transform-origin: top center;
}
.popup-survey-enter-from,
.popup-survey-leave-to {
opacity: 0;
transform: translateY(10rem) scale(0.8) scaleY(1.6);
}
</style>
@@ -10,61 +10,65 @@ import {
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { computed, onUnmounted, ref, watch } from 'vue'
import { computed, ref } from 'vue'
import FriendsSection from '@/components/ui/friends/FriendsSection.vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { friend_listener } from '@/helpers/events'
import {
add_friend,
friends,
type FriendWithUserData,
remove_friend,
transformFriends,
} from '@/helpers/friends.ts'
import { useFriends } from '@/composables/use-friends'
import type { FriendWithUserData } from '@/helpers/friends.ts'
import type { ModrinthCredentials } from '@/helpers/mr_auth'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
import { useTheming } from '@/store/state'
const { formatMessage } = useVIntl()
const { handleError } = injectNotificationManager()
const formatRelativeTime = useRelativeTime()
const themeStore = useTheming()
const props = defineProps<{
credentials: ModrinthCredentials | null
signIn: () => void
}>()
type FriendsSectionCollapsedFlag =
| 'friends_active_collapsed'
| 'friends_online_collapsed'
| 'friends_offline_collapsed'
| 'friends_pending_collapsed'
function isFriendsSectionCollapsed(flag: FriendsSectionCollapsedFlag) {
return themeStore.getFeatureFlag(flag)
}
function setFriendsSectionCollapsed(flag: FriendsSectionCollapsedFlag, collapsed: boolean) {
themeStore.featureFlags[flag] = collapsed
getSettings()
.then((settings) => {
settings.feature_flags[flag] = collapsed
return setSettings(settings)
})
.catch(handleError)
}
const userCredentials = computed(() => props.credentials)
const {
friends: userFriends,
loading,
requestFriend,
acceptFriend,
removeFriend: removeFriendRecord,
} = useFriends({
currentUserId: () => userCredentials.value?.user_id,
getCredentials: () => userCredentials.value,
onError: handleError,
})
const search = ref('')
const friendInvitesModal = ref()
const username = ref('')
const addFriendModal = ref()
async function addFriendFromModal() {
addFriendModal.value.hide()
await add_friend(username.value).catch(handleError)
username.value = ''
await loadFriends()
}
async function addFriend(friend: FriendWithUserData) {
const id = friend.id === userCredentials.value?.user_id ? friend.friend_id : friend.id
if (id) {
await add_friend(id).catch(handleError)
await loadFriends()
}
}
async function removeFriend(friend: FriendWithUserData) {
const id = friend.id === userCredentials.value?.user_id ? friend.friend_id : friend.id
if (id) {
await remove_friend(id).catch(handleError)
await loadFriends()
}
}
const userFriends = ref<FriendWithUserData[]>([])
const sortedFriends = computed<FriendWithUserData[]>(() =>
userFriends.value.slice().sort((a, b) => {
if (a.last_updated === null && b.last_updated === null) {
@@ -108,42 +112,22 @@ const incomingRequests = computed(() =>
.sort((a, b) => b.created.diff(a.created)),
)
const loading = ref(true)
async function loadFriends(timeout = false) {
loading.value = timeout
function addFriendFromModal() {
const target = username.value.trim()
if (!target) return
try {
const friendsList = await friends()
userFriends.value = await transformFriends(friendsList, userCredentials.value)
loading.value = false
} catch (e) {
console.error('Error loading friends', e)
if (timeout) {
setTimeout(() => loadFriends(), 15 * 1000)
}
}
addFriendModal.value.hide()
requestFriend({ id: target, username: target })
username.value = ''
}
watch(
userCredentials,
() => {
if (userCredentials.value === undefined) {
userFriends.value = []
loading.value = false
} else if (userCredentials.value === null) {
userFriends.value = []
loading.value = false
} else {
loadFriends(true)
}
},
{ immediate: true },
)
function addFriend(friend: FriendWithUserData) {
acceptFriend(friend)
}
const unlisten = await friend_listener(() => loadFriends())
onUnmounted(() => {
unlisten()
})
function removeFriend(friend: FriendWithUserData) {
removeFriendRecord(friend)
}
const messages = defineMessages({
addFriend: {
@@ -370,33 +354,42 @@ const messages = defineMessages({
<FriendsSection
v-if="activeFriends.length > 0"
:is-searching="!!search"
open-by-default
:open-by-default="!isFriendsSectionCollapsed('friends_active_collapsed')"
:friends="activeFriends"
:heading="formatMessage(messages.active)"
:remove-friend="removeFriend"
@on-open="setFriendsSectionCollapsed('friends_active_collapsed', false)"
@on-close="setFriendsSectionCollapsed('friends_active_collapsed', true)"
/>
<FriendsSection
v-if="onlineFriends.length > 0"
:is-searching="!!search"
open-by-default
:open-by-default="!isFriendsSectionCollapsed('friends_online_collapsed')"
:friends="onlineFriends"
:heading="formatMessage(messages.online)"
:remove-friend="removeFriend"
@on-open="setFriendsSectionCollapsed('friends_online_collapsed', false)"
@on-close="setFriendsSectionCollapsed('friends_online_collapsed', true)"
/>
<FriendsSection
v-if="offlineFriends.length > 0"
:is-searching="!!search"
:open-by-default="activeFriends.length + onlineFriends.length < 3"
:open-by-default="!isFriendsSectionCollapsed('friends_offline_collapsed')"
:friends="offlineFriends"
:heading="formatMessage(messages.offline)"
:remove-friend="removeFriend"
@on-open="setFriendsSectionCollapsed('friends_offline_collapsed', false)"
@on-close="setFriendsSectionCollapsed('friends_offline_collapsed', true)"
/>
<FriendsSection
v-if="pendingFriends.length > 0"
:is-searching="!!search"
:open-by-default="!isFriendsSectionCollapsed('friends_pending_collapsed')"
:friends="pendingFriends"
:heading="formatMessage(messages.pending)"
:remove-friend="removeFriend"
@on-open="setFriendsSectionCollapsed('friends_pending_collapsed', false)"
@on-close="setFriendsSectionCollapsed('friends_pending_collapsed', true)"
/>
<p v-if="filteredFriends.length === 0 && search" class="text-sm text-secondary my-1 mx-4">
{{ formatMessage(messages.noFriendsMatch, { query: search }) }}
@@ -8,13 +8,14 @@ import {
OverflowMenu,
useVIntl,
} from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import { useTemplateRef } from 'vue'
import { useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import type { FriendWithUserData } from '@/helpers/friends.ts'
const { formatMessage } = useVIntl()
const router = useRouter()
const props = withDefaults(
defineProps<{
@@ -30,6 +31,11 @@ const props = withDefaults(
},
)
const emit = defineEmits<{
onOpen: []
onClose: []
}>()
function createContextMenuOptions(friend: FriendWithUserData) {
if (friend.accepted) {
return [
@@ -54,7 +60,7 @@ function createContextMenuOptions(friend: FriendWithUserData) {
}
function openProfile(username: string) {
openUrl('https://modrinth.com/user/' + username)
void router.push(`/user/${encodeURIComponent(username)}`)
}
const friendOptions = useTemplateRef('friendOptions')
@@ -111,6 +117,8 @@ const messages = defineMessages({
? ''
: ' cursor-pointer hover:brightness-[--hover-brightness] active:scale-[0.98] transition-all')
"
@on-open="emit('onOpen')"
@on-close="emit('onClose')"
>
<template #title>
<h3 class="text-base text-primary font-medium m-0">
@@ -122,45 +130,46 @@ const messages = defineMessages({
<div
v-for="friend in friends"
:key="friend.username"
class="group grid items-center grid-cols-[auto_1fr_auto] gap-2 hover:bg-button-bg transition-colors rounded-full mr-1"
class="group grid items-center grid-cols-[1fr_auto] gap-2 hover:bg-button-bg transition-colors rounded-full mr-1"
@contextmenu.prevent.stop="
(event) => friendOptions?.showMenu(event, friend, createContextMenuOptions(friend))
"
>
<div class="relative">
<Avatar
:src="friend.avatar"
:class="{ grayscale: !friend.online && friend.accepted }"
class="w-12 h-12 rounded-full"
size="32px"
circle
/>
<span
v-if="friend.online"
aria-hidden="true"
class="bottom-[2px] right-[-2px] absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
/>
</div>
<div class="flex flex-col">
<span
class="text-sm m-0"
:class="friend.online || !friend.accepted ? 'text-contrast' : 'text-primary'"
>
{{ friend.username }}
</span>
<span v-if="!friend.accepted" class="m-0 text-xs">
{{ formatMessage(messages.friendRequestSent) }}
</span>
<span v-else-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
</div>
<RouterLink
:to="`/user/${encodeURIComponent(friend.username)}`"
class="grid min-w-0 grid-cols-[auto_1fr] items-center gap-2 text-inherit no-underline"
>
<div class="relative">
<Avatar
:src="friend.avatar"
:class="{ grayscale: !friend.online && friend.accepted }"
class="w-12 h-12 rounded-full"
size="32px"
circle
/>
<span
v-if="friend.online"
aria-hidden="true"
class="bottom-[2px] right-[-2px] absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
/>
</div>
<div class="flex flex-col">
<span
class="text-sm m-0"
:class="friend.online || !friend.accepted ? 'text-contrast' : 'text-primary'"
>
{{ friend.username }}
</span>
<span v-if="!friend.accepted" class="m-0 text-xs">
{{ formatMessage(messages.friendRequestSent) }}
</span>
<span v-else-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
</div>
</RouterLink>
<ButtonStyled v-if="friend.accepted" circular type="transparent">
<OverflowMenu
class="opacity-0 group-hover:opacity-100 transition-opacity"
:options="[
{
id: 'view-profile',
action: () => openProfile(friend.username),
},
{
id: 'remove-friend',
action: () => removeFriend(friend),
@@ -169,10 +178,6 @@ const messages = defineMessages({
]"
>
<MoreVerticalIcon />
<template #view-profile>
<UserIcon />
{{ formatMessage(messages.viewProfile) }}
</template>
<template #remove-friend>
<TrashIcon />
{{ formatMessage(messages.removeFriend) }}
@@ -0,0 +1,409 @@
<template>
<PageHeader :title="instance.name">
<template #leading>
<Avatar :src="iconSrc" :alt="instance.name" size="64px" :tint-by="instance.id" />
</template>
<template v-if="instance.shared_instance || instance.quarantined" #badges>
<PageHeaderBadgeItem
v-if="instance.quarantined"
:icon="LockIcon"
aria-label="Locked instance information"
class="!border-orange !bg-highlight-orange !text-orange"
>
Locked
</PageHeaderBadgeItem>
<PageHeaderBadgeItem
v-else
:tooltip="sharedInstanceTooltip"
aria-label="Shared instance information"
class="!border-blue !bg-highlight-blue !text-blue"
>
Shared
<UnknownIcon class="block size-4 shrink-0 text-current" aria-hidden="true" />
</PageHeaderBadgeItem>
</template>
<template #metadata>
<div v-if="isServerInstance" class="flex flex-wrap items-center gap-2">
<InstanceHeaderServerMetadata
:loading-server-ping="loadingServerPing"
:players-online="playersOnline"
:status-online="statusOnline"
:recent-plays="recentPlays"
:ping="ping"
:minecraft-server="minecraftServer"
:linked-project-v3="linkedProjectV3"
:instance-id="instance.id"
/>
<PageHeaderMetadataItem v-if="sharedInstanceManager" :action="sharedInstanceManagerAction">
{{ sharedInstanceManagerLabel }}
<Avatar
:src="sharedInstanceManager.avatarUrl"
:alt="sharedInstanceManager.name"
:tint-by="sharedInstanceManager.tintBy"
size="24px"
:circle="sharedInstanceManager.type === 'user'"
no-shadow
/>
<span class="min-w-0 truncate">{{ sharedInstanceManager.name }}</span>
</PageHeaderMetadataItem>
</div>
<PageHeaderMetadata v-else>
<PageHeaderMetadataItem :icon="Gamepad2Icon" tooltip="Minecraft version">
Minecraft {{ instance.game_version }}
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
v-if="sharedInstanceManager?.type !== 'user'"
:icon="ServerLoaderIcon"
:icon-props="{ loader: loaderDisplayName }"
tooltip="Mod loader"
>
{{ loaderLabel }}
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
v-if="showInstancePlayTime"
:icon="TimerIcon"
tooltip="Total playtime"
>
{{ playtimeLabel }}
</PageHeaderMetadataItem>
<PageHeaderMetadataItem v-if="sharedInstanceManager" :action="sharedInstanceManagerAction">
{{ sharedInstanceManagerLabel }}
<Avatar
:src="sharedInstanceManager.avatarUrl"
:alt="sharedInstanceManager.name"
:tint-by="sharedInstanceManager.tintBy"
size="24px"
:circle="sharedInstanceManager.type === 'user'"
no-shadow
/>
<span class="min-w-0 truncate">{{ sharedInstanceManager.name }}</span>
</PageHeaderMetadataItem>
</PageHeaderMetadata>
</template>
<template #actions>
<PageHeaderActions>
<ButtonStyled v-if="isInstalling" color="brand" size="large">
<button type="button" disabled>
{{ formatMessage(commonMessages.installingLabel) }}
</button>
</ButtonStyled>
<ButtonStyled v-else-if="playing" color="red" size="large">
<button type="button" :disabled="stopping" @click="emit('stop')">
<StopCircleIcon />
{{
stopping ? formatMessage(messages.stopping) : formatMessage(commonMessages.stopButton)
}}
</button>
</ButtonStyled>
<ButtonStyled v-else-if="instance.quarantined" color="brand" size="large">
<button v-tooltip="formatMessage(messages.lockedPlayTooltip)" type="button" disabled>
<PlayIcon />
{{ formatMessage(commonMessages.playButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-else-if="instance.install_stage !== 'installed'" color="brand" size="large">
<button type="button" @click="emit('repair')">
<DownloadIcon />
{{ formatMessage(messages.repair) }}
</button>
</ButtonStyled>
<JoinedButtons
v-else-if="!loading && isServerInstance"
:actions="serverPlayActions"
color="brand"
size="large"
/>
<ButtonStyled v-else-if="!loading" color="brand" size="large">
<button type="button" @click="emit('play')">
<PlayIcon />
{{ formatMessage(commonMessages.playButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-else color="brand" size="large">
<button type="button" disabled>{{ formatMessage(messages.starting) }}</button>
</ButtonStyled>
<ButtonStyled circular size="large">
<button
v-tooltip="formatMessage(messages.instanceSettings)"
type="button"
:aria-label="formatMessage(messages.instanceSettings)"
@click="emit('settings')"
>
<SettingsIcon />
</button>
</ButtonStyled>
<ButtonStyled circular size="large" type="transparent">
<TeleportOverflowMenu
:options="moreActions"
:tooltip="formatMessage(messages.moreActions)"
:aria-label="formatMessage(messages.moreActions)"
>
<MoreVerticalIcon />
</TeleportOverflowMenu>
</ButtonStyled>
</PageHeaderActions>
</template>
</PageHeader>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
DownloadIcon,
ExternalIcon,
FolderOpenIcon,
LockIcon,
MoreVerticalIcon,
PackageIcon,
PlayIcon,
ReportIcon,
SettingsIcon,
StopCircleIcon,
TagCategoryGamepad2Icon as Gamepad2Icon,
TimerIcon,
UnknownIcon,
} from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
commonMessages,
defineMessages,
formatLoaderLabel,
type JoinedButtonAction,
JoinedButtons,
LoaderIcon as ServerLoaderIcon,
PageHeader,
PageHeaderActions,
PageHeaderBadgeItem,
PageHeaderMetadata,
PageHeaderMetadataItem,
type ServerLoader,
TeleportOverflowMenu,
type TeleportOverflowMenuItem,
useVIntl,
} from '@modrinth/ui'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import type { GameInstance } from '@/helpers/types'
import InstanceHeaderServerMetadata from './instance-page-header-server-metadata.vue'
const messages = defineMessages({
createShortcut: {
id: 'instance.action.create-shortcut',
defaultMessage: 'Create shortcut',
},
exportModpack: {
id: 'instance.action.export-modpack',
defaultMessage: 'Export modpack',
},
instanceSettings: {
id: 'instance.action.settings',
defaultMessage: 'Instance settings',
},
launchInstance: {
id: 'instance.action.launch-instance',
defaultMessage: 'Launch instance',
},
moreActions: {
id: 'instance.action.more-actions',
defaultMessage: 'More actions',
},
neverPlayed: {
id: 'instance.playtime.never-played',
defaultMessage: 'Never played',
},
openFolder: {
id: 'instance.action.open-folder',
defaultMessage: 'Open folder',
},
repair: {
id: 'instance.action.repair',
defaultMessage: 'Repair',
},
lockedPlayTooltip: {
id: 'instance.locked.play-tooltip',
defaultMessage: 'This instance has been locked',
},
starting: {
id: 'instance.action.starting',
defaultMessage: 'Starting...',
},
stopping: {
id: 'instance.action.stopping',
defaultMessage: 'Stopping...',
},
sharedInstanceTooltip: {
id: 'instance.shared-instance.tooltip',
defaultMessage: "This instance's content is being managed by someone else.",
},
sharedInstanceOwnerTooltip: {
id: 'instance.shared-instance.owner-tooltip',
defaultMessage: "This instance's content is being shared to other users.",
},
})
const router = useRouter()
const props = withDefaults(
defineProps<{
instance: GameInstance
iconSrc?: string | null
isServerInstance?: boolean
showInstancePlayTime?: boolean
timePlayed?: number
playing?: boolean
loading?: boolean
stopping?: boolean
loadingServerPing?: boolean
playersOnline?: number
statusOnline?: boolean
recentPlays?: number
ping?: number
minecraftServer?: Labrinth.Projects.v3.Project['minecraft_server']
linkedProjectV3?: Labrinth.Projects.v3.Project
sharedInstanceManager?: {
type: 'user' | 'server'
name: string
avatarUrl?: string
tintBy: string
} | null
}>(),
{
iconSrc: null,
isServerInstance: false,
showInstancePlayTime: false,
timePlayed: 0,
playing: false,
loading: false,
stopping: false,
loadingServerPing: false,
playersOnline: undefined,
statusOnline: false,
recentPlays: undefined,
ping: undefined,
minecraftServer: undefined,
linkedProjectV3: undefined,
sharedInstanceManager: null,
},
)
const emit = defineEmits<{
repair: []
stop: []
play: []
playServer: []
settings: []
openFolder: []
export: []
createShortcut: []
report: [event?: MouseEvent]
}>()
const installingStages = [
'installing',
'pack_installing',
'pack_installed',
'not_installed',
'minecraft_installing',
]
const { formatMessage } = useVIntl()
const isInstalling = computed(() => installingStages.includes(props.instance.install_stage))
const loaderDisplayName = computed(() => formatLoaderLabel(props.instance.loader) as ServerLoader)
const loaderLabel = computed(() =>
[loaderDisplayName.value, props.instance.loader_version].filter(Boolean).join(' '),
)
const sharedInstanceTooltip = computed(() =>
formatMessage(
props.instance.shared_instance?.role === 'owner'
? messages.sharedInstanceOwnerTooltip
: messages.sharedInstanceTooltip,
),
)
const sharedInstanceManagerLabel = computed(() =>
props.sharedInstanceManager?.type === 'server' ? 'Linked to' : 'Managed by',
)
const sharedInstanceManagerAction = computed(() => {
const manager = props.sharedInstanceManager
if (manager?.type !== 'user') return undefined
return () => router.push(`/user/${encodeURIComponent(manager.name)}`)
})
const playtimeLabel = computed(() => {
if (props.timePlayed <= 0) return formatMessage(messages.neverPlayed)
const hours = Math.floor(props.timePlayed / 3600)
if (hours >= 1) {
return `${hours} hour${hours > 1 ? 's' : ''}`
}
const minutes = Math.floor(props.timePlayed / 60)
if (minutes >= 1) {
return `${minutes} minute${minutes > 1 ? 's' : ''}`
}
const seconds = Math.floor(props.timePlayed)
return `${seconds} second${seconds > 1 ? 's' : ''}`
})
const serverPlayActions = computed<JoinedButtonAction[]>(() => [
{
id: 'join_server',
label: formatMessage(commonMessages.playButton),
icon: PlayIcon,
action: () => emit('playServer'),
},
{
id: 'launch_instance',
label: formatMessage(messages.launchInstance),
icon: PlayIcon,
action: () => emit('play'),
},
])
const moreActions = computed<TeleportOverflowMenuItem[]>(() => {
const actions: TeleportOverflowMenuItem[] = [
{
id: 'open-folder',
label: formatMessage(messages.openFolder),
icon: FolderOpenIcon,
action: () => emit('openFolder'),
},
]
if (!props.instance.quarantined) {
actions.push(
{
id: 'export-mrpack',
label: formatMessage(messages.exportModpack),
icon: PackageIcon,
action: () => emit('export'),
},
{
id: 'create-shortcut',
label: formatMessage(messages.createShortcut),
icon: ExternalIcon,
action: () => emit('createShortcut'),
},
)
}
if (props.instance.shared_instance?.role === 'member') {
actions.push(
{ divider: true },
{
id: 'report-shared-instance',
label: formatMessage(commonMessages.reportButton),
icon: ReportIcon,
color: 'red',
action: (event) => emit('report', event),
},
)
}
return actions
})
</script>
@@ -0,0 +1,63 @@
<template>
<div class="flex items-center flex-wrap gap-2">
<template v-if="loadingServerPing">
<ServerOnlinePlayers
v-if="playersOnline !== undefined"
:online="playersOnline"
:status-online="statusOnline"
hide-label
/>
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
<div
v-if="
(playersOnline !== undefined || recentPlays !== undefined) &&
(minecraftServer?.region || ping)
"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
<ServerPing v-if="ping" :ping="ping" />
</template>
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
<div v-if="minecraftServer?.region || ping" class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
<div v-if="linkedProjectV3" class="flex gap-1.5 items-center font-medium text-primary">
Linked to
<Avatar
:src="linkedProjectV3.icon_url"
:alt="linkedProjectV3.name"
:tint-by="instanceId"
size="24px"
/>
<router-link
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
class="hover:underline text-primary truncate"
>
{{ linkedProjectV3.name }}
</router-link>
</div>
</div>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
Avatar,
ServerOnlinePlayers,
ServerPing,
ServerRecentPlays,
ServerRegion,
} from '@modrinth/ui'
defineProps<{
loadingServerPing?: boolean
playersOnline?: number
statusOnline?: boolean
recentPlays?: number
ping?: number
minecraftServer?: Labrinth.Projects.v3.Project['minecraft_server']
linkedProjectV3?: Labrinth.Projects.v3.Project
instanceId?: string
}>()
</script>
@@ -0,0 +1,141 @@
<template>
<StackedAdmonitions v-bind="$attrs" :items="stackItems" class="w-full">
<template #item="{ item, dismissible }">
<InstanceAdmonitionsSharedInstanceStale
v-if="item.kind === 'shared-instance-stale'"
:instance="instance"
@published="emit('published')"
/>
<InstanceAdmonitionsSharedInstanceUpdateAvailable
v-else-if="item.kind === 'shared-instance-update-available'"
:instance-name="instance.name"
@review="emit('review-update', $event)"
/>
<InstanceAdmonitionsSharedInstanceWrongAccount
v-else-if="item.kind === 'shared-instance-wrong-account'"
:expected-user-id="sharedInstanceExpectedUserId"
:role="sharedInstanceRole"
:signed-out="sharedInstanceSignedOut"
/>
<InstanceAdmonitionsSharedInstanceUnavailable
v-else-if="item.kind === 'shared-instance-unavailable'"
:reason="displayedSharedInstanceUnavailableReason"
:manager="sharedInstanceUnavailableManager"
:dismissible="dismissible"
@dismiss="sharedInstanceUnavailableDismissed = true"
@delete="emit('delete')"
/>
</template>
</StackedAdmonitions>
</template>
<script setup lang="ts">
import { StackedAdmonitions } from '@modrinth/ui'
import { computed, ref, watch } from 'vue'
import type { SharedInstanceUnavailableReason } from '@/helpers/install'
import type { GameInstance } from '@/helpers/types'
import InstanceAdmonitionsSharedInstanceStale from './instance-admonitions-shared-instance-stale.vue'
import InstanceAdmonitionsSharedInstanceUnavailable from './instance-admonitions-shared-instance-unavailable.vue'
import InstanceAdmonitionsSharedInstanceUpdateAvailable from './instance-admonitions-shared-instance-update-available.vue'
import InstanceAdmonitionsSharedInstanceWrongAccount from './instance-admonitions-shared-instance-wrong-account.vue'
import type { InstanceAdmonitionItem, SharedInstanceRole } from './types'
defineOptions({
inheritAttrs: false,
})
const props = defineProps<{
instance: GameInstance
sharedInstanceUnavailableReason?: SharedInstanceUnavailableReason | null
sharedInstanceUnavailableManager?: string | null
sharedInstanceWrongAccount?: boolean
sharedInstanceExpectedUserId?: string | null
sharedInstanceRole?: SharedInstanceRole | null
sharedInstanceSignedOut?: boolean
sharedInstanceUpdateAvailable?: boolean
}>()
const emit = defineEmits<{
published: []
delete: []
'review-update': [event: MouseEvent]
}>()
const sharedInstanceWrongAccount = computed(() => props.sharedInstanceWrongAccount ?? false)
const displayedSharedInstanceUnavailableReason = computed<SharedInstanceUnavailableReason | null>(
() =>
props.instance.quarantined ? 'quarantined' : (props.sharedInstanceUnavailableReason ?? null),
)
const sharedInstanceUnavailableDismissed = ref(false)
const showSharedInstancePublishAdmonition = computed(
() =>
!sharedInstanceWrongAccount.value &&
props.instance.install_stage === 'installed' &&
props.instance.shared_instance?.role === 'owner' &&
props.instance.shared_instance.status === 'stale',
)
const showSharedInstanceUpdateAdmonition = computed(
() =>
!sharedInstanceWrongAccount.value &&
!displayedSharedInstanceUnavailableReason.value &&
props.instance.install_stage === 'installed' &&
props.sharedInstanceRole === 'member' &&
props.sharedInstanceUpdateAvailable === true,
)
const stackItems = computed<InstanceAdmonitionItem[]>(() => {
const items: InstanceAdmonitionItem[] = []
if (sharedInstanceWrongAccount.value) {
items.push({
id: 'shared-instance-wrong-account',
type: 'warning',
dismissible: false,
kind: 'shared-instance-wrong-account',
})
}
const unavailableReason = displayedSharedInstanceUnavailableReason.value
const sharedInstanceQuarantined = unavailableReason === 'quarantined'
if (
unavailableReason &&
(sharedInstanceQuarantined || !sharedInstanceUnavailableDismissed.value)
) {
items.push({
id: 'shared-instance-unavailable',
type: 'warning',
dismissible: !sharedInstanceQuarantined,
kind: 'shared-instance-unavailable',
})
}
if (showSharedInstancePublishAdmonition.value) {
items.push({
id: 'shared-instance-stale',
type: 'warning',
dismissible: false,
kind: 'shared-instance-stale',
})
}
if (showSharedInstanceUpdateAdmonition.value) {
items.push({
id: 'shared-instance-update-available',
type: 'info',
dismissible: false,
kind: 'shared-instance-update-available',
})
}
return items
})
watch(
() => [props.instance.id, displayedSharedInstanceUnavailableReason.value],
() => {
sharedInstanceUnavailableDismissed.value = false
},
)
</script>
@@ -0,0 +1,82 @@
import { defineMessages } from '@modrinth/ui'
export const instanceAdmonitionsMessages = defineMessages({
sharedInstanceChangesHeader: {
id: 'app.instance.admonitions.shared-instance.changes-header',
defaultMessage: "Your changes haven't been shared yet",
},
sharedInstanceChangesBody: {
id: 'app.instance.admonitions.shared-instance.changes-body',
defaultMessage: "Your local instance is ahead of the users you've shared it with.",
},
sharedInstancePublishButton: {
id: 'app.instance.admonitions.shared-instance.publish-button',
defaultMessage: 'Push update',
},
sharedInstancePublishingButton: {
id: 'app.instance.admonitions.shared-instance.publishing-button',
defaultMessage: 'Pushing...',
},
sharedInstanceReviewingButton: {
id: 'app.instance.admonitions.shared-instance.reviewing-button',
defaultMessage: 'Reviewing...',
},
sharedInstanceUpdateAvailableHeader: {
id: 'app.instance.admonitions.shared-instance.update-available-header',
defaultMessage: 'An update is available',
},
sharedInstanceUpdateAvailableBody: {
id: 'app.instance.admonitions.shared-instance.update-available-body',
defaultMessage:
'An update is required to play {name}. Please update to latest version to launch the game.',
},
sharedInstanceReviewUpdateButton: {
id: 'app.instance.admonitions.shared-instance.review-update-button',
defaultMessage: 'Review update',
},
sharedInstanceReviewHeader: {
id: 'app.instance.admonitions.shared-instance.review-header',
defaultMessage: 'Review changes',
},
sharedInstanceReviewAdmonitionHeader: {
id: 'app.instance.admonitions.shared-instance.review-admonition-header',
defaultMessage: 'Push update',
},
sharedInstanceReviewDescription: {
id: 'app.instance.admonitions.shared-instance.review-description',
defaultMessage:
'Review the content changes that will be shared with everyone using this instance.',
},
sharedInstanceAddedLabel: {
id: 'app.instance.admonitions.shared-instance.added-label',
defaultMessage: 'Added',
},
sharedInstanceRemovedLabel: {
id: 'app.instance.admonitions.shared-instance.removed-label',
defaultMessage: 'Removed',
},
sharedInstanceWrongAccountHeader: {
id: 'app.instance.shared-instance-wrong-account.warning-header',
defaultMessage: 'You are using the wrong Modrinth account',
},
sharedInstanceSignedOutHeader: {
id: 'app.instance.shared-instance-wrong-account.signed-out-header',
defaultMessage: 'You need to sign in to Modrinth',
},
sharedInstanceWrongAccountSignInAs: {
id: 'app.instance.shared-instance-wrong-account.sign-in-as-label',
defaultMessage: 'Sign in as',
},
sharedInstanceWrongAccountUserBody: {
id: 'app.instance.shared-instance-wrong-account.user-admonition-body-v2',
defaultMessage: 'to receive updates for this shared instance.',
},
sharedInstanceWrongAccountOwnerBody: {
id: 'app.instance.shared-instance-wrong-account.owner-admonition-body-v2',
defaultMessage: "to manage this shared instance. You won't be able to push updates to users.",
},
sharedInstanceWrongAccountFallbackUsername: {
id: 'app.instance.shared-instance-wrong-account.fallback-username',
defaultMessage: 'the linked account',
},
})
@@ -0,0 +1,66 @@
<template>
<Admonition
type="info"
inline-actions
:header="formatMessage(messages.sharedInstanceChangesHeader)"
>
{{ formatMessage(messages.sharedInstanceChangesBody) }}
<template #actions>
<ButtonStyled color="blue">
<button class="!h-10" :disabled="isPublishButtonDisabled" @click="reviewChanges">
<SpinnerIcon
v-if="isReviewingPublish || isPublishing"
class="animate-spin"
aria-hidden="true"
/>
<UploadIcon v-else aria-hidden="true" />
{{
isPublishing
? formatMessage(messages.sharedInstancePublishingButton)
: isReviewingPublish
? formatMessage(messages.sharedInstanceReviewingButton)
: formatMessage(messages.sharedInstancePublishButton)
}}
</button>
</ButtonStyled>
</template>
</Admonition>
<SharedInstancePublishModal
ref="publishModal"
:instance="instance"
@published="emit('published')"
@state-change="publishState = $event"
/>
</template>
<script setup lang="ts">
import { SpinnerIcon, UploadIcon } from '@modrinth/assets'
import { Admonition, ButtonStyled, useVIntl } from '@modrinth/ui'
import { computed, ref } from 'vue'
import SharedInstancePublishModal from '@/components/ui/shared-instances/SharedInstancePublishModal.vue'
import type { GameInstance } from '@/helpers/types'
import { instanceAdmonitionsMessages as messages } from './instance-admonitions-messages'
defineProps<{
instance: GameInstance
}>()
const emit = defineEmits<{
published: []
}>()
const { formatMessage } = useVIntl()
const publishModal = ref<InstanceType<typeof SharedInstancePublishModal>>()
const publishState = ref<'idle' | 'reviewing' | 'publishing'>('idle')
const isPublishing = computed(() => publishState.value === 'publishing')
const isReviewingPublish = computed(() => publishState.value === 'reviewing')
const isPublishButtonDisabled = computed(() => isPublishing.value || isReviewingPublish.value)
function reviewChanges(e?: MouseEvent) {
publishModal.value?.show(e)
}
</script>
@@ -0,0 +1,51 @@
<template>
<Admonition
type="warning"
:inline-actions="reason === 'quarantined'"
:header="formatMessage(sharedInstanceUnavailableTitleMessage(reason ?? null))"
:dismissible="dismissible"
@dismiss="emit('dismiss')"
>
{{ formatSharedInstanceUnavailable(reason ?? null, manager) }}
<template v-if="reason === 'quarantined'" #actions>
<ButtonStyled color="orange">
<button class="!h-10" @click="emit('delete')">
<TrashIcon aria-hidden="true" />
{{ formatMessage(messages.deleteInstance) }}
</button>
</ButtonStyled>
</template>
</Admonition>
</template>
<script setup lang="ts">
import { TrashIcon } from '@modrinth/assets'
import { Admonition, ButtonStyled, defineMessages, useVIntl } from '@modrinth/ui'
import type { SharedInstanceUnavailableReason } from '@/helpers/install'
import {
sharedInstanceUnavailableTitleMessage,
useSharedInstanceErrors,
} from '@/helpers/shared-instance-errors'
defineProps<{
reason?: SharedInstanceUnavailableReason | null
manager?: string | null
dismissible?: boolean
}>()
const emit = defineEmits<{
dismiss: []
delete: []
}>()
const { formatMessage } = useVIntl()
const { formatSharedInstanceUnavailable } = useSharedInstanceErrors()
const messages = defineMessages({
deleteInstance: {
id: 'instance.locked.delete-button',
defaultMessage: 'Delete instance',
},
})
</script>
@@ -0,0 +1,34 @@
<template>
<Admonition
type="info"
inline-actions
:header="formatMessage(messages.sharedInstanceUpdateAvailableHeader)"
>
{{ formatMessage(messages.sharedInstanceUpdateAvailableBody, { name: instanceName }) }}
<template #actions>
<ButtonStyled color="blue">
<button class="!h-10" @click="emit('review', $event)">
<DownloadIcon aria-hidden="true" />
{{ formatMessage(messages.sharedInstanceReviewUpdateButton) }}
</button>
</ButtonStyled>
</template>
</Admonition>
</template>
<script setup lang="ts">
import { DownloadIcon } from '@modrinth/assets'
import { Admonition, ButtonStyled, useVIntl } from '@modrinth/ui'
import { instanceAdmonitionsMessages as messages } from './instance-admonitions-messages'
defineProps<{
instanceName: string
}>()
const emit = defineEmits<{
review: [event: MouseEvent]
}>()
const { formatMessage } = useVIntl()
</script>
@@ -0,0 +1,78 @@
<template>
<Admonition type="warning" :header="formatMessage(headerMessage)">
<span class="flex flex-wrap items-center gap-x-1.5 gap-y-1">
<span>{{ formatMessage(messages.sharedInstanceWrongAccountSignInAs) }}</span>
<span
v-if="sharedInstanceExpectedUser"
class="inline-flex max-w-full min-w-0 items-center gap-1.5 align-middle font-semibold text-contrast"
>
<Avatar
:src="sharedInstanceExpectedUser.avatarUrl"
:alt="sharedInstanceExpectedUsername"
:tint-by="sharedInstanceExpectedUser.tintBy"
size="20px"
circle
no-shadow
/>
<span class="min-w-0 truncate">{{ sharedInstanceExpectedUsername }}</span>
</span>
<span v-else class="font-semibold">{{ sharedInstanceExpectedUsername }}</span>
<span>{{ formatMessage(bodyMessage) }}</span>
</span>
</Admonition>
</template>
<script setup lang="ts">
import { Admonition, Avatar, useVIntl } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import { computed } from 'vue'
import { get_user } from '@/helpers/cache'
import { instanceAdmonitionsMessages as messages } from './instance-admonitions-messages'
import type { SharedInstanceRole } from './types'
const props = defineProps<{
expectedUserId?: string | null
role?: SharedInstanceRole | null
signedOut?: boolean
}>()
const { formatMessage } = useVIntl()
const expectedUserId = computed(() => props.expectedUserId ?? null)
const expectedUserQuery = useQuery({
queryKey: computed(() => ['user', expectedUserId.value]),
queryFn: async () => {
if (!expectedUserId.value) return null
return await get_user(expectedUserId.value, 'bypass').catch(() => null)
},
enabled: () => !!expectedUserId.value,
staleTime: 30_000,
})
const sharedInstanceExpectedUser = computed(() => {
const user = expectedUserQuery.data.value
if (!user) return null
return {
username: user.username,
avatarUrl: user.avatar_url ?? undefined,
tintBy: user.id,
}
})
const sharedInstanceExpectedUsername = computed(
() =>
sharedInstanceExpectedUser.value?.username ||
formatMessage(messages.sharedInstanceWrongAccountFallbackUsername),
)
const headerMessage = computed(() =>
props.signedOut
? messages.sharedInstanceSignedOutHeader
: messages.sharedInstanceWrongAccountHeader,
)
const bodyMessage = computed(() =>
props.role === 'owner'
? messages.sharedInstanceWrongAccountOwnerBody
: messages.sharedInstanceWrongAccountUserBody,
)
</script>
@@ -0,0 +1,13 @@
import type { StackedAdmonitionItem } from '@modrinth/ui'
export type InstanceAdmonitionKind =
| 'shared-instance-stale'
| 'shared-instance-update-available'
| 'shared-instance-unavailable'
| 'shared-instance-wrong-account'
export type InstanceAdmonitionItem = StackedAdmonitionItem & {
kind: InstanceAdmonitionKind
}
export type SharedInstanceRole = 'owner' | 'member'
@@ -109,9 +109,13 @@ watch(selectedReleaseChannel, async (channel, previousChannel) => {
})
async function resetIcon() {
icon.value = undefined
await edit_icon(instance.value.id, null).catch(handleError)
trackEvent('InstanceRemoveIcon')
try {
await edit_icon(instance.value.id, null)
icon.value = undefined
trackEvent('InstanceRemoveIcon')
} catch (error) {
handleError(error)
}
}
async function setIcon() {
@@ -127,10 +131,13 @@ async function setIcon() {
if (!value) return
icon.value = value
await edit_icon(instance.value.id, icon.value).catch(handleError)
trackEvent('InstanceSetIcon')
try {
await edit_icon(instance.value.id, value)
icon.value = value
trackEvent('InstanceSetIcon')
} catch (error) {
handleError(error)
}
}
const editInstanceObject = computed(() => ({
@@ -7,7 +7,6 @@ import {
injectFilePicker,
injectNotificationManager,
InstallationSettingsLayout,
provideAppBackup,
provideInstallationSettings,
useDebugLogger,
useVIntl,
@@ -16,24 +15,25 @@ import type { GameVersionTag, PlatformTag } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import SharedInstanceInstallationSettingsControls from '@/components/ui/shared-instances/SharedInstanceInstallationSettingsControls.vue'
import { useManagedContentPolicy } from '@/composables/instances/use-managed-content-policy'
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_version } from '@/helpers/cache'
import {
install_duplicate_instance,
install_existing_instance,
install_pack_to_existing_instance,
installJobInstanceId,
wait_for_install_job,
} from '@/helpers/install'
import {
edit,
get_linked_modpack_info,
list,
unlink_shared_instance,
update_managed_modrinth_version,
update_repair_modrinth,
} from '@/helpers/instance'
import { get_loader_versions } from '@/helpers/metadata'
import { get_game_versions, get_loaders } from '@/helpers/tags'
import { provideInstanceBackup } from '@/providers/instance-backup'
import { injectInstanceSettings } from '@/providers/instance-settings'
import { useTheming } from '@/store/state'
@@ -47,6 +47,7 @@ const debug = useDebugLogger('AppInstallationSettings')
const themeStore = useTheming()
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
const managedContentPolicy = useManagedContentPolicy(instance)
const skipNonEssentialWarnings = computed(() =>
themeStore.getFeatureFlag('skip_non_essential_warnings'),
)
@@ -111,9 +112,14 @@ debug('metadata queries configured', {
const isModrinthLinkedModpack = computed(
() =>
instance.value.link?.type === 'modrinth_modpack' ||
instance.value.link?.type === 'server_project_modpack',
instance.value.link?.type === 'server_project_modpack' ||
(instance.value.link?.type === 'shared_instance' &&
!!instance.value.link.modpack_project_id &&
!!instance.value.link.modpack_version_id),
)
const isImportedModpack = computed(() => instance.value.link?.type === 'imported_modpack')
const isSharedInstanceManagedModpack = managedContentPolicy.isManagedModpack
const canUnlinkSharedInstance = managedContentPolicy.canUnlink
const modpackInfoQuery = useQuery({
queryKey: computed(() => ['linkedModpackInfo', instance.value.id]),
@@ -124,12 +130,43 @@ const modpackInfo = modpackInfoQuery.data
const repairing = ref(false)
const reinstalling = ref(false)
const unlinkingSharedInstance = ref(false)
const installationSettingsBusy = computed(
() =>
instance.value.quarantined ||
instance.value.install_stage !== 'installed' ||
repairing.value ||
reinstalling.value ||
unlinkingSharedInstance.value ||
!!offline,
)
const installationSettingsBusyMessage = computed(() =>
instance.value.quarantined ? formatMessage(messages.locked) : null,
)
async function unlinkSharedInstance() {
unlinkingSharedInstance.value = true
try {
await unlink_shared_instance(instance.value.id)
await queryClient.invalidateQueries({ queryKey: ['sharedInstanceUsers', instance.value.id] })
await queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', instance.value.id] })
onUnlinked()
} catch (error) {
handleError(error)
} finally {
unlinkingSharedInstance.value = false
}
}
const messages = defineMessages({
loaderVersion: {
id: 'instance.settings.tabs.installation.loader-version',
defaultMessage: '{loader} version',
},
locked: {
id: 'instance.settings.tabs.installation.locked',
defaultMessage: 'Installation settings are unavailable while this instance is locked.',
},
})
function getManifest(loader: string) {
@@ -162,27 +199,7 @@ async function installLocalModpackFromPicker() {
return !!completed
}
provideAppBackup({
async createBackup() {
debug('createBackup: start', {
instanceId: instance.value.id,
instanceName: instance.value.name,
})
const allInstances = await list()
const prefix = `${instance.value.name} - Backup #`
const existingNums = allInstances
.filter((p) => p.name.startsWith(prefix))
.map((p) => parseInt(p.name.slice(prefix.length), 10))
.filter((n) => !isNaN(n))
const nextNum = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1
const job = await install_duplicate_instance(instance.value.id)
const newInstanceId = installJobInstanceId(job)
if (newInstanceId) {
await edit(newInstanceId, { name: `${prefix}${nextNum}` })
}
debug('createBackup: done', { newInstanceId, backupName: `${prefix}${nextNum}` })
},
})
provideInstanceBackup(instance)
provideInstallationSettings({
closeSettings: closeModal,
@@ -208,14 +225,14 @@ provideInstallationSettings({
}
return rows
}),
isLinked: computed(() => isModrinthLinkedModpack.value || isImportedModpack.value),
isBusy: computed(
isLinked: computed(
() =>
instance.value.install_stage !== 'installed' ||
repairing.value ||
reinstalling.value ||
!!offline,
isModrinthLinkedModpack.value ||
isImportedModpack.value ||
isSharedInstanceManagedModpack.value,
),
isBusy: installationSettingsBusy,
busyMessage: installationSettingsBusyMessage,
skipNonEssentialWarnings,
modpack: computed(() => {
if (isImportedModpack.value && instance.value.link?.type === 'imported_modpack') {
@@ -452,14 +469,28 @@ provideInstallationSettings({
isServer: false,
isApp: true,
showModpackVersionActions: computed(
() => isModrinthLinkedModpack.value && !isMinecraftServer.value,
() =>
isModrinthLinkedModpack.value &&
!isMinecraftServer.value &&
!isSharedInstanceManagedModpack.value,
),
isLocalFile: isImportedModpack,
isManagedModpack: isSharedInstanceManagedModpack,
managedModpackWarning: managedContentPolicy.managedModpackWarning,
repairing,
reinstalling,
})
</script>
<template>
<InstallationSettingsLayout />
<InstallationSettingsLayout>
<template #extra>
<SharedInstanceInstallationSettingsControls
:can-unlink="canUnlinkSharedInstance"
:busy="installationSettingsBusy"
:unlinking="unlinkingSharedInstance"
:unlink="unlinkSharedInstance"
/>
</template>
</InstallationSettingsLayout>
</template>
@@ -0,0 +1,244 @@
<template>
<div class="flex flex-col gap-8">
<section class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.activeInvitesTitle) }}
</h3>
<p class="m-0 text-secondary">
{{ formatMessage(messages.activeInvitesDescription) }}
</p>
</div>
<Table :columns="inviteColumns" :data="activeInvites" row-key="id" table-min-width="36rem">
<template #empty-state>
<div class="flex h-40 items-center justify-center text-secondary">
<SpinnerIcon
v-if="activeInvitesQuery.isLoading.value"
class="animate-spin"
aria-hidden="true"
/>
<template v-else>
{{ formatMessage(messages.noActiveInvites) }}
</template>
</div>
</template>
<template #cell-id="{ row }">
<CopyCode
:text="`${config.siteUrl}/share/${encodeURIComponent(row.id)}`"
:display-text="`/${row.id}`"
/>
</template>
<template #cell-uses="{ row }">
<span class="font-medium text-primary">{{ row.uses }}</span>
<span> / {{ row.maxUses }}</span>
</template>
<template #cell-expiration="{ row }">
<span v-tooltip="formatDateTime(row.expiration)" class="whitespace-nowrap">
{{ formatRelativeTime(row.expiration) }}
</span>
</template>
<template #cell-actions="{ row }">
<div class="flex justify-end">
<ButtonStyled circular type="transparent">
<button
v-tooltip="formatMessage(messages.revokeInvite)"
:aria-label="
formatMessage(messages.revokeInviteWithCode, {
code: row.id,
})
"
:disabled="revokeInviteMutation.isPending.value || isBusy"
class="text-secondary hover:!filter-none hover:text-red focus-visible:!filter-none"
@click="revokeInviteModal?.show(row.id)"
>
<SpinnerIcon
v-if="
revokeInviteMutation.isPending.value &&
revokeInviteMutation.variables.value?.inviteId === row.id
"
class="animate-spin"
aria-hidden="true"
/>
<XIcon v-else aria-hidden="true" />
</button>
</ButtonStyled>
</div>
</template>
</Table>
</section>
<SharedInstanceInstallationSettingsControls
can-unpublish
:busy="isBusy"
:unpublishing="unpublishing"
:unpublish="unpublishSharedInstance"
/>
<ConfirmRevokeSharedInstanceInviteModal ref="revokeInviteModal" @revoke="revokeInvite" />
</div>
</template>
<script setup lang="ts">
import { SpinnerIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
CopyCode,
defineMessages,
Table,
type TableColumn,
useFormatDateTime,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import ConfirmRevokeSharedInstanceInviteModal from '@/components/ui/shared-instances/ConfirmRevokeSharedInstanceInviteModal.vue'
import SharedInstanceInstallationSettingsControls from '@/components/ui/shared-instances/SharedInstanceInstallationSettingsControls.vue'
import { config } from '@/config'
import {
get_shared_instance_invites,
revoke_shared_instance_invite,
type SharedInstanceInvite,
unpublish_shared_instance,
} from '@/helpers/instance'
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
import { injectInstanceSettings } from '@/providers/instance-settings'
const { instance, offline, onUnlinked } = injectInstanceSettings()
const { notifySharedInstanceError } = useSharedInstanceErrors()
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const unpublishing = ref(false)
const revokeInviteModal = ref<InstanceType<typeof ConfirmRevokeSharedInstanceInviteModal>>()
const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({ dateStyle: 'medium', timeStyle: 'short' })
type InviteTableColumn = 'id' | 'uses' | 'expiration' | 'actions'
type ActiveInvitesQueryKey = readonly ['sharedInstanceInvites', string]
const activeInvitesQueryKey = computed(
() => ['sharedInstanceInvites', instance.value.id] as const satisfies ActiveInvitesQueryKey,
)
const activeInvitesQuery = useQuery({
queryKey: activeInvitesQueryKey,
queryFn: async ({ queryKey }) => {
try {
return await get_shared_instance_invites(queryKey[1])
} catch (error) {
notifySharedInstanceError(error)
throw error
}
},
enabled: () => !!instance.value.id && !offline,
retry: false,
staleTime: Infinity,
refetchOnMount: 'always',
refetchOnReconnect: false,
refetchOnWindowFocus: false,
})
const activeInvites = computed(() => activeInvitesQuery.data.value ?? [])
const revokeInviteMutation = useMutation({
mutationFn: ({ instanceId, inviteId }: { instanceId: string; inviteId: string }) =>
revoke_shared_instance_invite(instanceId, inviteId),
onSuccess: (_data, { instanceId, inviteId }) => {
queryClient.setQueryData<SharedInstanceInvite[]>(
['sharedInstanceInvites', instanceId],
(invites = []) => invites.filter((invite) => invite.id !== inviteId),
)
},
onError: notifySharedInstanceError,
})
const isBusy = computed(
() =>
instance.value.install_stage !== 'installed' ||
unpublishing.value ||
revokeInviteMutation.isPending.value ||
!!offline,
)
const inviteColumns = computed<TableColumn<InviteTableColumn>[]>(() => [
{
key: 'id',
label: formatMessage(messages.inviteCodeLabel),
width: 'clamp(11rem, 34%, 19rem)',
},
{
key: 'uses',
label: formatMessage(messages.usesLabel),
width: 'clamp(7rem, 18%, 10rem)',
},
{
key: 'expiration',
label: formatMessage(messages.expiresLabel),
width: 'clamp(9rem, 28%, 14rem)',
},
{
key: 'actions',
label: formatMessage(messages.actionsLabel),
align: 'right',
width: '5.5rem',
},
])
function revokeInvite(inviteId: string) {
if (revokeInviteMutation.isPending.value) return
revokeInviteMutation.mutate({ instanceId: instance.value.id, inviteId })
}
async function unpublishSharedInstance() {
unpublishing.value = true
try {
await unpublish_shared_instance(instance.value.id)
queryClient.setQueryData(['sharedInstanceUsers', instance.value.id], [])
await queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', instance.value.id] })
onUnlinked()
} catch (error) {
notifySharedInstanceError(error)
} finally {
unpublishing.value = false
}
}
const messages = defineMessages({
activeInvitesTitle: {
id: 'instance.settings.sharing.active-invites.title',
defaultMessage: 'Active invites',
},
activeInvitesDescription: {
id: 'instance.settings.sharing.active-invites.description',
defaultMessage: 'Anyone with one of these invite links can join while it remains active.',
},
inviteCodeLabel: {
id: 'instance.settings.sharing.active-invites.code',
defaultMessage: 'Invite link',
},
usesLabel: {
id: 'instance.settings.sharing.active-invites.uses',
defaultMessage: 'Uses',
},
expiresLabel: {
id: 'instance.settings.sharing.active-invites.expires',
defaultMessage: 'Expires',
},
actionsLabel: {
id: 'instance.settings.sharing.active-invites.actions',
defaultMessage: 'Actions',
},
noActiveInvites: {
id: 'instance.settings.sharing.active-invites.empty',
defaultMessage: 'There are no active invites.',
},
revokeInvite: {
id: 'instance.settings.sharing.active-invites.revoke',
defaultMessage: 'Revoke invite',
},
revokeInviteWithCode: {
id: 'instance.settings.sharing.active-invites.revoke-with-code',
defaultMessage: 'Revoke invite {code}',
},
})
</script>
@@ -0,0 +1,144 @@
<template>
<NewModal ref="modal" :header="formatMessage(messages.header)" max-width="544px" no-padding>
<div class="grid grid-cols-[1fr_auto] gap-2.5 h-[154px] px-7 pt-4 pb-1 pr-9">
<div class="flex flex-col gap-2.5 items-start justify-center h-min mt-5">
<div class="font-semibold text-xl text-contrast">
{{ formatMessage(messages.descriptionHeader) }}
</div>
<div class="text-secondary leading-6">
{{ formatMessage(messages.description) }}
</div>
</div>
<div class="relative h-full w-[96px] overflow-hidden mx-3">
<div class="absolute top-0 left-0 z-0 w-full flex grow-0 flex-col items-end p-0">
<img :src="steveImage" alt="" class="self-stretch" />
</div>
<div
class="absolute left-0 bottom-0 z-10 order-1 h-6 w-[120px] shrink-0 grow-0 bg-[linear-gradient(180deg,rgba(39,41,46,0)_0%,#27292E_80%,#27292E_100%)]"
></div>
</div>
</div>
<div class="flex flex-col gap-6 px-6 pb-6">
<div class="flex justify-end gap-2">
<ButtonStyled>
<a class="w-full !shadow-none" href="https://support.modrinth.com" @click="modal?.hide()">
<MessagesSquareIcon />
{{ formatMessage(messages.getSupport) }}
</a>
</ButtonStyled>
<ButtonStyled color="brand">
<button class="w-full !shadow-none" :disabled="loadingSignIn" @click="signIn">
<SpinnerIcon v-if="loadingSignIn" class="animate-spin" />
<svg
v-else
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
<rect x="10.75" width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
<rect y="10.75" width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
<rect
x="10.75"
y="10.75"
width="9.25"
height="9.25"
fill="black"
fill-opacity="0.9"
/>
</svg>
{{ formatMessage(messages.signIn) }}
</button>
</ButtonStyled>
</div>
<p class="m-0 text-center text-sm text-secondary">
{{ formatMessage(messages.dontHaveAccount) }}
<a
class="text-blue font-medium hover:underline"
href="https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc"
>
{{ formatMessage(messages.getMinecraft) }}
</a>
</p>
</div>
</NewModal>
</template>
<script setup lang="ts">
import { MessagesSquareIcon, SpinnerIcon } from '@modrinth/assets'
import { ButtonStyled, defineMessages, NewModal, useVIntl } from '@modrinth/ui'
import { inject, type Ref, ref } from 'vue'
import steveImage from '@/assets/steve-look-up-left.webp'
import type AccountsCard from '@/components/ui/AccountsCard.vue'
import { trackEvent } from '@/helpers/analytics'
import { login as loginFlow, set_default_user } from '@/helpers/auth.js'
import { handleSevereError } from '@/store/error.js'
const { formatMessage } = useVIntl()
const accountsCard = inject('accountsCard') as Ref<InstanceType<typeof AccountsCard> | null>
const messages = defineMessages({
header: {
id: 'minecraft-required.header',
defaultMessage: 'Minecraft required',
},
descriptionHeader: {
id: 'minecraft-required.description-header',
defaultMessage: 'Sign in to a Microsoft account',
},
description: {
id: 'minecraft-required.description',
defaultMessage:
'You need a Microsoft account that owns Minecraft before you can launch and play.',
},
getSupport: {
id: 'minecraft-required.get-support',
defaultMessage: 'Get support',
},
signIn: {
id: 'minecraft-required.sign-in',
defaultMessage: 'Sign in to Microsoft',
},
dontHaveAccount: {
id: 'minecraft-required.dont-have-account',
defaultMessage: 'Dont have an account?',
},
getMinecraft: {
id: 'minecraft-required.get-minecraft',
defaultMessage: 'Get Minecraft',
},
})
const modal = ref<InstanceType<typeof NewModal>>()
const loadingSignIn = ref(false)
function show() {
modal.value?.show()
}
async function signIn() {
loadingSignIn.value = true
try {
const loggedIn = await loginFlow()
if (!loggedIn) return
await set_default_user(loggedIn.profile.id)
await accountsCard.value?.refreshValues()
await trackEvent('AccountLogIn', { source: 'MinecraftRequiredModal' })
modal.value?.hide()
} catch (error) {
handleSevereError(error)
} finally {
loadingSignIn.value = false
}
}
defineExpose({
show,
})
</script>
@@ -3,12 +3,14 @@ import {
CoffeeIcon,
GameIcon,
GaugeIcon,
HeartHandshakeIcon,
LanguagesIcon,
ModrinthIcon,
PaintbrushIcon,
SettingsIcon,
Settings2Icon,
ShieldIcon,
ToggleRightIcon,
UserIcon,
} from '@modrinth/assets'
import {
commonMessages,
@@ -17,23 +19,32 @@ import {
defineMessages,
ProgressBar,
TabbedModal,
UnsavedChangesPopup,
useVIntl,
} from '@modrinth/ui'
import { getVersion } from '@tauri-apps/api/app'
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
import { ref, watch } from 'vue'
import { computed, provide, ref, watch } from 'vue'
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
import LanguageSettings from '@/components/ui/settings/LanguageSettings.vue'
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
import PrivacySettings from '@/components/ui/settings/account/PrivacySettings.vue'
import ProfileSettings from '@/components/ui/settings/account/ProfileSettings.vue'
import SocialSettings from '@/components/ui/settings/account/SocialSettings.vue'
import AppearanceSettings from '@/components/ui/settings/display/AppearanceSettings.vue'
import BehaviorSettings from '@/components/ui/settings/display/BehaviorSettings.vue'
import FeatureFlagSettings from '@/components/ui/settings/display/FeatureFlagSettings.vue'
import LanguageSettings from '@/components/ui/settings/display/LanguageSettings.vue'
import DefaultInstanceSettings from '@/components/ui/settings/instances/DefaultInstanceSettings.vue'
import JavaSettings from '@/components/ui/settings/instances/JavaSettings.vue'
import ResourceManagementSettings from '@/components/ui/settings/instances/ResourceManagementSettings.vue'
import { get, set } from '@/helpers/settings.ts'
import {
appSettingsModalContextKey,
type UnsavedChangesController,
} from '@/providers/app-settings-modal'
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
import { useTheming } from '@/store/state'
// TODO: Apply COMPONENT_STRUCTURE.md here and extract out common setting option components
const themeStore = useTheming()
const { formatMessage } = useVIntl()
@@ -45,71 +56,162 @@ const developerModeEnabled = defineMessage({
defaultMessage: 'Developer mode enabled.',
})
const tabCategories = defineMessages({
display: {
id: 'settings.sidebar.label.display',
defaultMessage: 'Display',
},
account: {
id: 'settings.sidebar.label.account',
defaultMessage: 'Account',
},
instances: {
id: 'app.settings.sidebar.label.instances',
defaultMessage: 'Instances',
},
})
const tabs = [
{
name: defineMessage({
id: 'app.settings.tabs.appearance',
defaultMessage: 'Appearance',
}),
category: tabCategories.display,
icon: PaintbrushIcon,
content: AppearanceSettings,
},
{
name: defineMessage({
id: 'app.settings.tabs.behavior',
defaultMessage: 'Behavior',
}),
category: tabCategories.display,
icon: Settings2Icon,
content: BehaviorSettings,
},
{
name: defineMessage({
id: 'app.settings.tabs.language',
defaultMessage: 'Language',
}),
category: tabCategories.display,
icon: LanguagesIcon,
content: LanguageSettings,
badge: commonMessages.beta,
},
{
name: commonSettingsMessages.featureFlags,
category: tabCategories.display,
icon: ToggleRightIcon,
content: FeatureFlagSettings,
developerOnly: true,
},
{
name: commonSettingsMessages.profile,
category: tabCategories.account,
icon: UserIcon,
content: ProfileSettings,
},
{
name: commonSettingsMessages.social,
category: tabCategories.account,
icon: HeartHandshakeIcon,
content: SocialSettings,
},
{
name: defineMessage({
id: 'app.settings.tabs.privacy',
defaultMessage: 'Privacy',
}),
category: tabCategories.account,
icon: ShieldIcon,
content: PrivacySettings,
},
{
name: defineMessage({
id: 'app.settings.tabs.default-instance-options',
defaultMessage: 'Default game options',
}),
category: tabCategories.instances,
icon: GameIcon,
content: DefaultInstanceSettings,
},
{
name: defineMessage({
id: 'app.settings.tabs.java-installations',
defaultMessage: 'Java installations',
}),
category: tabCategories.instances,
icon: CoffeeIcon,
content: JavaSettings,
},
{
name: defineMessage({
id: 'app.settings.tabs.default-instance-options',
defaultMessage: 'Default instance options',
}),
icon: GameIcon,
content: DefaultInstanceSettings,
},
{
name: defineMessage({
id: 'app.settings.tabs.resource-management',
defaultMessage: 'Resource management',
}),
category: tabCategories.instances,
icon: GaugeIcon,
content: ResourceManagementSettings,
},
{
name: commonSettingsMessages.featureFlags,
icon: ToggleRightIcon,
content: FeatureFlagSettings,
developerOnly: true,
},
]
const availableTabs = computed(() => tabs.filter((tab) => !tab.developerOnly || themeStore.devMode))
const modal = ref<InstanceType<typeof TabbedModal> | null>(null)
const unsavedChangesPopup = ref<{ nudge: () => void } | null>(null)
const unsavedChangesController = ref<UnsavedChangesController | null>(null)
const emptyUnsavedChangesState: Record<string, unknown> = {}
const originalUnsavedChangesState = computed(
() => unsavedChangesController.value?.getOriginal() ?? emptyUnsavedChangesState,
)
const modifiedUnsavedChangesState = computed(
() => unsavedChangesController.value?.getModified() ?? emptyUnsavedChangesState,
)
const savingUnsavedChanges = computed(() => unsavedChangesController.value?.isSaving() ?? false)
const hasUnsavedChanges = computed(() => unsavedChangesController.value?.hasChanges() ?? false)
function canLeaveCurrentTab(): boolean {
if (!unsavedChangesController.value?.hasChanges()) return true
unsavedChangesPopup.value?.nudge()
return false
}
function close(): boolean {
return modal.value?.hide() ?? false
}
function registerUnsavedChangesController(controller: UnsavedChangesController | null): void {
unsavedChangesController.value = controller
}
provide(appSettingsModalContextKey, {
close,
registerUnsavedChangesController,
})
function resetUnsavedChanges(): void {
unsavedChangesController.value?.reset()
}
function saveUnsavedChanges(): void {
void unsavedChangesController.value?.save()
}
function show() {
modal.value?.show()
}
defineExpose({ show })
function showProfile(): void {
const profileTabIndex = availableTabs.value.findIndex((tab) => tab.content === ProfileSettings)
if (profileTabIndex >= 0) {
modal.value?.setTab(profileTabIndex)
}
modal.value?.show()
}
defineExpose({ show, showProfile })
const { progress, version: downloadingVersion } = injectAppUpdateDownloadProgress()
@@ -129,12 +231,15 @@ watch(
function devModeCount() {
devModeCounter.value++
if (devModeCounter.value > 5) {
const selectedTab = modal.value ? availableTabs.value[modal.value.selectedTab] : undefined
themeStore.devMode = !themeStore.devMode
settings.value.developer_mode = !!themeStore.devMode
devModeCounter.value = 0
if (!themeStore.devMode && tabs[modal.value!.selectedTab].developerOnly) {
modal.value!.setTab(0)
if (modal.value) {
const selectedTabIndex = selectedTab ? availableTabs.value.indexOf(selectedTab) : -1
modal.value.setTab(selectedTabIndex >= 0 ? selectedTabIndex : 0)
}
}
}
@@ -144,15 +249,45 @@ const messages = defineMessages({
id: 'app.settings.downloading',
defaultMessage: 'Downloading v{version}',
},
appVersion: {
id: 'app.settings.app-version',
defaultMessage: 'Modrinth App {version}',
},
macos: {
id: 'app.settings.operating-system.macos',
defaultMessage: 'macOS',
},
developerModeButtonLabel: {
id: 'app.settings.developer-mode-button.label',
defaultMessage: 'Toggle developer mode',
},
})
</script>
<template>
<TabbedModal ref="modal" :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
<TabbedModal
ref="modal"
:tabs="availableTabs"
:width="'min(928px, calc(95vw - 10rem))'"
:before-hide="canLeaveCurrentTab"
:before-tab-change="canLeaveCurrentTab"
:floating-action-bar-shown="hasUnsavedChanges"
>
<template #title>
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
<SettingsIcon /> Settings
<span class="text-2xl font-semibold text-contrast">
{{ formatMessage(commonMessages.settingsLabel) }}
</span>
</template>
<template #floating-action-bar>
<UnsavedChangesPopup
ref="unsavedChangesPopup"
:original="originalUnsavedChangesState"
:modified="modifiedUnsavedChangesState"
:saving="savingUnsavedChanges"
inline
@reset="resetUnsavedChanges"
@save="saveUnsavedChanges"
/>
</template>
<template #footer>
<div class="mt-auto text-secondary text-sm">
<div class="mb-3">
@@ -168,6 +303,7 @@ const messages = defineMessages({
</p>
<div class="flex items-center gap-3">
<button
:aria-label="formatMessage(messages.developerModeButtonLabel)"
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
:class="{
'text-brand': themeStore.devMode,
@@ -175,12 +311,14 @@ const messages = defineMessages({
}"
@click="devModeCount"
>
<ModrinthIcon class="w-6 h-6" />
<ModrinthIcon aria-hidden="true" class="w-6 h-6" />
</button>
<div class="max-w-[200px]">
<p class="m-0">Modrinth App {{ version }}</p>
<p class="m-0">
<span v-if="osPlatform === 'macos'">macOS</span>
{{ formatMessage(messages.appVersion, { version }) }}
</p>
<p class="m-0">
<span v-if="osPlatform === 'macos'">{{ formatMessage(messages.macos) }}</span>
<span v-else class="capitalize">{{ osPlatform }}</span>
{{ osVersion }}
</p>
@@ -1,43 +0,0 @@
<script setup lang="ts">
import { LogInIcon, SpinnerIcon } from '@modrinth/assets'
import { ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
defineProps({
onFlowCancel: {
type: Function,
default() {
return async () => {}
},
},
})
const modal = ref()
function show() {
modal.value.show()
}
function hide() {
modal.value.hide()
}
defineExpose({ show, hide })
</script>
<template>
<ModalWrapper ref="modal" @hide="onFlowCancel">
<template #title>
<span class="items-center gap-2 text-lg font-extrabold text-contrast">
<LogInIcon /> Sign in
</span>
</template>
<div class="flex justify-center gap-2">
<SpinnerIcon class="w-12 h-12 animate-spin" />
</div>
<p class="text-sm text-secondary">
Please sign in at the browser window that just opened to continue.
</p>
</ModalWrapper>
</template>
@@ -4,8 +4,8 @@
:header="formatMessage(messages.installToPlay)"
:closable="true"
:on-hide="show_ads_window"
max-width="640px"
width="640px"
max-width="544px"
width="544px"
>
<div v-if="requiredContentProject" class="flex w-full flex-col gap-6">
<p class="m-0 text-primary">
@@ -423,8 +423,8 @@ const messages = defineMessages({
defaultMessage: 'Install anyway',
},
dontInstall: {
id: 'app.modal.install-to-play.dont-install',
defaultMessage: 'Dont install',
id: 'app.modal.install-to-play.external-files-dont-install',
defaultMessage: "Don't install",
},
})
@@ -6,6 +6,7 @@ import {
CoffeeIcon,
InfoIcon,
MonitorIcon,
UsersIcon,
WrenchIcon,
} from '@modrinth/assets'
import {
@@ -25,6 +26,7 @@ import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.v
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
import InstallationSettings from '@/components/ui/instance_settings/InstallationSettings.vue'
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
import SharingSettings from '@/components/ui/instance_settings/SharingSettings.vue'
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
import { get_project_v3 } from '@/helpers/cache'
import { get_linked_modpack_info } from '@/helpers/instance'
@@ -97,6 +99,15 @@ const tabs = computed<TabbedModalTab[]>(() => [
icon: WrenchIcon,
content: InstallationSettings,
},
{
name: defineMessage({
id: 'instance.settings.tabs.sharing',
defaultMessage: 'Sharing',
}),
icon: UsersIcon,
content: SharingSettings,
shown: props.instance.shared_instance?.role === 'owner' && !props.instance.quarantined,
},
{
name: defineMessage({
id: 'instance.settings.tabs.window',
@@ -0,0 +1,265 @@
<template>
<NewModal
ref="modal"
:header="formatMessage(authenticating ? messages.signingInHeader : messages.header)"
:on-hide="handleHide"
no-padding
max-width="548px"
width="100%"
>
<div v-if="authenticating === null" class="flex w-full flex-col gap-6 p-6">
<div class="flex flex-col gap-2 px-3">
<h2 class="m-0 text-xl font-semibold leading-7 text-contrast">
{{ formatMessage(messages.signInHeading) }}
</h2>
<p class="m-0 text-base leading-6 text-primary">
{{ formatMessage(messages.description) }}
</p>
</div>
<div class="flex flex-col gap-6">
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<ButtonStyled>
<button class="w-full !shadow-none" type="button" @click="authenticate('sign-up')">
<UserPlusIcon aria-hidden="true" />
{{ formatMessage(messages.createAccountButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button class="w-full" type="button" @click="authenticate('sign-in')">
<LogInIcon aria-hidden="true" />
{{ formatMessage(messages.signInButton) }}
</button>
</ButtonStyled>
</div>
<p class="m-0 text-center text-base font-medium leading-6 text-primary">
<IntlFormatted :message-id="messages.supportPrompt">
<template #support="{ children }">
<button
type="button"
class="inline cursor-pointer border-0 bg-transparent p-0 text-base font-medium leading-6 text-blue hover:underline"
@click="openSupport"
>
<component :is="() => children" />
</button>
</template>
</IntlFormatted>
</p>
</div>
</div>
<div v-else class="flex w-full flex-col gap-6 p-6">
<div class="flex flex-col gap-2.5 px-3">
<div class="flex flex-col gap-2">
<h2 class="m-0 text-xl font-semibold leading-7 text-contrast">
{{ formatMessage(messages.continueInBrowserHeading) }}
</h2>
<p class="m-0 text-base leading-6 text-primary">
{{ formatMessage(messages.browserDescription) }}
</p>
</div>
<div class="flex items-center gap-1.5 text-primary">
<SpinnerIcon aria-hidden="true" class="h-5 w-5 shrink-0 animate-spin" />
<span class="text-base leading-6">
{{ formatMessage(messages.waitingForBrowser) }}
</span>
</div>
</div>
<div class="flex flex-col gap-6">
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<ButtonStyled type="outlined">
<button class="w-full" type="button" @click="modal?.hide()">
<XIcon aria-hidden="true" />
{{ formatMessage(messages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled>
<button
class="w-full !shadow-none"
type="button"
:disabled="reopeningBrowser"
@click="reopenBrowser"
>
<RefreshCwIcon aria-hidden="true" />
{{ formatMessage(messages.openBrowserAgainButton) }}
</button>
</ButtonStyled>
</div>
<p class="m-0 text-center text-base font-medium leading-6 text-primary">
<IntlFormatted :message-id="messages.supportPrompt">
<template #support="{ children }">
<button
type="button"
class="inline cursor-pointer border-0 bg-transparent p-0 text-base font-medium leading-6 text-blue hover:underline"
@click="openSupport"
>
<component :is="() => children" />
</button>
</template>
</IntlFormatted>
</p>
</div>
</div>
</NewModal>
</template>
<script setup lang="ts">
import { LogInIcon, RefreshCwIcon, SpinnerIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, defineMessages, IntlFormatted, NewModal, useVIntl } from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import { ref } from 'vue'
import { cancelLogin, type ModrinthAuthFlow } from '@/helpers/mr_auth'
const props = defineProps<{
requestAuth: (flow: ModrinthAuthFlow) => Promise<boolean>
}>()
const { formatMessage } = useVIntl()
const modal = ref<InstanceType<typeof NewModal>>()
const authenticating = ref<ModrinthAuthFlow | null>(null)
const reopeningBrowser = ref(false)
let resolveShow: ((signedIn: boolean) => void) | undefined
let authenticationId = 0
let activeAuthentication: Promise<void> | undefined
function show(event?: MouseEvent) {
resetAuthentication(true)
resolveShow?.(false)
const modalInstance = modal.value
if (!modalInstance) return Promise.resolve(false)
return new Promise<boolean>((resolve) => {
resolveShow = resolve
modalInstance.show(event)
})
}
function showSigningIn(flow: ModrinthAuthFlow = 'sign-in', event?: MouseEvent) {
const result = show(event)
authenticate(flow)
return result
}
function finish(signedIn: boolean) {
resolveShow?.(signedIn)
resolveShow = undefined
}
function authenticate(flow: ModrinthAuthFlow) {
const id = ++authenticationId
authenticating.value = flow
const authentication = (async () => {
try {
if ((await props.requestAuth(flow)) && authenticationId === id) {
authenticating.value = null
activeAuthentication = undefined
finish(true)
modal.value?.hide()
}
} finally {
if (authenticationId === id) {
authenticating.value = null
activeAuthentication = undefined
}
}
})()
activeAuthentication = authentication
}
async function reopenBrowser() {
const flow = authenticating.value
if (!flow || reopeningBrowser.value) return
reopeningBrowser.value = true
const previousAuthentication = activeAuthentication
++authenticationId
try {
await cancelLogin()
await previousAuthentication?.catch(() => undefined)
if (authenticating.value === flow) authenticate(flow)
} finally {
reopeningBrowser.value = false
}
}
function resetAuthentication(cancelActive: boolean) {
const wasAuthenticating = authenticating.value !== null
++authenticationId
activeAuthentication = undefined
authenticating.value = null
reopeningBrowser.value = false
if (cancelActive && wasAuthenticating) void cancelLogin()
}
function handleHide() {
resetAuthentication(true)
finish(false)
}
function openSupport() {
openUrl('https://support.modrinth.com')
}
const messages = defineMessages({
header: {
id: 'modal.modrinth-account-required.header',
defaultMessage: 'Account required',
},
signingInHeader: {
id: 'modal.modrinth-account-required.signing-in-header',
defaultMessage: 'Signing in',
},
signInHeading: {
id: 'modal.modrinth-account-required.sign-in-heading',
defaultMessage: 'Sign in to a Modrinth account',
},
description: {
id: 'modal.modrinth-account-required.description',
defaultMessage:
"You'll need to sign into your Modrinth account before you can use this feature.",
},
createAccountButton: {
id: 'modal.modrinth-account-required.create-account-button',
defaultMessage: 'Create an account',
},
signInButton: {
id: 'modal.modrinth-account-required.sign-in-button',
defaultMessage: 'Sign in to Modrinth',
},
continueInBrowserHeading: {
id: 'modal.modrinth-account-required.continue-in-browser-heading',
defaultMessage: 'Continue in your browser',
},
browserDescription: {
id: 'modal.modrinth-account-required.browser-description',
defaultMessage:
'A new tab opened to sign in. Complete the sign in there, then return to the app.',
},
waitingForBrowser: {
id: 'modal.modrinth-account-required.waiting-for-browser',
defaultMessage: 'Waiting for browser confirmation...',
},
cancelButton: {
id: 'modal.modrinth-account-required.cancel-button',
defaultMessage: 'Cancel',
},
openBrowserAgainButton: {
id: 'modal.modrinth-account-required.open-browser-again-button',
defaultMessage: 'Open browser again',
},
supportPrompt: {
id: 'modal.modrinth-account-required.support-prompt',
defaultMessage: 'Having trouble signing in? <support>Get support</support>',
},
})
defineExpose({ show, showSigningIn })
</script>
@@ -8,6 +8,7 @@
:diffs="normalizedDiffs"
:version-date="versionDate"
:show-external-warnings="showExternalWarnings"
:external-warning-description="formatMessage(messages.externalWarningDescription)"
:confirm-label="formatMessage(commonMessages.updateButton)"
:confirm-icon="DownloadIcon"
:removed-label="formatMessage(messages.removed)"
@@ -76,10 +77,15 @@ const { formatMessage } = useVIntl()
const { startInstallingServer, stopInstallingServer } = injectServerInstall()
type UpdateCompleteCallback = () => void | Promise<void>
defineProps<{
const { showExternalWarnings } = defineProps<{
showExternalWarnings?: boolean
}>()
const emit = defineEmits<{
cancel: []
complete: []
}>()
const diffModal = ref<InstanceType<typeof ContentDiffModal>>()
const instance = ref<GameInstance | null>(null)
const onUpdateComplete = ref<UpdateCompleteCallback>(() => {})
@@ -87,16 +93,16 @@ const diffs = ref<DependencyDiff[]>([])
const modpackVersionId = ref<string | null>(null)
const modpackVersion = ref<Version | null>(null)
const normalizedDiffs = computed<ContentDiffItem[]>(() =>
diffs.value.map((diff) => ({
const normalizedDiffs = computed<ContentDiffItem[]>(() => {
return diffs.value.map((diff) => ({
type: diff.type,
external: Boolean(diff.fileName && !diff.project),
projectName: diff.project?.title,
fileName: diff.fileName,
currentVersionName: diff.currentVersion?.version_number,
newVersionName: diff.newVersion?.version_number,
})),
)
}))
})
const versionDate = computed(() =>
modpackVersion.value?.date_published
@@ -242,7 +248,6 @@ async function checkUpdateAvailable(inst: GameInstance): Promise<DependencyDiff[
}
async function handleUpdate() {
hide()
const serverProjectId = instance.value?.link?.project_id
if (serverProjectId) startInstallingServer(serverProjectId)
try {
@@ -255,11 +260,12 @@ async function handleUpdate() {
console.error('Error updating instance:', error)
} finally {
if (serverProjectId) stopInstallingServer(serverProjectId)
emit('complete')
}
}
function handleDecline() {
hide()
emit('cancel')
}
function show(
@@ -293,12 +299,17 @@ const messages = defineMessages({
updateRequiredDescription: {
id: 'app.modal.update-to-play.update-required-description',
defaultMessage:
'An update is required to play {name}. Please update to the latest version to launch the game.',
'An update is required to play {name}. Please update to latest version to launch the game.',
},
removed: {
id: 'app.modal.update-to-play.removed',
defaultMessage: 'Removed',
},
externalWarningDescription: {
id: 'app.modal.update-to-play.server-modpack-unknown-files-description',
defaultMessage:
'This server modpack update contains files that arent published on Modrinth. We strongly recommend only installing files from sources you trust.',
},
})
const hasUpdate = computed(() => {
@@ -1,335 +0,0 @@
<script setup lang="ts">
import { Combobox, defineMessages, ThemeSelector, Toggle, useVIntl } from '@modrinth/ui'
import { ref, watch } from 'vue'
import { get, set } from '@/helpers/settings.ts'
import { getOS } from '@/helpers/utils'
import { useTheming } from '@/store/state'
import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
const themeStore = useTheming()
const { formatMessage } = useVIntl()
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
const messages = defineMessages({
colorThemeTitle: {
id: 'app.appearance-settings.color-theme.title',
defaultMessage: 'Color theme',
},
colorThemeDescription: {
id: 'app.appearance-settings.color-theme.description',
defaultMessage: 'Select your preferred color theme for Modrinth App.',
},
advancedRenderingTitle: {
id: 'app.appearance-settings.advanced-rendering.title',
defaultMessage: 'Advanced rendering',
},
advancedRenderingDescription: {
id: 'app.appearance-settings.advanced-rendering.description',
defaultMessage:
'Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering.',
},
hideNametagTitle: {
id: 'app.appearance-settings.hide-nametag.title',
defaultMessage: 'Hide nametag',
},
hideNametagDescription: {
id: 'app.appearance-settings.hide-nametag.description',
defaultMessage: 'Disables the nametag above your player on the skins page.',
},
nativeDecorationsTitle: {
id: 'app.appearance-settings.native-decorations.title',
defaultMessage: 'Native decorations',
},
nativeDecorationsDescription: {
id: 'app.appearance-settings.native-decorations.description',
defaultMessage: 'Use system window frame (app restart required).',
},
minimizeLauncherTitle: {
id: 'app.appearance-settings.minimize-launcher.title',
defaultMessage: 'Minimize launcher',
},
minimizeLauncherDescription: {
id: 'app.appearance-settings.minimize-launcher.description',
defaultMessage: 'Minimize the launcher when a Minecraft process starts.',
},
defaultLandingPageTitle: {
id: 'app.appearance-settings.default-landing-page.title',
defaultMessage: 'Default landing page',
},
defaultLandingPageDescription: {
id: 'app.appearance-settings.default-landing-page.description',
defaultMessage: 'Change the page to which the launcher opens on.',
},
defaultLandingPageHome: {
id: 'app.appearance-settings.default-landing-page.home',
defaultMessage: 'Home',
},
defaultLandingPageLibrary: {
id: 'app.appearance-settings.default-landing-page.library',
defaultMessage: 'Library',
},
selectOption: {
id: 'app.appearance-settings.select-option',
defaultMessage: 'Select an option',
},
jumpBackIntoWorldsTitle: {
id: 'app.appearance-settings.jump-back-into-worlds.title',
defaultMessage: 'Jump back into worlds',
},
jumpBackIntoWorldsDescription: {
id: 'app.appearance-settings.jump-back-into-worlds.description',
defaultMessage: 'Includes recent worlds in the "Jump back in" section on the Home page.',
},
toggleSidebarTitle: {
id: 'app.appearance-settings.toggle-sidebar.title',
defaultMessage: 'Toggle sidebar',
},
toggleSidebarDescription: {
id: 'app.appearance-settings.toggle-sidebar.description',
defaultMessage: 'Enables the ability to toggle the sidebar.',
},
unknownPackWarningTitle: {
id: 'app.appearance-settings.unknown-pack-warning.title',
defaultMessage: 'Warn me before installing unknown modpacks',
},
unknownPackWarningDescription: {
id: 'app.appearance-settings.unknown-pack-warning.description',
defaultMessage:
"If you attempt to install a Modrinth Pack file (.mrpack) that isn't hosted on Modrinth, we'll make sure you understand the risks before installing it.",
},
skipNonEssentialWarningsTitle: {
id: 'app.appearance-settings.skip-non-essential-warnings.title',
defaultMessage: 'Skip non-essential warnings',
},
skipNonEssentialWarningsDescription: {
id: 'app.appearance-settings.skip-non-essential-warnings.description',
defaultMessage:
'Automatically skips low-risk confirmations like duplicate modpack installs, normal content deletion, bulk updates, unlinking modpacks, and repair prompts. Dangerous warnings will still be shown.',
},
showPlayTimeTitle: {
id: 'app.appearance-settings.show-play-time.title',
defaultMessage: 'Show play time',
},
showPlayTimeDescription: {
id: 'app.appearance-settings.show-play-time.description',
defaultMessage: `Displays how much time you've spent playing an instance.`,
},
})
const os = ref(await getOS())
const settings = ref(await get())
watch(
settings,
async () => {
await set(settings.value)
},
{ deep: true },
)
</script>
<template>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.colorThemeTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.colorThemeDescription) }}</p>
<ThemeSelector
:update-color-theme="
(theme: ColorTheme) => {
themeStore.setThemeState(theme)
settings.theme = theme
}
"
:current-theme="settings.theme"
:theme-options="themeStore.getThemeOptions()"
system-theme-color="system"
/>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.advancedRenderingTitle) }}
</h2>
<p class="m-0 mt-1">
{{ formatMessage(messages.advancedRenderingDescription) }}
</p>
</div>
<Toggle
id="advanced-rendering"
:model-value="themeStore.advancedRendering"
@update:model-value="
(e) => {
themeStore.advancedRendering = !!e
settings.advanced_rendering = themeStore.advancedRendering
}
"
/>
</div>
<div v-if="os !== 'MacOS'" class="mt-6 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.nativeDecorationsTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.nativeDecorationsDescription) }}</p>
</div>
<Toggle id="native-decorations" v-model="settings.native_decorations" />
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.minimizeLauncherTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.minimizeLauncherDescription) }}</p>
</div>
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.showPlayTimeTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.showPlayTimeDescription) }}</p>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(showPlayTimeFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(showPlayTimeFlag)
themeStore.featureFlags[showPlayTimeFlag] = newValue
settings.feature_flags[showPlayTimeFlag] = newValue
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.hideNametagTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.hideNametagDescription) }}</p>
</div>
<Toggle
id="hide-nametag-skins-page"
:model-value="themeStore.hideNametagSkinsPage"
@update:model-value="
(e) => {
themeStore.hideNametagSkinsPage = !!e
settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.defaultLandingPageTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.defaultLandingPageDescription) }}</p>
</div>
<Combobox
id="opening-page"
v-model="settings.default_page"
name="Opening page dropdown"
class="max-w-40"
:options="[
{
value: 'Home',
label: formatMessage(messages.defaultLandingPageHome),
},
{
value: 'Library',
label: formatMessage(messages.defaultLandingPageLibrary),
},
]"
:display-value="settings.default_page ?? 'Select an option'"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.jumpBackIntoWorldsTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}</p>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(worldsInHomeFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(worldsInHomeFlag)
themeStore.featureFlags[worldsInHomeFlag] = newValue
settings.feature_flags[worldsInHomeFlag] = newValue
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.unknownPackWarningTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.unknownPackWarningDescription) }}</p>
</div>
<Toggle
:model-value="!themeStore.getFeatureFlag(skipUnknownPackWarningFlag)"
@update:model-value="
(e) => {
const warnBeforeUnknownPackInstall = !!e
const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
themeStore.featureFlags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
settings.feature_flags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.skipNonEssentialWarningsTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.skipNonEssentialWarningsDescription) }}</p>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.toggleSidebarTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.toggleSidebarDescription) }}</p>
</div>
<Toggle
id="toggle-sidebar"
:model-value="settings.toggle_sidebar"
@update:model-value="
(e) => {
settings.toggle_sidebar = !!e
themeStore.toggleSidebar = settings.toggle_sidebar
}
"
/>
</div>
</template>
@@ -1,186 +0,0 @@
<script setup lang="ts">
import { injectNotificationManager, Slider, StyledInput, Toggle } from '@modrinth/ui'
import { ref, watch } from 'vue'
import useMemorySlider from '@/composables/useMemorySlider'
import { get, set } from '@/helpers/settings.ts'
const { handleError } = injectNotificationManager()
const fetchSettings = await get()
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
fetchSettings.envVars = fetchSettings.custom_env_vars.map((x) => x.join('=')).join(' ')
const settings = ref(fetchSettings)
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
maxMemory: number
snapPoints: number[]
}
watch(
settings,
async () => {
const setSettings = JSON.parse(JSON.stringify(settings.value))
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
setSettings.custom_env_vars = setSettings.envVars
.trim()
.split(/\s+/)
.filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
if (!setSettings.hooks.pre_launch) {
setSettings.hooks.pre_launch = null
}
if (!setSettings.hooks.wrapper) {
setSettings.hooks.wrapper = null
}
if (!setSettings.hooks.post_exit) {
setSettings.hooks.post_exit = null
}
if (!setSettings.custom_dir) {
setSettings.custom_dir = null
}
await set(setSettings)
},
{ deep: true },
)
</script>
<template>
<div>
<div class="flex flex-col gap-6">
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-1">
<h3 class="m-0 text-lg font-semibold text-contrast">Fullscreen</h3>
<p class="m-0 leading-tight">
Overwrites the options.txt file to start in full screen when launched.
</p>
</div>
<Toggle id="fullscreen" v-model="settings.force_fullscreen" />
</div>
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-1">
<h3 class="m-0 text-lg font-semibold text-contrast">Width</h3>
<p class="m-0 leading-tight">The width of the game window when launched.</p>
</div>
<StyledInput
id="width"
v-model="settings.game_resolution[0]"
:disabled="settings.force_fullscreen"
autocomplete="off"
type="number"
placeholder="Enter width..."
/>
</div>
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-1">
<h3 class="m-0 text-lg font-semibold text-contrast">Height</h3>
<p class="m-0 leading-tight">The height of the game window when launched.</p>
</div>
<StyledInput
id="height"
v-model="settings.game_resolution[1]"
:disabled="settings.force_fullscreen"
autocomplete="off"
type="number"
placeholder="Enter height..."
/>
</div>
</div>
<hr class="my-6 bg-button-border border-none h-[1px]" />
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">Memory allocated</h2>
<Slider
id="max-memory"
v-model="settings.memory.maximum"
:min="512"
:max="maxMemory"
:step="64"
:snap-points="snapPoints"
:snap-range="512"
unit="MB"
/>
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">Java arguments</h2>
<StyledInput
id="java-args"
v-model="settings.launchArgs"
autocomplete="off"
type="text"
placeholder="Enter java arguments..."
wrapper-class="w-full"
/>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">Environmental variables</h2>
<StyledInput
id="env-vars"
v-model="settings.envVars"
autocomplete="off"
type="text"
placeholder="Enter environmental variables..."
wrapper-class="w-full"
/>
</div>
</div>
<hr class="my-6 bg-button-border border-none h-[1px]" />
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">Pre launch hook</h3>
<StyledInput
id="pre-launch"
v-model="settings.hooks.pre_launch"
autocomplete="off"
type="text"
placeholder="Enter pre-launch command..."
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">Ran before the instance is launched.</p>
</div>
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">Wrapper hook</h3>
<StyledInput
id="wrapper"
v-model="settings.hooks.wrapper"
autocomplete="off"
type="text"
placeholder="Enter wrapper command..."
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">Wrapper command for launching Minecraft.</p>
</div>
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">Post exit hook</h3>
<StyledInput
id="post-exit"
v-model="settings.hooks.post_exit"
autocomplete="off"
type="text"
placeholder="Enter post-exit command..."
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">Ran after the game closes.</p>
</div>
</div>
</div>
</template>
@@ -1,63 +0,0 @@
<script setup lang="ts">
import { Toggle } from '@modrinth/ui'
import { ref, watch } from 'vue'
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
import { get, set } from '@/helpers/settings.ts'
const settings = ref(await get())
watch(
settings,
async () => {
if (settings.value.telemetry) {
optInAnalytics()
} else {
optOutAnalytics()
}
await set(settings.value)
},
{ deep: true },
)
</script>
<template>
<div class="flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">Personalized ads</h2>
<p class="m-0 mt-1 text-sm">
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
option, you opt out and ads will no longer be shown based on your interests.
</p>
</div>
<Toggle id="personalized-ads" v-model="settings.personalized_ads" />
</div>
<div class="mt-4 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">Telemetry</h2>
<p class="m-0 mt-1 text-sm">
Modrinth collects anonymized analytics and usage data to improve our user experience and
customize your experience. By disabling this option, you opt out and your data will no
longer be collected.
</p>
</div>
<Toggle id="opt-out-analytics" v-model="settings.telemetry" />
</div>
<div class="mt-4 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">Discord RPC</h2>
<p class="m-0 mt-1 text-sm">
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to no
longer show up as a game or app you are using on your Discord profile.
</p>
<p class="m-0 mt-2 text-sm">
Note: This will not prevent any instance-specific Discord Rich Presence integrations, such
as those added by mods. (app restart required to take effect)
</p>
</div>
<Toggle id="disable-discord-rpc" v-model="settings.discord_rpc" />
</div>
</template>
@@ -1,168 +0,0 @@
<script setup>
import { BoxIcon, FolderOpenIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, Slider, StyledInput } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref, watch } from 'vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { purge_cache_types } from '@/helpers/cache.js'
import { get, set } from '@/helpers/settings.ts'
import { showAppDbBackupsFolder } from '@/helpers/utils.js'
import { useTheming } from '@/store/state'
const { handleError } = injectNotificationManager()
const themeStore = useTheming()
const settings = ref(await get())
const purgeCacheConfirmModal = ref(null)
watch(
settings,
async () => {
const setSettings = JSON.parse(JSON.stringify(settings.value))
if (!setSettings.custom_dir) {
setSettings.custom_dir = null
}
await set(setSettings)
},
{ deep: true },
)
async function purgeCache() {
await purge_cache_types([
'project',
'project_v3',
'version',
'user',
'team',
'organization',
'file',
'loader_manifest',
'minecraft_manifest',
'categories',
'report_types',
'loaders',
'game_versions',
'donation_platforms',
'file_hash',
'file_update',
'search_results',
'search_results_v3',
]).catch(handleError)
}
function handlePurgeCacheClick() {
if (themeStore.getFeatureFlag('skip_non_essential_warnings')) {
void purgeCache()
return
}
purgeCacheConfirmModal.value?.show()
}
async function openDbBackupsFolder() {
await showAppDbBackupsFolder().catch(handleError)
}
async function findLauncherDir() {
const newDir = await open({
multiple: false,
directory: true,
title: 'Select a new app directory',
})
if (newDir) {
settings.value.custom_dir = newDir
}
}
</script>
<template>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">App directory</h2>
<StyledInput
id="appDir"
v-model="settings.custom_dir"
:icon="BoxIcon"
type="text"
wrapper-class="w-full"
>
<template #right>
<ButtonStyled circular>
<button class="ml-1.5" @click="findLauncherDir">
<FolderSearchIcon />
</button>
</ButtonStyled>
</template>
</StyledInput>
<p class="m-0 leading-tight text-secondary">
The directory where the launcher stores all of its files. Changes will be applied after
restarting the launcher.
</p>
</div>
<div class="flex flex-col gap-2.5">
<ConfirmModalWrapper
ref="purgeCacheConfirmModal"
title="Are you sure you want to purge the cache?"
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
:has-to-type="false"
proceed-label="Purge cache"
:show-ad-on-close="false"
@proceed="purgeCache"
/>
<h2 class="m-0 text-lg font-semibold text-contrast">App cache</h2>
<button id="purge-cache" class="btn min-w-max" @click="handlePurgeCacheClick">
<TrashIcon />
Purge cache
</button>
<p class="m-0 leading-tight text-secondary">
The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
app to reload data. This may slow down the app temporarily.
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast mt-4">Maximum concurrent downloads</h2>
<Slider
id="max-downloads"
v-model="settings.max_concurrent_downloads"
:min="1"
:max="10"
:step="1"
/>
<p class="m-0 leading-tight text-secondary">
The maximum amount of files the launcher can download at the same time. Set this to a lower
value if you have a poor internet connection. (app restart required to take effect)
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">Maximum concurrent writes</h2>
<Slider
id="max-writes"
v-model="settings.max_concurrent_writes"
:min="1"
:max="50"
:step="1"
/>
<p class="m-0 leading-tight text-secondary">
The maximum amount of files the launcher can write to the disk at once. Set this to a lower
value if you are frequently getting I/O errors. (app restart required to take effect)
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">App database backups</h2>
<button id="open-db-backups-folder" class="btn min-w-max" @click="openDbBackupsFolder">
<FolderOpenIcon />
Open backups folder
</button>
<p class="m-0 leading-tight text-secondary">
Backups of important app data are stored here in case you need to recover them later.
</p>
</div>
</div>
</template>
@@ -0,0 +1,116 @@
<script setup lang="ts">
import { Settings2Icon } from '@modrinth/assets'
import {
ButtonStyled,
defineMessages,
injectNotificationManager,
injectPageContext,
Toggle,
useVIntl,
} from '@modrinth/ui'
import { ref, watch } from 'vue'
import { open_ads_consent_preferences } from '@/helpers/ads.js'
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
import { get, set } from '@/helpers/settings.ts'
const { formatMessage } = useVIntl()
const { handleError } = injectNotificationManager()
const { adConsentAvailable } = injectPageContext()
const settings = ref(await get())
const messages = defineMessages({
adsConsentTitle: {
id: 'app.ads-consent.title',
defaultMessage: 'Your privacy and how ads support Modrinth',
},
adsConsentIntro: {
id: 'app.settings.privacy.ads-consent.intro',
defaultMessage:
'Ads make Modrinth possible and fund creator payouts. Our partners may store or access cookies in the app to personalize ads and measure performance. You can opt out or manage your preferences below.',
},
adsConsentManage: {
id: 'app.ads-consent.manage',
defaultMessage: 'Manage preferences',
},
telemetryTitle: {
id: 'app.settings.privacy.telemetry.title',
defaultMessage: 'Telemetry',
},
telemetryDescription: {
id: 'app.settings.privacy.telemetry.description',
defaultMessage:
'Modrinth collects anonymized analytics and usage data to improve our user experience and customize your experience. By disabling this option, you opt out and your data will no longer be collected.',
},
discordRichPresenceTitle: {
id: 'app.settings.privacy.discord-rich-presence.title',
defaultMessage: 'Discord Rich Presence',
},
discordRichPresenceDescription: {
id: 'app.settings.privacy.discord-rich-presence.description',
defaultMessage:
'Show Modrinth App as your current activity on Discord. This does not affect Rich Presence added to instances by mods. Requires an app restart.',
},
})
async function manageAdsPreferences() {
await open_ads_consent_preferences().catch(handleError)
}
watch(
settings,
async () => {
if (settings.value.telemetry) {
optInAnalytics()
} else {
optOutAnalytics()
}
await set(settings.value)
},
{ deep: true },
)
</script>
<template>
<div v-if="adConsentAvailable">
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.adsConsentTitle) }}
</h2>
<div class="mt-2 flex flex-col gap-2.5 items-start">
<ButtonStyled>
<button class="!shadow-none" @click="manageAdsPreferences">
<Settings2Icon aria-hidden="true" />
{{ formatMessage(messages.adsConsentManage) }}
</button>
</ButtonStyled>
<div>
{{ formatMessage(messages.adsConsentIntro) }}
</div>
</div>
</div>
<div class="mt-8 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.telemetryTitle) }}
</h2>
<p class="m-0 mt-1">
{{ formatMessage(messages.telemetryDescription) }}
</p>
</div>
<Toggle id="opt-out-analytics" v-model="settings.telemetry" />
</div>
<div class="mt-4 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.discordRichPresenceTitle) }}
</h2>
<p class="m-0 mt-1">
{{ formatMessage(messages.discordRichPresenceDescription) }}
</p>
</div>
<Toggle id="disable-discord-rpc" v-model="settings.discord_rpc" />
</div>
</template>
@@ -0,0 +1,70 @@
<template>
<AccountProfileSettings
ref="profileSettings"
:patch-user="patchUser"
:change-avatar="changeAvatar"
:delete-avatar="deleteAvatar"
:get-authenticated-user="getAuthenticatedUser"
@profile-link-click="handleProfileLinkClick"
/>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { AccountProfileSettings, injectAuth } from '@modrinth/ui'
import { inject, onBeforeUnmount, onMounted, ref } from 'vue'
import {
change_user_avatar,
delete_user_avatar,
get_user_profile,
patch_user,
} from '@/helpers/users'
import { appSettingsModalContextKey } from '@/providers/app-settings-modal'
const settingsModal = inject(appSettingsModalContextKey, null)
const auth = injectAuth()
const profileSettings = ref<InstanceType<typeof AccountProfileSettings> | null>(null)
onMounted(() => {
settingsModal?.registerUnsavedChangesController({
hasChanges: () => profileSettings.value?.hasChanges ?? false,
getOriginal: () => profileSettings.value?.originalState ?? {},
getModified: () => profileSettings.value?.modifiedState ?? {},
isSaving: () => profileSettings.value?.saving ?? false,
reset: () => profileSettings.value?.reset(),
save: () => profileSettings.value?.save(),
})
})
onBeforeUnmount(() => {
settingsModal?.registerUnsavedChangesController(null)
})
function handleProfileLinkClick(event: MouseEvent): void {
if (settingsModal && !settingsModal.close()) {
event.preventDefault()
}
}
function patchUser(
userId: string,
patch: Partial<Pick<Labrinth.Users.v2.User, 'bio' | 'username'>>,
): Promise<void> {
return patch_user(userId, patch)
}
async function changeAvatar(userId: string, file: Blob, extension: string): Promise<void> {
await change_user_avatar(userId, new Uint8Array(await file.arrayBuffer()), extension)
}
function deleteAvatar(userId: string): Promise<void> {
return delete_user_avatar(userId)
}
function getAuthenticatedUser(): Promise<Labrinth.Users.v3.User> {
const userId = auth.user.value?.id
if (!userId) throw new Error('Cannot refresh a signed-out user.')
return get_user_profile(userId)
}
</script>
@@ -0,0 +1,20 @@
<template>
<AccountSocialSettings
:get-blocked-users="get_blocked_users"
:get-users="getUsers"
:unblock-user="unblock_user"
/>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { AccountSocialSettings, injectModrinthClient } from '@modrinth/ui'
import { get_blocked_users, unblock_user } from '@/helpers/users'
const client = injectModrinthClient()
function getUsers(userIds: string[]): Promise<Labrinth.Users.v2.User[]> {
return client.labrinth.users_v2.getMultiple(userIds)
}
</script>
@@ -0,0 +1,108 @@
<script setup lang="ts">
import { defineMessages, ThemeSelector, Toggle, useVIntl } from '@modrinth/ui'
import { computed, ref, watch } from 'vue'
import { get, set } from '@/helpers/settings.ts'
import { getOS } from '@/helpers/utils'
import { useTheming } from '@/store/state'
import type { ColorTheme } from '@/store/theme.ts'
const themeStore = useTheming()
const { formatMessage } = useVIntl()
const messages = defineMessages({
colorThemeTitle: {
id: 'app.appearance-settings.color-theme.title',
defaultMessage: 'Color theme',
},
colorThemeDescription: {
id: 'app.appearance-settings.color-theme.description',
defaultMessage: 'Choose the color theme used by Modrinth App.',
},
advancedRenderingTitle: {
id: 'app.appearance-settings.advanced-rendering.title',
defaultMessage: 'Advanced rendering',
},
advancedRenderingDescription: {
id: 'app.appearance-settings.advanced-rendering.description',
defaultMessage:
'Enable visual effects such as background blur. This may reduce performance without hardware acceleration.',
},
nativeDecorationsTitle: {
id: 'app.appearance-settings.native-decorations.title',
defaultMessage: 'System window frame',
},
nativeDecorationsDescription: {
id: 'app.appearance-settings.native-decorations.description',
defaultMessage:
"Use your operating system's title bar and window controls. Requires an app restart.",
},
})
const os = ref(await getOS())
const settings = ref(await get())
const themeOptions = computed(() =>
themeStore
.getThemeOptions()
.filter((theme) => theme !== 'retro' || themeStore.devMode || settings.value.theme === 'retro'),
)
watch(
settings,
async () => {
await set(settings.value)
},
{ deep: true },
)
</script>
<template>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.colorThemeTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.colorThemeDescription) }}</p>
<ThemeSelector
:update-color-theme="
(theme: ColorTheme) => {
themeStore.setThemeState(theme)
settings.theme = theme
}
"
:current-theme="settings.theme"
:theme-options="themeOptions"
system-theme-color="system"
/>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.advancedRenderingTitle) }}
</h2>
<p class="m-0 mt-1">
{{ formatMessage(messages.advancedRenderingDescription) }}
</p>
</div>
<Toggle
id="advanced-rendering"
:model-value="themeStore.advancedRendering"
@update:model-value="
(e) => {
themeStore.advancedRendering = !!e
settings.advanced_rendering = themeStore.advancedRendering
}
"
/>
</div>
<div v-if="os !== 'MacOS'" class="mt-6 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.nativeDecorationsTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.nativeDecorationsDescription) }}</p>
</div>
<Toggle id="native-decorations" v-model="settings.native_decorations" />
</div>
</template>
@@ -0,0 +1,302 @@
<script setup lang="ts">
import { Chips, defineMessages, Toggle, useVIntl } from '@modrinth/ui'
import { ref, watch } from 'vue'
import { get, set } from '@/helpers/settings.ts'
import { useTheming } from '@/store/state'
import type { FeatureFlag } from '@/store/theme.ts'
const themeStore = useTheming()
const { formatMessage } = useVIntl()
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
type LandingPage = 'Home' | 'Library'
const landingPageOptions: LandingPage[] = ['Home', 'Library']
const messages = defineMessages({
startupAndNavigationTitle: {
id: 'app.behavior-settings.startup-and-navigation.title',
defaultMessage: 'Startup and navigation',
},
contentTitle: {
id: 'app.behavior-settings.content.title',
defaultMessage: 'Home and content',
},
confirmationsTitle: {
id: 'app.behavior-settings.confirmations.title',
defaultMessage: 'Confirmations',
},
minimizeLauncherTitle: {
id: 'app.appearance-settings.minimize-launcher.title',
defaultMessage: 'Minimize app',
},
minimizeLauncherDescription: {
id: 'app.appearance-settings.minimize-launcher.description',
defaultMessage: 'Minimize Modrinth App when Minecraft starts.',
},
defaultLandingPageTitle: {
id: 'app.appearance-settings.default-landing-page.title',
defaultMessage: 'Default landing page',
},
defaultLandingPageDescription: {
id: 'app.appearance-settings.default-landing-page.description',
defaultMessage: 'Choose the page shown when Modrinth App opens.',
},
defaultLandingPageHome: {
id: 'app.appearance-settings.default-landing-page.home',
defaultMessage: 'Home',
},
defaultLandingPageLibrary: {
id: 'app.appearance-settings.default-landing-page.library',
defaultMessage: 'Library',
},
toggleSidebarTitle: {
id: 'app.appearance-settings.toggle-sidebar.title',
defaultMessage: 'Hide right sidebar',
},
toggleSidebarDescription: {
id: 'app.appearance-settings.toggle-sidebar.description',
defaultMessage: 'Hide the right sidebar by default and add a button to show or hide it.',
},
jumpBackIntoWorldsTitle: {
id: 'app.appearance-settings.jump-back-into-worlds.title',
defaultMessage: 'Jump back into worlds',
},
jumpBackIntoWorldsDescription: {
id: 'app.appearance-settings.jump-back-into-worlds.description',
defaultMessage: 'Show recently played worlds in the "Jump back in" section on the Home page.',
},
showPlayTimeTitle: {
id: 'app.appearance-settings.show-play-time.title',
defaultMessage: 'Show play time',
},
showPlayTimeDescription: {
id: 'app.appearance-settings.show-play-time.description',
defaultMessage: `Show how long you've played each instance.`,
},
hideNametagTitle: {
id: 'app.appearance-settings.hide-nametag.title',
defaultMessage: 'Hide nametag',
},
hideNametagDescription: {
id: 'app.appearance-settings.hide-nametag.description',
defaultMessage: 'Hide your username above the player preview on the Skin selector page.',
},
unknownPackWarningTitle: {
id: 'app.appearance-settings.unknown-pack-warning.title',
defaultMessage: 'Warn me before installing unknown modpacks',
},
unknownPackWarningDescription: {
id: 'app.appearance-settings.unknown-pack-warning.description',
defaultMessage:
"Show a safety warning before installing a Modrinth Pack (.mrpack) that isn't hosted on Modrinth.",
},
skipNonEssentialWarningsTitle: {
id: 'app.appearance-settings.skip-non-essential-warnings.title',
defaultMessage: 'Skip non-essential warnings',
},
skipNonEssentialWarningsDescription: {
id: 'app.appearance-settings.skip-non-essential-warnings.description',
defaultMessage:
'Skip confirmations for low-risk actions such as duplicate installs, normal content deletion, bulk updates, unlinking, and repairs. Warnings for dangerous actions are always shown.',
},
})
function formatLandingPageLabel(page: LandingPage) {
switch (page) {
case 'Home':
return formatMessage(messages.defaultLandingPageHome)
case 'Library':
return formatMessage(messages.defaultLandingPageLibrary)
}
}
const settings = ref(await get())
watch(
settings,
async () => {
await set(settings.value)
},
{ deep: true },
)
</script>
<template>
<section>
<h2 class="m-0 text-xl font-semibold text-contrast">
{{ formatMessage(messages.startupAndNavigationTitle) }}
</h2>
<div class="mt-4 flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.defaultLandingPageTitle) }}
</h3>
<Chips
v-model="settings.default_page"
:items="landingPageOptions"
:format-label="formatLandingPageLabel"
:capitalize="false"
:aria-label="formatMessage(messages.defaultLandingPageTitle)"
/>
<p class="m-0">
{{ formatMessage(messages.defaultLandingPageDescription) }}
</p>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.minimizeLauncherTitle) }}
</h3>
<p class="m-0 mt-1">
{{ formatMessage(messages.minimizeLauncherDescription) }}
</p>
</div>
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.toggleSidebarTitle) }}
</h3>
<p class="m-0 mt-1">{{ formatMessage(messages.toggleSidebarDescription) }}</p>
</div>
<Toggle
id="toggle-sidebar"
:model-value="settings.toggle_sidebar"
@update:model-value="
(e) => {
settings.toggle_sidebar = !!e
themeStore.toggleSidebar = settings.toggle_sidebar
}
"
/>
</div>
</div>
</section>
<section class="mt-8 border-0 border-t border-solid border-divider pt-6">
<h2 class="m-0 text-xl font-semibold text-contrast">
{{ formatMessage(messages.contentTitle) }}
</h2>
<div class="mt-4 flex flex-col gap-6">
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.jumpBackIntoWorldsTitle) }}
</h3>
<p class="m-0 mt-1">
{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}
</p>
</div>
<Toggle
id="jump-back-into-worlds"
:model-value="themeStore.getFeatureFlag(worldsInHomeFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(worldsInHomeFlag)
themeStore.featureFlags[worldsInHomeFlag] = newValue
settings.feature_flags[worldsInHomeFlag] = newValue
}
"
/>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.showPlayTimeTitle) }}
</h3>
<p class="m-0 mt-1">{{ formatMessage(messages.showPlayTimeDescription) }}</p>
</div>
<Toggle
id="show-play-time"
:model-value="themeStore.getFeatureFlag(showPlayTimeFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(showPlayTimeFlag)
themeStore.featureFlags[showPlayTimeFlag] = newValue
settings.feature_flags[showPlayTimeFlag] = newValue
}
"
/>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.hideNametagTitle) }}
</h3>
<p class="m-0 mt-1">{{ formatMessage(messages.hideNametagDescription) }}</p>
</div>
<Toggle
id="hide-nametag-skins-page"
:model-value="themeStore.hideNametagSkinsPage"
@update:model-value="
(e) => {
themeStore.hideNametagSkinsPage = !!e
settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
}
"
/>
</div>
</div>
</section>
<section class="mt-8 border-0 border-t border-solid border-divider pt-6">
<h2 class="m-0 text-xl font-semibold text-contrast">
{{ formatMessage(messages.confirmationsTitle) }}
</h2>
<div class="mt-4 flex flex-col gap-6">
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.unknownPackWarningTitle) }}
</h3>
<p class="m-0 mt-1">
{{ formatMessage(messages.unknownPackWarningDescription) }}
</p>
</div>
<Toggle
id="warn-before-installing-unknown-modpacks"
:model-value="!themeStore.getFeatureFlag(skipUnknownPackWarningFlag)"
@update:model-value="
(e) => {
const warnBeforeUnknownPackInstall = !!e
const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
themeStore.featureFlags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
settings.feature_flags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
}
"
/>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.skipNonEssentialWarningsTitle) }}
</h3>
<p class="m-0 mt-1">
{{ formatMessage(messages.skipNonEssentialWarningsDescription) }}
</p>
</div>
<Toggle
id="skip-non-essential-warnings"
:model-value="themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
}
"
/>
</div>
</div>
</section>
</template>
@@ -25,7 +25,7 @@ watch(
)
</script>
<template>
<div class="flex flex-col gap-2.5 min-w-[600px]">
<div class="flex flex-col gap-2.5">
<div v-for="option in options" :key="option" class="flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast capitalize">
@@ -2,6 +2,7 @@
import {
Admonition,
AutoLink,
commonSettingsMessages,
IntlFormatted,
LanguageSelector,
languageSelectorMessages,
@@ -43,7 +44,9 @@ async function onLocaleChange(newLocale: string) {
</script>
<template>
<h2 class="m-0 text-lg font-semibold text-contrast">Language</h2>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(commonSettingsMessages.language) }}
</h2>
<Admonition type="warning" class="mt-2 mb-4">
{{ formatMessage(languageSelectorMessages.languageWarning, { platform }) }}
@@ -0,0 +1,333 @@
<script setup lang="ts">
import {
defineMessages,
injectNotificationManager,
Slider,
StyledInput,
Toggle,
useVIntl,
} from '@modrinth/ui'
import { ref, watch } from 'vue'
import useMemorySlider from '@/composables/useMemorySlider'
import { get, set } from '@/helpers/settings.ts'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const messages = defineMessages({
fullscreenTitle: {
id: 'app.settings.default-instance-options.fullscreen.title',
defaultMessage: 'Fullscreen',
},
fullscreenDescription: {
id: 'app.settings.default-instance-options.fullscreen.description',
defaultMessage: 'Start instances in fullscreen by updating their options.txt file.',
},
widthTitle: {
id: 'app.settings.default-instance-options.width.title',
defaultMessage: 'Width',
},
widthDescription: {
id: 'app.settings.default-instance-options.width.description',
defaultMessage: 'The width of the game window when launched.',
},
widthPlaceholder: {
id: 'app.settings.default-instance-options.width.placeholder',
defaultMessage: 'Enter width...',
},
heightTitle: {
id: 'app.settings.default-instance-options.height.title',
defaultMessage: 'Height',
},
heightDescription: {
id: 'app.settings.default-instance-options.height.description',
defaultMessage: 'The height of the game window when launched.',
},
heightPlaceholder: {
id: 'app.settings.default-instance-options.height.placeholder',
defaultMessage: 'Enter height...',
},
memoryAllocationTitle: {
id: 'app.settings.default-instance-options.memory-allocation.title',
defaultMessage: 'Memory allocation',
},
memoryAllocationDescription: {
id: 'app.settings.default-instance-options.memory-allocation.description',
defaultMessage: 'Maximum memory available to each instance.',
},
javaArgumentsTitle: {
id: 'app.settings.default-instance-options.java-arguments.title',
defaultMessage: 'Java arguments',
},
javaArgumentsPlaceholder: {
id: 'app.settings.default-instance-options.java-arguments.placeholder',
defaultMessage: 'Enter Java arguments...',
},
javaArgumentsDescription: {
id: 'app.settings.default-instance-options.java-arguments.description',
defaultMessage: 'Arguments passed to Java when launching an instance.',
},
environmentVariablesTitle: {
id: 'app.settings.default-instance-options.environment-variables.title',
defaultMessage: 'Environment variables',
},
environmentVariablesPlaceholder: {
id: 'app.settings.default-instance-options.environment-variables.placeholder',
defaultMessage: 'Enter environment variables...',
},
environmentVariablesDescription: {
id: 'app.settings.default-instance-options.environment-variables.description',
defaultMessage: 'Environment variables set when launching an instance.',
},
preLaunchHookTitle: {
id: 'app.settings.default-instance-options.pre-launch-hook.title',
defaultMessage: 'Pre-launch hook',
},
preLaunchHookPlaceholder: {
id: 'app.settings.default-instance-options.pre-launch-hook.placeholder',
defaultMessage: 'Enter pre-launch command...',
},
preLaunchHookDescription: {
id: 'app.settings.default-instance-options.pre-launch-hook.description',
defaultMessage: 'Runs before the instance starts.',
},
wrapperHookTitle: {
id: 'app.settings.default-instance-options.wrapper-hook.title',
defaultMessage: 'Wrapper hook',
},
wrapperHookPlaceholder: {
id: 'app.settings.default-instance-options.wrapper-hook.placeholder',
defaultMessage: 'Enter wrapper command...',
},
wrapperHookDescription: {
id: 'app.settings.default-instance-options.wrapper-hook.description',
defaultMessage: 'Command used to wrap the Minecraft launch process.',
},
postExitHookTitle: {
id: 'app.settings.default-instance-options.post-exit-hook.title',
defaultMessage: 'Post-exit hook',
},
postExitHookPlaceholder: {
id: 'app.settings.default-instance-options.post-exit-hook.placeholder',
defaultMessage: 'Enter post-exit command...',
},
postExitHookDescription: {
id: 'app.settings.default-instance-options.post-exit-hook.description',
defaultMessage: 'Runs after the game closes.',
},
})
const fetchSettings = await get()
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
fetchSettings.envVars = fetchSettings.custom_env_vars.map((x) => x.join('=')).join(' ')
const settings = ref(fetchSettings)
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
maxMemory: number
snapPoints: number[]
}
watch(
settings,
async () => {
const setSettings = JSON.parse(JSON.stringify(settings.value))
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
setSettings.custom_env_vars = setSettings.envVars
.trim()
.split(/\s+/)
.filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
if (!setSettings.hooks.pre_launch) {
setSettings.hooks.pre_launch = null
}
if (!setSettings.hooks.wrapper) {
setSettings.hooks.wrapper = null
}
if (!setSettings.hooks.post_exit) {
setSettings.hooks.post_exit = null
}
if (!setSettings.custom_dir) {
setSettings.custom_dir = null
}
await set(setSettings)
},
{ deep: true },
)
</script>
<template>
<div>
<div class="flex flex-col gap-6">
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-1">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.fullscreenTitle) }}
</h3>
<p class="m-0 leading-tight">
{{ formatMessage(messages.fullscreenDescription) }}
</p>
</div>
<Toggle id="fullscreen" v-model="settings.force_fullscreen" />
</div>
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-1">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.widthTitle) }}
</h3>
<p class="m-0 leading-tight">
{{ formatMessage(messages.widthDescription) }}
</p>
</div>
<StyledInput
id="width"
v-model="settings.game_resolution[0]"
:disabled="settings.force_fullscreen"
autocomplete="off"
type="number"
:placeholder="formatMessage(messages.widthPlaceholder)"
/>
</div>
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-1">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.heightTitle) }}
</h3>
<p class="m-0 leading-tight">
{{ formatMessage(messages.heightDescription) }}
</p>
</div>
<StyledInput
id="height"
v-model="settings.game_resolution[1]"
:disabled="settings.force_fullscreen"
autocomplete="off"
type="number"
:placeholder="formatMessage(messages.heightPlaceholder)"
/>
</div>
</div>
<hr class="my-6 bg-button-border border-none h-[1px]" />
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.memoryAllocationTitle) }}
</h2>
<Slider
id="max-memory"
v-model="settings.memory.maximum"
:min="512"
:max="maxMemory"
:step="64"
:snap-points="snapPoints"
:snap-range="512"
unit="MB"
/>
<p class="m-0 mt-1 leading-tight">
{{ formatMessage(messages.memoryAllocationDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.javaArgumentsTitle) }}
</h2>
<StyledInput
id="java-args"
v-model="settings.launchArgs"
autocomplete="off"
type="text"
:placeholder="formatMessage(messages.javaArgumentsPlaceholder)"
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">
{{ formatMessage(messages.javaArgumentsDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.environmentVariablesTitle) }}
</h2>
<StyledInput
id="env-vars"
v-model="settings.envVars"
autocomplete="off"
type="text"
:placeholder="formatMessage(messages.environmentVariablesPlaceholder)"
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">
{{ formatMessage(messages.environmentVariablesDescription) }}
</p>
</div>
</div>
<hr class="my-6 bg-button-border border-none h-[1px]" />
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.preLaunchHookTitle) }}
</h3>
<StyledInput
id="pre-launch"
v-model="settings.hooks.pre_launch"
autocomplete="off"
type="text"
:placeholder="formatMessage(messages.preLaunchHookPlaceholder)"
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">
{{ formatMessage(messages.preLaunchHookDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.wrapperHookTitle) }}
</h3>
<StyledInput
id="wrapper"
v-model="settings.hooks.wrapper"
autocomplete="off"
type="text"
:placeholder="formatMessage(messages.wrapperHookPlaceholder)"
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">
{{ formatMessage(messages.wrapperHookDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.postExitHookTitle) }}
</h3>
<StyledInput
id="post-exit"
v-model="settings.hooks.post_exit"
autocomplete="off"
type="text"
:placeholder="formatMessage(messages.postExitHookPlaceholder)"
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">
{{ formatMessage(messages.postExitHookDescription) }}
</p>
</div>
</div>
</div>
</template>
@@ -1,11 +1,19 @@
<script setup>
import { injectNotificationManager } from '@modrinth/ui'
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { ref } from 'vue'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import { get_java_versions, set_java_version } from '@/helpers/jre'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const messages = defineMessages({
javaLocation: {
id: 'app.settings.java-installations.location.title',
defaultMessage: 'Java {version, number} location',
},
})
const javaVersions = ref(await get_java_versions().catch(handleError))
async function updateJavaVersion(version) {
@@ -28,7 +36,7 @@ async function updateJavaVersion(version) {
class="flex flex-col gap-2.5"
>
<h2 class="m-0 text-lg font-semibold text-contrast" :class="{ 'mt-4': index !== 0 }">
Java {{ javaVersion }} location
{{ formatMessage(messages.javaLocation, { version: javaVersion }) }}
</h2>
<JavaSelector
:id="'java-selector-' + javaVersion"
@@ -0,0 +1,292 @@
<script setup>
import { BoxIcon, FolderOpenIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import {
ButtonStyled,
defineMessages,
injectNotificationManager,
Slider,
StyledInput,
Toggle,
useVIntl,
} from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref, watch } from 'vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { purge_cache_types } from '@/helpers/cache.js'
import { get, set } from '@/helpers/settings.ts'
import { showAppDbBackupsFolder } from '@/helpers/utils.js'
import { useTheming } from '@/store/state'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const themeStore = useTheming()
const settings = ref(await get())
const purgeCacheConfirmModal = ref(null)
const alwaysShowCopyDetailsFlag = 'always_show_copy_details'
const messages = defineMessages({
appDirectoryTitle: {
id: 'app.settings.resource-management.app-directory.title',
defaultMessage: 'App directory',
},
appDirectoryDescription: {
id: 'app.settings.resource-management.app-directory.description',
defaultMessage:
'Where Modrinth App stores instances and other files. Changes take effect after restarting the app.',
},
selectAppDirectory: {
id: 'app.settings.resource-management.app-directory.select',
defaultMessage: 'Select a new app directory',
},
browseAppDirectory: {
id: 'app.settings.resource-management.app-directory.browse',
defaultMessage: 'Browse for an app directory',
},
appCacheTitle: {
id: 'app.settings.resource-management.app-cache.title',
defaultMessage: 'App cache',
},
purgeCache: {
id: 'app.settings.resource-management.app-cache.purge',
defaultMessage: 'Purge cache',
},
purgeCacheConfirmTitle: {
id: 'app.settings.resource-management.app-cache.confirm.title',
defaultMessage: 'Purge the app cache?',
},
purgeCacheConfirmDescription: {
id: 'app.settings.resource-management.app-cache.confirm.description',
defaultMessage: 'The app may load more slowly until the cache is rebuilt.',
},
appCacheDescription: {
id: 'app.settings.resource-management.app-cache.description',
defaultMessage:
'Clear cached data and download it again from Modrinth. The app may load more slowly until the cache is rebuilt.',
},
maximumConcurrentDownloadsTitle: {
id: 'app.settings.resource-management.maximum-concurrent-downloads.title',
defaultMessage: 'Maximum concurrent downloads',
},
maximumConcurrentDownloadsDescription: {
id: 'app.settings.resource-management.maximum-concurrent-downloads.description',
defaultMessage:
'Number of files the app can download at once. Lower this if downloads are unreliable on your connection. Requires an app restart.',
},
maximumConcurrentWritesTitle: {
id: 'app.settings.resource-management.maximum-concurrent-writes.title',
defaultMessage: 'Maximum concurrent writes',
},
maximumConcurrentWritesDescription: {
id: 'app.settings.resource-management.maximum-concurrent-writes.description',
defaultMessage:
'Number of files the app can write to disk at once. Lower this if you frequently encounter I/O errors. Requires an app restart.',
},
alwaysShowCopyDetailsTitle: {
id: 'app.settings.resource-management.always-show-copy-details.title',
defaultMessage: 'Always show copy details',
},
alwaysShowCopyDetailsDescription: {
id: 'app.settings.resource-management.always-show-copy-details.description',
defaultMessage:
'Show the Copy details action while an install is queued or running. It is always available for failed or interrupted installs.',
},
appDatabaseBackupsTitle: {
id: 'app.settings.resource-management.app-database-backups.title',
defaultMessage: 'App database backups',
},
openBackupsFolder: {
id: 'app.settings.resource-management.app-database-backups.open-folder',
defaultMessage: 'Open backups folder',
},
appDatabaseBackupsDescription: {
id: 'app.settings.resource-management.app-database-backups.description',
defaultMessage:
'Backups of important app data are stored here in case you need to recover them later.',
},
})
watch(
settings,
async () => {
const setSettings = JSON.parse(JSON.stringify(settings.value))
if (!setSettings.custom_dir) {
setSettings.custom_dir = null
}
await set(setSettings)
},
{ deep: true },
)
async function purgeCache() {
await purge_cache_types([
'project',
'project_v3',
'version',
'user',
'team',
'organization',
'file',
'loader_manifest',
'minecraft_manifest',
'categories',
'report_types',
'loaders',
'game_versions',
'donation_platforms',
'file_hash',
'file_update',
'search_results',
'search_results_v3',
]).catch(handleError)
}
function handlePurgeCacheClick() {
if (themeStore.getFeatureFlag('skip_non_essential_warnings')) {
void purgeCache()
return
}
purgeCacheConfirmModal.value?.show()
}
async function openDbBackupsFolder() {
await showAppDbBackupsFolder().catch(handleError)
}
async function findLauncherDir() {
const newDir = await open({
multiple: false,
directory: true,
title: formatMessage(messages.selectAppDirectory),
})
if (newDir) {
settings.value.custom_dir = newDir
}
}
</script>
<template>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.appDirectoryTitle) }}
</h2>
<StyledInput
id="appDir"
v-model="settings.custom_dir"
:icon="BoxIcon"
type="text"
wrapper-class="w-full"
>
<template #right>
<ButtonStyled circular>
<button
v-tooltip="formatMessage(messages.browseAppDirectory)"
:aria-label="formatMessage(messages.browseAppDirectory)"
class="ml-1.5"
@click="findLauncherDir"
>
<FolderSearchIcon aria-hidden="true" />
</button>
</ButtonStyled>
</template>
</StyledInput>
<p class="m-0 leading-tight text-secondary">
{{ formatMessage(messages.appDirectoryDescription) }}
</p>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.alwaysShowCopyDetailsTitle) }}
</h2>
<p class="m-0 mt-1">
{{ formatMessage(messages.alwaysShowCopyDetailsDescription) }}
</p>
</div>
<Toggle
id="always-show-copy-details"
:model-value="themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)
themeStore.featureFlags[alwaysShowCopyDetailsFlag] = newValue
settings.feature_flags[alwaysShowCopyDetailsFlag] = newValue
}
"
/>
</div>
<div class="flex flex-col gap-2.5">
<ConfirmModalWrapper
ref="purgeCacheConfirmModal"
:title="formatMessage(messages.purgeCacheConfirmTitle)"
:description="formatMessage(messages.purgeCacheConfirmDescription)"
:has-to-type="false"
:proceed-label="formatMessage(messages.purgeCache)"
:show-ad-on-close="false"
@proceed="purgeCache"
/>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.appCacheTitle) }}
</h2>
<button id="purge-cache" class="btn min-w-max" @click="handlePurgeCacheClick">
<TrashIcon aria-hidden="true" />
{{ formatMessage(messages.purgeCache) }}
</button>
<p class="m-0 leading-tight text-secondary">
{{ formatMessage(messages.appCacheDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast mt-4">
{{ formatMessage(messages.maximumConcurrentDownloadsTitle) }}
</h2>
<Slider
id="max-downloads"
v-model="settings.max_concurrent_downloads"
:min="1"
:max="10"
:step="1"
/>
<p class="m-0 leading-tight text-secondary">
{{ formatMessage(messages.maximumConcurrentDownloadsDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.maximumConcurrentWritesTitle) }}
</h2>
<Slider
id="max-writes"
v-model="settings.max_concurrent_writes"
:min="1"
:max="50"
:step="1"
/>
<p class="m-0 leading-tight text-secondary">
{{ formatMessage(messages.maximumConcurrentWritesDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.appDatabaseBackupsTitle) }}
</h2>
<button id="open-db-backups-folder" class="btn min-w-max" @click="openDbBackupsFolder">
<FolderOpenIcon aria-hidden="true" />
{{ formatMessage(messages.openBackupsFolder) }}
</button>
<p class="m-0 leading-tight text-secondary">
{{ formatMessage(messages.appDatabaseBackupsDescription) }}
</p>
</div>
</div>
</template>
@@ -0,0 +1,82 @@
<template>
<NewModal ref="modal" :header="formatMessage(messages.header)" fade="danger" max-width="500px">
<Admonition type="critical" :header="formatMessage(messages.admonitionHeader)">
<IntlFormatted :message-id="messages.admonitionBody" :values="{ code: inviteCode }">
<template #monospace="{ children }">
<code class="font-mono"><component :is="() => children" /></code>
</template>
</IntlFormatted>
</Admonition>
<template #actions>
<div class="flex justify-end gap-2">
<ButtonStyled type="outlined">
<button @click="modal?.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="confirm">
<XIcon />
{{ formatMessage(messages.revokeButton) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
</template>
<script setup lang="ts">
import { XIcon } from '@modrinth/assets'
import {
Admonition,
ButtonStyled,
commonMessages,
defineMessages,
IntlFormatted,
NewModal,
useVIntl,
} from '@modrinth/ui'
import { ref } from 'vue'
const { formatMessage } = useVIntl()
const modal = ref<InstanceType<typeof NewModal>>()
const inviteCode = ref('')
const emit = defineEmits<{
revoke: [inviteCode: string]
}>()
function show(code: string) {
inviteCode.value = code
modal.value?.show()
}
function confirm() {
modal.value?.hide()
emit('revoke', inviteCode.value)
}
const messages = defineMessages({
header: {
id: 'instance.settings.sharing.revoke-invite.header',
defaultMessage: 'Revoke invite',
},
admonitionHeader: {
id: 'instance.settings.sharing.revoke-invite.admonition-header',
defaultMessage: 'This action cannot be undone',
},
admonitionBody: {
id: 'instance.settings.sharing.revoke-invite.admonition-body',
defaultMessage:
'The invite link <monospace>{code}</monospace> will stop working immediately. People who already joined will keep access.',
},
revokeButton: {
id: 'instance.settings.sharing.revoke-invite.confirm',
defaultMessage: 'Revoke invite',
},
})
defineExpose({ show })
</script>
@@ -0,0 +1,100 @@
<template>
<NewModal ref="modal" :header="formatMessage(messages.header)" fade="warning" max-width="500px">
<p class="m-0 text-secondary">
<IntlFormatted :message-id="messages.body" :values="{ instanceName }">
<template #bold="{ children }">
<span class="font-medium text-contrast"><component :is="() => children" /></span>
</template>
</IntlFormatted>
</p>
<template #actions>
<div class="flex justify-end gap-2">
<ButtonStyled type="outlined">
<button @click="handleCancel">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="handleGoToInstance">
{{ formatMessage(messages.instance) }}
<RightArrowIcon />
</button>
</ButtonStyled>
<ButtonStyled color="orange">
<button @click="handleInstallAnyway">
<DownloadIcon />
{{ formatMessage(messages.installAnyway) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
</template>
<script setup lang="ts">
import { DownloadIcon, RightArrowIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
commonMessages,
defineMessages,
IntlFormatted,
NewModal,
useVIntl,
} from '@modrinth/ui'
import { ref } from 'vue'
const { formatMessage } = useVIntl()
const messages = defineMessages({
header: {
id: 'app.instance.shared-instance-already-installed.header',
defaultMessage: 'Shared instance already installed',
},
body: {
id: 'app.instance.shared-instance-already-installed.body',
defaultMessage:
'This shared instance is already installed as <bold>{instanceName}</bold>. Are you sure you want to install another copy?',
},
instance: {
id: 'app.instance.shared-instance-already-installed.instance',
defaultMessage: 'Instance',
},
installAnyway: {
id: 'app.instance.shared-instance-already-installed.install-anyway',
defaultMessage: 'Install anyway',
},
})
const emit = defineEmits<{
(e: 'cancel' | 'go-to-instance' | 'install-anyway'): void
}>()
const modal = ref<InstanceType<typeof NewModal>>()
const instanceName = ref('')
function show(name: string) {
instanceName.value = name
modal.value?.show()
}
function handleCancel() {
modal.value?.hide()
emit('cancel')
}
function handleGoToInstance() {
modal.value?.hide()
emit('go-to-instance')
}
function handleInstallAnyway() {
modal.value?.hide()
emit('install-anyway')
}
defineExpose({
show,
})
</script>
@@ -0,0 +1,195 @@
<template>
<div v-if="canUnpublish" class="flex flex-col gap-2.5">
<span class="text-lg font-semibold text-contrast">{{ formatMessage(messages.title) }}</span>
<div>
<ButtonStyled color="orange">
<button :disabled="busy" @click="unpublishModal?.show()">
<SpinnerIcon v-if="unpublishing" class="animate-spin" />
<UnlinkIcon v-else class="size-5" />
{{ formatMessage(unpublishing ? messages.unpublishingButton : messages.unpublishButton) }}
</button>
</ButtonStyled>
</div>
<span class="text-primary">{{ formatMessage(messages.unpublishDescription) }}</span>
</div>
<div v-if="canUnlink" class="flex flex-col gap-2.5">
<span class="text-lg font-semibold text-contrast">{{
formatMessage(messages.linkedTitle)
}}</span>
<div>
<ButtonStyled color="orange">
<button :disabled="busy" @click="unlinkModal?.show()">
<SpinnerIcon v-if="unlinking" class="animate-spin" />
<UnlinkIcon v-else class="size-5" />
{{ formatMessage(unlinking ? messages.unlinkingButton : messages.unlinkButton) }}
</button>
</ButtonStyled>
</div>
<span class="text-primary">{{ formatMessage(messages.unlinkDescription) }}</span>
</div>
<NewModal
ref="unpublishModal"
:header="formatMessage(messages.unpublishModalHeader)"
fade="warning"
max-width="500px"
>
<Admonition type="warning" :header="formatMessage(messages.unpublishModalAdmonitionHeader)">{{
formatMessage(messages.unpublishModalBody)
}}</Admonition>
<template #actions
><div class="flex justify-end gap-2">
<ButtonStyled type="outlined"
><button class="!border" @click="unpublishModal?.hide()">
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
</button></ButtonStyled
><ButtonStyled color="orange"
><button :disabled="busy" @click="confirmUnpublish">
<UnlinkIcon />{{ formatMessage(messages.unpublishButton) }}
</button></ButtonStyled
>
</div></template
>
</NewModal>
<NewModal
ref="unlinkModal"
:header="formatMessage(messages.unlinkModalHeader)"
fade="warning"
max-width="500px"
:on-hide="() => backupCreator?.cancelBackup()"
>
<div class="flex flex-col gap-6">
<Admonition type="warning" :header="formatMessage(messages.unlinkModalAdmonitionHeader)">{{
formatMessage(messages.unlinkModalBody)
}}</Admonition>
<InlineBackupCreator
ref="backupCreator"
backup-name="Before unlinking shared instance"
@update:buttons-disabled="backupBusy = $event"
/>
</div>
<template #actions
><div class="flex justify-end gap-2">
<ButtonStyled type="outlined"
><button class="!border" @click="unlinkModal?.hide()">
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
</button></ButtonStyled
><ButtonStyled color="orange"
><button :disabled="busy || backupBusy" @click="confirmUnlink">
<UnlinkIcon />{{ formatMessage(messages.unlinkButton) }}
</button></ButtonStyled
>
</div></template
>
</NewModal>
</template>
<script setup lang="ts">
import { SpinnerIcon, UnlinkIcon, XIcon } from '@modrinth/assets'
import {
Admonition,
ButtonStyled,
commonMessages,
defineMessages,
InlineBackupCreator,
NewModal,
useVIntl,
} from '@modrinth/ui'
import { ref } from 'vue'
const props = withDefaults(
defineProps<{
canUnpublish?: boolean
canUnlink?: boolean
busy: boolean
unpublishing?: boolean
unlinking?: boolean
unpublish?: () => Promise<void>
unlink?: () => Promise<void>
}>(),
{
canUnpublish: false,
canUnlink: false,
unpublishing: false,
unlinking: false,
unpublish: undefined,
unlink: undefined,
},
)
const { formatMessage } = useVIntl()
const unpublishModal = ref<InstanceType<typeof NewModal>>()
const unlinkModal = ref<InstanceType<typeof NewModal>>()
const backupCreator = ref<InstanceType<typeof InlineBackupCreator>>()
const backupBusy = ref(false)
async function confirmUnpublish() {
unpublishModal.value?.hide()
await props.unpublish?.()
}
async function confirmUnlink() {
unlinkModal.value?.hide()
await props.unlink?.()
}
const messages = defineMessages({
title: {
id: 'installation-settings.shared-instance.title',
defaultMessage: 'Unpublish instance',
},
linkedTitle: {
id: 'installation-settings.shared-instance.linked-title',
defaultMessage: 'Linked shared instance',
},
unpublishButton: {
id: 'installation-settings.shared-instance.unpublish-button',
defaultMessage: 'Unpublish shared instance',
},
unpublishingButton: {
id: 'installation-settings.shared-instance.unpublishing-button',
defaultMessage: 'Unpublishing...',
},
unpublishDescription: {
id: 'installation-settings.shared-instance.unpublish-description',
defaultMessage:
'Remove this shared instance from Modrinth and stop sending updates to anyone using it. Your local instance will not be affected.',
},
unlinkButton: {
id: 'installation-settings.shared-instance.unlink-button',
defaultMessage: 'Unlink shared instance',
},
unlinkingButton: {
id: 'installation-settings.shared-instance.unlinking-button',
defaultMessage: 'Unlinking...',
},
unlinkDescription: {
id: 'installation-settings.shared-instance.unlink-description',
defaultMessage: 'Disconnect this local instance from future shared updates.',
},
unpublishModalHeader: {
id: 'installation-settings.unpublish-shared-instance.modal.header',
defaultMessage: 'Unpublish shared instance',
},
unpublishModalAdmonitionHeader: {
id: 'installation-settings.unpublish-shared-instance.modal.admonition-header',
defaultMessage: 'Unpublishing shared instance',
},
unpublishModalBody: {
id: 'installation-settings.unpublish-shared-instance.modal.admonition-body',
defaultMessage:
"This deletes the shared instance from Modrinth's servers. People using it in the Modrinth App will stop receiving updates, but your local instance and its content will stay on this device.",
},
unlinkModalHeader: {
id: 'installation-settings.unlink-shared-instance.modal.header',
defaultMessage: 'Unlink shared instance',
},
unlinkModalAdmonitionHeader: {
id: 'installation-settings.unlink-shared-instance.modal.admonition-header',
defaultMessage: 'Unlinking shared instance',
},
unlinkModalBody: {
id: 'installation-settings.unlink-shared-instance.modal.admonition-body',
defaultMessage:
'This only affects your local instance. Your installed content will stay on this device, and the shared instance and other people using it will not be affected.',
},
})
</script>
@@ -0,0 +1,197 @@
<template>
<ContentDiffModal
ref="publishReviewModal"
:header="formatMessage(messages.header)"
:admonition-header="formatMessage(messages.admonitionHeader)"
:description="formatMessage(messages.description)"
:diffs="publishDiffs"
:confirm-label="formatMessage(messages.publishButton)"
:confirm-icon="UploadIcon"
:confirm-disabled="publishDiffs.length === 0 && selectedConfigPaths.length === 0"
:added-label="formatMessage(messages.addedLabel)"
:removed-label="formatMessage(messages.removedLabel)"
@confirm="publishChanges"
@cancel="finishReview"
>
<template #additional-content>
<Accordion
class="min-w-0"
overflow-visible
button-class="w-full bg-transparent m-0 p-0 border-none"
>
<template #title>
<FileCogIcon class="size-4 shrink-0 text-primary" />
<span class="text-lg font-semibold text-contrast">
{{ formatMessage(messages.configTitle) }}
</span>
</template>
<div class="flex min-w-0 flex-col gap-3 pt-4">
<div ref="configFileTreeContainer" class="max-h-[292px] overflow-y-auto rounded-[20px]">
<FileTreeSelect
v-model="selectedConfigPaths"
:items="configFileItems"
:show-size="false"
:show-modified="false"
@navigate="scrollConfigFileTreeToTop"
/>
</div>
</div>
</Accordion>
</template>
</ContentDiffModal>
</template>
<script setup lang="ts">
import { FileCogIcon, UploadIcon } from '@modrinth/assets'
import {
Accordion,
type ContentDiffItem,
ContentDiffModal,
defineMessages,
FileTreeSelect,
type FileTreeSelectItem,
useVIntl,
} from '@modrinth/ui'
import { computed, ref } from 'vue'
import {
getSharedInstanceUnavailableReason,
isSharedInstanceUnavailableError,
} from '@/helpers/install'
import { get_shared_instance_publish_preview, publish_shared_instance } from '@/helpers/instance'
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
import type { GameInstance } from '@/helpers/types'
type SharedInstancePublishState = 'idle' | 'reviewing' | 'publishing'
const props = defineProps<{
instance: GameInstance
}>()
const emit = defineEmits<{
published: []
'state-change': [state: SharedInstancePublishState]
}>()
const { formatMessage } = useVIntl()
const { notifySharedInstanceError, notifySharedInstanceUnavailable } = useSharedInstanceErrors()
const publishReviewModal = ref<InstanceType<typeof ContentDiffModal>>()
const configFileTreeContainer = ref<HTMLElement>()
const publishDiffs = ref<ContentDiffItem[]>([])
const configFilePaths = ref<string[]>([])
const selectedConfigPaths = ref<string[]>([])
const state = ref<SharedInstancePublishState>('idle')
const configFileItems = computed<FileTreeSelectItem[]>(() =>
configFilePaths.value.map((path) => ({ path, type: 'file' })),
)
async function show(e?: MouseEvent) {
if (state.value !== 'idle') return
setState('reviewing')
let reviewOpened = false
try {
const preview = await get_shared_instance_publish_preview(props.instance.id)
if (!preview) {
notifySharedInstanceUnavailable()
emit('published')
return
}
publishDiffs.value = preview.diffs.map((diff) => ({
type: diff.type,
projectName: diff.projectName ?? undefined,
fileName: diff.fileName ?? undefined,
currentVersionName: diff.currentVersionName ?? undefined,
newVersionName: diff.newVersionName ?? undefined,
fileCount: diff.configFileCount ?? undefined,
disabled: diff.disabled,
}))
configFilePaths.value = preview.configFiles
selectedConfigPaths.value = []
if (!publishReviewModal.value) return
publishReviewModal.value.show(e)
reviewOpened = true
} catch (error) {
handlePublishError(error)
} finally {
if (!reviewOpened) finishReview()
}
}
async function publishChanges() {
if (state.value !== 'reviewing') return
setState('publishing')
try {
await publish_shared_instance(props.instance.id, selectedConfigPaths.value)
emit('published')
} catch (error) {
handlePublishError(error)
} finally {
setState('idle')
}
}
function scrollConfigFileTreeToTop() {
if (configFileTreeContainer.value) {
configFileTreeContainer.value.scrollTop = 0
}
}
function finishReview() {
if (state.value === 'reviewing') {
setState('idle')
}
}
function handlePublishError(error: unknown) {
if (isSharedInstanceUnavailableError(error)) {
notifySharedInstanceUnavailable(getSharedInstanceUnavailableReason(error))
emit('published')
return
}
notifySharedInstanceError(error)
}
function setState(nextState: SharedInstancePublishState) {
state.value = nextState
emit('state-change', nextState)
}
const messages = defineMessages({
header: {
id: 'instance.shared-instance.publish-review.header',
defaultMessage: 'Review changes',
},
admonitionHeader: {
id: 'instance.shared-instance.publish-review.admonition-header',
defaultMessage: 'Push update to players',
},
description: {
id: 'instance.shared-instance.publish-review.description',
defaultMessage:
'Review the content changes and choose any config files to include in this update.',
},
publishButton: {
id: 'instance.shared-instance.publish-review.publish-button',
defaultMessage: 'Push update',
},
addedLabel: {
id: 'instance.shared-instance.publish-review.added-label',
defaultMessage: 'Added',
},
removedLabel: {
id: 'instance.shared-instance.publish-review.removed-label',
defaultMessage: 'Removed',
},
configTitle: {
id: 'instance.shared-instance.publish-review.config-title-v2',
defaultMessage: 'Select config files',
},
})
defineExpose({ show })
</script>
@@ -0,0 +1,138 @@
<template>
<ContentDiffModal
ref="modal"
:header="formatMessage(messages.updateToPlay)"
:admonition-header="formatMessage(messages.updateRequired)"
:description="instance ? formatMessage(messages.description, { name: instance.name }) : ''"
:diffs="diffs"
:confirm-label="formatMessage(commonMessages.updateButton)"
:confirm-icon="DownloadIcon"
:added-label="formatMessage(messages.addedLabel)"
:removed-label="formatMessage(messages.removedLabel)"
show-report-button
show-external-warnings
:external-warning-description="formatMessage(messages.externalWarningDescription)"
@confirm="update"
@cancel="emit('cancel')"
@report="emit('report', $event)"
/>
</template>
<script setup lang="ts">
import { DownloadIcon } from '@modrinth/assets'
import {
commonMessages,
type ContentDiffItem,
ContentDiffModal,
defineMessages,
useVIntl,
} from '@modrinth/ui'
import { computed, ref } from 'vue'
import {
getSharedInstanceUnavailableReason,
install_update_shared_instance,
isSharedInstanceUnavailableError,
type SharedInstanceUnavailableReason,
type SharedInstanceUpdatePreview,
wait_for_install_job,
} from '@/helpers/install'
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
import type { GameInstance } from '@/helpers/types'
type UpdateCompleteCallback = () => void | Promise<void>
const emit = defineEmits<{
accepted: []
cancel: []
complete: [successful: boolean]
report: [event?: MouseEvent]
sharedInstanceUnavailable: [reason: SharedInstanceUnavailableReason | null]
}>()
const modal = ref<InstanceType<typeof ContentDiffModal>>()
const instance = ref<GameInstance | null>(null)
const preview = ref<SharedInstanceUpdatePreview | null>(null)
const onComplete = ref<UpdateCompleteCallback>(() => {})
const { formatMessage } = useVIntl()
const { notifySharedInstanceError } = useSharedInstanceErrors()
const diffs = computed<ContentDiffItem[]>(
() =>
preview.value?.diffs
// TODO: This is TEMP!!! Hashing needs to be done on backend
.filter((diff) => !(diff.type === 'updated' && !diff.projectId && diff.fileName))
.map((diff) => ({
type: diff.type,
projectName: diff.projectName ?? undefined,
fileName: diff.fileName ?? undefined,
currentVersionName: diff.currentVersionName ?? undefined,
newVersionName: diff.newVersionName ?? undefined,
fileCount: diff.configFileCount ?? undefined,
disabled: diff.disabled,
external: diff.type === 'added' && !diff.projectId && !!diff.fileName,
})) ?? [],
)
async function update() {
let successful = false
emit('accepted')
try {
if (instance.value) {
const job = await install_update_shared_instance(instance.value.id)
await wait_for_install_job(job.job_id)
await onComplete.value()
successful = true
}
} catch (error) {
if (isSharedInstanceUnavailableError(error)) {
emit('sharedInstanceUnavailable', getSharedInstanceUnavailableReason(error))
return
}
notifySharedInstanceError(error)
} finally {
emit('complete', successful)
}
}
function show(
instanceValue: GameInstance,
previewValue: SharedInstanceUpdatePreview,
callback: UpdateCompleteCallback = () => {},
event?: MouseEvent,
) {
instance.value = instanceValue
preview.value = previewValue
onComplete.value = callback
modal.value?.show(event)
}
function hide() {
modal.value?.hide()
}
const messages = defineMessages({
updateToPlay: { id: 'app.modal.update-to-play.header', defaultMessage: 'Update to play' },
updateRequired: {
id: 'app.modal.update-to-play.update-required',
defaultMessage: 'Update required',
},
description: {
id: 'app.modal.update-to-play.update-required-description',
defaultMessage:
'An update is required to play {name}. Please update to latest version to launch the game.',
},
addedLabel: {
id: 'app.modal.update-to-play.shared-instance-added-label',
defaultMessage: 'Added',
},
removedLabel: {
id: 'app.modal.update-to-play.shared-instance-removed-label',
defaultMessage: 'Removed',
},
externalWarningDescription: {
id: 'app.modal.update-to-play.shared-instance-unknown-files-description',
defaultMessage:
'This shared instance update contains files that arent published on Modrinth. We strongly recommend only installing files from sources you trust.',
},
})
defineExpose({ show, hide })
</script>
@@ -0,0 +1,638 @@
<template>
<NewModal
ref="modal"
:header="
reportMode
? formatMessage(messages.reportSharedInstance)
: formatMessage(messages.installToPlay)
"
:closable="!submitLoading"
:on-hide="handleHide"
:max-width="reportMode ? '816px' : '544px'"
:width="reportMode ? '816px' : '544px'"
:no-padding="reportMode"
:scrollable="reportMode"
>
<div v-if="preview" class="flex w-full flex-col gap-6" :class="{ 'p-6 pb-2': reportMode }">
<Admonition v-if="reportMode" type="info">
<div class="flex flex-col gap-2">
<p class="m-0">
<IntlFormatted :message-id="messages.reportDescription">
<template #rules-link="{ children }">
<AutoLink class="text-link hover:underline" to="https://modrinth.com/legal/rules">
<component :is="() => children" />
</AutoLink>
</template>
<template #terms-link="{ children }">
<AutoLink class="text-link hover:underline" to="https://modrinth.com/legal/terms">
<component :is="() => children" />
</AutoLink>
</template>
</IntlFormatted>
</p>
<ul class="m-0 list-disc pl-5">
<li>
<IntlFormatted :message-id="messages.supportAndBugReports">
<template #support-link="{ children }">
<AutoLink class="text-link hover:underline" to="https://support.modrinth.com">
<component :is="() => children" />
</AutoLink>
</template>
<template #github-link="{ children }">
<AutoLink
class="text-link hover:underline"
to="https://github.com/modrinth/code/issues"
>
<component :is="() => children" />
</AutoLink>
</template>
</IntlFormatted>
</li>
<li>
<IntlFormatted :message-id="messages.legalClaims">
<template #copyright-link="{ children }">
<AutoLink
class="text-link hover:underline"
to="https://modrinth.com/legal/copyright"
>
<component :is="() => children" />
</AutoLink>
</template>
</IntlFormatted>
</li>
</ul>
</div>
</Admonition>
<p v-else class="m-0 text-primary">
<IntlFormatted
v-if="creator"
:message-id="messages.inviteWarningWithCreator"
:values="{ username: creator.username }"
>
<template #creator="{ children }">
<AutoLink :to="creatorProfileLink" class="font-medium text-contrast hover:underline">
<Avatar
:src="creator.avatarUrl"
:alt="creator.username"
:tint-by="creator.username"
size="24px"
circle
no-shadow
class="mr-1 inline-block align-middle"
/>
<span><component :is="() => children" /></span>
</AutoLink>
</template>
</IntlFormatted>
<template v-else>
{{ formatMessage(messages.inviteWarning) }}
</template>
</p>
<SharedInstanceInstallSummary
:preview="preview"
:heading="reportMode ? formatMessage(messages.contentYouAreReporting) : undefined"
@view-contents="openViewContents"
/>
<Transition
enter-active-class="overflow-hidden transition-all duration-200 ease-out"
enter-from-class="max-h-0 opacity-0"
enter-to-class="max-h-[400px] opacity-100"
leave-active-class="overflow-hidden transition-all duration-200 ease-in"
leave-from-class="max-h-[400px] opacity-100"
leave-to-class="max-h-0 opacity-0"
>
<div v-if="reportMode" class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<span class="font-semibold text-contrast">
{{ formatMessage(messages.reportReason) }}
</span>
<Combobox v-model="reportReason" :options="reportReasonOptions" />
</div>
<div class="flex flex-col gap-2.5">
<span class="font-semibold text-contrast">
{{ formatMessage(messages.additionalContext) }}
</span>
<MarkdownEditor
v-model="additionalContext"
:placeholder="formatMessage(messages.additionalContextPlaceholder)"
:on-image-upload="onImageUpload"
:min-height="120"
:max-height="240"
/>
</div>
<div v-if="reportOnly || blockTargetUserId" class="flex flex-col gap-2">
<Checkbox
v-if="reportOnly"
v-model="deleteInstance"
:label="formatMessage(messages.deleteInstance)"
/>
<Checkbox
v-if="blockTargetUserId"
v-model="blockUser"
:label="formatMessage(messages.blockUser)"
/>
</div>
</div>
</Transition>
<Admonition
v-if="!reportMode && hasExternalFiles"
type="warning"
:header="formatMessage(messages.unknownFilesWarning)"
>
{{ formatMessage(messages.unknownFilesDescription) }}
</Admonition>
<div v-if="!reportMode && hasExternalFiles" class="relative w-full">
<div
ref="externalFileTable"
class="max-h-[242px] overflow-y-auto rounded-2xl"
@scroll="checkTableScrollState"
>
<Table
:columns="externalFileColumns"
:data="externalFileRows"
row-key="id"
virtualized
:virtual-row-height="48"
class="shadow-sm"
>
<template #cell-name="{ value }">
<span class="block truncate" :title="String(value)">{{ value }}</span>
</template>
</Table>
</div>
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-2"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-2"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="showTableTopFade"
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-2 bg-gradient-to-b from-bg-raised to-transparent"
/>
</Transition>
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-2"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-2"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="showTableBottomFade"
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-2 bg-gradient-to-t from-bg-raised to-transparent"
/>
</Transition>
</div>
<p v-if="!reportMode && hasExternalFiles" class="m-0 text-primary">
{{ formatMessage(messages.reviewedFiles) }}
</p>
<div v-if="!reportMode" class="flex w-full items-center justify-between gap-2">
<ButtonStyled color="red" type="transparent">
<button @click="reportMode = true">
<ReportIcon />{{ formatMessage(commonMessages.reportButton) }}
</button>
</ButtonStyled>
<div class="flex items-center gap-2">
<template v-if="hasExternalFiles">
<ButtonStyled type="transparent" color="orange">
<button @click="accept">
{{ formatMessage(messages.installAnyway) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button @click="handleCancel">
<BanIcon />{{ formatMessage(messages.dontInstall) }}
</button>
</ButtonStyled>
</template>
<template v-else>
<ButtonStyled type="outlined">
<button class="!border" @click="handleCancel">
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button @click="accept">
<DownloadIcon />{{ formatMessage(messages.installButton) }}
</button>
</ButtonStyled>
</template>
</div>
</div>
</div>
<template v-if="reportMode" #actions>
<div class="flex justify-end gap-2">
<ButtonStyled type="outlined">
<button class="!border" :disabled="submitLoading" @click="handleCancel">
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button :disabled="!canSubmitReport" @click="submitReport">
<SpinnerIcon v-if="submitLoading" class="animate-spin" />
<SendIcon v-else />
{{ formatMessage(commonMessages.reportButton) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
<ModpackContentModal
ref="contentModal"
:header="formatMessage(messages.sharedInstanceContent)"
:modpack-name="preview?.name ?? ''"
:modpack-icon-url="preview?.iconUrl ?? undefined"
/>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { BanIcon, DownloadIcon, ReportIcon, SendIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import {
Admonition,
AutoLink,
Avatar,
blockedUsersQueryKey,
ButtonStyled,
Checkbox,
Combobox,
type ComboboxOption,
commonMessages,
defineMessages,
injectAuth,
injectModrinthClient,
injectNotificationManager,
IntlFormatted,
MarkdownEditor,
ModpackContentModal,
NewModal,
Table,
type TableColumn,
useScrollIndicator,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, nextTick, ref } from 'vue'
import { config } from '@/config'
import { hide_ads_window, show_ads_window } from '@/helpers/ads'
import { toError } from '@/helpers/errors'
import type { SharedInstanceInstallPreview } from '@/helpers/install'
import { create_report } from '@/helpers/reports'
import { block_user } from '@/helpers/users'
import SharedInstanceInstallSummary from './shared-instance-install-summary.vue'
import { useSharedInstancePreviewContent } from './use-shared-instance-preview-content'
type ExternalFileColumn = 'name'
type ExternalFileRow = {
id: string
name: string
}
type SharedInstanceCreator = {
id: string | null
username: string
avatarUrl: string | null
}
const modal = ref<InstanceType<typeof NewModal>>()
const contentModal = ref<InstanceType<typeof ModpackContentModal>>()
const externalFileTable = ref<HTMLElement | null>(null)
const preview = ref<SharedInstanceInstallPreview | null>(null)
const creator = ref<SharedInstanceCreator | null>(null)
const install = ref<() => void | Promise<void>>(() => {})
const reportMode = ref(false)
const reportOnly = ref(false)
type ReportReason = 'malicious' | 'inappropriate' | 'spam'
const reportReason = ref<ReportReason>('malicious')
const additionalContext = ref('')
const deleteInstance = ref(true)
const blockUser = ref(true)
const blockTargetUserId = ref<string | null>(null)
const submitLoading = ref(false)
const uploadedImageIDs = ref<string[]>([])
const emit = defineEmits<{
reported: [deleteInstance: boolean]
}>()
const { formatMessage } = useVIntl()
const auth = injectAuth()
const client = injectModrinthClient()
const queryClient = useQueryClient()
const { addNotification, handleError } = injectNotificationManager()
const { load } = useSharedInstancePreviewContent()
const {
showTopFade: showTableTopFade,
showBottomFade: showTableBottomFade,
checkScrollState: checkTableScrollState,
forceCheck: forceCheckTableScroll,
} = useScrollIndicator(externalFileTable)
const hasExternalFiles = computed(() => Boolean(preview.value?.externalFiles.length))
const externalFileRows = computed<ExternalFileRow[]>(() =>
(preview.value?.externalFiles ?? [])
.map((file, index) => ({
id: `${index}-${file.fileType}-${file.fileName}`,
name: file.fileName,
}))
.sort((left, right) => left.name.localeCompare(right.name)),
)
const reportReasonOptions = computed<ComboboxOption<ReportReason>[]>(() => [
{ value: 'malicious', label: formatMessage(messages.maliciousReason) },
{ value: 'inappropriate', label: formatMessage(messages.inappropriateReason) },
{ value: 'spam', label: formatMessage(messages.spamReason) },
])
const canSubmitReport = computed(
() => Boolean(preview.value && additionalContext.value.trim()) && !submitLoading.value,
)
const creatorProfileLink = computed(() => {
const username = creator.value?.username
return username
? () => openUrl(`${config.siteUrl}/user/${encodeURIComponent(username)}`)
: undefined
})
async function accept() {
hide()
try {
await install.value()
} catch (error) {
console.error('Failed to install shared instance:', error)
}
}
async function openViewContents() {
if (!preview.value) return
contentModal.value?.showLoading()
try {
contentModal.value?.show(await load(preview.value))
} catch (error) {
console.error('Failed to load shared instance contents:', error)
contentModal.value?.show([])
}
}
async function submitReport() {
const reportPreview = preview.value
const body = additionalContext.value.trim()
if (!reportPreview || !body || submitLoading.value) return
submitLoading.value = true
try {
const uploadedImages = uploadedImageIDs.value.slice(-10)
const blockTarget = blockUser.value ? blockTargetUserId.value : null
const [reportResult, blockResult] = await Promise.allSettled([
create_report({
report_type: reportReason.value,
item_type: 'shared-instance',
item_id: `${reportPreview.sharedInstanceId}/${reportPreview.version}`,
body,
uploaded_images: uploadedImages,
}),
blockTarget ? block_user(blockTarget) : Promise.resolve(),
])
if (blockTarget) {
if (blockResult.status === 'fulfilled') {
blockUser.value = false
const authUserId = auth.user.value?.id
if (authUserId) {
queryClient.setQueryData<Labrinth.BlockedUsers.v3.BlockedUserId[]>(
blockedUsersQueryKey(authUserId),
(blockedUsers = []) =>
blockedUsers.includes(blockTarget) ? blockedUsers : [...blockedUsers, blockTarget],
)
}
addNotification({
type: 'success',
title: formatMessage(messages.userBlocked),
})
} else {
handleError(toError(blockResult.reason))
}
}
if (reportResult.status === 'rejected') throw reportResult.reason
const shouldDeleteInstance = reportOnly.value && deleteInstance.value
hide()
addNotification({
type: 'success',
title: formatMessage(messages.reportSubmitted),
})
emit('reported', shouldDeleteInstance)
} catch (error) {
handleError(toError(error))
} finally {
submitLoading.value = false
}
}
async function onImageUpload(file: File) {
const imageExtensionByType: Partial<Record<string, Labrinth.Images.v3.ImageExtension>> = {
'image/gif': 'gif',
'image/jpeg': 'jpeg',
'image/png': 'png',
'image/webp': 'webp',
}
const extension = imageExtensionByType[file.type]
if (!extension) {
throw new Error(formatMessage(messages.invalidImageType))
}
if (file.size > 1024 * 1024) {
throw new Error(formatMessage(messages.imageTooLarge))
}
const image = await client.labrinth.images_v3.uploadImage(file, extension, {
context: 'report',
}).promise
uploadedImageIDs.value.push(image.id)
return image.url
}
function handleCancel() {
if (reportMode.value && !reportOnly.value) {
reportMode.value = false
void nextTick(() => forceCheckTableScroll())
return
}
hide()
}
function handleHide() {
resetReportState()
creator.value = null
show_ads_window()
}
function resetReportState() {
reportMode.value = false
reportOnly.value = false
reportReason.value = 'malicious'
additionalContext.value = ''
deleteInstance.value = true
blockUser.value = true
blockTargetUserId.value = null
submitLoading.value = false
uploadedImageIDs.value = []
}
function show(
previewValue: SharedInstanceInstallPreview,
installValue: () => void | Promise<void>,
creatorValue?: SharedInstanceCreator,
event?: MouseEvent,
) {
resetReportState()
creator.value = creatorValue ?? null
blockTargetUserId.value = creatorValue?.id ?? null
install.value = installValue
showPreview(previewValue, event)
}
function showReport(
previewValue: SharedInstanceInstallPreview,
blockTargetUserIdValue?: string | null,
event?: MouseEvent,
) {
resetReportState()
creator.value = null
blockTargetUserId.value = blockTargetUserIdValue ?? null
reportMode.value = true
reportOnly.value = true
install.value = () => {}
showPreview(previewValue, event)
}
function showPreview(previewValue: SharedInstanceInstallPreview, event?: MouseEvent) {
preview.value = previewValue
hide_ads_window()
modal.value?.show(event)
void nextTick(() => forceCheckTableScroll())
}
function hide() {
modal.value?.hide()
}
const messages = defineMessages({
installToPlay: { id: 'app.modal.install-to-play.header', defaultMessage: 'Install to play' },
reportSharedInstance: {
id: 'app.modal.install-to-play.report-shared-instance-header',
defaultMessage: 'Report shared instance',
},
reportSubmitted: {
id: 'app.modal.install-to-play.report-submitted',
defaultMessage: 'Report submitted',
},
sharedInstanceContent: {
id: 'app.modal.install-to-play.shared-instance-content',
defaultMessage: 'Shared instance content',
},
inviteWarning: {
id: 'app.modal.install-to-play.invite-warning',
defaultMessage:
'This invite was created by another Modrinth user, not Modrinth. Only accept invites from people you trust.',
},
inviteWarningWithCreator: {
id: 'app.modal.install-to-play.invite-warning-with-creator',
defaultMessage:
'This invite was created by <creator>{username}</creator>, not Modrinth. Only accept invites from people you trust.',
},
reportDescription: {
id: 'app.modal.install-to-play.report-description',
defaultMessage:
'Use this form to report instances that may violate our <rules-link>Rules</rules-link> or <terms-link>Terms of Use</terms-link>.',
},
supportAndBugReports: {
id: 'app.modal.install-to-play.report-support-and-bugs',
defaultMessage:
'For support requests, contact our <support-link>support team</support-link>. For bug reports, open a <github-link>GitHub issue</github-link>.',
},
legalClaims: {
id: 'app.modal.install-to-play.report-legal-claims',
defaultMessage:
'For DMCA notices or other legal claims, see our <copyright-link>Copyright Policy</copyright-link>.',
},
contentYouAreReporting: {
id: 'app.modal.install-to-play.content-you-are-reporting',
defaultMessage: 'Instance youre reporting',
},
reportReason: {
id: 'app.modal.install-to-play.report-reason',
defaultMessage: 'Which rule does this instance violate?',
},
maliciousReason: {
id: 'app.modal.install-to-play.report-reason.malicious',
defaultMessage: 'Malicious',
},
inappropriateReason: {
id: 'app.modal.install-to-play.report-reason.inappropriate',
defaultMessage: 'Inappropriate',
},
spamReason: {
id: 'app.modal.install-to-play.report-reason.spam',
defaultMessage: 'Spam',
},
additionalContext: {
id: 'app.modal.install-to-play.additional-context',
defaultMessage: 'Additional context',
},
additionalContextPlaceholder: {
id: 'app.modal.install-to-play.additional-context-placeholder',
defaultMessage: 'Include links and images if possible and relevant',
},
invalidImageType: {
id: 'app.modal.install-to-play.report-image-invalid-type',
defaultMessage: 'File is not an accepted image type',
},
imageTooLarge: {
id: 'app.modal.install-to-play.report-image-too-large',
defaultMessage: 'File exceeds the 1 MiB size limit',
},
deleteInstance: {
id: 'app.modal.install-to-play.delete-instance',
defaultMessage: 'Delete instance',
},
blockUser: {
id: 'app.modal.install-to-play.block-user',
defaultMessage: 'Block user',
},
userBlocked: {
id: 'app.modal.install-to-play.user-blocked',
defaultMessage: 'User blocked',
},
unknownFilesWarning: {
id: 'app.modal.install-to-play.unknown-files-warning',
defaultMessage: 'Unknown files warning',
},
unknownFilesDescription: {
id: 'app.modal.install-to-play.shared-instance-unknown-files-description',
defaultMessage:
'This shared instance contains files that arent published on Modrinth. We strongly recommend only installing files from sources you trust.',
},
unrecognizedFiles: {
id: 'app.modal.install-to-play.unrecognized-files',
defaultMessage: 'Unrecognized files',
},
reviewedFiles: {
id: 'app.modal.install-to-play.reviewed-files',
defaultMessage:
'A file is only reviewed if its published to Modrinth, regardless of its file format (including .mrpack).',
},
installAnyway: {
id: 'app.modal.install-to-play.install-anyway',
defaultMessage: 'Install anyway',
},
dontInstall: {
id: 'app.modal.install-to-play.external-files-dont-install',
defaultMessage: "Don't install",
},
installButton: { id: 'app.modal.install-to-play.install-button', defaultMessage: 'Install' },
})
const externalFileColumns = computed<TableColumn<ExternalFileColumn>[]>(() => [
{
key: 'name',
label: formatMessage(messages.unrecognizedFiles),
cellClass: '!h-12',
},
])
defineExpose({ show, showReport, hide })
</script>
@@ -0,0 +1,60 @@
<template>
<div class="flex flex-col gap-2.5">
<div class="flex items-center justify-between">
<span class="font-semibold text-contrast">{{
heading ?? formatMessage(messages.sharedInstance)
}}</span>
<ButtonStyled type="transparent">
<button @click="emit('viewContents')">
<EyeIcon />
{{ formatMessage(messages.viewContents) }}
</button>
</ButtonStyled>
</div>
<div class="flex items-center gap-3 rounded-2xl bg-surface-2 p-3">
<Avatar
:src="preview.iconUrl"
:alt="preview.name"
size="56px"
no-shadow
class="!rounded-2xl"
/>
<div class="flex min-w-0 flex-col gap-0.5">
<span class="truncate font-semibold text-contrast">{{ preview.name }}</span>
<span class="truncate text-sm font-medium text-secondary">
{{ loaderDisplay }} {{ preview.gameVersion }}
<template v-if="preview.modCount">
· {{ formatMessage(messages.modCount, { count: preview.modCount }) }}
</template>
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { EyeIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled, defineMessages, formatLoader, useVIntl } from '@modrinth/ui'
import { computed, toRefs } from 'vue'
import type { SharedInstanceInstallPreview } from '@/helpers/install'
const props = defineProps<{ preview: SharedInstanceInstallPreview; heading?: string }>()
const { preview, heading } = toRefs(props)
const emit = defineEmits<{ viewContents: [] }>()
const { formatMessage } = useVIntl()
const loaderDisplay = computed(() =>
preview.value.loader ? formatLoader(formatMessage, preview.value.loader) : '',
)
const messages = defineMessages({
sharedInstance: {
id: 'app.modal.install-to-play.shared-instance',
defaultMessage: 'Shared instance',
},
viewContents: { id: 'app.modal.install-to-play.view-contents', defaultMessage: 'View contents' },
modCount: {
id: 'app.modal.install-to-play.mod-count',
defaultMessage: '{count, plural, one {# mod} other {# mods}}',
},
})
</script>
@@ -0,0 +1,146 @@
import type { Labrinth } from '@modrinth/api-client'
import type { ContentItem } from '@modrinth/ui'
import { get_project, get_project_many, get_version, get_version_many } from '@/helpers/cache.js'
import type { SharedInstanceInstallPreview } from '@/helpers/install'
type VersionDependency = Labrinth.Versions.v2.Dependency & { version_id?: string }
export function useSharedInstancePreviewContent() {
async function load(preview: SharedInstanceInstallPreview): Promise<ContentItem[]> {
return [
...preview.externalFiles.map(externalFileContentItem),
...(await modpackContentItems(preview)),
...(await contentItemsFromVersionIds(
preview.contentVersionIds.filter((id) => id !== preview.modpackVersionId),
)),
]
}
async function modpackContentItems(preview: SharedInstanceInstallPreview) {
if (!preview.modpackVersionId) return []
const version = await get_version(preview.modpackVersionId, 'must_revalidate')
if (!version) return []
const [project, contentItems] = await Promise.all([
get_project(version.project_id, 'must_revalidate'),
contentItemsFromDependencies(version.dependencies ?? []),
])
if (!project) return contentItems
return contentItems.map((item) => ({
...item,
source: { project },
}))
}
async function contentItemsFromDependencies(dependencies: Labrinth.Versions.v2.Dependency[]) {
const deps = dependencies as VersionDependency[]
const projectIds = unique(deps.map((dep) => dep.project_id).filter((id): id is string => !!id))
const versionIds = unique(deps.map((dep) => dep.version_id).filter((id): id is string => !!id))
const [projects, versions]: [Labrinth.Projects.v2.Project[], Labrinth.Versions.v2.Version[]] =
await Promise.all([
projectIds.length ? get_project_many(projectIds, 'must_revalidate') : [],
versionIds.length ? get_version_many(versionIds, 'must_revalidate') : [],
])
const projectMap = new Map(projects.map((project) => [project.id, project]))
const versionMap = new Map(versions.map((version) => [version.id, version]))
return deps.map((dependency): ContentItem => {
const project = dependency.project_id ? projectMap.get(dependency.project_id) : null
const version = dependency.version_id ? versionMap.get(dependency.version_id) : null
const fileName =
version?.files?.[0]?.filename ?? dependency.file_name ?? project?.title ?? 'Unknown'
return contentItem(
version?.id ?? project?.id ?? fileName,
fileName,
project,
version,
!project && !version,
dependency.project_id ?? fileName,
dependency.file_name ?? fileName,
)
})
}
async function contentItemsFromVersionIds(versionIds: string[]) {
const versions: Labrinth.Versions.v2.Version[] = versionIds.length
? await get_version_many(unique(versionIds), 'must_revalidate')
: []
const projectIds = unique(versions.map((version) => version.project_id).filter(Boolean))
const projects: Labrinth.Projects.v2.Project[] = projectIds.length
? await get_project_many(projectIds, 'must_revalidate')
: []
const projectMap = new Map(projects.map((project) => [project.id, project]))
return versions.map((version): ContentItem => {
const project = projectMap.get(version.project_id)
const fileName = version.files?.[0]?.filename ?? project?.title ?? version.name ?? 'Unknown'
return contentItem(
version.id,
fileName,
project,
version,
false,
version.project_id,
version.name,
)
})
}
return { load }
}
function contentItem(
id: string,
fileName: string,
project?: Labrinth.Projects.v2.Project | null,
version?: Labrinth.Versions.v2.Version | null,
external = false,
fallbackProjectId = id,
fallbackTitle = fileName,
projectType = project?.project_type ?? 'mod',
): ContentItem {
return {
id,
file_name: fileName,
project_type: projectType,
has_update: false,
update_version_id: null,
external,
project: {
id: project?.id ?? fallbackProjectId,
slug: project?.slug ?? fallbackProjectId,
title: project?.title ?? fallbackTitle,
icon_url: project?.icon_url ?? undefined,
},
...(version
? {
version: {
id: version.id,
file_name: fileName,
version_number: version.version_number ?? undefined,
date_published: version.date_published ?? undefined,
},
}
: {}),
}
}
function externalFileContentItem(
file: SharedInstanceInstallPreview['externalFiles'][number],
): ContentItem {
return contentItem(
`external:${file.fileType}:${file.fileName}`,
file.fileName,
null,
null,
true,
file.fileName,
file.fileName,
file.fileType,
)
}
function unique<T>(values: T[]) {
return Array.from(new Set(values))
}
@@ -0,0 +1,48 @@
<template>
<ModrinthAccountRequiredModal ref="accountRequiredModal" :request-auth="requestAuth" />
<SharedInstanceInstallModal ref="installModal" />
<SharedInstanceAlreadyInstalledModal
ref="alreadyInstalledModal"
@cancel="handleAlreadyInstalledCancel"
@go-to-instance="handleAlreadyInstalledGoToInstance"
@install-anyway="handleAlreadyInstalledInstallAnyway"
/>
</template>
<script setup lang="ts">
import { injectAuth } from '@modrinth/ui'
import { nextTick, ref } from 'vue'
import ModrinthAccountRequiredModal from '@/components/ui/modal/ModrinthAccountRequiredModal.vue'
import SharedInstanceInstallModal from '@/components/ui/shared-instances/shared-instance-install-modal/index.vue'
import SharedInstanceAlreadyInstalledModal from '@/components/ui/shared-instances/SharedInstanceAlreadyInstalledModal.vue'
import type { ModrinthAuthFlow } from '@/helpers/mr_auth'
import type { SharedInstanceInviteHandler } from './shared-instance-invite-types'
import { useSharedInstanceInviteHandler } from './use-shared-instance-invite-handler'
const auth = injectAuth()
const installModal = ref<InstanceType<typeof SharedInstanceInstallModal>>()
const alreadyInstalledModal = ref<InstanceType<typeof SharedInstanceAlreadyInstalledModal>>()
const accountRequiredModal = ref<InstanceType<typeof ModrinthAccountRequiredModal>>()
const {
handleNotification,
installFromInviteId,
clearNotifications,
handleAlreadyInstalledCancel,
handleAlreadyInstalledGoToInstance,
handleAlreadyInstalledInstallAnyway,
} = useSharedInstanceInviteHandler(installModal, alreadyInstalledModal, accountRequiredModal)
async function requestAuth(flow: ModrinthAuthFlow) {
await auth.requestSignIn('', flow, { showModal: false })
await nextTick()
return !!auth.session_token.value
}
defineExpose<SharedInstanceInviteHandler>({
handleNotification,
installFromInviteId,
clearNotifications,
})
</script>
@@ -0,0 +1,29 @@
import type {
AppNotification,
SharedInstanceInvite,
SharedInstanceInviteNotificationBody,
} from './shared-instance-invite-types'
function optionalString(value: unknown) {
return typeof value === 'string' ? value : null
}
export function parseSharedInstanceInviteNotification(
notification: AppNotification,
): SharedInstanceInvite | null {
if (notification.body?.type !== 'shared_instance_invite') return null
const body = notification.body as SharedInstanceInviteNotificationBody
const sharedInstanceId = optionalString(body.shared_instance_id)
const sharedInstanceName = optionalString(body.shared_instance_name)
if (!sharedInstanceId || !sharedInstanceName) return null
return {
sharedInstanceId,
sharedInstanceName,
invitedById: optionalString(body.invited_by),
invitedByUsername: optionalString(body.invited_by_username),
invitedByAvatarUrl: optionalString(body.invited_by_avatar_url),
instanceIconUrl: optionalString(body.shared_instance_icon),
}
}
@@ -0,0 +1,30 @@
export type SharedInstanceInviteNotificationBody = {
type: 'shared_instance_invite'
shared_instance_id?: unknown
shared_instance_name?: unknown
invited_by?: unknown
invited_by_username?: unknown
invited_by_avatar_url?: unknown
shared_instance_icon?: unknown
}
export type AppNotification = {
id: string | number
read?: boolean
body?: { type?: unknown } & Record<string, unknown>
}
export type SharedInstanceInvite = {
sharedInstanceId: string
sharedInstanceName: string
invitedById: string | null
invitedByUsername: string | null
invitedByAvatarUrl: string | null
instanceIconUrl: string | null
}
export type SharedInstanceInviteHandler = {
handleNotification(notification: AppNotification): Promise<boolean>
installFromInviteId(inviteId: string): Promise<void>
clearNotifications(): void
}
@@ -0,0 +1,318 @@
import { ModrinthApiError } from '@modrinth/api-client'
import {
injectAuth,
injectModrinthClient,
injectNotificationManager,
injectPopupNotificationManager,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { type Ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { get_user } from '@/helpers/cache'
import { toError } from '@/helpers/errors'
import {
install_accept_shared_instance_invite,
install_get_shared_instance_preview,
install_shared_instance,
} from '@/helpers/install'
import { list } from '@/helpers/instance'
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
import { useTheming } from '@/store/state'
import { parseSharedInstanceInviteNotification } from './shared-instance-invite-parser'
import type { AppNotification, SharedInstanceInvite } from './shared-instance-invite-types'
type InstallModal = {
show(
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>,
install: () => Promise<void>,
creator?: SharedInstanceCreator,
): void
}
type SharedInstanceCreator = {
id: string | null
username: string
avatarUrl: string | null
}
type AccountRequiredModal = {
show(event?: MouseEvent): Promise<boolean>
}
type AlreadyInstalledModal = {
show(instanceName: string): void
}
export function useSharedInstanceInviteHandler(
installModal: Ref<InstallModal | undefined>,
alreadyInstalledModal: Ref<AlreadyInstalledModal | undefined>,
accountRequiredModal: Ref<AccountRequiredModal | undefined>,
) {
const auth = injectAuth()
const client = injectModrinthClient()
const { handleError } = injectNotificationManager()
const { notifySharedInstanceConnectionError, notifySharedInstanceError } =
useSharedInstanceErrors()
const popupNotificationManager = injectPopupNotificationManager()
const queryClient = useQueryClient()
const router = useRouter()
const themeStore = useTheming()
const displayedNotifications = new Set<string | number>()
const displayedNotificationKeys = new Set<string>()
const popupNotificationIds = new Set<string | number>()
let notificationGeneration = 0
let pendingAlreadyInstalled:
| {
instanceId: string
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>
install: () => Promise<void>
creator?: SharedInstanceCreator
onGoToInstance?: () => void | Promise<void>
}
| undefined
async function markNotificationRead(notification: AppNotification) {
try {
await client.labrinth.notifications_v2.markAsRead(String(notification.id))
} catch (error) {
if (error instanceof ModrinthApiError && error.statusCode === 404) return
throw error
}
}
async function resolveInvite(invite: SharedInstanceInvite) {
const [invitedBy, sharedInstance] = await Promise.all([
(!invite.invitedByUsername || !invite.invitedByAvatarUrl) && invite.invitedById
? get_user(invite.invitedById, 'bypass').catch(() => null)
: null,
client.sharedinstances.instances_v1.get(invite.sharedInstanceId).catch(() => {
notifySharedInstanceConnectionError()
return null
}),
])
return {
...invite,
invitedByUsername: invite.invitedByUsername ?? invitedBy?.username ?? null,
invitedByAvatarUrl: invite.invitedByAvatarUrl ?? invitedBy?.avatar_url ?? null,
instanceIconUrl: sharedInstance ? sharedInstance.icon : invite.instanceIconUrl,
}
}
function showInstall(
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>,
install: () => Promise<void>,
creator?: SharedInstanceCreator,
) {
if (!installModal.value) throw new Error('Shared instance install modal is not available.')
installModal.value.show(preview, install, creator)
}
async function showInstallOrAlreadyInstalled(
sharedInstanceId: string,
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>,
install: () => Promise<void>,
creator?: SharedInstanceCreator,
onGoToInstance?: () => void | Promise<void>,
) {
const existingInstance = (await list()).find(
(instance) => instance.shared_instance?.id === sharedInstanceId,
)
if (!existingInstance || themeStore.getFeatureFlag('skip_non_essential_warnings')) {
showInstall(preview, install, creator)
return
}
if (!alreadyInstalledModal.value) {
throw new Error('Shared instance already installed modal is not available.')
}
pendingAlreadyInstalled = {
instanceId: existingInstance.id,
preview,
install,
creator,
onGoToInstance,
}
alreadyInstalledModal.value.show(existingInstance.name)
}
function handleAlreadyInstalledCancel() {
pendingAlreadyInstalled = undefined
}
async function handleAlreadyInstalledGoToInstance() {
const pending = pendingAlreadyInstalled
pendingAlreadyInstalled = undefined
if (!pending) return
if (pending.onGoToInstance) {
try {
await pending.onGoToInstance()
} catch (error) {
handleError(toError(error))
}
}
await router.push(`/instance/${encodeURIComponent(pending.instanceId)}/`)
}
function handleAlreadyInstalledInstallAnyway() {
const pending = pendingAlreadyInstalled
pendingAlreadyInstalled = undefined
if (!pending) return
showInstall(pending.preview, pending.install, pending.creator)
}
async function acceptNotification(notification: AppNotification, invite: SharedInstanceInvite) {
try {
const preview = await install_get_shared_instance_preview(
invite.sharedInstanceId,
invite.sharedInstanceName,
)
if (invite.instanceIconUrl) preview.iconUrl = invite.instanceIconUrl
await showInstallOrAlreadyInstalled(
invite.sharedInstanceId,
preview,
async () => {
await install_shared_instance(
invite.sharedInstanceId,
invite.sharedInstanceName,
invite.invitedById,
null,
null,
invite.instanceIconUrl,
)
await markNotificationRead(notification)
await queryClient.invalidateQueries({ queryKey: ['instances'] })
},
invite.invitedByUsername
? {
id: invite.invitedById,
username: invite.invitedByUsername,
avatarUrl: invite.invitedByAvatarUrl,
}
: undefined,
() => markNotificationRead(notification),
)
} catch (error) {
notifySharedInstanceError(error)
}
}
async function handleNotification(notification: AppNotification) {
const parsedInvite = parseSharedInstanceInviteNotification(notification)
if (!parsedInvite) return false
if (displayedNotifications.has(notification.id)) return true
const generation = notificationGeneration
displayedNotifications.add(notification.id)
const invite = await resolveInvite(parsedInvite)
if (generation !== notificationGeneration) return true
const notificationKey = JSON.stringify([
invite.invitedById ?? invite.invitedByUsername,
invite.sharedInstanceName,
invite.instanceIconUrl,
])
if (displayedNotificationKeys.has(notificationKey)) {
await markNotificationRead(notification).catch((error) => handleError(toError(error)))
return true
}
displayedNotificationKeys.add(notificationKey)
const popupNotification = popupNotificationManager.addPopupNotification({
title: invite.sharedInstanceName,
autoCloseMs: null,
toast: {
type: 'instance-invite',
actorName: invite.invitedByUsername,
actorAvatarUrl: invite.invitedByAvatarUrl ?? undefined,
entityName: invite.sharedInstanceName,
entityIconUrl: invite.instanceIconUrl ?? undefined,
onAccept: () => acceptNotification(notification, invite),
onDecline: () =>
markNotificationRead(notification).catch((error) => handleError(toError(error))),
onOpenActor: () => {
if (invite.invitedByUsername) {
void router.push(`/user/${encodeURIComponent(invite.invitedByUsername)}`)
}
},
},
})
popupNotificationIds.add(popupNotification.id)
return true
}
function clearNotifications() {
notificationGeneration++
for (const id of popupNotificationIds) {
popupNotificationManager.removeNotification(id)
}
displayedNotifications.clear()
displayedNotificationKeys.clear()
popupNotificationIds.clear()
pendingAlreadyInstalled = undefined
}
async function requireAccount() {
if (!auth.isReady?.value) {
await new Promise<void>((resolve) => {
const stop = watch(auth.isReady!, (ready) => {
if (ready) {
stop()
resolve()
}
})
})
}
if (auth.session_token.value) return true
return (await accountRequiredModal.value?.show()) ?? false
}
async function installFromInviteId(inviteId: string) {
try {
if (!(await requireAccount())) return
const invite = await install_accept_shared_instance_invite(inviteId)
const manager = invite.managerId
? await get_user(invite.managerId, 'bypass').catch(() => null)
: null
await showInstallOrAlreadyInstalled(
invite.sharedInstanceId,
invite.preview,
async () => {
await install_shared_instance(
invite.sharedInstanceId,
invite.preview.name,
invite.managerId,
invite.serverManagerName,
invite.serverManagerIconUrl,
invite.instanceIconUrl,
)
await queryClient.invalidateQueries({ queryKey: ['instances'] })
},
manager
? {
id: manager.id,
username: manager.username,
avatarUrl: manager.avatar_url ?? null,
}
: undefined,
)
} catch (error) {
notifySharedInstanceError(error)
}
}
return {
handleNotification,
installFromInviteId,
clearNotifications,
handleAlreadyInstalledCancel,
handleAlreadyInstalledGoToInstance,
handleAlreadyInstalledInstallAnyway,
}
}
@@ -11,6 +11,7 @@
<SkinPreviewRenderer
:variant="variant"
:texture-src="previewSkin || ''"
:ears-texture-src="uploadedTextureUrl?.original ?? currentSkin?.texture"
:cape-src="selectedCapeTexture"
framing="modal"
:initial-rotation="Math.PI / 8"
@@ -80,6 +80,7 @@ const playing = ref(false)
const play = async (event: MouseEvent) => {
event?.stopPropagation()
if (props.instance.quarantined) return
loading.value = true
await run(props.instance.id)
.catch((err) => handleSevereError(err, { instanceId: props.instance.id }))
@@ -192,8 +193,14 @@ onUnmounted(() => {
</ButtonStyled>
<ButtonStyled v-else>
<button
v-tooltip="playing ? 'Instance is already open' : null"
:disabled="playing || loading"
v-tooltip="
instance.quarantined
? 'This instance has been locked'
: playing
? 'Instance is already open'
: null
"
:disabled="instance.quarantined || playing || loading"
@click="play"
>
<SpinnerIcon v-if="loading" class="animate-spin" />
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { LoaderCircleIcon } from '@modrinth/assets'
import type { GameVersion } from '@modrinth/ui'
import { GAME_MODES, HeadingLink, injectNotificationManager } from '@modrinth/ui'
import { GAME_MODES, injectNotificationManager } from '@modrinth/ui'
import { platform } from '@tauri-apps/plugin-os'
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
@@ -176,6 +176,7 @@ function refreshServer(address: string, instanceId: string) {
}
async function joinWorld(world: WorldWithInstance, instance?: GameInstance) {
if (instance?.quarantined) return
console.log(`Joining world ${getWorldIdentifier(world)}`)
if (world.type === 'server') {
await start_join_server(world.instance_id, world.address).catch(handleError)
@@ -192,6 +193,7 @@ async function joinWorld(world: WorldWithInstance, instance?: GameInstance) {
}
async function playInstance(instance: GameInstance) {
if (instance.quarantined) return
await run(instance.id)
.catch((err) => handleSevereError(err, { instanceId: instance.id }))
.finally(() => {
@@ -266,13 +268,7 @@ onUnmounted(() => {
</div>
</div>
<div v-else-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
<HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
Jump back in
</HeadingLink>
<span
v-else
class="flex mt-1 mb-3 leading-none items-center gap-1 text-primary text-lg font-bold"
>
<span class="flex mt-1 mb-3 leading-none items-center gap-1 text-primary text-lg font-bold">
Jump back in
</span>
<div class="grid-when-huge flex flex-col w-full gap-2">
@@ -300,6 +296,7 @@ onUnmounted(() => {
item.world.type === 'singleplayer' &&
hasWorldQuickPlaySupport(gameVersions, item.instance.game_version || '')
"
:quarantined="item.instance.quarantined"
:server-status="
item.world.type === 'server' ? serverData[item.world.address].status : undefined
"
@@ -77,6 +77,7 @@ const props = withDefaults(
startingInstance?: boolean
supportsServerQuickPlay?: boolean
supportsWorldQuickPlay?: boolean
quarantined?: boolean
currentProtocol?: ProtocolVersion | null
highlighted?: boolean
@@ -105,6 +106,7 @@ const props = withDefaults(
startingInstance: false,
supportsServerQuickPlay: true,
supportsWorldQuickPlay: false,
quarantined: false,
currentProtocol: null,
refreshing: false,
@@ -139,7 +141,7 @@ const managed = computed(() => props.managed)
const shortcutInstanceId = computed(() => props.shortcutInstanceId ?? props.instanceId)
async function createShortcut() {
if (!shortcutInstanceId.value) return
if (!shortcutInstanceId.value || props.quarantined) return
try {
const shortcutPath = await createInstanceShortcut(
@@ -422,23 +424,26 @@ const messages = defineMessages({
<ButtonStyled v-else>
<button
v-tooltip="
world.type === 'server'
? !supportsServerQuickPlay
? formatMessage(messages.noServerQuickPlay)
: playingOtherWorld
? formatMessage(messages.gameAlreadyOpen)
: !serverStatus
? formatMessage(messages.noContact)
: serverIncompatible
? formatMessage(messages.incompatibleServer)
: null
: !supportsWorldQuickPlay
? formatMessage(messages.noSingleplayerQuickPlay)
: playingOtherWorld || locked
? formatMessage(messages.gameAlreadyOpen)
: null
quarantined
? 'This instance has been locked'
: world.type === 'server'
? !supportsServerQuickPlay
? formatMessage(messages.noServerQuickPlay)
: playingOtherWorld
? formatMessage(messages.gameAlreadyOpen)
: !serverStatus
? formatMessage(messages.noContact)
: serverIncompatible
? formatMessage(messages.incompatibleServer)
: null
: !supportsWorldQuickPlay
? formatMessage(messages.noSingleplayerQuickPlay)
: playingOtherWorld || locked
? formatMessage(messages.gameAlreadyOpen)
: null
"
:disabled="
quarantined ||
playingOtherWorld ||
startingInstance ||
(world.type == 'server' && !supportsServerQuickPlay) ||
@@ -457,7 +462,7 @@ const messages = defineMessages({
{
id: 'play-instance',
shown: !!instanceId,
disabled: playingInstance,
disabled: playingInstance || quarantined,
action: () => emit('play-instance'),
},
{
@@ -511,7 +516,7 @@ const messages = defineMessages({
},
{
id: 'create-shortcut',
shown: !!shortcutInstanceId,
shown: !!shortcutInstanceId && !quarantined,
action: () => createShortcut(),
},
{
@@ -23,6 +23,7 @@ import {
type InstallProgress,
} from '@/helpers/install'
import { get_many as getInstances } from '@/helpers/instance'
import { useTheming } from '@/store/state'
const messages = defineMessages({
installs: {
@@ -53,6 +54,10 @@ const messages = defineMessages({
id: 'app.action-bar.install.unknown-instance',
defaultMessage: 'Unknown instance',
},
updatingSharedContent: {
id: 'app.action-bar.install.updating-shared-content',
defaultMessage: 'Updating shared content',
},
})
const phaseMessages = defineMessages({
@@ -217,13 +222,6 @@ const failureSummaryMessages = defineMessages({
})
const visibleJobStatuses = new Set<InstallJobStatus>(['queued', 'running', 'failed', 'interrupted'])
const copyDetailsStallMs = 30_000
interface ProgressSnapshot {
signature: string
changedAt: number
timeout: number | null
}
function getDisplayIconUrl(icon: string | null | undefined): string | null {
if (!icon) return null
@@ -237,6 +235,7 @@ export async function useInstallJobNotifications(opts: {
onChange: () => void
}) {
const { formatMessage } = useVIntl()
const themeStore = useTheming()
const jobs = ref<InstallJobSnapshot[]>([])
const iconUrls = ref<Record<string, string | null>>({})
const instanceNames = ref<Record<string, string>>({})
@@ -246,7 +245,6 @@ export async function useInstallJobNotifications(opts: {
let metadataRequest = 0
let nextJobOrder = 0
const copiedResetTimeouts = new Map<string, number>()
const progressSnapshots = new Map<string, ProgressSnapshot>()
function getTitle(job: InstallJobSnapshot): string {
if (job.display?.title) return job.display.title
@@ -268,6 +266,9 @@ export async function useInstallJobNotifications(opts: {
version: job.details.major_version,
})
}
if (job.kind === 'update_shared_instance' && job.phase === 'downloading_content') {
return formatMessage(messages.updatingSharedContent)
}
return formatMessage(phaseMessages[job.phase])
}
@@ -414,116 +415,14 @@ export async function useInstallJobNotifications(opts: {
return job.status === 'failed' || job.status === 'interrupted'
}
function canShowStalledProgressDetails(job: InstallJobSnapshot): boolean {
return (
job.status === 'running' &&
job.phase !== 'preparing_instance' &&
job.phase !== 'finalizing' &&
job.phase !== 'rolling_back'
)
}
function getJobSortRank(job: InstallJobSnapshot): number {
if (isTerminalJob(job)) return 0
if (job.status === 'queued' || job.phase === 'preparing_instance') return 2
return 1
}
function progressSignature(job: InstallJobSnapshot): string {
const progress = job.progress
const secondary = progress?.secondary
return [
job.status,
job.phase,
JSON.stringify(job.details),
progress?.current ?? '',
progress?.total ?? '',
secondary?.current ?? '',
secondary?.total ?? '',
].join(':')
}
function clearCopied(jobId: string) {
if (!copiedJobIds.value.has(jobId)) {
return
}
const timeout = copiedResetTimeouts.get(jobId)
if (timeout != null) {
window.clearTimeout(timeout)
copiedResetTimeouts.delete(jobId)
}
const nextCopiedJobIds = new Set(copiedJobIds.value)
nextCopiedJobIds.delete(jobId)
copiedJobIds.value = nextCopiedJobIds
}
function clearProgressSnapshot(jobId: string) {
const snapshot = progressSnapshots.get(jobId)
if (snapshot?.timeout != null) {
window.clearTimeout(snapshot.timeout)
}
progressSnapshots.delete(jobId)
}
function scheduleStaleProgressRefresh(jobId: string) {
const snapshot = progressSnapshots.get(jobId)
if (!snapshot) {
return
}
snapshot.timeout = window.setTimeout(() => {
const snapshot = progressSnapshots.get(jobId)
if (!snapshot) {
return
}
snapshot.timeout = null
opts.onChange()
}, copyDetailsStallMs)
}
function syncProgressSnapshots(nextJobs: InstallJobSnapshot[]) {
const trackedJobIds = new Set<string>()
const now = Date.now()
for (const job of nextJobs) {
if (!canShowStalledProgressDetails(job)) {
continue
}
trackedJobIds.add(job.job_id)
const signature = progressSignature(job)
const snapshot = progressSnapshots.get(job.job_id)
if (snapshot?.signature === signature) {
continue
}
clearProgressSnapshot(job.job_id)
clearCopied(job.job_id)
progressSnapshots.set(job.job_id, {
signature,
changedAt: now,
timeout: null,
})
scheduleStaleProgressRefresh(job.job_id)
}
for (const jobId of progressSnapshots.keys()) {
if (!trackedJobIds.has(jobId)) {
clearProgressSnapshot(jobId)
}
}
}
function hasStalledProgress(job: InstallJobSnapshot): boolean {
const snapshot = progressSnapshots.get(job.job_id)
return !!snapshot && Date.now() - snapshot.changedAt >= copyDetailsStallMs
}
function shouldShowCopyDetails(job: InstallJobSnapshot): boolean {
return isTerminalJob(job) || (canShowStalledProgressDetails(job) && hasStalledProgress(job))
return isTerminalJob(job) || themeStore.getFeatureFlag('always_show_copy_details')
}
function isCopied(job: InstallJobSnapshot): boolean {
@@ -600,6 +499,16 @@ export async function useInstallJobNotifications(opts: {
return buttons
}
function getDismissHandler(job: InstallJobSnapshot): (() => Promise<void>) | undefined {
if (isTerminalJob(job)) {
return async () => {
await install_job_dismiss(job.job_id).catch(opts.handleError)
await refresh()
}
}
return undefined
}
function setJobs(nextJobs: InstallJobSnapshot[]) {
for (const job of nextJobs) {
if (!jobOrder.has(job.job_id)) {
@@ -608,7 +517,6 @@ export async function useInstallJobNotifications(opts: {
}
const visibleJobs = nextJobs.filter((job) => visibleJobStatuses.has(job.status))
syncProgressSnapshots(visibleJobs)
jobs.value = visibleJobs.sort(
(a, b) =>
@@ -635,12 +543,8 @@ export async function useInstallJobNotifications(opts: {
progressCurrent: isTerminalJob(job) ? undefined : progress?.current,
progressTotal: isTerminalJob(job) ? undefined : progress?.total,
buttons: getButtons(job),
onDismiss: isTerminalJob(job)
? async () => {
await install_job_dismiss(job.job_id).catch(opts.handleError)
await refresh()
}
: undefined,
dismissible: isTerminalJob(job),
onDismiss: getDismissHandler(job),
}
}),
)
@@ -723,8 +627,8 @@ export async function useInstallJobNotifications(opts: {
void refreshMetadata()
}
await refresh(false)
const unlisten = await install_job_listener((job: InstallJobSnapshot) => applyJobUpdate(job))
await refresh(false)
return {
active: computed(() => jobs.value.length > 0),
@@ -736,9 +640,6 @@ export async function useInstallJobNotifications(opts: {
for (const timeout of copiedResetTimeouts.values()) {
window.clearTimeout(timeout)
}
for (const jobId of progressSnapshots.keys()) {
clearProgressSnapshot(jobId)
}
unlisten()
},
}
@@ -27,14 +27,14 @@ interface BrowseServerInstance {
interface ContextMenuHandle {
showMenu: (
event: MouseEvent,
result: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
result: Labrinth.Search.v3.ResultSearchProject,
options: { name: string }[],
) => void
}
interface ContextMenuOptionClick {
option: 'open_link' | 'copy_link'
item: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject
item: Labrinth.Search.v3.ResultSearchProject
}
export interface UseAppServerBrowseOptions {
@@ -263,10 +263,7 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
return actions
}
function handleRightClick(
event: MouseEvent,
result: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
) {
function handleRightClick(event: MouseEvent, result: Labrinth.Search.v3.ResultSearchProject) {
contextMenuRef.value?.showMenu(event, result, [{ name: 'open_link' }, { name: 'copy_link' }])
}
@@ -315,9 +312,7 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
}
}
function getProjectUrl(
item: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
) {
const projectType = 'project_types' in item ? item.project_types?.[0] : item.project_type
function getProjectUrl(item: Labrinth.Search.v3.ResultSearchProject) {
const projectType = item.project_types?.[0]
return `https://modrinth.com/${projectType ?? 'project'}/${item.slug ?? item.project_id}`
}
@@ -0,0 +1,124 @@
import { type ContentActionWarning, type ContentItem, defineMessages, useVIntl } from '@modrinth/ui'
import { computed, type Ref } from 'vue'
import type { GameInstance } from '@/helpers/types'
const managedSourceKinds = new Set(['shared_instance', 'modrinth_modpack', 'imported_modpack'])
export function useManagedContentPolicy(instance: Ref<GameInstance>) {
const { formatMessage } = useVIntl()
const isManagedModpack = computed(() => instance.value.shared_instance?.role === 'member')
const isQuarantined = computed(() => instance.value.quarantined)
const canUnpublish = computed(() => instance.value.shared_instance?.role === 'owner')
const canUnlink = computed(() => instance.value.shared_instance?.role === 'member')
const managedModpackWarning = computed(() => ({
admonitionHeader: formatMessage(messages.warningHeader),
changeVersionBody: formatMessage(messages.changeVersionBody),
unlinkBody: formatMessage(messages.unlinkBody),
}))
function isManagedContent(item: ContentItem) {
return (
isQuarantined.value ||
(isManagedModpack.value && !!item.source_kind && managedSourceKinds.has(item.source_kind))
)
}
function canMutateContent(item: ContentItem) {
return !isManagedContent(item)
}
function canUpdateContent(item: ContentItem) {
return (
canMutateContent(item) && !!item.file_path && !!item.has_update && !!item.update_version_id
)
}
function deleteWarning(items: ContentItem[]): ContentActionWarning | null {
if (!items.some(isManagedContent)) return null
return {
admonitionHeader: formatMessage(messages.warningHeader),
admonitionBody: formatMessage(
items.length === 1 ? messages.deleteSingleBody : messages.deleteBulkBody,
),
actionLabel: formatMessage(
items.length === 1 ? messages.deleteButton : messages.deleteManyButton,
{ count: items.length },
),
}
}
function disableWarning(items: ContentItem[]): ContentActionWarning | null {
if (!items.some(isManagedContent)) return null
return {
admonitionHeader: formatMessage(messages.warningHeader),
admonitionBody: formatMessage(
items.length === 1 ? messages.disableSingleBody : messages.disableBulkBody,
),
actionLabel: formatMessage(
items.length === 1 ? messages.disableButton : messages.disableManyButton,
{ count: items.length },
),
}
}
return {
isManagedModpack,
isQuarantined,
canUnpublish,
canUnlink,
managedModpackWarning,
isManagedContent,
canMutateContent,
canUpdateContent,
deleteWarning,
disableWarning,
}
}
const messages = defineMessages({
warningHeader: {
id: 'content.shared-instance.warning-header',
defaultMessage: 'This is part of the shared instance',
},
changeVersionBody: {
id: 'content.shared-instance.change-version-body',
defaultMessage:
'Changing the version only changes your local copy. Future shared instance updates may restore or change it again.',
},
unlinkBody: {
id: 'content.shared-instance.unlink-body',
defaultMessage:
'Unlinking only changes your local copy. Future shared instance updates may restore or change it again.',
},
deleteSingleBody: {
id: 'content.shared-instance.delete-single-body',
defaultMessage:
'Deleting it only changes your local copy. Future shared instance updates may restore or change it again.',
},
deleteBulkBody: {
id: 'content.shared-instance.delete-bulk-body',
defaultMessage:
'Some selected projects are part of the shared instance. Deleting them only changes your local copy, and future shared instance updates may restore or change them again.',
},
deleteButton: { id: 'content.shared-instance.delete-button', defaultMessage: 'Delete anyway' },
deleteManyButton: {
id: 'content.shared-instance.delete-many-button',
defaultMessage: 'Delete {count, number} projects anyway',
},
disableSingleBody: {
id: 'content.shared-instance.disable-single-body',
defaultMessage:
'Disabling it only changes your local copy. Future shared instance updates may re-enable, restore, or change it again.',
},
disableBulkBody: {
id: 'content.shared-instance.disable-bulk-body',
defaultMessage:
'Some selected projects are part of the shared instance. Disabling them only changes your local copy, and future shared instance updates may re-enable, restore, or change them again.',
},
disableButton: { id: 'content.shared-instance.disable-button', defaultMessage: 'Disable anyway' },
disableManyButton: {
id: 'content.shared-instance.disable-many-button',
defaultMessage: 'Disable {count, number} projects anyway',
},
})
@@ -0,0 +1,148 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, type MaybeRefOrGetter, onUnmounted, toValue } from 'vue'
import { toError } from '@/helpers/errors'
import { friend_listener } from '@/helpers/events.js'
import {
acceptCachedFriend,
add_friend,
createPendingFriend,
type FriendCacheUser,
friendsQueryKey,
type FriendWithUserData,
getFriendsWithUserData,
getFriendUserId,
matchesFriend,
remove_friend,
removeCachedFriend,
upsertCachedFriend,
} from '@/helpers/friends'
import type { ModrinthCredentials } from '@/helpers/mr_auth'
type FriendsMutationContext = {
queryKey: ReturnType<typeof friendsQueryKey>
previousFriends?: FriendWithUserData[]
}
type AddFriendMutationVariables = {
userId: string
user: FriendCacheUser
acceptExisting: boolean
}
type RemoveFriendMutationVariables = {
userId: string
user: FriendWithUserData
}
export function useFriends(options: {
currentUserId: MaybeRefOrGetter<string | null | undefined>
getCredentials: () => ModrinthCredentials | null | Promise<ModrinthCredentials | null>
enabled?: MaybeRefOrGetter<boolean>
onError?: (error: Error) => void
}) {
const queryClient = useQueryClient()
const queryKey = computed(() => friendsQueryKey(toValue(options.currentUserId)))
const query = useQuery({
queryKey,
queryFn: async () => getFriendsWithUserData(await options.getCredentials()),
enabled: () => !!toValue(options.currentUserId) && toValue(options.enabled ?? true),
staleTime: 30_000,
})
const friends = computed(() => query.data.value ?? [])
function restore(context?: FriendsMutationContext) {
if (!context) return
if (context.previousFriends === undefined) {
queryClient.removeQueries({ queryKey: context.queryKey, exact: true })
return
}
queryClient.setQueryData(context.queryKey, context.previousFriends)
}
const addMutation = useMutation({
mutationFn: ({ userId }: AddFriendMutationVariables) => add_friend(userId),
onMutate: async ({ user, acceptExisting }): Promise<FriendsMutationContext> => {
const activeQueryKey = queryKey.value
await queryClient.cancelQueries({ queryKey: activeQueryKey })
const previousFriends = queryClient.getQueryData<FriendWithUserData[]>(activeQueryKey)
const currentUserId = toValue(options.currentUserId)
queryClient.setQueryData<FriendWithUserData[]>(activeQueryKey, (cachedFriends = []) =>
acceptExisting
? acceptCachedFriend(cachedFriends, user.id, user.username, currentUserId)
: upsertCachedFriend(
cachedFriends,
createPendingFriend(user, currentUserId),
currentUserId,
),
)
return { queryKey: activeQueryKey, previousFriends }
},
onError: (error, _variables, context) => {
restore(context)
options.onError?.(toError(error))
},
onSettled: (_data, _error, _variables, context) => {
void queryClient.invalidateQueries({ queryKey: context?.queryKey ?? queryKey.value })
},
})
const removeMutation = useMutation({
mutationFn: ({ userId }: RemoveFriendMutationVariables) => remove_friend(userId),
onMutate: async ({ user, userId }): Promise<FriendsMutationContext> => {
const activeQueryKey = queryKey.value
await queryClient.cancelQueries({ queryKey: activeQueryKey })
const previousFriends = queryClient.getQueryData<FriendWithUserData[]>(activeQueryKey)
queryClient.setQueryData<FriendWithUserData[]>(activeQueryKey, (cachedFriends = []) =>
removeCachedFriend(cachedFriends, userId, user.username, toValue(options.currentUserId)),
)
return { queryKey: activeQueryKey, previousFriends }
},
onError: (error, _variables, context) => {
restore(context)
options.onError?.(toError(error))
},
onSettled: (_data, _error, _variables, context) => {
void queryClient.invalidateQueries({ queryKey: context?.queryKey ?? queryKey.value })
},
})
function requestFriend(user: FriendCacheUser, acceptExisting = false) {
addMutation.mutate({ userId: user.id, user, acceptExisting })
}
function acceptFriend(friend: FriendWithUserData) {
const userId = getFriendUserId(friend, toValue(options.currentUserId))
requestFriend({ id: userId, username: friend.username, avatarUrl: friend.avatar }, true)
}
function removeFriend(friend: FriendWithUserData) {
const userId = getFriendUserId(friend, toValue(options.currentUserId))
removeMutation.mutate({ userId, user: friend })
}
function findFriend(id: string, username: string) {
return friends.value.find((friend) =>
matchesFriend(friend, id, username, toValue(options.currentUserId)),
)
}
let unlisten: (() => void) | undefined
void friend_listener(() => {
void queryClient.invalidateQueries({ queryKey: queryKey.value })
}).then((listener) => {
unlisten = listener
})
onUnmounted(() => unlisten?.())
return {
query,
queryKey,
friends,
loading: computed(() => !!toValue(options.currentUserId) && query.isLoading.value),
requestFriend,
acceptFriend,
removeFriend,
findFriend,
}
}
@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/vue-query'
import { computed, type Ref } from 'vue'
import { get_user } from '@/helpers/cache.js'
export function useUserQuery(userId: Ref<string | null | undefined>) {
return useQuery({
queryKey: computed(() => ['user', userId.value]),
queryFn: async ({ queryKey }) => {
const id = queryKey[1]
if (typeof id !== 'string') return null
return await get_user(id, 'bypass').catch(() => null)
},
enabled: () => !!userId.value,
staleTime: 30_000,
})
}
+4
View File
@@ -7,6 +7,9 @@ const labrinthBaseUrl = trimTrailingSlash(
const archonBaseUrl = trimTrailingSlash(
import.meta.env.MODRINTH_ARCHON_BASE_URL || 'https://archon.modrinth.com',
)
const sharedInstancesBaseUrl = trimTrailingSlash(
import.meta.env.SHARED_INSTANCES_API_BASE_URL || 'https://shared-instances.modrinth.com',
)
export const config = {
siteUrl,
@@ -15,4 +18,5 @@ export const config = {
'pk_test_51JbFxJJygY5LJFfKV50mnXzz3YLvBVe2Gd1jn7ljWAkaBlRz3VQdxN9mXcPSrFbSqxwAb0svte9yhnsmm7qHfcWn00R611Ce7b',
labrinthBaseUrl,
archonBaseUrl,
sharedInstancesBaseUrl,
}
+17
View File
@@ -1,4 +1,5 @@
import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event'
export async function init_ads_window(overrideShown = false) {
return await invoke('plugin:ads|init_ads_window', {
@@ -15,6 +16,22 @@ export async function hide_ads_window(reset) {
return await invoke('plugin:ads|hide_ads_window', { reset })
}
export async function should_show_ads_consent_popup() {
return await invoke('plugin:ads|should_show_ads_consent_popup')
}
export async function perform_ads_consent_action(action) {
return await invoke('plugin:ads|perform_ads_consent_action', { action })
}
export async function open_ads_consent_preferences() {
return await invoke('plugin:ads|open_ads_consent_preferences')
}
export async function ads_consent_listener(callback) {
return await listen('ads-consent-required', (event) => callback(event.payload))
}
export async function record_ads_click() {
return await invoke('plugin:ads|record_ads_click')
}
+11
View File
@@ -0,0 +1,11 @@
export function toError(error: unknown) {
if (error instanceof Error) return error
if (typeof error === 'string') return new Error(error)
if (error && typeof error === 'object') {
const record = error as Record<string, unknown>
const message = record.message ?? record.error
if (typeof message === 'string') return new Error(message)
return new Error(JSON.stringify(error))
}
return new Error(String(error))
}
+107
View File
@@ -6,6 +6,8 @@ import dayjs from 'dayjs'
import { get_user_many } from '@/helpers/cache'
import type { ModrinthCredentials } from '@/helpers/mr_auth'
export const friendsQueryKey = (userId?: string | null) => ['friends', userId ?? null] as const
export type UserStatus = {
user_id: string
instance_name: string | null
@@ -46,6 +48,111 @@ export type FriendWithUserData = {
online: boolean
avatar: string
}
export type FriendCacheUser = {
id: string
username: string
avatarUrl?: string | null
}
export async function getFriendsWithUserData(
credentials: ModrinthCredentials | null,
): Promise<FriendWithUserData[]> {
if (!credentials) return []
const friendsList = await friends()
return await transformFriends(friendsList, credentials)
}
export function createPendingFriend(
user: FriendCacheUser,
currentUserId?: string | null,
): FriendWithUserData {
return {
id: user.id,
friend_id: currentUserId ?? null,
status: null,
last_updated: null,
created: dayjs(),
username: user.username,
accepted: false,
online: false,
avatar: user.avatarUrl ?? '',
}
}
export function getFriendUserId(
friend: Pick<FriendWithUserData, 'id' | 'friend_id'>,
currentUserId?: string | null,
) {
return friend.id === currentUserId && friend.friend_id ? friend.friend_id : friend.id
}
export function matchesFriend(
friend: FriendWithUserData,
id: string,
username: string,
currentUserId?: string | null,
) {
const friendId = getFriendUserId(friend, currentUserId)
return (
normalizeFriendKey(friendId) === normalizeFriendKey(id) ||
normalizeFriendKey(friend.username) === normalizeFriendKey(username)
)
}
export function upsertCachedFriend(
friends: FriendWithUserData[],
friend: FriendWithUserData,
currentUserId?: string | null,
) {
const existingFriend = friends.find((cachedFriend) =>
matchesFriend(cachedFriend, friend.id, friend.username, currentUserId),
)
if (!existingFriend) return [friend, ...friends]
return friends.map((cachedFriend) =>
matchesFriend(cachedFriend, friend.id, friend.username, currentUserId)
? {
...cachedFriend,
...friend,
id: cachedFriend.id,
friend_id: cachedFriend.friend_id,
}
: cachedFriend,
)
}
export function acceptCachedFriend(
friends: FriendWithUserData[],
id: string,
username: string,
currentUserId?: string | null,
) {
return friends.map((friend) =>
matchesFriend(friend, id, username, currentUserId)
? {
...friend,
accepted: true,
}
: friend,
)
}
export function removeCachedFriend(
friends: FriendWithUserData[],
id: string,
username: string,
currentUserId?: string | null,
) {
return friends.filter((friend) => !matchesFriend(friend, id, username, currentUserId))
}
export function normalizeFriendKey(value: string) {
return value.trim().toLowerCase()
}
export async function transformFriends(
friends: UserFriend[],
credentials: ModrinthCredentials | null,
+148 -1
View File
@@ -45,6 +45,97 @@ export interface InstallPostInstallEdit {
link?: InstanceLink | null
}
export interface SharedInstanceInstallPreview {
sharedInstanceId: string
version: number
name: string
iconUrl?: string | null
gameVersion: string
loader: InstanceLoader
modCount: number
externalFileCount: number
modpackVersionId?: string | null
contentVersionIds: string[]
externalFiles: SharedInstanceExternalFilePreview[]
}
export interface SharedInstanceInviteInstallPreview {
sharedInstanceId: string
managerId?: string | null
serverManagerName?: string | null
serverManagerIconUrl?: string | null
instanceIconUrl?: string | null
preview: SharedInstanceInstallPreview
}
export interface SharedInstanceExternalFilePreview {
fileName: string
fileType: string
}
export interface SharedInstanceUpdatePreview {
sharedInstanceId: string
currentVersion?: number | null
latestVersion: number
updateAvailable: boolean
diffs: SharedInstanceUpdateDiff[]
}
export interface SharedInstanceUpdateDiff {
type:
| 'added'
| 'removed'
| 'updated'
| 'modpack_linked'
| 'modpack_updated'
| 'modpack_unlinked'
| 'game_version_updated'
| 'loader_updated'
| 'config_files_updated'
projectId?: string | null
projectName?: string | null
fileName?: string | null
currentVersionName?: string | null
newVersionName?: string | null
configFileCount?: number | null
disabled?: boolean
}
export const SHARED_INSTANCE_UNAVAILABLE_ERROR_CODE = 'shared_instance_unavailable'
export const SHARED_INSTANCES_API_ERROR_CODE = 'shared_instances_api_error'
export type SharedInstanceUnavailableReason = 'deleted' | 'access_revoked' | 'quarantined'
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
export function isSharedInstanceUnavailableError(error: unknown) {
return getSharedInstanceUnavailableReason(error) !== null
}
export function isSharedInstancesApiError(error: unknown) {
return isRecord(error) && error.code === SHARED_INSTANCES_API_ERROR_CODE
}
export function getSharedInstanceUnavailableReason(
error: unknown,
): SharedInstanceUnavailableReason | null {
if (!isRecord(error) || error.code !== SHARED_INSTANCE_UNAVAILABLE_ERROR_CODE) return null
return error.reason === 'deleted' ||
error.reason === 'access_revoked' ||
error.reason === 'quarantined'
? error.reason
: null
}
export function getErrorMessage(error: unknown): string {
if (typeof error === 'string') return error
if (error instanceof Error) return error.message || 'Unknown error'
if (isRecord(error) && typeof error.message === 'string') return error.message
return 'Unknown error'
}
export type InstallJobStatus =
| 'queued'
| 'running'
@@ -90,6 +181,7 @@ export interface InstallErrorView {
code: string
phase?: InstallPhaseId | null
message: string
reason?: SharedInstanceUnavailableReason | null
api?: {
error: string
status?: number | null
@@ -122,6 +214,8 @@ export interface InstallJobSnapshot {
kind:
| 'create_instance'
| 'create_modpack_instance'
| 'create_shared_instance'
| 'update_shared_instance'
| 'import_instance'
| 'duplicate_instance'
| 'install_existing_instance'
@@ -172,6 +266,58 @@ export async function install_create_modpack_instance(
})
}
export async function install_get_shared_instance_preview(sharedInstanceId: string, name: string) {
return await invoke<SharedInstanceInstallPreview>(
'plugin:install|install_get_shared_instance_preview',
{
sharedInstanceId,
name,
},
)
}
export async function install_accept_shared_instance_invite(inviteId: string) {
return await invoke<SharedInstanceInviteInstallPreview>(
'plugin:install|install_accept_shared_instance_invite',
{
inviteId,
},
)
}
export async function install_get_shared_instance_update_preview(instanceId: string) {
return await invoke<SharedInstanceUpdatePreview | null>(
'plugin:install|install_get_shared_instance_update_preview',
{
instanceId,
},
)
}
export async function install_shared_instance(
sharedInstanceId: string,
name: string,
managerId?: string | null,
serverManagerName?: string | null,
serverManagerIconUrl?: string | null,
instanceIconUrl?: string | null,
) {
return await invoke<InstallJobSnapshot>('plugin:install|install_shared_instance', {
sharedInstanceId,
name,
managerId,
serverManagerName,
serverManagerIconUrl,
instanceIconUrl,
})
}
export async function install_update_shared_instance(instanceId: string) {
return await invoke<InstallJobSnapshot>('plugin:install|install_update_shared_instance', {
instanceId,
})
}
export async function install_import_instance(
launcherType: string,
basePath: string,
@@ -249,7 +395,8 @@ export function isInstallJobFinished(status: InstallJobStatus) {
function settleInstallJob(job: InstallJobSnapshot) {
if (job.status === 'succeeded') return job
throw new Error(job.error?.message ?? `Install job ${job.job_id} ${job.status}`)
if (job.error) throw job.error
throw new Error(`Install job ${job.job_id} ${job.status}`)
}
export async function wait_for_install_job(jobId: string) {
+112 -1
View File
@@ -7,13 +7,14 @@ import type { Labrinth } from '@modrinth/api-client'
import type { ContentItem, ContentOwner } from '@modrinth/ui'
import { invoke } from '@tauri-apps/api/core'
import type { InstallJobSnapshot } from './install'
import type { InstallJobSnapshot, SharedInstanceUpdateDiff } from './install'
import type {
CacheBehaviour,
ContentFile,
ContentFileProjectType,
GameInstance,
InstanceLoader,
SharedInstanceAttachment,
} from './types'
export async function remove(instanceId: string): Promise<void> {
@@ -335,3 +336,113 @@ export async function edit(instanceId: string, editInstance: Partial<GameInstanc
export async function edit_icon(instanceId: string, iconPath: string | null): Promise<void> {
return await invoke('plugin:instance|instance_edit_icon', { instanceId, iconPath })
}
export type SharedInstanceUsers = {
user_ids: string[]
users: SharedInstanceUser[]
tokens: number
}
export type SharedInstanceJoinType = 'owner' | 'invite' | 'link'
export type SharedInstanceUser = {
id: string
joined_at?: string | null
join_type: SharedInstanceJoinType
last_played?: string | null
}
export interface SharedInstancePublishPreview {
sharedInstanceId: string
latestVersion: number
diffs: SharedInstanceUpdateDiff[]
configFiles: string[]
}
export interface SharedInstanceInviteLink {
inviteId: string
expiresAt: string
maxUses: number
}
export interface SharedInstanceInvite {
id: string
expiration: string
maxUses: number
uses: number
}
export async function can_current_user_use_shared_instances(): Promise<boolean> {
return await invoke('plugin:instance|instance_share_can_current_user_use')
}
export async function get_shared_instance_users(instanceId: string): Promise<SharedInstanceUsers> {
return await invoke('plugin:instance|instance_share_get_users', { instanceId })
}
export async function invite_shared_instance_users(
instanceId: string,
userIds: string[],
): Promise<SharedInstanceUsers> {
return await invoke('plugin:instance|instance_share_invite_users', { instanceId, userIds })
}
export async function create_shared_instance_invite_link(
instanceId: string,
options: {
maxAgeSeconds?: number
maxUses?: number
replaceInviteId?: string
} = {},
): Promise<SharedInstanceInviteLink> {
return await invoke('plugin:instance|instance_share_create_invite_link', {
instanceId,
...options,
})
}
export async function get_shared_instance_invites(
instanceId: string,
): Promise<SharedInstanceInvite[]> {
return await invoke('plugin:instance|instance_share_get_invites', { instanceId })
}
export async function revoke_shared_instance_invite(
instanceId: string,
inviteId: string,
): Promise<void> {
return await invoke('plugin:instance|instance_share_revoke_invite', { instanceId, inviteId })
}
export async function remove_shared_instance_users(
instanceId: string,
userIds: string[],
hasPendingRecipients: boolean,
): Promise<SharedInstanceUsers> {
return await invoke('plugin:instance|instance_share_remove_users', {
instanceId,
userIds,
hasPendingRecipients,
})
}
export async function get_shared_instance_publish_preview(
instanceId: string,
): Promise<SharedInstancePublishPreview | null> {
return await invoke('plugin:instance|instance_share_get_publish_preview', { instanceId })
}
export async function publish_shared_instance(
instanceId: string,
configPaths: string[],
): Promise<SharedInstanceAttachment> {
return await invoke('plugin:instance|instance_share_publish', { instanceId, configPaths })
}
export async function unlink_shared_instance(instanceId: string): Promise<void> {
return await invoke('plugin:instance|instance_share_unlink', { instanceId })
}
export async function unpublish_shared_instance(instanceId: string): Promise<void> {
return await invoke('plugin:instance|instance_share_unpublish', { instanceId })
}
+4 -2
View File
@@ -12,8 +12,10 @@ export type ModrinthCredentials = {
active: boolean
}
export async function login(): Promise<ModrinthCredentials> {
return await invoke('plugin:mr-auth|modrinth_login')
export type ModrinthAuthFlow = 'sign-in' | 'sign-up'
export async function login(flow: ModrinthAuthFlow = 'sign-in'): Promise<ModrinthCredentials> {
return await invoke('plugin:mr-auth|modrinth_login', { flow })
}
export async function logout(): Promise<void> {
@@ -1,8 +1,11 @@
import { ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
import {
applyCapeTexture,
applyEarsMod,
createTransparentTexture,
disposeCaches,
loadTexture,
removeEarsMod,
setupSkinModel,
} from '@modrinth/ui'
import * as THREE from 'three'
@@ -71,12 +74,13 @@ class BatchSkinRenderer {
textureUrl: string,
modelUrl: string,
capeUrl?: string,
earsTextureUrl?: string,
): Promise<RawRenderResult> {
this.initializeRenderer()
this.clearScene()
await this.setupModel(modelUrl, textureUrl, capeUrl)
await this.setupModel(modelUrl, textureUrl, capeUrl, earsTextureUrl)
const headPart = this.currentModel!.getObjectByName('Head')
let lookAtTarget: [number, number, number]
@@ -123,17 +127,29 @@ class BatchSkinRenderer {
})
}
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
private async setupModel(
modelUrl: string,
textureUrl: string,
capeUrl?: string,
earsTextureUrl?: string,
): Promise<void> {
if (!this.scene) {
throw new Error('Renderer not initialized')
}
const { model } = await setupSkinModel(modelUrl, textureUrl, capeUrl)
const [{ model }, earsTexture] = await Promise.all([
setupSkinModel(modelUrl, textureUrl, capeUrl),
earsTextureUrl ? loadTexture(earsTextureUrl) : Promise.resolve(null),
])
if (!capeUrl) {
applyCapeTexture(model, null, this.getTransparentTexture())
}
if (earsTexture) {
applyEarsMod(model, earsTexture)
}
const group = new THREE.Group()
group.add(model)
group.position.set(0, 0.3, 1.95)
@@ -154,6 +170,7 @@ class BatchSkinRenderer {
private clearScene(): void {
if (!this.scene || !this.currentModel) return
removeEarsMod(this.currentModel)
this.scene.remove(this.currentModel)
this.currentModel.clear()
this.currentModel = null
@@ -193,6 +210,7 @@ function getModelUrlForVariant(variant: string): string {
export const skinBlobUrlMap = reactive(new Map<string, RenderResult>())
export const headBlobUrlMap = reactive(new Map<string, string>())
const DEBUG_MODE = false
const SKIN_PREVIEW_RENDER_VERSION = 'ears-2-fixed-uvs'
let sharedRenderer: BatchSkinRenderer | null = null
let latestPreviewGeneration = 0
@@ -205,6 +223,10 @@ function getSharedRenderer(): BatchSkinRenderer {
return sharedRenderer
}
export function getSkinPreviewKey(skin: Skin): string {
return `${SKIN_PREVIEW_RENDER_VERSION}+${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
}
export function disposeSharedRenderer(): void {
if (sharedRenderer) {
sharedRenderer.dispose()
@@ -217,7 +239,7 @@ export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
const validHeadKeys = new Set<string>()
for (const skin of skins) {
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
const key = getSkinPreviewKey(skin)
const headKey = `${skin.texture_key}-head`
validKeys.add(key)
validHeadKeys.add(headKey)
@@ -382,9 +404,7 @@ async function generateSkinPreviewsForGeneration(
const isCurrentGeneration = () => generation === latestPreviewGeneration
try {
const skinKeys = skins.map(
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
)
const skinKeys = skins.map(getSkinPreviewKey)
const headKeys = skins.map((skin) => `${skin.texture_key}-head`)
const [cachedSkinPreviews, cachedHeadPreviews] = await Promise.all([
@@ -415,7 +435,7 @@ async function generateSkinPreviewsForGeneration(
for (const skin of skins) {
if (!isCurrentGeneration()) return
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
const key = getSkinPreviewKey(skin)
if (skinBlobUrlMap.has(key)) {
if (DEBUG_MODE) {
@@ -443,6 +463,7 @@ async function generateSkinPreviewsForGeneration(
await get_normalized_skin_texture(skin),
modelUrl,
cape?.texture,
skin.texture,
)
if (!isCurrentGeneration()) return
+19
View File
@@ -0,0 +1,19 @@
import { invoke } from '@tauri-apps/api/core'
export type ReportItemType = 'project' | 'version' | 'user' | 'shared-instance'
export interface CreateReportRequest {
report_type: string
item_id: string
item_type: ReportItemType
body: string
uploaded_images: string[]
}
export interface CreateReportResponse {
id: string
}
export async function create_report(request: CreateReportRequest) {
return await invoke<CreateReportResponse>('plugin:reports|reports_create', { request })
}
+1 -1
View File
@@ -37,7 +37,7 @@ export type AppSettings = {
theme: ColorTheme
locale: string
default_page: 'home' | 'library'
default_page: 'Home' | 'Library'
collapsed_navigation: boolean
hide_nametag_skins_page: boolean
advanced_rendering: boolean
@@ -0,0 +1,124 @@
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
import {
getErrorMessage,
isSharedInstancesApiError,
type SharedInstanceUnavailableReason,
} from '@/helpers/install'
export const sharedInstanceErrorMessages = defineMessages({
unavailableTitle: {
id: 'instance.shared-instance.unavailable.title',
defaultMessage: 'Shared instance no longer available',
},
lockedTitle: {
id: 'instance.shared-instance.unavailable.locked-title',
defaultMessage: 'Instance locked',
},
unavailableText: {
id: 'instance.shared-instance.unavailable.text',
defaultMessage:
"Your local instance is still available, but it is no longer linked and won't receive updates.",
},
deletedText: {
id: 'instance.shared-instance.unavailable.deleted-text',
defaultMessage:
'The primary instance was deleted. This instance is still available, but it is no longer linked and will no longer receive updates.',
},
accessRevokedText: {
id: 'instance.shared-instance.unavailable.access-revoked-text',
defaultMessage:
'Your access to the shared instance was revoked. This instance is still available, but it is no longer linked and will no longer receive updates.',
},
lockedText: {
id: 'instance.shared-instance.unavailable.locked-text',
defaultMessage:
'This shared instance was locked by the Content Moderation team. It will no longer receive updates from the primary instance and cannot be played.',
},
unavailableFallbackManager: {
id: 'instance.shared-instance.unavailable.manager-fallback',
defaultMessage: 'the instance manager',
},
errorTitle: {
id: 'instance.shared-instance.error.title',
defaultMessage: 'Something has gone wrong',
},
networkErrorTitle: {
id: 'instance.shared-instance.network-error.title',
defaultMessage: 'Network error',
},
networkErrorText: {
id: 'instance.shared-instance.network-error.text',
defaultMessage: 'Unable to connect to shared instances API',
},
})
export function sharedInstanceUnavailableTextMessage(
reason: SharedInstanceUnavailableReason | null,
) {
if (reason === 'deleted') return sharedInstanceErrorMessages.deletedText
if (reason === 'access_revoked') return sharedInstanceErrorMessages.accessRevokedText
if (reason === 'quarantined') return sharedInstanceErrorMessages.lockedText
return sharedInstanceErrorMessages.unavailableText
}
export function sharedInstanceUnavailableTitleMessage(
reason: SharedInstanceUnavailableReason | null,
) {
return reason === 'quarantined'
? sharedInstanceErrorMessages.lockedTitle
: sharedInstanceErrorMessages.unavailableTitle
}
export function useSharedInstanceErrors() {
const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()
function formatSharedInstanceUnavailable(
reason: SharedInstanceUnavailableReason | null = null,
manager?: string | null,
) {
return formatMessage(sharedInstanceUnavailableTextMessage(reason), {
manager: manager || formatMessage(sharedInstanceErrorMessages.unavailableFallbackManager),
})
}
function notifySharedInstanceUnavailable(
reason: SharedInstanceUnavailableReason | null = null,
manager?: string | null,
) {
addNotification({
type: 'warning',
title: formatMessage(sharedInstanceUnavailableTitleMessage(reason)),
text: formatSharedInstanceUnavailable(reason, manager),
})
}
function notifySharedInstanceConnectionError() {
addNotification({
type: 'error',
title: formatMessage(sharedInstanceErrorMessages.networkErrorTitle),
text: formatMessage(sharedInstanceErrorMessages.networkErrorText),
})
}
function notifySharedInstanceError(error: unknown) {
if (isSharedInstancesApiError(error)) {
notifySharedInstanceConnectionError()
return
}
addNotification({
type: 'error',
title: formatMessage(sharedInstanceErrorMessages.errorTitle),
text: getErrorMessage(error),
})
}
return {
formatSharedInstanceUnavailable,
notifySharedInstanceConnectionError,
notifySharedInstanceError,
notifySharedInstanceUnavailable,
}
}
+33 -2
View File
@@ -17,6 +17,8 @@ export type GameInstance = {
groups: string[]
link?: InstanceLink | null
shared_instance?: SharedInstanceAttachment | null
quarantined: boolean
update_channel: ReleaseChannel
created: Date
@@ -86,18 +88,47 @@ export type InstanceLink = InstanceLinkIdentity &
}
| {
type: 'shared_instance'
shared_instance_id: string
modpack_project_id?: ModrinthId | null
modpack_version_id?: ModrinthId | null
}
)
export type SharedInstanceAttachment = {
id: string
role: 'owner' | 'member'
manager_id?: string | null
server_manager_name?: string | null
server_manager_icon_url?: string | null
linked_user_id?: string | null
status:
| 'unknown'
| 'up_to_date'
| 'update_available'
| 'applying'
| 'stale'
| 'not_ready'
| 'error'
applied_version?: number | null
latest_version?: number | null
}
export type Instance = GameInstance
type ReleaseChannel = 'release' | 'beta' | 'alpha'
export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
export type ContentSourceKind =
| 'local'
| 'modrinth_modpack'
| 'server_project'
| 'modrinth_hosting'
| 'imported_modpack'
| 'shared_instance'
type ContentFile = {
enabled: boolean
source_kind?: ContentSourceKind | null
metadata?: {
project_id: string
version_id: string
@@ -158,7 +189,7 @@ type AppSettings = {
max_concurrent_downloads: number
max_concurrent_writes: number
theme: 'dark' | 'light' | 'oled'
theme: 'dark' | 'light' | 'oled' | 'retro' | 'system'
default_page: 'Home' | 'Library'
collapsed_navigation: boolean
advanced_rendering: boolean
+80
View File
@@ -0,0 +1,80 @@
import type { Labrinth } from '@modrinth/api-client'
import { invoke } from '@tauri-apps/api/core'
// Converts user profile links from rendered Markdown/any dynamic content into app routes.
export function parse_modrinth_user_link(href: string): string | null {
try {
const url = new URL(href)
if (url.hostname !== 'modrinth.com' && url.hostname !== 'www.modrinth.com') return null
const segments = url.pathname.split('/').filter(Boolean)
if (segments[0]?.toLowerCase() !== 'user' || !segments[1] || segments.length > 3) return null
const path = `/user/${encodeURIComponent(decodeURIComponent(segments[1]))}`
return segments[2] ? `${path}/${encodeURIComponent(decodeURIComponent(segments[2]))}` : path
} catch {
return null
}
}
export async function search_user(query: string): Promise<Labrinth.Users.v3.SearchUser[]> {
return await invoke<Labrinth.Users.v3.SearchUser[]>('plugin:users|search_user', { query })
}
export async function get_user_profile(userId: string): Promise<Labrinth.Users.v3.User> {
return await invoke<Labrinth.Users.v3.User>('plugin:users|get_user_profile', { userId })
}
export async function get_user_projects(userId: string): Promise<Labrinth.Projects.v2.Project[]> {
return await invoke<Labrinth.Projects.v2.Project[]>('plugin:users|get_user_projects', {
userId,
})
}
export async function get_user_organizations(
userId: string,
): Promise<Labrinth.Organizations.v3.Organization[]> {
return await invoke<Labrinth.Organizations.v3.Organization[]>(
'plugin:users|get_user_organizations',
{ userId },
)
}
export async function get_user_collections(
userId: string,
): Promise<Labrinth.Collections.Collection[]> {
return await invoke<Labrinth.Collections.Collection[]>('plugin:users|get_user_collections', {
userId,
})
}
export async function patch_user(
userId: string,
patch: Partial<Pick<Labrinth.Users.v2.User, 'badges' | 'bio' | 'role' | 'username'>>,
): Promise<void> {
await invoke('plugin:users|patch_user', { userId, patch })
}
export async function change_user_avatar(
userId: string,
image: Uint8Array,
extension: string,
): Promise<void> {
await invoke('plugin:users|change_user_avatar', { userId, image, extension })
}
export async function delete_user_avatar(userId: string): Promise<void> {
await invoke('plugin:users|delete_user_avatar', { userId })
}
export async function block_user(userId: string): Promise<void> {
await invoke('plugin:users|block_user', { userId })
}
export async function unblock_user(userId: string): Promise<void> {
await invoke('plugin:users|unblock_user', { userId })
}
export async function get_blocked_users(): Promise<Labrinth.BlockedUsers.v3.BlockedUserId[]> {
return await invoke<Labrinth.BlockedUsers.v3.BlockedUserId[]>('plugin:users|get_blocked_users')
}
+11 -5
View File
@@ -435,11 +435,12 @@ export async function refreshServerData(
}
}
export function refreshServers(
export async function refreshServers(
worlds: World[],
serverData: Record<string, ServerData>,
protocolVersion: ProtocolVersion | null,
) {
ping = true,
): Promise<void> {
const servers = worlds.filter(isServerWorld)
servers.forEach((server) => {
if (!serverData[server.address]) {
@@ -451,9 +452,14 @@ export function refreshServers(
}
})
// noinspection ES6MissingAwait - handled by refreshServerData
Object.keys(serverData).forEach((address) =>
refreshServerData(serverData[address], protocolVersion, address),
if (!ping) {
return
}
await Promise.all(
Object.keys(serverData).map((address) =>
refreshServerData(serverData[address], protocolVersion, address),
),
)
}
+48 -12
View File
@@ -125,6 +125,15 @@
"app.action-bar.view-logs": {
"message": "عرض التسجيل"
},
"app.ads-consent.accept": {
"message": "قبول الكل"
},
"app.ads-consent.manage": {
"message": "إدارة التفضيلات"
},
"app.ads-consent.reject": {
"message": "رفض الكل"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "يتيح العرض المتقدم لتأثيرات مثل التشويش الذي قد تسبب مشكلات في الأداء دون استخدام العرض المسرع بالأجهزة."
},
@@ -173,9 +182,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "الزخارف التقليدية"
},
"app.appearance-settings.select-option": {
"message": "حدد خياراً"
},
"app.appearance-settings.show-play-time.description": {
"message": "يعرض مقدار الوقت الذي قضيته في لعب إحدى المهام."
},
@@ -227,9 +233,6 @@
"app.browse.back-to-instance": {
"message": "العودة للنموذج"
},
"app.browse.discover-content": {
"message": "استكشف محتوى"
},
"app.browse.discover-servers": {
"message": "استكشف خوادم"
},
@@ -428,24 +431,33 @@
"app.modal.install-to-play.header": {
"message": "نزل للعب"
},
"app.modal.install-to-play.install-anyway": {
"message": "تثبيت على أي حال"
},
"app.modal.install-to-play.install-button": {
"message": "تثبيت"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# تعديل} other {# تعديلات}}"
},
"app.modal.install-to-play.reviewed-files": {
"message": "لا يتم مراجعة الملف إلا إذا تم نشره على Modrinth، بغض النظر عن صيغته (بما في ذلك .mrpack)."
},
"app.modal.install-to-play.shared-instance": {
"message": "النماذج المشتركة"
},
"app.modal.install-to-play.unknown-files-warning": {
"message": "تحذير ملفات غير معروفة"
},
"app.modal.install-to-play.unrecognized-files": {
"message": "ملفات غير متعرّف عليها"
},
"app.modal.install-to-play.view-contents": {
"message": "عرض المحتويات"
},
"app.modal.update-to-play.header": {
"message": "حدث للعب"
},
"app.modal.update-to-play.update-required-description": {
"message": "هناك تحديث مطلوب للعب بـ {name}. الرجاء التحديث إلى أحدث اصدار لتشغيل اللعبة."
},
"app.project.install-button.already-installed": {
"message": "هذا المشروع مُثبَّت بالفعل"
},
@@ -455,9 +467,6 @@
"app.project.install-context.back-to-browse": {
"message": "العودة إلى الاستكشاف"
},
"app.project.install-context.install-content-to-instance": {
"message": "تثبيت المحتويات الى النموذج"
},
"app.project.version.all-versions": {
"message": "جميع الإصدارات"
},
@@ -644,6 +653,12 @@
"app.skins.title": {
"message": "اختيار المظهر"
},
"app.skins.toggle-ears-features-off": {
"message": "إيقاف"
},
"app.skins.toggle-ears-features-on": {
"message": "تشغيل"
},
"app.update-popup.body.download-complete": {
"message": "انتهى تنزيل تطبيق Modrinth v{version}. أعد التحميل للتحديث الآن، أو تلقائيًا عند إغلاق تطبيق Modrinth."
},
@@ -755,6 +770,24 @@
"friends.sign-in-to-add-friends": {
"message": "<link>سجّل الدخول إلى حساب Modrinth</link> لإضافة الأصدقاء ومعرفة ما الذي يلعبونه!"
},
"instance.action.create-shortcut": {
"message": "إنشاء اختصار"
},
"instance.action.more-actions": {
"message": "مزيد من الخيارات"
},
"instance.action.open-folder": {
"message": "فتح المجلد"
},
"instance.action.repair": {
"message": "إصلاح"
},
"instance.action.starting": {
"message": "قيد التشغيل..."
},
"instance.action.stopping": {
"message": "قيد الإيقاف..."
},
"instance.add-server.add-and-play": {
"message": "إضافة واللعب"
},
@@ -797,6 +830,9 @@
"instance.files.save-as": {
"message": "حفظ ك..."
},
"instance.playtime.never-played": {
"message": "لم تُلعب من قبل"
},
"instance.server-modal.address": {
"message": "العنوان"
},
@@ -113,9 +113,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Systémové dekorace"
},
"app.appearance-settings.select-option": {
"message": "Vyberte možnost"
},
"app.appearance-settings.show-play-time.description": {
"message": "Zobrazuje, kolik času jsi strávil hraním instance."
},
@@ -167,9 +164,6 @@
"app.browse.back-to-instance": {
"message": "Zpět k instanci"
},
"app.browse.discover-content": {
"message": "Objevit obsah"
},
"app.browse.discover-servers": {
"message": "Objevit servery"
},
@@ -380,9 +374,6 @@
"app.modal.update-to-play.header": {
"message": "Aktualizuj pro hraní"
},
"app.modal.update-to-play.update-required-description": {
"message": "Pro hraní {name} je vyžadována aktualizace. Prosím aktualizuj na nejnovější verzi, abys mohl hru spustit."
},
"app.project.install-button.already-installed": {
"message": "Tento projekt je již nainstalován"
},
@@ -392,9 +383,6 @@
"app.project.install-context.back-to-browse": {
"message": "Zpět k Objevování"
},
"app.project.install-context.install-content-to-instance": {
"message": "Nainstalovat obsah do instance"
},
"app.project.version.all-versions": {
"message": "Všechny verze"
},
+280 -10
View File
@@ -11,6 +11,12 @@
"app.action-bar.hide-more-running-instances": {
"message": "Gem flere kørende instances"
},
"app.action-bar.install.copied-details": {
"message": "Kopiere"
},
"app.action-bar.install.copy-details": {
"message": "Kopier detaljer"
},
"app.action-bar.install.dismiss": {
"message": "Tilsidesæt"
},
@@ -20,9 +26,69 @@
"app.action-bar.install.retry": {
"message": "Prøv igen"
},
"app.action-bar.install.summary.app-closing": {
"message": "Annulleret på grund af appen blev lukket"
},
"app.action-bar.install.summary.bad-modpack-file": {
"message": "Kunne ikke indlæse modpack"
},
"app.action-bar.install.summary.canceled": {
"message": "Annulleret"
},
"app.action-bar.install.summary.content-download-failed": {
"message": "Kunne ikke download filer"
},
"app.action-bar.install.summary.corrupt-download": {
"message": "Downloaded filer er korrupt"
},
"app.action-bar.install.summary.could-not-save-files": {
"message": "Kunne ikke gemme filer"
},
"app.action-bar.install.summary.download-failed": {
"message": "Download kunne ikke blive fuldført"
},
"app.action-bar.install.summary.instance-not-found": {
"message": "Instance kunne ikke blive fundet"
},
"app.action-bar.install.summary.invalid-file-path": {
"message": "Fil sti er ugyldig"
},
"app.action-bar.install.summary.invalid-modpack": {
"message": "Modpack data ugyldig"
},
"app.action-bar.install.summary.invalid-modpack-files": {
"message": "Modpack har ugyldig metadata"
},
"app.action-bar.install.summary.java-setup-failed": {
"message": "Java setup kunne ikke færdiggøres"
},
"app.action-bar.install.summary.loader-setup-failed": {
"message": "Loader setup fejlede"
},
"app.action-bar.install.summary.local-data-error": {
"message": "Kunne ikke opdatere lokal data"
},
"app.action-bar.install.summary.minecraft-setup-failed": {
"message": "Minecraft setup fejlede"
},
"app.action-bar.install.summary.modrinth-unreachable": {
"message": "Kunne ikke få forbindelse til Modrinth"
},
"app.action-bar.install.summary.no-write-permission": {
"message": "Ingen adgang til at skrive"
},
"app.action-bar.install.summary.pack-download-failed": {
"message": "Kunne ikke download pack"
},
"app.action-bar.install.summary.unexpected-error": {
"message": "Noget gik galt"
},
"app.action-bar.install.unknown-instance": {
"message": "Ukendt instance"
},
"app.action-bar.install.updating-shared-content": {
"message": "Opdatere delt indhold"
},
"app.action-bar.make-primary-instance": {
"message": "Gør til primære instance"
},
@@ -56,6 +122,15 @@
"app.action-bar.view-logs": {
"message": "Vis logs"
},
"app.ads-consent.accept": {
"message": "Tillad alle"
},
"app.ads-consent.body": {
"message": "Reklamer gør Modrinth muligt og finansiere skaber udbetalinger. Vores partnere må gemme og tilgå cookies i appen for at personliggøre reklamer og måle effektivitet."
},
"app.ads-consent.reject": {
"message": "Afvis alle"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Aktiverer avanceret gengivelse som sløringseffekter, der kan forårsage ydelsesproblemer uden hardwareaccelereret gengivelse."
},
@@ -98,9 +173,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimer launcher"
},
"app.appearance-settings.select-option": {
"message": "Vælg en mulighed"
},
"app.appearance-settings.show-play-time.description": {
"message": "Vis hvor meget tid du har brugt på at spille en instance."
},
@@ -146,9 +218,6 @@
"app.browse.back-to-instance": {
"message": "Tilbage til instance"
},
"app.browse.discover-content": {
"message": "Opdag indhold"
},
"app.browse.discover-servers": {
"message": "Opdag servere"
},
@@ -197,6 +266,9 @@
"app.install.phase.preparing_java.downloading": {
"message": "Downloader Java {version}"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Tilføjet"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Al' data for din instance vil blive permanent slettet, dette inkludere dine verdener, konfigurationer, og alt installeret indhold."
},
@@ -242,12 +314,51 @@
"app.instance.mods.successfully-uploaded": {
"message": "Uploadet"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "for at tilgå denne side."
},
"app.instance.share.locked.signed-out-heading": {
"message": "Ikke logget ind"
},
"app.instance.share.locked.switch-account-button": {
"message": "Skift konto"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "Forkert konto"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "Hvad vil der ske?"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "{username}'s avatar"
},
"app.instance.share.sign-in.button": {
"message": "Log ind"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "Installer alligevel"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "Instans"
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "Log ind som"
},
"app.instance.worlds.add-server": {
"message": "Tilføj server"
},
"app.instance.worlds.browse-servers": {
"message": "Søg servere"
},
"app.instance.worlds.delete-world-modal.delete-button": {
"message": "Slet verden"
},
"app.instance.worlds.delete-world-modal.title": {
"message": "Slet verden"
},
"app.instance.worlds.delete-world-modal.warning-header": {
"message": "Sletter {name}"
},
"app.instance.worlds.filter-modded": {
"message": "Moddet"
},
@@ -266,18 +377,42 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Ingen servere eller verdener tilføjet"
},
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Fjern server"
},
"app.instance.worlds.remove-server-modal.title": {
"message": "Fjern server"
},
"app.instance.worlds.remove-server-modal.warning-header": {
"message": "Fjerner {name}"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Søg {count} verdener..."
},
"app.modal.install-to-play.additional-context": {
"message": "Yderligere kontekst"
},
"app.modal.install-to-play.delete-instance": {
"message": "Slet instans"
},
"app.modal.install-to-play.header": {
"message": "Installer for at spille"
},
"app.modal.install-to-play.install-anyway": {
"message": "Installer alligevel"
},
"app.modal.install-to-play.install-button": {
"message": "Installer"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} other {# mods}}"
},
"app.modal.install-to-play.report-image-too-large": {
"message": "Filen overskrider 1MiB størrelse begrænsning"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.shared-instance": {
"message": "Delt instance"
},
@@ -287,17 +422,32 @@
"app.modal.update-to-play.header": {
"message": "Opdater for at spille"
},
"app.modal.update-to-play.update-required-description": {
"message": "En opdatering er krævet for at spille {name}. Venligst opdater til den seneste version for at køre spillet."
"app.modal.update-to-play.shared-instance-added-label": {
"message": "Tilføjede"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "Fjernede"
},
"app.modal.update-to-play.update-required": {
"message": "Opdatering krævet"
},
"app.project.install-button.already-installed": {
"message": "Dette projekt er allerede installeret"
},
"app.project.install-button.switch-version": {
"message": "Skift version"
},
"app.project.install-context.back-to-browse": {
"message": "Tilbage til udforsk"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installer indhold til instance"
"app.project.version.all-versions": {
"message": "Alle versioner"
},
"app.project.version.download-in-browser": {
"message": "Download i browser"
},
"app.project.version.installing": {
"message": "Installere"
},
"app.project.versions.already-installed": {
"message": "Allerede installeret"
@@ -359,6 +509,9 @@
"app.skins.modal.add-title": {
"message": "Tilføjer et skin"
},
"app.skins.modal.arm-style-section": {
"message": "Arm stil"
},
"app.skins.modal.arm-style-slim": {
"message": "Slank"
},
@@ -461,6 +614,12 @@
"app.skins.title": {
"message": "Skin vælger"
},
"app.skins.toggle-ears-features-off": {
"message": "Slå fra"
},
"app.skins.toggle-ears-features-on": {
"message": "Slå til"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} er færdig med at download. Genindlæs for at opdatere nu, eller automatisk når du lukker Modrinth App."
},
@@ -509,6 +668,9 @@
"app.world.world-item.players-online": {
"message": "{count} online"
},
"content.shared-instance.delete-button": {
"message": "Slet alligevel"
},
"friends.action.add-friend": {
"message": "Tilføj en ven"
},
@@ -572,6 +734,27 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Log ind på en Modrinth konto</link> for at tilføje venner og se hvad de spiller!"
},
"instance.action.create-shortcut": {
"message": "Opret genvej"
},
"instance.action.export-modpack": {
"message": "Eksporter modpack"
},
"instance.action.more-actions": {
"message": "Flere handlinger"
},
"instance.action.open-folder": {
"message": "Åbn mappe"
},
"instance.action.settings": {
"message": "Instans indstillinger"
},
"instance.action.starting": {
"message": "Starter..."
},
"instance.action.stopping": {
"message": "Stopper..."
},
"instance.add-server.add-and-play": {
"message": "Tilføj og spil"
},
@@ -614,6 +797,15 @@
"instance.files.save-as": {
"message": "Gem som..."
},
"instance.locked.delete-button": {
"message": "Slet instans"
},
"instance.locked.play-tooltip": {
"message": "Denne instans er blevet låst"
},
"instance.playtime.never-played": {
"message": "Aldrig spillet"
},
"instance.server-modal.address": {
"message": "Adresse"
},
@@ -785,6 +977,33 @@
"instance.settings.tabs.window.width.enter": {
"message": "Indtast bredde..."
},
"instance.shared-instance.error.title": {
"message": "Noget gik galt"
},
"instance.shared-instance.publish-review.added-label": {
"message": "Tilføjede"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "Skub opdatering til spillere"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "Vælg konfigurations filer"
},
"instance.shared-instance.publish-review.header": {
"message": "Gennemså ændringer"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "Skub opdatering"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Fjernede"
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Instans låst"
},
"instance.shared-instance.unavailable.title": {
"message": "Delt instans er ikke længere tilgængelig"
},
"instance.worlds.a_minecraft_server": {
"message": "En Minecraft server"
},
@@ -794,6 +1013,9 @@
"instance.worlds.copy_address": {
"message": "Kopier adresse"
},
"instance.worlds.create_shortcut": {
"message": "Opret genvej"
},
"instance.worlds.dont_show_on_home": {
"message": "Vis ikke på forsiden"
},
@@ -845,6 +1067,54 @@
"minecraft-account.sign-in": {
"message": "Log ind på Minecraft"
},
"minecraft-required.description-header": {
"message": "Log ind med en Microsoft konto"
},
"minecraft-required.dont-have-account": {
"message": "Har du ikke en konto?"
},
"minecraft-required.get-minecraft": {
"message": "Få Minecraft"
},
"minecraft-required.get-support": {
"message": "Få hjælp"
},
"minecraft-required.header": {
"message": "Minecraft påkrævet"
},
"minecraft-required.sign-in": {
"message": "Log ind på Microsoft"
},
"modal.modrinth-account-required.cancel-button": {
"message": "Annuller"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "Fortsæt i din browser"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Opret en konto"
},
"modal.modrinth-account-required.header": {
"message": "Konto påkrævet"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "Åbn browser igen"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Log ind på Modrinth"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Log ind med en Modrinth konto"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "Logger ind"
},
"modal.modrinth-account-required.support-prompt": {
"message": "Har du problemer med at logge ind? <support>Få hjælp</support>"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "Venter på browser bekræftigelse..."
},
"search.filter.locked.instance": {
"message": "Givet af instanceen"
},
+482 -17
View File
@@ -15,7 +15,7 @@
"message": "Kopiert"
},
"app.action-bar.install.copy-details": {
"message": "Details kopieren"
"message": "Kopier Details"
},
"app.action-bar.install.dismiss": {
"message": "Ignorieren"
@@ -27,19 +27,19 @@
"message": "Wiederholen"
},
"app.action-bar.install.summary.app-closing": {
"message": "Aufgrund von Schließung der App abgebrochen"
"message": "Abbroche will app gschlosse worde isch"
},
"app.action-bar.install.summary.bad-modpack-file": {
"message": "Modpack konnte nicht gelesen werden"
"message": "Modpack kud nöd glese werde"
},
"app.action-bar.install.summary.canceled": {
"message": "Abgebrochen"
"message": "Abbroche"
},
"app.action-bar.install.summary.cleanup-incomplete": {
"message": "Bereinigung konnte nicht abgeschlossen werden"
"message": "Ufrume kunnti nöt beendet werde"
},
"app.action-bar.install.summary.content-download-failed": {
"message": "Dateien konnte nicht heruntergeladen werden"
"message": "Dateien konnten nicht heruntergeladen werden"
},
"app.action-bar.install.summary.corrupt-download": {
"message": "Heruntergeladene Datei ist beschädigt"
@@ -84,11 +84,14 @@
"message": "Das Pack konnte nicht heruntergeladen werden"
},
"app.action-bar.install.summary.unexpected-error": {
"message": "Irgendwas ist schiefgelaufen"
"message": "Etwas ist schiefgelaufen"
},
"app.action-bar.install.unknown-instance": {
"message": "Unbekannte Instanz"
},
"app.action-bar.install.updating-shared-content": {
"message": "Geteilte Inhalte werden aktualisiert"
},
"app.action-bar.installs": {
"message": "Installationen"
},
@@ -125,6 +128,21 @@
"app.action-bar.view-logs": {
"message": "Protokolle anzeigen"
},
"app.ads-consent.accept": {
"message": "Alle akzeptieren"
},
"app.ads-consent.body": {
"message": "Werbung ermöglicht Modrinth und finanziert die Auszahlungen an die Ersteller. Unsere Partner speichern möglicherweise Cookies in der App oder greifen auf diese zu, um Werbung zu personalisieren und die Leistung zu messen."
},
"app.ads-consent.manage": {
"message": "Einstellungen verwalten"
},
"app.ads-consent.reject": {
"message": "Alle ablehnen"
},
"app.ads-consent.title": {
"message": "Deine Privatsphäre und wie Werbung Modrinth unterstützt"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Aktiviert erweiterte Rendering-Funktionen wie Unschärfeeffekte, welche ohne hardwarebeschleunigtes Rendering zu Leistungsproblemen führen können."
},
@@ -173,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Native Dekorationen"
},
"app.appearance-settings.select-option": {
"message": "Option wählen"
},
"app.appearance-settings.show-play-time.description": {
"message": "Zeigt an, wie viel Zeit du mit dem Spielen einer Instanz verbracht hast."
},
@@ -227,9 +242,6 @@
"app.browse.back-to-instance": {
"message": "Zu Instanz zurückgehen"
},
"app.browse.discover-content": {
"message": "Inhalte entdecken"
},
"app.browse.discover-servers": {
"message": "Server entdecken"
},
@@ -323,6 +335,36 @@
"app.install.phase.running_loader_processors": {
"message": "Loader-Prozesse werden ausgeführt"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Hinzugefügt"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "Deine lokale Instanz ist den Benutzern, mit denen du sie geteilt hast, voraus."
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "Deine Änderungen wurden noch nicht geteilt"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "Update senden"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "Wird gesendet..."
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "Entfernt"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "Update senden"
},
"app.instance.admonitions.shared-instance.review-description": {
"message": "Überprüfe die Inhaltsänderungen, die allen Nutzern dieser Instanz bereitgestellt werden."
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Änderungen überprüfen"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Wird überprüft..."
},
"app.instance.confirm-delete.admonition-body": {
"message": "Alle Daten deiner Instanz werden permanent gelöscht, inlusive deiner Welten, Konfigurationen und allen installierten Inhalten."
},
@@ -359,6 +401,9 @@
"app.instance.mods.content-type-project": {
"message": "Projekt"
},
"app.instance.mods.locked-content": {
"message": "Inhalte in gesperrten Instanzen können nicht geändert werden."
},
"app.instance.mods.project-was-added": {
"message": "\"{name}\" wurde hinzugefügt"
},
@@ -374,6 +419,102 @@
"app.instance.mods.successfully-uploaded": {
"message": "Erfolgreich hochgeladen"
},
"app.instance.share.empty.description": {
"message": "Du kannst diese Instanz mit deinen Freunden teilen!"
},
"app.instance.share.empty.heading": {
"message": "Keine Freunde eingeladen"
},
"app.instance.share.empty.invite-friends-button": {
"message": "Freunde einladen"
},
"app.instance.share.invite-modal.heading": {
"message": "{name} teilen"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "Du musst dich als"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "anmelden um diese Seite aufzurufen."
},
"app.instance.share.locked.linked-account-fallback": {
"message": "das verknüpfte Konto"
},
"app.instance.share.locked.signed-out-heading": {
"message": "Nicht angemeldet"
},
"app.instance.share.locked.switch-account-button": {
"message": "Konto wechseln"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "Falsches Konto"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Diese Person wird für diese geteilte Instanz keine Updates mehr erhalten"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "Jede bereits installierte Kopie bleibt auf deren Gerät erhalten"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "Du kannst diese Person später erneut einladen"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "Dies ist der letzte Benutzer, das Teilen wird für diese Instanz deaktiviert"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "Was passiert?"
},
"app.instance.share.remove-user-modal.header": {
"message": "Zugriff widerrufen"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "Zugriff widerrufen"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "Avatar von {username}"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "Falls du den Zugriff von {username} auf diese geteilte Instanz widerrufst, musst du diese Person erneut einladen, bevor diese Updates erhalten kann."
},
"app.instance.share.sign-in.button": {
"message": "Anmelden"
},
"app.instance.share.unlink.body": {
"message": "Du musst die Verknüpfung zu diesem Modpack trennen, um deine Instanz zu teilen"
},
"app.instance.share.unlink.header": {
"message": "Zum Teilen muss die Verknüpfung getrennt werden"
},
"app.instance.shared-instance-already-installed.body": {
"message": "Diese geteilte Instanz ist bereits als <bold>{instanceName}</bold> installiert. Möchtest du wirklich eine weitere Kopie installieren?"
},
"app.instance.shared-instance-already-installed.header": {
"message": "Geteilte Instanz bereits installiert"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "Trotzdem Installieren"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "Instanz"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "das verknüpfte Konto"
},
"app.instance.shared-instance-wrong-account.owner-admonition-body-v2": {
"message": "um diese geteilte Instanz zu verwalten. Du wirst keine Updates an Nutzer senden können."
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "Anmelden als"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Du musst dich bei Modrinth anmelden"
},
"app.instance.shared-instance-wrong-account.user-admonition-body-v2": {
"message": "um Updates für diese geteilte Instanz zu erhalten."
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "Du nutzt das falsche Modrinth-Konto"
},
"app.instance.worlds.add-server": {
"message": "Server hinzufügen"
},
@@ -425,26 +566,116 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "Durchsuche {count} Welten..."
},
"app.modal.install-to-play.additional-context": {
"message": "Zusätzlicher Kontext"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "Füge gegebenenfalls relevante Links und Bilder hinzu"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "Die Instanz, die du meldest"
},
"app.modal.install-to-play.delete-instance": {
"message": "Instanz löschen"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "Nicht Installieren"
},
"app.modal.install-to-play.header": {
"message": "Installieren zum Spielen"
},
"app.modal.install-to-play.install-anyway": {
"message": "Trotzdem Installieren"
},
"app.modal.install-to-play.install-button": {
"message": "Installieren"
},
"app.modal.install-to-play.invite-warning": {
"message": "Diese Einladung wurde von einem anderen Modrinth-Nutzer erstellt, nicht von Modrinth. Nimm nur Einladungen von Personen an, denen du vertraust."
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# Mod} other {# Mods}}"
},
"app.modal.install-to-play.report-description": {
"message": "Nutze dieses Formular, um Instanzen zu melden, die möglicherweise gegen unsere <rules-link>Regeln</rules-link> oder <terms-link>Nutzungsbedingungen</terms-link> verstoßen."
},
"app.modal.install-to-play.report-image-invalid-type": {
"message": "Datei ist kein zulässiger Bildtyp"
},
"app.modal.install-to-play.report-image-too-large": {
"message": "Datei überschreitet die Größenbeschränkung von 1 MiB"
},
"app.modal.install-to-play.report-legal-claims": {
"message": "Informationen zu DMCA-Meldungen oder anderen rechtlichen Forderungen findest du in unserer <copyright-link>Urheberrechtsrichtlinie</copyright-link>."
},
"app.modal.install-to-play.report-reason": {
"message": "Gegen welche Regel verstößt diese Instanz?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "Unangemessen"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "Bösartig"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "Geteilte Instanz melden"
},
"app.modal.install-to-play.report-submitted": {
"message": "Meldung eingereicht"
},
"app.modal.install-to-play.report-support-and-bugs": {
"message": "Wende dich bei Supportanfragen bitte an unser <support-link>Support-Team</support-link>. Um Fehler zu melden, öffne bitte ein <github-link>Issue auf GitHub</github-link>."
},
"app.modal.install-to-play.reviewed-files": {
"message": "Eine Datei wird nur geprüft, wenn sie auf Modrinth veröffentlicht wird, unabhängig von ihrem Dateiformat (auch .mrpack)."
},
"app.modal.install-to-play.shared-instance": {
"message": "Geteilte Instanz"
},
"app.modal.install-to-play.shared-instance-content": {
"message": "Inhalte der geteilten Instanz"
},
"app.modal.install-to-play.shared-instance-unknown-files-description": {
"message": "Diese geteilte Instanz enthält Dateien, die nicht auf Modrinth veröffentlicht sind. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
},
"app.modal.install-to-play.unknown-files-description": {
"message": "Dieses Server-Modpack enthält Dateien, die nicht auf Modrinth veröffentlicht sind. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
},
"app.modal.install-to-play.unknown-files-warning": {
"message": "Warnung vor unbekannten Dateien"
},
"app.modal.install-to-play.unrecognized-files": {
"message": "Nicht erkannte Dateien"
},
"app.modal.install-to-play.view-contents": {
"message": "Inhalte ansehen"
},
"app.modal.update-to-play.header": {
"message": "Aktualisieren zum Spielen"
},
"app.modal.update-to-play.removed": {
"message": "Entfernt"
},
"app.modal.update-to-play.server-modpack-unknown-files-description": {
"message": "Dieses Update eines Server-Modpacks enthält Dateien, die nicht auf Modrinth veröffentlicht sind. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "Hinzugefügt"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "Entfernt"
},
"app.modal.update-to-play.shared-instance-unknown-files-description": {
"message": "Dieses Update einer geteilten Instanz enthält Dateien, die nicht auf Modrinth veröffentlicht sind. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
},
"app.modal.update-to-play.update-required": {
"message": "Aktualisierung erforderlich"
},
"app.modal.update-to-play.update-required-description": {
"message": "Eine aktualisierung zum spielen von {name} ist benötigt. Bitte aktualisiere auf die neuste Version um das Spiel zu starten."
"message": "Zum Spielen von {name} ist eine Aktualisierung erforderlich. Bitte aktualisiere auf die neueste Version, um das Spiel zu starten."
},
"app.project.install-button.already-installed": {
"message": "Dieses Projekt ist bereits installiert"
@@ -455,9 +686,6 @@
"app.project.install-context.back-to-browse": {
"message": "Zurück zu Entdecken"
},
"app.project.install-context.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
},
"app.project.version.all-versions": {
"message": "Alle Versionen"
},
@@ -476,6 +704,9 @@
"app.settings.downloading": {
"message": "Lade v{version} herunter"
},
"app.settings.privacy.ads-consent.intro": {
"message": "Werbung ermöglicht Modrinth und finanziert die Auszahlungen an die Ersteller. Unsere Partner speichern möglicherweise Cookies in der App oder greifen auf diese zu, um Werbung zu personalisieren und die Leistung zu messen. Du kannst dies unten deaktivieren oder deine Einstellungen verwalten."
},
"app.settings.tabs.appearance": {
"message": "Erscheinungsbild"
},
@@ -518,6 +749,9 @@
"app.skins.dropped-file-error.title": {
"message": "Fehler bei der Dateiverarbeitung"
},
"app.skins.ears-feature-notice": {
"message": "Dieser Skin nutzt Funktionen aus dem {ears}-Mod"
},
"app.skins.edit-button": {
"message": "Skin bearbeiten"
},
@@ -644,6 +878,12 @@
"app.skins.title": {
"message": "Skin-Auswahl"
},
"app.skins.toggle-ears-features-off": {
"message": "Deaktivieren"
},
"app.skins.toggle-ears-features-on": {
"message": "Aktivieren"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} wurde heruntergeladen. Lade die App neu um jetzt zu aktualisieren, oder automatisch nach dem schliessen der Modrinth App."
},
@@ -692,6 +932,39 @@
"app.world.world-item.players-online": {
"message": "{count} online"
},
"content.shared-instance.change-version-body": {
"message": "Das Ändern der Version wirkt sich nur auf deine lokale Kopie aus. Zukünftige Updates der geteilten Instanz können diese wiederherstellen oder erneut ändern."
},
"content.shared-instance.delete-bulk-body": {
"message": "Einige ausgewählte Projekte sind Teil der geteilten Instanz. Wenn du diese löschst, wirkt sich dies nur auf deine lokale Kopie aus, und zukünftige Updates der geteilten Instanz können diese möglicherweise wiederherstellen oder erneut ändern."
},
"content.shared-instance.delete-button": {
"message": "Trotzdem löschen"
},
"content.shared-instance.delete-many-button": {
"message": "{count, plural, one {# Projekt} other {# Projekte}} trotzdem löschen"
},
"content.shared-instance.delete-single-body": {
"message": "Das Löschen wirkt sich nur auf deine lokale Kopie aus. Zukünftige Updates der geteilten Instanz können dies wiederherstellen oder erneut ändern."
},
"content.shared-instance.disable-bulk-body": {
"message": "Einige ausgewählte Projekte sind Teil der geteilten Instanz. Wenn du diese deaktivierst, wirkt sich dies nur auf deine lokale Kopie aus, und zukünftige Updates der geteilten Instanz können diese möglicherweise wieder aktivieren, wiederherstellen oder erneut ändern."
},
"content.shared-instance.disable-button": {
"message": "Trotzdem deaktivieren"
},
"content.shared-instance.disable-many-button": {
"message": "{count, plural, one {# Projekt} other {# Projekte}} trotzdem deaktivieren"
},
"content.shared-instance.disable-single-body": {
"message": "Das Deaktivieren wirkt sich nur auf deine lokale Kopie aus. Zukünftige Updates der geteilten Instanz können dies wieder aktivieren, wiederherstellen oder erneut ändern."
},
"content.shared-instance.unlink-body": {
"message": "Das Aufheben der Verknüpfung wirkt sich nur auf deine lokale Kopie aus. Zukünftige Updates der geteilten Instanz können diese wiederherstellen oder erneut ändern."
},
"content.shared-instance.warning-header": {
"message": "Dies ist Teil der geteilten Instanz"
},
"friends.action.add-friend": {
"message": "Freund hinzufügen"
},
@@ -755,6 +1028,75 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Logge dich in ein Modrinth Konto ein</link>, um Freunde hinzuzufügen und zu sehen, was sie spielen!"
},
"installation-settings.shared-instance.linked-title": {
"message": "Verknüpfte geteilte Instanz"
},
"installation-settings.shared-instance.title": {
"message": "Instanz zurückziehen"
},
"installation-settings.shared-instance.unlink-button": {
"message": "Geteilte Instanz trennen"
},
"installation-settings.shared-instance.unlink-description": {
"message": "Trenne diese lokale Instanz von zukünftigen geteilten Updates."
},
"installation-settings.shared-instance.unlinking-button": {
"message": "Wird getrennt..."
},
"installation-settings.shared-instance.unpublish-button": {
"message": "Geteilte Instanz zurückziehen"
},
"installation-settings.shared-instance.unpublish-description": {
"message": "Entferne diese geteilte Instanz von Modrinth und sende keine Updates mehr an Personen, die sie nutzen. Deine lokale Instanz ist davon nicht betroffen."
},
"installation-settings.shared-instance.unpublishing-button": {
"message": "Wird zurückgezogen..."
},
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Dies betrifft nur deine lokale Instanz. Deine installierten Inhalte bleiben auf diesem Gerät, und die freigegebene Instanz sowie andere Benutzer, die diese verwenden, sind davon nicht betroffen."
},
"installation-settings.unlink-shared-instance.modal.admonition-header": {
"message": "Geteilte Instanz trennen"
},
"installation-settings.unlink-shared-instance.modal.header": {
"message": "Geteilte Instanz trennen"
},
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Dadurch wird die geteilte Instanz von den Servern von Modrinth gelöscht. Personen, die diese in der Modrinth-App nutzen, erhalten keine Updates mehr, aber deine lokale Instanz und dessen Inhalte bleiben auf diesem Gerät erhalten."
},
"installation-settings.unpublish-shared-instance.modal.admonition-header": {
"message": "Geteilte Instanz zurückziehen"
},
"installation-settings.unpublish-shared-instance.modal.header": {
"message": "Geteilte Instanz zurückziehen"
},
"instance.action.create-shortcut": {
"message": "Verknüpfung erstellen"
},
"instance.action.export-modpack": {
"message": "Modpack exportieren"
},
"instance.action.launch-instance": {
"message": "Instanz starten"
},
"instance.action.more-actions": {
"message": "Weitere Aktionen"
},
"instance.action.open-folder": {
"message": "Ordner öffnen"
},
"instance.action.repair": {
"message": "Reparieren"
},
"instance.action.settings": {
"message": "Instanz-Einstellungen"
},
"instance.action.starting": {
"message": "Wird gestartet..."
},
"instance.action.stopping": {
"message": "Wird gestoppt..."
},
"instance.add-server.add-and-play": {
"message": "Ersteue u starte"
},
@@ -797,6 +1139,15 @@
"instance.files.save-as": {
"message": "Speichern unter..."
},
"instance.locked.delete-button": {
"message": "Instanz löschen"
},
"instance.locked.play-tooltip": {
"message": "Diese Instanz wurde gesperrt"
},
"instance.playtime.never-played": {
"message": "Noch nie gespielt"
},
"instance.server-modal.address": {
"message": "Adresse"
},
@@ -932,6 +1283,9 @@
"instance.settings.tabs.installation.loader-version": {
"message": "{loader} Version"
},
"instance.settings.tabs.installation.locked": {
"message": "Installationseinstellungen sind nicht verfügbar, solange diese Instanz gesperrt ist."
},
"instance.settings.tabs.java": {
"message": "Java und Arbeitsspeicher"
},
@@ -971,6 +1325,9 @@
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/pfad/zu/java"
},
"instance.settings.tabs.sharing": {
"message": "Teilen"
},
"instance.settings.tabs.window": {
"message": "Spielfenster"
},
@@ -1001,6 +1358,57 @@
"instance.settings.tabs.window.width.enter": {
"message": "Breite eingeben..."
},
"instance.shared-instance.error.title": {
"message": "Etwas ist schiefgelaufen"
},
"instance.shared-instance.owner-tooltip": {
"message": "Der Inhalt dieser Instanz wird mit anderen Benutzern geteilt."
},
"instance.shared-instance.publish-review.added-label": {
"message": "Hinzugefügt"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "Update an Spieler senden"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "Konfigurationsdateien auswählen"
},
"instance.shared-instance.publish-review.description": {
"message": "Überprüfe die Inhaltsänderungen und wähle Konfigurationsdateien aus, die in diesem Update enthalten sein sollen."
},
"instance.shared-instance.publish-review.header": {
"message": "Änderungen überprüfen"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "Update senden"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Entfernt"
},
"instance.shared-instance.tooltip": {
"message": "Der Inhalt dieser Instanz wird von jemand anderes verwaltet."
},
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Dein Zugriff auf die geteilte Instanz wurde widerrufen. Diese Instanz ist weiterhin verfügbar, ist jedoch nicht mehr verknüpft und erhält keine Updates mehr."
},
"instance.shared-instance.unavailable.deleted-text": {
"message": "Die primäre Instanz wurde gelöscht. Diese Instanz ist weiterhin verfügbar, ist jedoch nicht mehr verknüpft und erhält keine Updates mehr."
},
"instance.shared-instance.unavailable.locked-text": {
"message": "Diese geteilte Instanz wurde vom Inhaltsmoderationsteam gesperrt. Sie erhält keine Updates von der primären Instanz mehr und kann nicht mehr gespielt werden."
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Instanz gesperrt"
},
"instance.shared-instance.unavailable.manager-fallback": {
"message": "der Instanzmanager"
},
"instance.shared-instance.unavailable.text": {
"message": "Deine lokale Instanz ist weiterhin verfügbar, ist jedoch nicht mehr verknüpft und erhält keine Updates mehr."
},
"instance.shared-instance.unavailable.title": {
"message": "Geteilte Instanz nicht mehr verfügbar"
},
"instance.worlds.a_minecraft_server": {
"message": "Ein Minecraft-Server"
},
@@ -1064,6 +1472,63 @@
"minecraft-account.sign-in": {
"message": "In Minecraft anmelden"
},
"minecraft-required.description": {
"message": "Du benötigst ein Microsoft-Konto, welches Minecraft besitzt, bevor du das Spiel starten und spielen kannst."
},
"minecraft-required.description-header": {
"message": "Bei einem Microsoft-Konto anmelden"
},
"minecraft-required.dont-have-account": {
"message": "Du hast kein Konto?"
},
"minecraft-required.get-minecraft": {
"message": "Minecraft holen"
},
"minecraft-required.get-support": {
"message": "Hilfe erhalten"
},
"minecraft-required.header": {
"message": "Minecraft erforderlich"
},
"minecraft-required.sign-in": {
"message": "Bei Microsoft anmelden"
},
"modal.modrinth-account-required.browser-description": {
"message": "Ein neuer Tab zum Anmelden wurde geöffnet. Schließe die Anmeldung dort ab und kehren anschließend zur App zurück."
},
"modal.modrinth-account-required.cancel-button": {
"message": "Abbrechen"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "Im Browser fortfahren"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Ein Konto erstellen"
},
"modal.modrinth-account-required.description": {
"message": "Du musst dich bei deinem Modrinth-Konto anmelden, bevor du diese Funktion nutzen kannst."
},
"modal.modrinth-account-required.header": {
"message": "Konto erforderlich"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "Browser erneut öffnen"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Bei Modrinth anmelden"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Bei einem Modrinth-Konto anmelden"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "Anmelden"
},
"modal.modrinth-account-required.support-prompt": {
"message": "Hast du Probleme bei der Anmeldung? <support>Hilfe erhalten</support>"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "Warte auf Bestätigung durch den Browser..."
},
"search.filter.locked.instance": {
"message": "Von der Instanz vorgegeben"
},
+479 -14
View File
@@ -39,7 +39,7 @@
"message": "Bereinigung konnte nicht abgeschlossen werden"
},
"app.action-bar.install.summary.content-download-failed": {
"message": "Dateien konnte nicht heruntergeladen werden"
"message": "Dateien konnten nicht heruntergeladen werden"
},
"app.action-bar.install.summary.corrupt-download": {
"message": "Heruntergeladene Datei ist beschädigt"
@@ -84,11 +84,14 @@
"message": "Das Pack konnte nicht heruntergeladen werden"
},
"app.action-bar.install.summary.unexpected-error": {
"message": "Irgendwas ist schiefgelaufen"
"message": "Etwas ist schiefgelaufen"
},
"app.action-bar.install.unknown-instance": {
"message": "Unbekannte Instanz"
},
"app.action-bar.install.updating-shared-content": {
"message": "Geteilte Inhalte werden aktualisiert"
},
"app.action-bar.installs": {
"message": "Installationen"
},
@@ -125,6 +128,21 @@
"app.action-bar.view-logs": {
"message": "Logs anzeigen"
},
"app.ads-consent.accept": {
"message": "Alle akzeptieren"
},
"app.ads-consent.body": {
"message": "Werbung ermöglicht Modrinth und finanziert die Auszahlungen an die Ersteller. Unsere Partner speichern möglicherweise Cookies in der App oder greifen auf diese zu, um Werbung zu personalisieren und die Leistung zu messen."
},
"app.ads-consent.manage": {
"message": "Einstellungen verwalten"
},
"app.ads-consent.reject": {
"message": "Alle ablehnen"
},
"app.ads-consent.title": {
"message": "Deine Privatsphäre und wie Werbung Modrinth unterstützt"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Aktiviert erweiterte Rendering-Funktionen wie Unschärfe-Effekte, welche ohne Hardware-beschleunigtes Rendering zu Leistungsproblemen führen können."
},
@@ -173,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Native Dekorationen"
},
"app.appearance-settings.select-option": {
"message": "Wähle eine Option"
},
"app.appearance-settings.show-play-time.description": {
"message": "Zeigt an, wie viel Zeit du damit verbracht hast, eine Instanz zu spielen."
},
@@ -207,10 +222,10 @@
"message": "Authentifizierungsserver sind nicht erreichbar"
},
"app.browse.add-servers-to-instance": {
"message": "Server zu Instanz hinzufügen"
"message": "Server zur Instanz hinzufügen"
},
"app.browse.add-to-an-instance": {
"message": "Zu Instanz hinzufügen"
"message": "Zu einer Instanz hinzufügen"
},
"app.browse.add-to-instance": {
"message": "Zur Instanz hinzufügen"
@@ -227,9 +242,6 @@
"app.browse.back-to-instance": {
"message": "Zurück zur Instanz"
},
"app.browse.discover-content": {
"message": "Inhalte entdecken"
},
"app.browse.discover-servers": {
"message": "Server entdecken"
},
@@ -323,6 +335,36 @@
"app.install.phase.running_loader_processors": {
"message": "Loader-Prozesse werden ausgeführt"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Hinzugefügt"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "Deine lokale Instanz ist den Benutzern, mit denen du sie geteilt hast, voraus."
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "Deine Änderungen wurden noch nicht geteilt"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "Update senden"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "Wird gesendet..."
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "Entfernt"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "Update senden"
},
"app.instance.admonitions.shared-instance.review-description": {
"message": "Überprüfe die Inhaltsänderungen, die allen Nutzern dieser Instanz bereitgestellt werden."
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Änderungen überprüfen"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Wird überprüft..."
},
"app.instance.confirm-delete.admonition-body": {
"message": "Alle Daten deiner Instanz werden permanent gelöscht, einschließlich deiner Welten, Konfigurationen und allen installierten Inhalten."
},
@@ -348,7 +390,7 @@
"message": "Instanz"
},
"app.instance.mods.bulk-update.downloading-projects": {
"message": "{current, number}/{total, number} Projekte werden heruntergelanden..."
"message": "{current, number}/{total, number} Projekte werden heruntergeladen..."
},
"app.instance.mods.bulk-update.finishing": {
"message": "Update wird fertiggestellt..."
@@ -359,6 +401,9 @@
"app.instance.mods.content-type-project": {
"message": "Projekt"
},
"app.instance.mods.locked-content": {
"message": "Inhalte in gesperrten Instanzen können nicht geändert werden."
},
"app.instance.mods.project-was-added": {
"message": "\"{name}\" wurde hinzugefügt"
},
@@ -374,6 +419,102 @@
"app.instance.mods.successfully-uploaded": {
"message": "Erfolgreich hochgeladen"
},
"app.instance.share.empty.description": {
"message": "Du kannst diese Instanz mit deinen Freunden teilen!"
},
"app.instance.share.empty.heading": {
"message": "Keine Freunde eingeladen"
},
"app.instance.share.empty.invite-friends-button": {
"message": "Freunde einladen"
},
"app.instance.share.invite-modal.heading": {
"message": "{name} teilen"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "Du musst dich als"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "anmelden um diese Seite aufzurufen."
},
"app.instance.share.locked.linked-account-fallback": {
"message": "das verknüpfte Konto"
},
"app.instance.share.locked.signed-out-heading": {
"message": "Nicht angemeldet"
},
"app.instance.share.locked.switch-account-button": {
"message": "Konto wechseln"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "Falsches Konto"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Diese Person wird für diese geteilte Instanz keine Updates mehr erhalten"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "Jede bereits installierte Kopie bleibt auf deren Gerät erhalten"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "Du kannst diese Person später erneut einladen"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "Dies ist der letzte Benutzer, das Teilen wird für diese Instanz deaktiviert"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "Was passiert?"
},
"app.instance.share.remove-user-modal.header": {
"message": "Zugriff widerrufen"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "Zugriff widerrufen"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "Avatar von {username}"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "Falls du den Zugriff von {username} auf diese geteilte Instanz widerrufst, musst du diese Person erneut einladen, bevor diese Updates erhalten kann."
},
"app.instance.share.sign-in.button": {
"message": "Anmelden"
},
"app.instance.share.unlink.body": {
"message": "Du musst die Verknüpfung zu diesem Modpack trennen, um deine Instanz zu teilen"
},
"app.instance.share.unlink.header": {
"message": "Zum Teilen muss die Verknüpfung getrennt werden"
},
"app.instance.shared-instance-already-installed.body": {
"message": "Diese geteilte Instanz ist bereits als <bold>{instanceName}</bold> installiert. Möchtest du wirklich eine weitere Kopie installieren?"
},
"app.instance.shared-instance-already-installed.header": {
"message": "Geteilte Instanz bereits installiert"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "Trotzdem Installieren"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "Instanz"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "das verknüpfte Konto"
},
"app.instance.shared-instance-wrong-account.owner-admonition-body-v2": {
"message": "um diese geteilte Instanz zu verwalten. Du wirst keine Updates an Nutzer senden können."
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "Anmelden als"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Du musst dich bei Modrinth anmelden"
},
"app.instance.shared-instance-wrong-account.user-admonition-body-v2": {
"message": "um Updates für diese geteilte Instanz zu erhalten."
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "Du nutzt das falsche Modrinth-Konto"
},
"app.instance.worlds.add-server": {
"message": "Server hinzufügen"
},
@@ -425,24 +566,114 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "Durchsuche {count} Welten..."
},
"app.modal.install-to-play.additional-context": {
"message": "Zusätzlicher Kontext"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "Füge gegebenenfalls relevante Links und Bilder hinzu"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "Die Instanz, die du meldest"
},
"app.modal.install-to-play.delete-instance": {
"message": "Instanz löschen"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "Nicht Installieren"
},
"app.modal.install-to-play.header": {
"message": "Installieren zum Spielen"
},
"app.modal.install-to-play.install-anyway": {
"message": "Trotzdem Installieren"
},
"app.modal.install-to-play.install-button": {
"message": "Installieren"
},
"app.modal.install-to-play.invite-warning": {
"message": "Diese Einladung wurde von einem anderen Modrinth-Nutzer erstellt, nicht von Modrinth. Nimm nur Einladungen von Personen an, denen du vertraust."
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# Mod} other {# Mods}}"
},
"app.modal.install-to-play.report-description": {
"message": "Nutze dieses Formular, um Instanzen zu melden, die möglicherweise gegen unsere <rules-link>Regeln</rules-link> oder <terms-link>Nutzungsbedingungen</terms-link> verstoßen."
},
"app.modal.install-to-play.report-image-invalid-type": {
"message": "Datei ist kein zulässiger Bildtyp"
},
"app.modal.install-to-play.report-image-too-large": {
"message": "Datei überschreitet die Größenbeschränkung von 1 MiB"
},
"app.modal.install-to-play.report-legal-claims": {
"message": "Informationen zu DMCA-Meldungen oder anderen rechtlichen Forderungen findest du in unserer <copyright-link>Urheberrechtsrichtlinie</copyright-link>."
},
"app.modal.install-to-play.report-reason": {
"message": "Gegen welche Regel verstößt diese Instanz?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "Unangemessen"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "Bösartig"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "Geteilte Instanz melden"
},
"app.modal.install-to-play.report-submitted": {
"message": "Meldung eingereicht"
},
"app.modal.install-to-play.report-support-and-bugs": {
"message": "Wende dich bei Supportanfragen bitte an unser <support-link>Support-Team</support-link>. Um Fehler zu melden, öffne bitte ein <github-link>Issue auf GitHub</github-link>."
},
"app.modal.install-to-play.reviewed-files": {
"message": "Eine Datei wird nur geprüft, wenn sie auf Modrinth veröffentlicht wird, unabhängig von ihrem Dateiformat (auch .mrpack)."
},
"app.modal.install-to-play.shared-instance": {
"message": "Geteilte Instanz"
},
"app.modal.install-to-play.shared-instance-content": {
"message": "Inhalte der geteilten Instanz"
},
"app.modal.install-to-play.shared-instance-unknown-files-description": {
"message": "Diese geteilte Instanz enthält Dateien, die nicht auf Modrinth veröffentlicht sind. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
},
"app.modal.install-to-play.unknown-files-description": {
"message": "Dieses Server-Modpack enthält Dateien, die nicht auf Modrinth veröffentlicht sind. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
},
"app.modal.install-to-play.unknown-files-warning": {
"message": "Warnung vor unbekannten Dateien"
},
"app.modal.install-to-play.unrecognized-files": {
"message": "Nicht erkannte Dateien"
},
"app.modal.install-to-play.view-contents": {
"message": "Inhalte ansehen"
},
"app.modal.update-to-play.header": {
"message": "Aktualisieren zum Spielen"
},
"app.modal.update-to-play.removed": {
"message": "Entfernt"
},
"app.modal.update-to-play.server-modpack-unknown-files-description": {
"message": "Dieses Update eines Server-Modpacks enthält Dateien, die nicht auf Modrinth veröffentlicht sind. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "Hinzugefügt"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "Entfernt"
},
"app.modal.update-to-play.shared-instance-unknown-files-description": {
"message": "Dieses Update einer geteilten Instanz enthält Dateien, die nicht auf Modrinth veröffentlicht sind. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
},
"app.modal.update-to-play.update-required": {
"message": "Aktualisierung erforderlich"
},
"app.modal.update-to-play.update-required-description": {
"message": "Zum Spielen von {name} ist eine Aktualisierung erforderlich. Bitte aktualisiere auf die neueste Version, um das Spiel zu starten."
},
@@ -455,9 +686,6 @@
"app.project.install-context.back-to-browse": {
"message": "Zurück zum Entdecken"
},
"app.project.install-context.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
},
"app.project.version.all-versions": {
"message": "Alle Versionen"
},
@@ -476,6 +704,9 @@
"app.settings.downloading": {
"message": "Lade v{version} herunter"
},
"app.settings.privacy.ads-consent.intro": {
"message": "Werbung ermöglicht Modrinth und finanziert die Auszahlungen an die Ersteller. Unsere Partner speichern möglicherweise Cookies in der App oder greifen auf diese zu, um Werbung zu personalisieren und die Leistung zu messen. Du kannst dies unten deaktivieren oder deine Einstellungen verwalten."
},
"app.settings.tabs.appearance": {
"message": "Erscheinungsbild"
},
@@ -518,6 +749,9 @@
"app.skins.dropped-file-error.title": {
"message": "Fehler bei der Verarbeitung der Datei"
},
"app.skins.ears-feature-notice": {
"message": "Dieser Skin nutzt Funktionen aus dem {ears}-Mod"
},
"app.skins.edit-button": {
"message": "Skin bearbeiten"
},
@@ -644,6 +878,12 @@
"app.skins.title": {
"message": "Skin-Auswahl"
},
"app.skins.toggle-ears-features-off": {
"message": "Deaktivieren"
},
"app.skins.toggle-ears-features-on": {
"message": "Aktivieren"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} wurde heruntergeladen. Jetzt neu laden, um sofort zu aktualisieren, oder beim Schließen der Modrinth-App automatisch aktualisieren."
},
@@ -692,6 +932,39 @@
"app.world.world-item.players-online": {
"message": "{count} online"
},
"content.shared-instance.change-version-body": {
"message": "Das Ändern der Version wirkt sich nur auf deine lokale Kopie aus. Zukünftige Updates der geteilten Instanz können diese wiederherstellen oder erneut ändern."
},
"content.shared-instance.delete-bulk-body": {
"message": "Einige ausgewählte Projekte sind Teil der geteilten Instanz. Wenn du diese löschst, wirkt sich dies nur auf deine lokale Kopie aus, und zukünftige Updates der geteilten Instanz können diese möglicherweise wiederherstellen oder erneut ändern."
},
"content.shared-instance.delete-button": {
"message": "Trotzdem löschen"
},
"content.shared-instance.delete-many-button": {
"message": "{count, plural, one {# Projekt} other {# Projekte}} trotzdem löschen"
},
"content.shared-instance.delete-single-body": {
"message": "Das Löschen wirkt sich nur auf deine lokale Kopie aus. Zukünftige Updates der geteilten Instanz können dies wiederherstellen oder erneut ändern."
},
"content.shared-instance.disable-bulk-body": {
"message": "Einige ausgewählte Projekte sind Teil der geteilten Instanz. Wenn du diese deaktivierst, wirkt sich dies nur auf deine lokale Kopie aus, und zukünftige Updates der geteilten Instanz können diese möglicherweise wieder aktivieren, wiederherstellen oder erneut ändern."
},
"content.shared-instance.disable-button": {
"message": "Trotzdem deaktivieren"
},
"content.shared-instance.disable-many-button": {
"message": "{count, plural, one {# Projekt} other {# Projekte}} trotzdem deaktivieren"
},
"content.shared-instance.disable-single-body": {
"message": "Das Deaktivieren wirkt sich nur auf deine lokale Kopie aus. Zukünftige Updates der geteilten Instanz können dies wieder aktivieren, wiederherstellen oder erneut ändern."
},
"content.shared-instance.unlink-body": {
"message": "Das Aufheben der Verknüpfung wirkt sich nur auf deine lokale Kopie aus. Zukünftige Updates der geteilten Instanz können diese wiederherstellen oder erneut ändern."
},
"content.shared-instance.warning-header": {
"message": "Dies ist Teil der geteilten Instanz"
},
"friends.action.add-friend": {
"message": "Freund hinzufügen"
},
@@ -755,6 +1028,75 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Melde dich bei einem Modrinth-Konto an</link>, um Freunde hinzuzufügen und zu sehen, was sie gerade spielen!"
},
"installation-settings.shared-instance.linked-title": {
"message": "Verknüpfte geteilte Instanz"
},
"installation-settings.shared-instance.title": {
"message": "Instanz zurückziehen"
},
"installation-settings.shared-instance.unlink-button": {
"message": "Geteilte Instanz trennen"
},
"installation-settings.shared-instance.unlink-description": {
"message": "Trenne diese lokale Instanz von zukünftigen geteilten Updates."
},
"installation-settings.shared-instance.unlinking-button": {
"message": "Wird getrennt..."
},
"installation-settings.shared-instance.unpublish-button": {
"message": "Geteilte Instanz zurückziehen"
},
"installation-settings.shared-instance.unpublish-description": {
"message": "Entferne diese geteilte Instanz von Modrinth und sende keine Updates mehr an Personen, die sie nutzen. Deine lokale Instanz ist davon nicht betroffen."
},
"installation-settings.shared-instance.unpublishing-button": {
"message": "Wird zurückgezogen..."
},
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Dies betrifft nur deine lokale Instanz. Deine installierten Inhalte bleiben auf diesem Gerät, und die freigegebene Instanz sowie andere Benutzer, die diese verwenden, sind davon nicht betroffen."
},
"installation-settings.unlink-shared-instance.modal.admonition-header": {
"message": "Geteilte Instanz trennen"
},
"installation-settings.unlink-shared-instance.modal.header": {
"message": "Geteilte Instanz trennen"
},
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Dadurch wird die geteilte Instanz von den Servern von Modrinth gelöscht. Personen, die diese in der Modrinth-App nutzen, erhalten keine Updates mehr, aber deine lokale Instanz und dessen Inhalte bleiben auf diesem Gerät erhalten."
},
"installation-settings.unpublish-shared-instance.modal.admonition-header": {
"message": "Geteilte Instanz zurückziehen"
},
"installation-settings.unpublish-shared-instance.modal.header": {
"message": "Geteilte Instanz zurückziehen"
},
"instance.action.create-shortcut": {
"message": "Verknüpfung erstellen"
},
"instance.action.export-modpack": {
"message": "Modpack exportieren"
},
"instance.action.launch-instance": {
"message": "Instanz starten"
},
"instance.action.more-actions": {
"message": "Weitere Aktionen"
},
"instance.action.open-folder": {
"message": "Ordner öffnen"
},
"instance.action.repair": {
"message": "Reparieren"
},
"instance.action.settings": {
"message": "Instanz-Einstellungen"
},
"instance.action.starting": {
"message": "Wird gestartet..."
},
"instance.action.stopping": {
"message": "Wird gestoppt..."
},
"instance.add-server.add-and-play": {
"message": "Hinzufügen und spielen"
},
@@ -797,6 +1139,15 @@
"instance.files.save-as": {
"message": "Speichern unter..."
},
"instance.locked.delete-button": {
"message": "Instanz löschen"
},
"instance.locked.play-tooltip": {
"message": "Diese Instanz wurde gesperrt"
},
"instance.playtime.never-played": {
"message": "Noch nie gespielt"
},
"instance.server-modal.address": {
"message": "Adresse"
},
@@ -932,6 +1283,9 @@
"instance.settings.tabs.installation.loader-version": {
"message": "{loader} Version"
},
"instance.settings.tabs.installation.locked": {
"message": "Installationseinstellungen sind nicht verfügbar, solange diese Instanz gesperrt ist."
},
"instance.settings.tabs.java": {
"message": "Java und Arbeitsspeicher"
},
@@ -971,6 +1325,9 @@
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/pfad/zu/java"
},
"instance.settings.tabs.sharing": {
"message": "Teilen"
},
"instance.settings.tabs.window": {
"message": "Fenster"
},
@@ -1001,6 +1358,57 @@
"instance.settings.tabs.window.width.enter": {
"message": "Breite eingeben..."
},
"instance.shared-instance.error.title": {
"message": "Etwas ist schiefgelaufen"
},
"instance.shared-instance.owner-tooltip": {
"message": "Der Inhalt dieser Instanz wird mit anderen Benutzern geteilt."
},
"instance.shared-instance.publish-review.added-label": {
"message": "Hinzugefügt"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "Update an Spieler senden"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "Konfigurationsdateien auswählen"
},
"instance.shared-instance.publish-review.description": {
"message": "Überprüfe die Inhaltsänderungen und wähle Konfigurationsdateien aus, die in diesem Update enthalten sein sollen."
},
"instance.shared-instance.publish-review.header": {
"message": "Änderungen überprüfen"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "Update senden"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Entfernt"
},
"instance.shared-instance.tooltip": {
"message": "Der Inhalt dieser Instanz wird von jemand anderes verwaltet."
},
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Dein Zugriff auf die geteilte Instanz wurde widerrufen. Diese Instanz ist weiterhin verfügbar, ist jedoch nicht mehr verknüpft und erhält keine Updates mehr."
},
"instance.shared-instance.unavailable.deleted-text": {
"message": "Die primäre Instanz wurde gelöscht. Diese Instanz ist weiterhin verfügbar, ist jedoch nicht mehr verknüpft und erhält keine Updates mehr."
},
"instance.shared-instance.unavailable.locked-text": {
"message": "Diese geteilte Instanz wurde vom Inhaltsmoderationsteam gesperrt. Sie erhält keine Updates von der primären Instanz mehr und kann nicht mehr gespielt werden."
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Instanz gesperrt"
},
"instance.shared-instance.unavailable.manager-fallback": {
"message": "der Instanzmanager"
},
"instance.shared-instance.unavailable.text": {
"message": "Deine lokale Instanz ist weiterhin verfügbar, ist jedoch nicht mehr verknüpft und erhält keine Updates mehr."
},
"instance.shared-instance.unavailable.title": {
"message": "Geteilte Instanz nicht mehr verfügbar"
},
"instance.worlds.a_minecraft_server": {
"message": "Ein Minecraft-Server"
},
@@ -1064,6 +1472,63 @@
"minecraft-account.sign-in": {
"message": "Bei Minecraft anmelden"
},
"minecraft-required.description": {
"message": "Du benötigst ein Microsoft-Konto, welches Minecraft besitzt, bevor du das Spiel starten und spielen kannst."
},
"minecraft-required.description-header": {
"message": "Bei einem Microsoft-Konto anmelden"
},
"minecraft-required.dont-have-account": {
"message": "Du hast kein Konto?"
},
"minecraft-required.get-minecraft": {
"message": "Minecraft holen"
},
"minecraft-required.get-support": {
"message": "Hilfe erhalten"
},
"minecraft-required.header": {
"message": "Minecraft erforderlich"
},
"minecraft-required.sign-in": {
"message": "Bei Microsoft anmelden"
},
"modal.modrinth-account-required.browser-description": {
"message": "Ein neuer Tab zum Anmelden wurde geöffnet. Schließe die Anmeldung dort ab und kehren anschließend zur App zurück."
},
"modal.modrinth-account-required.cancel-button": {
"message": "Abbrechen"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "Im Browser fortfahren"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Ein Konto erstellen"
},
"modal.modrinth-account-required.description": {
"message": "Du musst dich bei deinem Modrinth-Konto anmelden, bevor du diese Funktion nutzen kannst."
},
"modal.modrinth-account-required.header": {
"message": "Konto erforderlich"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "Browser erneut öffnen"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Bei Modrinth anmelden"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Bei einem Modrinth-Konto anmelden"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "Anmelden"
},
"modal.modrinth-account-required.support-prompt": {
"message": "Hast du Probleme bei der Anmeldung? <support>Hilfe erhalten</support>"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "Warte auf Bestätigung durch den Browser..."
},
"search.filter.locked.instance": {
"message": "Von der Instanz vorgegeben"
},
File diff suppressed because it is too large Load Diff
+20 -11
View File
@@ -173,9 +173,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas"
},
"app.appearance-settings.select-option": {
"message": "Selecciona una opción"
},
"app.appearance-settings.show-play-time.description": {
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
},
@@ -227,9 +224,6 @@
"app.browse.back-to-instance": {
"message": "Volver a instancia"
},
"app.browse.discover-content": {
"message": "Descubrir contenido"
},
"app.browse.discover-servers": {
"message": "Descubrir servidores"
},
@@ -428,23 +422,41 @@
"app.modal.install-to-play.header": {
"message": "Instalar para jugar"
},
"app.modal.install-to-play.install-anyway": {
"message": "Instalar de todos modos"
},
"app.modal.install-to-play.install-button": {
"message": "Instalar"
},
"app.modal.install-to-play.invite-warning": {
"message": "Esta invitación la creó otro usuario de Modrinth, no Modrinth. Solo acepta invitaciones de personas en las que confíes."
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} other {# mods}}"
},
"app.modal.install-to-play.reviewed-files": {
"message": "Un archivo solo se revisa si se publica en Modrinth, sin importar su formato (incluido el .mrpack)."
},
"app.modal.install-to-play.shared-instance": {
"message": "Instancia compartida"
},
"app.modal.install-to-play.unknown-files-description": {
"message": "Este modpack para servidor contiene archivos que no están publicados en Modrinth. Recomendamos encarecidamente instalar únicamente archivos de fuentes de confianza."
},
"app.modal.install-to-play.unknown-files-warning": {
"message": "Advertencia sobre archivos desconocidos"
},
"app.modal.install-to-play.unrecognized-files": {
"message": "Archivos no reconocidos"
},
"app.modal.install-to-play.view-contents": {
"message": "Ver contenidos"
},
"app.modal.update-to-play.header": {
"message": "Actualizar para jugar"
},
"app.modal.update-to-play.update-required-description": {
"message": "Se requiere una actualización para jugar {name}. Por favor, actualiza a la versión más reciente para iniciar el juego."
"app.modal.update-to-play.removed": {
"message": "Eliminado"
},
"app.project.install-button.already-installed": {
"message": "Este proyecto ya está instalado"
@@ -455,9 +467,6 @@
"app.project.install-context.back-to-browse": {
"message": "Volver al explorador"
},
"app.project.install-context.install-content-to-instance": {
"message": "Instalar contenido a la instancia"
},
"app.project.version.all-versions": {
"message": "Todas las versiones"
},
@@ -107,9 +107,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas"
},
"app.appearance-settings.select-option": {
"message": "Selecciona una opción"
},
"app.appearance-settings.show-play-time.description": {
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
},
@@ -161,9 +158,6 @@
"app.browse.back-to-instance": {
"message": "Volver a la instancia"
},
"app.browse.discover-content": {
"message": "Descubrir contenido"
},
"app.browse.discover-servers": {
"message": "Descubrir servidores"
},
@@ -287,18 +281,12 @@
"app.modal.update-to-play.header": {
"message": "Actualiza para jugar"
},
"app.modal.update-to-play.update-required-description": {
"message": "Una actualización es requerida para jugar {name}. Por favor actualízala a la versión más reciente para ejecutar el juego."
},
"app.project.install-button.already-installed": {
"message": "El proyecto ya está instalado"
},
"app.project.install-context.back-to-browse": {
"message": "Volver a descubrir"
},
"app.project.install-context.install-content-to-instance": {
"message": "Instalar contenido"
},
"app.settings.developer-mode-enabled": {
"message": "Modo desarrollador activado."
},
+32 -11
View File
@@ -20,6 +20,18 @@
"app.action-bar.install.retry": {
"message": "Yritä uudelleen"
},
"app.action-bar.install.summary.canceled": {
"message": "Peruutettu"
},
"app.action-bar.install.summary.content-download-failed": {
"message": "Ei voitu ladata tiedostoja"
},
"app.action-bar.install.summary.corrupt-download": {
"message": "Ladattu tiedosto on vioittunut"
},
"app.action-bar.install.summary.could-not-save-files": {
"message": "Ei voitu tallentaa tiedostoja"
},
"app.action-bar.install.unknown-instance": {
"message": "Tuntematon instanssi"
},
@@ -107,9 +119,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Natiivit koristukset"
},
"app.appearance-settings.select-option": {
"message": "Valitse vaihtoehto"
},
"app.appearance-settings.show-play-time.description": {
"message": "Näyttää, kuinka paljon aikaa olet käyttänyt instanssin pelaamiseen."
},
@@ -161,9 +170,6 @@
"app.browse.back-to-instance": {
"message": "Takaisin instanssiin"
},
"app.browse.discover-content": {
"message": "Löydä sisältöä"
},
"app.browse.discover-servers": {
"message": "Löydä palvelimia"
},
@@ -299,6 +305,9 @@
"app.instance.mods.successfully-uploaded": {
"message": "Onnistuneesti ladattu"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "Kaikki laitteeseen jo asennetut kopiot säilyvät laitteessa"
},
"app.instance.worlds.add-server": {
"message": "Lisää palvelin"
},
@@ -353,6 +362,9 @@
"app.modal.install-to-play.header": {
"message": "Asenna pelataksesi"
},
"app.modal.install-to-play.install-anyway": {
"message": "Asenna silti"
},
"app.modal.install-to-play.install-button": {
"message": "Asenna"
},
@@ -368,17 +380,17 @@
"app.modal.update-to-play.header": {
"message": "Päivitä pelataksesi"
},
"app.modal.update-to-play.update-required-description": {
"message": "Pävitys vaaditaan pelataksesi {name}. Päivitä viimeisimpään versioon pelataksesi."
},
"app.project.install-button.already-installed": {
"message": "Tämä projekti on jo asennettu"
},
"app.project.install-context.back-to-browse": {
"message": "Takaisin sisällön löytöön"
},
"app.project.install-context.install-content-to-instance": {
"message": "Asenna sisältöä instanssiin"
"app.project.version.installing": {
"message": "Asennetaan"
},
"app.project.versions.already-installed": {
"message": "On jo asennettu"
},
"app.settings.developer-mode-enabled": {
"message": "Kehittäjätila käytössä."
@@ -551,6 +563,12 @@
"app.skins.title": {
"message": "Skinivalinta"
},
"app.skins.toggle-ears-features-off": {
"message": "Poista käytöstä"
},
"app.skins.toggle-ears-features-on": {
"message": "Ota käyttöön"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App versio {version} on ladattu. Voit käynnistää sovelluksen uudelleen päivittääksesi heti tai antaa päivityksen asentua automaattisesti, kun suljet Modrinth Appin."
},
@@ -662,6 +680,9 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Kirjaudu Modrinth-tilille</link> lisätäksesi ystäviä ja nähdäksesi mitä he pelaavat!"
},
"instance.action.create-shortcut": {
"message": "Luo pikakuvake"
},
"instance.add-server.add-and-play": {
"message": "Lisää ja pelaa"
},
@@ -95,9 +95,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Native decorations"
},
"app.appearance-settings.select-option": {
"message": "Pumili ng opsiyon"
},
"app.appearance-settings.show-play-time.description": {
"message": "Magpapakita ang katagal ng iyong paglalaro sa isang instansiya."
},
@@ -140,9 +137,6 @@
"app.browse.back-to-instance": {
"message": "Bumalik sa instansiya"
},
"app.browse.discover-content": {
"message": "Tumuklas ng kontento"
},
"app.browse.discover-servers": {
"message": "Tumuklas ng mga server"
},
@@ -260,9 +254,6 @@
"app.modal.update-to-play.header": {
"message": "Mag-update upang malaro"
},
"app.modal.update-to-play.update-required-description": {
"message": "Kailangang mag-update upang malaro ang {name}. Mangyaring mag-update sa pinakabagong bersiyon upang ma-launch ang laro."
},
"app.project.install-button.already-installed": {
"message": "Naka-install na ang proyektong ito"
},
+519 -30
View File
@@ -1,6 +1,6 @@
{
"app.action-bar.downloading-java": {
"message": "Téléchargement de Java {version} en cours"
"message": "Téléchargement de Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Téléchargement de la mise à jour"
@@ -9,7 +9,7 @@
"message": "Téléchargements"
},
"app.action-bar.hide-more-running-instances": {
"message": "Masquer plus d'instances en exécution"
"message": "Masquer plus d'instances en cours d'exécution"
},
"app.action-bar.install.copied-details": {
"message": "Copié"
@@ -36,7 +36,7 @@
"message": "Annulé"
},
"app.action-bar.install.summary.cleanup-incomplete": {
"message": "Le nettoyage n'est pas terminé"
"message": "Le nettoyage n'a pas pu terminer"
},
"app.action-bar.install.summary.content-download-failed": {
"message": "Impossible de télécharger les fichiers"
@@ -65,29 +65,53 @@
"app.action-bar.install.summary.java-setup-failed": {
"message": "L'installation de Java n'a pas pu se terminer"
},
"app.action-bar.install.summary.loader-setup-failed": {
"message": "La mise en place du loader a échoué"
},
"app.action-bar.install.summary.local-data-error": {
"message": "Impossible de mettre à jour les données locales"
},
"app.action-bar.install.summary.minecraft-setup-failed": {
"message": "La mise en place de Minecraft a échoué"
},
"app.action-bar.install.summary.modrinth-unreachable": {
"message": "Impossible d'atteindre les serveurs Modrinth"
},
"app.action-bar.install.summary.no-write-permission": {
"message": "Aucune permission d'écriture"
},
"app.action-bar.install.summary.pack-download-failed": {
"message": "Impossible de télécharger le pack"
},
"app.action-bar.install.summary.unexpected-error": {
"message": "Quelque chose s'est mal passé"
},
"app.action-bar.install.unknown-instance": {
"message": "Instance inconnue"
},
"app.action-bar.install.updating-shared-content": {
"message": "Mise à jour du contenu partagé"
},
"app.action-bar.installs": {
"message": "Installations"
},
"app.action-bar.make-primary-instance": {
"message": "Définir en tante qu'instance première"
"message": "Définir en tant qu'instance principale"
},
"app.action-bar.no-instances-running": {
"message": "Aucune instance en exécution"
"message": "Aucune instance en cours d'exécution"
},
"app.action-bar.offline": {
"message": "Hors ligne"
},
"app.action-bar.primary-instance": {
"message": "Instance première"
"message": "Instance principale"
},
"app.action-bar.reload-to-update": {
"message": "Recharger pour mettre à jour"
},
"app.action-bar.show-more-running-instances": {
"message": "Montrer plus d'instances en exécution"
"message": "Montrer plus d'instances en cours d'exécution"
},
"app.action-bar.stop-instance": {
"message": "Arrêter l'instance"
@@ -104,6 +128,21 @@
"app.action-bar.view-logs": {
"message": "Voir les journaux"
},
"app.ads-consent.accept": {
"message": "Tout accepter"
},
"app.ads-consent.body": {
"message": "Les publicités rendent Modrinth possible et financent la rémunération des créateurs. Nos partenaires peuvent stocker des cookies ou y accéder au sein de l'application afin de personnaliser les publicités et de mesurer les performances."
},
"app.ads-consent.manage": {
"message": "Gérer les préférences"
},
"app.ads-consent.reject": {
"message": "Tout refuser"
},
"app.ads-consent.title": {
"message": "Votre vie privée et comment les publicités soutiennent Modrinth"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Permet un rendu avancé tel que des effets de flou qui peuvent causer des problèmes de performance sans rendu accéléré par le matériel."
},
@@ -117,7 +156,7 @@
"message": "Couleur du thème"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Modifier la page sur laquelle le lanceur s'ouvre."
"message": "Modifier la page sur laquelle le launcher s'ouvre."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Accueil"
@@ -141,7 +180,7 @@
"message": "Mondes sur partie rapide"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimiser le lanceur quand Minecraft démarre."
"message": "Minimiser le launcher quand a processus Minecraft démarre."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimiser le lanceur"
@@ -152,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Décorations natives"
},
"app.appearance-settings.select-option": {
"message": "Sélectionnez une option"
},
"app.appearance-settings.show-play-time.description": {
"message": "Affiche combien de temps vous avez passé sur une instance."
},
@@ -206,9 +242,6 @@
"app.browse.back-to-instance": {
"message": "Retour à l'instance"
},
"app.browse.discover-content": {
"message": "Découvrir du contenu"
},
"app.browse.discover-servers": {
"message": "Découvrir des serveurs"
},
@@ -263,6 +296,9 @@
"app.install.phase.finalizing": {
"message": "Finalisation"
},
"app.install.phase.preparing_instance": {
"message": "En attente d'installation"
},
"app.install.phase.preparing_java": {
"message": "Préparation de Java"
},
@@ -299,6 +335,36 @@
"app.install.phase.running_loader_processors": {
"message": "Exécution des processeurs du chargeur"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Ajouté"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "Votre instance locale est en avance sur celles des utilisateurs avec lesquels vous lavez partagée."
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "Vos modifications nont pas encore été partagées"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "Publier la mise à jour"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "Publication..."
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "Supprimé"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "Publier la mise à jour"
},
"app.instance.admonitions.shared-instance.review-description": {
"message": "Examiner les modifications de contenu qui seront partagées avec tous les utilisateurs de cette instance."
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Examiner les modifications"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Examination..."
},
"app.instance.confirm-delete.admonition-body": {
"message": "Toutes les données pour votre instance seront supprimées à jamais, y compris vos mondes, vos configurations, et le contenu installé."
},
@@ -335,6 +401,9 @@
"app.instance.mods.content-type-project": {
"message": "projet"
},
"app.instance.mods.locked-content": {
"message": "Le contenu dans les instances verrouillées ne peut pas être modifié."
},
"app.instance.mods.project-was-added": {
"message": "« {name} » a été ajouté"
},
@@ -350,8 +419,104 @@
"app.instance.mods.successfully-uploaded": {
"message": "Mis en ligne avec succès"
},
"app.instance.share.empty.description": {
"message": "Vous pouvez partager cette instance avec vos amis !"
},
"app.instance.share.empty.heading": {
"message": "Aucun ami invité"
},
"app.instance.share.empty.invite-friends-button": {
"message": "Inviter des amis"
},
"app.instance.share.invite-modal.heading": {
"message": "Partager {name}"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "Vous devez vous connecter en tant que"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "pour accéder à cette page."
},
"app.instance.share.locked.linked-account-fallback": {
"message": "le compte lié"
},
"app.instance.share.locked.signed-out-heading": {
"message": "Non connecté"
},
"app.instance.share.locked.switch-account-button": {
"message": "Changer de compte"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "Mauvais compte"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Ils vont désormais cesser de recevoir des mises à jour pour cette instance partagée"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "N'importe copie qu'ils ont installée va rester sur leur appareil"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "Vous pouvez les inviter plus tard"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "C'est le dernier utilisateur, le partage va être désactivé pour cette instance"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "Que se passe-t-il ?"
},
"app.instance.share.remove-user-modal.header": {
"message": "Révoquer l'accès"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "Révoquer l'accès"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "avatar de {username}"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "Si vous révoquez l'accès de {username} à cette instance partagée, vous aurez besoin de les inviter de nouveau avant qu'ils puissent recevoir des mises à jour."
},
"app.instance.share.sign-in.button": {
"message": "Se connecter"
},
"app.instance.share.unlink.body": {
"message": "Vous devez délier ce modpack pour partager votre instance"
},
"app.instance.share.unlink.header": {
"message": "Partager nécessite de délier"
},
"app.instance.shared-instance-already-installed.body": {
"message": "Cette instance partagée est déjà installée en tant que <bold>{instanceName}</bold>. Êtes-vous sûr de vouloir installer une autre copie ?"
},
"app.instance.shared-instance-already-installed.header": {
"message": "L'instance partagée est déjà installée"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "Installer quand même"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "Instance"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "le compte lié"
},
"app.instance.shared-instance-wrong-account.owner-admonition-body-v2": {
"message": "pour gérer cette instance partagée. Vous ne pourrez pas déployer des mises à jour aux utilisateurs."
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "Se connecter en tant que"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Vous devez vous connecter à Modrinth"
},
"app.instance.shared-instance-wrong-account.user-admonition-body-v2": {
"message": "pour recevoir des mises à jour pour cette instance partagée."
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "Vous utilisez le mauvais compte Modrinth"
},
"app.instance.worlds.add-server": {
"message": "Ajouter le serveur"
"message": "Ajouter un serveur"
},
"app.instance.worlds.browse-servers": {
"message": "Parcourir des serveurs"
@@ -384,13 +549,13 @@
"message": "Ajouter un serveur ou parcourir pour commencer"
},
"app.instance.worlds.no-worlds-heading": {
"message": "Aucun serveur ou monde ajoutés"
"message": "Aucun serveur ou monde ajouté"
},
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Enlever le serveur"
"message": "Supprimer le serveur"
},
"app.instance.worlds.remove-server-modal.title": {
"message": "Enlever le serveur"
"message": "Supprimer le serveur"
},
"app.instance.worlds.remove-server-modal.warning-body": {
"message": "Ce serveur sera enlevé de votre liste de serveur et de la liste de serveur en jeu. Vous pourrez le rajouter plus tard si vous connaissez son adresse."
@@ -401,26 +566,116 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "Rechercher {count} mondes..."
},
"app.modal.install-to-play.additional-context": {
"message": "Contexte additionnel"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "Si possible, incluez des liens et des images pertinentes"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "L'instance que vous signalez"
},
"app.modal.install-to-play.delete-instance": {
"message": "Supprimer l'instance"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "Ne pas installer"
},
"app.modal.install-to-play.header": {
"message": "Installer pour jouer"
},
"app.modal.install-to-play.install-anyway": {
"message": "Installer quand même"
},
"app.modal.install-to-play.install-button": {
"message": "Installer"
},
"app.modal.install-to-play.invite-warning": {
"message": "Cette invitation a été crée par un autre utilisateur Modrinth, pas par Modrinth. N'acceptez seulement les invitations de personnes en qui vous avez confiance."
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} other {# mods}}"
},
"app.modal.install-to-play.report-description": {
"message": "Utilisez ce formulaire pour signaler les instances qui peuvent enfreindre nos <rules-link>Règles</rules-link> ou <terms-link>les Conditions d'Utilisation</terms-link>."
},
"app.modal.install-to-play.report-image-invalid-type": {
"message": "Le fichier n'est pas un type d'image accepté"
},
"app.modal.install-to-play.report-image-too-large": {
"message": "Le fichier dépasse la limite de 1 MiB"
},
"app.modal.install-to-play.report-legal-claims": {
"message": "Pour les notifications DMCA ou autres réclamations juridiques, veuillez consulter notre <copyright-link>Politique relative aux droits d'auteur</copyright-link>."
},
"app.modal.install-to-play.report-reason": {
"message": "Quelle règle cette instance enfreint-elle ?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "Inapproprié"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "Malicieux"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "Signaler une instance partagée"
},
"app.modal.install-to-play.report-submitted": {
"message": "Signalement soumis"
},
"app.modal.install-to-play.report-support-and-bugs": {
"message": "Pour des demandes de support, contactez notre <support-link>équipe de support</support-link>. Pour signaler des bugs, ouvrez <github-link>une issue Github</github-link>."
},
"app.modal.install-to-play.reviewed-files": {
"message": "Un fichier est seulement examiné s'il est publié sur Modrinth, peu importe son extension (y compris .mrpack)."
},
"app.modal.install-to-play.shared-instance": {
"message": "Instance partagée"
},
"app.modal.install-to-play.shared-instance-content": {
"message": "Contenu de l'instance partagée"
},
"app.modal.install-to-play.shared-instance-unknown-files-description": {
"message": "Cette instance partagée contient des fichiers qui ne sont pas publiés sur Modrinth. Nous recommandons fortement de seulement installer des fichiers de sources en qui vous avez confiance."
},
"app.modal.install-to-play.unknown-files-description": {
"message": "Ce modpack de serveur contient des fichiers qui ne sont pas publiés sur Modrinth. Nous recommandons fortement de seulement installer des fichiers de sources en qui vous avez confiance."
},
"app.modal.install-to-play.unknown-files-warning": {
"message": "Avertissement de fichiers inconnus"
},
"app.modal.install-to-play.unrecognized-files": {
"message": "Fichiers non reconnus"
},
"app.modal.install-to-play.view-contents": {
"message": "Voir le contenu"
},
"app.modal.update-to-play.header": {
"message": "Mettre à jour pour jouer"
},
"app.modal.update-to-play.removed": {
"message": "Supprimé"
},
"app.modal.update-to-play.server-modpack-unknown-files-description": {
"message": "Cette mise à jour du modpack de ce serveur contient des fichiers qui ne sont pas publiés sur Modrinth. Nous recommandons fortement de seulement installer des fichiers de sources en qui vous avez confiance."
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "Ajouté"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "Supprimé"
},
"app.modal.update-to-play.shared-instance-unknown-files-description": {
"message": "Cette mise à jour d'instance partagée contient des fichiers qui ne sont pas publiés sur Modrinth. Nous recommandons fortement de seulement installer des fichiers de sources en qui vous avez confiance."
},
"app.modal.update-to-play.update-required": {
"message": "Mise à jour requise"
},
"app.modal.update-to-play.update-required-description": {
"message": "Une mise à jour est requise pour jouer à {name}. Veuillez mettre à jour à la dernière version pour lancer le jeu."
"message": "Une mise à jour est requise pour jouer à {name}. Veuillez mettre à jour vers la dernière version pour lancer le jeu."
},
"app.project.install-button.already-installed": {
"message": "Ce projet a déjà été installé"
@@ -431,9 +686,6 @@
"app.project.install-context.back-to-browse": {
"message": "Retour vers la page découvrir"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installer du contenu à l'instance"
},
"app.project.version.all-versions": {
"message": "Toutes les versions"
},
@@ -452,6 +704,9 @@
"app.settings.downloading": {
"message": "Téléchargement de la version {version}"
},
"app.settings.privacy.ads-consent.intro": {
"message": "Les publicités rendent Modrinth possible et financent les rémunérations des créateurs. Nos partenaires peuvent stocker des cookies ou y accéder au sein de l'application afin de personnaliser les publicités et de mesurer les performances. Vous pouvez refuser ou gérer vos préférences ci-dessous."
},
"app.settings.tabs.appearance": {
"message": "Apparence"
},
@@ -471,7 +726,7 @@
"message": "Gestion des ressources"
},
"app.skins.add-button": {
"message": "Ajouter une skin"
"message": "Ajouter un skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Glisser et déposer"
@@ -486,7 +741,7 @@
"message": "Cela va supprimer de manière permanente ce skin. Cette action est irréversible."
},
"app.skins.delete-modal.title": {
"message": "Êtes-vous sûr de vouloir supprimer cette skin ?"
"message": "Êtes-vous sûr de vouloir supprimer ce skin ?"
},
"app.skins.dropped-file-error.text": {
"message": "Impossible de lire le fichier déposé."
@@ -494,17 +749,20 @@
"app.skins.dropped-file-error.title": {
"message": "Erreur lors du traitement du fichier"
},
"app.skins.ears-feature-notice": {
"message": "Ce skin utilise des fonctionnalités du mod {ears}"
},
"app.skins.edit-button": {
"message": "Modifier le skin"
},
"app.skins.modal.add-skin-button": {
"message": "Ajouter une skin"
"message": "Ajouter un skin"
},
"app.skins.modal.add-title": {
"message": "Ajouter une skin"
"message": "Ajouter un skin"
},
"app.skins.modal.arm-style-section": {
"message": "Bras"
"message": "Style du bras"
},
"app.skins.modal.arm-style-slim": {
"message": "Mince"
@@ -543,7 +801,7 @@
"message": "Texture"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Importez d'abord une skin !"
"message": "Importez d'abord un skin !"
},
"app.skins.preview.edit-button": {
"message": "Modifier le skin"
@@ -620,6 +878,12 @@
"app.skins.title": {
"message": "Sélecteur de skin"
},
"app.skins.toggle-ears-features-off": {
"message": "Désactiver"
},
"app.skins.toggle-ears-features-on": {
"message": "Activer"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} a finie d'être téléchargée. Rechargez pour mettre à jour maintenant, ou automatiquement quand vous fermez Modrinth App."
},
@@ -665,6 +929,39 @@
"app.world.world-item.offline": {
"message": "Hors ligne"
},
"content.shared-instance.change-version-body": {
"message": "Changer la version change seulement votre copie locale. Des futures mises à jour d'instance partagées peuvent restaurer ou changer la version."
},
"content.shared-instance.delete-bulk-body": {
"message": "Certains projets sélectionnés font partie de l'instance partagée. Les supprimer changerait seulement votre copie locale, et de futures mises à jour d'instance partagées peuvent restaurer ou les changer de nouveau."
},
"content.shared-instance.delete-button": {
"message": "Supprimer quand même"
},
"content.shared-instance.delete-many-button": {
"message": "Supprimer {count, number} projets quand même"
},
"content.shared-instance.delete-single-body": {
"message": "Le supprimer change seulement votre copie locale. De futures mises à jour d'instance partagées peuvent restaurer ou changer la version."
},
"content.shared-instance.disable-bulk-body": {
"message": "Certains projets sélectionnés font partie de l'instance partagée. Les désactiver changerait seulement votre copie locale, et de futures mises à jour d'instance partagées peuvent restaurer ou les changer de nouveau."
},
"content.shared-instance.disable-button": {
"message": "Désactiver quand même"
},
"content.shared-instance.disable-many-button": {
"message": "Désactiver {count, number} projets quand même"
},
"content.shared-instance.disable-single-body": {
"message": "Le désactiver change seulement votre copie locale. De futures mises à jour d'instance partagées peuvent restaurer ou changer la version."
},
"content.shared-instance.unlink-body": {
"message": "Le délier change seulement votre copie locale. De futures mises à jour d'instance partagées peuvent restaurer ou changer la version."
},
"content.shared-instance.warning-header": {
"message": "Ceci fait partie de l'instance partagée"
},
"friends.action.add-friend": {
"message": "Ajouter un ami"
},
@@ -699,7 +996,7 @@
"message": "La demande d'ami a été envoyé"
},
"friends.friend.view-profile": {
"message": "Voir le profile"
"message": "Voir le profil"
},
"friends.heading": {
"message": "Amis"
@@ -728,6 +1025,75 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Créer un compte Modrinth</link> pour ajouter des amis et voir à quoi ils jouent !"
},
"installation-settings.shared-instance.linked-title": {
"message": "Instance partagée liée"
},
"installation-settings.shared-instance.title": {
"message": "Dépublier l'instance"
},
"installation-settings.shared-instance.unlink-button": {
"message": "Dissocier l'instance partagée"
},
"installation-settings.shared-instance.unlink-description": {
"message": "Déconnecter cette instance locale de futures mises à jour partagées."
},
"installation-settings.shared-instance.unlinking-button": {
"message": "Dissociation..."
},
"installation-settings.shared-instance.unpublish-button": {
"message": "Dépublier l'instance partagée"
},
"installation-settings.shared-instance.unpublish-description": {
"message": "Supprime cette instance partagée de Modrinth et arrête d'envoyer des mises à jour à toute personne l'utilisant. Votre copie locale ne sera pas affectée."
},
"installation-settings.shared-instance.unpublishing-button": {
"message": "Dépublication..."
},
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Cela affecte seulement votre instance locale. Votre contenu installé restera sur cet appareil, et l'instance partagée et les autres personnes l'utilisant ne seront pas affectés."
},
"installation-settings.unlink-shared-instance.modal.admonition-header": {
"message": "Dissociation de l'instance partagée"
},
"installation-settings.unlink-shared-instance.modal.header": {
"message": "Dissocier l'instance partagée"
},
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Cela supprime l'instance partagée des serveurs de Modrinth. Les personnes l'utilisant dans l'application Modrinth vont cesser de recevoir des mises à jour, mais votre instance locale et son contenu resteront sur cet appareil."
},
"installation-settings.unpublish-shared-instance.modal.admonition-header": {
"message": "Dépublication de l'instance partagée"
},
"installation-settings.unpublish-shared-instance.modal.header": {
"message": "Dépublier l'instance partagée"
},
"instance.action.create-shortcut": {
"message": "Créer un raccourci"
},
"instance.action.export-modpack": {
"message": "Exporter le modpack"
},
"instance.action.launch-instance": {
"message": "Lancer l'instance"
},
"instance.action.more-actions": {
"message": "Plus d'actions"
},
"instance.action.open-folder": {
"message": "Ouvrir le dossier"
},
"instance.action.repair": {
"message": "Réparer"
},
"instance.action.settings": {
"message": "Paramètres d'instance"
},
"instance.action.starting": {
"message": "Démarrage..."
},
"instance.action.stopping": {
"message": "Arrêt..."
},
"instance.add-server.add-and-play": {
"message": "Ajouter et jouer"
},
@@ -770,6 +1136,15 @@
"instance.files.save-as": {
"message": "Sauvegarder en tant que..."
},
"instance.locked.delete-button": {
"message": "Supprimer l'instance"
},
"instance.locked.play-tooltip": {
"message": "Cette instance a été verrouillée"
},
"instance.playtime.never-played": {
"message": "Jamais joué"
},
"instance.server-modal.address": {
"message": "Adresse"
},
@@ -905,6 +1280,9 @@
"instance.settings.tabs.installation.loader-version": {
"message": "Version de {loader}"
},
"instance.settings.tabs.installation.locked": {
"message": "Les paramètres d'installation ne sont pas disponible pendant que cette instance est verrouillée."
},
"instance.settings.tabs.java": {
"message": "Java et mémoire"
},
@@ -944,6 +1322,9 @@
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/chemin/vers/java"
},
"instance.settings.tabs.sharing": {
"message": "Partage"
},
"instance.settings.tabs.window": {
"message": "Fenêtre"
},
@@ -974,6 +1355,57 @@
"instance.settings.tabs.window.width.enter": {
"message": "Entrer la largeur..."
},
"instance.shared-instance.error.title": {
"message": "Quelque chose s'est mal passé"
},
"instance.shared-instance.owner-tooltip": {
"message": "Le contenu de cette instance est partagé à d'autres utilisateurs."
},
"instance.shared-instance.publish-review.added-label": {
"message": "Ajouté"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "Déployer la mise à jour aux joueurs"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "Sélectionner les fichiers de configuration"
},
"instance.shared-instance.publish-review.description": {
"message": "Examiner les changements de contenu et choisir n'importe quel fichier de configuration à inclure dans cette mise à jour."
},
"instance.shared-instance.publish-review.header": {
"message": "Examiner les changements"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "Déployer la mise à jour"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Supprimé"
},
"instance.shared-instance.tooltip": {
"message": "Le contenu de cette instance est gérée par quelqu'un d'autre."
},
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Votre accès à l'instance partagée a été révoqué. Cette instance reste disponible, mais n'est plus liée et ne recevra plus de mises à jour."
},
"instance.shared-instance.unavailable.deleted-text": {
"message": "L'instance principale a été supprimée. Cette instance est encore disponible, mais n'est plus liée et va cesser de recevoir des mises à jour."
},
"instance.shared-instance.unavailable.locked-text": {
"message": "Cette instance partagée a été verrouillée par la Team de Modération de Contenu. Elle ne recevra plus de mises à jour de l'instance primaire et ne peut plus être jouée."
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Instance verrouillée"
},
"instance.shared-instance.unavailable.manager-fallback": {
"message": "le gestionnaire d'instance"
},
"instance.shared-instance.unavailable.text": {
"message": "L'instance principale a été supprimée. Cette instance est encore disponible, mais n'est plus liée et ne va plus recevoir de mises à jour."
},
"instance.shared-instance.unavailable.title": {
"message": "Instance partagée n'est plus disponible"
},
"instance.worlds.a_minecraft_server": {
"message": "Un Serveur Minecraft"
},
@@ -1037,6 +1469,63 @@
"minecraft-account.sign-in": {
"message": "Se connecter à Minecraft"
},
"minecraft-required.description": {
"message": "Vous avez besoin d'un compte Microsoft qui possède Minecraft avant de jouer."
},
"minecraft-required.description-header": {
"message": "Se connecter à un compte Microsoft"
},
"minecraft-required.dont-have-account": {
"message": "Pas de compte ?"
},
"minecraft-required.get-minecraft": {
"message": "Obtenir Minecraft"
},
"minecraft-required.get-support": {
"message": "Obtenir de l'aide"
},
"minecraft-required.header": {
"message": "Minecraft requis"
},
"minecraft-required.sign-in": {
"message": "Se connecter à Microsoft"
},
"modal.modrinth-account-required.browser-description": {
"message": "A nouvel onglet a été ouvert pour vous connecter. Finissez la connexion, puis retournez dans l'application."
},
"modal.modrinth-account-required.cancel-button": {
"message": "Annuler"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "Continuer dans votre navigateur"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Créer un compte"
},
"modal.modrinth-account-required.description": {
"message": "Vous aurez besoin de vous connecter à votre compte Modrinth avant de pouvoir utiliser cette fonctionnalité."
},
"modal.modrinth-account-required.header": {
"message": "Compte requis"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "Ouvrir de nouveau le navigateur"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Se connecter à Modrinth"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Se connecter à un compte Modrinth"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "Connexion"
},
"modal.modrinth-account-required.support-prompt": {
"message": "Vous avez des problèmes à vous connecter ? <support>Obtenez de l'aide</support>"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "En attente de la confirmation du navigateur..."
},
"search.filter.locked.instance": {
"message": "Procuré par l'instance"
},
@@ -44,9 +44,6 @@
"app.browse.already-added": {
"message": "כבר נוסף"
},
"app.browse.discover-content": {
"message": "גלה תוכן"
},
"app.browse.discover-servers": {
"message": "גלה שרתים"
},
@@ -155,9 +152,6 @@
"app.modal.update-to-play.header": {
"message": "צריך לעדכן כדי לשחק"
},
"app.modal.update-to-play.update-required-description": {
"message": "עדכון נדרש כדי לשחק ב{name}. ניתן להתחיל את המשחק רק לאחר עדכון לגרסה החדשה."
},
"app.settings.developer-mode-enabled": {
"message": "מצב מפתח מופעל."
},
+368 -92
View File
@@ -9,7 +9,7 @@
"message": "Letöltések"
},
"app.action-bar.hide-more-running-instances": {
"message": "További futó példányok elrejtése"
"message": "További futó játékpéldányok elrejtése"
},
"app.action-bar.install.copied-details": {
"message": "Másolva"
@@ -21,7 +21,7 @@
"message": "Elutasítás"
},
"app.action-bar.install.open-instance": {
"message": "Példány megnyitása"
"message": "Játékpéldány megnyitása"
},
"app.action-bar.install.retry": {
"message": "Újra"
@@ -48,10 +48,10 @@
"message": "Nem sikerült elmenteni a fájlokat"
},
"app.action-bar.install.summary.download-failed": {
"message": "A letöltés nem sikerült"
"message": "A letöltés nem fejeződött be"
},
"app.action-bar.install.summary.instance-not-found": {
"message": "A példány nem található"
"message": "A játékpéldány nem található"
},
"app.action-bar.install.summary.invalid-file-path": {
"message": "A fájl elérési útja érvénytelen"
@@ -63,7 +63,7 @@
"message": "A modcsomag fájljainak metaadatai érvénytelenek"
},
"app.action-bar.install.summary.java-setup-failed": {
"message": "A Java telepítése nem sikerült"
"message": "A Java telepítése nem tudott befejeződni"
},
"app.action-bar.install.summary.loader-setup-failed": {
"message": "A betöltő beállítása sikertelen volt"
@@ -72,13 +72,13 @@
"message": "A helyi adatok frissítése nem sikerült"
},
"app.action-bar.install.summary.minecraft-setup-failed": {
"message": "A Minecraft telepítése sikertelen volt"
"message": "A Minecraft telepítése meghiúsult"
},
"app.action-bar.install.summary.modrinth-unreachable": {
"message": "Nem sikerült elérni Modrinthot"
},
"app.action-bar.install.summary.no-write-permission": {
"message": "Nincs felülírási jogosultság"
"message": "Nincs írási jogosultság"
},
"app.action-bar.install.summary.pack-download-failed": {
"message": "Nem sikerült letölteni a csomagot"
@@ -87,31 +87,34 @@
"message": "Valami hiba történt"
},
"app.action-bar.install.unknown-instance": {
"message": "Ismeretlen profil"
"message": "Ismeretlen játékpéldány"
},
"app.action-bar.install.updating-shared-content": {
"message": "Megosztott tartalom frissítése"
},
"app.action-bar.installs": {
"message": "Telepítések"
},
"app.action-bar.make-primary-instance": {
"message": "Beállítás elsődleges példányként"
"message": "Beállítás elsődleges játékpéldányként"
},
"app.action-bar.no-instances-running": {
"message": "Nincsenek futó példányok"
"message": "Nincsenek futó játékpéldányok"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Elsődleges példány"
"message": "Elsődleges játékpéldány"
},
"app.action-bar.reload-to-update": {
"message": "Töltsd újra a frissítéshez"
"message": "Frissítés elérhető"
},
"app.action-bar.show-more-running-instances": {
"message": "További futó pédányok megjelenítése"
},
"app.action-bar.stop-instance": {
"message": "Példány leállítása"
"message": "Játékpéldány leállítása"
},
"app.action-bar.update": {
"message": "Frissítés"
@@ -120,10 +123,25 @@
"message": "Aktív letöltések megtekintése"
},
"app.action-bar.view-instance": {
"message": "Példányok megtekintése"
"message": "Játékpéldány megtekintése"
},
"app.action-bar.view-logs": {
"message": "Naplózások megtekintése"
"message": "Naplók megtekintése"
},
"app.ads-consent.accept": {
"message": "Összes elfogadása"
},
"app.ads-consent.body": {
"message": "A hirdetések teszik lehetővé a Modrinth működését, és biztosítják a tartalomkészítők juttatásait. Partnereink sütiket tárolhatnak vagy érhetnek el az alkalmazásban a hirdetések személyre szabása és a teljesítmény mérése céljából."
},
"app.ads-consent.manage": {
"message": "Beállítások kezelése"
},
"app.ads-consent.reject": {
"message": "Összes elutasítása"
},
"app.ads-consent.title": {
"message": "Adataid védelme és a hirdetések szerepe a Modrinth működésében"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Lehetővé teszi olyan speciális megjelenítési effektusok használatát, mint például az elmosódás, amelyek hardveres gyorsítás nélkül teljesítménycsökkenést okozhatnak."
@@ -173,11 +191,8 @@
"app.appearance-settings.native-decorations.title": {
"message": "Rendszer ablakkeret használata"
},
"app.appearance-settings.select-option": {
"message": "Válassz egy lehetőséget"
},
"app.appearance-settings.show-play-time.description": {
"message": "Megmutatja, mennyi időt töltöttél egy példányban."
"message": "Megmutatja, mennyi időt töltöttél egy játékpéldányban."
},
"app.appearance-settings.show-play-time.title": {
"message": "Játékidő megjelenítése"
@@ -207,13 +222,13 @@
"message": "Nem lehet elérni a hitelesítési kiszolgálókat"
},
"app.browse.add-servers-to-instance": {
"message": "Szerver hozzáadása a példányhoz"
"message": "Szerver hozzáadása a játékpéldányhoz"
},
"app.browse.add-to-an-instance": {
"message": "Hozzáadás egy példányhoz"
"message": "Hozzáadás egy játékpéldányhoz"
},
"app.browse.add-to-instance": {
"message": "Példányhoz adás"
"message": "Hozzáadás a játékpéldányhoz"
},
"app.browse.add-to-instance-name": {
"message": "Hozzáadás ehhez: {instanceName}"
@@ -225,10 +240,7 @@
"message": "Már hozzá van adva"
},
"app.browse.back-to-instance": {
"message": "Vissza a példányhoz"
},
"app.browse.discover-content": {
"message": "Tartalom böngészése"
"message": "Vissza a játékpéldányhoz"
},
"app.browse.discover-servers": {
"message": "Szerverek böngészése"
@@ -270,73 +282,103 @@
"message": "1.0.0"
},
"app.install.phase.downloading_content": {
"message": "Tartalom letöltése"
"message": "A tartalom letöltése..."
},
"app.install.phase.downloading_minecraft": {
"message": "Minecraft letöltése"
"message": "A Minecraft letöltése..."
},
"app.install.phase.downloading_pack_file": {
"message": "Csomagfájlok letöltése"
"message": "A csomagfájl letöltése..."
},
"app.install.phase.extracting_overrides": {
"message": "Felülírások kibontása"
"message": "Felülírások kibontása..."
},
"app.install.phase.finalizing": {
"message": "Befejezés"
"message": "Befejezés..."
},
"app.install.phase.preparing_instance": {
"message": "Telepítésre vár"
"message": "Telepítésre vár..."
},
"app.install.phase.preparing_java": {
"message": "Java előkészítése"
"message": "A Java előkészítése..."
},
"app.install.phase.preparing_java.downloading": {
"message": "Java verzió {version} letöltése"
"message": "A Java {version} letöltése..."
},
"app.install.phase.preparing_java.extracting": {
"message": "Java verzió {version} kibontása"
"message": "A Java {version} kibontása..."
},
"app.install.phase.preparing_java.fetching-metadata": {
"message": "Java verzió ellőkészítése {version}"
"message": "A Java {version} beszerzése..."
},
"app.install.phase.preparing_java.resolving": {
"message": "Java verzió {version} előkészítése"
"message": "A Java {version} előkészítése..."
},
"app.install.phase.preparing_java.validating": {
"message": "A Java {version} ellenőrzése"
"message": "A Java {version} ellenőrzése..."
},
"app.install.phase.reading_pack_manifest": {
"message": "Csomag adati kiolvasása"
"message": "A modcsomag leírófájljának beolvasása..."
},
"app.install.phase.resolving_loader": {
"message": ""
"message": "A betöltő meghatározása..."
},
"app.install.phase.resolving_minecraft": {
"message": "Minecraft Betöltése"
"message": "A Minecraft-verzió meghatározása..."
},
"app.install.phase.resolving_pack": {
"message": "Tartalom betöltése"
"message": "A tartalom meghatározása..."
},
"app.install.phase.rolling_back": {
"message": "Visszavonás"
"message": "Visszavonás..."
},
"app.install.phase.running_loader_processors": {
"message": "Betöltő folyamatok futtatása"
"message": "Betöltőfolyamatok futtatása..."
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Hozzáadva"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "A te gépeden lévő játékpéldány újabb, mint azoké a felhasználóké, akikkel megosztottad."
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "A változtatásaid még nem lettek megosztva"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "Frissítés közzététele"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "Közzététel..."
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "Eltávolítva"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "Frissítés közzététele"
},
"app.instance.admonitions.shared-instance.review-description": {
"message": "A játékpéldányt használó összes résztvevővel megosztásra kerülő tartalomváltoztatások áttekintése."
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Változtatások áttekintése"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Áttekintés..."
},
"app.instance.confirm-delete.admonition-body": {
"message": "Az profilodhoz tartozó összes adat véglegesen törlődik, beleértve a világokat, a beállításokat és az összes telepített tartalmat."
"message": "A játékpéldányhoz tartozó összes adat véglegesen törlődik, beleértve a világokat, a beállításokat és az összes telepített tartalmat."
},
"app.instance.confirm-delete.admonition-header": {
"message": "Ezt a műveletet nem lehet visszavonni"
},
"app.instance.confirm-delete.delete-button": {
"message": "Profil törlése"
"message": "Játékpéldány törlése"
},
"app.instance.confirm-delete.header": {
"message": "Profil törlése"
"message": "Játékpéldány törlése"
},
"app.instance.modpack-already-installed.body": {
"message": "Ez a modcsomag már telepítve van a(z) <bold>{instanceName}</bold> profilban. Biztosan duplikálni szeretnéd?"
"message": "Ez a modcsomag már telepítve van a(z) <bold>{instanceName}</bold> játékpéldányban. Biztosan duplikálni szeretnéd?"
},
"app.instance.modpack-already-installed.create": {
"message": "Létrehozás"
@@ -345,7 +387,7 @@
"message": "A modcsomag már telepítve van"
},
"app.instance.modpack-already-installed.instance": {
"message": "Példány"
"message": "Játékpéldány"
},
"app.instance.mods.bulk-update.downloading-projects": {
"message": "{current, number}/{total, number} projekt letöltése..."
@@ -359,8 +401,11 @@
"app.instance.mods.content-type-project": {
"message": "projekt"
},
"app.instance.mods.locked-content": {
"message": "Zárolt játékpéldányokban a tartalom nem változtatható meg."
},
"app.instance.mods.project-was-added": {
"message": " „{name}” hozzá lett adva"
"message": "„{name}” hozzá lett adva"
},
"app.instance.mods.projects-were-added": {
"message": "{count} projekt került hozzáadásra"
@@ -374,6 +419,96 @@
"app.instance.mods.successfully-uploaded": {
"message": "Sikeresen feltöltve"
},
"app.instance.share.empty.description": {
"message": "Ezt a játékpéldányt megoszthatod a barátaiddal!"
},
"app.instance.share.empty.heading": {
"message": "Nincsenek barátok meghívva"
},
"app.instance.share.empty.invite-friends-button": {
"message": "Barátok meghívása"
},
"app.instance.share.invite-modal.heading": {
"message": "{name} megosztása"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "Be kell jelentkezned mint"
},
"app.instance.share.locked.empty-description-suffix": {
"message": ", hogy hozzáférj ehhez az oldalhoz."
},
"app.instance.share.locked.linked-account-fallback": {
"message": "az összekapcsolt fiók"
},
"app.instance.share.locked.signed-out-heading": {
"message": "Nem vagy bejelentkezve"
},
"app.instance.share.locked.switch-account-button": {
"message": "Fiókváltás"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "Helytelen fiók"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Többé nem fog frissítéseket kapni ehhez a megosztott játékpéldányhoz"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "A már telepített játékpéldányok megmaradnak az eszközén"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "Később újra meghívhatod"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "Ő az utolsó felhasználó, a megosztás ezen az játékpéldányon le lesz tiltva"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "Mi fog történni?"
},
"app.instance.share.remove-user-modal.header": {
"message": "Hozzáférés visszavonása"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "Hozzáférés visszavonása"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "{username} avatárja"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "Ha visszavonod {username} hozzáférését ehhez a megosztott játékpéldányhoz, újra meg kell hívnod, mielőtt frissítéseket kaphatna."
},
"app.instance.share.sign-in.button": {
"message": "Bejelentkezés"
},
"app.instance.share.unlink.body": {
"message": "A játékpéldány megosztásához le kell választanod ezt a modcsomagot"
},
"app.instance.share.unlink.header": {
"message": "A megosztáshoz le kell választani"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "Telepítés mindenképpen"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "Játékpéldány"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "az összekapcsolt fiók"
},
"app.instance.shared-instance-wrong-account.owner-admonition-body-v2": {
"message": ", hogy kezelhesd ezt a megosztott játékpéldányt. Nem fogod tudni a frissítéseket eljuttatni a felhasználókhoz."
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "Jelentkezz be mint"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Be kell jelentkezned a Modrinthba"
},
"app.instance.shared-instance-wrong-account.user-admonition-body-v2": {
"message": ", hogy értesítéseket kapj erről a megosztott játékpéldányról."
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "Nem a megfelelő Modrinth-fiókot használod"
},
"app.instance.worlds.add-server": {
"message": "Szerver hozzáadása"
},
@@ -381,13 +516,13 @@
"message": "Szerverek böngészése"
},
"app.instance.worlds.delete-world-modal.delete-button": {
"message": "Törlés"
"message": "Világ törlése"
},
"app.instance.worlds.delete-world-modal.title": {
"message": "Világ törlése"
},
"app.instance.worlds.delete-world-modal.warning-body": {
"message": "Ez a világ véglegesen törölve lesz ebből a példányból. Ezt NEM tudod visszavonni!"
"message": "Ez a világ véglegesen törölve lesz ebből a játékpéldányból. Ezt NEM tudod visszavonni!"
},
"app.instance.worlds.delete-world-modal.warning-header": {
"message": "{name} törlése"
@@ -420,22 +555,55 @@
"message": "Ez a szerver el lesz távolítva a szerverlistádból és a játékbeli szerverlistából is. Később újra hozzáadhatod, ha tudod a címet."
},
"app.instance.worlds.remove-server-modal.warning-header": {
"message": "{name} eltávolítása"
"message": "{name} törlése"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Keresés {count} világ között..."
},
"app.modal.install-to-play.additional-context": {
"message": "További kontextus"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "A játékpéldány, amelyet bejelenteni kívánsz"
},
"app.modal.install-to-play.delete-instance": {
"message": "Játékpéldány törlése"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "Ne telepítse"
},
"app.modal.install-to-play.header": {
"message": "Telepítés a játékhoz"
},
"app.modal.install-to-play.install-anyway": {
"message": "Telepítés mindenképpen"
},
"app.modal.install-to-play.install-button": {
"message": "Telepítés"
},
"app.modal.install-to-play.invite-warning": {
"message": "Ezt a meghívót nem a Modrinth, hanem egy másik Modrinth-felhasználó hozta létre. Csak olyan személyektől fogadd el a meghívásokat, akikben megbízol."
},
"app.modal.install-to-play.mod-count": {
"message": "{count} mod"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.reviewed-files": {
"message": "Egy fájlt csak akkor vizsgálnak meg, ha azt a Modrinth-en közzétették, függetlenül a fájlformátumtól (beleértve a .mrpack formátumot is)."
},
"app.modal.install-to-play.shared-instance": {
"message": "Megosztott profil"
"message": "Megosztott játékpéldány"
},
"app.modal.install-to-play.unknown-files-description": {
"message": "Ez a szerver-modpack olyan fájlokat tartalmaz, amelyek nem szerepelnek a Modrinth oldalon. Határozottan javasoljuk, hogy kizárólag olyan forrásokból telepíts fájlokat, amelyekben megbízol."
},
"app.modal.install-to-play.unknown-files-warning": {
"message": "Figyelmeztetés ismeretlen fájlokról"
},
"app.modal.install-to-play.unrecognized-files": {
"message": "Felismerhetetlen fájlok"
},
"app.modal.install-to-play.view-contents": {
"message": "Tartalom megtekintése"
@@ -443,8 +611,8 @@
"app.modal.update-to-play.header": {
"message": "Frissítsd a játékhoz"
},
"app.modal.update-to-play.update-required-description": {
"message": "Frissítés szükséges ehhez: {name}. Kérlek, frissíts a legújabb verzióra a játék elindításához."
"app.modal.update-to-play.removed": {
"message": "Törölve"
},
"app.project.install-button.already-installed": {
"message": "Ez a projekt már telepítve van"
@@ -455,9 +623,6 @@
"app.project.install-context.back-to-browse": {
"message": "Vissza a böngészéshez"
},
"app.project.install-context.install-content-to-instance": {
"message": "Tartalom telepítése a példányba"
},
"app.project.version.all-versions": {
"message": "Összes verzió"
},
@@ -476,6 +641,9 @@
"app.settings.downloading": {
"message": "v{version} letöltése"
},
"app.settings.privacy.ads-consent.intro": {
"message": "A hirdetések teszik lehetővé a Modrinth működését, és biztosítják a tartalomkészítők juttatásait. Partnereink sütiket tárolhatnak vagy érhetnek el az alkalmazásban a hirdetések személyre szabása és a teljesítmény mérése céljából. Az alábbiakban leiratkozhatsz erről, illetve beállíthatod a preferenciáidat."
},
"app.settings.tabs.appearance": {
"message": "Megjelenés"
},
@@ -518,6 +686,9 @@
"app.skins.dropped-file-error.title": {
"message": "Hiba a fájl feldolgozása közben"
},
"app.skins.ears-feature-notice": {
"message": "Ez a kinézet az {ears} mod funkcióit használja"
},
"app.skins.edit-button": {
"message": "Kinézet szerkesztése"
},
@@ -644,6 +815,12 @@
"app.skins.title": {
"message": "Kinézetváltó"
},
"app.skins.toggle-ears-features-off": {
"message": "Kikapcsolás"
},
"app.skins.toggle-ears-features-on": {
"message": "Bekapcsolás"
},
"app.update-popup.body.download-complete": {
"message": "A Modrinth App v{version} letöltése befejeződött. Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás a bezárásakor automatikusan frissül."
},
@@ -663,7 +840,7 @@
"message": "Sikeres letöltés"
},
"app.update-popup.reload": {
"message": "Töltsd újra a frissítéshez"
"message": "Frissítés"
},
"app.update-popup.title": {
"message": "Frissítés elérhető"
@@ -755,6 +932,42 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Lépj be Modrinth fiókodba</link>, hogy felvehess barátokat és lásd mivel játszanak!"
},
"installation-settings.shared-instance.unpublishing-button": {
"message": "Közzététel visszavonása..."
},
"installation-settings.unlink-shared-instance.modal.admonition-header": {
"message": "Megosztott játékpéldány leválasztása"
},
"installation-settings.unlink-shared-instance.modal.header": {
"message": "Megosztott játékpéldány leválasztása"
},
"instance.action.create-shortcut": {
"message": "Parancsikon létrehozása"
},
"instance.action.export-modpack": {
"message": "Modcsomag exportálása"
},
"instance.action.launch-instance": {
"message": "Játékpéldány elindítása"
},
"instance.action.more-actions": {
"message": "További műveletek"
},
"instance.action.open-folder": {
"message": "Mappa megnyitása"
},
"instance.action.repair": {
"message": "Javítás"
},
"instance.action.settings": {
"message": "Játékpéldány-beállítások"
},
"instance.action.starting": {
"message": "Indítás..."
},
"instance.action.stopping": {
"message": "Leállítás..."
},
"instance.add-server.add-and-play": {
"message": "Hozzáadás és csatlakozás"
},
@@ -762,7 +975,7 @@
"message": "Szerver hozzáadása"
},
"instance.add-server.resource-pack.disabled": {
"message": "Kikapcsolva"
"message": "Letiltva"
},
"instance.add-server.resource-pack.enabled": {
"message": "Engedélyezve"
@@ -771,13 +984,13 @@
"message": "Mindig kérdezzen rá"
},
"instance.add-server.title": {
"message": "Adjon hozzá egy szervert"
"message": "Szerver hozzáadása"
},
"instance.edit-server.title": {
"message": "Szerver módosítása"
},
"instance.edit-world.hide-from-home": {
"message": "Elrejtés a Főoldalról"
"message": "Elrejtés a kezdőlapról"
},
"instance.edit-world.name": {
"message": "Név"
@@ -792,11 +1005,17 @@
"message": "Világ szerkesztése"
},
"instance.files.adding-files": {
"message": "Fájlok hozzáadása ({completed}/{total})"
"message": "Fájlok hozzáadása... ({completed}/{total})"
},
"instance.files.save-as": {
"message": "Mentés másként..."
},
"instance.locked.delete-button": {
"message": "Játékpéldány törlése"
},
"instance.playtime.never-played": {
"message": "Még nem játszott"
},
"instance.server-modal.address": {
"message": "Cím"
},
@@ -807,19 +1026,19 @@
"message": "Egy Minecraft szerver"
},
"instance.server-modal.resource-pack": {
"message": "Erőforráscsomag"
"message": "Forráscsomag"
},
"instance.settings.tabs.general": {
"message": "Általános"
},
"instance.settings.tabs.general.delete": {
"message": "Profil törlése"
"message": "Játékpéldány törlése"
},
"instance.settings.tabs.general.delete.button": {
"message": "Profil törlése"
"message": "Játékpéldány törlése"
},
"instance.settings.tabs.general.delete.description": {
"message": "Örökké eltávolít egy profilt az eszközről, beleértve a világait, beállításait és minden telepített tartalmat. Legyen óvatos, mert ha egyszer kitöröl egy profilt, azt többé nem lehet visszaállítani."
"message": "Örökké eltávolít egy játékpéldányt az eszközről, beleértve a világait, beállításait és minden telepített tartalmat. Legyél óvatos, mert ha egyszer kitörölsz egy játékpéldányt, azt többé nem lehet visszaállítani."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Törlés..."
@@ -831,10 +1050,10 @@
"message": "Telepítés közben nem lehet duplikálni."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "Profil duplikálása"
"message": "Játékpéldány duplikálása"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Készít egy másolatot erről a profilról, beleértve a világokat, beállításokat, modokat, stb."
"message": "Készít egy másolatot erről a játékpéldányról, beleértve a világokat, beállításokat, modokat stb."
},
"instance.settings.tabs.general.edit-icon": {
"message": "Ikon szerkesztése"
@@ -849,16 +1068,16 @@
"message": "Ikon kiválasztása"
},
"instance.settings.tabs.general.library-groups": {
"message": "Könyvtár gyűjtemények"
"message": "Könyvtárgyűjtemények"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "Új csoport létrehozása"
"message": "Új gyűjtemény létrehozása"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "A könyvtárgyűjtemények segítenek külön kategóriákba rendszerezni a profiljait."
"message": "A könyvtárgyűjtemények segítenek külön kategóriákba rendszerezni a játékpéldányokat."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "Adja meg a gyűjtemény nevét"
"message": "Add meg a gyűjtemény nevét"
},
"instance.settings.tabs.general.name": {
"message": "Név"
@@ -882,10 +1101,10 @@
"message": "Kiadás"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Csak a 'kiadás' verziók fognak megjelenni az elérhető frissítések között."
"message": "Csak a kiadás verziók fognak megjelenni az elérhető frissítések között."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Válaszd ki a frissítési csatornát"
"message": "Frissítési csatorna kiválasztása"
},
"instance.settings.tabs.hooks": {
"message": "Indítási horgok"
@@ -909,7 +1128,7 @@
"message": "Indítás előtt"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Futtatás a profil futtatása előtt."
"message": "Futtatás a játékpéldány futtatása előtt."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Írjon be indítás előtti parancsokat..."
@@ -924,7 +1143,7 @@
"message": "Indítóparancs a Minecraft elindításához."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Írjon ide indítóparancsot..."
"message": "Add meg az indítóparancsot..."
},
"instance.settings.tabs.installation": {
"message": "Telepítés"
@@ -971,6 +1190,9 @@
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/eleresi/ut/a/javahoz"
},
"instance.settings.tabs.sharing": {
"message": "Megosztás"
},
"instance.settings.tabs.window": {
"message": "Ablak"
},
@@ -981,7 +1203,7 @@
"message": "Teljes képernyő"
},
"instance.settings.tabs.window.fullscreen.description": {
"message": "A játék indításakor teljes képernyős módban indítsa el (az options.txt fájl segítségével)."
"message": "A játék az indításakor teljes képernyős módban indul el (az options.txt fájl segítségével)."
},
"instance.settings.tabs.window.height": {
"message": "Magasság"
@@ -990,7 +1212,7 @@
"message": "A játék ablakának magassága indításkor."
},
"instance.settings.tabs.window.height.enter": {
"message": "Magaság megadása..."
"message": "Magasság megadása..."
},
"instance.settings.tabs.window.width": {
"message": "Szélesség"
@@ -1001,6 +1223,18 @@
"instance.settings.tabs.window.width.enter": {
"message": "Szélesség megadása..."
},
"instance.shared-instance.publish-review.added-label": {
"message": "Hozzáadva"
},
"instance.shared-instance.publish-review.header": {
"message": "Változtatások áttekintése"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Eltávolítva"
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Pédány lezárva"
},
"instance.worlds.a_minecraft_server": {
"message": "Egy Minecraft szerver"
},
@@ -1011,13 +1245,13 @@
"message": "Cím másolása"
},
"instance.worlds.create_shortcut": {
"message": "Gyorshivatkozás létrehozása"
"message": "Parancsikon létrehozása"
},
"instance.worlds.dont_show_on_home": {
"message": "Ne mutasd a kezdőlapon"
},
"instance.worlds.game_already_open": {
"message": "A profil már el van indítva"
"message": "A játékpéldány már el van indítva"
},
"instance.worlds.hardcore": {
"message": "Hardcore mód"
@@ -1035,13 +1269,13 @@
"message": "Csak Minecraft Alpha 1.0.5+-tól tudsz egyből szerverhez csatlakozni"
},
"instance.worlds.no_singleplayer_quick_play": {
"message": "Használj 1.20+ verziót az azonnali egyjátékos módba lépéshez"
"message": "Csak Minecraft 1.20+-tól tudsz egyből egyjátékos világba belépni"
},
"instance.worlds.play_instance": {
"message": "Játék a profillal"
"message": "Játékpéldány indítása"
},
"instance.worlds.view_instance": {
"message": "Példány megtekintése"
"message": "Játékpéldány megtekintése"
},
"instance.worlds.world_in_use": {
"message": "A világ használatban van"
@@ -1064,28 +1298,70 @@
"minecraft-account.sign-in": {
"message": "Bejelentkezés a Minecraftba"
},
"minecraft-required.description": {
"message": "A játék elindításához és játszásához szükséged van egy olyan Microsoft-fiókra, amelyhez a Minecraft tartozik."
},
"minecraft-required.description-header": {
"message": "Bejelentkezés egy Microsoft-fiókba"
},
"minecraft-required.dont-have-account": {
"message": "Még nincs fiókod?"
},
"minecraft-required.get-minecraft": {
"message": "Szerezd be a Minecraftot!"
},
"minecraft-required.get-support": {
"message": "Támogatás"
},
"minecraft-required.header": {
"message": "Minecraft szükséges"
},
"minecraft-required.sign-in": {
"message": "Bejelentkezés a Microsft-ba"
},
"modal.modrinth-account-required.cancel-button": {
"message": "Mégse"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Fiók létrehozása"
},
"modal.modrinth-account-required.header": {
"message": "Fiók szükséges"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Jelentkezz be a Modrinth-be"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Jelentkezz be egy Modrinth-fiókba"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "Bejelentkezés"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "Várakozás a böngésző megerősítésére..."
},
"search.filter.locked.instance": {
"message": "dány által megadva"
"message": "A játékpéldány által van megadva"
},
"search.filter.locked.instance-game-version.title": {
"message": "A játékverziót az adott példány biztosítja"
"message": "A játékverziót az adott játékpéldány biztosítja"
},
"search.filter.locked.instance-loader.title": {
"message": "A betöltőt az adott példány biztosítja"
"message": "A betöltőt az adott játékpéldány biztosítja"
},
"search.filter.locked.instance.sync": {
"message": "Profil szinkronizálása"
"message": "Szinkronizálás a játékpéldánnyal"
},
"search.filter.locked.server": {
"message": "A szerver által van megadva"
},
"search.filter.locked.server-environment.title": {
"message": "Csak kliensoldali modok adhatók hozzá a szerverprofilhoz"
"message": "Csak kliensoldali modok adhatók hozzá a szerverpéldányhoz"
},
"search.filter.locked.server-game-version.title": {
"message": "A játékverzió a szerver által van megadva"
"message": "A játékverziót az adott szerver biztosítja"
},
"search.filter.locked.server-loader.title": {
"message": "A betöltő a szerver által van megadva"
"message": "A betöltőt az adott szerver biztosítja"
}
}
@@ -92,9 +92,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Hiasan asli"
},
"app.appearance-settings.select-option": {
"message": "Pilih opsi"
},
"app.appearance-settings.show-play-time.description": {
"message": "Menampilkan seberapa lama Anda memainkan sebuah instans."
},
@@ -140,9 +137,6 @@
"app.browse.back-to-instance": {
"message": "Kembali ke instans"
},
"app.browse.discover-content": {
"message": "Temukan konten"
},
"app.browse.discover-servers": {
"message": "Temukan server"
},
@@ -263,18 +257,12 @@
"app.modal.update-to-play.header": {
"message": "Perbarui untuk memainkan"
},
"app.modal.update-to-play.update-required-description": {
"message": "{name} perlu diperbarui sebelum dimainkan. Mohon perbarui ke versi terkini untuk meluncurkan permainan."
},
"app.project.install-button.already-installed": {
"message": "Proyek ini sudah terpasang"
},
"app.project.install-context.back-to-browse": {
"message": "Kembali ke halaman pencarian"
},
"app.project.install-context.install-content-to-instance": {
"message": "Pasang konten ke instans"
},
"app.settings.developer-mode-enabled": {
"message": "Mode pengembang dihidupkan."
},
+493 -28
View File
@@ -27,7 +27,7 @@
"message": "Riprova"
},
"app.action-bar.install.summary.app-closing": {
"message": "Annullato per app chiusa"
"message": "Annullato per chiusura app"
},
"app.action-bar.install.summary.bad-modpack-file": {
"message": "Impossibile leggere il pacchetto"
@@ -89,6 +89,9 @@
"app.action-bar.install.unknown-instance": {
"message": "Istanza sconosciuta"
},
"app.action-bar.install.updating-shared-content": {
"message": "Aggiornando i contenuti condivisi"
},
"app.action-bar.installs": {
"message": "Installazioni"
},
@@ -105,13 +108,13 @@
"message": "Primaria"
},
"app.action-bar.reload-to-update": {
"message": "Ricarica per aggiornare"
"message": "Riavvia"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostra più istanze in esecuzione"
},
"app.action-bar.stop-instance": {
"message": "Ferma l'istanza"
"message": "Arresta l'istanza"
},
"app.action-bar.update": {
"message": "Aggiorna"
@@ -125,6 +128,21 @@
"app.action-bar.view-logs": {
"message": "Mostra i log"
},
"app.ads-consent.accept": {
"message": "Accetta tutto"
},
"app.ads-consent.body": {
"message": "Le pubblicità aiutano Modrinth a crescere e a finanziare i creatori. I nostri partner potrebbero memorizzare o accedere ai cookie nell'app per personalizzare e misurarne le prestazioni."
},
"app.ads-consent.manage": {
"message": "Gestisci le opzioni"
},
"app.ads-consent.reject": {
"message": "Rifiuta tutto"
},
"app.ads-consent.title": {
"message": "La tua privacy e come le pubblicità supportano Modrinth"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Abilita alcuni effetti come la sfocatura, ma può causare problemi di prestazioni senza accelerazione hardware."
},
@@ -173,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Decorazioni native"
},
"app.appearance-settings.select-option": {
"message": "Seleziona"
},
"app.appearance-settings.show-play-time.description": {
"message": "Mostra quanto tempo hai speso giocando a un'istanza."
},
@@ -186,7 +201,7 @@
"message": "Salta automaticamente le conferme a basso rischio, come l'installazione di pacchetti di mod duplicati, la normale eliminazione di contenuti, gli aggiornamenti simultanei, lo scollegare i pacchetti di mod e le richieste di riparazione. Le avvertenze verranno comunque mostrate."
},
"app.appearance-settings.skip-non-essential-warnings.title": {
"message": "Salta le conferme poco importanti"
"message": "Salta le conferme meno importanti"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Mostra o nascondi la barra laterale."
@@ -198,7 +213,7 @@
"message": "Mostrerà un avviso prima di installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth, a causa dei possibili rischi."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Avvisami se installo pacchetti sconosciuti"
"message": "Avvisami quando installo pacchetti sconosciuti"
},
"app.auth-servers.unreachable.body": {
"message": "I server di autenticazione di Minecraft stanno riscontrando problemi. Controlla la tua connessione a Internet e riprova più tardi."
@@ -227,11 +242,8 @@
"app.browse.back-to-instance": {
"message": "Torna all'istanza"
},
"app.browse.discover-content": {
"message": "Esplora i contenuti"
},
"app.browse.discover-servers": {
"message": "Esplora i server"
"message": "Sfoglia i server"
},
"app.browse.hide-added-servers": {
"message": "Nascondi server già aggiunti"
@@ -323,6 +335,36 @@
"app.install.phase.running_loader_processors": {
"message": "Esecuzione dei processori del loader"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Aggiunto"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "L'istanza locale è più avanti di quella degli utenti che hai invitato."
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "Le modifiche non sono ancora state condivise"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "Invia"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "Invio..."
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "Rimosso"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "Invia aggiornamento"
},
"app.instance.admonitions.shared-instance.review-description": {
"message": "Revisiona i contenuti modificati che verranno condivisi con l'intera istanza."
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Revisiona modifiche"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Revisione..."
},
"app.instance.confirm-delete.admonition-body": {
"message": "Tutti i dati della tua istanza verranno eliminati permanentemente, inclusi i tuoi mondi, configurazioni e tutti i contenuti installati."
},
@@ -359,6 +401,9 @@
"app.instance.mods.content-type-project": {
"message": "progetto"
},
"app.instance.mods.locked-content": {
"message": "I contenuti nelle istanze bloccate non possono essere modificati."
},
"app.instance.mods.project-was-added": {
"message": "\"{name}\" è stato aggiunto"
},
@@ -371,11 +416,107 @@
"app.instance.mods.successfully-uploaded": {
"message": "Caricato con successo"
},
"app.instance.share.empty.description": {
"message": "Invita chi vuoi a questa istanza!"
},
"app.instance.share.empty.heading": {
"message": "Nessun invito inviato"
},
"app.instance.share.empty.invite-friends-button": {
"message": "Invia un invito"
},
"app.instance.share.invite-modal.heading": {
"message": "Condividi {name}"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "Devi effettuare l'accesso come"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "per accedere a questa pagina."
},
"app.instance.share.locked.linked-account-fallback": {
"message": "l'account collegato"
},
"app.instance.share.locked.signed-out-heading": {
"message": "Accesso non effettuato"
},
"app.instance.share.locked.switch-account-button": {
"message": "Cambia account"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "Account sbagliato"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Non riceveranno più gli aggiornamenti di questa istanza condivisa"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "La copia che hanno già nei loro dispositivi rimarrà"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "Puoi reinvitarli più tardi"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "Questo è l'ultimo utente, la condivisione verrà spenta per questa istanza"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "Cosa succede?"
},
"app.instance.share.remove-user-modal.header": {
"message": "Revoca accesso"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "Revoca accesso"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "Foto profilo di {username}"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "Se revochi l'accesso di {username} a questa istanza condivisa, dovrai reinvitarli prima che ricevano gli aggiornamenti."
},
"app.instance.share.sign-in.button": {
"message": "Accedi"
},
"app.instance.share.unlink.body": {
"message": "Devi scollegare questo pacchetto per condividere l'istanza"
},
"app.instance.share.unlink.header": {
"message": "Scollega prima di condividere"
},
"app.instance.shared-instance-already-installed.body": {
"message": "Questa istanza condivisa è già installata come <bold>{instanceName}</bold>. Vuoi davvero installarne un'altra copia?"
},
"app.instance.shared-instance-already-installed.header": {
"message": "Istanza condivisa già installata"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "Installa comunque"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "Istanza"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "l'account collegato"
},
"app.instance.shared-instance-wrong-account.owner-admonition-body-v2": {
"message": "per gestire l'istanza condivisa. Non potrai inviare gli aggiornamenti agli utenti."
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "Accedi come"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Devi aver effettuato l'accesso a Modrinth"
},
"app.instance.shared-instance-wrong-account.user-admonition-body-v2": {
"message": "per ricevere gli aggiornamenti dell'istanza condivisa."
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "Stai usando l'account Modrinth sbagliato"
},
"app.instance.worlds.add-server": {
"message": "Aggiungi server"
},
"app.instance.worlds.browse-servers": {
"message": "Esplora i server"
"message": "Sfoglia i server"
},
"app.instance.worlds.delete-world-modal.delete-button": {
"message": "Elimina mondo"
@@ -384,7 +525,7 @@
"message": "Eliminare il mondo"
},
"app.instance.worlds.delete-world-modal.warning-body": {
"message": "Questo mondo verrà eliminato permanentemente da questa istanza. Questa azione non può essere annullata."
"message": "Questo mondo verrà eliminato permanentemente dall'istanza. Questa azione non può essere annullata."
},
"app.instance.worlds.delete-world-modal.warning-header": {
"message": "Eliminare {name}"
@@ -402,7 +543,7 @@
"message": "Vanilla"
},
"app.instance.worlds.no-worlds-description": {
"message": "Inizia esplorando o aggiungendo un server"
"message": "Inizia sfogliando o aggiungendo un server"
},
"app.instance.worlds.no-worlds-heading": {
"message": "Nessun server o mondo aggiunto"
@@ -422,24 +563,114 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "Cerca tra {count} mondi..."
},
"app.modal.install-to-play.additional-context": {
"message": "Contesto aggiuntivo"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "Se possibile, includi link e immagini pertinenti"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "L'istanza che stai segnalando"
},
"app.modal.install-to-play.delete-instance": {
"message": "Elimina istanza"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "Non installare"
},
"app.modal.install-to-play.header": {
"message": "Installa per continuare"
},
"app.modal.install-to-play.install-anyway": {
"message": "Installa comunque"
},
"app.modal.install-to-play.install-button": {
"message": "Installa"
},
"app.modal.install-to-play.invite-warning": {
"message": "Questo invito è stato creato da un utente e non da Modrinth. Accetta solo gli inviti da persone fidate."
},
"app.modal.install-to-play.mod-count": {
"message": "{count} mod"
},
"app.modal.install-to-play.report-description": {
"message": "Usa questo modulo per segnalare le istanze che violano le nostre <rules-link>Regole</rules-link> o <terms-link>Termini d'uso</terms-link>."
},
"app.modal.install-to-play.report-image-invalid-type": {
"message": "Il file non è un'immagine supportata"
},
"app.modal.install-to-play.report-image-too-large": {
"message": "Il file supera il limite di 1 MiB"
},
"app.modal.install-to-play.report-legal-claims": {
"message": "Per DMCA o altre rivendicazioni legali, controlla la nostra <copyright-link>Politica copyright</copyright-link>."
},
"app.modal.install-to-play.report-reason": {
"message": "Quale regola è violata da quest'istanza?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "Inappropriato"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "Dannoso"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "Segnala istanza condivisa"
},
"app.modal.install-to-play.report-submitted": {
"message": "Segnalazione inviata"
},
"app.modal.install-to-play.report-support-and-bugs": {
"message": "Per assistenza, contatta il nostro <support-link>team di assistenza</support-link>. Per segnalare bug, apri un <github-link>Issue su GitHub</github-link>."
},
"app.modal.install-to-play.reviewed-files": {
"message": "Solo i file pubblicati su Modrinth vengono esaminati, qualunque sia il loro formato (.mrpack inclusi)."
},
"app.modal.install-to-play.shared-instance": {
"message": "Istanza condivisa"
},
"app.modal.install-to-play.shared-instance-content": {
"message": "Contenuti istanza condivisa"
},
"app.modal.install-to-play.shared-instance-unknown-files-description": {
"message": "L'istanza condivisa contiene dei file che non sono su Modrinth. Consigliamo di installare file solo da fonti attendibili."
},
"app.modal.install-to-play.unknown-files-description": {
"message": "Il pacchetto contiene dei file che non sono su Modrinth. Consigliamo di installare file solo da fonti attendibili."
},
"app.modal.install-to-play.unknown-files-warning": {
"message": "File sconosciuti"
},
"app.modal.install-to-play.unrecognized-files": {
"message": "File non riconosciuti"
},
"app.modal.install-to-play.view-contents": {
"message": "Mostra contenuti"
},
"app.modal.update-to-play.header": {
"message": "Aggiorna per continuare"
},
"app.modal.update-to-play.removed": {
"message": "Rimosso"
},
"app.modal.update-to-play.server-modpack-unknown-files-description": {
"message": "L'aggiornamento del pacchetto contiene dei file che non sono su Modrinth. Consigliamo di installare file solo da fonti attendibili."
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "Aggiunto"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "Rimosso"
},
"app.modal.update-to-play.shared-instance-unknown-files-description": {
"message": "L'aggiornamento dell'istanza condivisa contiene dei file che non sono su Modrinth. Consigliamo di installare file solo da fonti attendibili."
},
"app.modal.update-to-play.update-required": {
"message": "Aggiornamento richiesto"
},
"app.modal.update-to-play.update-required-description": {
"message": "{name} richiede degli aggiornamenti. Installa l'ultima versione per poter giocare."
},
@@ -452,20 +683,17 @@
"app.project.install-context.back-to-browse": {
"message": "Torna a Esplora"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installa il contenuto nell'istanza"
},
"app.project.version.all-versions": {
"message": "Tutte le versioni"
},
"app.project.version.download-in-browser": {
"message": "Scarica col browser"
"message": "Scarica dal browser"
},
"app.project.version.installing": {
"message": "Installazione"
},
"app.project.versions.already-installed": {
"message": "Già installate"
"message": "Già installata"
},
"app.settings.developer-mode-enabled": {
"message": "Modalità sviluppatore attiva."
@@ -473,6 +701,9 @@
"app.settings.downloading": {
"message": "Scaricando v{version}"
},
"app.settings.privacy.ads-consent.intro": {
"message": "Le pubblicità aiutano Modrinth a crescere e a finanziare i creatori. I nostri partner potrebbero memorizzare o accedere ai cookie nell'app per personalizzare e misurarne le prestazioni. Di seguito puoi scegliere di disdire o di gestire le opzioni."
},
"app.settings.tabs.appearance": {
"message": "Aspetto"
},
@@ -515,6 +746,9 @@
"app.skins.dropped-file-error.title": {
"message": "Impossibile processare il file"
},
"app.skins.ears-feature-notice": {
"message": "Questa skin usa caratteristiche della mod {ears}"
},
"app.skins.edit-button": {
"message": "Modifica"
},
@@ -558,7 +792,7 @@
"message": "Salva"
},
"app.skins.modal.saving-tooltip": {
"message": "Salvando..."
"message": "Salvataggio..."
},
"app.skins.modal.texture-section": {
"message": "Aspetto"
@@ -641,8 +875,14 @@
"app.skins.title": {
"message": "Seleziona una skin"
},
"app.skins.toggle-ears-features-off": {
"message": "Disattiva"
},
"app.skins.toggle-ears-features-on": {
"message": "Attiva"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} è stata scaricata. Ricarica per aggiornare ora, o avverrà in automatico alla chiusura dell'app."
"message": "Modrinth App v{version} è stata scaricata. Riavvia per aggiornare ora, o avverrà in automatico alla chiusura dell'app."
},
"app.update-popup.body.linux": {
"message": "Modrinth App v{version} è disponibile. Usa il tuo gestore di pacchetti per aggiornare e ricevere le novità più recenti!"
@@ -660,7 +900,7 @@
"message": "Download completato"
},
"app.update-popup.reload": {
"message": "Ricarica"
"message": "Riavvia"
},
"app.update-popup.title": {
"message": "Aggiornamento disponibile"
@@ -689,11 +929,44 @@
"app.world.world-item.players-online": {
"message": "{count} online"
},
"content.shared-instance.change-version-body": {
"message": "Modificare la versione cambia solo la copia locale. Gli aggiornamenti condivisi futuri potrebbero rimodificarla."
},
"content.shared-instance.delete-bulk-body": {
"message": "Alcuni progetti selezionati fanno parte dell'istanza condivisa. Eliminarli cambia solo la copia locale, e gli aggiornamenti condivisi futuri potrebbero ripristinarli."
},
"content.shared-instance.delete-button": {
"message": "Elimina comunque"
},
"content.shared-instance.delete-many-button": {
"message": "Elimina {count, number} progetti comunque"
},
"content.shared-instance.delete-single-body": {
"message": "Eliminarlo cambia solo la copia locale. Gli aggiornamenti condivisi futuri potrebbero ripristinarlo."
},
"content.shared-instance.disable-bulk-body": {
"message": "Alcuni progetti selezionati fanno parte dell'istanza condivisa. Disabilitarli cambia solo la copia locale, e gli aggiornamenti condivisi futuri potrebbero riabilitarli."
},
"content.shared-instance.disable-button": {
"message": "Disabilita comunque"
},
"content.shared-instance.disable-many-button": {
"message": "Disabilita {count, number} progetti comunque"
},
"content.shared-instance.disable-single-body": {
"message": "Disabilitarlo cambia solo la copia locale. Gli aggiornamenti condivisi futuri potrebbero riabilitarlo."
},
"content.shared-instance.unlink-body": {
"message": "Scollegarlo cambia solo la copia locale. Gli aggiornamenti condivisi futuri potrebbero ricollegarlo."
},
"content.shared-instance.warning-header": {
"message": "Questo fa parte dell'istanza condivisa"
},
"friends.action.add-friend": {
"message": "Stringi un'amicizia"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, one {# richiesta} other {# richieste}} d''amicizia"
"message": "{count, plural, one {# richiesta} other {# richieste}} d'amicizia"
},
"friends.add-friend.submit": {
"message": "Invia richiesta d'amicizia"
@@ -705,10 +978,10 @@
"message": "Potrebbe essere diverso dal nome utente di Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Inserisci il nome utente Modrinth..."
"message": "Nome utente di Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Con quale utente Modrinth vuoi stringere l'amicizia?"
"message": "Con quale utente di Modrinth vuoi stringere amicizia?"
},
"friends.add-friends-to-share": {
"message": "<link>Stringi un''amicizia</link> per sapere a cosa stanno giocando!"
@@ -752,6 +1025,75 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Accedi a un account Modrinth</link> per stringere amicizie e sapere a cosa stanno giocando!"
},
"installation-settings.shared-instance.linked-title": {
"message": "Istanza condivisa collegata"
},
"installation-settings.shared-instance.title": {
"message": "Rimuovi l'istanza dai server"
},
"installation-settings.shared-instance.unlink-button": {
"message": "Scollega l'istanza condivisa"
},
"installation-settings.shared-instance.unlink-description": {
"message": "Sconnetti l'istanza locale dai prossimi aggiornamenti condivisi."
},
"installation-settings.shared-instance.unlinking-button": {
"message": "Scollegamento..."
},
"installation-settings.shared-instance.unpublish-button": {
"message": "Rimuovi l'istanza condivisa dai server"
},
"installation-settings.shared-instance.unpublish-description": {
"message": "Rimuovi l'istanza condivisa da Modrinth e non inviare più aggiornamenti a chi la usa. L'istanza locale non sarà toccata."
},
"installation-settings.shared-instance.unpublishing-button": {
"message": "Rimozione..."
},
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Ciò influirà solo sull'istanza locale. I contenuti installati rimarranno sul dispositivo e l'istanza condivisa e gli utenti che la usano non verranno toccati."
},
"installation-settings.unlink-shared-instance.modal.admonition-header": {
"message": "Scollegare l'istanza condivisa"
},
"installation-settings.unlink-shared-instance.modal.header": {
"message": "Scollega l'istanza condivisa"
},
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Ciò eliminerà l'istanza condivisa dai server di Modrinth. Gli utenti che la usavano non riceveranno più aggiornamenti, ma l'istanza locale e i contenuti rimarranno su questo dispositivo."
},
"installation-settings.unpublish-shared-instance.modal.admonition-header": {
"message": "Rimuovere l'istanza condivisa dai server"
},
"installation-settings.unpublish-shared-instance.modal.header": {
"message": "Rimuovi l'istanza condivisa dai server"
},
"instance.action.create-shortcut": {
"message": "Crea collegamento"
},
"instance.action.export-modpack": {
"message": "Esporta pacchetto"
},
"instance.action.launch-instance": {
"message": "Avvia istanza"
},
"instance.action.more-actions": {
"message": "Altre azioni"
},
"instance.action.open-folder": {
"message": "Apri cartella"
},
"instance.action.repair": {
"message": "Ripara"
},
"instance.action.settings": {
"message": "Impostazioni istanza"
},
"instance.action.starting": {
"message": "Avvio..."
},
"instance.action.stopping": {
"message": "Arresto..."
},
"instance.add-server.add-and-play": {
"message": "Aggiungi e avvia"
},
@@ -794,6 +1136,15 @@
"instance.files.save-as": {
"message": "Salva come..."
},
"instance.locked.delete-button": {
"message": "Elimina istanza"
},
"instance.locked.play-tooltip": {
"message": "Questa istanza è stata bloccata"
},
"instance.playtime.never-played": {
"message": "Mai avviata"
},
"instance.server-modal.address": {
"message": "Indirizzo"
},
@@ -819,7 +1170,7 @@
"message": "Elimina permanentemente un'istanza dal tuo dispositivo, compresi i tuoi mondi, file di configurazione e tutto il contenuto installato. Fai attenzione: eliminata un'istanza non c'è modo di recuperarla."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Eliminando..."
"message": "Eliminazione..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "Duplica"
@@ -840,7 +1191,7 @@
"message": "Rimuovi icona"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "Cambia icona"
"message": "Sfoglia"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Scegli un'icona"
@@ -929,6 +1280,9 @@
"instance.settings.tabs.installation.loader-version": {
"message": "Versione di {loader}"
},
"instance.settings.tabs.installation.locked": {
"message": "Le impostazioni non sono disponibili mentre l'istanza è bloccata."
},
"instance.settings.tabs.java": {
"message": "Java e memoria"
},
@@ -968,6 +1322,9 @@
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/percorso/di/java"
},
"instance.settings.tabs.sharing": {
"message": "Condivisione"
},
"instance.settings.tabs.window": {
"message": "Finestra"
},
@@ -998,6 +1355,57 @@
"instance.settings.tabs.window.width.enter": {
"message": "Inserisci larghezza..."
},
"instance.shared-instance.error.title": {
"message": "Qualcosa è andato storto"
},
"instance.shared-instance.owner-tooltip": {
"message": "I contenuti dell'istanza stanno venendo condivisi agli utenti."
},
"instance.shared-instance.publish-review.added-label": {
"message": "Aggiunto"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "Invia l'aggiornamento ai giocatori"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "Seleziona i file di configurazione"
},
"instance.shared-instance.publish-review.description": {
"message": "Revisiona i contenuti modificati e scegli i file di configurazione da includere nell'aggiornamento."
},
"instance.shared-instance.publish-review.header": {
"message": "Revisiona modifiche"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "Invia"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Rimosso"
},
"instance.shared-instance.tooltip": {
"message": "I contenuti dell'istanza stanno venendo gestiti da qualcun altro."
},
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "L'accesso all'istanza condivisa è stato revocato. È ancora disponibile localmente, ma non è più collegata e non riceverà aggiornamenti."
},
"instance.shared-instance.unavailable.deleted-text": {
"message": "L'istanza primaria è stata eliminata. È ancora disponibile localmente, ma non è più collegata e non riceverà aggiornamenti."
},
"instance.shared-instance.unavailable.locked-text": {
"message": "L'istanza condivisa è stata bloccata dal team di moderazione dei contenuti. Non riceverà più aggiornamenti dall'istanza primara e non potrà essere avviata."
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Istanza bloccata"
},
"instance.shared-instance.unavailable.manager-fallback": {
"message": "il gestore dell'istanza"
},
"instance.shared-instance.unavailable.text": {
"message": "L'istanza locale è ancora disponibile, ma non è più collegata e non riceverà aggiornamenti."
},
"instance.shared-instance.unavailable.title": {
"message": "Istanza condivisa non più disponibile"
},
"instance.worlds.a_minecraft_server": {
"message": "Un server Minecraft"
},
@@ -1061,6 +1469,63 @@
"minecraft-account.sign-in": {
"message": "Accedi a Minecraft"
},
"minecraft-required.description": {
"message": "Devi disporre di una copia di Minecraft da un account Microsoft prima di poter giocare."
},
"minecraft-required.description-header": {
"message": "Accedi a un account Microsoft"
},
"minecraft-required.dont-have-account": {
"message": "Non hai un account?"
},
"minecraft-required.get-minecraft": {
"message": "Ottieni Minecraft"
},
"minecraft-required.get-support": {
"message": "Assistenza"
},
"minecraft-required.header": {
"message": "Minecraft richiesto"
},
"minecraft-required.sign-in": {
"message": "Accedi a Microsoft"
},
"modal.modrinth-account-required.browser-description": {
"message": "Si è aperta una nuova scheda per effettuare l'accesso. Poi torna all'app."
},
"modal.modrinth-account-required.cancel-button": {
"message": "Annulla"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "Continua nel browser"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Crea un account"
},
"modal.modrinth-account-required.description": {
"message": "Devi aver effettuato l'accesso a Modrinth per usare questa funzione."
},
"modal.modrinth-account-required.header": {
"message": "Account richiesto"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "Apri di nuovo il browser"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Accedi a Modrinth"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Accedi a un account Modrinth"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "Accesso"
},
"modal.modrinth-account-required.support-prompt": {
"message": "Hai problemi ad accedere? Scrivi al <support>supporto</support>"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "Aspettando la conferma del browser..."
},
"search.filter.locked.instance": {
"message": "Determinato dall'istanza"
},
+645 -9
View File
@@ -50,6 +50,42 @@
"app.action-bar.install.summary.download-failed": {
"message": "ダウンロードを完了することができませんでした"
},
"app.action-bar.install.summary.instance-not-found": {
"message": "インスタンスが見つかりませんでした"
},
"app.action-bar.install.summary.invalid-file-path": {
"message": "ファイルパスが無効です"
},
"app.action-bar.install.summary.invalid-modpack": {
"message": "Modパックのデータが無効です"
},
"app.action-bar.install.summary.invalid-modpack-files": {
"message": "Modパックのファイルのメタデータに不正な部分があります"
},
"app.action-bar.install.summary.java-setup-failed": {
"message": "Javaのセットアップに失敗しました"
},
"app.action-bar.install.summary.loader-setup-failed": {
"message": "ローダーのセットアップに失敗しました"
},
"app.action-bar.install.summary.local-data-error": {
"message": "ローカルデータを更新できませんでした"
},
"app.action-bar.install.summary.minecraft-setup-failed": {
"message": "マインクラフトのセットアップに失敗しました"
},
"app.action-bar.install.summary.modrinth-unreachable": {
"message": "Modrinthに接続できませんでした"
},
"app.action-bar.install.summary.no-write-permission": {
"message": "書き込み権限がありません"
},
"app.action-bar.install.summary.pack-download-failed": {
"message": "パックをダウンロードできませんでした"
},
"app.action-bar.install.summary.unexpected-error": {
"message": "問題が発生しました"
},
"app.action-bar.install.unknown-instance": {
"message": "不明なインスタンス"
},
@@ -68,6 +104,9 @@
"app.action-bar.primary-instance": {
"message": "デフォルトのインスタンス"
},
"app.action-bar.reload-to-update": {
"message": "再読み込みしてアップデート"
},
"app.action-bar.show-more-running-instances": {
"message": "実行中のインスタンスをさらに表示"
},
@@ -86,6 +125,21 @@
"app.action-bar.view-logs": {
"message": "ログを表示"
},
"app.ads-consent.accept": {
"message": "すべて受け入れる"
},
"app.ads-consent.body": {
"message": "Modrinthは広告収益によって運営され、クリエイターへの報酬も広告収益から支払われています。当社のパートナーは、広告のパーソナライズや効果測定のために、アプリ内でCookieを保存・利用する場合があります。"
},
"app.ads-consent.manage": {
"message": "設定の管理"
},
"app.ads-consent.reject": {
"message": "すべて拒否する"
},
"app.ads-consent.title": {
"message": "プライバシーと広告がModrinthを支える仕組み"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "ぼかし効果などの高度なレンダリングを有効にします。グラフィックアクセラレーションが使用できない場合は、パフォーマンスが低下する可能性があります。"
},
@@ -99,7 +153,7 @@
"message": "テーマ"
},
"app.appearance-settings.default-landing-page.description": {
"message": "ランチャー起動時に表示するページを変更します"
"message": "ランチャー起動時に表示するページを変更します"
},
"app.appearance-settings.default-landing-page.home": {
"message": "ホーム"
@@ -129,20 +183,23 @@
"message": "ランチャーを最小化"
},
"app.appearance-settings.native-decorations.description": {
"message": "OSデフォルトのウィンドウUIを使用します(アプリの再起動が必要です)"
"message": "OsのウィンドウUIを使用します(アプリの再起動が必要です)"
},
"app.appearance-settings.native-decorations.title": {
"message": "OSのウィンドウUIを使用"
},
"app.appearance-settings.select-option": {
"message": "オプションを選択してください"
},
"app.appearance-settings.show-play-time.description": {
"message": "インスタンスのプレイ時間を表示します。"
},
"app.appearance-settings.show-play-time.title": {
"message": "プレイ時間を表示"
},
"app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Modパックの重複インストール、通常のコンテンツ削除、一括更新、Modパックのリンク解除、修復の促しなど、リスクの低い確認画面は自動的にスキップされます。危険な警告は引き続き表示されます。"
},
"app.appearance-settings.skip-non-essential-warnings.title": {
"message": "重要でない警告をスキップ"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "サイドバーの表示/非表示を切り替える機能を有効にします。"
},
@@ -161,6 +218,12 @@
"app.auth-servers.unreachable.header": {
"message": "認証サーバーにアクセスできません"
},
"app.browse.add-servers-to-instance": {
"message": "インスタンスにサーバーを追加中"
},
"app.browse.add-to-an-instance": {
"message": "インスタンスに追加"
},
"app.browse.add-to-instance": {
"message": "インスタンスに追加"
},
@@ -173,18 +236,27 @@
"app.browse.already-added": {
"message": "すでに追加済み"
},
"app.browse.discover-content": {
"message": "コンテンツを探す"
"app.browse.back-to-instance": {
"message": "インスタンスに戻る"
},
"app.browse.discover-servers": {
"message": "サーバーを探す"
},
"app.browse.hide-added-servers": {
"message": "既に追加済みのサーバーを非表示にする"
},
"app.browse.project-type.modpacks": {
"message": "Modパック"
},
"app.browse.server-instance-content-warning": {
"message": "コンテンツを追加すると、サーバーに参加する際に互換性が失われる可能性があります。また、追加されたコンテンツはインスタンスのコンテンツをアップデートした際に失われます。"
},
"app.browse.server.installing": {
"message": "インストール中"
},
"app.content-install.no-compatible-versions": {
"message": "{compatibilityLabel} に一致する利用可能なバージョンがありません。このままインストールしますか?依存関係は自動的にインストールされません。"
},
"app.export-modal.description-placeholder": {
"message": "Modパックの説明を入力…"
},
@@ -194,6 +266,9 @@
"app.export-modal.header": {
"message": "Modパックを書き出す"
},
"app.export-modal.modpack-name-label": {
"message": "Modパック名"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Modパック名"
},
@@ -203,9 +278,78 @@
"app.export-modal.version-number-placeholder": {
"message": "1.0.0"
},
"app.install.phase.downloading_content": {
"message": "コンテンツをダウンロード中"
},
"app.install.phase.downloading_minecraft": {
"message": "マインクラフトをダウンロード中"
},
"app.install.phase.downloading_pack_file": {
"message": "パックファイルをダウンロード中"
},
"app.install.phase.extracting_overrides": {
"message": "追加ファイルを展開中"
},
"app.install.phase.finalizing": {
"message": "最終処理中"
},
"app.install.phase.preparing_instance": {
"message": "インストール待ち"
},
"app.install.phase.preparing_java": {
"message": "Javaを準備中"
},
"app.install.phase.preparing_java.downloading": {
"message": "Java {version} をダウンロードしています"
},
"app.install.phase.preparing_java.extracting": {
"message": "Java {version} を展開中"
},
"app.install.phase.preparing_java.fetching-metadata": {
"message": "Java {version} を取得中"
},
"app.install.phase.preparing_java.resolving": {
"message": "Java {version} を準備中"
},
"app.install.phase.preparing_java.validating": {
"message": "Java {version} を検証中"
},
"app.install.phase.reading_pack_manifest": {
"message": "パックのマニフェストを読み込み中"
},
"app.install.phase.resolving_loader": {
"message": "ローダーを解析中"
},
"app.install.phase.resolving_minecraft": {
"message": "マインクラフトを解析中"
},
"app.install.phase.resolving_pack": {
"message": "コンテンツを解析中"
},
"app.install.phase.rolling_back": {
"message": "変更を取り消し中"
},
"app.install.phase.running_loader_processors": {
"message": "ローダーのプロセッサーを実行中"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "追加済"
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "変更はまだ共有されていません"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "アップデートを公開する"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "公開しています…"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "アップデートを公開する"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "審査中…"
},
"app.instance.confirm-delete.admonition-body": {
"message": "インスタンス内のすべてのデータは、ワールド、設定、インストール済みのコンテンツを含め、完全に削除されます。"
},
@@ -219,7 +363,7 @@
"message": "インスタンスを削除"
},
"app.instance.modpack-already-installed.body": {
"message": "このModpackは既に <bold>{instanceName}</bold> インスタンスにインストール済みです。本当に複製しますか?"
"message": "このModパックは既に <bold>{instanceName}</bold> インスタンスにインストール済みです。本当に複製しますか?"
},
"app.instance.modpack-already-installed.create": {
"message": "新規作成"
@@ -230,9 +374,21 @@
"app.instance.modpack-already-installed.instance": {
"message": "インスタンス"
},
"app.instance.mods.bulk-update.downloading-projects": {
"message": "プロジェクトをダウンロード中 {current, number}/{total, number}…"
},
"app.instance.mods.bulk-update.finishing": {
"message": "アップデートを完了中…"
},
"app.instance.mods.bulk-update.resolving-versions": {
"message": "バージョンを解析中…"
},
"app.instance.mods.content-type-project": {
"message": "プロジェクト"
},
"app.instance.mods.locked-content": {
"message": "ロックされた起動構成の内にあるコンテンツは変更できません。"
},
"app.instance.mods.project-was-added": {
"message": "「{name}」が追加されました"
},
@@ -248,12 +404,105 @@
"app.instance.mods.successfully-uploaded": {
"message": "アップロードに成功しました"
},
"app.instance.share.empty.description": {
"message": "フレンドとこのインスタンスをシェアしましょう!"
},
"app.instance.share.empty.heading": {
"message": "まだ誰も招待していないようです"
},
"app.instance.share.empty.invite-friends-button": {
"message": "フレンドを招待"
},
"app.instance.share.invite-modal.heading": {
"message": "{name} をシェアする"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "このページにアクセスするには"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "としてサインインする必要があります。"
},
"app.instance.share.locked.linked-account-fallback": {
"message": "リンクされたアカウント"
},
"app.instance.share.locked.signed-out-heading": {
"message": "サインインしていません"
},
"app.instance.share.locked.switch-account-button": {
"message": "アカウントを切り替える"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "アカウントが違います"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "このインスタンスのアップデートは受信できなくなります"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "後でもう一度招待することもできます"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "彼らがすでにインストールしたコピーは彼らのデバイスに残ります"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "この変更で何が起きるか?"
},
"app.instance.share.remove-user-modal.header": {
"message": "アクセスを取り消す"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "アクセスを取り消す"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "{username}のアバター"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "もし{username}のアクセス権を取り消した場合、彼らが再びアップデートを受け取れるようにするには、もう一度招待し直す必要があります。"
},
"app.instance.share.sign-in.button": {
"message": "サインイン"
},
"app.instance.share.unlink.body": {
"message": "この起動構成を共有するには、このmodpackのリンクを解除する必要があります"
},
"app.instance.share.unlink.header": {
"message": "共有にはリンクを解除する必要があります"
},
"app.instance.shared-instance-already-installed.body": {
"message": "この共有された起動構成はすでに<bold> {instanceName} </bold>としてインストールされています。別のコピーをインストールしてもよろしいですか?"
},
"app.instance.shared-instance-already-installed.header": {
"message": "共有された起動構成はすでにインストールされています"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "インストールを続行"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "起動構成"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "リンクされたアカウント"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Modrinthへのサインインが必要です"
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "Modrinthのアカウントが間違っています"
},
"app.instance.worlds.add-server": {
"message": "サーバーを追加"
},
"app.instance.worlds.browse-servers": {
"message": "サーバーを探す"
},
"app.instance.worlds.delete-world-modal.delete-button": {
"message": "ワールドを削除"
},
"app.instance.worlds.delete-world-modal.title": {
"message": "ワールドの削除"
},
"app.instance.worlds.delete-world-modal.warning-body": {
"message": "このワールドはインスタンスから完全に削除されます。この操作は取り消せません。"
},
"app.instance.worlds.delete-world-modal.warning-header": {
"message": "{name} を削除します"
},
@@ -275,39 +524,159 @@
"app.instance.worlds.no-worlds-heading": {
"message": "サーバー、ワールドは追加されていません"
},
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "サーバーを削除"
},
"app.instance.worlds.remove-server-modal.title": {
"message": "サーバーの削除"
},
"app.instance.worlds.remove-server-modal.warning-body": {
"message": "このサーバーはサーバーリストおよびゲーム内のサーバーリストから削除されます。アドレスが分かれば、後からいつでも再追加できます。"
},
"app.instance.worlds.remove-server-modal.warning-header": {
"message": "{name} を削除します"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "{count} 個のワールドを検索…"
},
"app.modal.install-to-play.additional-context": {
"message": "追加のコンテンツ"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "可能であれば、リンクや画像も含めてください"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "起動構成を報告する"
},
"app.modal.install-to-play.delete-instance": {
"message": "インスタンスを削除する"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "インストールしない"
},
"app.modal.install-to-play.header": {
"message": "インストールしてプレイ"
},
"app.modal.install-to-play.install-anyway": {
"message": "インストールを続行"
},
"app.modal.install-to-play.install-button": {
"message": "インストール"
},
"app.modal.install-to-play.invite-warning": {
"message": "この招待は他のModrinthユーザーによって作成されたものであり、Modrinth公式のものではありません。信頼できる人からの招待のみ承諾してください。"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {#個のMod}}"
},
"app.modal.install-to-play.report-description": {
"message": "このフォームを使用して、<rules-link>ルール</rules-link>または<terms-link>利用規約</terms-link>に違反する可能性のある事例を報告してください。"
},
"app.modal.install-to-play.report-image-invalid-type": {
"message": "対応していない画像形式です"
},
"app.modal.install-to-play.report-image-too-large": {
"message": "ファイルのサイズが 1 MiBの制限を超えています"
},
"app.modal.install-to-play.report-reason": {
"message": "この起動構成はどの規則に違反していますか?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "不適切なコンテンツ"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "悪意がある"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "スパム"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "共有された起動構成を通報する"
},
"app.modal.install-to-play.report-submitted": {
"message": "通報は送信されました"
},
"app.modal.install-to-play.report-support-and-bugs": {
"message": "サポートに関するお問い合わせは、<support-link>サポートチーム</support-link>までご連絡ください。バグの報告は、<github-link>GitHubのIssue</github-link>を作成してください。"
},
"app.modal.install-to-play.reviewed-files": {
"message": "ファイル形式(.mrpackを含む)に関わらず、Modrinthに公開されているファイルのみが審査の対象となります。"
},
"app.modal.install-to-play.shared-instance": {
"message": "共有インスタンス"
},
"app.modal.install-to-play.shared-instance-content": {
"message": "共有された起動構成の内容"
},
"app.modal.install-to-play.shared-instance-unknown-files-description": {
"message": "この共有された起動構成には、Modrinthで公開されていないファイルが含まれています。信頼できる提供元からのファイルのみをインストールすることを強くお勧めします。"
},
"app.modal.install-to-play.unknown-files-description": {
"message": "このサーバーModパックには、Modrinthに公開されていないファイルが含まれています。信頼できるソースからのファイルのみをインストールすることを強く推奨します。"
},
"app.modal.install-to-play.unknown-files-warning": {
"message": "不明なファイルの警告"
},
"app.modal.install-to-play.unrecognized-files": {
"message": "認識されないファイル"
},
"app.modal.install-to-play.view-contents": {
"message": "コンテンツを見る"
},
"app.modal.update-to-play.header": {
"message": "更新してプレイ"
},
"app.modal.update-to-play.removed": {
"message": "削除されました"
},
"app.modal.update-to-play.server-modpack-unknown-files-description": {
"message": "このサーバーModパックのアップデートには、Modrinthに公開されていないファイルが含まれています。信頼できるソースからのファイルのみをインストールすることを強く推奨します。"
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "追加済み"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "削除済み"
},
"app.modal.update-to-play.shared-instance-unknown-files-description": {
"message": "この共有された起動構成のアップデートには、Modrinthで公開されていないファイルが含まれています。信頼できる提供元からのファイルのみをインストールすることを強くお勧めします。"
},
"app.modal.update-to-play.update-required": {
"message": "更新が必要です"
},
"app.modal.update-to-play.update-required-description": {
"message": "{name}をプレイするには更新が必要です。ゲームを起動するには最新版に更新してください。"
"message": "{name}をプレイするにはアップデートが必要です。ゲームを起動するには最新バージョンにアップデートしてください。"
},
"app.project.install-button.already-installed": {
"message": "このプロジェクトは既にインストールされています"
},
"app.project.install-button.switch-version": {
"message": "バージョンを切り替える"
},
"app.project.install-context.back-to-browse": {
"message": "ディスカバーに戻る"
},
"app.project.version.all-versions": {
"message": "すべてのバージョン"
},
"app.project.version.download-in-browser": {
"message": "ブラウザでダウンロード"
},
"app.project.version.installing": {
"message": "インストール中"
},
"app.project.versions.already-installed": {
"message": "既にインストールされています"
},
"app.settings.developer-mode-enabled": {
"message": "開発者モードがオンになっています。"
},
"app.settings.downloading": {
"message": "v{version}をダウンロード中"
},
"app.settings.privacy.ads-consent.intro": {
"message": "Modrinthは広告収益によって運営され、クリエイターへの報酬も広告収益から支払われています。当社のパートナーは、広告のパーソナライズや効果測定のために、アプリ内でCookieを保存・利用する場合があります。この機能は下のボタンから個別に拒否や許可または変更することができます。"
},
"app.settings.tabs.appearance": {
"message": "表示設定"
},
@@ -326,6 +695,45 @@
"app.settings.tabs.resource-management": {
"message": "リソース管理"
},
"app.skins.add-button": {
"message": "スキンを追加"
},
"app.skins.add-button.drag-and-drop": {
"message": "ドラッグ&ドロップ"
},
"app.skins.apply-button": {
"message": "適用"
},
"app.skins.delete-button": {
"message": "スキンを削除"
},
"app.skins.delete-modal.description": {
"message": "選択したスキンは永久に削除されます。この操作は取り消せません。"
},
"app.skins.delete-modal.title": {
"message": "本当にこのスキンを削除しますか?"
},
"app.skins.dropped-file-error.text": {
"message": "ドロップされたファイルの読み込みに失敗しました。"
},
"app.skins.dropped-file-error.title": {
"message": "ファイルの処理中にエラーが発生しました"
},
"app.skins.ears-feature-notice": {
"message": "このスキンに{ears} modの機能を使用する"
},
"app.skins.edit-button": {
"message": "スキンを編集"
},
"app.skins.modal.add-skin-button": {
"message": "スキンを追加"
},
"app.skins.modal.add-title": {
"message": "スキンを追加中"
},
"app.skins.modal.arm-style-section": {
"message": "腕のスタイル"
},
"app.skins.modal.arm-style-slim": {
"message": "スリム"
},
@@ -338,6 +746,12 @@
"app.skins.modal.cape-section": {
"message": "マント"
},
"app.skins.modal.edit-title": {
"message": "スキンを編集中"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "まずスキンを編集してください!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "マントなし"
},
@@ -356,15 +770,39 @@
"app.skins.modal.texture-section": {
"message": "テクスチャ"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "まずスキンをアップロードしてください!"
},
"app.skins.preview.edit-button": {
"message": "編集"
},
"app.skins.previewing-badge": {
"message": "プレビュー中"
},
"app.skins.rate-limit.text": {
"message": "スキンの変更頻度が高すぎます。Mojangのサーバーが一時的にリクエストをブロックしました。しばらく待ってからもう一度お試しください。"
},
"app.skins.rate-limit.title": {
"message": "落ち着いてください!"
},
"app.skins.reorder-error.text": {
"message": "スキンの順序を保存できませんでした。"
},
"app.skins.reorder-error.title": {
"message": "スキンの並び替えに失敗しました"
},
"app.skins.section.builders-and-biomes": {
"message": "ビルダーズ&バイオーム"
},
"app.skins.section.chaos-cubed": {
"message": "Chaos Cubed"
},
"app.skins.section.chase-the-skies": {
"message": "チェイス・ザ・スカイ"
},
"app.skins.section.default-skins": {
"message": "デフォルトのスキンたち"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
@@ -374,6 +812,9 @@
"app.skins.section.modrinth-pride": {
"message": "Modrinth プライド"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "これらのスキンは、プライド月間中にModrinthのプライド募金活動に寄付したことで獲得したものです。"
},
"app.skins.section.mounts-of-mayhem": {
"message": "マウント・オブ・メイヘム"
},
@@ -392,9 +833,27 @@
"app.skins.section.tiny-takeover": {
"message": "タイニー・テイクオーバー"
},
"app.skins.sign-in.button": {
"message": "サイン審"
},
"app.skins.sign-in.description": {
"message": "Modrinthアプリのスキン管理機能を使用するには、マインクラフトアカウントにサインインしてください。"
},
"app.skins.sign-in.rinthbot-alt": {
"message": "ワクワクしている Modrinth ボット"
},
"app.skins.sign-in.title": {
"message": "サインインしてください"
},
"app.skins.title": {
"message": "スキンセレクター"
},
"app.skins.toggle-ears-features-off": {
"message": "オフに切り替え"
},
"app.skins.toggle-ears-features-on": {
"message": "オンに切り替え"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} のダウンロードが完了しました。今すぐ更新するには再読み込みするか、Modrinth Appを閉じる際に自動更新されます。"
},
@@ -413,6 +872,9 @@
"app.update-popup.download-complete": {
"message": "ダウンロード完了"
},
"app.update-popup.reload": {
"message": "再度読み込みして更新"
},
"app.update-popup.title": {
"message": "アップデートが利用可能です"
},
@@ -440,6 +902,36 @@
"app.world.world-item.players-online": {
"message": "{count} 人がオンライン"
},
"content.shared-instance.change-version-body": {
"message": "バージョンの変更はローカルコピーにのみ影響します。今後共有された起動構成がアップデートされた際に、元の状態に戻ったり、別のバージョンに変更されたりする場合があります。"
},
"content.shared-instance.delete-bulk-body": {
"message": "選択された一部のプロジェクトは共有された起動構成の一部です。これらを削除してもローカルコピーのみが変更され、今後共有された起動構成がアップデートされた際に、再び復元されたり変更されたりする場合があります。"
},
"content.shared-instance.delete-button": {
"message": "とにかく削除する"
},
"content.shared-instance.delete-many-button": {
"message": "それでも {count, number} のプロジェクトを削除する"
},
"content.shared-instance.delete-single-body": {
"message": "削除はローカルコピーにのみ影響します。今後共有された起動構成がアップデートされた際に、元の状態に戻ったり、別のバージョンに変更されたりする場合があります。"
},
"content.shared-instance.disable-bulk-body": {
"message": "選択された一部のプロジェクトは共有された起動構成の一部です。これらを無効化してもローカルコピーのみが変更され、今後共有された起動構成がアップデートされた際に、再び有効化されたり、復元されたり、変更されたりする場合があります。"
},
"content.shared-instance.disable-button": {
"message": "とにかく無効化する"
},
"content.shared-instance.disable-many-button": {
"message": "それでも {count, number} のプロジェクトを無効化する"
},
"content.shared-instance.disable-single-body": {
"message": "無効化はローカルコピーにのみ影響します。今後共有された起動構成がアップデートされた際に、再び有効化されたり、元の状態に戻ったり、変更されたりする場合があります。"
},
"content.shared-instance.warning-header": {
"message": "これは共有された起動構成の一部です"
},
"friends.action.add-friend": {
"message": "フレンドを追加"
},
@@ -503,6 +995,48 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Modrinthアカウントにサインイン</link>して友達を追加し、彼らがプレイしているゲームをチェックしよう!"
},
"installation-settings.shared-instance.linked-title": {
"message": "リンク済の共有された起動構成"
},
"installation-settings.shared-instance.title": {
"message": "起動構成を非公開化"
},
"installation-settings.shared-instance.unlink-button": {
"message": "起動構成の共有を解除"
},
"installation-settings.shared-instance.unlinking-button": {
"message": "共有を解除中…"
},
"installation-settings.shared-instance.unpublish-button": {
"message": "共有された軌道構成を非公開にする"
},
"instance.action.create-shortcut": {
"message": "ショートカット作成"
},
"instance.action.export-modpack": {
"message": "Modパックとしてエクスポート"
},
"instance.action.launch-instance": {
"message": "インスタンスを起動"
},
"instance.action.more-actions": {
"message": "その他"
},
"instance.action.open-folder": {
"message": "フォルダーを開く"
},
"instance.action.repair": {
"message": "修復"
},
"instance.action.settings": {
"message": "インスタンスの設定"
},
"instance.action.starting": {
"message": "起動中…"
},
"instance.action.stopping": {
"message": "停止中…"
},
"instance.add-server.add-and-play": {
"message": "追加してプレイ"
},
@@ -545,6 +1079,9 @@
"instance.files.save-as": {
"message": "名前をつけて保存…"
},
"instance.playtime.never-played": {
"message": "未プレイ"
},
"instance.server-modal.address": {
"message": "アドレス"
},
@@ -611,12 +1148,30 @@
"instance.settings.tabs.general.name": {
"message": "名前"
},
"instance.settings.tabs.general.update-channel": {
"message": "アップデートチャンネル"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "アルファ"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "リリース、ベータ、およびアルファバージョンが利用可能なアップデートとして表示されます。"
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "ベータ"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "リリースおよびベータバージョンが利用可能なアップデートとして表示されます。"
},
"instance.settings.tabs.general.update-channel.release": {
"message": "リリース"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "リリースバージョンのみが利用可能なアップデートとして表示されます。"
},
"instance.settings.tabs.general.update-channel.select": {
"message": "アップデートチャンネルを選択"
},
"instance.settings.tabs.hooks": {
"message": "起動フック"
},
@@ -665,6 +1220,24 @@
"instance.settings.tabs.java": {
"message": "Javaとメモリ"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "カスタム環境変数"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "カスタムJava引数"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "カスタムJavaインストール"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "カスタムメモリ割り当て"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "環境変数を入力…"
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Java引数を入力…"
},
"instance.settings.tabs.java.environment-variables": {
"message": "環境変数"
},
@@ -713,6 +1286,9 @@
"instance.settings.tabs.window.width.enter": {
"message": "幅を入力…"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "アップデートを配信する"
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraftサーバー"
},
@@ -722,6 +1298,9 @@
"instance.worlds.copy_address": {
"message": "アドレスをコピー"
},
"instance.worlds.create_shortcut": {
"message": "ショートカット作成"
},
"instance.worlds.dont_show_on_home": {
"message": "ホームに表示しない"
},
@@ -773,6 +1352,63 @@
"minecraft-account.sign-in": {
"message": "Minecraftにサインイン"
},
"minecraft-required.description": {
"message": "Minecraft を起動してプレイするには、Minecraft を所有している Microsoft アカウントが必要です。"
},
"minecraft-required.description-header": {
"message": "Microsoft アカウントにサインインしてください"
},
"minecraft-required.dont-have-account": {
"message": "まだアカウントを持っていませんか?"
},
"minecraft-required.get-minecraft": {
"message": "Minecraft を入手する"
},
"minecraft-required.get-support": {
"message": "サポートを受ける"
},
"minecraft-required.header": {
"message": "Minecraft が必要です"
},
"minecraft-required.sign-in": {
"message": "Microsoft にサインイン"
},
"modal.modrinth-account-required.browser-description": {
"message": "サインイン用のタブが開きました。そちらでサインインを完了してから、アプリに戻ってください。"
},
"modal.modrinth-account-required.cancel-button": {
"message": "キャンセル"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "ブラウザで続行"
},
"modal.modrinth-account-required.create-account-button": {
"message": "アカウントを作成"
},
"modal.modrinth-account-required.description": {
"message": "この機能を利用するには、Modrinthアカウントにログインする必要があります。"
},
"modal.modrinth-account-required.header": {
"message": "アカウントが必要です"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "もう一度ブラウザを開く"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Modrinthへサインイン"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Modrinth アカウントにサインインしてください"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "サインイン"
},
"modal.modrinth-account-required.support-prompt": {
"message": "サインインでお困りですか?<support>サポート</support>"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "ブラウザからの確認を待っています…"
},
"search.filter.locked.instance": {
"message": "インスタンスによる条件"
},
+362 -11
View File
@@ -11,6 +11,12 @@
"app.action-bar.hide-more-running-instances": {
"message": "실행 중인 인스턴스 숨기기"
},
"app.action-bar.install.copied-details": {
"message": "복사됨"
},
"app.action-bar.install.copy-details": {
"message": "세부 정보 복사"
},
"app.action-bar.install.dismiss": {
"message": "무시"
},
@@ -20,9 +26,72 @@
"app.action-bar.install.retry": {
"message": "재시도"
},
"app.action-bar.install.summary.app-closing": {
"message": "앱이 종료되어 취소되었습니다"
},
"app.action-bar.install.summary.bad-modpack-file": {
"message": "모드팩을 읽을 수 없음"
},
"app.action-bar.install.summary.canceled": {
"message": "취소됨"
},
"app.action-bar.install.summary.cleanup-incomplete": {
"message": "파일 정리가 완료되지 않았습니다"
},
"app.action-bar.install.summary.content-download-failed": {
"message": "파일을 다운로드할 수 없습니다"
},
"app.action-bar.install.summary.corrupt-download": {
"message": "다운로드한 파일이 손상되었습니다"
},
"app.action-bar.install.summary.could-not-save-files": {
"message": "파일을 저장할 수 없습니다"
},
"app.action-bar.install.summary.download-failed": {
"message": "다운로드를 완료할 수 없습니다"
},
"app.action-bar.install.summary.instance-not-found": {
"message": "인스턴스를 찾을 수 없습니다"
},
"app.action-bar.install.summary.invalid-file-path": {
"message": "파일 경로가 올바르지 않습니다"
},
"app.action-bar.install.summary.invalid-modpack": {
"message": "모드팩 데이터가 올바르지 않습니다"
},
"app.action-bar.install.summary.invalid-modpack-files": {
"message": "모드팩 파일이 올바르지 않은 메타데이터를 가지고 있습니다"
},
"app.action-bar.install.summary.java-setup-failed": {
"message": "Java 설치를 완료할 수 없습니다"
},
"app.action-bar.install.summary.loader-setup-failed": {
"message": "로더 설치에 실패했습니다"
},
"app.action-bar.install.summary.local-data-error": {
"message": "로컬 데이터를 업데이트할 수 없습니다"
},
"app.action-bar.install.summary.minecraft-setup-failed": {
"message": "Minecraft 설치에 실패했습니다"
},
"app.action-bar.install.summary.modrinth-unreachable": {
"message": "Modrinth에 연결할 수 없습니다"
},
"app.action-bar.install.summary.no-write-permission": {
"message": "쓰기 권한이 없습니다"
},
"app.action-bar.install.summary.pack-download-failed": {
"message": "팩을 다운로드할 수 없습니다"
},
"app.action-bar.install.summary.unexpected-error": {
"message": "무언가 잘못되었습니다"
},
"app.action-bar.install.unknown-instance": {
"message": "알 수 없는 인스턴스"
},
"app.action-bar.install.updating-shared-content": {
"message": "공유 콘텐츠 업데이트 중"
},
"app.action-bar.installs": {
"message": "설치"
},
@@ -59,6 +128,21 @@
"app.action-bar.view-logs": {
"message": "로그 보기"
},
"app.ads-consent.accept": {
"message": "모두 수락"
},
"app.ads-consent.body": {
"message": "광고는 Modrinth가 운영될 수 있도록 하며, 크리에이터에게 지급되는 보상을 지원합니다. 우리의 파트너는 앱에서 광고를 개인화하고 성과를 측정하기 위해 쿠키를 저장하거나 접근할 수 있습니다."
},
"app.ads-consent.manage": {
"message": "환경 설정 관리"
},
"app.ads-consent.reject": {
"message": "모두 거절"
},
"app.ads-consent.title": {
"message": "개인정보와 광고가 Modrinth를 지원하는 방식"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "블러 효과와 같은 고급 렌더링 기능을 활성화합니다. 하드웨어 가속 없이는 성능 저하가 발생할 수 있습니다."
},
@@ -107,9 +191,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "네이티브 제목 표시줄"
},
"app.appearance-settings.select-option": {
"message": "옵션 선택"
},
"app.appearance-settings.show-play-time.description": {
"message": "인스턴스의 플레이 시간을 표시합니다."
},
@@ -161,9 +242,6 @@
"app.browse.back-to-instance": {
"message": "인스턴스로 돌아가기"
},
"app.browse.discover-content": {
"message": "콘텐츠 탐색하기"
},
"app.browse.discover-servers": {
"message": "서버 탐색하기"
},
@@ -218,6 +296,9 @@
"app.install.phase.finalizing": {
"message": "마무리 중"
},
"app.install.phase.preparing_instance": {
"message": "설치 대기열에 추가됨"
},
"app.install.phase.preparing_java": {
"message": "Java 준비 중"
},
@@ -254,6 +335,36 @@
"app.install.phase.running_loader_processors": {
"message": "로더 진행기 실행 중"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "추가됨"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "현재 로컬 인스턴스가 콘텐츠를 공유한 다른 사용자들의 인스턴스보다 최신 상태입니다."
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "변경 사항이 아직 공유되지 않았습니다"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "업데이트 게시"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "게시중..."
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "삭제중"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "Push 업데이트"
},
"app.instance.admonitions.shared-instance.review-description": {
"message": "이 인스턴스를 사용하는 모든 사람과 공유될 콘텐츠 변경 사항을 검토하세요."
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Review changes"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "검토중..."
},
"app.instance.confirm-delete.admonition-body": {
"message": "인스턴스의 모든 데이터가 삭제됩니다. 세계 폴더, 설정 폴더 등 설치된 모든 컨텐츠가 포함됩니다."
},
@@ -290,6 +401,9 @@
"app.instance.mods.content-type-project": {
"message": "프로젝트"
},
"app.instance.mods.locked-content": {
"message": "잠긴 인스턴스의 콘텐츠는 변경할 수 없습니다."
},
"app.instance.mods.project-was-added": {
"message": "\"{name}\" 추가됨"
},
@@ -305,6 +419,102 @@
"app.instance.mods.successfully-uploaded": {
"message": "업로드 성공"
},
"app.instance.share.empty.description": {
"message": "친구들과 이 인스턴스를 공유할 수 있습니다!"
},
"app.instance.share.empty.heading": {
"message": "아직 초대된 친구가 없습니다"
},
"app.instance.share.empty.invite-friends-button": {
"message": "친구 초대"
},
"app.instance.share.invite-modal.heading": {
"message": "{name} 공유"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "이 페이지에 접근하려면"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "계정으로 로그인해야 합니다."
},
"app.instance.share.locked.linked-account-fallback": {
"message": "연동된"
},
"app.instance.share.locked.signed-out-heading": {
"message": "로그인되지 않음"
},
"app.instance.share.locked.switch-account-button": {
"message": "계정 전환"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "잘못된 계정"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "이들은 더 이상 해당 공유 인스턴스의 업데이트를 받지 않습니다"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "이미 설치한 사본은 해당 기기에 그대로 남아 있습니다"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "나중에 다시 초대할 수 있습니다"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "마지막 사용자이며, 이 인스턴스의 공유가 비활성화됩니다"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "어떻게 되나요?"
},
"app.instance.share.remove-user-modal.header": {
"message": "액세스 권한 취소"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "액세스 권한 취소"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "{username}의 계정"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "{username} 님의 이 공유 인스턴스 액세스 권한을 취소하면, 해당 사용자가 업데이트를 받기 전에 다시 초대해야 합니다."
},
"app.instance.share.sign-in.button": {
"message": "로그인"
},
"app.instance.share.unlink.body": {
"message": "인스턴스를 공유하려면 이 모드팩 연동을 해제해야 합니다"
},
"app.instance.share.unlink.header": {
"message": "공유를 진행하려면 연동 해제가 필요합니다"
},
"app.instance.shared-instance-already-installed.body": {
"message": "이 공유 인스턴스는 이미 <bold>{instanceName}</bold> 이름으로 설치되어 있습니다. 사본을 하나 더 설치하시겠습니까?"
},
"app.instance.shared-instance-already-installed.header": {
"message": "공유 인스턴스가 이미 설치됨"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "그래도 설치"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "인스턴스"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "연동된 계정"
},
"app.instance.shared-instance-wrong-account.owner-admonition-body-v2": {
"message": "이 공유 인스턴스를 관리해야 합니다. 그렇지 않으면 사용자에게 업데이트를 푸시할 수 없습니다."
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "계정 로그인:"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Modrinth 로그인 필요"
},
"app.instance.shared-instance-wrong-account.user-admonition-body-v2": {
"message": "이 공유 인스턴스의 업데이트를 받으려면 로그인하세요."
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "잘못된 Modrinth 계정을 사용하고 있습니다"
},
"app.instance.worlds.add-server": {
"message": "서버 추가"
},
@@ -356,35 +566,83 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "{count}개의 세계 검색..."
},
"app.modal.install-to-play.additional-context": {
"message": "추가 정보"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "관련이 있는 경우 가능하면 링크 및 이미지를 포함하세요"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "신고 대상 인스턴스"
},
"app.modal.install-to-play.delete-instance": {
"message": "인스턴스 삭제"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "설치하지 않음"
},
"app.modal.install-to-play.header": {
"message": "설치하고 플레이"
},
"app.modal.install-to-play.install-anyway": {
"message": "무시하고 설치"
},
"app.modal.install-to-play.install-button": {
"message": "설치"
},
"app.modal.install-to-play.invite-warning": {
"message": "이 초대장은 다른 Modrinth 사용자가 생성했으며, Modrinth에서 생성하지 않았습니다. 신뢰할 수 있는 초대장만 수락하십시오."
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {모드 #개} other {모드 #개}}"
},
"app.modal.install-to-play.report-description": {
"message": "본 서식을 사용하여 당사의 <rules-link>규칙</rules-link> 또는 <terms-link>이용약관</terms-link> 위반 가능성이 있는 인스턴스를 신고하세요."
},
"app.modal.install-to-play.reviewed-files": {
"message": "모든 파일은 형식(.mrpack 포함)에 무관하게 Modrinth에 게시되어야만 검수를 거칩니다."
},
"app.modal.install-to-play.shared-instance": {
"message": "인스턴스 공유됨"
},
"app.modal.install-to-play.unknown-files-description": {
"message": "이 서버의 모드팩은 Modrinth에 게시되지 않은 파일을 포함하고 있습니다. 신뢰할 수 있는 출처의 파일만 설치하는 것을 강력히 권장합니다."
},
"app.modal.install-to-play.unknown-files-warning": {
"message": "출처를 알 수 없는 파일"
},
"app.modal.install-to-play.unrecognized-files": {
"message": "확인되지 않은 파일"
},
"app.modal.install-to-play.view-contents": {
"message": "구성 요소 보기"
},
"app.modal.update-to-play.header": {
"message": "업데이트하고 플레이"
},
"app.modal.update-to-play.update-required-description": {
"message": "{name}을(를) 플레이하려면 업데이트가 필요합니다. 게임을 실행하려면 최신 버전으로 업데이트해 주세요."
"app.modal.update-to-play.removed": {
"message": "제거됨"
},
"app.project.install-button.already-installed": {
"message": "이미 설치되어 있음"
},
"app.project.install-button.switch-version": {
"message": "버전 변경"
},
"app.project.install-context.back-to-browse": {
"message": "탐색으로 돌아가기"
},
"app.project.install-context.install-content-to-instance": {
"message": "인스턴스에 콘텐츠 설치"
"app.project.version.all-versions": {
"message": "모든 버전"
},
"app.project.version.download-in-browser": {
"message": "브라우저에서 다운로드"
},
"app.project.version.installing": {
"message": "설치 중"
},
"app.project.versions.already-installed": {
"message": "이미 설치됨"
},
"app.settings.developer-mode-enabled": {
"message": "개발자 모드가 활성화되었습니다."
@@ -392,6 +650,9 @@
"app.settings.downloading": {
"message": "v{version} 다운로드 중"
},
"app.settings.privacy.ads-consent.intro": {
"message": "광고는 Modrinth가 운영될 수 있도록 하며, 크리에이터에게 지급되는 보상을 지원합니다. 우리의 파트너는 앱에서 광고를 개인화하고 성과를 측정하기 위해 쿠키를 저장하거나 접근할 수 있습니다. 아래에서 거부하거나 환경 설정을 관리할 수 있습니다."
},
"app.settings.tabs.appearance": {
"message": "모양"
},
@@ -434,6 +695,9 @@
"app.skins.dropped-file-error.title": {
"message": "파일 처리 중 오류가 발생했습니다."
},
"app.skins.ears-feature-notice": {
"message": "이 스킨은 {ears} 모드의 기능을 사용합니다"
},
"app.skins.edit-button": {
"message": "스킨 편집"
},
@@ -560,6 +824,12 @@
"app.skins.title": {
"message": "스킨 선택"
},
"app.skins.toggle-ears-features-off": {
"message": "끄기"
},
"app.skins.toggle-ears-features-on": {
"message": "켜기"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} 다운로드가 완료되었습니다. 새로고침하거나 Modrinth App을 종료하면 자동으로 업데이트됩니다."
},
@@ -579,7 +849,7 @@
"message": "다운로드 완료"
},
"app.update-popup.reload": {
"message": "업데이트하려면 다시 로드"
"message": "새로고침하여 업데이트"
},
"app.update-popup.title": {
"message": "업데이트 가능"
@@ -671,6 +941,33 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Modrinth 계정으로 로그인</link>하여 친구를 추가하고 무엇을 플레이 중인지 확인하세요!"
},
"instance.action.create-shortcut": {
"message": "바로가기 만들기"
},
"instance.action.export-modpack": {
"message": "모드팩 내보내기"
},
"instance.action.launch-instance": {
"message": "인스턴스 실행"
},
"instance.action.more-actions": {
"message": "더보기"
},
"instance.action.open-folder": {
"message": "폴더 열기"
},
"instance.action.repair": {
"message": "복구하기"
},
"instance.action.settings": {
"message": "인스턴스 설정"
},
"instance.action.starting": {
"message": "시작중..."
},
"instance.action.stopping": {
"message": "정지중..."
},
"instance.add-server.add-and-play": {
"message": "추가 후 플레이"
},
@@ -713,6 +1010,9 @@
"instance.files.save-as": {
"message": "다른 이름으로 저장..."
},
"instance.playtime.never-played": {
"message": "아직 플레이 하지 않음"
},
"instance.server-modal.address": {
"message": "주소"
},
@@ -917,6 +1217,36 @@
"instance.settings.tabs.window.width.enter": {
"message": "너비 입력..."
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "플레이어에게 업데이트 게시"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "설정 파일 선택"
},
"instance.shared-instance.publish-review.description": {
"message": "콘텐츠 변경 사항을 검토하고 이번 업데이트에 포함할 설정 파일을 선택하세요."
},
"instance.shared-instance.publish-review.header": {
"message": "변경 사항 검토"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "업데이트 푸시"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "제거됨"
},
"instance.shared-instance.tooltip": {
"message": "이 인스턴스의 콘텐츠는 다른 사용자가 관리하고 있습니다."
},
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "공유 인스턴스 접근 권한이 취소되었습니다. 이 인스턴스는 계속 사용 가능하지만, 더 이상 연동되지 않으며 업데이트를 받지 않습니다."
},
"instance.shared-instance.unavailable.deleted-text": {
"message": "기본 인스턴스가 삭제되었습니다. 이 인스턴스는 계속 사용 가능하지만, 더 이상 연동되지 않으며 업데이트를 받지 않습니다."
},
"instance.shared-instance.unavailable.locked-text": {
"message": "이 공유 인스턴스는 콘텐츠 조정 팀에 의해 잠겼습니다. 기본 인스턴스 업데이트를 받지 않으며 플레이할 수 없습니다."
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraft 서버"
},
@@ -980,6 +1310,27 @@
"minecraft-account.sign-in": {
"message": "마인크래프트 계정으로 로그인"
},
"minecraft-required.description": {
"message": "게임을 실행하고 플레이하려면 마인크래프트를 보유한 Microsoft 계정이 필요합니다."
},
"minecraft-required.description-header": {
"message": "Microsoft 계정 로그인"
},
"minecraft-required.dont-have-account": {
"message": "계정이 없으신가요?"
},
"minecraft-required.get-minecraft": {
"message": "마인크래프트 얻기"
},
"minecraft-required.get-support": {
"message": "지원 받기"
},
"minecraft-required.header": {
"message": "마인크래프트 필요"
},
"minecraft-required.sign-in": {
"message": "Microsoft 계정 로그인"
},
"search.filter.locked.instance": {
"message": "인스턴스에서 관리"
},
@@ -92,9 +92,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Hiasan asli"
},
"app.appearance-settings.select-option": {
"message": "Pilih satu pilihan"
},
"app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Melangkau pengesahan berisiko rendah secara automatik seperti pemasangan pek mod pendua, pemadaman kandungan biasa, kemas kini pukal, menyahpautkan pek mod dan gesaan pembaikan. Amaran berbahaya masih akan ditunjukkan."
},
@@ -137,9 +134,6 @@
"app.browse.back-to-instance": {
"message": "Kembali ke pemasangan"
},
"app.browse.discover-content": {
"message": "Temui kandungan"
},
"app.browse.discover-servers": {
"message": "Temui pelayan"
},
@@ -278,18 +272,12 @@
"app.modal.update-to-play.header": {
"message": "Kemas kini untuk mainkan"
},
"app.modal.update-to-play.update-required-description": {
"message": "Kemas kini diperlukan untuk memainkan {name}. Sila kemas kini kepada versi terkini untuk melancarkan permainan."
},
"app.project.install-button.already-installed": {
"message": "Projek ini sudah dipasang"
},
"app.project.install-context.back-to-browse": {
"message": "Kembali menemukan"
},
"app.project.install-context.install-content-to-instance": {
"message": "Pasang kandungan ke dalam pemasangan"
},
"app.settings.developer-mode-enabled": {
"message": "Mod pembangun didayakan."
},

Some files were not shown because too many files have changed in this diff Show More