Compare commits

..
415 changed files with 11239 additions and 25090 deletions
@@ -9,6 +9,8 @@ body:
options:
- label: I checked the [existing issues](https://github.com/modrinth/code/issues?q=is%3Aissue) for duplicate feature requests
required: true
- label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com)
required: true
- type: dropdown
id: projects
attributes:
-14
View File
@@ -56,17 +56,6 @@ jobs:
ref: ${{ github.ref }}
token: ${{ secrets.CROWDIN_GH_TOKEN }}
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
- name: Enable Corepack
run: corepack enable
- name: Install script dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Configure Git author
id: git-author
uses: MarcoIeni/git-config@59144859caf016f8b817a2ac9b051578729173c4 # v0.1.2
@@ -106,9 +95,6 @@ jobs:
shell: bash
run: sudo chown -R $USER:$USER .
- name: Prune stale ICU translations
run: pnpm scripts i18n-icu-contract prune-local
- name: Create Pull Request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
+1 -22
View File
@@ -4,14 +4,11 @@ on:
push:
branches: ['main']
paths:
- '.github/workflows/i18n-push.yml'
- '.github/workflows/i18n.push.yml'
- 'apps/*/src/locales/en-US/**'
- 'apps/*/locales/en-US/**'
- 'packages/*/src/locales/en-US/**'
- 'packages/*/locales/en-US/**'
- 'scripts/i18n-icu-contract.ts'
- 'package.json'
- 'pnpm-lock.yaml'
- 'crowdin.yml'
workflow_dispatch:
@@ -59,18 +56,6 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.ref }}
fetch-depth: 2
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
- name: Enable Corepack
run: corepack enable
- name: Install script dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Query branch name
id: branch-name
@@ -94,9 +79,3 @@ jobs:
env:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Clear stale ICU translations in Crowdin
run: pnpm scripts i18n-icu-contract clear-crowdin-changed --base-ref HEAD^ --crowdin-branch "[${{ github.repository_owner }}.${{ github.event.repository.name }}] ${{ steps.branch-name.outputs.safe_branch_name }}"
env:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
+1 -2
View File
@@ -204,5 +204,4 @@ jobs:
- name: Verify intl:extract has been run
run: |
pnpm turbo run intl:extract --force
pnpm scripts i18n-icu-contract prune-local --check
git diff --exit-code --color */*/src/locales/*/index.json
git diff --exit-code --color */*/src/locales/en-US/index.json
Generated
-4
View File
@@ -10565,8 +10565,6 @@ version = "1.0.0-local"
dependencies = [
"async_zip",
"chrono",
"core-foundation 0.10.1",
"core-graphics",
"daedalus",
"dashmap",
"either",
@@ -10574,7 +10572,6 @@ dependencies = [
"hyper 1.7.0",
"hyper-util",
"native-dialog",
"objc2-app-kit",
"paste",
"path-util",
"serde",
@@ -10600,7 +10597,6 @@ dependencies = [
"urlencoding",
"uuid 1.18.1",
"webview2-com",
"windows",
"windows-core 0.61.2",
]
-3
View File
@@ -64,8 +64,6 @@ clickhouse = "0.14.0"
color-eyre = "0.6.5"
color-thief = "0.2.2"
const_format = "0.2.34"
core-foundation = "0.10.1"
core-graphics = "0.24.0"
daedalus = { path = "packages/daedalus" }
darling = { version = "0.23" }
dashmap = "6.1.0"
@@ -125,7 +123,6 @@ murmur2 = "0.1.0"
native-dialog = "0.9.2"
notify = { version = "8.2.0", default-features = false }
notify-debouncer-mini = { version = "0.7.0", default-features = false }
objc2-app-kit = { version = "0.3.2", default-features = false }
p256 = "0.13.2"
parking_lot = "0.12.5"
paste = "1.0.15"
+1 -1
View File
@@ -10,10 +10,10 @@
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write .",
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
"intl:prune-local": "pnpm -w scripts i18n-icu-contract prune-local --scope apps/app-frontend",
"test": "vue-tsc --noEmit"
},
"dependencies": {
"@intercom/messenger-js-sdk": "^0.0.14",
"@modrinth/api-client": "workspace:^",
"@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*",
+94 -44
View File
@@ -1,4 +1,5 @@
<script setup>
import { Intercom, shutdown as shutdownIntercom } from '@intercom/messenger-js-sdk'
import {
AuthFeature,
ModrinthApiError,
@@ -52,7 +53,6 @@ import {
providePopupNotificationManager,
useDebugLogger,
useFormatBytes,
useHostingIntercom,
useVIntl,
} from '@modrinth/ui'
import { renderString } from '@modrinth/utils'
@@ -88,6 +88,7 @@ import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import WindowControls from '@/components/ui/WindowControls.vue'
import { useIntercomPositioning } from '@/composables/intercom-positioning'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import { config } from '@/config'
import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js'
@@ -131,36 +132,15 @@ import { AppPopupNotificationManager } from './providers/app-popup-notifications
const themeStore = useTheming()
const router = useRouter()
const route = useRoute()
const APP_LEFT_NAV_WIDTH = '4rem'
const APP_SIDEBAR_WIDTH = 300
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
const credentials = ref()
const sidebarToggled = ref(true)
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
sidebarToggled.value = !themeStore.toggleSidebar
})
const forceSidebar = computed(
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
const hostingIntercomIdentityKey = computed(() => {
const rawServerId = route.params.id
const serverId = Array.isArray(rawServerId) ? rawServerId[0] : rawServerId
const userId = credentials.value?.user_id ?? credentials.value?.user?.id ?? 'anonymous'
return `${userId}:${serverId ?? 'hosting'}`
})
const hostingIntercom = useHostingIntercom({
enabled: computed(() => hostingRouteActive.value && !!credentials.value?.session),
appId: 'ykeritl9',
fetchToken: fetchIntercomToken,
identityKey: hostingIntercomIdentityKey,
horizontalPadding: computed(() =>
sidebarVisible.value
? APP_SIDEBAR_WIDTH + INTERCOM_BUBBLE_DEFAULT_PADDING
: INTERCOM_BUBBLE_DEFAULT_PADDING,
),
})
const intercomBubblePositioning = useIntercomPositioning({ route, themeStore })
const {
sidebarToggled,
forceSidebar,
sidebarVisible,
intercomBubblePosition,
updateIntercomBubbleStyles,
clearIntercomBubbleStyles,
} = intercomBubblePositioning
const notificationManager = new AppNotificationManager()
provideNotificationManager(notificationManager)
@@ -195,11 +175,7 @@ provideModrinthClient(tauriApiClient)
providePageContext({
hierarchicalSidebarAvailable: ref(true),
showAds: ref(false),
floatingActionBarOffsets: {
left: ref(APP_LEFT_NAV_WIDTH),
right: computed(() => (sidebarVisible.value ? `${APP_SIDEBAR_WIDTH}px` : '0px')),
},
intercomBubble: hostingIntercom.intercomBubble,
...intercomBubblePositioning.pageContext,
featureFlags: {
serverRamAsBytesAlwaysOn: computed(() =>
themeStore.getFeatureFlag('server_ram_as_bytes_always_on'),
@@ -283,7 +259,8 @@ onMounted(async () => {
onUnmounted(async () => {
document.querySelector('body').removeEventListener('click', handleClick)
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
unsubscribeSidebarToggle()
shutdownHostingIntercom()
clearIntercomBubbleStyles()
await unlistenUpdateDownload?.()
})
@@ -330,7 +307,6 @@ async function setupApp() {
locale,
telemetry,
collapsed_navigation,
hide_nametag_skins_page,
advanced_rendering,
onboarded,
default_page,
@@ -361,7 +337,6 @@ async function setupApp() {
themeStore.setThemeState(theme)
themeStore.collapsedNavigation = collapsed_navigation
themeStore.advancedRendering = advanced_rendering
themeStore.hideNametagSkinsPage = hide_nametag_skins_page
themeStore.toggleSidebar = toggle_sidebar
themeStore.devMode = developer_mode
themeStore.featureFlags = feature_flags
@@ -618,6 +593,8 @@ const incompatibilityWarningModal = ref()
const installToPlayModal = ref()
const updateToPlayModal = ref()
const credentials = ref()
const modrinthLoginFlowWaitModal = ref()
setupAuthProvider(credentials, async (_redirectPath) => {
@@ -687,6 +664,10 @@ const hasPlus = computed(
const showAd = computed(
() => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined,
)
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
let intercomBooting = false
let intercomBooted = false
async function fetchIntercomToken() {
const creds = await getCreds()
@@ -695,10 +676,8 @@ async function fetchIntercomToken() {
}
const params = new URLSearchParams()
const rawServerId = route.params.id
const serverId = Array.isArray(rawServerId) ? rawServerId[0] : rawServerId
if (route.path.startsWith('/hosting/manage/') && typeof serverId === 'string') {
params.set('server_id', serverId)
if (route.path.startsWith('/hosting/manage/') && typeof route.params.id === 'string') {
params.set('server_id', route.params.id)
}
const query = params.size > 0 ? `?${params.toString()}` : ''
@@ -714,6 +693,69 @@ async function fetchIntercomToken() {
return await response.json()
}
async function bootIntercom() {
if (
intercomBooting ||
intercomBooted ||
!hostingRouteActive.value ||
!credentials.value?.session
) {
return
}
intercomBooting = true
console.debug('[APP][INTERCOM] initializing secure support chat')
try {
const { token } = await fetchIntercomToken()
Intercom({
app_id: 'ykeritl9',
intercom_user_jwt: token,
session_duration: 1000 * 60 * 60 * 24,
alignment: 'right',
horizontal_padding: intercomBubblePosition.value.horizontalPadding,
vertical_padding: intercomBubblePosition.value.verticalPadding,
})
intercomBooted = true
} catch (error) {
console.warn('[APP][INTERCOM] failed to initialize secure support chat', error)
} finally {
intercomBooting = false
}
}
function shutdownHostingIntercom() {
if (!intercomBooted && !intercomBooting) return
shutdownIntercom()
intercomBooting = false
intercomBooted = false
}
watch(
intercomBubblePosition,
(position) => {
updateIntercomBubbleStyles(position)
if (intercomBooted) {
window.Intercom?.('update', {
horizontal_padding: position.horizontalPadding,
vertical_padding: position.verticalPadding,
})
}
},
{ immediate: true },
)
watch(
[hostingRouteActive, credentials],
([active]) => {
if (active) {
void bootIntercom()
} else {
shutdownHostingIntercom()
}
},
{ immediate: true },
)
watch(showAd, () => {
if (!showAd.value) {
hide_ads_window(true)
@@ -1229,7 +1271,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
>
<CompassIcon />
</NavButton>
<NavButton v-tooltip.right="'Skin selector'" to="/skins">
<NavButton v-tooltip.right="'Skins (Beta)'" to="/skins">
<ChangeSkinIcon />
</NavButton>
<NavButton
@@ -1780,6 +1822,14 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
--os-handle-bg-active: var(--color-scrollbar) !important;
}
.intercom-lightweight-app-launcher,
.intercom-launcher-frame,
iframe[name='intercom-launcher-frame'] {
right: var(--app-support-launcher-right, 20px) !important;
bottom: var(--app-support-launcher-bottom, 20px) !important;
z-index: 9 !important;
}
.mac {
.app-grid-statusbar {
padding-left: 5rem;
@@ -150,10 +150,7 @@ async function refreshValues() {
if (equippedSkin.value) {
try {
const headUrl = await getPlayerHeadUrl(equippedSkin.value)
headUrlCache.value = new Map(headUrlCache.value).set(
equippedSkin.value.texture_key,
headUrl,
)
headUrlCache.value.set(equippedSkin.value.texture_key, headUrl)
} catch (error) {
console.warn('Failed to get head render for equipped skin:', error)
}
@@ -163,24 +160,12 @@ async function refreshValues() {
}
}
async function setEquippedSkin(skin: Skin) {
equippedSkin.value = skin
try {
const headUrl = await getPlayerHeadUrl(skin)
headUrlCache.value = new Map(headUrlCache.value).set(skin.texture_key, headUrl)
} catch (error) {
console.warn('Failed to get head render for equipped skin:', error)
}
}
function setLoginDisabled(value: boolean) {
loginDisabled.value = value
}
defineExpose({
refreshValues,
setEquippedSkin,
setLoginDisabled,
loginDisabled,
})
@@ -1,33 +1,35 @@
<template>
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
<div class="flex flex-col gap-4">
<Table :columns="javaInstallColumns" :data="chosenInstallOptions" row-key="path">
<template #cell-version="{ value }">
<span class="font-semibold text-primary">{{ value }}</span>
</template>
<template #cell-path="{ value }">
<span v-tooltip="value" class="block truncate font-mono text-xs">{{ value }}</span>
</template>
<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>
<div class="auto-detect-modal">
<div class="table">
<div class="table-row table-head">
<div class="table-cell table-text">Version</div>
<div class="table-cell table-text">Path</div>
<div class="table-cell table-text">Actions</div>
</div>
<div v-for="javaInstall in chosenInstallOptions" :key="javaInstall.path" class="table-row">
<div class="table-cell table-text">
<span>{{ javaInstall.version }}</span>
</div>
<div v-tooltip="javaInstall.path" class="table-cell table-text">
<span>{{ javaInstall.path }}</span>
</div>
<div class="table-cell table-text manage">
<ButtonStyled v-if="currentSelected.path === javaInstall.path">
<button disabled><CheckIcon /> Selected</button>
</ButtonStyled>
<ButtonStyled v-else>
<button class="!shadow-none" @click="setJavaInstall(row)"><PlusIcon /> Select</button>
<button @click="setJavaInstall(javaInstall)"><PlusIcon /> Select</button>
</ButtonStyled>
</div>
</template>
<template #empty-state>
<div class="p-4 text-secondary">No java installations found!</div>
</template>
</Table>
<div class="flex justify-end">
</div>
<div v-if="chosenInstallOptions.length === 0" class="table-row entire-row">
<div class="table-cell table-text">No java installations found!</div>
</div>
</div>
<div class="input-group push-right">
<ButtonStyled type="outlined">
<button
class="!shadow-none !border-surface-4 !border"
@click="$refs.detectJavaModal.hide()"
>
<button @click="$refs.detectJavaModal.hide()">
<XIcon />
Cancel
</button>
@@ -38,7 +40,7 @@
</template>
<script setup>
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, Table } from '@modrinth/ui'
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
import { ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
@@ -50,11 +52,6 @@ const { handleError } = injectNotificationManager()
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' },
]
defineExpose({
show: async (version, currentSelectedJava) => {
@@ -80,3 +77,25 @@ function setJavaInstall(javaInstall) {
})
}
</script>
<style lang="scss" scoped>
.auto-detect-modal {
.table {
.table-row {
grid-template-columns: 1fr 4fr min-content;
}
span {
display: inherit;
align-items: center;
justify-content: center;
}
padding: 0.5rem;
}
}
.manage {
display: flex;
gap: 0.5rem;
}
</style>
@@ -1,104 +1,85 @@
<template>
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
<div :id="props.id" class="toggle-setting" :class="{ compact }">
<div class="input-with-status">
<StyledInput
autocomplete="off"
:disabled="props.disabled"
:model-value="props.modelValue ? props.modelValue.path : ''"
:placeholder="placeholder ?? '/path/to/java'"
wrapper-class="installation-input"
@update:model-value="
(val) => {
emit('update:modelValue', {
...props.modelValue,
path: val,
})
}
"
/>
<ButtonStyled
:color="
!hoveringTest && !testingJava
? testingJavaSuccess === true
? 'green'
: 'red'
: 'standard'
"
color-fill="text"
>
<button
class="!shadow-none"
:disabled="testingJava || props.disabled"
@click="runTest(props.modelValue?.path)"
@mouseenter="!props.disabled && (hoveringTest = true)"
@mouseleave="hoveringTest = false"
>
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
<CheckCircleIcon
v-else-if="testingJavaSuccess === true && !hoveringTest"
class="h-4 w-4"
/>
<XCircleIcon v-else-if="testingJavaSuccess !== true && !hoveringTest" class="h-4 w-4" />
<RefreshCwIcon v-else-if="!props.disabled" class="h-4 w-4" />
</button>
</ButtonStyled>
</div>
<div class="toggle-setting" :class="{ compact }">
<StyledInput
autocomplete="off"
:disabled="props.disabled"
:model-value="props.modelValue ? props.modelValue.path : ''"
:placeholder="placeholder ?? '/path/to/java'"
wrapper-class="installation-input"
@update:model-value="
(val) => {
emit('update:modelValue', {
...props.modelValue,
path: val,
})
}
"
/>
<span class="installation-buttons">
<ButtonStyled v-if="props.version">
<button
v-tooltip="testingJavaSuccess === true ? 'Already installed' : undefined"
class="!shadow-none"
:disabled="props.disabled || installingJava || testingJavaSuccess === true"
@click="reinstallJava"
>
<button :disabled="props.disabled || installingJava" @click="reinstallJava">
<DownloadIcon />
{{ installingJava ? 'Installing...' : 'Install recommended' }}
</button>
</ButtonStyled>
<ButtonStyled>
<button class="!shadow-none" :disabled="props.disabled" @click="autoDetect">
<button :disabled="props.disabled" @click="autoDetect">
<SearchIcon />
Detect
</button>
</ButtonStyled>
<ButtonStyled>
<button class="!shadow-none" :disabled="props.disabled" @click="handleJavaFileInput()">
<button :disabled="props.disabled" @click="handleJavaFileInput()">
<FolderSearchIcon />
Browse
</button>
</ButtonStyled>
<ButtonStyled v-if="testingJava">
<button disabled>Testing...</button>
</ButtonStyled>
<ButtonStyled v-else-if="testingJavaSuccess === true">
<button disabled>
<CheckIcon />
Success
</button>
</ButtonStyled>
<ButtonStyled v-else-if="testingJavaSuccess === false">
<button disabled>
<XIcon />
Failed
</button>
</ButtonStyled>
<ButtonStyled v-else>
<button :disabled="props.disabled" @click="testJava">
<PlayIcon />
Test
</button>
</ButtonStyled>
</span>
</div>
</template>
<script setup>
import {
CheckCircleIcon,
CheckIcon,
DownloadIcon,
FolderSearchIcon,
RefreshCwIcon,
PlayIcon,
SearchIcon,
SpinnerIcon,
XCircleIcon,
XIcon,
} from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, StyledInput } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref, watch } from 'vue'
import { ref } from 'vue'
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import useJavaTest from '@/composables/useJavaTest'
import { trackEvent } from '@/helpers/analytics'
import { auto_install_java, find_filtered_jres, get_jre } from '@/helpers/jre.js'
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
const { handleError } = injectNotificationManager()
const props = defineProps({
id: {
type: String,
required: false,
default: null,
},
version: {
type: Number,
required: false,
@@ -129,36 +110,29 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const {
testingJava,
javaTestResult: testingJavaSuccess,
testJavaInstallationDebounced,
testJavaInstallation,
} = useJavaTest()
const testingJava = ref(false)
const testingJavaSuccess = ref(null)
const installingJava = ref(false)
const hoveringTest = ref(false)
let hasInitialized = false
async function runTest(path) {
await testJavaInstallation(path, props.version, true)
async function testJava() {
testingJava.value = true
testingJavaSuccess.value = await test_jre(
props.modelValue ? props.modelValue.path : '',
props.version,
)
testingJava.value = false
trackEvent('JavaTest', {
path: props.modelValue ? props.modelValue.path : '',
success: testingJavaSuccess.value,
})
setTimeout(() => {
testingJavaSuccess.value = null
}, 2000)
}
watch(
() => props.modelValue?.path,
(newPath) => {
if (newPath) {
if (!hasInitialized) {
testJavaInstallation(newPath, props.version, false)
hasInitialized = true
} else {
testJavaInstallationDebounced(newPath, props.version)
}
}
},
{ immediate: true },
)
async function handleJavaFileInput() {
const filePath = await open()
@@ -168,7 +142,6 @@ async function handleJavaFileInput() {
result = {
path: filePath.path ?? filePath,
version: props.version.toString(),
parsed_version: props.version,
architecture: 'x86',
}
}
@@ -202,7 +175,6 @@ async function reinstallJava() {
result = {
path: path,
version: props.version.toString(),
parsed_version: props.version,
architecture: 'x86',
}
}
@@ -214,23 +186,13 @@ async function reinstallJava() {
emit('update:modelValue', result)
installingJava.value = false
runTest(result.path)
}
</script>
<style lang="scss" scoped>
.input-with-status {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
width: 100%;
min-width: 0;
}
.installation-input {
flex: 1 1 0;
min-width: 0;
width: 100% !important;
flex-grow: 1;
}
.toggle-setting {
@@ -253,4 +215,12 @@ async function reinstallJava() {
gap: 0.5rem;
margin: 0;
}
.test-success {
color: var(--color-green);
}
.test-fail {
color: var(--color-red);
}
</style>
@@ -35,7 +35,7 @@ const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
const { instance, offline, isMinecraftServer, onUnlinked } = injectInstanceSettings()
const [
fabric_versions,
@@ -113,7 +113,6 @@ provideAppBackup({
})
provideInstallationSettings({
closeSettings: closeModal,
loading: ref(false),
installationInfo: computed(() => {
const rows = [
@@ -1,15 +1,6 @@
<script setup lang="ts">
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
import {
CheckCircleIcon,
CoffeeIcon,
FolderSearchIcon,
RefreshCwIcon,
SearchIcon,
SpinnerIcon,
XCircleIcon,
} from '@modrinth/assets'
import {
ButtonStyled,
Checkbox,
defineMessages,
injectNotificationManager,
@@ -17,11 +8,9 @@ import {
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { computed, readonly, ref, watch } from 'vue'
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import useJavaTest from '@/composables/useJavaTest'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import useMemorySlider from '@/composables/useMemorySlider'
import { edit, get_optimal_jre_key } from '@/helpers/profile'
import { get } from '@/helpers/settings.ts'
@@ -36,54 +25,9 @@ const { instance } = injectInstanceSettings()
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
const optimalJava = readonly(await get_optimal_jre_key(instance.value.path).catch(handleError))
const overrideJavaInstall = ref(!!instance.value.java_path)
const javaPath = ref(instance.value.java_path ?? optimalJava?.path ?? '')
const activePath = computed(() =>
overrideJavaInstall.value ? javaPath.value : (optimalJava?.path ?? ''),
)
watch(overrideJavaInstall, (enabled) => {
if (enabled && !javaPath.value) {
javaPath.value = optimalJava?.path ?? ''
}
})
const { testingJava, javaTestResult, testJavaInstallationDebounced, testJavaInstallation } =
useJavaTest()
const hoveringTest = ref(false)
let hasInitialized = false
watch(
activePath,
(newPath) => {
if (newPath && optimalJava?.parsed_version) {
if (!hasInitialized) {
testJavaInstallation(newPath, optimalJava?.parsed_version, false)
hasInitialized = true
} else {
testJavaInstallationDebounced(newPath, optimalJava?.parsed_version)
}
}
},
{ immediate: true },
)
const javaDetectionModal = ref<{ show: (version: number, current: object) => void } | null>(null)
async function handleBrowseJava() {
const result = await open({ multiple: false })
if (result) {
javaPath.value = result
}
}
function handleDetectJava() {
javaDetectionModal.value?.show(optimalJava?.parsed_version, { path: javaPath.value })
}
const optimalJava = readonly(await get_optimal_jre_key(instance.value.path).catch(handleError))
const javaInstall = ref({ path: optimalJava.path ?? instance.value.java_path })
const overrideJavaArgs = ref((instance.value.extra_launch_args?.length ?? 0) > 0)
const javaArgs = ref(
@@ -107,8 +51,8 @@ const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) a
const editProfileObject = computed(() => {
return {
java_path:
overrideJavaInstall.value && javaPath.value
? javaPath.value.replace('java.exe', 'javaw.exe')
overrideJavaInstall.value && javaInstall.value.path !== ''
? javaInstall.value.path.replace('java.exe', 'javaw.exe')
: null,
extra_launch_args: overrideJavaArgs.value
? javaArgs.value.trim().split(/\s+/).filter(Boolean)
@@ -127,7 +71,7 @@ const editProfileObject = computed(() => {
watch(
[
overrideJavaInstall,
javaPath,
javaInstall,
overrideJavaArgs,
javaArgs,
overrideEnvVars,
@@ -146,45 +90,17 @@ const messages = defineMessages({
id: 'instance.settings.tabs.java.java-installation',
defaultMessage: 'Java installation',
},
customJavaInstallation: {
id: 'instance.settings.tabs.java.custom-java-installation',
defaultMessage: 'Custom Java installation',
},
javaPathPlaceholder: {
id: 'instance.settings.tabs.java.java-path-placeholder',
defaultMessage: '/path/to/java',
},
javaMemory: {
id: 'instance.settings.tabs.java.java-memory',
defaultMessage: 'Memory allocated',
},
customMemoryAllocation: {
id: 'instance.settings.tabs.java.custom-memory-allocation',
defaultMessage: 'Custom memory allocation',
},
javaArguments: {
id: 'instance.settings.tabs.java.java-arguments',
defaultMessage: 'Java arguments',
},
customJavaArguments: {
id: 'instance.settings.tabs.java.custom-java-arguments',
defaultMessage: 'Custom Java arguments',
},
enterJavaArguments: {
id: 'instance.settings.tabs.java.enter-java-arguments',
defaultMessage: 'Enter Java arguments...',
},
javaEnvironmentVariables: {
id: 'instance.settings.tabs.java.environment-variables',
defaultMessage: 'Environment variables',
},
customEnvironmentVariables: {
id: 'instance.settings.tabs.java.custom-environment-variables',
defaultMessage: 'Custom environment variables',
},
enterEnvironmentVariables: {
id: 'instance.settings.tabs.java.enter-environment-variables',
defaultMessage: 'Enter environmental variables...',
javaMemory: {
id: 'instance.settings.tabs.java.java-memory',
defaultMessage: 'Memory allocated',
},
hooks: {
id: 'instance.settings.tabs.java.hooks',
@@ -195,86 +111,43 @@ const messages = defineMessages({
<template>
<div>
<JavaDetectionModal ref="javaDetectionModal" @submit="(val) => (javaPath = val.path)" />
<h2 class="m-0 mb-2 text-lg font-extrabold text-contrast block">
<h2 id="project-name" class="m-0 mb-2.5 text-lg font-semibold text-contrast block">
{{ formatMessage(messages.javaInstallation) }}
</h2>
<Checkbox
v-model="overrideJavaInstall"
:label="formatMessage(messages.customJavaInstallation)"
class="mb-2"
/>
<div class="flex gap-4 p-4 bg-bg rounded-2xl">
<div class="flex gap-3 items-start flex-1 min-w-0">
<div
class="w-10 h-10 flex items-center justify-center rounded-full bg-button-bg border-solid border-[1px] border-button-border p-2 mt-1 shrink-0 [&_svg]:h-full [&_svg]:w-full"
>
<CoffeeIcon />
</div>
<div class="flex flex-col gap-2 flex-1 min-w-0">
<span class="font-semibold leading-none mt-2"
>Java {{ optimalJava?.parsed_version }}</span
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2.5" />
<template v-if="!overrideJavaInstall">
<div class="flex my-2 items-center gap-2 font-semibold">
<template v-if="javaInstall">
<CheckCircleIcon class="text-brand-green h-4 w-4" />
<span>Using default Java {{ optimalJava.major_version }} installation:</span>
</template>
<template v-else-if="optimalJava">
<XCircleIcon class="text-brand-red h-5 w-5" />
<span
>Could not find a default Java {{ optimalJava.major_version }} installation. Please set
one below:</span
>
<div class="flex gap-2 items-center">
<StyledInput
:model-value="activePath"
:disabled="!overrideJavaInstall"
autocomplete="off"
:placeholder="formatMessage(messages.javaPathPlaceholder)"
wrapper-class="flex-1 min-w-0"
@update:model-value="(val) => (javaPath = String(val))"
/>
<ButtonStyled
:color="
!hoveringTest && !testingJava
? javaTestResult === true
? 'green'
: 'red'
: 'standard'
"
color-fill="text"
>
<button
:disabled="!overrideJavaInstall || testingJava"
@click="testJavaInstallation(activePath, optimalJava?.parsed_version, true)"
@mouseenter="overrideJavaInstall && (hoveringTest = true)"
@mouseleave="hoveringTest = false"
>
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
<CheckCircleIcon
v-else-if="javaTestResult === true && !hoveringTest"
class="h-4 w-4"
/>
<XCircleIcon v-else-if="javaTestResult !== true && !hoveringTest" class="h-4 w-4" />
<RefreshCwIcon v-else-if="overrideJavaInstall" class="h-4 w-4" />
</button>
</ButtonStyled>
</div>
<div v-if="overrideJavaInstall" class="flex gap-2">
<ButtonStyled>
<button @click="handleDetectJava">
<SearchIcon />
Detect
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="handleBrowseJava">
<FolderSearchIcon />
Browse
</button>
</ButtonStyled>
</div>
</div>
</template>
<template v-else>
<XCircleIcon class="text-brand-red h-5 w-5" />
<span
>Could not automatically determine a Java installation to use. Please set one
below:</span
>
</template>
</div>
</div>
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
<div
v-if="javaInstall && !overrideJavaInstall"
class="p-4 bg-bg rounded-xl text-xs text-secondary leading-none font-mono"
>
{{ javaInstall.path }}
</div>
</template>
<JavaSelector v-if="overrideJavaInstall || !javaInstall" v-model="javaInstall" />
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
{{ formatMessage(messages.javaMemory) }}
</h2>
<Checkbox
v-model="overrideMemorySettings"
:label="formatMessage(messages.customMemoryAllocation)"
class="mb-2"
/>
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2.5" />
<Slider
id="max-memory"
v-model="memory.maximum"
@@ -286,36 +159,28 @@ const messages = defineMessages({
:snap-range="512"
unit="MB"
/>
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
{{ formatMessage(messages.javaArguments) }}
</h2>
<Checkbox
v-model="overrideJavaArgs"
:label="formatMessage(messages.customJavaArguments)"
class="my-2"
/>
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
<StyledInput
id="java-args"
v-model="javaArgs"
autocomplete="off"
:disabled="!overrideJavaArgs"
:placeholder="formatMessage(messages.enterJavaArguments)"
placeholder="Enter java arguments..."
wrapper-class="w-full"
/>
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
{{ formatMessage(messages.javaEnvironmentVariables) }}
</h2>
<Checkbox
v-model="overrideEnvVars"
:label="formatMessage(messages.customEnvironmentVariables)"
class="mb-2"
/>
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2.5" />
<StyledInput
id="env-vars"
v-model="envVars"
autocomplete="off"
:disabled="!overrideEnvVars"
:placeholder="formatMessage(messages.enterEnvironmentVariables)"
placeholder="Enter environmental variables..."
wrapper-class="w-full"
/>
</div>
@@ -45,19 +45,12 @@ const isMinecraftServer = ref(false)
const handleUnlinked = () => emit('unlinked')
const instanceRef = computed(() => props.instance)
const queryClient = useQueryClient()
const tabbedModal = ref<InstanceType<typeof TabbedModal> | null>(null)
function hide() {
tabbedModal.value?.hide()
}
provideInstanceSettings({
instance: instanceRef,
offline: props.offline,
isMinecraftServer,
onUnlinked: handleUnlinked,
closeModal: hide,
})
watch(
@@ -120,6 +113,9 @@ const tabs = computed<TabbedModalTab[]>(() => [
},
])
const queryClient = useQueryClient()
const tabbedModal = ref<InstanceType<typeof TabbedModal> | null>(null)
function show(tabIndex?: number) {
if (props.instance.linked_data?.project_id) {
queryClient.prefetchQuery({
@@ -133,7 +129,7 @@ function show(tabIndex?: number) {
}
}
defineExpose({ show, hide })
defineExpose({ show })
</script>
<template>
<TabbedModal
@@ -160,16 +160,7 @@ watch(
</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
}
"
/>
<Toggle id="hide-nametag-skins-page" v-model="settings.hide_nametag_skins_page" />
</div>
<div v-if="os !== 'MacOS'" class="mt-6 flex items-center justify-between gap-4">
@@ -1,242 +1,149 @@
<template>
<NewModal ref="modal" :on-hide="handleModalHide">
<UploadSkinModal ref="uploadModal" />
<ModalWrapper ref="modal" @on-hide="resetState">
<template #title>
<span class="text-lg font-extrabold text-contrast">
{{ formatMessage(mode === 'edit' ? messages.editSkinTitle : messages.addSkinTitle) }}
{{ mode === 'edit' ? 'Editing skin' : 'Adding a skin' }}
</span>
</template>
<div class="flex flex-col md:flex-row gap-6">
<div class="h-[25rem] w-[16rem] min-w-[16rem] flex-shrink-0 md:self-center">
<SkinPreviewRenderer
:variant="variant"
:texture-src="previewSkin || ''"
:cape-src="selectedCapeTexture"
framing="modal"
:initial-rotation="Math.PI / 8"
class="h-full w-full"
/>
<div class="max-h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
<SkinPreviewRenderer
:variant="variant"
:texture-src="previewSkin || ''"
:cape-src="selectedCapeTexture"
:scale="1.4"
:fov="50"
:initial-rotation="Math.PI / 8"
class="h-full w-full"
/>
</div>
</div>
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
<section v-if="mode === 'edit' && canEditTextureAndModel">
<h2 class="text-base font-semibold mb-2">{{ formatMessage(messages.textureSection) }}</h2>
<section>
<h2 class="text-base font-semibold mb-2">Texture</h2>
<ButtonStyled>
<button class="!shadow-none" @click="openTextureFileBrowser">
<UploadIcon /> {{ formatMessage(messages.replaceTextureButton) }}
</button>
<button @click="openUploadSkinModal"><UploadIcon /> Replace texture</button>
</ButtonStyled>
<input
ref="textureFileInput"
type="file"
accept="image/png"
class="hidden"
@change="onTextureFileInputChange"
/>
</section>
<section v-if="canEditTextureAndModel">
<h2 class="text-base font-semibold mb-2">
{{ formatMessage(messages.armStyleSection) }}
</h2>
<section>
<h2 class="text-base font-semibold mb-2">Arm style</h2>
<RadioButtons v-model="variant" :items="['CLASSIC', 'SLIM']">
<template #default="{ item }">
{{
formatMessage(item === 'CLASSIC' ? messages.wideArmStyle : messages.slimArmStyle)
}}
{{ item === 'CLASSIC' ? 'Wide' : 'Slim' }}
</template>
</RadioButtons>
</section>
<section>
<h2 class="text-base font-semibold mb-2">{{ formatMessage(messages.capeSection) }}</h2>
<div class="relative w-fit max-w-full">
<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-6"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-6"
leave-to-class="opacity-0 max-h-0"
<h2 class="text-base font-semibold mb-2">Cape</h2>
<div class="flex gap-2">
<CapeButton
v-if="defaultCape"
:id="defaultCape.id"
:texture="defaultCape.texture"
:name="undefined"
:selected="!selectedCape"
faded
@select="selectCape(undefined)"
>
<div
v-if="showCapeTopFade"
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-6 bg-gradient-to-b from-bg-raised to-transparent"
/>
</Transition>
<span>Use default cape</span>
</CapeButton>
<CapeLikeTextButton v-else :highlighted="!selectedCape" @click="selectCape(undefined)">
<span>Use default cape</span>
</CapeLikeTextButton>
<div
ref="capeListRef"
class="grid grid-cols-[repeat(4,max-content)] auto-rows-max gap-2 overflow-y-auto pr-1"
:style="{ maxHeight: capeListMaxHeight }"
@scroll="checkCapeScrollState"
<CapeButton
v-for="cape in visibleCapeList"
:id="cape.id"
:key="cape.id"
:texture="cape.texture"
:name="cape.name || 'Cape'"
:selected="selectedCape?.id === cape.id"
@select="selectCape(cape)"
/>
<CapeLikeTextButton
v-if="(capes?.length ?? 0) > 2"
tooltip="View more capes"
@mouseup="openSelectCapeModal"
>
<CapeLikeTextButton
:tooltip="formatMessage(messages.noCapeTooltip)"
:highlighted="!selectedCape"
@click="selectCape(undefined)"
>
<template #icon><XIcon /></template>
<span>{{ formatMessage(messages.noneCapeOption) }}</span>
</CapeLikeTextButton>
<CapeButton
v-for="cape in sortedCapes"
:id="cape.id"
:key="cape.id"
:texture="cape.texture"
:name="cape.name || formatMessage(messages.capeFallbackName)"
:selected="selectedCape?.id === cape.id"
@select="selectCape(cape)"
/>
</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-6"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-6"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="showCapeBottomFade"
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-6 bg-gradient-to-t from-bg-raised to-transparent"
/>
</Transition>
<template #icon><ChevronRightIcon /></template>
<span>More</span>
</CapeLikeTextButton>
</div>
</section>
</div>
</div>
<template #actions>
<div class="flex gap-2 justify-end">
<ButtonStyled type="outlined">
<button :disabled="isSaving" @click="hide">
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
<SpinnerIcon v-if="isSaving" class="animate-spin" />
<CheckIcon v-else-if="mode === 'new'" />
<SaveIcon v-else />
{{ formatMessage(mode === 'new' ? messages.addSkinButton : messages.saveSkinButton) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
<div class="flex gap-2 mt-12">
<ButtonStyled color="brand">
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
<SpinnerIcon v-if="isSaving" class="animate-spin" />
<CheckIcon v-else-if="mode === 'new'" />
<SaveIcon v-else />
{{ mode === 'new' ? 'Add skin' : 'Save skin' }}
</button>
</ButtonStyled>
<ButtonStyled type="outlined">
<button :disabled="isSaving" @click="hide"><XIcon />Cancel</button>
</ButtonStyled>
</div>
</ModalWrapper>
<SelectCapeModal
ref="selectCapeModal"
:capes="capes || []"
@select="handleCapeSelected"
@cancel="handleCapeCancel"
/>
</template>
<script setup lang="ts">
import { CheckIcon, SaveIcon, SpinnerIcon, UploadIcon, XIcon } from '@modrinth/assets'
import {
CheckIcon,
ChevronRightIcon,
SaveIcon,
SpinnerIcon,
UploadIcon,
XIcon,
} from '@modrinth/assets'
import {
ButtonStyled,
CapeButton,
CapeLikeTextButton,
commonMessages,
defineMessages,
injectNotificationManager,
NewModal,
RadioButtons,
SkinPreviewRenderer,
useScrollIndicator,
useVIntl,
} from '@modrinth/ui'
import { arrayBufferToBase64 } from '@modrinth/utils'
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
import { computed, ref, useTemplateRef, watch } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
import {
add_and_equip_custom_skin,
type Cape,
determineModelType,
equip_skin,
get_normalized_skin_texture,
normalize_skin_texture,
save_custom_skin,
remove_custom_skin,
type Skin,
type SkinModel,
type SkinTextureUrl,
unequip_skin,
} from '@/helpers/skins.ts'
const CAPE_LIST_MAX_HEIGHT = 334
const messages = defineMessages({
editSkinTitle: {
id: 'app.skins.modal.edit-title',
defaultMessage: 'Editing skin',
},
addSkinTitle: {
id: 'app.skins.modal.add-title',
defaultMessage: 'Adding a skin',
},
textureSection: {
id: 'app.skins.modal.texture-section',
defaultMessage: 'Texture',
},
replaceTextureButton: {
id: 'app.skins.modal.replace-texture-button',
defaultMessage: 'Replace texture',
},
armStyleSection: {
id: 'app.skins.modal.arm-style-section',
defaultMessage: 'Arm style',
},
wideArmStyle: {
id: 'app.skins.modal.arm-style-wide',
defaultMessage: 'Wide',
},
slimArmStyle: {
id: 'app.skins.modal.arm-style-slim',
defaultMessage: 'Slim',
},
capeSection: {
id: 'app.skins.modal.cape-section',
defaultMessage: 'Cape',
},
noCapeTooltip: {
id: 'app.skins.modal.no-cape-tooltip',
defaultMessage: 'No cape',
},
noneCapeOption: {
id: 'app.skins.modal.none-cape-option',
defaultMessage: 'None',
},
capeFallbackName: {
id: 'app.skins.modal.cape-fallback-name',
defaultMessage: 'Cape',
},
savingTooltip: {
id: 'app.skins.modal.saving-tooltip',
defaultMessage: 'Saving...',
},
uploadSkinFirstTooltip: {
id: 'app.skins.modal.upload-skin-first-tooltip',
defaultMessage: 'Upload a skin first!',
},
makeEditFirstTooltip: {
id: 'app.skins.modal.make-edit-first-tooltip',
defaultMessage: 'Make an edit to the skin first!',
},
addSkinButton: {
id: 'app.skins.modal.add-skin-button',
defaultMessage: 'Add skin',
},
saveSkinButton: {
id: 'app.skins.modal.save-skin-button',
defaultMessage: 'Save skin',
},
})
const { formatMessage } = useVIntl()
const { handleError } = injectNotificationManager()
const modal = useTemplateRef('modal')
const textureFileInput = useTemplateRef<HTMLInputElement>('textureFileInput')
const capeListRef = ref<HTMLElement | null>(null)
const capeListMaxHeight = ref(`${CAPE_LIST_MAX_HEIGHT}px`)
const selectCapeModal = useTemplateRef('selectCapeModal')
const mode = ref<'new' | 'edit'>('new')
const currentSkin = ref<Skin | null>(null)
const shouldRestoreModal = ref(false)
const isSaving = ref(false)
const uploadedTextureUrl = ref<SkinTextureUrl | null>(null)
@@ -244,49 +151,10 @@ const previewSkin = ref<string>('')
const variant = ref<SkinModel>('CLASSIC')
const selectedCape = ref<Cape | undefined>(undefined)
const props = defineProps<{ capes?: Cape[] }>()
const props = defineProps<{ capes?: Cape[]; defaultCape?: Cape }>()
const selectedCapeTexture = computed(() => selectedCape.value?.texture)
const canEditTextureAndModel = computed(() => currentSkin.value?.source !== 'default')
const {
showTopFade: showCapeTopFade,
showBottomFade: showCapeBottomFade,
checkScrollState: checkCapeScrollState,
forceCheck: forceCapeScrollCheck,
} = useScrollIndicator(capeListRef)
let capeListLayoutFrame: number | null = null
function updateCapeListLayout() {
const capeList = capeListRef.value
const modalContent = capeList?.closest('[data-modal-content]') as HTMLElement | null
if (!capeList || !modalContent) {
capeListMaxHeight.value = `${CAPE_LIST_MAX_HEIGHT}px`
forceCapeScrollCheck()
return
}
const availableHeight =
modalContent.getBoundingClientRect().bottom - capeList.getBoundingClientRect().top
capeListMaxHeight.value = `${Math.min(
CAPE_LIST_MAX_HEIGHT,
Math.max(0, Math.floor(availableHeight)),
)}px`
nextTick(() => forceCapeScrollCheck())
}
function refreshCapeListLayout() {
if (capeListLayoutFrame !== null) {
cancelAnimationFrame(capeListLayoutFrame)
}
capeListLayoutFrame = requestAnimationFrame(() => {
capeListLayoutFrame = null
updateCapeListLayout()
})
}
const visibleCapeList = ref<Cape[]>([])
const sortedCapes = computed(() => {
return [...(props.capes || [])].sort((a, b) => {
@@ -296,6 +164,32 @@ const sortedCapes = computed(() => {
})
})
function initVisibleCapeList() {
if (!props.capes || props.capes.length === 0) {
visibleCapeList.value = []
return
}
if (visibleCapeList.value.length === 0) {
if (selectedCape.value) {
const otherCape = getSortedCapeExcluding(selectedCape.value.id)
visibleCapeList.value = otherCape ? [selectedCape.value, otherCape] : [selectedCape.value]
} else {
visibleCapeList.value = getSortedCapes(2)
}
}
}
function getSortedCapes(count: number): Cape[] {
if (!sortedCapes.value || sortedCapes.value.length === 0) return []
return sortedCapes.value.slice(0, count)
}
function getSortedCapeExcluding(excludeId: string): Cape | undefined {
if (!sortedCapes.value || sortedCapes.value.length <= 1) return undefined
return sortedCapes.value.find((cape) => cape.id !== excludeId)
}
async function loadPreviewSkin() {
if (uploadedTextureUrl.value) {
previewSkin.value = uploadedTextureUrl.value.normalized
@@ -327,13 +221,9 @@ const disableSave = computed(
)
const saveTooltip = computed(() => {
if (isSaving.value) return formatMessage(messages.savingTooltip)
if (mode.value === 'new' && !uploadedTextureUrl.value) {
return formatMessage(messages.uploadSkinFirstTooltip)
}
if (mode.value === 'edit' && !hasEdits.value) {
return formatMessage(messages.makeEditFirstTooltip)
}
if (isSaving.value) return 'Saving...'
if (mode.value === 'new' && !uploadedTextureUrl.value) return 'Upload a skin first!'
if (mode.value === 'edit' && !hasEdits.value) return 'Make an edit to the skin first!'
return undefined
})
@@ -344,13 +234,11 @@ function resetState() {
previewSkin.value = ''
variant.value = 'CLASSIC'
selectedCape.value = undefined
visibleCapeList.value = []
shouldRestoreModal.value = false
isSaving.value = false
}
function handleModalHide() {
setTimeout(() => resetState(), 250)
}
async function show(e: MouseEvent, skin?: Skin) {
mode.value = skin ? 'edit' : 'new'
currentSkin.value = skin ?? null
@@ -361,11 +249,12 @@ async function show(e: MouseEvent, skin?: Skin) {
variant.value = 'CLASSIC'
selectedCape.value = undefined
}
visibleCapeList.value = []
initVisibleCapeList()
await loadPreviewSkin()
modal.value?.show(e)
nextTick(() => refreshCapeListLayout())
}
async function showNew(e: MouseEvent, skinTextureUrl: SkinTextureUrl) {
@@ -374,54 +263,98 @@ async function showNew(e: MouseEvent, skinTextureUrl: SkinTextureUrl) {
uploadedTextureUrl.value = skinTextureUrl
variant.value = await determineModelType(skinTextureUrl.original)
selectedCape.value = undefined
visibleCapeList.value = []
initVisibleCapeList()
await loadPreviewSkin()
modal.value?.show(e)
nextTick(() => refreshCapeListLayout())
}
async function setUploadedTexture(skinTextureUrl: SkinTextureUrl) {
async function restoreWithNewTexture(skinTextureUrl: SkinTextureUrl) {
uploadedTextureUrl.value = skinTextureUrl
await loadPreviewSkin()
nextTick(() => refreshCapeListLayout())
if (shouldRestoreModal.value) {
setTimeout(() => {
modal.value?.show()
shouldRestoreModal.value = false
}, 0)
}
}
function hide() {
modal.value?.hide()
setTimeout(() => resetState(), 250)
}
function selectCape(cape: Cape | undefined) {
if (cape && selectedCape.value?.id !== cape.id) {
const isInVisibleList = visibleCapeList.value.some((c) => c.id === cape.id)
if (!isInVisibleList && visibleCapeList.value.length > 0) {
visibleCapeList.value.splice(0, 1, cape)
if (visibleCapeList.value.length > 1 && visibleCapeList.value[1].id === cape.id) {
const otherCape = getSortedCapeExcluding(cape.id)
if (otherCape) {
visibleCapeList.value.splice(1, 1, otherCape)
}
}
}
}
selectedCape.value = cape
}
function openTextureFileBrowser() {
textureFileInput.value?.click()
function handleCapeSelected(cape: Cape | undefined) {
selectCape(cape)
if (shouldRestoreModal.value) {
setTimeout(() => {
modal.value?.show()
shouldRestoreModal.value = false
}, 0)
}
}
async function onTextureFileInputChange(e: Event) {
const files = (e.target as HTMLInputElement).files
const file = files?.[0]
if (!file) {
return
function handleCapeCancel() {
if (shouldRestoreModal.value) {
setTimeout(() => {
modal.value?.show()
shouldRestoreModal.value = false
}, 0)
}
}
try {
const originalSkinTexUrl = `data:image/png;base64,${arrayBufferToBase64(
await file.arrayBuffer(),
)}`
const skinTextureNormalized = await normalize_skin_texture(originalSkinTexUrl)
await setUploadedTexture({
original: originalSkinTexUrl,
normalized: `data:image/png;base64,${arrayBufferToBase64(skinTextureNormalized)}`,
})
} catch (error) {
handleError(error)
} finally {
if (textureFileInput.value) {
textureFileInput.value.value = ''
}
function openSelectCapeModal(e: MouseEvent) {
if (!selectCapeModal.value) return
shouldRestoreModal.value = true
modal.value?.hide()
setTimeout(() => {
selectCapeModal.value?.show(
e,
currentSkin.value?.texture_key,
selectedCape.value,
previewSkin.value,
variant.value,
)
}, 0)
}
function openUploadSkinModal(e: MouseEvent) {
shouldRestoreModal.value = true
modal.value?.hide()
emit('open-upload-modal', e)
}
function restoreModal() {
if (shouldRestoreModal.value) {
setTimeout(() => {
const fakeEvent = new MouseEvent('click')
modal.value?.show(fakeEvent)
shouldRestoreModal.value = false
}, 500)
}
}
@@ -437,32 +370,17 @@ async function save() {
textureUrl = currentSkin.value!.texture
}
await unequip_skin()
const bytes: Uint8Array = new Uint8Array(await (await fetch(textureUrl)).arrayBuffer())
if (mode.value === 'new') {
const addedSkin = await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
emit('saved', {
applied: true,
skin: addedSkin,
})
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
emit('saved')
} else {
const updatedSkin = await save_custom_skin(
currentSkin.value!,
bytes,
variant.value,
selectedCape.value,
!!uploadedTextureUrl.value && textureUrl !== currentSkin.value?.texture,
)
if (currentSkin.value?.is_equipped) {
await equip_skin(updatedSkin)
}
emit('saved', {
applied: !!currentSkin.value?.is_equipped,
skin: updatedSkin,
previousSkin: currentSkin.value!,
})
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
await remove_custom_skin(currentSkin.value!)
emit('saved')
}
hide()
@@ -475,53 +393,28 @@ async function save() {
watch([uploadedTextureUrl, currentSkin], async () => {
await loadPreviewSkin()
refreshCapeListLayout()
})
watch(
() => props.capes,
() => {
nextTick(() => refreshCapeListLayout())
initVisibleCapeList()
},
{ immediate: true },
)
watch(
capeListRef,
(capeList, _, onCleanup) => {
if (!capeList) return
const modalContent = capeList.closest('[data-modal-content]')
const resizeObserver = new ResizeObserver(() => refreshCapeListLayout())
if (modalContent instanceof HTMLElement) {
resizeObserver.observe(modalContent)
}
window.addEventListener('resize', refreshCapeListLayout, { passive: true })
refreshCapeListLayout()
onCleanup(() => {
resizeObserver.disconnect()
window.removeEventListener('resize', refreshCapeListLayout)
if (capeListLayoutFrame !== null) {
cancelAnimationFrame(capeListLayoutFrame)
capeListLayoutFrame = null
}
})
},
{ flush: 'post' },
)
const emit = defineEmits<{
(event: 'saved', options: { applied: boolean; skin?: Skin; previousSkin?: Skin }): void
(event: 'saved'): void
(event: 'deleted', skin: Skin): void
(event: 'open-upload-modal', mouseEvent: MouseEvent): void
}>()
defineExpose({
show,
showNew,
restoreWithNewTexture,
hide,
shouldRestoreModal,
restoreModal,
})
</script>
@@ -0,0 +1,141 @@
<script setup lang="ts">
import { CheckIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
CapeButton,
CapeLikeTextButton,
ScrollablePanel,
SkinPreviewRenderer,
} from '@modrinth/ui'
import { computed, ref, useTemplateRef } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import type { Cape, SkinModel } from '@/helpers/skins.ts'
const modal = useTemplateRef('modal')
const emit = defineEmits<{
(e: 'select', cape: Cape | undefined): void
(e: 'cancel'): void
}>()
const props = defineProps<{
capes: Cape[]
}>()
const sortedCapes = computed(() => {
return [...props.capes].sort((a, b) => {
const nameA = (a.name || '').toLowerCase()
const nameB = (b.name || '').toLowerCase()
return nameA.localeCompare(nameB)
})
})
const currentSkinId = ref<string | undefined>()
const currentSkinTexture = ref<string | undefined>()
const currentSkinVariant = ref<SkinModel>('CLASSIC')
const currentCapeTexture = computed<string | undefined>(() => currentCape.value?.texture)
const currentCape = ref<Cape | undefined>()
function show(
e: MouseEvent,
skinId?: string,
selected?: Cape,
skinTexture?: string,
variant?: SkinModel,
) {
currentSkinId.value = skinId
currentSkinTexture.value = skinTexture
currentSkinVariant.value = variant || 'CLASSIC'
currentCape.value = selected
modal.value?.show(e)
}
function select() {
emit('select', currentCape.value)
hide()
}
function hide() {
modal.value?.hide()
emit('cancel')
}
function updateSelectedCape(cape: Cape | undefined) {
currentCape.value = cape
}
function onModalHide() {
emit('cancel')
}
defineExpose({
show,
hide,
})
</script>
<template>
<ModalWrapper ref="modal" @on-hide="onModalHide">
<template #title>
<div class="flex flex-col">
<span class="text-lg font-extrabold text-heading">Change cape</span>
</div>
</template>
<div class="flex flex-col md:flex-row gap-6">
<div class="max-h-[25rem] h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
<SkinPreviewRenderer
v-if="currentSkinTexture"
:cape-src="currentCapeTexture"
:texture-src="currentSkinTexture"
:variant="currentSkinVariant"
:scale="1.4"
:fov="50"
:initial-rotation="Math.PI + Math.PI / 8"
class="h-full w-full"
/>
</div>
</div>
<div class="flex flex-col gap-4 w-full my-auto">
<ScrollablePanel class="max-h-[20rem] max-w-[30rem] mb-5 h-full">
<div class="flex flex-wrap gap-2 justify-center content-start overflow-y-auto h-full">
<CapeLikeTextButton
tooltip="No Cape"
:highlighted="!currentCape"
@click="updateSelectedCape(undefined)"
>
<template #icon>
<XIcon />
</template>
<span>None</span>
</CapeLikeTextButton>
<CapeButton
v-for="cape in sortedCapes"
:id="cape.id"
:key="cape.id"
:name="cape.name"
:texture="cape.texture"
:selected="currentCape?.id === cape.id"
@select="updateSelectedCape(cape)"
/>
</div>
</ScrollablePanel>
</div>
</div>
<div class="flex gap-2 items-center">
<ButtonStyled color="brand">
<button @click="select">
<CheckIcon />
Select
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="hide">
<XIcon />
Cancel
</button>
</ButtonStyled>
</div>
</ModalWrapper>
</template>
@@ -0,0 +1,141 @@
<template>
<ModalWrapper ref="modal" @on-hide="hide(true)">
<template #title>
<span class="text-lg font-extrabold text-contrast"> Upload skin texture </span>
</template>
<div class="relative">
<div
class="border-2 border-dashed border-highlight-gray rounded-xl h-[173px] flex flex-col items-center justify-center p-8 cursor-pointer bg-button-bg hover:bg-button-hover transition-colors relative"
@click="triggerFileInput"
>
<p class="mx-auto mb-0 text-primary font-bold text-lg text-center flex items-center gap-2">
<UploadIcon /> Select skin texture file
</p>
<p class="mx-auto mt-0 text-secondary text-sm text-center">
Drag and drop or click here to browse
</p>
<input
ref="fileInput"
type="file"
accept="image/png"
class="hidden"
@change="handleInputFileChange"
/>
</div>
</div>
</ModalWrapper>
</template>
<script setup lang="ts">
import { UploadIcon } from '@modrinth/assets'
import { injectNotificationManager } from '@modrinth/ui'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { onBeforeUnmount, ref, watch } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { get_dragged_skin_data } from '@/helpers/skins'
const { addNotification } = injectNotificationManager()
const modal = ref()
const fileInput = ref<HTMLInputElement>()
const unlisten = ref<() => void>()
const modalVisible = ref(false)
const emit = defineEmits<{
(e: 'uploaded', data: ArrayBuffer): void
(e: 'canceled'): void
}>()
function show(e?: MouseEvent) {
modal.value?.show(e)
modalVisible.value = true
setupDragDropListener()
}
function hide(emitCanceled = false) {
modal.value?.hide()
modalVisible.value = false
cleanupDragDropListener()
resetState()
if (emitCanceled) {
emit('canceled')
}
}
function resetState() {
if (fileInput.value) fileInput.value.value = ''
}
function triggerFileInput() {
fileInput.value?.click()
}
async function handleInputFileChange(e: Event) {
const files = (e.target as HTMLInputElement).files
if (!files || files.length === 0) {
return
}
const file = files[0]
const buffer = await file.arrayBuffer()
await processData(buffer)
}
async function setupDragDropListener() {
try {
if (modalVisible.value) {
await cleanupDragDropListener()
unlisten.value = await getCurrentWebview().onDragDropEvent(async (event) => {
if (event.payload.type !== 'drop') {
return
}
if (!event.payload.paths || event.payload.paths.length === 0) {
return
}
const filePath = event.payload.paths[0]
try {
const data = await get_dragged_skin_data(filePath)
await processData(data.buffer)
} catch (error) {
addNotification({
title: 'Error processing file',
text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
type: 'error',
})
}
})
}
} catch (error) {
console.error('Failed to set up drag and drop listener:', error)
}
}
async function cleanupDragDropListener() {
if (unlisten.value) {
unlisten.value()
unlisten.value = undefined
}
}
async function processData(buffer: ArrayBuffer) {
emit('uploaded', buffer)
hide()
}
watch(modalVisible, (isVisible) => {
if (isVisible) {
setupDragDropListener()
} else {
cleanupDragDropListener()
}
})
onBeforeUnmount(() => {
cleanupDragDropListener()
})
defineExpose({ show, hide })
</script>
@@ -1,410 +0,0 @@
<script setup lang="ts">
import { DropdownIcon, EditIcon, PlusIcon, TrashIcon } from '@modrinth/assets'
import {
Accordion,
ButtonStyled,
commonMessages,
defineMessages,
SkinButton,
SkinLikeTextButton,
useScrollViewport,
useVIntl,
} from '@modrinth/ui'
import { useElementSize, useWindowSize } from '@vueuse/core'
import { computed, nextTick, onUnmounted, ref, useTemplateRef, watch } from 'vue'
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
import type { Skin } from '@/helpers/skins.ts'
type SkinSectionKind = 'saved' | 'default'
type SkinLikeTextButtonExpose = {
getRootElement: () => HTMLElement | null | undefined
}
type AddSkinButtonRef = SkinLikeTextButtonExpose | SkinLikeTextButtonExpose[]
interface DefaultSkinSection {
title: string
skins: Skin[]
}
interface SkinSection {
key: string
title: string
kind: SkinSectionKind
skins: Skin[]
}
interface VirtualSkinSection {
section: SkinSection
top: number
index: number
}
const SKIN_CARD_ASPECT_WIDTH = 31
const SKIN_CARD_ASPECT_HEIGHT = 40
const SKIN_GRID_GAP = 12
const SKIN_SECTION_FIRST_SPACING = 4
const SKIN_SECTION_SPACING = 24
const SKIN_SECTION_HEADER_HEIGHT = 28
const SKIN_SECTION_CONTENT_SPACING = 8
const SKIN_SECTION_OVERSCAN = 900
const FALLBACK_CARD_WIDTH = 220
const messages = defineMessages({
savedSkinsSection: {
id: 'app.skins.section.saved-skins',
defaultMessage: 'Saved skins',
},
addSkinButton: {
id: 'app.skins.add-button',
defaultMessage: 'Add skin',
},
dragAndDropSubtitle: {
id: 'app.skins.add-button.drag-and-drop',
defaultMessage: 'Drag and drop',
},
editSkinButton: {
id: 'app.skins.edit-button',
defaultMessage: 'Edit skin',
},
deleteSkinButton: {
id: 'app.skins.delete-button',
defaultMessage: 'Delete skin',
},
})
const props = defineProps<{
savedSkins: Skin[]
defaultSkinSections: DefaultSkinSection[]
getBakedSkinTextures: (skin: Skin) => RenderResult | undefined
isSkinSelected: (skin: Skin) => boolean
isSkinActive: (skin: Skin) => boolean
isAddSkinButtonDragActive: boolean
}>()
const emit = defineEmits<{
select: [skin: Skin]
edit: [skin: Skin, event: MouseEvent]
delete: [skin: Skin]
'add-skin': []
'add-skin-dragenter': [event: DragEvent]
'add-skin-dragover': [event: DragEvent]
'add-skin-dragleave': [event: DragEvent]
'add-skin-drop': [event: DragEvent]
}>()
const addSkinButton = useTemplateRef<AddSkinButtonRef>('addSkinButton')
const { formatMessage } = useVIntl()
const { listContainer, relativeScrollTop, scrollContainer, viewportHeight } = useScrollViewport()
const openSectionKeys = ref<Set<string>>(new Set())
const hasSettledInitialLayout = ref(false)
const knownSectionKeys = new Set<string>()
let enableLayoutTransitionsFrame: number | null = null
let isEnableLayoutTransitionsScheduled = false
let isUnmounted = false
const { width: listWidth } = useElementSize(listContainer)
const { width: windowWidth } = useWindowSize()
const columnCount = computed(() => {
if (windowWidth.value >= 2050) {
return 6
}
if (windowWidth.value >= 1750) {
return 5
}
if (windowWidth.value >= 1300) {
return 4
}
return 3
})
const cardWidth = computed(() => {
if (listWidth.value <= 0) {
return FALLBACK_CARD_WIDTH
}
const gapsWidth = (columnCount.value - 1) * SKIN_GRID_GAP
return Math.max(0, (listWidth.value - gapsWidth) / columnCount.value)
})
const cardHeight = computed(
() => (cardWidth.value * SKIN_CARD_ASPECT_HEIGHT) / SKIN_CARD_ASPECT_WIDTH,
)
const sections = computed<SkinSection[]>(() => [
{
key: 'saved-skins',
title: formatMessage(messages.savedSkinsSection),
kind: 'saved',
skins: props.savedSkins,
},
...props.defaultSkinSections.map((section) => ({
key: defaultSkinSectionKey(section.title),
title: section.title,
kind: 'default' as const,
skins: section.skins,
})),
])
const sectionLayouts = computed(() => {
const layouts: Array<{ section: SkinSection; top: number; height: number; index: number }> = []
let top = 0
sections.value.forEach((section, index) => {
const height = getSectionHeightEstimate(section, index)
layouts.push({ section, top, height, index })
top += height
})
return layouts
})
const totalHeight = computed(() => {
const lastSection = sectionLayouts.value[sectionLayouts.value.length - 1]
return lastSection ? lastSection.top + lastSection.height : 0
})
const visibleSections = computed<VirtualSkinSection[]>(() => {
if (!listContainer.value || !scrollContainer.value) {
return sectionLayouts.value.slice(0, 4)
}
const viewportStart = Math.max(0, relativeScrollTop.value - SKIN_SECTION_OVERSCAN)
const viewportEnd = relativeScrollTop.value + viewportHeight.value + SKIN_SECTION_OVERSCAN
return sectionLayouts.value
.filter((layout) => layout.top + layout.height >= viewportStart && layout.top <= viewportEnd)
.map(({ section, top, index }) => ({ section, top, index }))
})
watch(
sections,
(nextSections) => {
const sectionKeys = new Set(nextSections.map((section) => section.key))
const openKeys = new Set(openSectionKeys.value)
for (const section of nextSections) {
if (!knownSectionKeys.has(section.key)) {
knownSectionKeys.add(section.key)
openKeys.add(section.key)
}
}
for (const key of knownSectionKeys) {
if (!sectionKeys.has(key)) {
knownSectionKeys.delete(key)
openKeys.delete(key)
}
}
openSectionKeys.value = openKeys
},
{ immediate: true },
)
watch(
listWidth,
(width) => {
if (
typeof window === 'undefined' ||
width <= 0 ||
hasSettledInitialLayout.value ||
isEnableLayoutTransitionsScheduled
) {
return
}
isEnableLayoutTransitionsScheduled = true
void nextTick(() => {
if (isUnmounted) return
enableLayoutTransitionsFrame = window.requestAnimationFrame(() => {
if (isUnmounted) return
enableLayoutTransitionsFrame = window.requestAnimationFrame(() => {
if (isUnmounted) return
hasSettledInitialLayout.value = true
enableLayoutTransitionsFrame = null
isEnableLayoutTransitionsScheduled = false
})
})
})
},
{ immediate: true },
)
onUnmounted(() => {
isUnmounted = true
if (enableLayoutTransitionsFrame !== null) {
window.cancelAnimationFrame(enableLayoutTransitionsFrame)
}
})
function defaultSkinSectionKey(title: string) {
return `default-skins-${title}`
}
function skinKey(skin: Skin, prefix: string) {
return `${prefix}-${skin.source}-${skin.texture_key}-${skin.variant}-${skin.cape_id ?? 'no-cape'}`
}
function isSectionOpen(key: string) {
return openSectionKeys.value.has(key)
}
function setSectionOpen(key: string, open: boolean) {
const openKeys = new Set(openSectionKeys.value)
if (open) {
openKeys.add(key)
} else {
openKeys.delete(key)
}
openSectionKeys.value = openKeys
}
function getSectionHeightEstimate(section: SkinSection, index: number) {
const spacing = index === 0 ? SKIN_SECTION_FIRST_SPACING : SKIN_SECTION_SPACING
if (!isSectionOpen(section.key)) {
return spacing + SKIN_SECTION_HEADER_HEIGHT
}
const cardCount = section.kind === 'saved' ? section.skins.length + 1 : section.skins.length
const rowCount = Math.ceil(cardCount / columnCount.value)
const gridHeight = rowCount * cardHeight.value + Math.max(0, rowCount - 1) * SKIN_GRID_GAP
return spacing + SKIN_SECTION_HEADER_HEIGHT + SKIN_SECTION_CONTENT_SPACING + gridHeight
}
function getAddSkinButtonElement() {
const button = Array.isArray(addSkinButton.value)
? addSkinButton.value.find((candidate) => candidate.getRootElement())
: addSkinButton.value
return button?.getRootElement()
}
defineExpose({ getAddSkinButtonElement })
</script>
<template>
<div
ref="listContainer"
class="relative w-full"
:style="{ height: `${totalHeight}px`, overflowAnchor: 'none' }"
>
<div
v-for="{ section, top, index } in visibleSections"
:key="section.key"
class="absolute inset-x-0"
:class="[
index === 0 ? 'pt-1' : 'pt-6',
hasSettledInitialLayout
? 'transition-transform duration-300 ease-in-out will-change-transform motion-reduce:transition-none'
: '',
]"
:style="{ transform: `translateY(${top}px)` }"
>
<Accordion
button-class="group flex w-full items-center gap-[6px] bg-transparent m-0 p-0 border-none cursor-pointer text-left"
content-class="pt-2"
:open-by-default="isSectionOpen(section.key)"
@on-open="setSectionOpen(section.key, true)"
@on-close="setSectionOpen(section.key, false)"
>
<template #title>
{{ section.title }}
</template>
<template #button="{ open }">
<DropdownIcon
class="size-6 shrink-0 text-primary transition-transform duration-300"
:class="{ 'rotate-180': open }"
/>
<span class="min-w-0 text-xl font-semibold leading-7 text-primary">
{{ section.title }}
</span>
</template>
<div
v-if="section.kind === 'saved'"
class="grid w-full grid-cols-3 gap-3 min-[1300px]:grid-cols-4 min-[1750px]:grid-cols-5 min-[2050px]:grid-cols-6"
>
<SkinLikeTextButton
ref="addSkinButton"
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
dropzone
:drag-active="isAddSkinButtonDragActive"
@click="emit('add-skin')"
@dragenter="emit('add-skin-dragenter', $event)"
@dragover="emit('add-skin-dragover', $event)"
@dragleave="emit('add-skin-dragleave', $event)"
@drop="emit('add-skin-drop', $event)"
>
<template #icon>
<PlusIcon class="size-8" />
</template>
{{ formatMessage(messages.addSkinButton) }}
<template #subtitle>{{ formatMessage(messages.dragAndDropSubtitle) }}</template>
</SkinLikeTextButton>
<SkinButton
v-for="skin in section.skins"
:key="skinKey(skin, 'saved-skin')"
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
:selected="isSkinSelected(skin)"
:active="isSkinActive(skin)"
@select="emit('select', skin)"
>
<template #overlay-buttons>
<ButtonStyled color="brand">
<button
:aria-label="formatMessage(messages.editSkinButton)"
class="pointer-events-auto"
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
>
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-show="!skin.is_equipped" circular color="red">
<button
v-tooltip="formatMessage(messages.deleteSkinButton)"
:aria-label="formatMessage(messages.deleteSkinButton)"
class="!rounded-[100%] pointer-events-auto"
@click.stop="emit('delete', skin)"
>
<TrashIcon />
</button>
</ButtonStyled>
</template>
</SkinButton>
</div>
<div
v-else
class="grid w-full grid-cols-3 gap-3 min-[1300px]:grid-cols-4 min-[1750px]:grid-cols-5 min-[2050px]:grid-cols-6"
>
<SkinButton
v-for="skin in section.skins"
:key="skinKey(skin, section.key)"
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
:selected="isSkinSelected(skin)"
:active="isSkinActive(skin)"
:tooltip="skin.name"
@select="emit('select', skin)"
/>
</div>
</Accordion>
</div>
</div>
</template>
@@ -244,7 +244,7 @@ const messages = defineMessages({
</TagItem>
<div
v-if="world.type === 'singleplayer'"
class="text-sm text-secondary flex items-center gap-1 font-semibold flex-nowrap whitespace-nowrap"
class="text-sm text-secondary flex items-center gap-1 font-semibold"
>
<UserIcon
aria-hidden="true"
@@ -0,0 +1,121 @@
import { computed, onUnmounted, ref } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
interface ThemeStore {
toggleSidebar: boolean
$subscribe: (callback: () => void) => () => void
}
interface IntercomBubblePosition {
horizontalPadding: number
verticalPadding: number
}
const APP_LEFT_NAV_WIDTH = '4rem'
const APP_SIDEBAR_WIDTH = 300
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
const INTERCOM_BUBBLE_WIDTH = 72
const INTERCOM_BUBBLE_RIGHT_VAR = '--app-support-launcher-right'
const INTERCOM_BUBBLE_BOTTOM_VAR = '--app-support-launcher-bottom'
export function useIntercomPositioning({
route,
themeStore,
}: {
route: RouteLocationNormalizedLoaded
themeStore: ThemeStore
}) {
const sidebarToggled = ref(true)
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
sidebarToggled.value = !themeStore.toggleSidebar
})
onUnmounted(unsubscribeSidebarToggle)
const forceSidebar = computed(
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const defaultIntercomBubbleHorizontalPadding = computed(() =>
sidebarVisible.value
? APP_SIDEBAR_WIDTH + INTERCOM_BUBBLE_DEFAULT_PADDING
: INTERCOM_BUBBLE_DEFAULT_PADDING,
)
const intercomBubbleRequestedHorizontalPadding = ref<number | null>(null)
const intercomBubbleHorizontalPadding = computed(
() =>
intercomBubbleRequestedHorizontalPadding.value ??
defaultIntercomBubbleHorizontalPadding.value,
)
const intercomBubbleVerticalClearance = ref<number | null>(null)
const intercomBubblePosition = computed(() => ({
horizontalPadding: intercomBubbleHorizontalPadding.value,
verticalPadding: intercomBubbleVerticalClearance.value ?? INTERCOM_BUBBLE_DEFAULT_PADDING,
}))
const intercomBubbleHorizontalPaddingRequests = new Map<symbol, number>()
const intercomBubbleClearanceRequests = new Map<symbol, number>()
function requestIntercomBubbleHorizontalPadding(id: symbol, padding: number | null) {
if (padding === null) {
intercomBubbleHorizontalPaddingRequests.delete(id)
} else {
intercomBubbleHorizontalPaddingRequests.set(id, padding)
}
intercomBubbleRequestedHorizontalPadding.value =
intercomBubbleHorizontalPaddingRequests.size > 0
? Math.max(...intercomBubbleHorizontalPaddingRequests.values())
: null
}
function requestIntercomBubbleVerticalClearance(id: symbol, clearance: number | null) {
if (clearance === null) {
intercomBubbleClearanceRequests.delete(id)
} else {
intercomBubbleClearanceRequests.set(id, clearance)
}
intercomBubbleVerticalClearance.value =
intercomBubbleClearanceRequests.size > 0
? Math.max(...intercomBubbleClearanceRequests.values())
: null
}
function updateIntercomBubbleStyles({
horizontalPadding,
verticalPadding,
}: IntercomBubblePosition) {
if (typeof document === 'undefined') return
document.documentElement.style.setProperty(INTERCOM_BUBBLE_RIGHT_VAR, `${horizontalPadding}px`)
document.documentElement.style.setProperty(INTERCOM_BUBBLE_BOTTOM_VAR, `${verticalPadding}px`)
}
function clearIntercomBubbleStyles() {
if (typeof document === 'undefined') return
document.documentElement.style.removeProperty(INTERCOM_BUBBLE_RIGHT_VAR)
document.documentElement.style.removeProperty(INTERCOM_BUBBLE_BOTTOM_VAR)
}
return {
sidebarToggled,
forceSidebar,
sidebarVisible,
intercomBubblePosition,
updateIntercomBubbleStyles,
clearIntercomBubbleStyles,
pageContext: {
floatingActionBarOffsets: {
left: ref(APP_LEFT_NAV_WIDTH),
right: computed(() => (sidebarVisible.value ? `${APP_SIDEBAR_WIDTH}px` : '0px')),
},
intercomBubble: {
width: ref(INTERCOM_BUBBLE_WIDTH),
horizontalPadding: intercomBubbleHorizontalPadding,
requestHorizontalPadding: requestIntercomBubbleHorizontalPadding,
requestVerticalClearance: requestIntercomBubbleVerticalClearance,
},
},
}
}
@@ -8,7 +8,7 @@ interface LogEntry {
filename: string
name?: string
log_type: string
output?: string | null
stdout?: string
age?: number
live?: boolean
}
@@ -50,12 +50,12 @@ async function getHistoricalLogs(profilePathId: string, instancePath: string): P
const entry = getOrCreate(profilePathId)
if (entry.logList) return entry.logList
const logs: LogEntry[] = await get_logs(instancePath, true)
const logs: LogEntry[] = await get_logs(instancePath, false)
entry.logList = logs
for (const log of logs) {
if (log.output) {
entry.historicalCache.set(log.filename, log.output)
if (log.stdout && log.stdout !== '') {
entry.historicalCache.set(log.filename, log.stdout)
}
}
@@ -1,52 +0,0 @@
import { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics'
import { test_jre } from '@/helpers/jre.js'
export default function useJavaTest() {
const testingJava = ref(false)
const javaTestResult = ref<boolean | null>(null)
let testDebounceTimer: ReturnType<typeof setTimeout> | null = null
async function runJavaTest(path: string, version: number, track = true) {
if (testDebounceTimer) {
clearTimeout(testDebounceTimer)
testDebounceTimer = null
}
if (!path) {
javaTestResult.value = null
return
}
testingJava.value = true
try {
javaTestResult.value = await test_jre(path, version)
} catch {
javaTestResult.value = false
}
testingJava.value = false
if (track) {
trackEvent('JavaTest', { path, success: javaTestResult.value })
}
}
function testJavaInstallationDebounced(path: string, version: number, delay = 600) {
if (testDebounceTimer) clearTimeout(testDebounceTimer)
if (!path) {
javaTestResult.value = null
return
}
testDebounceTimer = setTimeout(() => runJavaTest(path, version, false), delay)
}
async function testJavaInstallation(path: string, version: number, track = false) {
await runJavaTest(path, version, track)
}
return {
testingJava,
javaTestResult,
testJavaInstallationDebounced,
testJavaInstallation,
}
}
@@ -1,100 +0,0 @@
import type {
ContentItem,
ContentModpackCardCategory,
ContentModpackCardProject,
ContentModpackCardVersion,
ContentOwner,
} from '@modrinth/ui'
import {
get_content_items,
get_linked_modpack_info,
type LinkedModpackInfo,
} from '@/helpers/profile'
import { get_categories } from '@/helpers/tags.js'
import type { CacheBehaviour } from '@/helpers/types'
export type InstanceContentData = {
path: string
contentItems: ContentItem[] | null
modpack: InstanceContentModpackData | null
}
export type InstanceContentModpackData = {
project: ContentModpackCardProject
version: ContentModpackCardVersion
owner: ContentOwner | null
categories: ContentModpackCardCategory[]
hasUpdate: boolean
updateVersionId: string | null
}
export async function loadInstanceContentData(
path: string,
cacheBehaviour?: CacheBehaviour,
onError?: (error: Error) => unknown,
): Promise<InstanceContentData> {
const [contentItems, modpackInfo, allCategories] = await Promise.all([
get_content_items(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)),
get_linked_modpack_info(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)),
get_categories().catch((error) => handleLoadError(error, onError)),
])
return {
path,
contentItems: (contentItems as ContentItem[] | null | undefined) ?? null,
modpack: normalizeLinkedModpackInfo(
modpackInfo as LinkedModpackInfo | null | undefined,
allCategories as ContentModpackCardCategory[] | null | undefined,
),
}
}
function handleLoadError(error: unknown, onError?: (error: Error) => unknown) {
onError?.(error as Error)
return null
}
function normalizeLinkedModpackInfo(
modpackInfo: LinkedModpackInfo | null | undefined,
allCategories: ContentModpackCardCategory[] | null | undefined,
): InstanceContentModpackData | null {
if (!modpackInfo) return null
return {
project: {
...modpackInfo.project,
slug: modpackInfo.project.slug ?? modpackInfo.project.id,
icon_url: modpackInfo.project.icon_url ?? undefined,
},
version: {
...modpackInfo.version,
date_published: modpackInfo.version.date_published.toString(),
},
owner: modpackInfo.owner
? {
...modpackInfo.owner,
avatar_url: modpackInfo.owner.avatar_url ?? undefined,
}
: null,
categories: resolveLinkedModpackCategories(modpackInfo, allCategories),
hasUpdate: modpackInfo.has_update,
updateVersionId: modpackInfo.update_version_id,
}
}
function resolveLinkedModpackCategories(
modpackInfo: LinkedModpackInfo,
allCategories: ContentModpackCardCategory[] | null | undefined,
) {
if (!allCategories || !modpackInfo.project.categories) return []
const seen = new Set<string>()
return allCategories.filter((category) => {
if (modpackInfo.project.categories.includes(category.name) && !seen.has(category.name)) {
seen.add(category.name)
return true
}
return false
})
}
+3 -2
View File
@@ -6,11 +6,12 @@
import { invoke } from '@tauri-apps/api/core'
/*
A log is a struct containing the filename string and optional output, as follows:
A log is a struct containing the filename string, stdout, and stderr, as follows:
pub struct Logs {
pub filename: String,
pub output: Option<String>,
pub stdout: String,
pub stderr: String,
}
*/
+1 -8
View File
@@ -147,16 +147,9 @@ export async function get_mod_full_path(path: string, projectPath: string): Prom
return await invoke('plugin:profile|profile_get_mod_full_path', { path, projectPath })
}
export interface JavaVersion {
parsed_version: number
version: string
architecture: string
path: string
}
// Get optimal java version from profile
// Returns a java version
export async function get_optimal_jre_key(path: string): Promise<JavaVersion | null> {
export async function get_optimal_jre_key(path: string): Promise<string | null> {
return await invoke('plugin:profile|profile_get_optimal_jre_key', { path })
}
@@ -3,8 +3,9 @@ import {
applyCapeTexture,
createTransparentTexture,
disposeCaches,
loadTexture,
setupSkinModel,
} from '@modrinth/ui'
} from '@modrinth/utils'
import * as THREE from 'three'
import { reactive } from 'vue'
@@ -28,7 +29,6 @@ class BatchSkinRenderer {
private scene: THREE.Scene | null = null
private camera: THREE.PerspectiveCamera | null = null
private currentModel: THREE.Group | null = null
private transparentTexture: THREE.Texture | null = null
private readonly width: number
private readonly height: number
@@ -52,7 +52,6 @@ class BatchSkinRenderer {
})
this.renderer.outputColorSpace = THREE.SRGBColorSpace
this.renderer.shadowMap.enabled = false
this.renderer.toneMapping = THREE.NoToneMapping
this.renderer.toneMappingExposure = 10.0
this.renderer.setClearColor(0x000000, 0)
@@ -63,7 +62,7 @@ class BatchSkinRenderer {
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
directionalLight.castShadow = false
directionalLight.castShadow = true
directionalLight.position.set(2, 4, 3)
this.scene.add(ambientLight)
this.scene.add(directionalLight)
@@ -113,19 +112,9 @@ class BatchSkinRenderer {
this.renderer.render(this.scene, this.camera)
return await new Promise<Blob>((resolve, reject) => {
this.renderer!.domElement.toBlob(
(blob) => {
if (blob) {
resolve(blob)
} else {
reject(new Error('Failed to create blob from rendered canvas'))
}
},
'image/webp',
0.9,
)
})
const dataUrl = this.renderer.domElement.toDataURL('image/webp', 0.9)
const response = await fetch(dataUrl)
return await response.blob()
}
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
@@ -133,10 +122,14 @@ class BatchSkinRenderer {
throw new Error('Renderer not initialized')
}
const { model } = await setupSkinModel(modelUrl, textureUrl, capeUrl)
const { model } = await setupSkinModel(modelUrl, textureUrl)
if (!capeUrl) {
applyCapeTexture(model, null, this.getTransparentTexture())
if (capeUrl) {
const capeTexture = await loadTexture(capeUrl)
applyCapeTexture(model, capeTexture)
} else {
const transparentTexture = createTransparentTexture()
applyCapeTexture(model, null, transparentTexture)
}
const group = new THREE.Group()
@@ -148,38 +141,39 @@ class BatchSkinRenderer {
this.currentModel = group
}
private getTransparentTexture(): THREE.Texture {
if (!this.transparentTexture) {
this.transparentTexture = createTransparentTexture()
private clearScene(): void {
if (!this.scene) return
while (this.scene.children.length > 0) {
const child = this.scene.children[0]
this.scene.remove(child)
if (child instanceof THREE.Mesh) {
if (child.geometry) child.geometry.dispose()
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach((material) => material.dispose())
} else {
child.material.dispose()
}
}
}
}
return this.transparentTexture
}
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
directionalLight.castShadow = true
directionalLight.position.set(2, 4, 3)
this.scene.add(ambientLight)
this.scene.add(directionalLight)
private clearScene(): void {
if (!this.scene || !this.currentModel) return
this.scene.remove(this.currentModel)
this.currentModel.clear()
this.currentModel = null
}
public dispose(): void {
this.clearScene()
if (this.transparentTexture) {
this.transparentTexture.dispose()
this.transparentTexture = null
}
if (this.renderer) {
this.renderer.dispose()
}
this.renderer = null
this.scene = null
this.camera = null
disposeCaches()
}
}
@@ -200,9 +194,6 @@ export const headBlobUrlMap = reactive(new Map<string, string>())
const DEBUG_MODE = false
let sharedRenderer: BatchSkinRenderer | null = null
let latestPreviewGeneration = 0
let previewGenerationQueue: Promise<void> = Promise.resolve()
function getSharedRenderer(): BatchSkinRenderer {
if (!sharedRenderer) {
sharedRenderer = new BatchSkinRenderer()
@@ -365,27 +356,7 @@ export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
return await generateHeadRender(skin)
}
export function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
const generation = ++latestPreviewGeneration
const skinsSnapshot = [...skins]
const capesSnapshot = [...capes]
const generationPromise = previewGenerationQueue.then(() =>
generateSkinPreviewsForGeneration(skinsSnapshot, capesSnapshot, generation),
)
previewGenerationQueue = generationPromise.catch(() => {})
return generationPromise
}
async function generateSkinPreviewsForGeneration(
skins: Skin[],
capes: Cape[],
generation: number,
): Promise<void> {
const isCurrentGeneration = () => generation === latestPreviewGeneration
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
try {
const skinKeys = skins.map(
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
@@ -397,8 +368,6 @@ async function generateSkinPreviewsForGeneration(
headStorage.batchRetrieve(headKeys),
])
if (!isCurrentGeneration()) return
for (let i = 0; i < skins.length; i++) {
const skinKey = skinKeys[i]
const headKey = headKeys[i]
@@ -419,8 +388,6 @@ async function generateSkinPreviewsForGeneration(
}
for (const skin of skins) {
if (!isCurrentGeneration()) return
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
if (skinBlobUrlMap.has(key)) {
@@ -452,8 +419,6 @@ async function generateSkinPreviewsForGeneration(
cape?.texture,
)
if (!isCurrentGeneration()) return
const renderResult: RenderResult = {
forwards: URL.createObjectURL(rawRenderResult.forwards),
backwards: URL.createObjectURL(rawRenderResult.backwards),
@@ -474,12 +439,9 @@ async function generateSkinPreviewsForGeneration(
}
} finally {
disposeSharedRenderer()
await cleanupUnusedPreviews(skins)
if (isCurrentGeneration()) {
await cleanupUnusedPreviews(skins)
await skinPreviewStorage.debugCalculateStorage()
await headStorage.debugCalculateStorage()
}
await skinPreviewStorage.debugCalculateStorage()
await headStorage.debugCalculateStorage()
}
}
+10 -30
View File
@@ -5,6 +5,7 @@ export interface Cape {
id: string
name: string
texture: string
is_default: boolean
is_equipped: boolean
}
@@ -14,7 +15,6 @@ export type SkinSource = 'default' | 'custom_external' | 'custom'
export interface Skin {
texture_key: string
name?: string
section?: string
variant: SkinModel
cape_id?: string
texture: string
@@ -121,11 +121,17 @@ export async function get_available_skins(): Promise<Skin[]> {
export async function add_and_equip_custom_skin(
textureBlob: Uint8Array,
variant: SkinModel,
cape?: Cape,
): Promise<Skin> {
return await invoke('plugin:minecraft-skins|add_and_equip_custom_skin', {
capeOverride?: Cape,
): Promise<void> {
await invoke('plugin:minecraft-skins|add_and_equip_custom_skin', {
textureBlob,
variant,
capeOverride,
})
}
export async function set_default_cape(cape?: Cape): Promise<void> {
await invoke('plugin:minecraft-skins|set_default_cape', {
cape,
})
}
@@ -142,22 +148,6 @@ export async function remove_custom_skin(skin: Skin): Promise<void> {
})
}
export async function save_custom_skin(
skin: Skin,
textureBlob: Uint8Array,
variant: SkinModel,
cape: Cape | undefined,
replaceTexture: boolean,
): Promise<Skin> {
return await invoke('plugin:minecraft-skins|save_custom_skin', {
skin,
textureBlob,
variant,
cape,
replaceTexture,
})
}
export async function get_normalized_skin_texture(skin: Skin): Promise<string> {
const data = await normalize_skin_texture(skin.texture)
const base64 = arrayBufferToBase64(data)
@@ -172,16 +162,6 @@ export async function unequip_skin(): Promise<void> {
await invoke('plugin:minecraft-skins|unequip_skin')
}
export async function flush_pending_skin_change(): Promise<void> {
await invoke('plugin:minecraft-skins|flush_pending_skin_change')
}
export async function flush_pending_skin_change_for_profile(profileId: string): Promise<void> {
await invoke('plugin:minecraft-skins|flush_pending_skin_change_for_profile', {
profileId,
})
}
export async function get_dragged_skin_data(path: string): Promise<Uint8Array> {
const data = await invoke('plugin:minecraft-skins|get_dragged_skin_data', { path })
return new Uint8Array(data)
@@ -5,15 +5,6 @@
"app.action-bar.downloads": {
"message": "التنزيلات"
},
"app.action-bar.offline": {
"message": "غير متصل"
},
"app.appearance-settings.default-landing-page.home": {
"message": "صفحة الرئيسية"
},
"app.appearance-settings.default-landing-page.library": {
"message": "مكتبة"
},
"app.auth-servers.unreachable.body": {
"message": "قد تكون خوادم مصادقة ماينكرافت معطلة حاليًا. تحقق من اتصالك بالإنترنت وحاول مرة أخرى لاحقًا."
},
@@ -575,12 +566,6 @@
"instance.worlds.world_in_use": {
"message": "العالم قيد الاستخدام"
},
"minecraft-account.add-account": {
"message": "إضافة حساب"
},
"minecraft-account.select-account": {
"message": "اختيار الحساب"
},
"search.filter.locked.instance": {
"message": "مقدَّم من النسخة"
},
+8 -248
View File
@@ -1,115 +1,10 @@
{
"app.action-bar.downloading-java": {
"message": "Stahování Javy {version}"
},
"app.action-bar.downloads": {
"message": "Stažené Soubory"
},
"app.action-bar.hide-more-running-instances": {
"message": "Skrýt více spuštěných instancí"
},
"app.action-bar.make-primary-instance": {
"message": "Použít jako primární instanci"
},
"app.action-bar.no-instances-running": {
"message": "Žádné spuštěné instance"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Primární instance"
},
"app.action-bar.show-more-running-instances": {
"message": "Zobrazit další spuštěné instance"
},
"app.action-bar.stop-instance": {
"message": "Ukončit instanci"
},
"app.action-bar.view-active-downloads": {
"message": "Zobrazit probíhající stahování"
},
"app.action-bar.view-instance": {
"message": "Zobrazit instanci"
},
"app.action-bar.view-logs": {
"message": "Zobrazit záznamy"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Umožňuje pokročilé vykreslování, jako jsou efekty rozostření, které mohou způsobit problémy s výkonem bez hardwarového vykreslení."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Pokročilé vykreslování"
},
"app.appearance-settings.color-theme.description": {
"message": "Vyberte si váš preferovaný motiv pro aplikaci Modrinth."
},
"app.appearance-settings.color-theme.title": {
"message": "Barevný motiv"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Změňte stránku, na které se spouštěč otevře."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Domov"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Knihovna"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Výchozí úvodní stránka"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Zakáže zobrazení jmenovky nad vaší postavou na stránce skinů."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Skrýt jmenovku"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "V sekci „Vrátit se do hry“ na úvodní stránce jsou uvedeny nejnovější světy."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Skočit zpět do světů"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimalizovat spouštěč při spuštění procesu Minecraftu."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimalizovat spouštěč"
},
"app.appearance-settings.native-decorations.description": {
"message": "Použít systémový rámeček okna (vyžaduje restart aplikace)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Nativní dekorace"
},
"app.appearance-settings.select-option": {
"message": "Vyberte možnost"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Umožňí přepínat zobrazení postranního panelu."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Přepnout postranní panel"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Pokud se pokusíte nainstalovat soubor Modrinth Pack (.mrpack), který není hostován na Modrinth, zajistíme, abyste si před instalací uvědomili související rizika."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Upozorni mě před instalací neznámých modpacků"
},
"app.auth-servers.unreachable.body": {
"message": "Autořizační servery Minecraftu aktuálně nejsou k dispozici. Zkontrolujte si své připojení k internetu a zkuste to znovu později."
},
"app.auth-servers.unreachable.header": {
"message": "Připojení k autorizačním serverům se nezdařilo"
},
"app.browse.add-servers-to-instance": {
"message": "Přidat server do instance"
},
"app.browse.add-to-an-instance": {
"message": "Přidat do instalace"
},
"app.browse.add-to-instance": {
"message": "Přidat do instalace"
},
@@ -122,33 +17,12 @@
"app.browse.already-added": {
"message": "Už přidáno"
},
"app.browse.back-to-instance": {
"message": "Zpět k instanci"
},
"app.browse.discover-content": {
"message": "Prozkoumat obsah"
},
"app.browse.discover-servers": {
"message": "Prozkoumat servery"
},
"app.browse.hide-added-servers": {
"message": "Skrýt již přidané servery"
},
"app.browse.project-type.modpacks": {
"message": "Balíčky Modifikací"
},
"app.browse.server-instance-content-warning": {
"message": "Přidání obsahu může narušit kompatibilitu při připojení k serveru. Veškerý přidaný obsah se navíc ztratí při aktualizaci obsahu instance serveru."
},
"app.browse.server.installing": {
"message": "Instalování"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Probíhá instalace modpacku..."
},
"app.export-modal.description-placeholder": {
"message": "Přidej popis modpacku..."
},
@@ -158,18 +32,12 @@
"app.export-modal.header": {
"message": "Exportovat modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Zahrnout \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Jméno Modpacku"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Jméno modpacku"
},
"app.export-modal.select-files-label": {
"message": "Nastavte, které soubory mají být do tohoto exportu zahrnuty"
},
"app.export-modal.version-number-label": {
"message": "Číslo verze"
},
@@ -224,6 +92,9 @@
"app.instance.worlds.browse-servers": {
"message": "Procházet servery"
},
"app.instance.worlds.delete-world-description": {
"message": "\"{name}\" bude **navždy smazáno** a nebude způsob, jak to obnovit."
},
"app.instance.worlds.delete-world-title": {
"message": "Opravdu si jsi jistý, že chceš navždy smazat tenhle svět?"
},
@@ -245,8 +116,8 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Žádné servery nebo světy nebyly přidány"
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) bude odstraněn z vašeho seznamu, a to i ve hře, a nebude možné jej obnovit."
"app.instance.worlds.remove-server-description": {
"message": "{name} bude odstraněn z tvého seznamu, včetně hry, a nebude žádný způsob, jak ho obnovit."
},
"app.instance.worlds.remove-server-title": {
"message": "Opravdu chceš odstranit {name}?"
@@ -257,9 +128,6 @@
"app.instance.worlds.this-server": {
"message": "tento server"
},
"app.modal.install-to-play.content-required": {
"message": "Požadovaný obsah"
},
"app.modal.install-to-play.header": {
"message": "Nainstaluj ke hraní"
},
@@ -269,21 +137,12 @@
"app.modal.install-to-play.mod-count": {
"message": "{count, plural,one {#mód} other {#módy}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Povinný modpack"
},
"app.modal.install-to-play.server-requires-mods": {
"message": "Tento server k hraní vyžaduje módy. Klikni na instalovat pro získání potřebných módů z Modrinth."
},
"app.modal.install-to-play.shared-instance": {
"message": "Sdílená instance"
},
"app.modal.install-to-play.shared-server-instance": {
"message": "Sdílená serverová instance"
},
"app.modal.install-to-play.view-contents": {
"message": "Zobrazit obsah"
},
"app.modal.update-to-play.header": {
"message": "Updatuj ke hraní"
},
@@ -293,15 +152,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "Je nutná aktualizace ke hraní {name}. Prosím updatuj na nejnovější verzi k spuštění hry."
},
"app.project.install-button.already-installed": {
"message": "Tento projekt je již nainstalován"
},
"app.project.install-context.back-to-browse": {
"message": "Zpět na Prozkoumat"
},
"app.project.install-context.install-content-to-instance": {
"message": "Nainstalovat obsah do instnce"
},
"app.settings.developer-mode-enabled": {
"message": "Vývojářský režim povolen."
},
@@ -326,33 +176,9 @@
"app.settings.tabs.resource-management": {
"message": "Správa zdrojů"
},
"app.update-popup.body": {
"message": "Aplikace Modrinth v{version} je připravena k instalaci! Nainstalujte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
},
"app.update-popup.body.download-complete": {
"message": "Stahování aplikace Modrinth v{version} bylo dokončeno. Nainstalujte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
},
"app.update-popup.body.linux": {
"message": "Aplikace Modrinth v{version} je k dispozici. Aktualizujte pomocí svého správce balíčků, abyste získali nejnovější funkce a opravy!"
},
"app.update-popup.body.metered": {
"message": "Aplikace Modrinth v{version} je nyní k dispozici! Protože jste v měřené síti, nebyla stažena automaticky."
},
"app.update-popup.changelog": {
"message": "Seznam změn"
},
"app.update-popup.download": {
"message": "Stáhnout ({size})"
},
"app.update-popup.download-complete": {
"message": "Stahování bylo dokončeno"
},
"app.update-popup.reload": {
"message": "Načíst znovu"
},
"app.update-popup.title": {
"message": "Je k dispozici aktualizace"
},
"app.update.complete-toast.text": {
"message": "Kliknutím sem zobrazíte seznam změn."
},
@@ -368,24 +194,6 @@
"app.update.reload-to-update": {
"message": "Restartovat aplikaci pro nainstalování aktualizace"
},
"app.world.server-modal.placeholder-address": {
"message": "priklad.modrinth.gg"
},
"app.world.server-modal.select-an-option": {
"message": "Vyberte možnost"
},
"app.world.world-item.incompatible-version": {
"message": "Nekompatibilní verze {version}"
},
"app.world.world-item.not-played-yet": {
"message": "Ještě nehráno"
},
"app.world.world-item.offline": {
"message": "Offline"
},
"app.world.world-item.players-online": {
"message": "{count} online"
},
"friends.action.add-friend": {
"message": "Přidat přítele"
},
@@ -437,6 +245,9 @@
"friends.heading.pending": {
"message": "Čeká na vyřízení"
},
"friends.no-friends-match": {
"message": "Žádní přátelé neodpovídají '{query}''"
},
"friends.search-friends-placeholder": {
"message": "Hledat přátele..."
},
@@ -482,12 +293,6 @@
"instance.edit-world.title": {
"message": "Upravit svět"
},
"instance.files.adding-files": {
"message": "Přidávání souborů ({completed}/{total})"
},
"instance.files.save-as": {
"message": "Uložit jako..."
},
"instance.server-modal.address": {
"message": "Adresa"
},
@@ -668,9 +473,6 @@
"instance.worlds.incompatible_server": {
"message": "Server je nekompatibilní"
},
"instance.worlds.linked_server": {
"message": "Spravováno projektem serveru"
},
"instance.worlds.no_contact": {
"message": "Server se nepodařilo spojit"
},
@@ -689,24 +491,6 @@
"instance.worlds.world_in_use": {
"message": "Svět je použit"
},
"minecraft-account.add-account": {
"message": "Přidat účet"
},
"minecraft-account.label": {
"message": "Účet Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Nejste přihlášeni"
},
"minecraft-account.remove-account": {
"message": "Odstranit účet"
},
"minecraft-account.select-account": {
"message": "Vyberte účet"
},
"minecraft-account.sign-in": {
"message": "Přihlásit se do Minecraftu"
},
"search.filter.locked.instance": {
"message": "Poskytováno instancí"
},
@@ -719,9 +503,6 @@
"search.filter.locked.instance.sync": {
"message": "Synchronizováno z instancí"
},
"search.filter.locked.server": {
"message": "Poskytováno serverem"
},
"search.filter.locked.server-environment.title": {
"message": "Jen klientské mody mohou být přidány na server"
},
@@ -730,26 +511,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "Loader zprostředkovává server"
},
"unknown-pack-warning-modal.body": {
"message": "Soubor je zkontrolován pouze v případě, že je nahrán na Modrinth, a to bez ohledu na jeho formát (včetně formátu .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Toto varování již nezobrazovat"
},
"unknown-pack-warning-modal.header": {
"message": "Potvrdit instalaci"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Přesto nainstalovat"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Malware se často šíří prostřednictvím souborů s modpacky, které se sdílejí na platformách jako Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Tento soubor jsme na Modrinthu nenašli. Důrazně doporučujeme instalovat pouze soubory z důvěryhodných zdrojů."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Upozornění na neznámý soubor"
}
}
@@ -104,12 +104,6 @@
"app.browse.discover-servers": {
"message": "Opdag servere"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.export-modal.description-placeholder": {
"message": "Indtast modpack beskrivelse..."
},
+10 -1
View File
@@ -224,6 +224,9 @@
"app.instance.worlds.browse-servers": {
"message": "Server durchsuchen"
},
"app.instance.worlds.delete-world-description": {
"message": "„{name}“ wird **endgültig gelöscht**, und es gibt keine Möglichkeit, sie wiederherzustellen."
},
"app.instance.worlds.delete-world-title": {
"message": "Möchtest du diese Welt wirklich endgültig löschen?"
},
@@ -245,6 +248,12 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Keine Server oder Welten hinzugefügt"
},
"app.instance.worlds.remove-server-description": {
"message": "„{name}“ wird aus deiner Liste entfernt, auch im Spiel, und es gibt keine Möglichkeit, ihn wiederherzustellen."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "„{name}“ ({address}) wird aus deiner Liste entfernt, inklusive im Spiel, und es gibt keine Möglichkeit, ihn wiederherzustellen."
},
"app.instance.worlds.remove-server-title": {
"message": "Möchtest du {name} wirklich entfernen?"
},
@@ -294,7 +303,7 @@
"message": "Dieses Projekt ist bereits installiert"
},
"app.project.install-context.back-to-browse": {
"message": "Zurück zum Entdecken"
"message": "Zurück zum Durchstöbern"
},
"app.project.install-context.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
+11 -8
View File
@@ -48,7 +48,7 @@
"message": "Farbschema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Ändere die Kategorie, die beim Öffnen des Launchers angezeigt wird."
"message": "Ändere die Seite, die beim Öffnen des Launchers angezeigt wird."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Start"
@@ -137,9 +137,6 @@
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Das Hinzufügen von Inhalten kann die Kompatibilität beim Beitreten zum Server beeinträchtigen. Außerdem gehen alle hinzugefügten Inhalte verloren, wenn du die Inhalte der Serverinstanz aktualisierst."
},
"app.browse.server.installing": {
"message": "Wird installiert"
},
@@ -224,6 +221,9 @@
"app.instance.worlds.browse-servers": {
"message": "Server durchsuchen"
},
"app.instance.worlds.delete-world-description": {
"message": "„{name}“ wird **endgültig gelöscht**, und es gibt keine Möglichkeit, sie wiederherzustellen."
},
"app.instance.worlds.delete-world-title": {
"message": "Möchtest du diese Welt wirklich endgültig löschen?"
},
@@ -245,6 +245,12 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Keine Server oder Welten hinzugefügt"
},
"app.instance.worlds.remove-server-description": {
"message": "„{name}“ wird aus deiner Liste entfernt, auch im Spiel, und es gibt keine Möglichkeit, ihn wiederherzustellen."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "„{name}“ ({address}) wird aus deiner Liste entfernt, inklusive im Spiel, und es gibt keine Möglichkeit, ihn wiederherzustellen."
},
"app.instance.worlds.remove-server-title": {
"message": "Möchtest du {name} wirklich entfernen?"
},
@@ -293,9 +299,6 @@
"app.project.install-button.already-installed": {
"message": "Dieses Projekt ist bereits installiert"
},
"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"
},
@@ -516,7 +519,7 @@
"message": "Wird gelöscht..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "Duplizieren"
"message": "Kopieren"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Kann während der Installation nicht dupliziert werden."
@@ -332,138 +332,6 @@
"app.settings.tabs.resource-management": {
"message": "Resource management"
},
"app.skins.add-button": {
"message": "Add skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Drag and drop"
},
"app.skins.apply-button": {
"message": "Apply"
},
"app.skins.delete-button": {
"message": "Delete skin"
},
"app.skins.delete-modal.description": {
"message": "This will permanently delete the selected skin. This action cannot be undone."
},
"app.skins.delete-modal.title": {
"message": "Are you sure you want to delete this skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Failed to read the dropped file."
},
"app.skins.dropped-file-error.title": {
"message": "Error processing file"
},
"app.skins.edit-button": {
"message": "Edit skin"
},
"app.skins.modal.add-skin-button": {
"message": "Add skin"
},
"app.skins.modal.add-title": {
"message": "Adding a skin"
},
"app.skins.modal.arm-style-section": {
"message": "Arm style"
},
"app.skins.modal.arm-style-slim": {
"message": "Slim"
},
"app.skins.modal.arm-style-wide": {
"message": "Wide"
},
"app.skins.modal.cape-fallback-name": {
"message": "Cape"
},
"app.skins.modal.cape-section": {
"message": "Cape"
},
"app.skins.modal.edit-title": {
"message": "Editing skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Make an edit to the skin first!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "No cape"
},
"app.skins.modal.none-cape-option": {
"message": "None"
},
"app.skins.modal.replace-texture-button": {
"message": "Replace texture"
},
"app.skins.modal.save-skin-button": {
"message": "Save skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Saving..."
},
"app.skins.modal.texture-section": {
"message": "Texture"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Upload a skin first!"
},
"app.skins.preview.edit-button": {
"message": "Edit skin"
},
"app.skins.previewing-badge": {
"message": "Previewing"
},
"app.skins.rate-limit.text": {
"message": "You're changing your skin too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again."
},
"app.skins.rate-limit.title": {
"message": "Slow down!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Default skins"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Saved skins"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Sign In"
},
"app.skins.sign-in.description": {
"message": "Please sign into your Minecraft account to use the skin management features of the Modrinth app."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Excited Modrinth Bot"
},
"app.skins.sign-in.title": {
"message": "Please sign in"
},
"app.skins.title": {
"message": "Skin selector"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App."
},
@@ -743,24 +611,6 @@
"instance.settings.tabs.java": {
"message": "Java and memory"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Custom environment variables"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Custom Java arguments"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Custom Java installation"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Custom memory allocation"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Enter environmental variables..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Enter Java arguments..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Environment variables"
},
@@ -776,9 +626,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memory allocated"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "Window"
},
@@ -104,12 +104,6 @@
"app.auth-servers.unreachable.header": {
"message": "No se puede acceder a los servidores de autenticación"
},
"app.browse.add-servers-to-instance": {
"message": "Añadiendo servidor a instancia"
},
"app.browse.add-to-an-instance": {
"message": "Añadir a una instancia"
},
"app.browse.add-to-instance": {
"message": "Añadir a instancia"
},
@@ -122,9 +116,6 @@
"app.browse.already-added": {
"message": "Ya se ha añadido"
},
"app.browse.back-to-instance": {
"message": "Volver a instancia"
},
"app.browse.discover-content": {
"message": "Descubrir contenido"
},
@@ -137,9 +128,6 @@
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Añadir contenido puede romper la compatibilidad cuando te unes al servidor. Cualquier contenido añadido tambien se perderá cuando actualizes el contenido en la instancia del servidor."
},
"app.browse.server.installing": {
"message": "Instalando"
},
@@ -224,6 +212,9 @@
"app.instance.worlds.browse-servers": {
"message": "Explorar servidores"
},
"app.instance.worlds.delete-world-description": {
"message": "\"{name}\" se **eliminará permanentemente** y no habrá forma de recuperarlo."
},
"app.instance.worlds.delete-world-title": {
"message": "¿Estás seguro de que quieres eliminar este mundo de forma permanente?"
},
@@ -245,6 +236,12 @@
"app.instance.worlds.no-worlds-heading": {
"message": "No hay servidores ni mundos añadidos"
},
"app.instance.worlds.remove-server-description": {
"message": "\"{name}\" se eliminará de tu lista, también dentro del juego, y no podrás recuperarlo."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "\"{name}\" ({address}) se eliminará de tu lista, también dentro del juego, y no podrás recuperarlo."
},
"app.instance.worlds.remove-server-title": {
"message": "¿Estás seguro de que quieres eliminar {name}?"
},
@@ -290,15 +287,6 @@
"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.project.install-button.already-installed": {
"message": "Este proyecto está actualmente instalado"
},
"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.settings.developer-mode-enabled": {
"message": "Modo desarrollador activado."
},
@@ -602,24 +590,6 @@
"instance.settings.tabs.java": {
"message": "Java y memoria"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variables de entorno personalizadas"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argumentos personalizados de Java"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Instalación personalizada de Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Asignación de memoria personalizada"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Introduce las variables de entorno..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Introduce los argumentos de Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variables de entorno"
},
@@ -635,9 +605,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memoria asignada"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/ruta/a/java"
},
"instance.settings.tabs.window": {
"message": "Ventana"
},
+8 -74
View File
@@ -21,7 +21,7 @@
"message": "Instancia principal"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar más instancias en ejecución"
"message": "Mostrar mas instancias en ejecución"
},
"app.action-bar.stop-instance": {
"message": "Finalizar instancia"
@@ -48,7 +48,7 @@
"message": "Tema de color"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Cambia la página en la que el lanzador inicia."
"message": "Cambia la página en la que el launcher se abre en."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Inicio"
@@ -72,13 +72,13 @@
"message": "Vuelve a jugar tus mundos"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimiza el lanzador cuando un proceso de Minecraft empieza."
"message": "Minimiza el launcher cuando un proceso de Minecraft empieza."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimiza el lanzador"
"message": "Minimiza el launcher"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usa los sistemas de marco de Windows (se requiere resetear la aplicación)."
"message": "Usa los sistemas de frame de Windows (se requiere resetear la aplicación)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas"
@@ -93,10 +93,10 @@
"message": "Alternar barra lateral"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Si intentas instalar un archivo de paquete de Modrinth (.mrpack) que no esté alojado en Modrinth, debes comprender los riesgos que esto conlleva antes de instalarlo."
"message": "Si intentas instalar un archivo de paquete de Modrinth (.mrpack) que no esté alojado en Modrinth, te advertiremos de los riesgos antes de instalarlo."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Advertir antes de instalarme paquetes de mods desconocidos"
"message": "Adviérteme antes de instalarme modpacks desconocidos"
},
"app.auth-servers.unreachable.body": {
"message": "Los servidores de autenticación de Minecraft pueden no estar funcionando en este momento. Verifica tu conexión a internet e inténtalo de nuevo más tarde."
@@ -104,12 +104,6 @@
"app.auth-servers.unreachable.header": {
"message": "No se puede conectar con los servidores de autenticación"
},
"app.browse.add-servers-to-instance": {
"message": "Añadir servidor a instancia"
},
"app.browse.add-to-an-instance": {
"message": "Añadir a una instancia"
},
"app.browse.add-to-instance": {
"message": "Añadir a la instancia"
},
@@ -122,9 +116,6 @@
"app.browse.already-added": {
"message": "Ya está añadido"
},
"app.browse.back-to-instance": {
"message": "Volver a la instancia"
},
"app.browse.discover-content": {
"message": "Descubrir contenido"
},
@@ -135,10 +126,7 @@
"message": "Ocultar servidores ya añadidos"
},
"app.browse.project-type.modpacks": {
"message": "Paquetes de Mods"
},
"app.browse.server-instance-content-warning": {
"message": "Añadir contenido puede comprometer la compatibilidad a la hora de unirse a un servidor. Cualquier contenido añadido se perderá cuando actualices la instancia del servidor."
"message": "Modpacks"
},
"app.browse.server.installing": {
"message": "Instalando"
@@ -158,18 +146,12 @@
"app.export-modal.header": {
"message": "Exportar modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "¿Incluir \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nombre del modpack"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Nombre del modpack"
},
"app.export-modal.select-files-label": {
"message": "Configura que archivos se incluirán en esta instancia"
},
"app.export-modal.version-number-label": {
"message": "Número de versión"
},
@@ -299,15 +281,6 @@
"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."
},
@@ -611,24 +584,6 @@
"instance.settings.tabs.java": {
"message": "Java y memoria"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variables de entorno personalizadas"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argumentos personalizados de Java"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Instalación personalizada de Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Asignación de memoria personalizada"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Introduce las variables de entorno..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Introduce los argumentos de Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variables de entorno"
},
@@ -644,9 +599,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memoria asignada"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/ruta/a/java"
},
"instance.settings.tabs.window": {
"message": "Ventana"
},
@@ -719,24 +671,6 @@
"instance.worlds.world_in_use": {
"message": "El mundo está en uso"
},
"minecraft-account.add-account": {
"message": "Añadir cuenta"
},
"minecraft-account.label": {
"message": "Cuenta de Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Registrarse"
},
"minecraft-account.remove-account": {
"message": "Eliminar cuenta"
},
"minecraft-account.select-account": {
"message": "Seleccionar cuenta"
},
"minecraft-account.sign-in": {
"message": "Registrarse en Minecraft"
},
"search.filter.locked.instance": {
"message": "Proporcionado por la instancia"
},
@@ -1,115 +1,10 @@
{
"app.action-bar.downloading-java": {
"message": "Ladataan Java-versiota {version}"
},
"app.action-bar.downloads": {
"message": "Lataukset"
},
"app.action-bar.hide-more-running-instances": {
"message": "Piilota enemmän käynnissä olevia instansseja"
},
"app.action-bar.make-primary-instance": {
"message": "Aseta ensisijaiseksi instanssiksi"
},
"app.action-bar.no-instances-running": {
"message": "Ei instansseja käynnissä"
},
"app.action-bar.offline": {
"message": "Offline-tilassa"
},
"app.action-bar.primary-instance": {
"message": "Ensisijainen instanssi"
},
"app.action-bar.show-more-running-instances": {
"message": "Näytä enemmän käynnissä olevia instansseja"
},
"app.action-bar.stop-instance": {
"message": "Pysäytä instanssi"
},
"app.action-bar.view-active-downloads": {
"message": "Näytä aktiiviset lataukset"
},
"app.action-bar.view-instance": {
"message": "Katso intanssia"
},
"app.action-bar.view-logs": {
"message": "Katso lokeja"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Mahdollistaa edistyneen renderöinnin, kuten sumennus efektit, jotka voivat aiheuttaa suorituskykyongelmia ilman laitteistokiihdytettyä renderöintiä."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Edistynyt renderointi"
},
"app.appearance-settings.color-theme.description": {
"message": "Valitse haluamasi väriteema Modrinth Appille."
},
"app.appearance-settings.color-theme.title": {
"message": "Väriteema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Muuta sivua mille laukaisin avautuu."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Koti"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Kirjasto"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Oletus laskeutumissivu"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Kytkee pois päältä nimikyltin pelaajasi päällä skinit sivulla."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Piilota nimikyltti"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Sisällyttää viimeaikaiset maailmat \"Hyppää takaisin sisään\" osiossa Koti sivulla."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Hyppää takaisin sisään maailmoihin"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Pienennä laukaisin kun Minecraft prosessi käynnistyy."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Piilota käynnistysohjelma"
},
"app.appearance-settings.native-decorations.description": {
"message": "Käytä järjestelmän ikkuna kehystä (Vaatii sovelluksen uudelleen käynnistämisen)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Natiivit koristukset"
},
"app.appearance-settings.select-option": {
"message": "Valitse vaihtoehto"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Laittaa päälle mahdollisuuden kytkeä sivupalkkia."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Kytke sivupalkki"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Jos yrität asentaa Modrinth-pakettitiedoston (.mrpack), jota ei ole isännöity Modrinthissä, varmistamme, että ymmärrät riskit ennen asennusta."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Varoita minua ennen tuntemattomien modipakettien asentamista"
},
"app.auth-servers.unreachable.body": {
"message": "Minecraftin todennuspalvelimet eivät ehkä ole tällä hetkellä tavoitettavissa. Tarkista internetyhteytesi ja yritä myöhemmin uudelleen."
},
"app.auth-servers.unreachable.header": {
"message": "Todennuspalvelimiin ei saada yhteyttä"
},
"app.browse.add-servers-to-instance": {
"message": "Lisää palvelin instanssiin"
},
"app.browse.add-to-an-instance": {
"message": "Lisää instanssiin"
},
"app.browse.add-to-instance": {
"message": "Lisää instanssiin"
},
@@ -122,33 +17,12 @@
"app.browse.already-added": {
"message": "On jo asennettu"
},
"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"
},
"app.browse.hide-added-servers": {
"message": "Piilota lisätyt palvelimet"
},
"app.browse.project-type.modpacks": {
"message": "Modipaketit"
},
"app.browse.server-instance-content-warning": {
"message": "Sisällön lisääminen voi rikkoa yhteensopivuuden palvelimelle liittyessä. Kaikki lisätty sisältö katoaa myös kun päivität palvelin instanssin sisällön."
},
"app.browse.server.installing": {
"message": "Asennetaan"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Asennetaan modipakettia..."
},
"app.export-modal.description-placeholder": {
"message": "Lisää modipaketin kuvaus..."
},
@@ -158,18 +32,12 @@
"app.export-modal.header": {
"message": "Vie modipaketti"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Sisällytä \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Modipaketin nimi"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Modipaketin nimi"
},
"app.export-modal.select-files-label": {
"message": "Muuta mitkä tiedostot sisältyvät tähän vientiin"
},
"app.export-modal.version-number-label": {
"message": "Versio numero"
},
@@ -212,9 +80,6 @@
"app.instance.mods.share-text": {
"message": "Tsekkaa projektit joita käytän minun modipaketissani!"
},
"app.instance.mods.share-title": {
"message": "Jaetaan modipaketti sisältöä"
},
"app.instance.mods.successfully-uploaded": {
"message": "Onnistuneesti ladattu"
},
@@ -299,15 +164,6 @@
"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.settings.developer-mode-enabled": {
"message": "Kehittäjätila käytössä."
},
@@ -611,24 +467,6 @@
"instance.settings.tabs.java": {
"message": "Java ja muisti"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Mukautetut ympäristö muuttujat"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Mukautetut Java argumentit"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Mukautettu Java asennus"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Mukautettu muistin allokointi"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Syötä ympäristö muuttujat..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Syötä Java argumentit..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Ympäristö muuttujat"
},
@@ -644,9 +482,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "Muisti allokoitu"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/javaan/johtava/polku/"
},
"instance.settings.tabs.window": {
"message": "Ikkuna"
},
@@ -719,24 +554,6 @@
"instance.worlds.world_in_use": {
"message": "Maailma on käytössä"
},
"minecraft-account.add-account": {
"message": "Lisää tili"
},
"minecraft-account.label": {
"message": "Minecraft tili"
},
"minecraft-account.not-signed-in": {
"message": "Ei kirjautuneena sisään"
},
"minecraft-account.remove-account": {
"message": "Poista tili"
},
"minecraft-account.select-account": {
"message": "Valitse tili"
},
"minecraft-account.sign-in": {
"message": "Kirjaudu sisään Minecraftiin"
},
"search.filter.locked.instance": {
"message": "Annettu instanssin toimesta"
},
@@ -760,26 +577,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "Modialusta on palvelimen tarjoama"
},
"unknown-pack-warning-modal.body": {
"message": "Tiedosto tarkistetaan vain jos se on ladattu Modrinthiin, riippumatta tiedoston muodosta (mukaanlukien .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Älä näytä tätä varoitusta uudestaan"
},
"unknown-pack-warning-modal.header": {
"message": "Vahvista asennus"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Asenna jokatapauksessa"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Haittaohjelmia levitetään usein modipaketti tiedostojen kautta jakamalla niitä alustoilla kuten Discordissa."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Emme löytäneet tätä tiedostoa Modrinthista. Suosittelemme vahvasti että asennat tiedostoja vain lähteistä joihin luotat."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Tuntematon tiedosto varoitus"
}
}
@@ -729,7 +729,7 @@
"message": "Kumpirmahin ang installation"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "I-install pa rin"
"message": "I-install parin"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Ang malware ay madalas naidadala sa mga modpack files sa pamamagitan ng pag-bigay ng mga ito sa mga platforms kagaya ng Discord."
+14 -20
View File
@@ -1,6 +1,6 @@
{
"app.action-bar.downloading-java": {
"message": "Téléchargement de Java {version} en cours"
"message": "Java version {version} en cours de téléchargement"
},
"app.action-bar.downloads": {
"message": "Téléchargements"
@@ -224,6 +224,9 @@
"app.instance.worlds.browse-servers": {
"message": "Parcourir des serveurs"
},
"app.instance.worlds.delete-world-description": {
"message": "« {name} » sera supprimé **pour toujours**, et il sera impossible de le récupérer."
},
"app.instance.worlds.delete-world-title": {
"message": "Êtes-vous sûr·e de vouloir supprimer ce monde pour toujours ?"
},
@@ -245,6 +248,12 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Aucun serveur ou monde ajoutés"
},
"app.instance.worlds.remove-server-description": {
"message": "« {name} » sera retiré de votre liste, y compris en jeu, et il sera impossible de le récupérer."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "« {name} » ({address}) sera retiré de votre liste, y compris en jeu, et il sera impossible de le récupérer."
},
"app.instance.worlds.remove-server-title": {
"message": "Êtes-vous sûr·e de vouloir retirer {name} ?"
},
@@ -294,7 +303,7 @@
"message": "Ce projet a déjà été installé"
},
"app.project.install-context.back-to-browse": {
"message": "Retour vers la page découvrir"
"message": "Retour à la navigation"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installer du contenu à l'instance"
@@ -380,6 +389,9 @@
"app.world.world-item.offline": {
"message": "Hors ligne"
},
"app.world.world-item.players-online": {
"message": "{count, plural, =0 {Personne en ligne} one {# est en ligne} other {# sont en ligne}}"
},
"friends.action.add-friend": {
"message": "Ajouter un ami"
},
@@ -599,24 +611,6 @@
"instance.settings.tabs.java": {
"message": "Java et mémoire"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variables d'environnement personnalisé"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Arguments Java personnalisée"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Installation Java personnalisée"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Allocation de mémoire personnalisée"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Entrer une variable d'environnement..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Entrer un argument Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variables d'environnement"
},
@@ -254,6 +254,9 @@
"friends.action.add-friend": {
"message": "הוספת חבר"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, one {בקשת חברות אחת} other {# בקשות חברות}}"
},
"friends.add-friend.submit": {
"message": "שליחת בקשת חברות"
},
+8 -113
View File
@@ -20,78 +20,15 @@
"app.action-bar.primary-instance": {
"message": "Elsődleges profil"
},
"app.action-bar.show-more-running-instances": {
"message": "További futó profilok megjelenítése"
},
"app.action-bar.stop-instance": {
"message": "Profil megállítása"
},
"app.action-bar.view-active-downloads": {
"message": "Aktív letöltések megtekintése"
},
"app.action-bar.view-instance": {
"message": "Profil megtekintése"
},
"app.action-bar.view-logs": {
"message": "Naplók megtekintése"
},
"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."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Fejlett renderelés"
},
"app.appearance-settings.color-theme.description": {
"message": "Válaszd ki a Modrinth Apphoz a kívánt színsémát."
},
"app.appearance-settings.color-theme.title": {
"message": "Téma"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Állítsd be, hogy az indító melyik oldalon nyíljon meg."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Kezdőlap"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Könyvtár"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Alapértelmezett nyitóoldal"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Letiltja a játékosod felett megjelenő névcímkét a Skinek oldalon."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Névcímke eltüntetése"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "A kezdőlap „Folytasd itt” részében a legújabb világok is megtalálhatók."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "„Folytasd itt” a világokat is"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimalizáld az indítót a Minecraft indításakor."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Indító minimalizálása"
},
"app.appearance-settings.native-decorations.description": {
"message": "A rendszer ablakkeret használata (az alkalmazás újraindítása szükséges)."
},
"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.toggle-sidebar.description": {
"message": "Lehetővé teszi az oldalsáv be- és kikapcsolását."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Oldalsáv megjelenítése/elrejtése"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Ha olyan Modrinth Csomagfájlt (.mrpack) próbálsz telepíteni, amely nem a Modrinth szerverén található, a telepítés előtt gondoskodunk arról, hogy tisztában legyél a kockázatokkal."
},
@@ -104,12 +41,6 @@
"app.auth-servers.unreachable.header": {
"message": "Nem lehet elérni a hitelesítési kiszolgálókat"
},
"app.browse.add-servers-to-instance": {
"message": "Szerver hozzáadása a profilhoz"
},
"app.browse.add-to-an-instance": {
"message": "Hozzáadás egy profilhoz"
},
"app.browse.add-to-instance": {
"message": "Hozzáadás a profilhoz"
},
@@ -137,9 +68,6 @@
"app.browse.project-type.modpacks": {
"message": "Modcsomagok"
},
"app.browse.server-instance-content-warning": {
"message": "A tartalom hozzáadása ronthatja a kompatibilitást a szerverhez való csatlakozáskor. A hozzáadott tartalom elveszik, ha frissíted a szerverprofil tartalmát."
},
"app.browse.server.installing": {
"message": "Letöltés..."
},
@@ -158,18 +86,12 @@
"app.export-modal.header": {
"message": "Modcsomag exportálása"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Tartalmaz „{file}” fájlokat?"
},
"app.export-modal.modpack-name-label": {
"message": "A modcsomag neve"
},
"app.export-modal.modpack-name-placeholder": {
"message": "A modcsomag neve"
},
"app.export-modal.select-files-label": {
"message": "Állítsd be, hogy mely fájlok kerüljenek bele az exportba"
},
"app.export-modal.version-number-label": {
"message": "Verziószám"
},
@@ -272,6 +194,9 @@
"app.modal.install-to-play.install-button": {
"message": "Telepítés"
},
"app.modal.install-to-play.mod-count": {
"message": "{count} mod"
},
"app.modal.install-to-play.required-modpack": {
"message": "Szükséges modcsomag"
},
@@ -294,10 +219,7 @@
"message": "Frissítés szükséges"
},
"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.project.install-button.already-installed": {
"message": "Ez a projekt már telepítve van"
"message": "\nFrissítés szükséges ehhez: {name}. Kérk, frissíts a legújabb verzióra a játék elindításához"
},
"app.project.install-context.back-to-browse": {
"message": "Vissza a böngészéshez"
@@ -392,6 +314,9 @@
"friends.action.add-friend": {
"message": "Barát hozzáadása"
},
"friends.action.view-friend-requests": {
"message": "{count} barát {count, plural, other {kérelem}}"
},
"friends.add-friend.submit": {
"message": "Barátkérelem elküldése"
},
@@ -663,7 +588,7 @@
"message": "Ne mutasd a kezdőlapon"
},
"instance.worlds.game_already_open": {
"message": "A profil már el van indítva"
"message": "A profil már meg van nyitva"
},
"instance.worlds.hardcore": {
"message": "Hardcore mód"
@@ -692,24 +617,6 @@
"instance.worlds.world_in_use": {
"message": "A világ használatban van"
},
"minecraft-account.add-account": {
"message": "Fiók hozzáadása"
},
"minecraft-account.label": {
"message": "Minecraft-fiók"
},
"minecraft-account.not-signed-in": {
"message": "Nem vagy bejelentkezve"
},
"minecraft-account.remove-account": {
"message": "Fiók törlése"
},
"minecraft-account.select-account": {
"message": "Fiók kiválasztása"
},
"minecraft-account.sign-in": {
"message": "Bejelentkezés a Minecraftba"
},
"search.filter.locked.instance": {
"message": "A profil által van megadva"
},
@@ -734,25 +641,13 @@
"search.filter.locked.server-loader.title": {
"message": "A betöltőt a szerver biztosítja"
},
"unknown-pack-warning-modal.body": {
"message": "Egy fájlt csak akkor vizsgálunk meg, ha azt feltöltik a Modrinthra, függetlenül a fájlformátumtól (beleértve a .mrpack formátumot is)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Ne mutasd újra ezt a figyelmeztetést"
},
"unknown-pack-warning-modal.header": {
"message": "Telepítés megerősítése"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Letöltés mindenképpen"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "A rosszindulatú programokat gyakran modcsomag-fájlokon keresztül terjesztik, például a Discordhoz hasonló platformokon történő megosztás révén."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Ezt a fájlt nem találtuk meg a Modrinthon. Határozottan javasoljuk, hogy kizárólag megbízható forrásokból származó fájlokat telepíts."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Figyelmeztetés ismeretlen fájlról"
}
}
+6 -36
View File
@@ -71,9 +71,6 @@
"app.appearance-settings.select-option": {
"message": "Pilih opsi"
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Peringatkan saya sebelum memasang paket mod tidak dikenal"
},
"app.auth-servers.unreachable.body": {
"message": "Server autentikasi Minecraft mungkin sedang tidak tersedia saat ini. Periksa koneksi internet Anda dan coba lagi nanti."
},
@@ -245,6 +242,9 @@
"app.modal.install-to-play.install-button": {
"message": "Pasang"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# mod}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Paket mod yang diperlukan"
},
@@ -269,9 +269,6 @@
"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-context.install-content-to-instance": {
"message": "Pasang konten ke instans"
},
"app.settings.developer-mode-enabled": {
"message": "Mode pengembang dihidupkan."
},
@@ -359,6 +356,9 @@
"friends.action.add-friend": {
"message": "Tambah teman"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, other {permintaan}} teman"
},
"friends.add-friend.submit": {
"message": "Kirim permintaan teman"
},
@@ -659,21 +659,6 @@
"instance.worlds.world_in_use": {
"message": "Dunia sedang dimainkan"
},
"minecraft-account.add-account": {
"message": "Tambah akun"
},
"minecraft-account.label": {
"message": "Akun Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Tidak masuk"
},
"minecraft-account.remove-account": {
"message": "Hapus akun"
},
"minecraft-account.select-account": {
"message": "Pilih akun"
},
"search.filter.locked.instance": {
"message": "Disediakan oleh instans"
},
@@ -697,20 +682,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "Pemuat disediakan oleh server"
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Jangan tampilkan peringatan ini lagi"
},
"unknown-pack-warning-modal.header": {
"message": "Konfirmasi pemasangan"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Tetap pasang"
},
"unknown-pack-warning-modal.warning.body": {
"message": "Kami tidak dapat menemukan berkas ini di Modrinth. Kami sangat menyarankan Anda untuk hanya memasang berkas dari sumber-sumber terpercaya."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Peringatan berkas tidak dikenal"
}
}
+15 -24
View File
@@ -84,7 +84,7 @@
"message": "Decorazioni native"
},
"app.appearance-settings.select-option": {
"message": "Seleziona"
"message": "Scegli una pagina"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Scegli se mostrare o nascondere la barra laterale."
@@ -93,7 +93,7 @@
"message": "Abilita la barra laterale"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Visti i rischi, apparirà un avviso prima di installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth."
"message": "Visti i rischi nell'installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth, mostriamo un avvertimento quando tenti di fare ciò."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Avvisami se installo pacchetti sconosciuti"
@@ -224,6 +224,9 @@
"app.instance.worlds.browse-servers": {
"message": "Esplora i server"
},
"app.instance.worlds.delete-world-description": {
"message": "\"{name}\" verrà **eliminato permanentemente** e non ci sarà modo di recuperarlo."
},
"app.instance.worlds.delete-world-title": {
"message": "Vuoi davvero eliminare questo mondo per sempre?"
},
@@ -245,6 +248,12 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Nessun server o mondo aggiunto"
},
"app.instance.worlds.remove-server-description": {
"message": "\"{name}\" sarà rimosso dalla tua lista, inclusa quella nel gioco, e non ci sarà modo di recuperarlo."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "\"{name}\" ({address}) sarà rimosso dalla tua lista, inclusa quella nel gioco, e non ci sarà modo di recuperarlo."
},
"app.instance.worlds.remove-server-title": {
"message": "Vuoi davvero rimuovere {name}?"
},
@@ -263,6 +272,9 @@
"app.modal.install-to-play.install-button": {
"message": "Installa"
},
"app.modal.install-to-play.mod-count": {
"message": "{count} mod"
},
"app.modal.install-to-play.required-modpack": {
"message": "Pacchetto di mod richiesto"
},
@@ -291,7 +303,7 @@
"message": "Questo progetto è già stato installato"
},
"app.project.install-context.back-to-browse": {
"message": "Torna a Esplora"
"message": "Torna su esplora"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installa il contenuto nell'istanza"
@@ -599,24 +611,6 @@
"instance.settings.tabs.java": {
"message": "Java e memoria"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variabili d'ambiente"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argomenti JVM"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Eseguibile di Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Memoria allocata"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Inserisci le variabili d'ambiente..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Inserisci gli argomenti JVM..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variabili d'ambiente"
},
@@ -632,9 +626,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memoria allocata"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/percorso/di/java"
},
"instance.settings.tabs.window": {
"message": "Finestra"
},
@@ -254,6 +254,9 @@
"app.modal.install-to-play.install-button": {
"message": "インストール"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {#個のMod}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "必須のModパック"
},
@@ -365,6 +368,9 @@
"friends.action.add-friend": {
"message": "フレンドを追加"
},
"friends.action.view-friend-requests": {
"message": "{count}件の友達リクエスト"
},
"friends.add-friend.submit": {
"message": "フレンド申請を送信"
},
+6 -45
View File
@@ -24,7 +24,7 @@
"message": "실행 중인 인스턴스 보이기"
},
"app.action-bar.stop-instance": {
"message": "인스턴스 멈추기"
"message": "인스턴스 중지"
},
"app.action-bar.view-active-downloads": {
"message": "다운로드 목록 보기"
@@ -104,12 +104,6 @@
"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": "인스턴스에 추가"
},
@@ -122,9 +116,6 @@
"app.browse.already-added": {
"message": "이미 추가됨"
},
"app.browse.back-to-instance": {
"message": "인스턴스로 돌아가기"
},
"app.browse.discover-content": {
"message": "콘텐츠 탐색하기"
},
@@ -137,9 +128,6 @@
"app.browse.project-type.modpacks": {
"message": "모드팩"
},
"app.browse.server-instance-content-warning": {
"message": "콘텐츠를 추가하면 서버 접속 시 호환이 안 될 수 있습니다. 또한 서버 인스턴스 콘텐츠를 업데이트하면 임의로 추가된 콘텐츠는 모두 손실됩니다."
},
"app.browse.server.installing": {
"message": "설치 중"
},
@@ -192,10 +180,10 @@
"message": "이 모드팩은 이미 <bold>{instanceName}</bold> 인스턴스에 설치되어 있습니다. 정말로 복제하시겠습니까?"
},
"app.instance.modpack-already-installed.create": {
"message": "만들기"
"message": "생성"
},
"app.instance.modpack-already-installed.header": {
"message": "이미 설치된 모드팩"
"message": "모드팩 이미 설치"
},
"app.instance.modpack-already-installed.instance": {
"message": "인스턴스"
@@ -299,15 +287,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "{name}을(를) 플레이하려면 업데이트가 필요합니다. 게임을 실행하려면 최신 버전으로 업데이트해 주세요."
},
"app.project.install-button.already-installed": {
"message": "이미 설치되어 있음"
},
"app.project.install-context.back-to-browse": {
"message": "탐색으로 돌아가기"
},
"app.project.install-context.install-content-to-instance": {
"message": "인스턴스에 콘텐츠 설치"
},
"app.settings.developer-mode-enabled": {
"message": "개발자 모드가 활성화되었습니다."
},
@@ -395,6 +374,9 @@
"friends.action.add-friend": {
"message": "친구 추가"
},
"friends.action.view-friend-requests": {
"message": "{count}건의 친구 요청"
},
"friends.add-friend.submit": {
"message": "친구 요청 보내기"
},
@@ -608,24 +590,6 @@
"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": "환경 변수"
},
@@ -641,9 +605,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "할당된 메모리"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "창"
},
@@ -272,6 +272,9 @@
"app.modal.install-to-play.install-button": {
"message": "Pasang"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# mod}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Pek mod yang diperlukan"
},
@@ -299,6 +302,9 @@
"app.project.install-button.already-installed": {
"message": "Projek ini sudah dipasang"
},
"app.project.install-context.back-to-browse": {
"message": "Kembali melayar"
},
"app.project.install-context.install-content-to-instance": {
"message": "Pasang kandungan ke dalam pemasangan"
},
@@ -389,6 +395,9 @@
"friends.action.add-friend": {
"message": "Tambah rakan"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, other {permintaan}} rakan"
},
"friends.add-friend.submit": {
"message": "Hantar permintaan rakan"
},
+10 -1
View File
@@ -189,7 +189,7 @@
"message": "Verwijder instantie"
},
"app.instance.modpack-already-installed.body": {
"message": "Deze modpakket is al in de <bold>{instanceName}</bold> instantie geïnstalleerd. Ben je zeker dat je het geen wil dupliceren?"
"message": "Deze modpakket is al in de <bold>{instanceName}<bold> instantie geïnstalleerd. Ben je zeker dat je het geen wil dupliceren?"
},
"app.instance.modpack-already-installed.create": {
"message": "Maak"
@@ -224,6 +224,9 @@
"app.instance.worlds.browse-servers": {
"message": "Zoek servers"
},
"app.instance.worlds.delete-world-description": {
"message": "{name} Zal **permanent verwijderd** worden en kan niet hersteld worden."
},
"app.instance.worlds.delete-world-title": {
"message": "Weet je zeker dat je deze wereld definitief wilt verwijderen?"
},
@@ -254,6 +257,9 @@
"app.instance.worlds.remove-server-title": {
"message": "Ben je zeker dat je {name} wil verwijderen?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Zoek werelden"
},
"app.instance.worlds.this-server": {
"message": "deze server"
},
@@ -296,6 +302,9 @@
"app.project.install-button.already-installed": {
"message": "Dit project is al geïnstalleerd"
},
"app.project.install-context.back-to-browse": {
"message": "Terug naar browsen"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installeer content naar instantie"
},
+11 -14
View File
@@ -137,9 +137,6 @@
"app.browse.project-type.modpacks": {
"message": "Paczki modów"
},
"app.browse.server-instance-content-warning": {
"message": "Dodawanie zawartości może zepsuć kompatybilność podczas dołączania na serwer. Jakakolwiek dodana zawartość zostanie stracona, gdy zaktualizujesz zawartość instancji serwera."
},
"app.browse.server.installing": {
"message": "Instalowanie"
},
@@ -158,18 +155,12 @@
"app.export-modal.header": {
"message": "Eksportuj paczkę modów"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Dołączyć \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nazwa paczki modów"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Nazwa paczki modów"
},
"app.export-modal.select-files-label": {
"message": "Skonfiguruje jakie pliki zostaną załączone w tym eksporcie"
},
"app.export-modal.version-number-label": {
"message": "Numer wersji"
},
@@ -177,7 +168,7 @@
"message": "1.0.0"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Wszystkie dane z Twojej instancji zostaną trwale usunięte, w tym Twoje światy, pliki konfiguracji i jakakolwiek dodana zawartość."
"message": "Wszystkie dane z Twojej instancji zostaną trwale usunięte, w tym Twoje światy, pliki konfiguracji i wszystkie zainstalowane treści."
},
"app.instance.confirm-delete.admonition-header": {
"message": "Tej akcji nie można odwrócić"
@@ -206,6 +197,9 @@
"app.instance.mods.project-was-added": {
"message": "Dodano \"{name}\""
},
"app.instance.mods.projects-were-added": {
"message": "Dodano {count} {count, plural, one {projekt} few {projekty} other {projektów}}"
},
"app.instance.mods.share-text": {
"message": "Sprawdź projekty, które używam w mojej paczce modów!"
},
@@ -269,6 +263,9 @@
"app.modal.install-to-play.install-button": {
"message": "Zainstaluj"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} few {# mody} other {# modów}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Wymagana paczka modów"
},
@@ -297,7 +294,7 @@
"message": "Ten projekt jest już zainstalowany"
},
"app.project.install-context.back-to-browse": {
"message": "Powróć do odkrywania"
"message": "Powrót do przeglądania"
},
"app.project.install-context.install-content-to-instance": {
"message": "Zainstaluj zasoby do instancji"
@@ -389,6 +386,9 @@
"friends.action.add-friend": {
"message": "Dodaj znajomego"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {zaproszenie} few {zaproszenia} other {zaproszeń}} do znajomych"
},
"friends.add-friend.submit": {
"message": "Wyślij zaproszenie"
},
@@ -695,9 +695,6 @@
"minecraft-account.label": {
"message": "Konto Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Nie zalogowano"
},
"minecraft-account.remove-account": {
"message": "Usuń konto"
},
+7 -22
View File
@@ -272,6 +272,9 @@
"app.modal.install-to-play.install-button": {
"message": "Instalar"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, =0 {Nenhum mod} one {# mod} other {# mods}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Pacote de mods necessário"
},
@@ -300,7 +303,7 @@
"message": "Este projeto já foi instalado"
},
"app.project.install-context.back-to-browse": {
"message": "Voltar ao descobrir"
"message": "Voltar ao explorar"
},
"app.project.install-context.install-content-to-instance": {
"message": "Instalar conteúdo para a instância"
@@ -392,6 +395,9 @@
"friends.action.add-friend": {
"message": "Adicionar um amigo"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, =0 {Nenhuma solicitação de amizade} one {{count} solicitação de amizade} other {{count} solicitações de amizade}}"
},
"friends.add-friend.submit": {
"message": "Enviar amizade"
},
@@ -605,24 +611,6 @@
"instance.settings.tabs.java": {
"message": "Java e memória"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variáveis de ambiente personalizadas"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argumentos Java personalizados"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Instalação personalizada do Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Alocação de memória personalizada"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Insira as variáveis de ambiente..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Insira os argumentos Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variáveis de ambiente"
},
@@ -638,9 +626,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memória alocada"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/caminho/ao/java"
},
"instance.settings.tabs.window": {
"message": "Janela"
},
+9 -57
View File
@@ -1,61 +1,4 @@
{
"app.action-bar.downloading-java": {
"message": "A descarregar a versão do java {version}"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Esconder mais instâncias a correr"
},
"app.action-bar.make-primary-instance": {
"message": "Transformar na instância principal"
},
"app.action-bar.no-instances-running": {
"message": "Nenhuma instância a correr"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Instância principal"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar mais instâncias a correr"
},
"app.action-bar.stop-instance": {
"message": "Para instância"
},
"app.action-bar.view-active-downloads": {
"message": "Ver instâncias ativas"
},
"app.action-bar.view-instance": {
"message": "Ver instância"
},
"app.action-bar.view-logs": {
"message": "Ver logs"
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Renderização avançada"
},
"app.appearance-settings.color-theme.description": {
"message": "Seleciona a tua cor preferida para a Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Cor de tema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Mudar a página em que o launcher abre."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Casa"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Livraria"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Página de carregamento principal"
},
"app.auth-servers.unreachable.body": {
"message": "Os servidores de autenticação do Minecraft poderão estar em baixo de momento. Verifica a tua ligação à internet e tenta novamente mais tarde."
},
@@ -149,6 +92,9 @@
"app.instance.worlds.browse-servers": {
"message": "Procurar servidores"
},
"app.instance.worlds.delete-world-description": {
"message": "\"{name}\" vai ser **apagado permanentemente**, e não haverá maneira de o recuperar."
},
"app.instance.worlds.delete-world-title": {
"message": "Tens a certeza que queres apagar permanentemente este mundo?"
},
@@ -170,6 +116,12 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Sem servidores ou mundos adicionados"
},
"app.instance.worlds.remove-server-description": {
"message": "\"{name}\" vai ser removido da tua lista, incluindo dentro do jogo, e não haverá maneira de o recuperar."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "\"{name}\" ({address}) vai ser removido da tua lista, incluindo dentro do jogo, e não haverá maneira de o recuperar."
},
"app.instance.worlds.remove-server-title": {
"message": "Tens a certeza que queres remover {name}?"
},
+27 -24
View File
@@ -3,7 +3,7 @@
"message": "Скачивание Java {version}"
},
"app.action-bar.downloads": {
"message": "Скачивание"
"message": "Скачивания"
},
"app.action-bar.hide-more-running-instances": {
"message": "Скрыть другие активные сборки"
@@ -206,6 +206,9 @@
"app.instance.mods.project-was-added": {
"message": "«{name}» добавлен"
},
"app.instance.mods.projects-were-added": {
"message": "{count, plural, one {Добавлен # проект} few {Добавлено # проекта} other {Добавлено # проектов}}"
},
"app.instance.mods.share-text": {
"message": "Что в моей сборке:"
},
@@ -221,6 +224,9 @@
"app.instance.worlds.browse-servers": {
"message": "Найти серверы"
},
"app.instance.worlds.delete-world-description": {
"message": "«{name}» будет **удалён навсегда**. Его невозможно восстановить."
},
"app.instance.worlds.delete-world-title": {
"message": "Вы действительно хотите удалить этот мир?"
},
@@ -242,9 +248,18 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Нет серверов и миров"
},
"app.instance.worlds.remove-server-description": {
"message": "«{name}» будет удалён из списка — в том числе в игре. Его невозможно восстановить."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "«{name}» ({address}) будет удалён из списка — в том числе в игре. Его невозможно восстановить."
},
"app.instance.worlds.remove-server-title": {
"message": "Вы действительно хотите удалить {name}?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Поиск по {count, plural, one {# миру} other {# мирам}}..."
},
"app.instance.worlds.this-server": {
"message": "этот сервер"
},
@@ -257,6 +272,9 @@
"app.modal.install-to-play.install-button": {
"message": "Установить"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# мод} few {# мода} other {# модов}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Необходимая сборка"
},
@@ -342,7 +360,7 @@
"message": "Доступно обновление"
},
"app.update.complete-toast.text": {
"message": "Нажмите, чтобы посмотреть список изменений."
"message": "Нажмите здесь, чтобы посмотреть изменения."
},
"app.update.complete-toast.title": {
"message": "Версия {version} успешно установлена!"
@@ -377,6 +395,9 @@
"friends.action.add-friend": {
"message": "Добавить в друзья"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {запрос} few {запроса} other {запросов}} дружбы"
},
"friends.add-friend.submit": {
"message": "Отправить запрос дружбы"
},
@@ -470,6 +491,9 @@
"instance.edit-world.title": {
"message": "Настройка мира"
},
"instance.files.adding-files": {
"message": "Добавление файлов ({completed, number}/{total, number})"
},
"instance.files.save-as": {
"message": "Сохранить как..."
},
@@ -587,24 +611,6 @@
"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": "Переменные среды"
},
@@ -620,9 +626,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "Выделенная память"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "Окно"
},
@@ -738,7 +741,7 @@
"message": "Загрузчик управляется сервером"
},
"unknown-pack-warning-modal.body": {
"message": "Файл проверяется только в том случае, если он загружен на Modrinth, независимо от его формата (включая .mrpack)."
"message": "Файлы любого формата проверены на безопасность, если они загружены на Modrinth (в том числе файлы⠀.mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Больше не предупреждать"
@@ -5,57 +5,12 @@
"app.action-bar.downloads": {
"message": "Nedladdningar"
},
"app.action-bar.hide-more-running-instances": {
"message": "Göm fler pågående instanser"
},
"app.action-bar.make-primary-instance": {
"message": "Gör till primärinstans"
},
"app.action-bar.no-instances-running": {
"message": "Ingen pågående instans"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Primärinstans"
},
"app.action-bar.show-more-running-instances": {
"message": "Visa fler pågående instanser"
},
"app.action-bar.stop-instance": {
"message": "Avsluta instans"
},
"app.action-bar.view-active-downloads": {
"message": "Visa aktiva nedladdningar"
},
"app.action-bar.view-instance": {
"message": "Visa instans"
},
"app.action-bar.view-logs": {
"message": "Visa loggar"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Aktiverar avancerad rendering som till exempel oskärpa vilket kan leda till prestandaproblem utan hårdvaruaccelererad rendering."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Avancerad rendering"
},
"app.appearance-settings.color-theme.description": {
"message": "Välj färgtema för Modrinth-appen."
},
"app.appearance-settings.color-theme.title": {
"message": "Färgtema"
},
"app.appearance-settings.default-landing-page.home": {
"message": "Hem"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliotek"
},
"app.appearance-settings.hide-nametag.title": {
"message": "Dölj namnskylt"
},
"app.auth-servers.unreachable.body": {
"message": "Minecrafts autentiseringsservrar kan vara nere just nu. Kontrollera din internetanslutning och försök igen senare."
},
@@ -260,6 +260,9 @@
"app.modal.install-to-play.install-button": {
"message": "ติดตั้ง"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# ม็อด}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "มอดแพ็กที่จำเป็น"
},
@@ -371,6 +374,9 @@
"friends.action.add-friend": {
"message": "เพิ่มเพื่อน"
},
"friends.action.view-friend-requests": {
"message": "คำขอเป็นเพื่อน {count} {count, plural, other {คน}}"
},
"friends.add-friend.submit": {
"message": "ส่งคำขอเป็นเพื่อน"
},
@@ -368,6 +368,9 @@
"friends.action.add-friend": {
"message": "Bir arkadaş ekle"
},
"friends.action.view-friend-requests": {
"message": "{count} arkadaşlık isteği"
},
"friends.add-friend.submit": {
"message": "Arkadaşlık isteği gönder"
},
+52 -94
View File
@@ -6,49 +6,49 @@
"message": "Завантаження"
},
"app.action-bar.hide-more-running-instances": {
"message": "Приховати більше запущених профілів"
"message": "Приховати більше запущених екземплярів"
},
"app.action-bar.make-primary-instance": {
"message": "Зробити основним профілем"
"message": "Зробити основним екземпляром"
},
"app.action-bar.no-instances-running": {
"message": "Немає запущених профілів"
"message": "Немає запущених екземплярів"
},
"app.action-bar.offline": {
"message": "Поза мережею"
"message": "Офлайн"
},
"app.action-bar.primary-instance": {
"message": "Основний профіль"
"message": "Основний екземпляр"
},
"app.action-bar.show-more-running-instances": {
"message": "Показати більше запущених профілів"
"message": "Показати більше запущених екземплярів"
},
"app.action-bar.stop-instance": {
"message": "Зупинити профіль"
"message": "Зупинити екземпляр"
},
"app.action-bar.view-active-downloads": {
"message": "Переглянути активні завантаження"
},
"app.action-bar.view-instance": {
"message": "Переглянути профіль"
"message": "Переглянути екземпляр"
},
"app.action-bar.view-logs": {
"message": "Переглянути журнали"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Дозволяє розширене промальовування, як-от ефекти розмиття, які можуть спричиняти проблеми з продуктивністю без апаратно-прискореного промальовування."
"message": "Дозволяє розширений рендеринг, такий як ефекти розмиття, які можуть спричиняти проблеми з продуктивністю без апаратно-прискореного рендерингу."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Розширене промальовування"
"message": "Розширений рендеринг"
},
"app.appearance-settings.color-theme.description": {
"message": "Виберіть бажану колірну тему для Modrinth App."
"message": "Виберіть бажану колірну тему для програми Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Колірна тема"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Змініть сторінку, на якій відкривається запускач."
"message": "Змініть сторінку, на якій відкривається панель запуску."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Головна"
@@ -57,46 +57,46 @@
"message": "Бібліотека"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Усталена цільова сторінка"
"message": "Цільова сторінка за замовчуванням"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Вимикає ім над вашим гравцем на сторінці скінів."
"message": "Вимикає іменний бейдж над вашим гравцем на сторінці скінів."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Приховати ім'я"
"message": "Приховати бейдж з іменем"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Уключає нещодавні світи в розділі «Повернутися» на головній сторінці."
"message": "Включає нещодавні світи в розділі «Повернення» на головній сторінці."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Повернутися у світи"
"message": "Стрибни назад у світи"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Згортає запускач під час запуску процесу Minecraft."
"message": "Згорнути лаунчер під час запуску процесу Minecraft."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Мінімізувати запускач"
"message": "Згорнути панель запуску"
},
"app.appearance-settings.native-decorations.description": {
"message": "Використовувати рамку системного вікна (потрібно перезапустити програму)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Системне оформлення"
"message": "Рідні прикраси"
},
"app.appearance-settings.select-option": {
"message": "Виберіть опцію"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Умикає можливість перемикання бокової панелі."
"message": "Вмикає можливість перемикання бічної панелі."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Перемикання бокової панелі"
"message": "Перемикання бічної панелі"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Якщо ви спробуєте встановити файл пакета Modrinth (.mrpack), який не розміщено на Modrinth, ми переконаємося, що ви розумієте ризики, перш ніж установлювати його."
"message": "Якщо ви спробуєте встановити файл пакета Modrinth (.mrpack), який не розміщено на Modrinth, ми переконаємося, що ви розумієте ризики, перш ніж встановлювати його."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Попереджати мене перед установленням невідомих збірок"
"message": "Попереджати мене перед встановленням невідомих модпаків"
},
"app.auth-servers.unreachable.body": {
"message": "Сервери автентифікації Minecraft можуть зараз не працювати. Перевірте з’єднання з інтернетом та спробуйте пізніше."
@@ -104,12 +104,6 @@
"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": "Додати до профілю"
},
@@ -122,9 +116,6 @@
"app.browse.already-added": {
"message": "Уже додано"
},
"app.browse.back-to-instance": {
"message": "Назад до профілю"
},
"app.browse.discover-content": {
"message": "Дослідити вміст"
},
@@ -132,25 +123,22 @@
"message": "Дослідити сервера"
},
"app.browse.hide-added-servers": {
"message": "Сховати наявні сервери"
"message": "Скрити наявні сервери"
},
"app.browse.project-type.modpacks": {
"message": "Збірки"
},
"app.browse.server-instance-content-warning": {
"message": "Додавання вмісту може порушити сумісність при приєднанні до сервера. Усі додані матеріали також будуть утрачені під час оновлення вмісту профілю сервера."
"message": "Модпаки"
},
"app.browse.server.installing": {
"message": "Установлення"
"message": "Встановлення"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Установлення збірки…"
"message": "Встановлення модпаку..."
},
"app.export-modal.description-placeholder": {
"message": "Уведіть опис збірки"
"message": "Уведіть опис збірки..."
},
"app.export-modal.export-button": {
"message": "Експортувати"
@@ -158,18 +146,12 @@
"app.export-modal.header": {
"message": "Експортувати збірку"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Уключити «{file}»?"
},
"app.export-modal.modpack-name-label": {
"message": "Назва збірки"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Назва збірки"
},
"app.export-modal.select-files-label": {
"message": "Змініть файли, які додані до експорту"
},
"app.export-modal.version-number-label": {
"message": "Номер версії"
},
@@ -177,7 +159,7 @@
"message": "1.0.0"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Усі дані вашого профілю будуть видалені назавжди, включно з вашими світами, налаштуваннями та всім установленим умістом."
"message": "Усі дані вашого профілю будуть видалені назавжди, включно з вашими світами, конфігураціями та усім встановленим контентом."
},
"app.instance.confirm-delete.admonition-header": {
"message": "Ця дія є незворотною"
@@ -222,13 +204,16 @@
"message": "Додати сервер"
},
"app.instance.worlds.browse-servers": {
"message": "Переглянути сервери"
"message": "Переглянути сервера"
},
"app.instance.worlds.delete-world-description": {
"message": "«{name}» буде **назавжди видалено** й у вас не буде можливости відновлення."
},
"app.instance.worlds.delete-world-title": {
"message": "Ви впевнені, що хочете назавжди видалити цей світ?"
},
"app.instance.worlds.filter-modded": {
"message": "Модифіковано"
"message": "Модифікований"
},
"app.instance.worlds.filter-offline": {
"message": "Поза мережею"
@@ -237,7 +222,7 @@
"message": "У мережі"
},
"app.instance.worlds.filter-vanilla": {
"message": "Стандартний"
"message": "Ванільний"
},
"app.instance.worlds.no-worlds-description": {
"message": "Додати сервер або переглянути, щоб почати"
@@ -245,11 +230,17 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Сервера або світи не додано"
},
"app.instance.worlds.remove-server-description": {
"message": "«{name}» буде видалено з вашого списку, включно в грі, і без можливости відновлення."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "«{name}» ({address}) буде видалено з вашого списку, включно в грі, і без можливости відновлення."
},
"app.instance.worlds.remove-server-title": {
"message": "Ви впевнені, що хочете видалити «{name}»?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Пошук {count} світів"
"message": "Пошук {count} світів..."
},
"app.instance.worlds.this-server": {
"message": "цей сервер"
@@ -263,6 +254,9 @@
"app.modal.install-to-play.install-button": {
"message": "Установити"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# мод} few {# мода} other {# модів}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Потрібна збірка"
},
@@ -287,15 +281,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "«{name}» потребує оновлення, щоб грати. Будь ласка, оновіть гру до останньої версії, щоб запустити її."
},
"app.project.install-button.already-installed": {
"message": "Проєкт уже встановлено"
},
"app.project.install-context.back-to-browse": {
"message": "Повернутися до дослідження"
},
"app.project.install-context.install-content-to-instance": {
"message": "Установити вміст до профілю"
},
"app.settings.developer-mode-enabled": {
"message": "Увімкнено режим розробника."
},
@@ -383,6 +368,9 @@
"friends.action.add-friend": {
"message": "Додати друга"
},
"friends.action.view-friend-requests": {
"message": "{count} запит{count, plural, one { у} few {и в} many {ів у} other {у в}} друзі"
},
"friends.add-friend.submit": {
"message": "Надіслати запит у друзі"
},
@@ -432,7 +420,7 @@
"message": "Немає друзів, які збігаються з «{query}»"
},
"friends.search-friends-placeholder": {
"message": "Пошук друзів"
"message": "Пошук друзів..."
},
"friends.section.heading": {
"message": "{title} — {count}"
@@ -480,7 +468,7 @@
"message": "Додавання файлів ({completed}/{total})"
},
"instance.files.save-as": {
"message": "Зберегти як"
"message": "Зберегти як..."
},
"instance.server-modal.address": {
"message": "Адреса"
@@ -596,24 +584,6 @@
"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": "Змінні середовища"
},
@@ -629,9 +599,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "Виділена пам’ять"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/шлях/до/java"
},
"instance.settings.tabs.window": {
"message": "Вікно"
},
@@ -707,21 +674,12 @@
"minecraft-account.add-account": {
"message": "Додати обліковий запис"
},
"minecraft-account.label": {
"message": "Майнкрафт аккаунт"
},
"minecraft-account.not-signed-in": {
"message": "Не ввійдено в систему"
},
"minecraft-account.remove-account": {
"message": "Видалити обліковий запис"
},
"minecraft-account.select-account": {
"message": "Обрати обліковий запис"
},
"minecraft-account.sign-in": {
"message": "Увійти в Майнкрафт"
},
"search.filter.locked.instance": {
"message": "Надано профілем"
},
@@ -759,7 +717,7 @@
"message": "Усе одно інсталювати"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Шкідливе програмне забезпечення часто поширюють через файли збірок, які публікуються на таких платформах, як Discord."
"message": "Шкідливе програмне забезпечення часто поширюють через файли модпаків, які публікуються на таких платформах, як Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Ми не змогли знайти цей файл на Modrinth. Ми рекомендуємо встановлювати файли лише з тих джерел яким ви довіряєте."
@@ -1,115 +1,10 @@
{
"app.action-bar.downloading-java": {
"message": "Đang tải xuống Java {version}"
},
"app.action-bar.downloads": {
"message": "Tải xuống"
},
"app.action-bar.hide-more-running-instances": {
"message": "Đóng các phiên bản đang chạy"
},
"app.action-bar.make-primary-instance": {
"message": "Đặt làm bản instance chính"
},
"app.action-bar.no-instances-running": {
"message": "Không có phiên bản nào đang chạy"
},
"app.action-bar.offline": {
"message": "Ngoại tuyến"
},
"app.action-bar.primary-instance": {
"message": "Bản instance chính"
},
"app.action-bar.show-more-running-instances": {
"message": "Hiện thêm các phiên bản đang chạy"
},
"app.action-bar.stop-instance": {
"message": "Dừng instance"
},
"app.action-bar.view-active-downloads": {
"message": "Xem các lượt tải xuống"
},
"app.action-bar.view-instance": {
"message": "Xem phiên bản"
},
"app.action-bar.view-logs": {
"message": "Xem logs"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Kích hoạt các tính năng dựng hình nâng cao như hiệu ứng làm mờ, có thể gây giảm hiệu năng nếu không có chế độ tăng tốc phần cứng."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Dựng hình nâng cao"
},
"app.appearance-settings.color-theme.description": {
"message": "Chọn màu nền ưu thích của bạn cho Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Màu chủ đề"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Thay đổi trang hiển thị khi khởi chạy launcher."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Trang chủ"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Thư viện"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Trang mặc định"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Tắt hiển thị thẻ tên phía trên người chơi ở trang skin."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Ẩn thẻ tên"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Hiển thị các thế giới gần đây trong mục \"Tiếp tục chơi\" ở Trang chủ."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Tiếp tục chơi"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Thu nhỏ launcher khi game Minecraft bắt đầu chạy."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Thu nhỏ launcher"
},
"app.appearance-settings.native-decorations.description": {
"message": "Sử dụng khung cửa sổ hệ thống (yêu cầu khởi động lại ứng dụng)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Giao diện cửa sổ hệ thống"
},
"app.appearance-settings.select-option": {
"message": "Chọn một tuỳ chọn"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Bật tính năng ẩn hiển thị thanh menu bên."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Ẩn/hiện thanh bên"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Nếu bạn cố gắng cài đặt tệp Modrinth Pack (.mrpack) không được lưu trữ trên máy chủ Modrinth, chúng tôi sẽ đảm bảo bạn hiểu rõ các rủi ro trước khi cài đặt."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Cảnh báo tôi trước khi cài đặt các modpacks không rõ nguồn gốc"
},
"app.auth-servers.unreachable.body": {
"message": "Máy chủ xác thực của Minecraft có thể đang bị sập. Hãy kiểm tra kết nối Internet của bạn và thử lại sau."
},
"app.auth-servers.unreachable.header": {
"message": "Không thể kết nối đến máy chủ xác thực"
},
"app.browse.add-servers-to-instance": {
"message": "Thêm máy chủ vào phiên bản"
},
"app.browse.add-to-an-instance": {
"message": "Thêm vào phiên bản"
},
"app.browse.add-to-instance": {
"message": "Thêm vào hồ sơ"
},
@@ -122,33 +17,12 @@
"app.browse.already-added": {
"message": "Đã được thêm"
},
"app.browse.back-to-instance": {
"message": "Quay lại phiên bản"
},
"app.browse.discover-content": {
"message": "Khám phá nội dung"
},
"app.browse.discover-servers": {
"message": "Khám phá máy chủ"
},
"app.browse.hide-added-servers": {
"message": "Ẩn các server đã thêm"
},
"app.browse.project-type.modpacks": {
"message": "Modpack"
},
"app.browse.server-instance-content-warning": {
"message": "Thêm nội dung có thể gây lỗi tương thích khi tham gia máy chủ. Bất kỳ nội dung nào được thêm vào cũng sẽ bị mất khi bạn cập nhật nội dung của phiên bản máy chủ."
},
"app.browse.server.installing": {
"message": "Đang cài đặt"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Đang cài đặt modpack..."
},
"app.export-modal.description-placeholder": {
"message": "Thêm miêu tả cho gói modpack..."
},
@@ -158,18 +32,12 @@
"app.export-modal.header": {
"message": "Xuất modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Bao gồm \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Tên modpack"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Tên modpack"
},
"app.export-modal.select-files-label": {
"message": "Cấu hình các tệp nào được bao gồm trong quá trình xuất phiên bản này"
},
"app.export-modal.version-number-label": {
"message": "Phiên bản"
},
@@ -299,15 +167,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "Bạn cần cập nhật {name} để có thể chơi. Vui lòng cập nhật lên bản mới nhất để khởi chạy trò chơi."
},
"app.project.install-button.already-installed": {
"message": "Dự án này đã được cài đặt"
},
"app.project.install-context.back-to-browse": {
"message": "Quay lại trang Khám Phá"
},
"app.project.install-context.install-content-to-instance": {
"message": "Cài đặt nội dung vào phiên bản"
},
"app.settings.developer-mode-enabled": {
"message": "Chế độ nhà phát triển đã được bật."
},
@@ -611,24 +470,6 @@
"instance.settings.tabs.java": {
"message": "Java và bộ nhớ"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Tùy chọn biến môi trường"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Tùy chỉnh tham số Java"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Tùy chỉnh phiên bản cài đặt Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Tùy chỉnh phân bổ bộ nhớ"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Nhập biến môi trường..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Nhập tham số Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Biến môi trường"
},
@@ -644,9 +485,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "Bộ nhớ phân bổ"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/đường_dẫn/tới/java"
},
"instance.settings.tabs.window": {
"message": "Cửa sổ"
},
@@ -719,24 +557,6 @@
"instance.worlds.world_in_use": {
"message": "Thế giới đang mở"
},
"minecraft-account.add-account": {
"message": "Thêm tài khoản"
},
"minecraft-account.label": {
"message": "Tài khoản Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Chưa đăng nhập"
},
"minecraft-account.remove-account": {
"message": "Xoá tài khoản"
},
"minecraft-account.select-account": {
"message": "Chọn tài khoản"
},
"minecraft-account.sign-in": {
"message": "Đăng nhập vào Minecraft"
},
"search.filter.locked.instance": {
"message": "Được cung cấp bởi phiên bản"
},
@@ -760,26 +580,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "Loader được cung cấp bởi máy chủ"
},
"unknown-pack-warning-modal.body": {
"message": "Tệp chỉ được xem xét nếu nó được tải lên Modrinth, bất kể định dạng tệp của nó là gì (bao gồm cả .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Đừng hiển thị cảnh báo này nữa"
},
"unknown-pack-warning-modal.header": {
"message": "Xác nhận cài đặt"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Tiếp tục cài đặt"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Phần mềm độc hại thường được phát tán thông qua các tệp modpack bằng cách chia sẻ chúng trên các nền tảng như Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Chúng tôi không tìm thấy tập tin này trên Modrinth. Chúng tôi đặc biệt khuyên bạn chỉ nên cài đặt các tập tin từ các nguồn đáng tin cậy."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Cảnh báo tệp không xác định"
}
}
+18 -3
View File
@@ -63,7 +63,7 @@
"message": "在皮肤页面中禁用你玩家头顶上的名牌。"
},
"app.appearance-settings.hide-nametag.title": {
"message": "隐藏名称标签"
"message": "隐藏名"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "在主页的“快速回到”部分包含最近的世界。"
@@ -224,6 +224,9 @@
"app.instance.worlds.browse-servers": {
"message": "浏览服务器"
},
"app.instance.worlds.delete-world-description": {
"message": "“{name}”将被**永久删除**,并且无法恢复。"
},
"app.instance.worlds.delete-world-title": {
"message": "你确定要永久删除这个世界吗?"
},
@@ -245,6 +248,12 @@
"app.instance.worlds.no-worlds-heading": {
"message": "未添加服务器或世界"
},
"app.instance.worlds.remove-server-description": {
"message": "将从你的列表中删除“{name}”,包括游戏内,并且无法恢复。"
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "将从你的列表中删除“{name}”({address}),包括游戏内,并且无法恢复。"
},
"app.instance.worlds.remove-server-title": {
"message": "你确定要删除 {name} 吗?"
},
@@ -263,6 +272,9 @@
"app.modal.install-to-play.install-button": {
"message": "安装"
},
"app.modal.install-to-play.mod-count": {
"message": "{count} 个模组"
},
"app.modal.install-to-play.required-modpack": {
"message": "需求整合包"
},
@@ -291,7 +303,7 @@
"message": "此项目已安装"
},
"app.project.install-context.back-to-browse": {
"message": "返回发现页"
"message": "返回浏览器"
},
"app.project.install-context.install-content-to-instance": {
"message": "将内容安装到实例中"
@@ -383,6 +395,9 @@
"friends.action.add-friend": {
"message": "添加好友"
},
"friends.action.view-friend-requests": {
"message": "{count} 个好友请求"
},
"friends.add-friend.submit": {
"message": "发送好友请求"
},
@@ -552,7 +567,7 @@
"message": "启动 Hooks"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "自定义启动 Hooks"
"message": "自定义启动Hooks"
},
"instance.settings.tabs.hooks.description": {
"message": "Hooks允许高级用户在启动游戏前后运行特定的系统命令。"
+19 -25
View File
@@ -147,10 +147,10 @@
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "正在安裝模組包……"
"message": "正在安裝模組包..."
},
"app.export-modal.description-placeholder": {
"message": "輸入模組包描述……"
"message": "輸入模組包描述..."
},
"app.export-modal.export-button": {
"message": "匯出"
@@ -224,6 +224,9 @@
"app.instance.worlds.browse-servers": {
"message": "瀏覽伺服器"
},
"app.instance.worlds.delete-world-description": {
"message": "「{name}」將**永久刪除**,且無法還原。"
},
"app.instance.worlds.delete-world-title": {
"message": "確定要永久刪除這個世界嗎?"
},
@@ -245,11 +248,17 @@
"app.instance.worlds.no-worlds-heading": {
"message": "未新增任何伺服器或世界"
},
"app.instance.worlds.remove-server-description": {
"message": "「{name}」將從你的清單中移除(包含遊戲內),且無法還原。"
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "「{name}」({address}) 將從你的清單中移除(包含遊戲內),且無法還原。"
},
"app.instance.worlds.remove-server-title": {
"message": "確定要移除「{name}」嗎?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "搜尋 {count} 個世界……"
"message": "搜尋 {count} 個世界..."
},
"app.instance.worlds.this-server": {
"message": "這個伺服器"
@@ -263,6 +272,9 @@
"app.modal.install-to-play.install-button": {
"message": "安裝"
},
"app.modal.install-to-play.mod-count": {
"message": "{count} 個模組"
},
"app.modal.install-to-play.required-modpack": {
"message": "所需模組包"
},
@@ -291,7 +303,7 @@
"message": "這個專案已安裝"
},
"app.project.install-context.back-to-browse": {
"message": "返回瀏覽"
"message": "回到瀏覽頁面"
},
"app.project.install-context.install-content-to-instance": {
"message": "將內容安裝至實例"
@@ -383,6 +395,9 @@
"friends.action.add-friend": {
"message": "新增好友"
},
"friends.action.view-friend-requests": {
"message": "{count} 個好友邀請"
},
"friends.add-friend.submit": {
"message": "送出好友邀請"
},
@@ -596,24 +611,6 @@
"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": "環境變數"
},
@@ -629,9 +626,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "記憶體配置"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "視窗"
},
File diff suppressed because it is too large Load Diff
+10 -44
View File
@@ -218,11 +218,7 @@
:key="instance.path"
>
<template v-if="Component">
<Suspense
:key="instance.path"
@pending="subpagePending = true"
@resolve="subpagePending = false"
>
<Suspense :key="instance.path">
<component
:is="Component"
:instance="instance"
@@ -232,7 +228,6 @@
:installed="instance.install_stage !== 'installed'"
:is-server-instance="isServerInstance"
:open-settings="() => settingsModal?.show(1)"
v-bind="contentSubpageProps"
@play="updatePlayState"
@stop="() => stopInstance('InstanceSubpage')"
></component>
@@ -300,7 +295,6 @@ import {
ServerPing,
ServerRecentPlays,
ServerRegion,
useLoadingBarToken,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
@@ -318,7 +312,6 @@ import { useInstanceConsole } from '@/composables/useInstanceConsole'
import { trackEvent } from '@/helpers/analytics'
import { get_project_v3 } from '@/helpers/cache.js'
import { process_listener, profile_listener } from '@/helpers/events'
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
import { get_by_profile_path } from '@/helpers/process'
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
import type { GameInstance } from '@/helpers/types'
@@ -338,7 +331,6 @@ const route = useRoute()
const router = useRouter()
const breadcrumbs = useBreadcrumbs()
const contentSubpageRouteNames = new Set(['Mods', 'ModsFilter'])
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
@@ -349,16 +341,12 @@ window.addEventListener('online', () => {
})
const instance = ref<GameInstance>()
const preloadedContent = ref<InstanceContentData | null>(null)
const playing = ref(false)
const loading = ref(false)
const subpagePending = ref(false)
const stopping = ref(false)
const exportModal = ref<InstanceType<typeof ExportModal>>()
const updateToPlayModal = ref<InstanceType<typeof UpdateToPlayModal>>()
useLoadingBarToken(subpagePending)
const isServerInstance = ref(false)
const linkedProjectV3 = ref<Labrinth.Projects.v3.Project>()
const selected = ref<unknown[]>([])
@@ -373,55 +361,36 @@ const playersOnline = ref<number | undefined>(undefined)
const ping = ref<number | undefined>(undefined)
const loadingServerPing = ref(false)
function isContentSubpageRoute(routeName = route.name) {
return typeof routeName === 'string' && contentSubpageRouteNames.has(routeName)
}
async function fetchInstance() {
isServerInstance.value = false
linkedProjectV3.value = undefined
preloadedContent.value = null
ping.value = undefined
playersOnline.value = undefined
loadingServerPing.value = false
const nextInstance = await get(route.params.id as string).catch(handleError)
let nextLinkedProjectV3: Labrinth.Projects.v3.Project | undefined
let nextIsServerInstance = false
instance.value = await get(route.params.id as string).catch(handleError)
const contentPreloadPromise =
nextInstance && isContentSubpageRoute()
? loadInstanceContentData(nextInstance.path, undefined, handleError)
: Promise.resolve(null)
if (!offline.value && nextInstance?.linked_data && nextInstance.linked_data.project_id) {
if (!offline.value && instance.value?.linked_data && instance.value.linked_data.project_id) {
try {
nextLinkedProjectV3 = await get_project_v3(
nextInstance.linked_data.project_id,
linkedProjectV3.value = await get_project_v3(
instance.value.linked_data.project_id,
'must_revalidate',
)
if (nextLinkedProjectV3?.minecraft_server != null) {
nextIsServerInstance = true
if (linkedProjectV3.value?.minecraft_server != null) {
isServerInstance.value = true
}
} catch (error) {
handleError(error as Error)
}
}
const nextPreloadedContent = await contentPreloadPromise
instance.value = nextInstance ?? undefined
linkedProjectV3.value = nextLinkedProjectV3
isServerInstance.value = nextIsServerInstance
preloadedContent.value = nextPreloadedContent
fetchDeferredData()
if (nextInstance) {
if (instance.value) {
queryClient.prefetchQuery({
queryKey: ['worlds', nextInstance.path],
queryFn: () => refreshWorlds(nextInstance.path),
queryKey: ['worlds', instance.value.path],
queryFn: () => refreshWorlds(instance.value!.path),
staleTime: 30_000,
})
}
@@ -479,9 +448,6 @@ const renderMode = computed<'scroll' | 'fixed'>(() =>
route.meta.renderMode === 'fixed' ? 'fixed' : 'scroll',
)
const isFixedRender = computed(() => renderMode.value === 'fixed')
const contentSubpageProps = computed(() =>
isContentSubpageRoute() ? { preloadedContent: preloadedContent.value } : {},
)
const tabs = computed(() => [
{
@@ -77,7 +77,7 @@ function buildLogList(rawLogs) {
log.filename !== 'latest_stdout.log' &&
log.filename !== 'latest_stdout' &&
log.filename !== 'launcher_log.txt' &&
(log.output == null || log.output !== '') &&
log.stdout !== '' &&
(log.filename.includes('.log') || log.filename.endsWith('.txt')),
)
.map((log) => ({
+108 -399
View File
@@ -13,7 +13,6 @@
:modpack-name="linkedModpackProject?.title"
:modpack-icon-url="linkedModpackProject?.icon_url ?? undefined"
:enable-toggle="!props.isServerInstance"
:busy="isBulkOperating"
:get-overflow-options="getOverflowOptions"
:switch-version="handleSwitchVersion"
@update:enabled="handleModpackContentToggle"
@@ -88,12 +87,12 @@ import {
ReadyTransition,
useDebugLogger,
useVIntl,
versionChangesGameVersion,
} from '@modrinth/ui'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { open } from '@tauri-apps/plugin-dialog'
import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import ExportModal from '@/components/ui/ExportModal.vue'
@@ -101,20 +100,22 @@ import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_version } from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events.js'
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
import {
add_project_from_path,
add_project_from_version,
duplicate,
edit,
get,
get_content_items,
get_linked_modpack_content,
get_linked_modpack_info,
list,
remove_project,
toggle_disable_project,
update_managed_modrinth_version,
update_project,
} from '@/helpers/profile'
import { get_categories } from '@/helpers/tags.js'
import type { CacheBehaviour, GameInstance } from '@/helpers/types'
import { highlightModInProfile } from '@/helpers/utils.js'
import { injectContentInstall } from '@/providers/content-install'
@@ -159,7 +160,6 @@ const props = defineProps<{
instance: GameInstance
isServerInstance?: boolean
openSettings?: () => void
preloadedContent?: InstanceContentData | null
}>()
const loading = ref(true)
@@ -212,7 +212,6 @@ const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal> | null>
const modpackContentModal = ref<InstanceType<typeof ModpackContentModal> | null>()
const modpackUpdateConfirmModal = ref<InstanceType<typeof ConfirmModpackUpdateModal> | null>()
// TODO: Extract content operation and updater modal state into composables; this page currently owns file mutations, dependency installs, busy flags, and version selection flow.
const updatingProject = ref<ContentItem | null>(null)
const updatingProjectVersions = ref<Labrinth.Versions.v2.Version[]>([])
const loadingVersions = ref(false)
@@ -220,82 +219,6 @@ const loadingChangelog = ref(false)
const updatingModpack = ref(false)
const pendingModpackUpdateVersion = ref<Labrinth.Versions.v2.Version | null>(null)
const isModpackUpdateDowngrade = ref(false)
const activeContentOperationKeys = ref(new Set<string>())
let activeContentOperationCount = 0
let updateRequestId = 0
const activeUpdateRequestId = ref(0)
function fileNameFromPath(path: string) {
return path.split('/').pop() ?? path
}
function getContentItemId(item: ContentItem | null | undefined) {
return item?.file_path ?? item?.file_name ?? item?.id ?? ''
}
function getContentOperationKeys(item: ContentItem) {
return [getContentItemId(item), item.file_path, item.file_name].filter(
(key): key is string => !!key,
)
}
function hasContentOperation(item: ContentItem) {
const keys = getContentOperationKeys(item)
return keys.some((key) => activeContentOperationKeys.value.has(key))
}
function setContentItemBusy(item: ContentItem, busy: boolean, originalFileName = item.file_name) {
item.installing = busy
modpackContentModal.value?.updateItem(originalFileName, {
installing: busy,
disabled: busy,
})
if (item.file_name !== originalFileName) {
modpackContentModal.value?.updateItem(item.file_name, {
installing: busy,
disabled: busy,
})
}
}
function beginContentOperation(item: ContentItem) {
if (hasContentOperation(item)) return null
const keys = getContentOperationKeys(item)
activeContentOperationKeys.value = new Set([...activeContentOperationKeys.value, ...keys])
activeContentOperationCount++
isBulkOperating.value = true
setContentItemBusy(item, true)
return { keys, originalFileName: item.file_name }
}
function finishContentOperation(
item: ContentItem,
operation: { keys: string[]; originalFileName: string },
) {
const nextKeys = new Set(activeContentOperationKeys.value)
for (const key of operation.keys) {
nextKeys.delete(key)
}
activeContentOperationKeys.value = nextKeys
activeContentOperationCount = Math.max(0, activeContentOperationCount - 1)
setContentItemBusy(item, false, operation.originalFileName)
if (activeContentOperationCount === 0) {
isBulkOperating.value = false
}
}
function beginUpdateRequest() {
updateRequestId++
activeUpdateRequestId.value = updateRequestId
return updateRequestId
}
function isActiveUpdateRequest(requestId: number) {
return activeUpdateRequestId.value === requestId
}
async function handleBrowseContent() {
if (!props.instance) return
@@ -342,21 +265,9 @@ async function handleUploadFiles() {
}
async function toggleDisableMod(mod: ContentItem) {
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
try {
const newPath = await toggle_disable_project(props.instance.path, mod.file_path)
const newFileName = fileNameFromPath(newPath)
mod.file_path = newPath
mod.file_name = newFileName
mod.file_path = await toggle_disable_project(props.instance.path, mod.file_path!)
mod.enabled = !mod.enabled
modpackContentModal.value?.updateItem(operation.originalFileName, {
file_path: newPath,
file_name: newFileName,
enabled: mod.enabled,
})
trackEvent('InstanceProjectDisable', {
loader: props.instance.loader,
@@ -368,48 +279,33 @@ async function toggleDisableMod(mod: ContentItem) {
})
} catch (err) {
handleError(err as Error)
} finally {
finishContentOperation(mod, operation)
}
}
const toggleDisableDebounced = toggleDisableMod
const toggleDisableDebounced = useDebounceFn(toggleDisableMod, 20)
async function removeMod(mod: ContentItem) {
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
await remove_project(props.instance.path, mod.file_path!).catch(handleError)
projects.value = projects.value.filter((x) => mod.file_path !== x.file_path)
try {
const removedPath = mod.file_path
await remove_project(props.instance.path, removedPath)
projects.value = projects.value.filter((x) => removedPath !== x.file_path)
trackEvent('InstanceProjectRemove', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.project?.id,
name: mod.project?.title ?? mod.file_name,
project_type: mod.project_type,
})
} catch (err) {
handleError(err as Error)
} finally {
finishContentOperation(mod, operation)
}
trackEvent('InstanceProjectRemove', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.project?.id,
name: mod.project?.title ?? mod.file_name,
project_type: mod.project_type,
})
}
async function updateProject(mod: ContentItem) {
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
try {
const updateVersionId = mod.update_version_id
await update_project(props.instance.path, mod.file_path)
const newPath = await update_project(props.instance.path, mod.file_path!)
mod.file_path = newPath
if (updateVersionId) {
const versionData = await get_version(updateVersionId, 'must_revalidate').catch(handleError)
if (mod.update_version_id) {
const versionData = await get_version(mod.update_version_id, 'must_revalidate').catch(
handleError,
)
if (versionData) {
const profile = await get(props.instance.path).catch(handleError)
@@ -420,6 +316,12 @@ async function updateProject(mod: ContentItem) {
}
}
mod.has_update = false
if (mod.version && mod.update_version_id) {
mod.version.id = mod.update_version_id
}
mod.update_version_id = null
trackEvent('InstanceProjectUpdate', {
loader: props.instance.loader,
game_version: props.instance.game_version,
@@ -429,65 +331,39 @@ async function updateProject(mod: ContentItem) {
})
} catch (err) {
handleError(err as Error)
} finally {
await refreshContentState('must_revalidate')
finishContentOperation(mod, operation)
}
}
async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions.v2.Version) {
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
const oldPath = mod.file_path
const wasDisabled = mod.enabled === false || oldPath.endsWith('.disabled')
let newPath: string | null = null
let shouldRemoveNewOnError = false
isBulkOperating.value = true
mod.installing = true
if (mod.version) {
mod.version.id = version.id
mod.version.version_number = version.version_number
}
try {
newPath = await add_project_from_version(props.instance.path, version.id, 'update')
shouldRemoveNewOnError = newPath !== oldPath
if (wasDisabled) {
newPath = await toggle_disable_project(props.instance.path, newPath)
}
await remove_project(props.instance.path, mod.file_path!)
const newPath = await add_project_from_version(props.instance.path, version.id, 'standalone')
const profile = await get(props.instance.path).catch(handleError)
if (profile) {
await installVersionDependencies(profile, version, 'update').catch(handleError)
}
shouldRemoveNewOnError = false
if (newPath !== oldPath) {
await remove_project(props.instance.path, oldPath)
}
trackEvent('InstanceProjectUpdate', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.project?.id,
name: mod.project?.title ?? mod.file_name,
project_type: mod.project_type,
})
mod.file_path = newPath
} catch (err) {
if (shouldRemoveNewOnError && newPath && newPath !== oldPath) {
await remove_project(props.instance.path, newPath).catch(() => {})
}
handleError(err as Error)
} finally {
await refreshContentState('must_revalidate')
finishContentOperation(mod, operation)
mod.installing = false
isBulkOperating.value = false
await initProjects()
}
}
async function handleUpdate(id: string) {
const item = projects.value.find((p) => getContentItemId(p) === id)
const item = projects.value.find((p) => p.id === id)
if (!item?.has_update || !item.project?.id || !item.version?.id) return
const requestId = beginUpdateRequest()
const itemId = getContentItemId(item)
debug('handleUpdate triggered', {
fileName: item.file_name,
projectType: item.project_type,
@@ -508,48 +384,12 @@ async function handleUpdate(id: string) {
await nextTick()
const initialVersionId = item.update_version_id ?? undefined
debug('handleUpdate: opening content updater modal', {
type: 'content',
initialVersionId,
item: {
id: item.id,
fileName: item.file_name,
projectType: item.project_type,
projectId: item.project.id,
projectTitle: item.project.title,
currentVersionId: item.version.id,
currentVersionNumber: item.version.version_number,
updateVersionId: item.update_version_id,
},
instance: {
path: props.instance.path,
name: props.instance.name,
gameVersion: props.instance.game_version,
loader: props.instance.loader,
linkedData: props.instance.linked_data,
},
modalStateBeforeFetch: {
updatingModpack: updatingModpack.value,
updatingProjectId: updatingProject.value?.id,
updatingProjectVersions: updatingProjectVersions.value.map((version) => ({
id: version.id,
versionNumber: version.version_number,
gameVersions: version.game_versions,
loaders: version.loaders,
datePublished: version.date_published,
})),
},
})
contentUpdaterModal.value?.show(initialVersionId)
contentUpdaterModal.value?.show(item.update_version_id ?? undefined)
const versions = (await get_project_versions(item.project.id).catch((e) => {
return handleError(e)
})) as Labrinth.Versions.v2.Version[] | null
if (!isActiveUpdateRequest(requestId) || getContentItemId(updatingProject.value) !== itemId)
return
loadingVersions.value = false
if (!versions) {
@@ -574,25 +414,6 @@ async function handleUpdate(id: string) {
versions.sort(
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
)
const preselectedVersion =
versions.find((version) => version.id === initialVersionId) ?? versions[0] ?? null
debug('handleUpdate: resolved content updater preselection', {
type: 'content',
initialVersionId,
foundInitialVersion: versions.some((version) => version.id === initialVersionId),
preselectedVersion: preselectedVersion
? {
id: preselectedVersion.id,
versionNumber: preselectedVersion.version_number,
gameVersions: preselectedVersion.game_versions,
loaders: preselectedVersion.loaders,
datePublished: preselectedVersion.date_published,
}
: null,
versionCount: versions.length,
currentVersionId: item.version.id,
updateVersionId: item.update_version_id,
})
updatingProjectVersions.value = versions
}
@@ -600,9 +421,6 @@ async function handleUpdate(id: string) {
async function handleSwitchVersion(item: ContentItem) {
if (!item.project?.id || !item.version?.id) return
const requestId = beginUpdateRequest()
const itemId = getContentItemId(item)
updatingModpack.value = false
updatingProject.value = item
updatingProjectVersions.value = []
@@ -617,9 +435,6 @@ async function handleSwitchVersion(item: ContentItem) {
return handleError(e)
})) as Labrinth.Versions.v2.Version[] | null
if (!isActiveUpdateRequest(requestId) || getContentItemId(updatingProject.value) !== itemId)
return
loadingVersions.value = false
if (!versions) return
@@ -653,28 +468,9 @@ async function handleModpackContent() {
}
}
async function refreshModpackContentItems(cacheBehaviour?: CacheBehaviour) {
if (!props.instance?.path) return
const contentItems = await get_linked_modpack_content(props.instance.path, cacheBehaviour).catch(
handleError,
)
if (contentItems) {
modpackContentModal.value?.setItems(contentItems)
}
}
async function refreshContentState(cacheBehaviour?: CacheBehaviour) {
await initProjects(cacheBehaviour)
await refreshModpackContentItems(cacheBehaviour)
}
async function handleModpackUpdate() {
if (!props.instance?.linked_data?.project_id) return
const requestId = beginUpdateRequest()
updatingModpack.value = true
updatingProject.value = null
updatingProjectVersions.value = []
@@ -683,42 +479,14 @@ async function handleModpackUpdate() {
await nextTick()
const initialVersionId =
linkedModpackUpdateVersionId.value ?? props.instance?.linked_data?.version_id ?? undefined
debug('handleModpackUpdate: opening modpack updater modal', {
type: 'modpack',
initialVersionId,
linkedModpackUpdateVersionId: linkedModpackUpdateVersionId.value,
linkedModpackProject: linkedModpackProject.value,
linkedModpackVersion: linkedModpackVersion.value,
linkedModpackHasUpdate: linkedModpackHasUpdate.value,
instance: {
path: props.instance.path,
name: props.instance.name,
gameVersion: props.instance.game_version,
loader: props.instance.loader,
linkedData: props.instance.linked_data,
},
modalStateBeforeFetch: {
updatingModpack: updatingModpack.value,
updatingProjectId: updatingProject.value?.id,
updatingProjectVersions: updatingProjectVersions.value.map((version) => ({
id: version.id,
versionNumber: version.version_number,
gameVersions: version.game_versions,
loaders: version.loaders,
datePublished: version.date_published,
})),
},
})
contentUpdaterModal.value?.show(initialVersionId)
contentUpdaterModal.value?.show(
linkedModpackUpdateVersionId.value ?? props.instance?.linked_data?.version_id ?? undefined,
)
const versions = (await get_project_versions(props.instance.linked_data.project_id).catch(
handleError,
)) as Labrinth.Versions.v2.Version[] | null
if (!isActiveUpdateRequest(requestId) || !updatingModpack.value) return
loadingVersions.value = false
if (!versions) return
@@ -726,25 +494,6 @@ async function handleModpackUpdate() {
versions.sort(
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
)
const preselectedVersion =
versions.find((version) => version.id === initialVersionId) ?? versions[0] ?? null
debug('handleModpackUpdate: resolved modpack updater preselection', {
type: 'modpack',
initialVersionId,
foundInitialVersion: versions.some((version) => version.id === initialVersionId),
preselectedVersion: preselectedVersion
? {
id: preselectedVersion.id,
versionNumber: preselectedVersion.version_number,
gameVersions: preselectedVersion.game_versions,
loaders: preselectedVersion.loaders,
datePublished: preselectedVersion.date_published,
}
: null,
versionCount: versions.length,
linkedModpackUpdateVersionId: linkedModpackUpdateVersionId.value,
currentLinkedVersionId: props.instance.linked_data.version_id,
})
updatingProjectVersions.value = versions
}
@@ -753,12 +502,10 @@ async function fetchAndSpliceVersion(
versionId: string,
cacheBehaviour?: Parameters<typeof get_version>[1],
onError?: (err: unknown) => void,
requestId = activeUpdateRequestId.value,
) {
const fullVersion = (await get_version(versionId, cacheBehaviour).catch(
onError ?? (() => null),
)) as Labrinth.Versions.v2.Version | null
if (!isActiveUpdateRequest(requestId)) return
if (!fullVersion) return
const index = updatingProjectVersions.value.findIndex((v) => v.id === versionId)
if (index !== -1) {
@@ -770,26 +517,17 @@ async function fetchAndSpliceVersion(
async function handleVersionSelect(version: Labrinth.Versions.v2.Version) {
if (version.changelog != null) return
const requestId = activeUpdateRequestId.value
loadingChangelog.value = true
await fetchAndSpliceVersion(
version.id,
'must_revalidate',
handleError as (err: unknown) => void,
requestId,
)
if (isActiveUpdateRequest(requestId)) {
loadingChangelog.value = false
}
await fetchAndSpliceVersion(version.id, 'must_revalidate', handleError as (err: unknown) => void)
loadingChangelog.value = false
}
async function handleVersionHover(version: Labrinth.Versions.v2.Version) {
if (version.changelog != null) return
await fetchAndSpliceVersion(version.id, undefined, undefined, activeUpdateRequestId.value)
await fetchAndSpliceVersion(version.id)
}
function resetUpdateState() {
activeUpdateRequestId.value = 0
updatingModpack.value = false
updatingProject.value = null
updatingProjectVersions.value = []
@@ -797,23 +535,13 @@ function resetUpdateState() {
loadingChangelog.value = false
}
async function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.Version) {
function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.Version) {
pendingModpackUpdateVersion.value = selectedVersion
const currentVersionId = props.instance?.linked_data?.version_id
const currentVersion = updatingProjectVersions.value.find((v) => v.id === currentVersionId)
isModpackUpdateDowngrade.value = currentVersion
? new Date(selectedVersion.date_published) < new Date(currentVersion.date_published)
: false
const shouldShowWarning =
isModpackUpdateDowngrade.value ||
versionChangesGameVersion(selectedVersion, props.instance.game_version)
if (!shouldShowWarning) {
await handleModpackUpdateConfirm()
return
}
modpackUpdateConfirmModal.value?.show()
}
@@ -823,7 +551,6 @@ async function handleModpackUpdateConfirm() {
const version = pendingModpackUpdateVersion.value
pendingModpackUpdateVersion.value = null
contentUpdaterModal.value?.hide()
isModpackUpdating.value = true
try {
await update_managed_modrinth_version(props.instance.path, version.id)
@@ -847,7 +574,7 @@ async function handleModalUpdate(
pendingModpackUpdateVersion.value = selectedVersion
await handleModpackUpdateConfirm()
} else {
await handleModpackUpdateRequest(selectedVersion)
handleModpackUpdateRequest(selectedVersion)
}
} else if (updatingProject.value) {
const mod = updatingProject.value
@@ -935,31 +662,51 @@ function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
async function initProjects(cacheBehaviour?: CacheBehaviour) {
if (!props.instance) return
const contentData = await loadInstanceContentData(
props.instance.path,
cacheBehaviour,
handleError,
)
applyContentData(contentData)
}
const [contentItems, modpackInfo, allCategories] = await Promise.all([
get_content_items(props.instance.path, cacheBehaviour).catch(handleError),
get_linked_modpack_info(props.instance.path, cacheBehaviour).catch(handleError),
get_categories().catch(handleError),
])
function applyContentData(contentData: InstanceContentData) {
if (contentData.path !== props.instance.path) return false
if (!contentData.contentItems) {
if (!contentItems) {
loading.value = false
return true
return
}
projects.value = contentData.contentItems
projects.value = contentItems
if (contentData.modpack) {
linkedModpackProject.value = contentData.modpack.project
linkedModpackVersion.value = contentData.modpack.version
linkedModpackOwner.value = contentData.modpack.owner
linkedModpackCategories.value = contentData.modpack.categories
linkedModpackHasUpdate.value = contentData.modpack.hasUpdate
linkedModpackUpdateVersionId.value = contentData.modpack.updateVersionId
if (modpackInfo) {
linkedModpackProject.value = {
...modpackInfo.project,
slug: modpackInfo.project.slug ?? modpackInfo.project.id,
icon_url: modpackInfo.project.icon_url ?? undefined,
}
linkedModpackVersion.value = {
...modpackInfo.version,
date_published: modpackInfo.version.date_published.toString(),
}
linkedModpackOwner.value = modpackInfo.owner
? {
...modpackInfo.owner,
avatar_url: modpackInfo.owner.avatar_url ?? undefined,
}
: null
linkedModpackHasUpdate.value = modpackInfo.has_update
linkedModpackUpdateVersionId.value = modpackInfo.update_version_id
if (allCategories && modpackInfo.project.categories) {
const seen = new Set<string>()
linkedModpackCategories.value = allCategories.filter((cat: { name: string }) => {
if (modpackInfo.project.categories.includes(cat.name) && !seen.has(cat.name)) {
seen.add(cat.name)
return true
}
return false
})
} else {
linkedModpackCategories.value = []
}
} else {
linkedModpackProject.value = null
linkedModpackVersion.value = null
@@ -970,7 +717,6 @@ function applyContentData(contentData: InstanceContentData) {
}
loading.value = false
return true
}
provideAppBackup({
@@ -1063,9 +809,8 @@ provideContentManager({
showContentHint,
dismissContentHint,
shareItems: handleShareItems,
getItemId: getContentItemId,
mapToTableItem: (item: ContentItem) => ({
id: getContentItemId(item),
id: item.id,
project: item.project ?? {
id: item.file_name,
slug: null,
@@ -1099,22 +844,10 @@ provideContentManager({
filterPersistKey: props.instance.path,
})
type UnlistenFn = () => void
const initialContentReady = loadInitialContent()
void initialContentReady.then(restoreModpackContentModalState).catch(handleError)
function loadInitialContent() {
if (props.preloadedContent && applyContentData(props.preloadedContent)) {
return Promise.resolve()
}
return initProjects()
}
async function restoreModpackContentModalState() {
if (!savedModalState) return
await initProjects()
// Restore modpack content modal state if returning via back navigation
if (savedModalState) {
const stateToRestore = savedModalState
savedModalState = null
await nextTick()
@@ -1127,32 +860,18 @@ const removeBeforeEach = router.beforeEach(() => {
savedModalState = state ?? null
})
let isUnmounted = false
let unlistenDragDrop: UnlistenFn | null = null
let unlistenProfiles: UnlistenFn | null = null
const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
if (event.payload.type !== 'drop' || !props.instance) return
onMounted(() => {
void getCurrentWebview()
.onDragDropEvent(async (event) => {
if (event.payload.type !== 'drop' || !props.instance) return
for (const file of event.payload.paths) {
if (file.endsWith('.mrpack')) continue
await add_project_from_path(props.instance.path, file).catch(handleError)
}
await initProjects()
})
for (const file of event.payload.paths) {
if (file.endsWith('.mrpack')) continue
await add_project_from_path(props.instance.path, file).catch(handleError)
}
await initProjects()
})
.then((unlisten) => {
if (isUnmounted) {
unlisten()
return
}
unlistenDragDrop = unlisten
})
.catch(handleError)
void profile_listener(async (event: { event: string; profile_path_id: string }) => {
const unlistenProfiles = await profile_listener(
async (event: { event: string; profile_path_id: string }) => {
if (
props.instance &&
event.profile_path_id === props.instance.path &&
@@ -1162,17 +881,8 @@ onMounted(() => {
) {
await initProjects()
}
})
.then((unlisten) => {
if (isUnmounted) {
unlisten()
return
}
unlistenProfiles = unlisten
})
.catch(handleError)
})
},
)
watch(
() => props.instance?.install_stage,
@@ -1195,9 +905,8 @@ watch(
)
onUnmounted(() => {
isUnmounted = true
removeBeforeEach()
unlistenDragDrop?.()
unlistenProfiles?.()
unlisten()
unlistenProfiles()
})
</script>
@@ -8,7 +8,6 @@ export interface InstanceSettingsContext {
offline?: boolean
isMinecraftServer: Ref<boolean>
onUnlinked: () => void
closeModal?: () => void
}
export const [injectInstanceSettings, provideInstanceSettings] =
@@ -17,7 +17,6 @@ import {
writePendingServerContentInstallBaseline,
writeStoredServerInstallQueue,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { computed, type ComputedRef, nextTick, type Ref, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
@@ -209,7 +208,6 @@ export function createServerInstallContent(opts: {
const router = useRouter()
const client = injectModrinthClient()
const { handleError } = injectNotificationManager()
const queryClient = useQueryClient()
const serverIdQuery = computed(() => readQueryString(route.query.sid))
const worldIdQuery = computed(() => readQueryString(route.query.wid))
@@ -473,9 +471,6 @@ export function createServerInstallContent(opts: {
...serverContentInstallKeys.value,
...result.flushedPlans.map((plan) => plan.projectId),
])
if (result.flushedPlans.length > 0) {
await queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', serverId] })
}
return true
} finally {
@@ -496,7 +491,6 @@ export function createServerInstallContent(opts: {
const plans = new Map(queuedServerInstalls.value)
if (sid && wid) {
writeStoredServerInstallQueue(sid, wid, plans)
writePendingServerContentInstallBaseline(sid, wid, serverContentInstallKeys.value)
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
void getQueuedInstallPlaceholders(client, plans)
+2 -2
View File
@@ -86,10 +86,10 @@ export default new createRouter({
},
{
path: '/skins',
name: 'Skin selector',
name: 'Skins',
component: Pages.Skins,
meta: {
breadcrumb: [{ name: 'Skin selector' }],
breadcrumb: [{ name: 'Skins' }],
},
},
{
-2
View File
@@ -21,7 +21,6 @@ export type ColorTheme = (typeof THEME_OPTIONS)[number]
export type ThemeStore = {
selectedTheme: ColorTheme
advancedRendering: boolean
hideNametagSkinsPage: boolean
toggleSidebar: boolean
devMode: boolean
@@ -31,7 +30,6 @@ export type ThemeStore = {
export const DEFAULT_THEME_STORE: ThemeStore = {
selectedTheme: 'dark',
advancedRendering: true,
hideNametagSkinsPage: false,
toggleSidebar: false,
devMode: false,
-1
View File
@@ -19,7 +19,6 @@
"types": ["vite/client"],
"paths": {
"@modrinth/api-client": ["../../packages/api-client/src/index.ts"],
"@/*": ["./src/*"]
}
},
-5
View File
@@ -8,7 +8,6 @@ import tauriConf from '../app/tauri.conf.json'
const projectRootDir = resolve(__dirname)
const appLibEnvDir = resolve(projectRootDir, '../../packages/app-lib')
const apiClientSource = resolve(projectRootDir, '../../packages/api-client/src/index.ts')
// Load .env from app-lib manually instead of using Vite's envDir, which would auto-load .env.local and override values
const envFilePath = resolve(appLibEnvDir, '.env')
@@ -38,10 +37,6 @@ export default defineConfig({
},
resolve: {
alias: [
{
find: '@modrinth/api-client',
replacement: apiClientSource,
},
{
find: '@',
replacement: resolve(projectRootDir, 'src'),
-13
View File
@@ -51,21 +51,8 @@ tauri-build = { workspace = true, features = ["codegen"] }
[target.'cfg(target_os = "linux")'.dependencies]
tauri-plugin-updater = { workspace = true, optional = true }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation.workspace = true
core-graphics.workspace = true
objc2-app-kit = { workspace = true, features = [
"NSWindow",
] }
[target.'cfg(windows)'.dependencies]
webview2-com.workspace = true
windows = { workspace = true, features = [
"Win32_Foundation",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_UI_WindowsAndMessaging",
] }
windows-core.workspace = true
[features]
+1 -3
View File
@@ -114,12 +114,10 @@ fn main() {
"get_available_capes",
"get_available_skins",
"add_and_equip_custom_skin",
"set_default_cape",
"equip_skin",
"remove_custom_skin",
"save_custom_skin",
"unequip_skin",
"flush_pending_skin_change",
"flush_pending_skin_change_for_profile",
"normalize_skin_texture",
"get_dragged_skin_data",
])
+16 -118
View File
@@ -11,14 +11,11 @@ use tokio::sync::RwLock;
pub struct AdsState {
pub shown: bool,
pub modal_shown: bool,
pub occluded: bool,
pub last_click: Option<Instant>,
pub malicious_origins: HashSet<String>,
}
const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie";
#[cfg(any(windows, target_os = "macos"))]
pub(super) const OCCLUDED_AREA_THRESHOLD: f64 = 0.5;
#[cfg(not(target_os = "linux"))]
const ADS_USER_AGENT: &str = concat!(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ",
@@ -63,8 +60,8 @@ fn configure_ads_cookie_settings(
core_webview2: &webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2,
) {
use webview2_com::Microsoft::Web::WebView2::Win32::{
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2_13,
ICoreWebView2Profile3,
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2,
ICoreWebView2_13, ICoreWebView2Profile3,
};
use windows_core::Interface;
@@ -94,12 +91,10 @@ fn configure_ads_cookie_settings(
}
}
fn set_webview_visible<R: Runtime>(webview: &tauri::Webview<R>, visible: bool) {
#[cfg(not(any(windows, target_os = "macos")))]
{
_ = visible;
}
fn set_webview_visible<R: Runtime>(
webview: &tauri::Webview<R>,
_visible: bool,
) {
webview
.with_webview(
#[allow(unused_variables)]
@@ -107,18 +102,11 @@ fn set_webview_visible<R: Runtime>(webview: &tauri::Webview<R>, visible: bool) {
#[cfg(windows)]
{
let controller = wv.controller();
unsafe { controller.SetIsVisible(visible) }.ok();
unsafe { controller.SetIsVisible(_visible) }.ok();
}
},
)
.ok();
#[cfg(target_os = "macos")]
if visible {
webview.show().ok();
} else {
webview.hide().ok();
}
}
fn set_webview_visible_for_window<R: Runtime>(
@@ -131,76 +119,7 @@ fn set_webview_visible_for_window<R: Runtime>(
.and_then(|window| window.is_minimized().ok())
.unwrap_or(false);
let is_occluded = app
.state::<RwLock<AdsState>>()
.try_read()
.map(|state| state.occluded)
.unwrap_or(false);
set_webview_visible(webview, visible && !is_minimized && !is_occluded);
}
#[cfg(any(windows, target_os = "macos"))]
fn compute_ads_webview_occlusion<R: Runtime>(
app: &tauri::AppHandle<R>,
) -> Option<bool> {
let main_window = app.get_window("main")?;
let webviews = app.webviews();
let webview = webviews.get("ads-window")?;
#[cfg(target_os = "macos")]
{
let position = webview.position().ok()?;
let size = webview.size().ok()?;
Some(
crate::api::ads_occlusion_macos::is_ads_webview_occluded(
&main_window,
position.x,
position.y,
size.width,
size.height,
)
.unwrap_or(false),
)
}
#[cfg(windows)]
{
let position = webview.position().ok()?;
let size = webview.size().ok()?;
let hwnd = main_window.hwnd().ok()?;
Some(crate::api::ads_occlusion_windows::is_ads_webview_occluded(
hwnd,
position.x,
position.y,
size.width,
size.height,
))
}
}
#[cfg(any(windows, target_os = "macos"))]
async fn sync_ads_occlusion<R: Runtime>(app: &tauri::AppHandle<R>) {
let Some(occluded) = compute_ads_webview_occlusion(app) else {
return;
};
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
if state.occluded == occluded {
return;
}
state.occluded = occluded;
let visible = state.shown && !state.modal_shown;
drop(state);
if let Some(webview) = app.webviews().get("ads-window") {
set_webview_visible_for_window(app, webview, visible);
}
set_webview_visible(webview, visible && !is_minimized);
}
fn sync_webview_visibility_for_main_window<R: Runtime>(
@@ -221,7 +140,7 @@ fn sync_webview_visibility_for_main_window<R: Runtime>(
false
} else {
match app.state::<RwLock<AdsState>>().try_read() {
Ok(state) => state.shown && !state.modal_shown && !state.occluded,
Ok(state) => state.shown && !state.modal_shown,
Err(_) => false,
}
};
@@ -254,28 +173,20 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
app.manage(RwLock::new(AdsState {
shown: true,
modal_shown: false,
occluded: false,
last_click: None,
malicious_origins: HashSet::new(),
}));
// We refresh the ads window periodically to mitigate memory leak issues.
// Skip refreshes when app state has hidden the ads WebView. The refresh does
// not reset the visibility state.
// We refresh the ads window every 5 minutes to mitigate memory leak issues.
// While this loop doesn't include explicit checks to see if the window is still
// visible when we refresh, the Aditude wrapper will not make any ad requests
// unless Chromium reports the page as visible. The refresh does not reset the
// visibility state.
let refresh_app = app.clone();
tauri::async_runtime::spawn(async move {
loop {
let should_refresh = refresh_app
.state::<RwLock<AdsState>>()
.try_read()
.map(|state| {
state.shown && !state.modal_shown && !state.occluded
})
.unwrap_or(false);
if should_refresh
&& let Some(webview) =
refresh_app.webviews().get_mut("ads-window")
if let Some(webview) =
refresh_app.webviews().get_mut("ads-window")
{
let _ = webview.navigate(AD_LINK.parse().unwrap());
}
@@ -313,19 +224,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
});
}
#[cfg(any(windows, target_os = "macos"))]
{
let app_handle = app.clone();
tauri::async_runtime::spawn(async move {
loop {
sync_ads_occlusion(&app_handle).await;
tokio::time::sleep(Duration::from_millis(200)).await;
}
});
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
-236
View File
@@ -1,236 +0,0 @@
use std::ptr::NonNull;
use core_foundation::array::CFArray;
use core_foundation::base::{CFType, TCFType};
use core_foundation::dictionary::CFDictionary;
use core_foundation::number::CFNumber;
use core_foundation::string::CFString;
use core_graphics::display::CGDisplay;
use core_graphics::geometry::{CGPoint, CGRect, CGSize};
use core_graphics::window::{
CGWindowID, kCGWindowAlpha, kCGWindowBounds, kCGWindowLayer,
kCGWindowListExcludeDesktopElements,
kCGWindowListOptionOnScreenAboveWindow, kCGWindowListOptionOnScreenOnly,
kCGWindowNumber, kCGWindowOwnerName, kCGWindowOwnerPID,
};
use objc2_app_kit::NSWindow;
pub fn is_ads_webview_occluded<R: tauri::Runtime>(
main_window: &tauri::Window<R>,
ad_x: i32,
ad_y: i32,
ad_width: u32,
ad_height: u32,
) -> Option<bool> {
let scale_factor = main_window.scale_factor().ok()?;
let maybe_ns_window = main_window.ns_window().ok()?;
let ns_window = NonNull::new(maybe_ns_window)?.cast::<NSWindow>();
let ns_window = unsafe { ns_window.as_ref() };
let window_number = ns_window.windowNumber();
let main_window_id = if window_number > 0 {
window_number as CGWindowID
} else {
return None;
};
let window_infos = CGDisplay::window_list_info(
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
None,
)?;
let window_infos = unsafe {
CFArray::<CFDictionary<CFString, CFType>>::wrap_under_get_rule(
window_infos.as_concrete_TypeRef(),
)
};
let main_window_rect = window_infos
.iter()
.find(|window_info| window_id(window_info) == Some(main_window_id))
.and_then(|window_info| window_rect(&window_info))?;
let ad_rect = ad_rect_from_main_window(
&main_window_rect,
ad_x,
ad_y,
ad_width,
ad_height,
scale_factor,
);
if is_empty_rect(&ad_rect) {
return None;
}
let ad_area = rect_area(&ad_rect);
if ad_area == 0.0 {
return None;
}
let app_process_id = std::process::id() as i32;
let windows_above_main = CGDisplay::window_list_info(
kCGWindowListOptionOnScreenAboveWindow
| kCGWindowListExcludeDesktopElements,
Some(main_window_id),
)?;
let windows_above_main = unsafe {
CFArray::<CFDictionary<CFString, CFType>>::wrap_under_get_rule(
windows_above_main.as_concrete_TypeRef(),
)
};
let mut occluded_area = 0.0;
for window_info in windows_above_main.iter() {
if window_id(&window_info) == Some(main_window_id) {
continue;
}
if window_process_id(&window_info) == Some(app_process_id) {
continue;
}
let owner_name = window_owner_name(&window_info);
if owner_name.as_deref().is_some_and(is_system_window_owner) {
continue;
}
let layer = window_layer(&window_info);
if layer != Some(0) {
continue;
}
let alpha = window_alpha(&window_info);
if alpha.is_some_and(|alpha| alpha <= 0.0) {
continue;
}
let Some(rect) = window_rect(&window_info) else {
continue;
};
let Some(intersection) = intersect_rects(&ad_rect, &rect) else {
continue;
};
occluded_area += rect_area(&intersection);
let occluded_ratio = occluded_area / ad_area;
if occluded_ratio >= super::ads::OCCLUDED_AREA_THRESHOLD {
return Some(true);
}
}
Some(false)
}
fn ad_rect_from_main_window(
main_window_rect: &CGRect,
x: i32,
y: i32,
width: u32,
height: u32,
scale_factor: f64,
) -> CGRect {
let scale_factor = if scale_factor > 0.0 {
scale_factor
} else {
1.0
};
CGRect::new(
&CGPoint::new(
main_window_rect.origin.x + x as f64 / scale_factor,
main_window_rect.origin.y + y as f64 / scale_factor,
),
&CGSize::new(width as f64 / scale_factor, height as f64 / scale_factor),
)
}
fn window_id(
window_info: &CFDictionary<CFString, CFType>,
) -> Option<CGWindowID> {
number_value(window_info, unsafe { kCGWindowNumber })
.and_then(|value| u32::try_from(value).ok())
}
fn window_process_id(
window_info: &CFDictionary<CFString, CFType>,
) -> Option<i32> {
number_value(window_info, unsafe { kCGWindowOwnerPID })
.and_then(|value| i32::try_from(value).ok())
}
fn window_layer(window_info: &CFDictionary<CFString, CFType>) -> Option<i32> {
number_value(window_info, unsafe { kCGWindowLayer })
.and_then(|value| i32::try_from(value).ok())
}
fn window_alpha(window_info: &CFDictionary<CFString, CFType>) -> Option<f64> {
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowAlpha) };
window_info.find(&key)?.downcast::<CFNumber>()?.to_f64()
}
fn window_owner_name(
window_info: &CFDictionary<CFString, CFType>,
) -> Option<String> {
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowOwnerName) };
Some(window_info.find(&key)?.downcast::<CFString>()?.to_string())
}
fn is_system_window_owner(owner_name: &str) -> bool {
matches!(owner_name, "WindowManager")
}
fn number_value(
window_info: &CFDictionary<CFString, CFType>,
key: core_foundation::string::CFStringRef,
) -> Option<i64> {
let key = unsafe { CFString::wrap_under_get_rule(key) };
window_info.find(&key)?.downcast::<CFNumber>()?.to_i64()
}
fn window_rect(window_info: &CFDictionary<CFString, CFType>) -> Option<CGRect> {
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowBounds) };
let bounds = window_info.find(&key)?.downcast::<CFDictionary>()?;
let rect = CGRect::from_dict_representation(&bounds)?;
if is_empty_rect(&rect) {
None
} else {
Some(rect)
}
}
fn is_empty_rect(rect: &CGRect) -> bool {
rect.size.width <= 0.0 || rect.size.height <= 0.0
}
fn rect_area(rect: &CGRect) -> f64 {
if is_empty_rect(rect) {
return 0.0;
}
rect.size.width * rect.size.height
}
fn intersect_rects(a: &CGRect, b: &CGRect) -> Option<CGRect> {
let left = a.origin.x.max(b.origin.x);
let top = a.origin.y.max(b.origin.y);
let right = (a.origin.x + a.size.width).min(b.origin.x + b.size.width);
let bottom = (a.origin.y + a.size.height).min(b.origin.y + b.size.height);
let rect = CGRect::new(
&CGPoint::new(left, top),
&CGSize::new(right - left, bottom - top),
);
if is_empty_rect(&rect) {
None
} else {
Some(rect)
}
}
-191
View File
@@ -1,191 +0,0 @@
use windows::Win32::Foundation::{HWND, POINT, RECT};
use windows::Win32::Graphics::Dwm::{
DWMWA_CLOAKED, DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute,
};
use windows::Win32::Graphics::Gdi::ClientToScreen;
use windows::Win32::UI::WindowsAndMessaging::{
GA_ROOT, GW_HWNDNEXT, GetAncestor, GetTopWindow, GetWindow, GetWindowRect,
GetWindowThreadProcessId, IsIconic, IsWindowVisible,
};
pub fn is_ads_webview_occluded(
main_hwnd: HWND,
x: i32,
y: i32,
width: u32,
height: u32,
) -> bool {
let Some(ad_rect) = ad_rect_in_screen(main_hwnd, x, y, width, height)
else {
return false;
};
if is_empty_rect(&ad_rect) {
return false;
}
let ad_area = rect_area(&ad_rect);
if ad_area == 0 {
return false;
}
let mut occluded_area = 0u64;
let app_root = unsafe { GetAncestor(main_hwnd, GA_ROOT) };
let app_process_id = std::process::id();
let mut hwnd = match unsafe { GetTopWindow(None) } {
Ok(hwnd) => hwnd,
Err(_) => return false,
};
while !hwnd.is_invalid() {
let window_root = unsafe { GetAncestor(hwnd, GA_ROOT) };
if window_root == app_root {
return false;
}
if window_process_id(hwnd) == Some(app_process_id) {
hwnd = match unsafe { GetWindow(hwnd, GW_HWNDNEXT) } {
Ok(hwnd) => hwnd,
Err(_) => break,
};
continue;
}
if window_counts_as_occluder(hwnd)
&& let Some(occluder_rect) = window_rect(hwnd)
&& let Some(intersection) =
intersect_rects(&ad_rect, &occluder_rect)
{
occluded_area =
occluded_area.saturating_add(rect_area(&intersection));
if (occluded_area as f64 / ad_area as f64)
>= super::ads::OCCLUDED_AREA_THRESHOLD
{
return true;
}
}
hwnd = match unsafe { GetWindow(hwnd, GW_HWNDNEXT) } {
Ok(hwnd) => hwnd,
Err(_) => break,
};
}
false
}
fn ad_rect_in_screen(
main_hwnd: HWND,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Option<RECT> {
let mut origin = POINT { x: 0, y: 0 };
if !unsafe { ClientToScreen(main_hwnd, &mut origin).as_bool() } {
return None;
}
let left = origin.x.saturating_add(x);
let top = origin.y.saturating_add(y);
let right = left.saturating_add(width as i32);
let bottom = top.saturating_add(height as i32);
Some(RECT {
left,
top,
right,
bottom,
})
}
fn window_counts_as_occluder(hwnd: HWND) -> bool {
if !unsafe { IsWindowVisible(hwnd).as_bool() } {
return false;
}
if unsafe { IsIconic(hwnd).as_bool() } {
return false;
}
if is_dwm_cloaked(hwnd) {
return false;
}
true
}
fn window_process_id(hwnd: HWND) -> Option<u32> {
let mut process_id = 0u32;
unsafe {
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
}
(process_id != 0).then_some(process_id)
}
fn is_dwm_cloaked(hwnd: HWND) -> bool {
let mut cloaked = 0u32;
unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_CLOAKED,
&mut cloaked as *mut u32 as *mut _,
std::mem::size_of::<u32>() as u32,
)
}
.is_ok()
&& cloaked != 0
}
fn window_rect(hwnd: HWND) -> Option<RECT> {
let mut rect = RECT::default();
if unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_EXTENDED_FRAME_BOUNDS,
&mut rect as *mut RECT as *mut _,
std::mem::size_of::<RECT>() as u32,
)
}
.is_err()
&& unsafe { GetWindowRect(hwnd, &mut rect) }.is_err()
{
return None;
}
if is_empty_rect(&rect) {
return None;
}
Some(rect)
}
fn is_empty_rect(rect: &RECT) -> bool {
rect.right <= rect.left || rect.bottom <= rect.top
}
fn rect_area(rect: &RECT) -> u64 {
if is_empty_rect(rect) {
return 0;
}
(rect.right - rect.left) as u64 * (rect.bottom - rect.top) as u64
}
fn intersect_rects(a: &RECT, b: &RECT) -> Option<RECT> {
let rect = RECT {
left: a.left.max(b.left),
top: a.top.max(b.top),
right: a.right.min(b.right),
bottom: a.bottom.min(b.bottom),
};
(!is_empty_rect(&rect)).then_some(rect)
}
+17 -51
View File
@@ -11,12 +11,10 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
get_available_capes,
get_available_skins,
add_and_equip_custom_skin,
set_default_cape,
equip_skin,
remove_custom_skin,
save_custom_skin,
unequip_skin,
flush_pending_skin_change,
flush_pending_skin_change_for_profile,
normalize_skin_texture,
get_dragged_skin_data,
])
@@ -39,19 +37,29 @@ pub async fn get_available_skins() -> Result<Vec<Skin>> {
Ok(minecraft_skins::get_available_skins().await?)
}
/// `invoke('plugin:minecraft-skins|add_and_equip_custom_skin', texture_blob, variant, cape)`
/// `invoke('plugin:minecraft-skins|add_and_equip_custom_skin', texture_blob, variant, cape_override)`
///
/// See also: [minecraft_skins::add_and_equip_custom_skin]
#[tauri::command]
pub async fn add_and_equip_custom_skin(
texture_blob: Bytes,
variant: MinecraftSkinVariant,
cape: Option<Cape>,
) -> Result<Skin> {
Ok(
minecraft_skins::add_and_equip_custom_skin(texture_blob, variant, cape)
.await?,
cape_override: Option<Cape>,
) -> Result<()> {
Ok(minecraft_skins::add_and_equip_custom_skin(
texture_blob,
variant,
cape_override,
)
.await?)
}
/// `invoke('plugin:minecraft-skins|set_default_cape', cape)`
///
/// See also: [minecraft_skins::set_default_cape]
#[tauri::command]
pub async fn set_default_cape(cape: Option<Cape>) -> Result<()> {
Ok(minecraft_skins::set_default_cape(cape).await?)
}
/// `invoke('plugin:minecraft-skins|equip_skin', skin)`
@@ -70,27 +78,6 @@ pub async fn remove_custom_skin(skin: Skin) -> Result<()> {
Ok(minecraft_skins::remove_custom_skin(skin).await?)
}
/// `invoke('plugin:minecraft-skins|save_custom_skin', skin, texture_blob, variant, cape, replace_texture)`
///
/// See also: [minecraft_skins::save_custom_skin]
#[tauri::command]
pub async fn save_custom_skin(
skin: Skin,
texture_blob: Bytes,
variant: MinecraftSkinVariant,
cape: Option<Cape>,
replace_texture: bool,
) -> Result<Skin> {
Ok(minecraft_skins::save_custom_skin(
skin,
texture_blob,
variant,
cape,
replace_texture,
)
.await?)
}
/// `invoke('plugin:minecraft-skins|unequip_skin')`
///
/// See also: [minecraft_skins::unequip_skin]
@@ -99,27 +86,6 @@ pub async fn unequip_skin() -> Result<()> {
Ok(minecraft_skins::unequip_skin().await?)
}
/// `invoke('plugin:minecraft-skins|flush_pending_skin_change')`
///
/// See also: [minecraft_skins::flush_pending_skin_change]
#[tauri::command]
pub async fn flush_pending_skin_change() -> Result<()> {
Ok(minecraft_skins::flush_pending_skin_change().await?)
}
/// `invoke('plugin:minecraft-skins|flush_pending_skin_change_for_profile', profile_id)`
///
/// See also: [minecraft_skins::flush_pending_skin_change_for_profile]
#[tauri::command]
pub async fn flush_pending_skin_change_for_profile(
profile_id: uuid::Uuid,
) -> Result<()> {
Ok(
minecraft_skins::flush_pending_skin_change_for_profile(profile_id)
.await?,
)
}
/// `invoke('plugin:minecraft-skins|normalize_skin_texture')`
///
/// See also: [minecraft_skins::normalize_skin_texture]
-4
View File
@@ -18,10 +18,6 @@ pub mod tags;
pub mod utils;
pub mod ads;
#[cfg(target_os = "macos")]
mod ads_occlusion_macos;
#[cfg(windows)]
mod ads_occlusion_windows;
pub mod cache;
pub mod files;
pub mod friends;
+2 -12
View File
@@ -270,20 +270,10 @@ fn main() {
Ok(app) => {
app.run(|app, event| {
#[cfg(not(any(feature = "updater", target_os = "macos")))]
let _ = app;
if matches!(&event, tauri::RunEvent::ExitRequested { .. })
&& let Err(error) = tauri::async_runtime::block_on(
theseus::minecraft_skins::flush_pending_skin_change(),
)
{
tracing::warn!(
"Failed to flush pending Minecraft skin change before exit: {error}"
);
}
drop((app, event));
#[cfg(feature = "updater")]
if matches!(&event, tauri::RunEvent::Exit) {
if matches!(event, tauri::RunEvent::Exit) {
let update_data = app.state::<PendingUpdateData>().inner();
let should_restart = State::get_if_initialized()
.map(|s| {
+35 -52
View File
@@ -65,65 +65,49 @@ async fn fetch(
)
.await?;
// We check Modrinth's manifest to find newly added loader versions,
// intermediary/mapping artifacts, and game versions.
let (
fetch_fabric_versions,
fetch_intermediary_versions,
has_new_game_versions,
) = if let Some(modrinth_manifest) = modrinth_manifest {
let (mut fetch_versions, mut fetch_intermediary_versions) =
(Vec::new(), Vec::new());
// We check Modrinth's fabric version manifest and compare if the fabric version exists in Modrinth's database
// We also check intermediary versions that are newly added to query
let (fetch_fabric_versions, fetch_intermediary_versions) =
if let Some(modrinth_manifest) = modrinth_manifest {
let (mut fetch_versions, mut fetch_intermediary_versions) =
(Vec::new(), Vec::new());
for version in &fabric_manifest.loader {
if !modrinth_manifest
.game_versions
.iter()
.any(|x| x.loaders.iter().any(|x| x.id == version.version))
&& !skip_versions.contains(&&*version.version)
{
fetch_versions.push(version);
}
}
for version in &fabric_manifest.intermediary {
if !modrinth_manifest
.game_versions
.iter()
.any(|x| x.id == version.version)
&& fabric_manifest
.game
for version in &fabric_manifest.loader {
if !modrinth_manifest
.game_versions
.iter()
.any(|x| x.version == version.version)
{
fetch_intermediary_versions.push(version);
.any(|x| x.loaders.iter().any(|x| x.id == version.version))
&& !skip_versions.contains(&&*version.version)
{
fetch_versions.push(version);
}
}
}
let has_new_game_versions =
fabric_manifest.game.iter().any(|version| {
!modrinth_manifest
for version in &fabric_manifest.intermediary {
if !modrinth_manifest
.game_versions
.iter()
.any(|x| x.id == version.version)
});
&& fabric_manifest
.game
.iter()
.any(|x| x.version == version.version)
{
fetch_intermediary_versions.push(version);
}
}
(
fetch_versions,
fetch_intermediary_versions,
has_new_game_versions,
)
} else {
(
fabric_manifest
.loader
.iter()
.filter(|x| !skip_versions.contains(&&*x.version))
.collect(),
fabric_manifest.intermediary.iter().collect(),
true,
)
};
(fetch_versions, fetch_intermediary_versions)
} else {
(
fabric_manifest
.loader
.iter()
.filter(|x| !skip_versions.contains(&&*x.version))
.collect(),
fabric_manifest.intermediary.iter().collect(),
)
};
const DUMMY_GAME_VERSION: &str = "1.21";
@@ -232,7 +216,6 @@ async fn fetch(
if !fetch_fabric_versions.is_empty()
|| !fetch_intermediary_versions.is_empty()
|| has_new_game_versions
{
let fabric_manifest_path =
format!("{mod_loader}/v{format_version}/manifest.json",);
+1 -12
View File
@@ -1,15 +1,10 @@
import { GenericModrinthClient, type Labrinth } from '@modrinth/api-client'
import serverSidedVue from '@vitejs/plugin-vue'
import fs from 'fs/promises'
import { defineNuxtConfig } from 'nuxt/config'
import { fileURLToPath } from 'url'
import svgLoader from 'vite-svg-loader'
import { GenericModrinthClient, type Labrinth } from '../../packages/api-client/src/index.ts'
const STAGING_API_URL = 'https://staging-api.modrinth.com/v2/'
const API_CLIENT_SOURCE = fileURLToPath(
new URL('../../packages/api-client/src/index.ts', import.meta.url),
)
const preloadedFonts = [
'inter/Inter-Regular.woff2',
@@ -29,9 +24,6 @@ const STAGING_MODRINTH_URL = 'https://staging.modrinth.com'
export default defineNuxtConfig({
srcDir: 'src/',
alias: {
'@modrinth/api-client': API_CLIENT_SOURCE,
},
app: {
head: {
htmlAttrs: {
@@ -90,9 +82,6 @@ export default defineNuxtConfig({
},
cacheDir: '../../node_modules/.vite/apps/knossos',
resolve: {
alias: {
'@modrinth/api-client': API_CLIENT_SOURCE,
},
dedupe: ['vue'],
},
plugins: [
+1 -1
View File
@@ -11,7 +11,6 @@
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write .",
"intl:extract": "formatjs extract \"src/{components,composables,layouts,middleware,modules,pages,plugins,utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
"intl:prune-local": "pnpm -w scripts i18n-icu-contract prune-local --scope apps/frontend",
"cf-deploy": "pnpm run build && wrangler deploy --env staging",
"cf-dev": "pnpm run build && wrangler dev --env staging",
"cf-typegen": "wrangler types"
@@ -39,6 +38,7 @@
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
"@intercom/messenger-js-sdk": "^0.0.14",
"@ltd/j-toml": "^1.38.0",
"@modrinth/api-client": "workspace:*",
"@modrinth/assets": "workspace:*",
@@ -5,6 +5,7 @@ import {
commonMessages,
defineMessages,
IntlFormatted,
normalizeChildren,
PagewideBanner,
useVIntl,
} from '@modrinth/ui'
@@ -21,7 +22,7 @@ const messages = defineMessages({
},
description: {
id: 'layout.banner.preview.description',
defaultMessage: `If you meant to access the official Modrinth website, visit {url}. This preview deploy is used by Modrinth staff for testing purposes. It was built using {ref}.`,
defaultMessage: `If you meant to access the official Modrinth website, visit {url}. This preview deploy is used by Modrinth staff for testing purposes. It was built using <branch-link>{owner}/{branch}</branch-link> @ {commit}.`,
},
})
@@ -40,22 +41,30 @@ const url = computed(() => `https://modrinth.com${route.fullPath}`)
</template>
<template #description>
<span>
<IntlFormatted :message-id="messages.description">
<IntlFormatted
:message-id="messages.description"
:values="{
owner: config.public.owner,
branch: config.public.branch,
}"
>
<template #url>
<a :href="url" target="_blank" rel="noopener" class="text-link">
{{ url }}
</a>
</template>
<template #ref>
<template #branch-link="{ children }">
<a
:href="`https://github.com/${config.public.owner}/code/tree/${config.public.branch}`"
target="_blank"
rel="noopener"
class="hover:underline"
>
{{ config.public.owner }} / {{ config.public.branch }}
<component :is="() => normalizeChildren(children)" />
</a>
@ <span v-if="config.public.hash === 'unknown'">unknown</span>
</template>
<template #commit>
<span v-if="config.public.hash === 'unknown'">unknown</span>
<a
v-else
:href="`https://github.com/${config.public.owner}/code/commit/${config.public.hash}`"
@@ -46,7 +46,7 @@
/>
</div>
<div class="flex flex-col gap-2">
<span class="font-semibold text-contrast"> Version changelog </span>
<span class="font-semibold text-contrast"> Version changlog </span>
<div class="w-full">
<MarkdownEditor
@@ -131,9 +131,10 @@
class="h-10"
/>
<DatePicker
<StyledInput
v-else-if="field.type === 'date'"
v-model="formData[field.name]"
type="date"
wrapper-class="w-full"
/>
@@ -229,7 +230,6 @@ import {
Admonition,
Checkbox,
Combobox,
DatePicker,
defineMessages,
financialMessages,
formFieldLabels,
@@ -95,11 +95,11 @@
<span class="text-red">*</span>
</span>
</label>
<DatePicker
<StyledInput
v-model="formData.dateOfBirth"
:max-date="maxDate"
type="date"
:max="maxDate"
autocomplete="bday"
placeholder="Select date of birth"
wrapper-class="w-full"
/>
</div>
@@ -213,7 +213,6 @@
import {
Chips,
Combobox,
DatePicker,
defineMessages,
formFieldLabels,
formFieldPlaceholders,
@@ -233,7 +233,7 @@ const visibleNags = computed<Nag[]>(() => {
link: {
path: 'moderation',
title: messages.visitModerationPage,
shouldShow: () => props.routeName !== 'type-project-moderation',
shouldShow: () => props.routeName !== 'type-id-moderation',
},
})
}
@@ -48,16 +48,6 @@
<span class="text-sm text-secondary">Requesting</span>
<Badge :type="queueEntry.project.requested_status" class="text-sm" />
</div>
<div
v-if="showExternalDependencies"
v-tooltip="'External dependencies'"
class="flex items-center gap-1 rounded-full border border-solid border-surface-5 bg-surface-4 px-2.5 py-1"
>
<FileIcon aria-hidden="true" class="size-4 text-secondary" />
<span class="text-sm font-medium text-secondary">
{{ queueEntry.external_dependencies_count }}
</span>
</div>
</div>
<div v-if="queueEntry.ownership?.kind === 'user'">
<NuxtLink
@@ -129,7 +119,7 @@
</template>
<script setup lang="ts">
import { ClipboardCopyIcon, FileIcon, LinkIcon, ScaleIcon } from '@modrinth/assets'
import { ClipboardCopyIcon, LinkIcon, ScaleIcon } from '@modrinth/assets'
import {
Avatar,
Badge,
@@ -161,7 +151,6 @@ const formatDateTimeFull = useFormatDateTime({
const props = defineProps<{
queueEntry: ModerationProject
showExternalDependencies?: boolean
}>()
const emit = defineEmits<{
@@ -701,11 +701,11 @@ async function navigateToNextUnlockedProject(): Promise<boolean> {
// Quick re-check if close to expiry (last 5 seconds of TTL)
if (now - next.validatedAt > PREFETCH_STALE_MS - 5000) {
const recheckResults = await batchCheckQueueCandidates([next.projectId])
const recheck = recheckResults.get(next.projectId)
if (!isEligibleQueueCandidate(recheck)) {
const recheck = await moderationQueue.checkLock(next.projectId)
if (recheck.locked && !recheck.expired) {
// Project got locked, remove from queue and try next
prefetchQueue.value.shift()
return navigateToNextUnlockedProject()
return navigateToNextUnlockedProject() // Recurse to try next
}
}
@@ -717,14 +717,33 @@ async function navigateToNextUnlockedProject(): Promise<boolean> {
next.skippedIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
)
notifySkippedQueueProjects(next.skippedIds.length)
if (next.skippedIds.length > 0) {
addNotification({
title: 'Skipped locked projects',
text: `Skipped ${next.skippedIds.length} project(s) being moderated by others.`,
type: 'info',
})
}
// Trigger prefetch replenishment in background (don't await)
maintainPrefetchQueue()
navigateToQueueProject(
{ slug: next.slug, projectType: next.projectType, locked: false, isProcessing: true },
next.projectId,
)
// Navigate to canonical URL if we have metadata (avoids middleware redirect)
if (next.slug && next.projectType) {
const urlType = getProjectTypeForUrlShorthand(next.projectType, [], tags.value)
navigateTo({
path: `/${urlType}/${next.slug}`,
state: { showChecklist: true },
})
} else {
// Fallback: use project ID (will trigger middleware redirect)
navigateTo({
name: 'type-id',
params: { type: 'project', id: next.projectId },
state: { showChecklist: true },
})
}
return true
}
@@ -850,108 +869,53 @@ function reviewAnyway() {
maintainPrefetchQueue()
}
// Batch check locks, processing status, and fetch project metadata in parallel
interface QueueCandidateCheck {
// Batch check locks and fetch project metadata in parallel
interface LockCheckResult {
locked: boolean
expired?: boolean
isOwnLock?: boolean
slug?: string
projectType?: string
status?: string
isProcessing: boolean
}
function isEligibleQueueCandidate(result: QueueCandidateCheck | undefined): boolean {
if (!result?.isProcessing) return false
return !result.locked || !!result.expired || !!result.isOwnLock
}
function notifySkippedQueueProjects(count: number) {
if (count <= 0) return
addNotification({
title: 'Skipped projects',
text: `Skipped ${count} project(s) already moderated or locked by others.`,
type: 'info',
autoCloseMs: 2000,
})
}
function navigateToQueueProject(result: QueueCandidateCheck, projectId: string) {
if (result.slug && result.projectType) {
const urlType = getProjectTypeForUrlShorthand(result.projectType, [], tags.value)
navigateTo({
path: `/${urlType}/${result.slug}`,
state: { showChecklist: true },
})
} else {
navigateTo({
name: 'type-project',
params: { type: 'project', project: projectId },
state: { showChecklist: true },
})
}
}
async function batchCheckQueueCandidates(
async function batchCheckLocksWithMetadata(
projectIds: string[],
): Promise<Map<string, QueueCandidateCheck>> {
const results = new Map<string, QueueCandidateCheck>()
): Promise<Map<string, LockCheckResult>> {
const results = new Map<string, LockCheckResult>()
// Check locks and fetch minimal project data in parallel
const checks = await Promise.allSettled(
projectIds.map(async (id) => {
// Parallel: check lock AND fetch project metadata
const [lockResponse, projectData] = await Promise.all([
moderationQueue.checkLock(id),
useBaseFetch(`project/${id}`, { method: 'GET' }).catch(() => null),
])
const status = (projectData as { status?: string } | null)?.status
return {
id,
locked: lockResponse.locked,
expired: lockResponse.expired,
isOwnLock: lockResponse.is_own_lock,
slug: (projectData as { slug?: string } | null)?.slug,
projectType: (projectData as { project_type?: string } | null)?.project_type,
status,
isProcessing: projectData === null ? true : status === 'processing',
slug: (projectData as { slug?: string })?.slug,
projectType: (projectData as { project_type?: string })?.project_type,
}
}),
)
// Use forEach with index to avoid indexOf bug on PromiseSettledResult
checks.forEach((result, index) => {
if (result.status === 'fulfilled') {
results.set(result.value.id, result.value)
} else {
results.set(projectIds[index], { locked: false, isProcessing: true })
// On error, mark as needing fallback (no metadata)
results.set(projectIds[index], { locked: false })
}
})
return results
}
async function findNextEligibleQueueProject(candidateIds: string[]) {
const skippedIds: string[] = []
let checkedCount = 0
while (checkedCount < candidateIds.length) {
const batch = candidateIds.slice(checkedCount, checkedCount + PREFETCH_BATCH_SIZE)
checkedCount += batch.length
const results = await batchCheckQueueCandidates(batch)
for (const id of batch) {
const result = results.get(id)
if (isEligibleQueueCandidate(result)) {
return { projectId: id, result: result!, skippedIds: [...skippedIds] }
}
skippedIds.push(id)
}
}
return null
}
// Maintain a queue of prefetched unlocked projects for instant navigation
async function maintainPrefetchQueue() {
if (isPrefetching.value) return
@@ -983,10 +947,13 @@ async function maintainPrefetchQueue() {
const remainingItems =
currentIndex >= 0 ? queueItems.slice(currentIndex + 1) : queueItems.slice(1)
const candidateIds = remainingItems.filter((id) => !prefetchedIds.has(id))
const candidateIds = remainingItems
.filter((id) => !prefetchedIds.has(id))
.slice(0, PREFETCH_BATCH_SIZE * 2) // Check up to 10 candidates
if (candidateIds.length === 0) return
// 5. Batch check locks AND fetch metadata in parallel
const skippedIds: string[] = []
let checkedCount = 0
@@ -997,18 +964,31 @@ async function maintainPrefetchQueue() {
const batch = candidateIds.slice(checkedCount, checkedCount + PREFETCH_BATCH_SIZE)
checkedCount += batch.length
const results = await batchCheckQueueCandidates(batch)
const results = await batchCheckLocksWithMetadata(batch)
for (const id of batch) {
const result = results.get(id)
if (isEligibleQueueCandidate(result)) {
prefetchQueue.value.push({
projectId: id,
slug: result?.slug ?? '',
projectType: result?.projectType ?? '',
validatedAt: Date.now(),
skippedIds: [...skippedIds],
})
// Treat as unlocked if: not locked, OR expired, OR it's our own lock
if (!result?.locked || result?.expired || result?.isOwnLock) {
// Found unlocked project with metadata
if (result?.slug && result?.projectType) {
prefetchQueue.value.push({
projectId: id,
slug: result.slug,
projectType: result.projectType,
validatedAt: Date.now(),
skippedIds: [...skippedIds],
})
} else {
// No metadata - still add but will need fallback navigation
prefetchQueue.value.push({
projectId: id,
slug: '', // Empty = use fallback
projectType: '',
validatedAt: Date.now(),
skippedIds: [...skippedIds],
})
}
if (prefetchQueue.value.length >= PREFETCH_TARGET_COUNT) break
} else {
@@ -1024,6 +1004,8 @@ async function maintainPrefetchQueue() {
// Debounced prefetch to prevent spam from rapid stage changes
const debouncedPrefetch = useDebounceFn(maintainPrefetchQueue, 300)
const MAX_SKIP_ATTEMPTS = 10
async function skipToNextProject() {
// Skip the current project
const currentProjectId = projectV2.value?.id
@@ -1047,28 +1029,60 @@ async function skipToNextProject() {
debug('[skipToNextProject] No prefetch, entering fallback with batch checking')
const remainingIds = moderationQueue.currentQueue.items.filter((id) => id !== currentProjectId)
// Fallback: batch check remaining projects with metadata (excluding current)
const remainingIds: string[] = []
const queueItems = moderationQueue.currentQueue.items
// Build list of remaining projects, excluding current
for (const id of queueItems) {
if (id === currentProjectId) continue
if (remainingIds.length >= MAX_SKIP_ATTEMPTS) break
remainingIds.push(id)
}
if (remainingIds.length > 0) {
const next = await findNextEligibleQueueProject(remainingIds)
const results = await batchCheckLocksWithMetadata(remainingIds)
if (next) {
await Promise.all(
next.skippedIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
)
notifySkippedQueueProjects(next.skippedIds.length)
navigateToQueueProject(next.result, next.projectId)
return
let skippedCount = 0
for (const id of remainingIds) {
const result = results.get(id)
// Treat as unlocked if: not locked, OR expired, OR it's our own lock
if (!result?.locked || result?.expired || result?.isOwnLock) {
// Found unlocked - skip the locked ones before it
if (skippedCount > 0) {
addNotification({
title: 'Skipped locked projects',
text: `Skipped ${skippedCount} project(s) being moderated by others.`,
type: 'info',
})
}
// Navigate to canonical URL if we have metadata
if (result?.slug && result?.projectType) {
const urlType = getProjectTypeForUrlShorthand(result.projectType, [], tags.value)
navigateTo({
path: `/${urlType}/${result.slug}`,
state: { showChecklist: true },
})
} else {
// Fallback: use project ID
navigateTo({
name: 'type-id',
params: { type: 'project', id },
state: { showChecklist: true },
})
}
return
}
await moderationQueue.completeCurrentProject(id, 'skipped')
skippedCount++
}
await Promise.all(
remainingIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
)
debug('[skipToNextProject] No eligible projects in queue')
// All checked were locked
debug('[skipToNextProject] All projects were locked, skippedCount:', skippedCount)
addNotification({
title: 'No projects available',
text: 'All remaining projects are already moderated or locked by others.',
title: 'All projects locked',
text: 'All remaining projects are currently being moderated by others.',
type: 'warning',
})
}
@@ -1284,16 +1298,32 @@ onMounted(async () => {
document.addEventListener('visibilitychange', handleVisibilityChange)
notifications.setNotificationLocation('left')
// Check if project has already been reviewed (not in processing status)
if (projectV2.value.status !== 'processing') {
alreadyReviewed.value = true
return
}
// Try to acquire lock
const result = await moderationQueue.acquireLock(projectV2.value.id)
if (result.success) {
handleLockAcquired()
} else if (result.locked_by) {
// Actually locked by another moderator
// In queue mode with more projects - auto-skip to next project
if (moderationQueue.isQueueMode && moderationQueue.queueLength > 1) {
addNotification({
title: 'Project locked',
text: `Skipped project locked by @${result.locked_by.username}.`,
type: 'info',
})
// skipToNextProject already calls completeCurrentProject
await skipToNextProject()
return
}
// Single project mode or last in queue - show locked UI
lockStatus.value = {
locked: true,
lockedBy: result.locked_by,
@@ -2055,36 +2085,72 @@ async function endChecklist(status?: string) {
})
}
} else {
// Use prefetched data if available for instant navigation
if (!(await navigateToNextUnlockedProject())) {
// Fallback: batch check remaining projects with metadata
const remainingIds: string[] = []
const currentProjectId = projectV2.value?.id
const remainingIds = moderationQueue.currentQueue.items.filter(
(id) => id !== currentProjectId,
)
const queueItems = moderationQueue.currentQueue.items
let foundEligible = false
// Build list of remaining projects, excluding current
for (const id of queueItems) {
if (id === currentProjectId) continue
if (remainingIds.length >= MAX_SKIP_ATTEMPTS) break
remainingIds.push(id)
}
let foundUnlocked = false
if (remainingIds.length > 0) {
const next = await findNextEligibleQueueProject(remainingIds)
const results = await batchCheckLocksWithMetadata(remainingIds)
if (next) {
await Promise.all(
next.skippedIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
)
notifySkippedQueueProjects(next.skippedIds.length)
navigateToQueueProject(next.result, next.projectId)
foundEligible = true
} else {
await Promise.all(
remainingIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
)
let skippedCount = 0
for (const id of remainingIds) {
const result = results.get(id)
// Treat as unlocked if: not locked, OR expired, OR it's our own lock
if (!result?.locked || result?.expired || result?.isOwnLock) {
// Found unlocked - skip the locked ones before it
if (skippedCount > 0) {
addNotification({
title: 'Skipped locked projects',
text: `Skipped ${skippedCount} project(s) being moderated by others.`,
type: 'info',
})
}
// Navigate to canonical URL if we have metadata
if (result?.slug && result?.projectType) {
const urlType = getProjectTypeForUrlShorthand(result.projectType, [], tags.value)
navigateTo({
path: `/${urlType}/${result.slug}`,
state: { showChecklist: true },
})
} else {
// Fallback: use project ID
navigateTo({
name: 'type-id',
params: { type: 'project', id },
state: { showChecklist: true },
})
}
foundUnlocked = true
break
}
await moderationQueue.completeCurrentProject(id, 'skipped')
skippedCount++
}
// If no unlocked projects found, show notification
if (!foundUnlocked && skippedCount > 0) {
addNotification({
title: 'No projects available',
text: 'All remaining projects are already moderated or locked by others.',
title: 'All projects locked',
text: 'All remaining projects are currently being moderated by others.',
type: 'warning',
})
}
}
if (!foundEligible) {
// If no unlocked projects found, go back to moderation queue
if (!foundUnlocked) {
await navigateTo({
name: 'moderation',
})
@@ -27,11 +27,6 @@ const messages = defineMessages({
id: 'hosting.plan.select-plan',
defaultMessage: 'Select plan',
},
billedInterval: {
id: 'servers.purchase.step.plan.billed',
defaultMessage:
'billed {interval, select, monthly {monthly} quarterly {quarterly} yearly {yearly} other {{interval}}}',
},
})
const plans: Record<
@@ -137,9 +132,7 @@ const billingMonths = computed(() => {
<span class="m-0 text-2xl font-bold text-contrast">
{{ formatPrice(price / billingMonths, currency, true) }}
<span class="text-lg font-semibold text-secondary">
/ month<template v-if="interval !== 'monthly'"
>, {{ formatMessage(messages.billedInterval, { interval }) }}</template
>
/ month<template v-if="interval !== 'monthly'">, billed {{ interval }}</template>
</span>
</span>
<p class="m-0 max-w-[18rem]">{{ formatMessage(plans[plan].description) }}</p>
@@ -14,7 +14,7 @@
<p class="m-0">
<IntlFormatted
:message-id="messages.resubmitModalDescription"
:values="{ projectTitle: project.title }"
:message-values="{ projectTitle: project.title }"
>
<template #project-title="{ children }">
<span class="font-semibold text-contrast">
+19 -38
View File
@@ -1,10 +1,3 @@
function normalizeAuthToken(value) {
if (typeof value === 'string') {
return value
}
return ''
}
export const useAuth = async (oldToken = null) => {
const auth = useState('auth', () => ({
user: null,
@@ -39,35 +32,27 @@ export const initAuth = async (oldToken = null) => {
})
if (oldToken) {
const normalized = normalizeAuthToken(oldToken)
if (normalized) {
authCookie.value = normalized
}
authCookie.value = oldToken
}
const oauthCode = normalizeAuthToken(route.query.code)
if (oauthCode && !route.fullPath.includes('new_account=true')) {
authCookie.value = oauthCode
if (route.query.code && !route.fullPath.includes('new_account=true')) {
authCookie.value = route.query.code
}
if (route.fullPath.includes('new_account=true') && route.path !== '/auth/welcome') {
const redirect = route.path.startsWith('/auth/') ? null : route.fullPath
await navigateTo(
`/auth/welcome?authToken=${oauthCode}${
`/auth/welcome?authToken=${route.query.code}${
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''
}`,
)
}
const tokenStr = normalizeAuthToken(authCookie.value)
if (authCookie.value) {
auth.token = authCookie.value
if (authCookie.value != null && tokenStr === '') {
authCookie.value = null
} else if (tokenStr) {
auth.token = tokenStr
if (!auth.token.startsWith('mra_')) {
if (!auth.token || !auth.token.startsWith('mra_')) {
return auth
}
@@ -86,7 +71,7 @@ export const initAuth = async (oldToken = null) => {
}
}
if (!auth.user && auth.token && typeof auth.token === 'string') {
if (!auth.user && auth.token) {
try {
const session = await useBaseFetch(
'session/refresh',
@@ -99,22 +84,18 @@ export const initAuth = async (oldToken = null) => {
true,
)
auth.token = normalizeAuthToken(session.session)
if (auth.token) {
authCookie.value = auth.token
auth.user = await useBaseFetch(
'user',
{
headers: {
Authorization: auth.token,
},
auth.token = session.session
authCookie.value = auth.token
auth.user = await useBaseFetch(
'user',
{
headers: {
Authorization: auth.token,
},
true,
)
} else {
authCookie.value = null
auth.token = ''
}
},
true,
)
} catch {
authCookie.value = null
}
+4 -4
View File
@@ -199,7 +199,7 @@ export const scopeMessages = defineMessages({
},
patCreateDescription: {
id: 'scopes.patCreate.description',
defaultMessage: 'Create personal access tokens',
defaultMessage: 'Create personal API tokens',
},
patReadLabel: {
id: 'scopes.patRead.label',
@@ -207,7 +207,7 @@ export const scopeMessages = defineMessages({
},
patReadDescription: {
id: 'scopes.patRead.description',
defaultMessage: 'View created personal access tokens',
defaultMessage: 'View created API tokens',
},
patWriteLabel: {
id: 'scopes.patWrite.label',
@@ -215,7 +215,7 @@ export const scopeMessages = defineMessages({
},
patWriteDescription: {
id: 'scopes.patWrite.description',
defaultMessage: 'Edit personal access tokens',
defaultMessage: 'Edit personal API tokens',
},
patDeleteLabel: {
id: 'scopes.patDelete.label',
@@ -223,7 +223,7 @@ export const scopeMessages = defineMessages({
},
patDeleteDescription: {
id: 'scopes.patDelete.description',
defaultMessage: 'Delete your personal access tokens',
defaultMessage: 'Delete your personal API tokens',
},
sessionReadLabel: {
id: 'scopes.sessionRead.label',
@@ -61,7 +61,7 @@ export interface UseServerInstallContentOptions {
const messages = defineMessages({
unsupportedContentType: {
id: 'discover.install.error.unsupported-content-type',
defaultMessage: 'This content type cannot be installed to a server from Discover.',
defaultMessage: 'This content type cannot be installed to a server from browse.',
},
noServerWorld: {
id: 'discover.install.error.no-server-world',
@@ -63,11 +63,7 @@ export function useCdnDownloadContext() {
})
function createProjectDownloadUrl(originalUrl: string, context?: DownloadContext): string {
if (
typeof originalUrl !== 'string' ||
!originalUrl ||
!originalUrl.startsWith('https://cdn.modrinth.com')
) {
if (!originalUrl.startsWith('https://cdn.modrinth.com')) {
return originalUrl
}
+1 -4
View File
@@ -199,20 +199,17 @@ export type ModerationOwnership = ModerationOwnershipUser | ModerationOwnershipO
export interface ProjectWithOwnership {
ownership: ModerationOwnership
external_dependencies_count: number
[key: string]: any
}
export interface ModerationProject {
project: any
ownership: ModerationOwnership | null
external_dependencies_count: number
}
export function toModerationProjects(projects: ProjectWithOwnership[]): ModerationProject[] {
return projects.map(({ ownership, external_dependencies_count, ...project }) => ({
return projects.map(({ ownership, ...project }) => ({
project,
ownership: ownership ?? null,
external_dependencies_count: external_dependencies_count,
}))
}
+1 -40
View File
@@ -761,13 +761,9 @@ import {
commonMessages,
commonProjectTypeCategoryMessages,
commonSettingsMessages,
createHostingIntercomIdentityKey,
defineMessages,
injectModrinthClient,
injectPageContext,
OverflowMenu,
providePageContext,
useHostingIntercom,
useVIntl,
} from '@modrinth/ui'
import TeleportOverflowMenu from '@modrinth/ui/src/components/base/TeleportOverflowMenu.vue'
@@ -812,25 +808,6 @@ const router = useNativeRouter()
const signInRouteObj = computed(() => getSignInRouteObj(route))
const link = config.public.siteUrl + route.path.replace(/\/+$/, '')
const client = injectModrinthClient()
const pageContext = injectPageContext()
const hostingIntercomActive = computed(() => route.path.startsWith('/hosting') && !!auth.value.user)
const hostingIntercomServerId = computed(() => {
const rawId = route.params.id
return Array.isArray(rawId) ? rawId[0] : rawId
})
const hostingIntercom = useHostingIntercom({
enabled: hostingIntercomActive,
appId: computed(() => config.public.intercomAppId),
fetchToken: fetchIntercomToken,
identityKey: computed(() =>
createHostingIntercomIdentityKey(auth.value.user, hostingIntercomServerId.value),
),
})
providePageContext({
...pageContext,
intercomBubble: hostingIntercom.intercomBubble,
})
const { data: payoutBalance } = useQuery({
queryKey: ['payout', 'balance'],
@@ -859,12 +836,6 @@ const showTinMismatchBanner = computed(() => {
const basePopoutId = useId()
async function fetchIntercomToken() {
return $fetch('/api/intercom/messenger-jwt', {
query: hostingIntercomServerId.value ? { server_id: hostingIntercomServerId.value } : {},
})
}
const navMenuMessages = defineMessages({
home: {
id: 'layout.nav.home',
@@ -1177,7 +1148,7 @@ const isDiscovering = computed(
)
const isDiscoveringSubpage = computed(
() => route.name && route.name.startsWith('type-project') && !route.query.sid,
() => route.name && route.name.startsWith('type-id') && !route.query.sid,
)
const isRussia = computed(() => country.value === 'ru')
@@ -1386,11 +1357,7 @@ const { cycle: changeTheme } = useTheme()
&-mobile {
.account-container {
opacity: 0;
padding-bottom: 0;
pointer-events: none;
transition: opacity 0.15s ease-in-out;
visibility: hidden;
.account-button {
padding: var(--spacing-card-md);
@@ -1413,12 +1380,6 @@ const { cycle: changeTheme } = useTheme()
&.expanded {
transform: translateY(0);
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.3);
.account-container {
opacity: 1;
pointer-events: auto;
visibility: visible;
}
}
}
}
+7 -1
View File
@@ -410,6 +410,9 @@
"create-project-version.create-modal.stage.add-files.admonition": {
"message": "الملفات التكميلية تستخدم لدعم الموارد مثل كود المصدر، وليس للإصدارات أو الأنواع البديلة."
},
"create.collection.collection-info": {
"message": "سيتم إنشاء مجموعتك الجديدة كمجموعة عامة تحتوي على {count, plural,\n one {# مشروع}\n other {# مشاريع}\n}."
},
"create.collection.create-collection": {
"message": "إنشاء مجموعة"
},
@@ -899,6 +902,9 @@
"dashboard.revenue.stats.withdrawn": {
"message": "تم سحبه "
},
"dashboard.revenue.tos": {
"message": "بتحميل المشاريع إلى مودرينث وسحب الأموال من حسابك، فإنك توافق على <terms-link> المكافآت. تعرّف على المزيد حول </info-link> برنامج المكافآت. "
},
"dashboard.revenue.transactions.btn.download-csv": {
"message": "قم بالتنزيل كملف CSV "
},
@@ -936,7 +942,7 @@
"message": "موعد"
},
"dashboard.withdraw.completion.email-confirmation": {
"message": "ستتلقى رسالة بريد إلكتروني في <b>{email}</b> مع تعليمات لاسترداد المبلغ المسحوب. "
"message": "ستتلقى رسالة بريد إلكتروني في <b>{email}<b> مع تعليمات لاسترداد المبلغ المسحوب. "
},
"dashboard.withdraw.completion.exchange-rate": {
"message": "سعر الصرف "
+12 -159
View File
@@ -38,9 +38,6 @@
"app-marketing.download.options-title": {
"message": "Stahovácí možnosti"
},
"app-marketing.download.terms": {
"message": "Stažením aplikace Modrinth vyjadřujete souhlas s našimi <terms-link>Podmínkami</terms-link> a <privacy-link>Zásadami ochrany osobních údajů</privacy-link>."
},
"app-marketing.download.title": {
"message": "Stáhnout Modrinth App (Beta)"
},
@@ -107,9 +104,6 @@
"app-marketing.features.performance.cpu-percent": {
"message": "% CPU"
},
"app-marketing.features.performance.description": {
"message": "Aplikace Modrinth je výkonnější než mnoho předních správců modů a využívá pouze 150MB paměti RAM!"
},
"app-marketing.features.performance.discord": {
"message": "Discord"
},
@@ -404,51 +398,12 @@
"collection.title": {
"message": "{name} - Kolekce"
},
"conversation-thread.action.add-private-note": {
"message": "Přidat soukromou poznámku"
},
"conversation-thread.action.close-thread": {
"message": "Uzavřít vlákno"
},
"conversation-thread.action.close-with-reply": {
"message": "Uzavřít s odpovědí"
},
"conversation-thread.action.reject": {
"message": "Zamítnout"
},
"conversation-thread.action.reject-with-reply": {
"message": "Zamítnout s odpovědí"
},
"conversation-thread.action.reopen-thread": {
"message": "Znovuotevřít vlákno"
},
"conversation-thread.action.reply": {
"message": "Odpovědět"
},
"conversation-thread.action.reply-to-thread": {
"message": "Odpovědět vláknu"
},
"conversation-thread.action.send": {
"message": "Odeslat"
},
"conversation-thread.action.send-to-review": {
"message": "Odeslat ke kontrole"
},
"conversation-thread.action.send-to-review-with-reply": {
"message": "Odeslat ke kontrole s odpovědí"
},
"conversation-thread.reply-editor.placeholder.reply": {
"message": "Odpovědět vláknu..."
},
"conversation-thread.reply-editor.placeholder.send": {
"message": "Odeslat zprávu..."
},
"conversation-thread.reply-modal.header": {
"message": "Odpovědět vláknu"
},
"create-project-version.create-modal.stage.add-files.admonition": {
"message": "Doplňkové soubory slouží jako podpůrné materiály (např. zdrojový kód), ne jako alternativní verze či varianty."
},
"create.collection.collection-info": {
"message": "Tvá nová kolekce bude vytvořena jako veřejná a bude obsahovat {count, plural, =0 {žádné projekty} one {# projekt} few {# projekty} other {# projektů}}."
},
"create.collection.create-collection": {
"message": "Vytvořit kolekci"
},
@@ -539,9 +494,6 @@
"create.project.name-placeholder": {
"message": "Zadejte název projektu..."
},
"create.project.owner-label": {
"message": "Vlastník"
},
"create.project.summary-description": {
"message": "Popiště vaši organizace dvěma nebo jednou větou."
},
@@ -593,18 +545,9 @@
"dashboard.affiliate-links.revoke-confirm.title": {
"message": "Jste si jistý, že chcete zrušit svůj affiliate odkaz „{title}“?"
},
"dashboard.analytics.total-downloads": {
"message": "Celková stažení"
},
"dashboard.analytics.total-followers": {
"message": "Celkoví sledující"
},
"dashboard.collections.button.create-new": {
"message": "Vytvořit novou"
},
"dashboard.collections.empty.get-started-hint": {
"message": "Vytvořte svou první kolekci a začněte!"
},
"dashboard.collections.label.projects-count": {
"message": "{count} {countPlural, plural, one {project} other {projects}}"
},
@@ -614,15 +557,6 @@
"dashboard.collections.long-title": {
"message": "Vaše kolekce"
},
"dashboard.collections.sort.name-ascending": {
"message": "Jméno (A-Z)"
},
"dashboard.collections.sort.recently-created": {
"message": "Nedávno vytvořeno"
},
"dashboard.collections.sort.recently-updated": {
"message": "Nedávno aktualizováno"
},
"dashboard.creator-tax-form-modal.confirmation.title": {
"message": "Hotovo! 🎉"
},
@@ -638,18 +572,9 @@
"dashboard.creator-withdraw-modal.details-label": {
"message": "Detaily"
},
"dashboard.creator-withdraw-modal.fee-breakdown-amount": {
"message": "Množství"
},
"dashboard.creator-withdraw-modal.fee-breakdown-fee": {
"message": "Poplatek"
},
"dashboard.creator-withdraw-modal.fee-breakdown-gift-card-value": {
"message": "Hodnota dárkové karty"
},
"dashboard.creator-withdraw-modal.fee-breakdown-usd-equivalent": {
"message": "Ekvivalent americkému dolaru"
},
"dashboard.creator-withdraw-modal.method-selection.country-placeholder": {
"message": "Zvolte vaši zemi"
},
@@ -698,18 +623,9 @@
"dashboard.creator-withdraw-modal.stage.tax-form": {
"message": ""
},
"dashboard.creator-withdraw-modal.tremendous-details.enter-denomination-placeholder": {
"message": "Zadejte množství"
},
"dashboard.creator-withdraw-modal.tremendous-details.payment-method": {
"message": "Platební metoda"
},
"dashboard.creator-withdraw-modal.tremendous-details.reward": {
"message": "Odměna"
},
"dashboard.creator-withdraw-modal.tremendous-details.reward-placeholder": {
"message": "Vyberte odměnu"
},
"dashboard.creator-withdraw-modal.tremendous-details.reward-plural": {
"message": "Odměny"
},
@@ -719,57 +635,9 @@
"dashboard.creator-withdraw-modal.withdraw-limit-used": {
"message": "Vyčerpali jste svůj limit pro výběr <b>{withdrawLimit}</b>. Pro vyšší výběr musíte vyplnit daňový formulář."
},
"dashboard.organizations.button.create": {
"message": "Vytvořit organizaci"
},
"dashboard.organizations.empty.cta": {
"message": "Vytvořte organizaci!"
},
"dashboard.organizations.error.fetch": {
"message": "Nepodařilo se načíst organizace"
},
"dashboard.overview.notifications.button.mark-all-as-read": {
"message": "Označit vše jako přečtené"
},
"dashboard.overview.notifications.button.view-history": {
"message": "Ukázat historii"
},
"dashboard.overview.notifications.history.label": {
"message": "Historie"
},
"dashboard.overview.notifications.history.title": {
"message": "Historie oznámení"
},
"dashboard.overview.notifications.loading": {
"message": "Načítání oznámení..."
},
"dashboard.projects.head-title": {
"message": "Projekty"
},
"dashboard.projects.links.discord-invite.label": {
"message": "Discord pozvánka"
},
"dashboard.projects.links.placeholder.valid-url": {
"message": "Zadejte platnou URL"
},
"dashboard.projects.sort.option.name": {
"message": "Jméno"
},
"dashboard.projects.sort.option.status": {
"message": "Stav"
},
"dashboard.projects.table.name": {
"message": "Jméno"
},
"dashboard.projects.table.status": {
"message": "Stav"
},
"dashboard.revenue.available-now": {
"message": "K dispozici nyní"
},
"dashboard.revenue.balance": {
"message": "Zůstatek"
},
"dashboard.revenue.estimated-with-date": {
"message": "Odhadováno {date}"
},
@@ -779,39 +647,18 @@
"dashboard.revenue.transactions.btn.download-csv": {
"message": "Stáhnout jako CSV"
},
"dashboard.sidebar.label.creators": {
"message": "Tvůrci"
},
"dashboard.sidebar.label.notifications": {
"message": "Oznámení"
},
"dashboard.sidebar.label.organizations": {
"message": "Organizace"
},
"dashboard.sidebar.label.projects": {
"message": "Projekty"
},
"dashboard.withdraw.completion.account": {
"message": "Účet"
},
"dashboard.withdraw.completion.amount": {
"message": "Množství"
},
"dashboard.withdraw.completion.date": {
"message": "Datum"
},
"dashboard.withdraw.completion.fee": {
"message": "Poplatek"
},
"dashboard.withdraw.completion.transactions-button": {
"message": "Transakce"
},
"dashboard.withdraw.completion.wallet": {
"message": "Peněženka"
},
"discover.install.back-to-server": {
"message": "Zpět na server"
},
"error.collection.404.list_item.1": {
"message": "Možná jste zadali URL adresu kolekce špatně."
},
@@ -902,9 +749,6 @@
"frog.title": {
"message": "Žába"
},
"hosting-marketing.included.custom-url": {
"message": "Vlastní URL"
},
"landing.creator.feature.monetization.title": {
"message": "Monetizace"
},
@@ -1208,6 +1052,9 @@
"profile.meta.description-with-bio": {
"message": "{bio} - Stáhněte si projekty od {username} na Modrinthu"
},
"profile.stats.projects-followers": {
"message": "{count, plural, one {<stat>{count}</stat> sledující projektu} few {<stat>{count}</stat> sledující projektu} other {<stat>{count}</stat> sledujících projektu}}"
},
"profile.user-id": {
"message": "ID uživatele: {id}"
},
@@ -1352,6 +1199,12 @@
"project.settings.title": {
"message": "Nastavení"
},
"project.stats.downloads-label": {
"message": "stažení{count, plural, one {} few {} many {} other {}}"
},
"project.stats.followers-label": {
"message": "sledující{count, plural, one {} few {} other{ch}}"
},
"project.versions.title": {
"message": "Verze"
},
@@ -1013,6 +1013,9 @@
"hosting-marketing.faq.location": {
"message": "Hvor er Modrinth Hosting servere placeret? Kan jeg vælge et område?"
},
"hosting-marketing.faq.location.answer": {
"message": "Vi har servere tilgængelig i Nord Amerika, Europa, og Sydøst Asien på nuværende tidspunkt som du kan vælge imellem under købet. Flere områder kommer i fremtiden! Hvis du gerne vil skifte til dit område, venligst kontakt support."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Hvilken Minecraft version og loaders kan blive brugt?"
},
@@ -1931,12 +1934,21 @@
"report.already-reported": {
"message": "Du har allerede rapporteret {title}"
},
"report.already-reported-description": {
"message": "Du har allerede en åben anmeldelse for {item, select, project {dette projekt} version {denne version} user {denne bruger} other {dette indhold}}. Du kan tilføje flere detaljer til denne anmeldse hvis du har mere information at tilføje."
},
"report.back-to-item": {
"message": "Tilbage til {item, select, project {projekt} version {version} user {bruger} other {indhold}}"
},
"report.body.description": {
"message": "Inkluder links og billeder hvis muligt og relevant. Tomme eller upræcise rapporter vil blive lukket eller ignoreret."
},
"report.body.title": {
"message": "Venligst tilføj mere information omkring din anmeldelse"
},
"report.could-not-find": {
"message": "Kunne ikke finde {item, select, project {projekt} version {version} user {bruger} other {indhold}}"
},
"report.for.violation.description": {
"message": "Eksempler inkluderer ondsindet, spam, stødende, vildledende, misvisende og ulovligt indhold."
},
@@ -1952,9 +1964,15 @@
"report.please-report": {
"message": "Venligst anmeld:"
},
"report.question.content-id": {
"message": "Hvad er IDet på {item, select, project {projektet} version {versionen} user {brugeren} other {indholdet}}?"
},
"report.question.content-type": {
"message": "Hvilket form for indhold anmelder du?"
},
"report.question.report-reason": {
"message": "Hvilke af Modrinths regler bryder {item, select, project {projektet} version {versionen} user {brugeren} other {indholdet}}?"
},
"report.submit": {
"message": "Indsend anmeldelse"
},
+73 -97
View File
@@ -407,51 +407,6 @@
"collection.title": {
"message": "{name} - Kollektion"
},
"conversation-thread.action.add-private-note": {
"message": "Private Notiz hinzufügen"
},
"conversation-thread.action.approve": {
"message": "Genehmigen"
},
"conversation-thread.action.approve-with-reply": {
"message": "Mit Antwort genehmigen"
},
"conversation-thread.action.close-thread": {
"message": ""
},
"conversation-thread.action.reject": {
"message": "Ablehnen"
},
"conversation-thread.action.reject-with-reply": {
"message": "Mit Antwort ablehnen"
},
"conversation-thread.action.reply": {
"message": "Antworten"
},
"conversation-thread.action.send": {
"message": "Senden"
},
"conversation-thread.action.withhold": {
"message": "Zurückhalten"
},
"conversation-thread.action.withhold-with-reply": {
"message": "Mit Antwort zurückhalten"
},
"conversation-thread.reply-editor.placeholder.send": {
"message": "Sende eine Nachricht..."
},
"conversation-thread.reply-modal.confirmation.label": {
"message": "Mir ist bewusst, dass dieser Thread nicht aktiv von den Moderatoren überwacht wird."
},
"conversation-thread.resubmit-modal.description": {
"message": "Du schickst <project-title>{projectTitle}</project-title> zur Wiederprüfung."
},
"conversation-thread.resubmit-modal.header.resubmitting": {
"message": "Erneut zur Überprüfung einreichen"
},
"conversation-thread.resubmit-modal.header.submitting": {
"message": "Erneut zur Überprüfung einreichen"
},
"create-project-version.create-modal.stage.add-files.admonition": {
"message": "Ergänzende Dateien dienen zur unterstützung von Ressourcen wie Quellcode, nicht für alternative Versionen oder Varianten."
},
@@ -920,8 +875,23 @@
"dashboard.head-title": {
"message": "Dashboard"
},
"dashboard.notifications.button.mark-all-as-read": {
"message": "Alle als gelesen markieren"
},
"dashboard.notifications.button.view-history": {
"message": "Verlauf ansehen"
},
"dashboard.notifications.empty.no-unread": {
"message": "Du hast keine ungelesenen Benachrichtigungen."
"message": "Du hast keine ungelesenen Nachrichten."
},
"dashboard.notifications.error.loading": {
"message": "Fehler beim laden von Benachrichtigungen:"
},
"dashboard.notifications.history.label": {
"message": "Verlauf"
},
"dashboard.notifications.history.title": {
"message": "Benachrichtigungsverlauf"
},
"dashboard.notifications.link.see-all": {
"message": "Alle ansehen"
@@ -932,6 +902,9 @@
"dashboard.notifications.link.view-more": {
"message": "Siehe {extraNotifs} weitere {extraNotifs, plural, one {Benachrichtigung} other {Benachrichtigungen}}"
},
"dashboard.notifications.loading": {
"message": "Lade Benachrichtigungen..."
},
"dashboard.organizations.button.create": {
"message": "Organisation erstellen"
},
@@ -947,27 +920,6 @@
"dashboard.organizations.title": {
"message": "Organisationen"
},
"dashboard.overview.notifications.button.mark-all-as-read": {
"message": "Alle als gelesen markieren"
},
"dashboard.overview.notifications.button.view-history": {
"message": "Verlauf ansehen"
},
"dashboard.overview.notifications.empty.no-unread": {
"message": "Du hast keine ungelesenen Benachrichtigungen."
},
"dashboard.overview.notifications.error.loading": {
"message": "Fehler beim Laden von Benachrichtigungen:"
},
"dashboard.overview.notifications.history.label": {
"message": "Verlauf"
},
"dashboard.overview.notifications.history.title": {
"message": "Benachrichtigungsverlauf"
},
"dashboard.overview.notifications.loading": {
"message": "Benachrichtigungen werden geladen..."
},
"dashboard.projects.bulk-edit-hint": {
"message": "Du kannst mehrere Projekte gleichzeitig bearbeiten, indem du sie unden auswählst."
},
@@ -1295,6 +1247,15 @@
"discover.install.heading.reset-modpack": {
"message": "Modpack zum installieren nach dem Reset auswählen"
},
"discover.seo.description": {
"message": "Suche und durchstöbere tausende von Minecraft {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}} auf Modrinth sofortigen, genauen Suchergebnissen. Unsere Filter helfen dir beim schnellen finden der besten Minecraft {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}}."
},
"discover.seo.title": {
"message": "Durchsuche {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}}"
},
"discover.seo.title-with-query": {
"message": "Durchsuche {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}} | {query}"
},
"error.collection.404.list_item.1": {
"message": "Du hast eventuell die Kollektions-URL falsch geschrieben."
},
@@ -1460,6 +1421,9 @@
"hosting-marketing.faq.location": {
"message": "Wo befinden sich Server von Modrinth Hosting? Kann ich eine Region auswählen?"
},
"hosting-marketing.faq.location.answer": {
"message": "Derzeit sind Server in Nordamerika, Europa und Südostasien verfügbar, die du beim Kauf auswählen kannst. Weitere Regionen werden in Zukunft dazukommen! Falls du deine Region wechseln möchtest, wende dich bitte an den Support."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Welche Minecraft-Versionen und Loader können verwendet werden?"
},
@@ -1790,18 +1754,15 @@
"layout.banner.add-email.description": {
"message": "Aus Sicherheitsgründen verlangt Modrinth, dass du eine E-Mail Adresse für dein Konto registrierst."
},
"layout.banner.build-fail.always-ignore": {
"message": "Immer ignorieren"
},
"layout.banner.build-fail.description": {
"message": "Diese Deployment von Modrinth's Frontend konnte keinen Status von der API generieren. Dies könnte aufgrund eines Ausfalls oder eines Konfigurationsfehlers sein. Erstelle neu wenn die API verfügbar ist. Fehlercodes: {errors}; Aktuelle API URL ist: {url}"
},
"layout.banner.build-fail.ignore": {
"message": "Ignorieren"
},
"layout.banner.build-fail.title": {
"message": "Fehler beim generieren des Status von der API beim erstellen."
},
"layout.banner.preview.description": {
"message": "Falls du die offizielle Modrinth-Website aufrufen wolltest, besuch <link>https://modrinth.com</link>. Diese Vorschauversion wird von Modrinth-Mitarbeitern zu Testzwecken genutzt. Sie wurde unter Verwendung von <branch-link>{owner}/{branch}</branch-link> @ {commit} erstellt."
},
"layout.banner.preview.title": {
"message": "Dies ist eine Vorschauversion der Modrinth-Webseite."
},
@@ -2630,30 +2591,6 @@
"project.license.title": {
"message": "Lizenz"
},
"project.moderation.admonition.approved.body.private": {
"message": "Dein Projekt ist privat, was bedeutet, dass nur du und von dir eingeladene Personen darauf zugreifen können."
},
"project.moderation.admonition.approved.header": {
"message": "Projekt genehmigt"
},
"project.moderation.admonition.rejected.header": {
"message": "Änderungen angefragt"
},
"project.moderation.admonition.under-review.body.1": {
"message": "Dein Projekt ist in der Warteschlange, um von Modrinths Moderationsteam überprüft zu werden."
},
"project.moderation.admonition.under-review.header": {
"message": "Projekt wird überprüft"
},
"project.moderation.admonition.withheld.header": {
"message": "Von Mitarbeitern entlistet"
},
"project.moderation.error.unauthorized": {
"message": "Nicht autorisiert"
},
"project.moderation.thread.title": {
"message": "Moderationsnachrichten"
},
"project.moderation.title": {
"message": "Moderation"
},
@@ -2786,12 +2723,24 @@
"report.already-reported": {
"message": "Du hast bereits {title} gemeldet"
},
"report.already-reported-description": {
"message": "Du hast bereits eine offene Meldung für {item, select, project {dieses Projekt} version {diese Version} user {diesen Benutzer} other {diesen Inhalt}}. Du kannst deiner Meldung weitere Details hinzufügen, falls du weitere Informationen hast."
},
"report.back-to-item": {
"message": "Zurück {item, select, project {zum Projekt} version {zur Version} user {zum Benutzer} other {zum Inhalt}}"
},
"report.body.description": {
"message": "Füge Links und Bilder hinzu, sofern relevant. Leere oder unzureichender Meldungen werden geschlossen und ignoriert."
},
"report.body.title": {
"message": "Bitte gib weiteren Kontext über deine Meldung"
},
"report.checking": {
"message": "Überprüfe {item, select, project {Projekt} version {Version} user {Benutzer} other {Inhalt}}..."
},
"report.could-not-find": {
"message": "Konnte {item, select, project {Projekt} version {Version} user {Benutzer} other {Inhalt}} nicht finden"
},
"report.for.violation": {
"message": "Verstoss gegen Modrinth's <rules-link>Regeln</rules-link> oder <terms-link>Nutzungsbedingungen</terms-link>"
},
@@ -2831,9 +2780,15 @@
"report.please-report": {
"message": "Bitte melde:"
},
"report.question.content-id": {
"message": "Was ist die ID {item, select, project {des Projekts} version {der Version} user {des Benutzers} other {des Inhalts}}?"
},
"report.question.content-type": {
"message": "Welche Art an Inhalt meldest du?"
},
"report.question.report-reason": {
"message": "Welche von Modrinths Regeln verletzt {item, select, project {dieses Projekt} version {diese Version} user {dieser Benutzer} other {dieser Inhalt}}?"
},
"report.report-content": {
"message": "Inhalt an Moderatoren melden"
},
@@ -3512,12 +3467,21 @@
"settings.billing.interval.month": {
"message": "Monat"
},
"settings.billing.interval.monthly": {
"message": "monatlich"
},
"settings.billing.interval.quarter": {
"message": "Quartal"
},
"settings.billing.interval.quarterly.adjective": {
"message": "vierteljährlich"
},
"settings.billing.interval.year": {
"message": "Jahr"
},
"settings.billing.interval.yearly": {
"message": "jährlich"
},
"settings.billing.midas.benefits.ad-free": {
"message": "Werbefreies Surfen auf modrinth.com und der Modrinth App"
},
@@ -3602,6 +3566,9 @@
"settings.billing.price.per-interval": {
"message": "{price} / {interval}"
},
"settings.billing.price.slash-interval": {
"message": "/{interval}"
},
"settings.billing.pyro.cpu": {
"message": "{shared} Geteilte CPUs (Spitzenlasten von bis zu {bursts} CPUs)"
},
@@ -3668,9 +3635,18 @@
"settings.billing.subscription.title": {
"message": "Abonnements"
},
"settings.billing.switch.switching-to-interval": {
"message": "Wechsle auf {interval}"
},
"settings.billing.switch.to-interval": {
"message": "Zu {interval} wechseln"
},
"settings.billing.switch.tooltip.monthly-additional-per-year": {
"message": "Monatliche Abrechnung kostet dich zusätzliche {amount} pro Jahr"
},
"settings.billing.switches-to-billing-on": {
"message": "Wechselt am {date} auf {interval} Abrechnung"
},
"settings.billing.update-method": {
"message": "Methode aktualisieren"
},
+71 -101
View File
@@ -407,51 +407,6 @@
"collection.title": {
"message": "{name} - Kollektion"
},
"conversation-thread.action.add-private-note": {
"message": "Private Notiz hinzufügen"
},
"conversation-thread.action.approve": {
"message": "Genehmigen"
},
"conversation-thread.action.approve-with-reply": {
"message": "Mit Antwort genehmigen"
},
"conversation-thread.action.close-thread": {
"message": ""
},
"conversation-thread.action.reject": {
"message": "Ablehnen"
},
"conversation-thread.action.reject-with-reply": {
"message": "Mit Antwort ablehnen"
},
"conversation-thread.action.reply": {
"message": "Antworten"
},
"conversation-thread.action.send": {
"message": "Senden"
},
"conversation-thread.action.withhold": {
"message": "Zurückhalten"
},
"conversation-thread.action.withhold-with-reply": {
"message": "Mit Antwort zurückhalten"
},
"conversation-thread.reply-editor.placeholder.send": {
"message": "Sende eine Nachricht..."
},
"conversation-thread.reply-modal.confirmation.label": {
"message": "Mir ist bewusst, dass dieser Thread nicht aktiv von den Moderatoren überwacht wird."
},
"conversation-thread.resubmit-modal.description": {
"message": "Du schickst <project-title>{projectTitle}</project-title> zur Wiederprüfung."
},
"conversation-thread.resubmit-modal.header.resubmitting": {
"message": "Erneut zur Überprüfung einreichen"
},
"conversation-thread.resubmit-modal.header.submitting": {
"message": "Erneut zur Überprüfung einreichen"
},
"create-project-version.create-modal.stage.add-files.admonition": {
"message": "Ergänzende Dateien dienen zur Unterstützung von Ressourcen wie Quellcode, nicht für alternative Versionen oder Varianten."
},
@@ -920,9 +875,24 @@
"dashboard.head-title": {
"message": "Dashboard"
},
"dashboard.notifications.button.mark-all-as-read": {
"message": "Alle als gelesen markieren"
},
"dashboard.notifications.button.view-history": {
"message": "Verlauf ansehen"
},
"dashboard.notifications.empty.no-unread": {
"message": "Du hast keine ungelesenen Benachrichtigungen."
},
"dashboard.notifications.error.loading": {
"message": "Fehler beim Laden von Benachrichtigungen:"
},
"dashboard.notifications.history.label": {
"message": "Verlauf"
},
"dashboard.notifications.history.title": {
"message": "Benachrichtigungsverlauf"
},
"dashboard.notifications.link.see-all": {
"message": "Alle ansehen"
},
@@ -932,6 +902,9 @@
"dashboard.notifications.link.view-more": {
"message": "Siehe {extraNotifs} weitere {extraNotifs, plural, one {Benachrichtigung} other {Benachrichtigungen}} an"
},
"dashboard.notifications.loading": {
"message": "Benachrichtigungen werden geladen..."
},
"dashboard.organizations.button.create": {
"message": "Organisation erstellen"
},
@@ -947,27 +920,6 @@
"dashboard.organizations.title": {
"message": "Organisationen"
},
"dashboard.overview.notifications.button.mark-all-as-read": {
"message": "Alle als gelesen markieren"
},
"dashboard.overview.notifications.button.view-history": {
"message": "Verlauf ansehen"
},
"dashboard.overview.notifications.empty.no-unread": {
"message": "Du hast keine ungelesenen Benachrichtigungen."
},
"dashboard.overview.notifications.error.loading": {
"message": "Fehler beim Laden von Benachrichtigungen:"
},
"dashboard.overview.notifications.history.label": {
"message": "Verlauf"
},
"dashboard.overview.notifications.history.title": {
"message": "Benachrichtigungsverlauf"
},
"dashboard.overview.notifications.loading": {
"message": "Benachrichtigungen werden geladen..."
},
"dashboard.projects.bulk-edit-hint": {
"message": "Du kannst mehrere Projekte gleichzeitig bearbeiten, indem du sie unten auswählst."
},
@@ -1289,11 +1241,14 @@
"discover.install.error.no-server-world": {
"message": "Keine Serverwelt ist zur Installation verfügbar."
},
"discover.install.error.unsupported-content-type": {
"message": "Dieser Inhaltstyp kann nicht vom Durchstöbern zu einem Server hinzugefügt werden."
"discover.seo.description": {
"message": "Durchsuche und entdecke tausende Minecraft {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}} auf Modrinth mit sofortigen und präzisen Suchergebnissen. Unsere Filter helfen dir dabei, schnell die besten Minecraft {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}} zu finden."
},
"discover.install.heading.reset-modpack": {
"message": "Modpack zur Installation nach dem Zurücksetzen auswählen"
"discover.seo.title": {
"message": "Durchsuche {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}}"
},
"discover.seo.title-with-query": {
"message": "Durchsuche {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}} | {query}"
},
"error.collection.404.list_item.1": {
"message": "Eventuell hast du die URL der Kollektion falsch eingegeben."
@@ -1460,6 +1415,9 @@
"hosting-marketing.faq.location": {
"message": "Wo befinden sich Server von Modrinth Hosting? Kann ich eine Region auswählen?"
},
"hosting-marketing.faq.location.answer": {
"message": "Derzeit sind Server in Nordamerika, Europa und Südostasien verfügbar, die du beim Kauf auswählen kannst. Weitere Regionen werden in Zukunft dazukommen! Falls du deine Region wechseln möchtest, wende dich bitte an den Support."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Welche Minecraft-Versionen und Loader können verwendet werden?"
},
@@ -1790,18 +1748,15 @@
"layout.banner.add-email.description": {
"message": "Aus Sicherheitsgründen verlangt Modrinth, dass du eine E-Mail-Adresse für dein Konto registrierst."
},
"layout.banner.build-fail.always-ignore": {
"message": "Immer ignorieren"
},
"layout.banner.build-fail.description": {
"message": "Dieses Deployment des Modrinth-Frontends konnte keinen Status aus der API generieren. Dies kann an einer Störung oder einem Fehler in der Konfiguration liegen. Erneut erstellen, wenn die API verfügbar ist. Fehlercodes: {errors}; Aktuelle API-URL lautet: {url}"
},
"layout.banner.build-fail.ignore": {
"message": "Ignorieren"
},
"layout.banner.build-fail.title": {
"message": "Fehler beim Generieren des Status aus der API beim Erstellen."
},
"layout.banner.preview.description": {
"message": "Falls du die offizielle Modrinth-Website aufrufen wolltest, besuch <link>https://modrinth.com</link>. Diese Vorschauversion wird von Modrinth-Mitarbeitern zu Testzwecken genutzt. Sie wurde unter Verwendung von <branch-link>{owner}/{branch}</branch-link> @ {commit} erstellt."
},
"layout.banner.preview.title": {
"message": "Dies ist eine Vorschauversion der Modrinth-Website."
},
@@ -2630,30 +2585,6 @@
"project.license.title": {
"message": "Lizenz"
},
"project.moderation.admonition.approved.body.private": {
"message": "Dein Projekt ist privat, was bedeutet, dass nur du und von dir eingeladene Personen darauf zugreifen können."
},
"project.moderation.admonition.approved.header": {
"message": "Projekt genehmigt"
},
"project.moderation.admonition.rejected.header": {
"message": "Änderungen angefragt"
},
"project.moderation.admonition.under-review.body.1": {
"message": "Dein Projekt ist in der Warteschlange, um von Modrinths Moderationsteam überprüft zu werden."
},
"project.moderation.admonition.under-review.header": {
"message": "Projekt wird überprüft"
},
"project.moderation.admonition.withheld.header": {
"message": "Von Mitarbeitern entlistet"
},
"project.moderation.error.unauthorized": {
"message": "Nicht autorisiert"
},
"project.moderation.thread.title": {
"message": "Moderationsnachrichten"
},
"project.moderation.title": {
"message": "Moderation"
},
@@ -2786,12 +2717,24 @@
"report.already-reported": {
"message": "Du hast {title} bereits gemeldet"
},
"report.already-reported-description": {
"message": "Du hast bereits eine offene Meldung für {item, select, project {dieses Projekt} version {diese Version} user {diesen Benutzer} other {diesen Inhalt}}. Du kannst deiner Meldung weitere Details hinzufügen, falls du weitere Informationen hast."
},
"report.back-to-item": {
"message": "Zurück {item, select, project {zum Projekt} version {zur Version} user {zum Benutzer} other {zum Inhalt}}"
},
"report.body.description": {
"message": "Füge nach Möglichkeit und Relevanz Links und Bilder ein. Leere oder unzureichende Meldungen werden geschlossen und ignoriert."
},
"report.body.title": {
"message": "Bitte gib weiteren Kontext über deine Meldung"
},
"report.checking": {
"message": "Überprüfe {item, select, project {Projekt} version {Version} user {Benutzer} other {Inhalt}}..."
},
"report.could-not-find": {
"message": "Konnte {item, select, project {Projekt} version {Version} user {Benutzer} other {Inhalt}} nicht finden"
},
"report.for.violation": {
"message": "Verstoß gegen Modrinths <rules-link>Regeln</rules-link> oder <terms-link>Nutzungsbedingungen</terms-link>"
},
@@ -2831,9 +2774,15 @@
"report.please-report": {
"message": "Bitte melde:"
},
"report.question.content-id": {
"message": "Wie lautet die ID {item, select, project {des Projekts} version {der Version} user {des Benutzers} other {des Inhalts}}?"
},
"report.question.content-type": {
"message": "Welche Art an Inhalt möchtest du melden?"
},
"report.question.report-reason": {
"message": "Welche von Modrinths Regeln verletzt {item, select, project {dieses Projekt} version {diese Version} user {dieser Benutzer} other {dieser Inhalt}}?"
},
"report.report-content": {
"message": "Inhalt den Moderatoren melden"
},
@@ -3512,12 +3461,21 @@
"settings.billing.interval.month": {
"message": "Monat"
},
"settings.billing.interval.monthly": {
"message": "monatlich"
},
"settings.billing.interval.quarter": {
"message": "Quartal"
},
"settings.billing.interval.quarterly.adjective": {
"message": "vierteljährlich"
},
"settings.billing.interval.year": {
"message": "Jahr"
},
"settings.billing.interval.yearly": {
"message": "jährlich"
},
"settings.billing.midas.benefits.ad-free": {
"message": "Werbungsfreies Surfen auf modrinth.com und in Modrinth App"
},
@@ -3602,6 +3560,9 @@
"settings.billing.price.per-interval": {
"message": "{price} / {interval}"
},
"settings.billing.price.slash-interval": {
"message": "/{interval}"
},
"settings.billing.pyro.cpu": {
"message": "{shared} Geteilte CPUs (Spitzenlasten von bis zu {bursts} CPUs)"
},
@@ -3668,14 +3629,23 @@
"settings.billing.subscription.title": {
"message": "Abonnements"
},
"settings.billing.switch.switching-to-interval": {
"message": "Wechsle auf {interval}"
},
"settings.billing.switch.to-interval": {
"message": "Wechsle auf {interval}"
},
"settings.billing.switch.tooltip.monthly-additional-per-year": {
"message": "Monatliche Abrechnung kostet dich zusätzliche {amount} pro Jahr"
},
"settings.billing.switches-to-billing-on": {
"message": "Wechselt am {date} auf {interval} Abrechnung"
},
"settings.billing.update-method": {
"message": "Methode aktualisieren"
},
"settings.billing.upgrade": {
"message": "Upgraden"
"message": "Verbessern"
},
"settings.display.banner.developer-mode.button": {
"message": "Entwicklermodus deaktivieren"
+35 -47
View File
@@ -123,7 +123,7 @@
"message": "∞ MB"
},
"app-marketing.features.performance.infinite-times-infinite-mb": {
"message": "∞ × ∞ MB"
"message": "∞ * ∞ MB"
},
"app-marketing.features.performance.less-than-150mb": {
"message": "< 150 MB"
@@ -1067,9 +1067,6 @@
"dashboard.projects.links.discord-invite.label": {
"message": "Discord invite"
},
"dashboard.projects.links.header.edit": {
"message": "Edit links"
},
"dashboard.projects.links.issue-tracker.description": {
"message": "A place for users to report bugs, issues, and concerns about your project."
},
@@ -1095,7 +1092,7 @@
"message": "Source code"
},
"dashboard.projects.links.wiki-page.description": {
"message": "A page containing information, documentation, and help for the project"
"message": "A page containing information, documentation, and help for the project."
},
"dashboard.projects.links.wiki-page.label": {
"message": "Wiki page"
@@ -1359,19 +1356,19 @@
"message": "No server world is available for install."
},
"discover.install.error.unsupported-content-type": {
"message": "This content type cannot be installed to a server from Discover."
"message": "This content type cannot be installed to a server from browse."
},
"discover.install.heading.reset-modpack": {
"message": "Selecting modpack to install after reset"
},
"discover.seo.description": {
"message": "Search and browse thousands of Minecraft {projectType} on Modrinth with instant, accurate search results. Our filters help you quickly find the best Minecraft {projectType}."
"message": "Search and browse thousands of Minecraft {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}} on Modrinth with instant, accurate search results. Our filters help you quickly find the best Minecraft {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}}."
},
"discover.seo.title": {
"message": "Search {projectType}"
"message": "Search {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}}"
},
"discover.seo.title-with-query": {
"message": "Search {projectType} | {query}"
"message": "Search {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}} | {query}"
},
"error.collection.404.list_item.1": {
"message": "You may have mistyped the collection's URL."
@@ -1539,7 +1536,7 @@
"message": "Where are Modrinth Hosting servers located? Can I choose a region?"
},
"hosting-marketing.faq.location.answer": {
"message": "We have servers available across North America, Europe, and Southeast Asia at the moment that you can choose upon purchase. More regions to come in the future! If you'd like to switch your region, please contact support."
"message": "We have servers available in North America, Europe, and Southeast Asia at the moment that you can choose upon purchase. More regions to come in the future! If you'd like to switch your region, please contact support."
},
"hosting-marketing.faq.versions-loaders": {
"message": "What Minecraft versions and loaders can be used?"
@@ -1884,7 +1881,7 @@
"message": "Error generating state from API when building."
},
"layout.banner.preview.description": {
"message": "If you meant to access the official Modrinth website, visit {url}. This preview deploy is used by Modrinth staff for testing purposes. It was built using {ref}."
"message": "If you meant to access the official Modrinth website, visit {url}. This preview deploy is used by Modrinth staff for testing purposes. It was built using <branch-link>{owner}/{branch}</branch-link> @ {commit}."
},
"layout.banner.preview.title": {
"message": "This is a preview deploy of the Modrinth website."
@@ -2360,9 +2357,6 @@
"muralpay.warning.wallet-address": {
"message": "Double-check your wallet address. Funds sent to an incorrect address cannot be recovered."
},
"organization.settings.projects.edit-links.affected-projects": {
"message": "Changes will be applied to {count, plural, one {# project} other {# projects}}."
},
"profile.bio.fallback.creator": {
"message": "A Modrinth creator."
},
@@ -2387,9 +2381,6 @@
"profile.button.set-affiliate": {
"message": "Set as affiliate"
},
"profile.collection.projects-count": {
"message": "{count, plural, one {# project} other {# projects}}"
},
"profile.details.label.auth-providers": {
"message": "Auth providers"
},
@@ -2459,12 +2450,6 @@
"profile.meta.description-with-bio": {
"message": "{bio} - Download {username}'s projects on Modrinth"
},
"profile.official-account": {
"message": "Official Modrinth account"
},
"profile.official-account.bio": {
"message": "The official user account of Modrinth. Get support at <support-link></support-link> or via email at <email></email>"
},
"profile.stats.projects-followers": {
"message": "{count, plural, one {<stat>{count}</stat> project follower} other {<stat>{count}</stat> project followers}}"
},
@@ -2913,10 +2898,10 @@
"message": "Visit projects dashboard"
},
"project.stats.downloads-label": {
"message": "{count, plural, one {download} other {downloads}}"
"message": "download{count, plural, one {} other {s}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {follower} other {followers}}"
"message": "follower{count, plural, one {} other {s}}"
},
"project.status.archived.message": {
"message": "{title} has been archived. {title} will not receive any further updates unless the author decides to unarchive the project."
@@ -2937,10 +2922,10 @@
"message": "You've already reported {title}"
},
"report.already-reported-description": {
"message": "You have an open report for this {item} already. You can add more details to your report if you have more information to add."
"message": "You have an open report for this {item, select, project {project} version {version} user {user} other {content}} already. You can add more details to your report if you have more information to add."
},
"report.back-to-item": {
"message": "Back to {item}"
"message": "Back to {item, select, project {project} version {version} user {user} other {content}}"
},
"report.body.description": {
"message": "Include links and images if possible and relevant. Empty or insufficient reports will be closed and ignored."
@@ -2949,10 +2934,10 @@
"message": "Please provide additional context about your report"
},
"report.checking": {
"message": "Checking {item}..."
"message": "Checking {item, select, project {project} version {version} user {user} other {content}}..."
},
"report.could-not-find": {
"message": "Could not find {item}"
"message": "Could not find {item, select, project {project} version {version} user {user} other {content}}"
},
"report.for.violation": {
"message": "Violation of Modrinth <rules-link>Rules</rules-link> or <terms-link>Terms of Use</terms-link>"
@@ -2994,13 +2979,13 @@
"message": "Please report:"
},
"report.question.content-id": {
"message": "What is the ID of the {item}?"
"message": "What is the ID of the {item, select, project {project} version {version} user {user} other {content}}?"
},
"report.question.content-type": {
"message": "What type of content are you reporting?"
},
"report.question.report-reason": {
"message": "Which of Modrinth's rules is this {item} violating?"
"message": "Which of Modrinth's rules is this {item, select, project {project} version {version} user {user} other {content}} violating?"
},
"report.report-content": {
"message": "Report content to moderators"
@@ -3114,25 +3099,25 @@
"message": "Write organizations"
},
"scopes.patCreate.description": {
"message": "Create personal access tokens"
"message": "Create personal API tokens"
},
"scopes.patCreate.label": {
"message": "Create PATs"
},
"scopes.patDelete.description": {
"message": "Delete your personal access tokens"
"message": "Delete your personal API tokens"
},
"scopes.patDelete.label": {
"message": "Delete PATs"
},
"scopes.patRead.description": {
"message": "View created personal access tokens"
"message": "View created API tokens"
},
"scopes.patRead.label": {
"message": "Read PATs"
},
"scopes.patWrite.description": {
"message": "Edit personal access tokens"
"message": "Edit personal API tokens"
},
"scopes.patWrite.label": {
"message": "Write PATs"
@@ -3341,9 +3326,6 @@
"servers.plan.small.description": {
"message": "Perfect for 15 friends with a few light mods."
},
"servers.purchase.step.plan.billed": {
"message": "billed {interval, select, monthly {monthly} quarterly {quarterly} yearly {yearly} other {{interval}}}"
},
"settings.account.button.complete-setup": {
"message": "Complete setup"
},
@@ -3506,12 +3488,6 @@
"settings.account.security.two-factor.description": {
"message": "Add an additional layer of security to your account during login."
},
"settings.account.security.two-factor.modal.remove.header": {
"message": "Remove 2FA"
},
"settings.account.security.two-factor.modal.setup.header": {
"message": "Setup 2FA"
},
"settings.account.security.two-factor.title": {
"message": "Two-factor authentication"
},
@@ -3689,12 +3665,21 @@
"settings.billing.interval.month": {
"message": "month"
},
"settings.billing.interval.monthly": {
"message": "monthly"
},
"settings.billing.interval.quarter": {
"message": "quarter"
},
"settings.billing.interval.quarterly.adjective": {
"message": "quarterly"
},
"settings.billing.interval.year": {
"message": "year"
},
"settings.billing.interval.yearly": {
"message": "yearly"
},
"settings.billing.midas.benefits.ad-free": {
"message": "Ad-free browsing on modrinth.com and Modrinth App"
},
@@ -3779,6 +3764,9 @@
"settings.billing.price.per-interval": {
"message": "{price} / {interval}"
},
"settings.billing.price.slash-interval": {
"message": "/{interval}"
},
"settings.billing.pyro.cpu": {
"message": "{shared} Shared CPUs (Bursts up to {bursts} CPUs)"
},
@@ -3846,16 +3834,16 @@
"message": "Subscriptions"
},
"settings.billing.switch.switching-to-interval": {
"message": "Switching to {interval, select, monthly {monthly} yearly {yearly} other {{interval}}}"
"message": "Switching to {interval}"
},
"settings.billing.switch.to-interval": {
"message": "Switch to {interval, select, monthly {monthly} yearly {yearly} other {{interval}}}"
"message": "Switch to {interval}"
},
"settings.billing.switch.tooltip.monthly-additional-per-year": {
"message": "Monthly billing will cost you an additional {amount} per year"
},
"settings.billing.switches-to-billing-on": {
"message": "Switches to {interval, select, monthly {monthly} yearly {yearly} other {{interval}}} billing on {date}"
"message": "Switches to {interval} billing on {date}"
},
"settings.billing.update-method": {
"message": "Update method"

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