Compare commits

...
Author SHA1 Message Date
Calum H. (IMB11) 25a5406e3f fix: z index 2026-05-25 18:53:39 +01:00
Prospector 9e5d29ced6 hotfix 2026-05-24 12:14:13 -07:00
Prospector 3e15d0b287 changelog 2026-05-24 10:46:35 -07:00
ProspectorandGitHub bcce7e28fd fix: guard against non-strings when decorating download urls (#6194) 2026-05-24 10:42:27 -07:00
ProspectorandGitHub 3889d0f5ec fix: simplify preview banner translation key (#6192) 2026-05-24 10:39:39 -07:00
ProspectorandGitHub 6f44c5b039 feat: add 2 second auto close on skipped notifications (#6191) 2026-05-24 10:39:27 -07:00
Modrinth BotandGitHub ea967845d9 New translations from Crowdin (main) (#6193) 2026-05-24 17:25:25 +00:00
Prospector a6967cf9cb changelog 2026-05-24 09:26:36 -07:00
ProspectorandGitHub 5bf92863b0 fix: spanish locale (#6189) 2026-05-24 09:21:14 -07:00
Prospector 71b6ecc10c fix changelog... 2026-05-23 23:26:34 -07:00
Prospector ed28bc7551 remove april fools redirect 2026-05-23 22:03:59 -07:00
Prospector 3e4197db7c changelog 2026-05-23 22:02:49 -07:00
ProspectorandGitHub e12230ff59 feat: update default memory from 2GB to 4GB (#6172)
* feat: update default memory from 2GB to 4GB

* prepr
2026-05-23 21:26:51 -07:00
ProspectorandGitHub aeb9f5a075 fix: invalid auth cookie causing page not to load (#6186) 2026-05-23 21:26:13 -07:00
Calum H.andGitHub 8b17441f40 feat: compact logs if they have logspam to prevent app crashing (#6181)
* feat: compact logs if they have logspam to prevent app crashing

* fix: lint
2026-05-23 18:22:15 +00:00
f9d47e8edc feat: Add new "Enabled" sorting button next to "Disabled" (#6000)
* feat: Added new "Enabled" sorting button next to "Disabled"

* Updated when filter buttons show

- When all mods are disabled, only "Disabled" shows
- When all mods are enabled, only "Enabled" shows
- When there disabled and enabled mods, then only both buttons show.

* Updated when filter buttons show

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-05-23 16:12:14 +00:00
a58bc3dc21 feat: java installation ui improvements (#5731)
* Clean impl of java installation ui improvements

* Migrate composable to ts

* Migrate to ButtonStyled, fix coloring

* Fix lint

* Fix clearing java path not refreshing state

* fix: use Table component + install btn disabled state tooltip

---------

Signed-off-by: Arthur <creeperkatze.dev@gmail.com>
Signed-off-by: Arthur <contact@creeperkatze.dev>
Co-authored-by: Creeperkatze <178587183+Creeperkatze@users.noreply.github.com>
Co-authored-by: Calum H. <calum@modrinth.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
2026-05-23 14:46:12 +00:00
ProspectorandGitHub 1e46444fb0 fix: checklist forcing redirects (#6173) 2026-05-22 15:53:22 -07:00
ProspectorandGitHub 1511e55597 fix: skip reviewed projects in queue (#6171) 2026-05-22 14:54:50 -07:00
aecsocketandGitHub 5727e156ed Fetch project analytics events on analytics get (#6143)
* Fetch project analytics events

* fix

* post-query ua bucketing

* fmt
2026-05-22 18:32:33 +00:00
Calum H.andGitHub 657186398d fix: remove broken roadmap reference (#6165)
Signed-off-by: Calum H. <calum@modrinth.com>
2026-05-22 18:10:01 +00:00
Michael H. 3ab2273782 chore: prepr 2026-05-22 19:58:11 +02:00
ProspectorandGitHub 893ec00fc6 feat: add external dep sorting to moderation queue (#6161)
* feat: add external dep sorting to moderation queue

* prepr
2026-05-21 16:40:13 -07:00
Prospector aa7dd1d210 web 2026-05-21 16:23:30 -07:00
Calum H.andGitHub f8733b0488 feat: changelog 21st may 2026 (#6158)
* feat: changelog 21st may 2026

* fix: changelog
2026-05-21 22:27:14 +00:00
Calum H.andGitHub d077d44540 fix: content tab uniqueness regression (#6156)
* fix: content tab uniqueness regression

Closes: #6154

* fix: further regressions

* fix: lint

* fix: lint
2026-05-21 22:03:35 +00:00
aecsocketandGitHub 4e1a61d8b6 Adjust window occlusion threshold (#6157) 2026-05-21 21:48:39 +00:00
aecsocketandGitHub 71dee4de40 Add modpacks with external files filter to moderation queue (#6155)
* simplify query

* make it a tristate

* external deps count
2026-05-21 20:21:54 +00:00
SychicandGitHub f74fad0cae fix: typo in version changelog (#6153) 2026-05-21 19:38:11 +00:00
Truman GaoandGitHub 07e81ac036 fix: project embeds (#6152)
* fix: project embeds

* remove: params.id
2026-05-21 17:02:53 +00:00
Calum H.andGitHub 6e7835fb35 feat: implement kryos upload sessions (#6145)
* feat: implement upload sessions

* fix: files not scoped

* feat: hide staging files folder and proper cancel feedback

* fix: lint
2026-05-21 16:49:48 +00:00
Calum H.andGitHub 2f95c4c441 feat: changelog 20th may 2026 (#6148) 2026-05-20 12:51:36 -07:00
aecsocketandGitHub 451b2d0e44 Window occlusion checks on MacOS (#6135)
* wip: window occlusion checks on MacOS

* wip: occlusion works

* occlusion notification

* fix ci

* fix

* wire in hiding into macos occlusion

* remove debug logs

* fix

* clean up
2026-05-20 19:31:12 +00:00
Truman GaoandGitHub 215643c846 fix: server project type page title (#6147)
* fix: server project type page title

* pnpm prepr
2026-05-20 19:01:19 +00:00
Truman GaoandGitHub d8b1415f9c feat: swap date input to use date picker (#6146)
* feat: swap date input to use date picker

* feat: update date picker to analytics branch changes

* feat: polish date picker usage
2026-05-20 17:50:46 +00:00
Calum H.andGitHub 3eeb549d20 fix: intercom bubble positioning properly (#6111)
* feat: fix intercom properly

* fix: positioning size + css transition

* fix: lint

* fix: ts

* fix: nitpick
2026-05-20 17:15:46 +00:00
Calum H.andGitHub c3fe7b4232 feat: content management changes (#6104)
* feat: change modpack updating flow

* fix: pending install state loss

* fix: mods.vue perf problems

* chore: todo doc

* draft: try preload/fix suspense

* fix: lint
2026-05-20 17:07:35 +00:00
Prospector 079a10bba9 changelog 2026-05-20 09:01:26 -07:00
Prospector 3c3d5702ba fix: permissions page being displayed 2026-05-20 08:59:50 -07:00
Prospector a34576a2c3 changelog 2026-05-19 12:51:59 -07:00
ProspectorandGitHub d8e4915a31 fix: 404 error when changing project slug (#6139) 2026-05-19 19:21:23 +00:00
Prospector ed723fa186 changelog 2026-05-19 11:50:49 -07:00
ProspectorandGitHub d6c8d4475b feat: new north american region info (#6091) 2026-05-19 11:49:41 -07:00
ProspectorandGitHub 302b60d89c fix: random 404s (#6138)
* fix: random 404s when navigating from user, org, or collection page to project

* more

* revert server

* clean names

* prepr
2026-05-19 18:11:48 +00:00
aecsocketandGitHub f106dc580f Restrict what tokens can be used for auth init flows (#6137) 2026-05-19 15:45:58 +00:00
223 changed files with 6794 additions and 1721 deletions
@@ -9,8 +9,6 @@ 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:
Generated
+3
View File
@@ -10565,6 +10565,8 @@ version = "1.0.0-local"
dependencies = [
"async_zip",
"chrono",
"core-foundation 0.10.1",
"core-graphics",
"daedalus",
"dashmap",
"either",
@@ -10572,6 +10574,7 @@ dependencies = [
"hyper 1.7.0",
"hyper-util",
"native-dialog",
"objc2-app-kit",
"paste",
"path-util",
"serde",
+3
View File
@@ -64,6 +64,8 @@ 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"
@@ -123,6 +125,7 @@ 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
View File
@@ -13,7 +13,6 @@
"test": "vue-tsc --noEmit"
},
"dependencies": {
"@intercom/messenger-js-sdk": "^0.0.14",
"@modrinth/api-client": "workspace:^",
"@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*",
+41 -93
View File
@@ -1,5 +1,4 @@
<script setup>
import { Intercom, shutdown as shutdownIntercom } from '@intercom/messenger-js-sdk'
import {
AuthFeature,
ModrinthApiError,
@@ -53,6 +52,7 @@ import {
providePopupNotificationManager,
useDebugLogger,
useFormatBytes,
useHostingIntercom,
useVIntl,
} from '@modrinth/ui'
import { renderString } from '@modrinth/utils'
@@ -88,7 +88,6 @@ 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'
@@ -132,15 +131,36 @@ import { AppPopupNotificationManager } from './providers/app-popup-notifications
const themeStore = useTheming()
const router = useRouter()
const route = useRoute()
const intercomBubblePositioning = useIntercomPositioning({ route, themeStore })
const {
sidebarToggled,
forceSidebar,
sidebarVisible,
intercomBubblePosition,
updateIntercomBubbleStyles,
clearIntercomBubbleStyles,
} = intercomBubblePositioning
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 notificationManager = new AppNotificationManager()
provideNotificationManager(notificationManager)
@@ -175,7 +195,11 @@ provideModrinthClient(tauriApiClient)
providePageContext({
hierarchicalSidebarAvailable: ref(true),
showAds: ref(false),
...intercomBubblePositioning.pageContext,
floatingActionBarOffsets: {
left: ref(APP_LEFT_NAV_WIDTH),
right: computed(() => (sidebarVisible.value ? `${APP_SIDEBAR_WIDTH}px` : '0px')),
},
intercomBubble: hostingIntercom.intercomBubble,
featureFlags: {
serverRamAsBytesAlwaysOn: computed(() =>
themeStore.getFeatureFlag('server_ram_as_bytes_always_on'),
@@ -259,8 +283,7 @@ onMounted(async () => {
onUnmounted(async () => {
document.querySelector('body').removeEventListener('click', handleClick)
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
shutdownHostingIntercom()
clearIntercomBubbleStyles()
unsubscribeSidebarToggle()
await unlistenUpdateDownload?.()
})
@@ -593,8 +616,6 @@ const incompatibilityWarningModal = ref()
const installToPlayModal = ref()
const updateToPlayModal = ref()
const credentials = ref()
const modrinthLoginFlowWaitModal = ref()
setupAuthProvider(credentials, async (_redirectPath) => {
@@ -664,10 +685,6 @@ 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()
@@ -676,8 +693,10 @@ async function fetchIntercomToken() {
}
const params = new URLSearchParams()
if (route.path.startsWith('/hosting/manage/') && typeof route.params.id === 'string') {
params.set('server_id', route.params.id)
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)
}
const query = params.size > 0 ? `?${params.toString()}` : ''
@@ -693,69 +712,6 @@ 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)
@@ -1822,14 +1778,6 @@ 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;
@@ -1,35 +1,33 @@
<template>
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
<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>
<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>
</ButtonStyled>
<ButtonStyled v-else>
<button @click="setJavaInstall(javaInstall)"><PlusIcon /> Select</button>
<button class="!shadow-none" @click="setJavaInstall(row)"><PlusIcon /> Select</button>
</ButtonStyled>
</div>
</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">
</template>
<template #empty-state>
<div class="p-4 text-secondary">No java installations found!</div>
</template>
</Table>
<div class="flex justify-end">
<ButtonStyled type="outlined">
<button @click="$refs.detectJavaModal.hide()">
<button
class="!shadow-none !border-surface-4 !border"
@click="$refs.detectJavaModal.hide()"
>
<XIcon />
Cancel
</button>
@@ -40,7 +38,7 @@
</template>
<script setup>
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
import { ButtonStyled, injectNotificationManager, Table } from '@modrinth/ui'
import { ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
@@ -52,6 +50,11 @@ 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) => {
@@ -77,25 +80,3 @@ 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,85 +1,104 @@
<template>
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
<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,
})
}
"
/>
<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>
<span class="installation-buttons">
<ButtonStyled v-if="props.version">
<button :disabled="props.disabled || installingJava" @click="reinstallJava">
<button
v-tooltip="testingJavaSuccess === true ? 'Already installed' : undefined"
class="!shadow-none"
:disabled="props.disabled || installingJava || testingJavaSuccess === true"
@click="reinstallJava"
>
<DownloadIcon />
{{ installingJava ? 'Installing...' : 'Install recommended' }}
</button>
</ButtonStyled>
<ButtonStyled>
<button :disabled="props.disabled" @click="autoDetect">
<button class="!shadow-none" :disabled="props.disabled" @click="autoDetect">
<SearchIcon />
Detect
</button>
</ButtonStyled>
<ButtonStyled>
<button :disabled="props.disabled" @click="handleJavaFileInput()">
<button class="!shadow-none" :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 {
CheckIcon,
CheckCircleIcon,
DownloadIcon,
FolderSearchIcon,
PlayIcon,
RefreshCwIcon,
SearchIcon,
XIcon,
SpinnerIcon,
XCircleIcon,
} from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, StyledInput } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref } from 'vue'
import { ref, watch } 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, test_jre } from '@/helpers/jre.js'
import { auto_install_java, find_filtered_jres, get_jre } from '@/helpers/jre.js'
const { handleError } = injectNotificationManager()
const props = defineProps({
id: {
type: String,
required: false,
default: null,
},
version: {
type: Number,
required: false,
@@ -110,29 +129,36 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const testingJava = ref(false)
const testingJavaSuccess = ref(null)
const {
testingJava,
javaTestResult: testingJavaSuccess,
testJavaInstallationDebounced,
testJavaInstallation,
} = useJavaTest()
const installingJava = ref(false)
const hoveringTest = ref(false)
let hasInitialized = false
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)
async function runTest(path) {
await testJavaInstallation(path, props.version, true)
}
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()
@@ -142,6 +168,7 @@ async function handleJavaFileInput() {
result = {
path: filePath.path ?? filePath,
version: props.version.toString(),
parsed_version: props.version,
architecture: 'x86',
}
}
@@ -175,6 +202,7 @@ async function reinstallJava() {
result = {
path: path,
version: props.version.toString(),
parsed_version: props.version,
architecture: 'x86',
}
}
@@ -186,13 +214,23 @@ 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 {
width: 100% !important;
flex-grow: 1;
flex: 1 1 0;
min-width: 0;
}
.toggle-setting {
@@ -215,12 +253,4 @@ 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 } = injectInstanceSettings()
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
const [
fabric_versions,
@@ -113,6 +113,7 @@ provideAppBackup({
})
provideInstallationSettings({
closeSettings: closeModal,
loading: ref(false),
installationInfo: computed(() => {
const rows = [
@@ -1,6 +1,15 @@
<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,
@@ -8,9 +17,11 @@ import {
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { computed, readonly, ref, watch } from 'vue'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import useJavaTest from '@/composables/useJavaTest'
import useMemorySlider from '@/composables/useMemorySlider'
import { edit, get_optimal_jre_key } from '@/helpers/profile'
import { get } from '@/helpers/settings.ts'
@@ -25,9 +36,54 @@ const { instance } = injectInstanceSettings()
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
const overrideJavaInstall = ref(!!instance.value.java_path)
const optimalJava = readonly(await get_optimal_jre_key(instance.value.path).catch(handleError))
const javaInstall = ref({ path: optimalJava.path ?? instance.value.java_path })
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 overrideJavaArgs = ref((instance.value.extra_launch_args?.length ?? 0) > 0)
const javaArgs = ref(
@@ -51,8 +107,8 @@ const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) a
const editProfileObject = computed(() => {
return {
java_path:
overrideJavaInstall.value && javaInstall.value.path !== ''
? javaInstall.value.path.replace('java.exe', 'javaw.exe')
overrideJavaInstall.value && javaPath.value
? javaPath.value.replace('java.exe', 'javaw.exe')
: null,
extra_launch_args: overrideJavaArgs.value
? javaArgs.value.trim().split(/\s+/).filter(Boolean)
@@ -71,7 +127,7 @@ const editProfileObject = computed(() => {
watch(
[
overrideJavaInstall,
javaInstall,
javaPath,
overrideJavaArgs,
javaArgs,
overrideEnvVars,
@@ -90,17 +146,45 @@ 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',
},
javaMemory: {
id: 'instance.settings.tabs.java.java-memory',
defaultMessage: 'Memory allocated',
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...',
},
hooks: {
id: 'instance.settings.tabs.java.hooks',
@@ -111,43 +195,86 @@ const messages = defineMessages({
<template>
<div>
<h2 id="project-name" class="m-0 mb-2.5 text-lg font-semibold text-contrast block">
<JavaDetectionModal ref="javaDetectionModal" @submit="(val) => (javaPath = val.path)" />
<h2 class="m-0 mb-2 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaInstallation) }}
</h2>
<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
<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
>
</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 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>
</div>
<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">
</div>
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaMemory) }}
</h2>
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2.5" />
<Checkbox
v-model="overrideMemorySettings"
:label="formatMessage(messages.customMemoryAllocation)"
class="mb-2"
/>
<Slider
id="max-memory"
v-model="memory.maximum"
@@ -159,28 +286,36 @@ const messages = defineMessages({
:snap-range="512"
unit="MB"
/>
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaArguments) }}
</h2>
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
<Checkbox
v-model="overrideJavaArgs"
:label="formatMessage(messages.customJavaArguments)"
class="my-2"
/>
<StyledInput
id="java-args"
v-model="javaArgs"
autocomplete="off"
:disabled="!overrideJavaArgs"
placeholder="Enter java arguments..."
:placeholder="formatMessage(messages.enterJavaArguments)"
wrapper-class="w-full"
/>
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaEnvironmentVariables) }}
</h2>
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2.5" />
<Checkbox
v-model="overrideEnvVars"
:label="formatMessage(messages.customEnvironmentVariables)"
class="mb-2"
/>
<StyledInput
id="env-vars"
v-model="envVars"
autocomplete="off"
:disabled="!overrideEnvVars"
placeholder="Enter environmental variables..."
:placeholder="formatMessage(messages.enterEnvironmentVariables)"
wrapper-class="w-full"
/>
</div>
@@ -45,12 +45,19 @@ 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(
@@ -113,9 +120,6 @@ 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({
@@ -129,7 +133,7 @@ function show(tabIndex?: number) {
}
}
defineExpose({ show })
defineExpose({ show, hide })
</script>
<template>
<TabbedModal
@@ -1,121 +0,0 @@
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
stdout?: string
output?: string | null
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, false)
const logs: LogEntry[] = await get_logs(instancePath, true)
entry.logList = logs
for (const log of logs) {
if (log.stdout && log.stdout !== '') {
entry.historicalCache.set(log.filename, log.stdout)
if (log.output) {
entry.historicalCache.set(log.filename, log.output)
}
}
@@ -0,0 +1,52 @@
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,
}
}
@@ -0,0 +1,100 @@
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
})
}
+2 -3
View File
@@ -6,12 +6,11 @@
import { invoke } from '@tauri-apps/api/core'
/*
A log is a struct containing the filename string, stdout, and stderr, as follows:
A log is a struct containing the filename string and optional output, as follows:
pub struct Logs {
pub filename: String,
pub stdout: String,
pub stderr: String,
pub output: Option<String>,
}
*/
+8 -1
View File
@@ -147,9 +147,16 @@ 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<string | null> {
export async function get_optimal_jre_key(path: string): Promise<JavaVersion | null> {
return await invoke('plugin:profile|profile_get_optimal_jre_key', { path })
}
@@ -302,6 +302,9 @@
"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"
},
@@ -104,6 +104,12 @@
"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..."
},
@@ -611,6 +611,24 @@
"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"
},
@@ -626,6 +644,9 @@
"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"
},
+52 -7
View File
@@ -21,7 +21,7 @@
"message": "Instancia principal"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar mas instancias en ejecución"
"message": "Mostrar más 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 launcher se abre en."
"message": "Cambia la página en la que el lanzador inicia."
},
"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 launcher cuando un proceso de Minecraft empieza."
"message": "Minimiza el lanzador cuando un proceso de Minecraft empieza."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimiza el launcher"
"message": "Minimiza el lanzador"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usa los sistemas de frame de Windows (se requiere resetear la aplicación)."
"message": "Usa los sistemas de marco 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, te advertiremos de los riesgos antes de instalarlo."
"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."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Adviérteme antes de instalarme modpacks desconocidos"
"message": "Advertir antes de instalarme paquetes de mods 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,6 +104,12 @@
"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"
},
@@ -116,6 +122,9 @@
"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"
},
@@ -128,6 +137,9 @@
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"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."
},
"app.browse.server.installing": {
"message": "Instalando"
},
@@ -146,12 +158,18 @@
"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"
},
@@ -281,6 +299,15 @@
"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."
},
@@ -671,6 +698,24 @@
"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,10 +1,115 @@
{
"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"
},
@@ -17,12 +122,33 @@
"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..."
},
@@ -32,12 +158,18 @@
"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"
},
@@ -80,6 +212,9 @@
"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"
},
@@ -164,6 +299,15 @@
"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ä."
},
@@ -467,6 +611,24 @@
"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"
},
@@ -482,6 +644,9 @@
"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"
},
@@ -554,6 +719,24 @@
"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"
},
@@ -577,5 +760,26 @@
},
"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 parin"
"message": "I-install pa rin"
},
"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."
@@ -71,6 +71,9 @@
"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."
},
@@ -269,6 +272,9 @@
"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."
},
@@ -659,6 +665,21 @@
"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"
},
@@ -682,5 +703,20 @@
},
"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"
}
}
+22 -1
View File
@@ -84,7 +84,7 @@
"message": "Decorazioni native"
},
"app.appearance-settings.select-option": {
"message": "Scegli una pagina"
"message": "Seleziona"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Scegli se mostrare o nascondere la barra laterale."
@@ -611,6 +611,24 @@
"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"
},
@@ -626,6 +644,9 @@
"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"
},
+24 -3
View File
@@ -24,7 +24,7 @@
"message": "실행 중인 인스턴스 보이기"
},
"app.action-bar.stop-instance": {
"message": "인스턴스 중지"
"message": "인스턴스 멈추기"
},
"app.action-bar.view-active-downloads": {
"message": "다운로드 목록 보기"
@@ -192,10 +192,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": "인스턴스"
@@ -611,6 +611,24 @@
"instance.settings.tabs.java": {
"message": "Java 및 메모리"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "사용자 지정 환경 변수"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "사용자 지정 Java 매개변수"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "사용자 지정 Java 설치"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "사용자 지정 메모리 할당"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "환경 변수를 입력하세요..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Java 인수를 입력하세요..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "환경 변수"
},
@@ -626,6 +644,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "할당된 메모리"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "창"
},
@@ -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"
@@ -611,6 +611,24 @@
"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"
},
@@ -626,6 +644,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memória alocada"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/caminho/para/java"
},
"instance.settings.tabs.window": {
"message": "Janela"
},
+23 -2
View File
@@ -360,7 +360,7 @@
"message": "Доступно обновление"
},
"app.update.complete-toast.text": {
"message": "Нажмите здесь, чтобы посмотреть изменения."
"message": "Нажмите, чтобы посмотреть список изменений."
},
"app.update.complete-toast.title": {
"message": "Версия {version} успешно установлена!"
@@ -611,6 +611,24 @@
"instance.settings.tabs.java": {
"message": "Java и память"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Пользовательские настройки запуска"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Пользовательские аргументы Java"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Пользовательская установка Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Пользовательский объём памяти"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Введите дополнительные параметры системы..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Введите аргументы Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Переменные среды"
},
@@ -626,6 +644,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Выделенная память"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "Окно"
},
@@ -741,7 +762,7 @@
"message": "Загрузчик управляется сервером"
},
"unknown-pack-warning-modal.body": {
"message": "Файлы любого формата проверены на безопасность, если они загружены на Modrinth (в том числе файлы .mrpack)."
"message": "Файл проверяется только в том случае, если он загружен на Modrinth, независимо от его формата (включая .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Больше не предупреждать"
+23 -2
View File
@@ -168,7 +168,7 @@
"message": "Назва збірки"
},
"app.export-modal.select-files-label": {
"message": "Змінити які файли додані до експорту"
"message": "Змініть файли, які додані до експорту"
},
"app.export-modal.version-number-label": {
"message": "Номер версії"
@@ -222,7 +222,7 @@
"message": "Додати сервер"
},
"app.instance.worlds.browse-servers": {
"message": "Переглянути сервера"
"message": "Переглянути сервери"
},
"app.instance.worlds.delete-world-description": {
"message": "«{name}» буде **назавжди видалено** й у вас не буде можливости відновлення."
@@ -611,6 +611,24 @@
"instance.settings.tabs.java": {
"message": "Java та пам’ять"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Власні змінні середовища"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Власні аргументи Java"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Власна інсталяція Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Власний розподіл пам'яті"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Уведіть змінні середовища…"
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Уведіть аргументи Java…"
},
"instance.settings.tabs.java.environment-variables": {
"message": "Змінні середовища"
},
@@ -626,6 +644,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Виділена пам’ять"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/шлях/до/java"
},
"instance.settings.tabs.window": {
"message": "Вікно"
},
@@ -1,10 +1,115 @@
{
"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ơ"
},
@@ -17,12 +122,33 @@
"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..."
},
@@ -32,12 +158,18 @@
"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"
},
@@ -167,6 +299,15 @@
"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."
},
@@ -470,6 +611,24 @@
"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"
},
@@ -485,6 +644,9 @@
"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ổ"
},
@@ -557,6 +719,24 @@
"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"
},
@@ -580,5 +760,26 @@
},
"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"
}
}
@@ -63,7 +63,7 @@
"message": "在皮肤页面中禁用你玩家头顶上的名牌。"
},
"app.appearance-settings.hide-nametag.title": {
"message": "隐藏名"
"message": "隐藏名称标签"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "在主页的“快速回到”部分包含最近的世界。"
+28 -4
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": "匯出"
@@ -252,13 +252,13 @@
"message": "「{name}」將從你的清單中移除(包含遊戲內),且無法還原。"
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "「{name}」({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": "這個伺服器"
@@ -302,6 +302,9 @@
"app.project.install-button.already-installed": {
"message": "這個專案已安裝"
},
"app.project.install-context.back-to-browse": {
"message": "返回瀏覽"
},
"app.project.install-context.install-content-to-instance": {
"message": "將內容安裝至實例"
},
@@ -608,6 +611,24 @@
"instance.settings.tabs.java": {
"message": "Java 和記憶體"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "自訂環境變數"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "自訂 Java 參數"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "自訂 Java 安裝"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "自訂記憶體分配"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "輸入環境變數……"
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "輸入 Java 參數……"
},
"instance.settings.tabs.java.environment-variables": {
"message": "環境變數"
},
@@ -623,6 +644,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "記憶體配置"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "視窗"
},
+44 -10
View File
@@ -218,7 +218,11 @@
:key="instance.path"
>
<template v-if="Component">
<Suspense :key="instance.path">
<Suspense
:key="instance.path"
@pending="subpagePending = true"
@resolve="subpagePending = false"
>
<component
:is="Component"
:instance="instance"
@@ -228,6 +232,7 @@
:installed="instance.install_stage !== 'installed'"
:is-server-instance="isServerInstance"
:open-settings="() => settingsModal?.show(1)"
v-bind="contentSubpageProps"
@play="updatePlayState"
@stop="() => stopInstance('InstanceSubpage')"
></component>
@@ -295,6 +300,7 @@ import {
ServerPing,
ServerRecentPlays,
ServerRegion,
useLoadingBarToken,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
@@ -312,6 +318,7 @@ 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'
@@ -331,6 +338,7 @@ const route = useRoute()
const router = useRouter()
const breadcrumbs = useBreadcrumbs()
const contentSubpageRouteNames = new Set(['Mods', 'ModsFilter'])
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
@@ -341,12 +349,16 @@ 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[]>([])
@@ -361,36 +373,55 @@ 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
instance.value = await get(route.params.id as string).catch(handleError)
const nextInstance = await get(route.params.id as string).catch(handleError)
let nextLinkedProjectV3: Labrinth.Projects.v3.Project | undefined
let nextIsServerInstance = false
if (!offline.value && instance.value?.linked_data && instance.value.linked_data.project_id) {
const contentPreloadPromise =
nextInstance && isContentSubpageRoute()
? loadInstanceContentData(nextInstance.path, undefined, handleError)
: Promise.resolve(null)
if (!offline.value && nextInstance?.linked_data && nextInstance.linked_data.project_id) {
try {
linkedProjectV3.value = await get_project_v3(
instance.value.linked_data.project_id,
nextLinkedProjectV3 = await get_project_v3(
nextInstance.linked_data.project_id,
'must_revalidate',
)
if (linkedProjectV3.value?.minecraft_server != null) {
isServerInstance.value = true
if (nextLinkedProjectV3?.minecraft_server != null) {
nextIsServerInstance = 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 (instance.value) {
if (nextInstance) {
queryClient.prefetchQuery({
queryKey: ['worlds', instance.value.path],
queryFn: () => refreshWorlds(instance.value!.path),
queryKey: ['worlds', nextInstance.path],
queryFn: () => refreshWorlds(nextInstance.path),
staleTime: 30_000,
})
}
@@ -448,6 +479,9 @@ 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.stdout !== '' &&
(log.output == null || log.output !== '') &&
(log.filename.includes('.log') || log.filename.endsWith('.txt')),
)
.map((log) => ({
+400 -109
View File
@@ -13,6 +13,7 @@
: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"
@@ -87,12 +88,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 { useDebounceFn } from '@vueuse/core'
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import ExportModal from '@/components/ui/ExportModal.vue'
@@ -100,22 +101,20 @@ 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'
@@ -160,6 +159,7 @@ const props = defineProps<{
instance: GameInstance
isServerInstance?: boolean
openSettings?: () => void
preloadedContent?: InstanceContentData | null
}>()
const loading = ref(true)
@@ -212,6 +212,7 @@ 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)
@@ -219,6 +220,82 @@ 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
@@ -265,9 +342,21 @@ async function handleUploadFiles() {
}
async function toggleDisableMod(mod: ContentItem) {
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
try {
mod.file_path = await toggle_disable_project(props.instance.path, mod.file_path!)
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.enabled = !mod.enabled
modpackContentModal.value?.updateItem(operation.originalFileName, {
file_path: newPath,
file_name: newFileName,
enabled: mod.enabled,
})
trackEvent('InstanceProjectDisable', {
loader: props.instance.loader,
@@ -279,33 +368,48 @@ async function toggleDisableMod(mod: ContentItem) {
})
} catch (err) {
handleError(err as Error)
} finally {
finishContentOperation(mod, operation)
}
}
const toggleDisableDebounced = useDebounceFn(toggleDisableMod, 20)
const toggleDisableDebounced = toggleDisableMod
async function removeMod(mod: ContentItem) {
await remove_project(props.instance.path, mod.file_path!).catch(handleError)
projects.value = projects.value.filter((x) => mod.file_path !== x.file_path)
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
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,
})
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)
}
}
async function updateProject(mod: ContentItem) {
try {
const newPath = await update_project(props.instance.path, mod.file_path!)
mod.file_path = newPath
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
if (mod.update_version_id) {
const versionData = await get_version(mod.update_version_id, 'must_revalidate').catch(
handleError,
)
try {
const updateVersionId = mod.update_version_id
await update_project(props.instance.path, mod.file_path)
if (updateVersionId) {
const versionData = await get_version(updateVersionId, 'must_revalidate').catch(handleError)
if (versionData) {
const profile = await get(props.instance.path).catch(handleError)
@@ -316,12 +420,6 @@ 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,
@@ -331,39 +429,65 @@ 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) {
isBulkOperating.value = true
mod.installing = true
if (mod.version) {
mod.version.id = version.id
mod.version.version_number = version.version_number
}
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
try {
await remove_project(props.instance.path, mod.file_path!)
const newPath = await add_project_from_version(props.instance.path, version.id, 'standalone')
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)
}
const profile = await get(props.instance.path).catch(handleError)
if (profile) {
await installVersionDependencies(profile, version, 'update').catch(handleError)
}
mod.file_path = newPath
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,
})
} catch (err) {
if (shouldRemoveNewOnError && newPath && newPath !== oldPath) {
await remove_project(props.instance.path, newPath).catch(() => {})
}
handleError(err as Error)
} finally {
mod.installing = false
isBulkOperating.value = false
await initProjects()
await refreshContentState('must_revalidate')
finishContentOperation(mod, operation)
}
}
async function handleUpdate(id: string) {
const item = projects.value.find((p) => p.id === id)
const item = projects.value.find((p) => getContentItemId(p) === 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,
@@ -384,12 +508,48 @@ async function handleUpdate(id: string) {
await nextTick()
contentUpdaterModal.value?.show(item.update_version_id ?? undefined)
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)
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) {
@@ -414,6 +574,25 @@ 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
}
@@ -421,6 +600,9 @@ 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 = []
@@ -435,6 +617,9 @@ 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
@@ -468,9 +653,28 @@ 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 = []
@@ -479,14 +683,42 @@ async function handleModpackUpdate() {
await nextTick()
contentUpdaterModal.value?.show(
linkedModpackUpdateVersionId.value ?? props.instance?.linked_data?.version_id ?? undefined,
)
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)
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
@@ -494,6 +726,25 @@ 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
}
@@ -502,10 +753,12 @@ 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) {
@@ -517,17 +770,26 @@ 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)
loadingChangelog.value = false
await fetchAndSpliceVersion(
version.id,
'must_revalidate',
handleError as (err: unknown) => void,
requestId,
)
if (isActiveUpdateRequest(requestId)) {
loadingChangelog.value = false
}
}
async function handleVersionHover(version: Labrinth.Versions.v2.Version) {
if (version.changelog != null) return
await fetchAndSpliceVersion(version.id)
await fetchAndSpliceVersion(version.id, undefined, undefined, activeUpdateRequestId.value)
}
function resetUpdateState() {
activeUpdateRequestId.value = 0
updatingModpack.value = false
updatingProject.value = null
updatingProjectVersions.value = []
@@ -535,13 +797,23 @@ function resetUpdateState() {
loadingChangelog.value = false
}
function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.Version) {
async 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()
}
@@ -551,6 +823,7 @@ 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)
@@ -574,7 +847,7 @@ async function handleModalUpdate(
pendingModpackUpdateVersion.value = selectedVersion
await handleModpackUpdateConfirm()
} else {
handleModpackUpdateRequest(selectedVersion)
await handleModpackUpdateRequest(selectedVersion)
}
} else if (updatingProject.value) {
const mod = updatingProject.value
@@ -662,51 +935,31 @@ function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
async function initProjects(cacheBehaviour?: CacheBehaviour) {
if (!props.instance) return
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),
])
const contentData = await loadInstanceContentData(
props.instance.path,
cacheBehaviour,
handleError,
)
applyContentData(contentData)
}
if (!contentItems) {
function applyContentData(contentData: InstanceContentData) {
if (contentData.path !== props.instance.path) return false
if (!contentData.contentItems) {
loading.value = false
return
return true
}
projects.value = contentItems
projects.value = contentData.contentItems
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 = []
}
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
} else {
linkedModpackProject.value = null
linkedModpackVersion.value = null
@@ -717,6 +970,7 @@ async function initProjects(cacheBehaviour?: CacheBehaviour) {
}
loading.value = false
return true
}
provideAppBackup({
@@ -809,8 +1063,9 @@ provideContentManager({
showContentHint,
dismissContentHint,
shareItems: handleShareItems,
getItemId: getContentItemId,
mapToTableItem: (item: ContentItem) => ({
id: item.id,
id: getContentItemId(item),
project: item.project ?? {
id: item.file_name,
slug: null,
@@ -844,10 +1099,22 @@ provideContentManager({
filterPersistKey: props.instance.path,
})
await initProjects()
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
// Restore modpack content modal state if returning via back navigation
if (savedModalState) {
const stateToRestore = savedModalState
savedModalState = null
await nextTick()
@@ -860,18 +1127,32 @@ const removeBeforeEach = router.beforeEach(() => {
savedModalState = state ?? null
})
const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
if (event.payload.type !== 'drop' || !props.instance) return
let isUnmounted = false
let unlistenDragDrop: UnlistenFn | null = null
let unlistenProfiles: UnlistenFn | null = null
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()
})
onMounted(() => {
void getCurrentWebview()
.onDragDropEvent(async (event) => {
if (event.payload.type !== 'drop' || !props.instance) return
const unlistenProfiles = await profile_listener(
async (event: { event: string; profile_path_id: string }) => {
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 }) => {
if (
props.instance &&
event.profile_path_id === props.instance.path &&
@@ -881,8 +1162,17 @@ const unlistenProfiles = await profile_listener(
) {
await initProjects()
}
},
)
})
.then((unlisten) => {
if (isUnmounted) {
unlisten()
return
}
unlistenProfiles = unlisten
})
.catch(handleError)
})
watch(
() => props.instance?.install_stage,
@@ -905,8 +1195,9 @@ watch(
)
onUnmounted(() => {
isUnmounted = true
removeBeforeEach()
unlisten()
unlistenProfiles()
unlistenDragDrop?.()
unlistenProfiles?.()
})
</script>
@@ -8,6 +8,7 @@ export interface InstanceSettingsContext {
offline?: boolean
isMinecraftServer: Ref<boolean>
onUnlinked: () => void
closeModal?: () => void
}
export const [injectInstanceSettings, provideInstanceSettings] =
@@ -17,6 +17,7 @@ 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'
@@ -208,6 +209,7 @@ 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))
@@ -471,6 +473,9 @@ 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 {
@@ -491,6 +496,7 @@ 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)
+1
View File
@@ -19,6 +19,7 @@
"types": ["vite/client"],
"paths": {
"@modrinth/api-client": ["../../packages/api-client/src/index.ts"],
"@/*": ["./src/*"]
}
},
+5
View File
@@ -8,6 +8,7 @@ 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')
@@ -37,6 +38,10 @@ export default defineConfig({
},
resolve: {
alias: [
{
find: '@modrinth/api-client',
replacement: apiClientSource,
},
{
find: '@',
replacement: resolve(projectRootDir, 'src'),
+7
View File
@@ -51,6 +51,13 @@ 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 = [
+50 -18
View File
@@ -17,6 +17,8 @@ pub struct AdsState {
}
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 ",
@@ -92,10 +94,12 @@ fn configure_ads_cookie_settings(
}
}
fn set_webview_visible<R: Runtime>(
webview: &tauri::Webview<R>,
_visible: bool,
) {
fn set_webview_visible<R: Runtime>(webview: &tauri::Webview<R>, visible: bool) {
#[cfg(not(any(windows, target_os = "macos")))]
{
_ = visible;
}
webview
.with_webview(
#[allow(unused_variables)]
@@ -103,11 +107,18 @@ fn set_webview_visible<R: Runtime>(
#[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>(
@@ -129,27 +140,48 @@ fn set_webview_visible_for_window<R: Runtime>(
set_webview_visible(webview, visible && !is_minimized && !is_occluded);
}
#[cfg(windows)]
#[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")?;
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(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(windows)]
#[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;
@@ -281,7 +313,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
});
}
#[cfg(windows)]
#[cfg(any(windows, target_os = "macos"))]
{
let app_handle = app.clone();
+236
View File
@@ -0,0 +1,236 @@
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)
}
}
+1 -3
View File
@@ -8,8 +8,6 @@ use windows::Win32::UI::WindowsAndMessaging::{
GetWindowThreadProcessId, IsIconic, IsWindowVisible,
};
const OCCLUDED_AREA_THRESHOLD: f64 = 1.0;
pub fn is_ads_webview_occluded(
main_hwnd: HWND,
x: i32,
@@ -63,7 +61,7 @@ pub fn is_ads_webview_occluded(
occluded_area.saturating_add(rect_area(&intersection));
if (occluded_area as f64 / ad_area as f64)
>= OCCLUDED_AREA_THRESHOLD
>= super::ads::OCCLUDED_AREA_THRESHOLD
{
return true;
}
+2
View File
@@ -18,6 +18,8 @@ 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;
+12 -1
View File
@@ -1,10 +1,15 @@
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',
@@ -24,6 +29,9 @@ const STAGING_MODRINTH_URL = 'https://staging.modrinth.com'
export default defineNuxtConfig({
srcDir: 'src/',
alias: {
'@modrinth/api-client': API_CLIENT_SOURCE,
},
app: {
head: {
htmlAttrs: {
@@ -82,6 +90,9 @@ export default defineNuxtConfig({
},
cacheDir: '../../node_modules/.vite/apps/knossos',
resolve: {
alias: {
'@modrinth/api-client': API_CLIENT_SOURCE,
},
dedupe: ['vue'],
},
plugins: [
-1
View File
@@ -38,7 +38,6 @@
},
"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,7 +5,6 @@ import {
commonMessages,
defineMessages,
IntlFormatted,
normalizeChildren,
PagewideBanner,
useVIntl,
} from '@modrinth/ui'
@@ -22,7 +21,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 <branch-link>{owner}/{branch}</branch-link> @ {commit}.`,
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}.`,
},
})
@@ -41,30 +40,22 @@ const url = computed(() => `https://modrinth.com${route.fullPath}`)
</template>
<template #description>
<span>
<IntlFormatted
:message-id="messages.description"
:values="{
owner: config.public.owner,
branch: config.public.branch,
}"
>
<IntlFormatted :message-id="messages.description">
<template #url>
<a :href="url" target="_blank" rel="noopener" class="text-link">
{{ url }}
</a>
</template>
<template #branch-link="{ children }">
<template #ref>
<a
:href="`https://github.com/${config.public.owner}/code/tree/${config.public.branch}`"
target="_blank"
rel="noopener"
class="hover:underline"
>
<component :is="() => normalizeChildren(children)" />
{{ config.public.owner }} / {{ config.public.branch }}
</a>
</template>
<template #commit>
<span v-if="config.public.hash === 'unknown'">unknown</span>
@ <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 changlog </span>
<span class="font-semibold text-contrast"> Version changelog </span>
<div class="w-full">
<MarkdownEditor
@@ -131,10 +131,9 @@
class="h-10"
/>
<StyledInput
<DatePicker
v-else-if="field.type === 'date'"
v-model="formData[field.name]"
type="date"
wrapper-class="w-full"
/>
@@ -230,6 +229,7 @@ import {
Admonition,
Checkbox,
Combobox,
DatePicker,
defineMessages,
financialMessages,
formFieldLabels,
@@ -95,11 +95,11 @@
<span class="text-red">*</span>
</span>
</label>
<StyledInput
<DatePicker
v-model="formData.dateOfBirth"
type="date"
:max="maxDate"
:max-date="maxDate"
autocomplete="bday"
placeholder="Select date of birth"
wrapper-class="w-full"
/>
</div>
@@ -213,6 +213,7 @@
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-id-moderation',
shouldShow: () => props.routeName !== 'type-project-moderation',
},
})
}
@@ -48,6 +48,16 @@
<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
@@ -119,7 +129,7 @@
</template>
<script setup lang="ts">
import { ClipboardCopyIcon, LinkIcon, ScaleIcon } from '@modrinth/assets'
import { ClipboardCopyIcon, FileIcon, LinkIcon, ScaleIcon } from '@modrinth/assets'
import {
Avatar,
Badge,
@@ -151,6 +161,7 @@ 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 recheck = await moderationQueue.checkLock(next.projectId)
if (recheck.locked && !recheck.expired) {
// Project got locked, remove from queue and try next
const recheckResults = await batchCheckQueueCandidates([next.projectId])
const recheck = recheckResults.get(next.projectId)
if (!isEligibleQueueCandidate(recheck)) {
prefetchQueue.value.shift()
return navigateToNextUnlockedProject() // Recurse to try next
return navigateToNextUnlockedProject()
}
}
@@ -717,33 +717,14 @@ async function navigateToNextUnlockedProject(): Promise<boolean> {
next.skippedIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
)
if (next.skippedIds.length > 0) {
addNotification({
title: 'Skipped locked projects',
text: `Skipped ${next.skippedIds.length} project(s) being moderated by others.`,
type: 'info',
})
}
notifySkippedQueueProjects(next.skippedIds.length)
// Trigger prefetch replenishment in background (don't await)
maintainPrefetchQueue()
// 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 },
})
}
navigateToQueueProject(
{ slug: next.slug, projectType: next.projectType, locked: false, isProcessing: true },
next.projectId,
)
return true
}
@@ -869,53 +850,108 @@ function reviewAnyway() {
maintainPrefetchQueue()
}
// Batch check locks and fetch project metadata in parallel
interface LockCheckResult {
// Batch check locks, processing status, and fetch project metadata in parallel
interface QueueCandidateCheck {
locked: boolean
expired?: boolean
isOwnLock?: boolean
slug?: string
projectType?: string
status?: string
isProcessing: boolean
}
async function batchCheckLocksWithMetadata(
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(
projectIds: string[],
): Promise<Map<string, LockCheckResult>> {
const results = new Map<string, LockCheckResult>()
): Promise<Map<string, QueueCandidateCheck>> {
const results = new Map<string, QueueCandidateCheck>()
// 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 })?.slug,
projectType: (projectData as { project_type?: string })?.project_type,
slug: (projectData as { slug?: string } | null)?.slug,
projectType: (projectData as { project_type?: string } | null)?.project_type,
status,
isProcessing: projectData === null ? true : status === 'processing',
}
}),
)
// 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 {
// On error, mark as needing fallback (no metadata)
results.set(projectIds[index], { locked: false })
results.set(projectIds[index], { locked: false, isProcessing: true })
}
})
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
@@ -947,13 +983,10 @@ async function maintainPrefetchQueue() {
const remainingItems =
currentIndex >= 0 ? queueItems.slice(currentIndex + 1) : queueItems.slice(1)
const candidateIds = remainingItems
.filter((id) => !prefetchedIds.has(id))
.slice(0, PREFETCH_BATCH_SIZE * 2) // Check up to 10 candidates
const candidateIds = remainingItems.filter((id) => !prefetchedIds.has(id))
if (candidateIds.length === 0) return
// 5. Batch check locks AND fetch metadata in parallel
const skippedIds: string[] = []
let checkedCount = 0
@@ -964,31 +997,18 @@ async function maintainPrefetchQueue() {
const batch = candidateIds.slice(checkedCount, checkedCount + PREFETCH_BATCH_SIZE)
checkedCount += batch.length
const results = await batchCheckLocksWithMetadata(batch)
const results = await batchCheckQueueCandidates(batch)
for (const id of batch) {
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 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 (isEligibleQueueCandidate(result)) {
prefetchQueue.value.push({
projectId: id,
slug: result?.slug ?? '',
projectType: result?.projectType ?? '',
validatedAt: Date.now(),
skippedIds: [...skippedIds],
})
if (prefetchQueue.value.length >= PREFETCH_TARGET_COUNT) break
} else {
@@ -1004,8 +1024,6 @@ 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
@@ -1029,60 +1047,28 @@ async function skipToNextProject() {
debug('[skipToNextProject] No prefetch, entering fallback with batch checking')
// 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)
}
const remainingIds = moderationQueue.currentQueue.items.filter((id) => id !== currentProjectId)
if (remainingIds.length > 0) {
const results = await batchCheckLocksWithMetadata(remainingIds)
const next = await findNextEligibleQueueProject(remainingIds)
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++
if (next) {
await Promise.all(
next.skippedIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
)
notifySkippedQueueProjects(next.skippedIds.length)
navigateToQueueProject(next.result, next.projectId)
return
}
// All checked were locked
debug('[skipToNextProject] All projects were locked, skippedCount:', skippedCount)
await Promise.all(
remainingIds.map((id) => moderationQueue.completeCurrentProject(id, 'skipped')),
)
debug('[skipToNextProject] No eligible projects in queue')
addNotification({
title: 'All projects locked',
text: 'All remaining projects are currently being moderated by others.',
title: 'No projects available',
text: 'All remaining projects are already moderated or locked by others.',
type: 'warning',
})
}
@@ -1298,32 +1284,16 @@ 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,
@@ -2085,72 +2055,36 @@ 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 queueItems = moderationQueue.currentQueue.items
const remainingIds = moderationQueue.currentQueue.items.filter(
(id) => id !== currentProjectId,
)
// 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
let foundEligible = false
if (remainingIds.length > 0) {
const results = await batchCheckLocksWithMetadata(remainingIds)
const next = await findNextEligibleQueueProject(remainingIds)
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) {
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')),
)
addNotification({
title: 'All projects locked',
text: 'All remaining projects are currently being moderated by others.',
title: 'No projects available',
text: 'All remaining projects are already moderated or locked by others.',
type: 'warning',
})
}
}
// If no unlocked projects found, go back to moderation queue
if (!foundUnlocked) {
if (!foundEligible) {
await navigateTo({
name: 'moderation',
})
+38 -19
View File
@@ -1,3 +1,10 @@
function normalizeAuthToken(value) {
if (typeof value === 'string') {
return value
}
return ''
}
export const useAuth = async (oldToken = null) => {
const auth = useState('auth', () => ({
user: null,
@@ -32,27 +39,35 @@ export const initAuth = async (oldToken = null) => {
})
if (oldToken) {
authCookie.value = oldToken
const normalized = normalizeAuthToken(oldToken)
if (normalized) {
authCookie.value = normalized
}
}
if (route.query.code && !route.fullPath.includes('new_account=true')) {
authCookie.value = route.query.code
const oauthCode = normalizeAuthToken(route.query.code)
if (oauthCode && !route.fullPath.includes('new_account=true')) {
authCookie.value = oauthCode
}
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=${route.query.code}${
`/auth/welcome?authToken=${oauthCode}${
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''
}`,
)
}
if (authCookie.value) {
auth.token = authCookie.value
const tokenStr = normalizeAuthToken(authCookie.value)
if (!auth.token || !auth.token.startsWith('mra_')) {
if (authCookie.value != null && tokenStr === '') {
authCookie.value = null
} else if (tokenStr) {
auth.token = tokenStr
if (!auth.token.startsWith('mra_')) {
return auth
}
@@ -71,7 +86,7 @@ export const initAuth = async (oldToken = null) => {
}
}
if (!auth.user && auth.token) {
if (!auth.user && auth.token && typeof auth.token === 'string') {
try {
const session = await useBaseFetch(
'session/refresh',
@@ -84,18 +99,22 @@ export const initAuth = async (oldToken = null) => {
true,
)
auth.token = session.session
authCookie.value = auth.token
auth.user = await useBaseFetch(
'user',
{
headers: {
Authorization: auth.token,
auth.token = normalizeAuthToken(session.session)
if (auth.token) {
authCookie.value = auth.token
auth.user = await useBaseFetch(
'user',
{
headers: {
Authorization: auth.token,
},
},
},
true,
)
true,
)
} else {
authCookie.value = null
auth.token = ''
}
} catch {
authCookie.value = null
}
@@ -63,7 +63,11 @@ export function useCdnDownloadContext() {
})
function createProjectDownloadUrl(originalUrl: string, context?: DownloadContext): string {
if (!originalUrl.startsWith('https://cdn.modrinth.com')) {
if (
typeof originalUrl !== 'string' ||
!originalUrl ||
!originalUrl.startsWith('https://cdn.modrinth.com')
) {
return originalUrl
}
+4 -1
View File
@@ -199,17 +199,20 @@ 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, ...project }) => ({
return projects.map(({ ownership, external_dependencies_count, ...project }) => ({
project,
ownership: ownership ?? null,
external_dependencies_count: external_dependencies_count,
}))
}
+30 -1
View File
@@ -761,9 +761,13 @@ 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'
@@ -808,6 +812,25 @@ 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'],
@@ -836,6 +859,12 @@ 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',
@@ -1148,7 +1177,7 @@ const isDiscovering = computed(
)
const isDiscoveringSubpage = computed(
() => route.name && route.name.startsWith('type-id') && !route.query.sid,
() => route.name && route.name.startsWith('type-project') && !route.query.sid,
)
const isRussia = computed(() => country.value === 'ru')
+1 -1
View File
@@ -942,7 +942,7 @@
"message": "موعد"
},
"dashboard.withdraw.completion.email-confirmation": {
"message": "ستتلقى رسالة بريد إلكتروني في <b>{email}<b> مع تعليمات لاسترداد المبلغ المسحوب. "
"message": "ستتلقى رسالة بريد إلكتروني في <b>{email}</b> مع تعليمات لاسترداد المبلغ المسحوب. "
},
"dashboard.withdraw.completion.exchange-rate": {
"message": "سعر الصرف "
+156
View File
@@ -107,6 +107,9 @@
"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"
},
@@ -401,6 +404,48 @@
"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."
},
@@ -497,6 +542,9 @@
"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."
},
@@ -548,9 +596,18 @@
"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}}"
},
@@ -560,6 +617,15 @@
"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! 🎉"
},
@@ -575,9 +641,18 @@
"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"
},
@@ -626,9 +701,18 @@
"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"
},
@@ -638,9 +722,57 @@
"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}"
},
@@ -650,18 +782,39 @@
"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ě."
},
@@ -752,6 +905,9 @@
"frog.title": {
"message": "Žába"
},
"hosting-marketing.included.custom-url": {
"message": "Vlastní URL"
},
"landing.creator.feature.monetization.title": {
"message": "Monetizace"
},
@@ -1013,9 +1013,6 @@
"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?"
},
@@ -1469,9 +1469,6 @@
"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?"
},
@@ -1469,9 +1469,6 @@
"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?"
},
+2 -2
View File
@@ -1536,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 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."
"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."
},
"hosting-marketing.faq.versions-loaders": {
"message": "What Minecraft versions and loaders can be used?"
@@ -1881,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 <branch-link>{owner}/{branch}</branch-link> @ {commit}."
"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}."
},
"layout.banner.preview.title": {
"message": "This is a preview deploy of the Modrinth website."
+111 -6
View File
@@ -42,7 +42,7 @@
"message": "Al descargar la Modrinth App, aceptas nuestros <terms-link>Términos</terms-link> y <privacy-link>Política de Privacidad</privacy-link>."
},
"app-marketing.download.title": {
"message": "Descargar la Modrinth App (Beta)"
"message": "Descarga Modrinth App (Beta)"
},
"app-marketing.download.windows": {
"message": "Windows"
@@ -497,6 +497,27 @@
"conversation-thread.reply-modal.help-center-note": {
"message": "Si necesitas ponerte en contacto con el equipo de moderación, por favor utiliza el <help-center-link>Centro de ayuda de Modrinth</help-center-link> y haz clic en la burbuja azúl en la esquina inferior derecha para contactar soporte."
},
"conversation-thread.resubmit-modal.confirmation.description": {
"message": "Confirma que has atendido las observaciones de los moderadores"
},
"conversation-thread.resubmit-modal.confirmation.label": {
"message": "Confirmo que he atendido correctamente las observaciones de los moderadores."
},
"conversation-thread.resubmit-modal.description": {
"message": "Estás enviando <project-title>{projectTitle}</project-title> para ser revisado de nuevo por los moderadores."
},
"conversation-thread.resubmit-modal.header.resubmitting": {
"message": "Enviando de nuevo para revisión"
},
"conversation-thread.resubmit-modal.header.submitting": {
"message": "Enviando para revisión"
},
"conversation-thread.resubmit-modal.reminder": {
"message": "Asegúrate de haber leído y resuelto todas las observaciones del equipo de moderación."
},
"conversation-thread.resubmit-modal.warning": {
"message": "Envíos repetidos sin atender las observaciones de los moderadores pueden resultar en la suspensión de la cuenta."
},
"create-project-version.create-modal.stage.add-files.admonition": {
"message": "Los archivos complementarios sirven para recursos de apoyo, como código fuente, no para versiones o variantes alternativas."
},
@@ -965,6 +986,9 @@
"dashboard.head-title": {
"message": "Panel de control"
},
"dashboard.notifications.empty.no-unread": {
"message": "No tienes notificaciones sin leer."
},
"dashboard.notifications.link.see-all": {
"message": "Ver todas"
},
@@ -995,6 +1019,9 @@
"dashboard.overview.notifications.button.view-history": {
"message": "Ver historial"
},
"dashboard.overview.notifications.empty.no-unread": {
"message": "No tienes notificaciones sin leer."
},
"dashboard.overview.notifications.error.loading": {
"message": "Error cargando notificaciones:"
},
@@ -1335,13 +1362,13 @@
"message": "Seleccionando modpack para instalar antes del reinicio"
},
"discover.seo.description": {
"message": "Busca y navega miles de Minecraft {projectType, select, mod {mods} modpack {modpacks} packs de recursos {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}} en Modrinth con resultados de busqueda instantáneos y precisos. Nuestros filtradores te ayudan a encontrar rápidamente lo mejor de Minecraft {projectType, select, mod {mods} modpack {modpacks} packs de recursos {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}}."
"message": "Busca y navega miles de {projectType, select, mod {mods} modpack {modpacks} resourcepack {packs de recursos} shader {shaders} plugin {plugins} datapack {datapacks} other {proyectos}} para Minecraft en Modrinth con resultados de busqueda instantáneos y precisos. Nuestros filtradores te ayudan a encontrar rápidamente los mejores {projectType, select, mod {mods} modpack {modpacks} resourcepack {packs de recursos} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}} para Minecraft."
},
"discover.seo.title": {
"message": "Buscar {projectType, select, mod {mods} modpack {modpacks} packs de recursos {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}"
"message": "Buscar {projectType, select, mod {mods} modpack {modpacks} resourcepack {paquetes de recursos} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}}"
},
"discover.seo.title-with-query": {
"message": "Buscar {projectType, select, mod {mods} modpack {modpacks} packs de recursos {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects} | {query}"
"message": "Buscar {projectType, select, mod {mods} modpack {modpacks} resourcepack {paquetes de recursos} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}} | {query}"
},
"error.collection.404.list_item.1": {
"message": "Puede que hayas escrito mal la URL de la colección."
@@ -1509,7 +1536,7 @@
"message": "¿Dónde se encuentran los servidores de Modrinth Hosting? ¿Puedo elegir una región?"
},
"hosting-marketing.faq.location.answer": {
"message": "Actualmente disponemos de servidores en Norteamérica, Europa y el Sudeste Asiático entre los que puede elegir al realizar la compra. ¡En el futuro añadiremos más regiones! Si deseas cambiar de región, ponte en contacto con soporte."
"message": "Tenemos servidores disponibles a través de Norteamérica, Europa y el sudeste asiático que puedes seleccionar en el momento de tu compra. ¡Más regiones en el futuro! Si deseas cambiar tu región, por favor contacta con soporte."
},
"hosting-marketing.faq.versions-loaders": {
"message": "¿Qué versiones y loaders de Minecraft se pueden utilizar?"
@@ -1853,6 +1880,9 @@
"layout.banner.build-fail.title": {
"message": "Error al generar el estado desde la API durante la compilación."
},
"layout.banner.preview.description": {
"message": "Si buscas acceder al sitio oficial de Modrinth visita {url}. Este sitio de previsualización es usado por el staff de Modrinth para fines de pruebas. Fue creado usando <branch-link>{owner}/{branch}</branch-link> @{commit}."
},
"layout.banner.preview.title": {
"message": "Esta es una vista previa de la implementación del sitio web de Modrinth."
},
@@ -2681,6 +2711,81 @@
"project.license.title": {
"message": "Licencia"
},
"project.moderation.admonition.approved.body.private": {
"message": "Tu proyecto es privado, lo que significa que solo pueden acceder tú y las personas a las que invites."
},
"project.moderation.admonition.approved.body.public": {
"message": "Tu proyecto está publicado y aparece en las búsquedas de Modrinth."
},
"project.moderation.admonition.approved.body.unlisted": {
"message": "Tu proyecto no está listado, lo que significa que solo se puede acceder a él a través de un link directo y no aparece en las búsquedas de Modrinth."
},
"project.moderation.admonition.approved.body.visibility-message": {
"message": "Puedes cambiar la visibilidad de tu proyecto en la <visibility-settings-link>configuración de visibilidad</visibility-settings-link> de tu proyecto."
},
"project.moderation.admonition.approved.header": {
"message": "Proyecto aprobado"
},
"project.moderation.admonition.draft.body": {
"message": "Este es un borrador de tu proyecto que nadie puede ver hasta que sea enviado para ser revisado y aprobado por el equipo de moderación de Modrinth."
},
"project.moderation.admonition.draft.header": {
"message": "Borrador"
},
"project.moderation.admonition.draft.submit-for-review": {
"message": "Una vez que hayas completado todos los pasos necesarios y te hayas asegurado de que tu proyecto cumple con las <rules-link>Reglas de contenido</rules-link> de Modrinth, puedes enviar tu proyecto a revisión."
},
"project.moderation.admonition.rejected.address-all-concerns": {
"message": "Por favor resuelve todas las observaciones de moderación, incluyendo cualquiera listada abajo, antes de enviar de nuevo el proyecto."
},
"project.moderation.admonition.rejected.header": {
"message": "Cambios solicitados"
},
"project.moderation.admonition.rejected.spam-notice": {
"message": "Enviar en repetidas ocasiones tu proyecto sin atender antes todas las observaciones de moderación puede resultar en la suspensión de la cuenta."
},
"project.moderation.admonition.under-review.body.1": {
"message": "Tu proyecto está en lista de espera para ser revisado por el equipo de moderación de Modrinth."
},
"project.moderation.admonition.under-review.body.2": {
"message": "Tu proyecto será escaneado y después revisado por moderadores humanos para asegurarnos de que esté de acuerdo con las <rules-link>Reglas de contenido</rules-link> y los <terms-link>Términos de uso</terms-link> de Modrinth."
},
"project.moderation.admonition.under-review.body.3": {
"message": "Aún puedes modificar tu proyecto, no afectará tu posición en la cola."
},
"project.moderation.admonition.under-review.body.4": {
"message": "Buscamos revisar envíos en un plazo de 24-48 horas, pero algunos proyectos pueden sufrir retrasos. Esto no indica ningún problema con tu envío."
},
"project.moderation.admonition.under-review.body.5": {
"message": "<emphasis>¡Apreciamos tu paciencia mientras nuestros moderadores trabajan duro para mantener Modrinth seguro y estaremos encantados de poder ayudarte a compartir tu contenido! 💚</emphasis>"
},
"project.moderation.admonition.under-review.header": {
"message": "Proyecto bajo revisión"
},
"project.moderation.admonition.withheld.body": {
"message": "Tu proyecto no aparecerá públicamente y solo se podrá acceder a él por un enlace directo.{requestedStatus, select,unlisted { Según tu <visibility-settings-link>configuración de visibilidad</visibility-settings-link>, es muy probable que no se necesite ninguna acción adicional.} other { Por favor resuelve todas las observaciones de moderación, incluyendo las listadas abajo, antes de enviar de nuevo el proyecto.}}"
},
"project.moderation.admonition.withheld.header": {
"message": "Deslistado por el staff"
},
"project.moderation.error.unauthorized": {
"message": "Sin autorización"
},
"project.moderation.thread.approved-warning": {
"message": "Este hilo no esta siendo monitorizado de manera activa, pero puede ser revisado para más información sobre tu proyecto de ser necesario."
},
"project.moderation.thread.help-center-note.1": {
"message": "Los moderadores de contenido no pueden dar soporte a la mayoría de los problemas y los mensajes de este hilo no notifican al staff."
},
"project.moderation.thread.help-center-note.2": {
"message": "Si necesitas ayuda o tienes consultas adicionales, por favor visita el <help-center-link>Centro de ayuda de Modrinth</help-center-link> y haz click en la burbuja azúl para contactar con soporte."
},
"project.moderation.thread.moderator-see-user-ui-toggle": {
"message": "Mostrar vista de miembro"
},
"project.moderation.thread.private-description": {
"message": "Este es un hilo de conversación con los moderadores de Modrinth. Podrían enviarte un mensaje con observaciones relacionadas a este proyecto."
},
"project.moderation.thread.title": {
"message": "Mensajes de moredación"
},
@@ -3684,7 +3789,7 @@
"message": "Error al renovar la suscripción"
},
"settings.billing.pyro.resubscribe.request-submitted.text": {
"message": "Si el servidor se encuentra cancelado, puede tomar 10-15 minutos en preparar el servidor"
"message": "Si el servidor se encuentra cancelado, puede tomar de 10 a 15 minutos en preparar el servidor."
},
"settings.billing.pyro.resubscribe.request-submitted.title": {
"message": "Solicitud de renovación de suscripción enviada"
@@ -1367,9 +1367,6 @@
"hosting-marketing.faq.location": {
"message": "¿Dónde están localizados los servidores de Modrinth Hosting? ¿Puedo elegir la región?"
},
"hosting-marketing.faq.location.answer": {
"message": "Tenemos servidores disponibles en América del Norte, Europa y Sureste de Asia por el momento, que puedes elegir al momento de comprarlo. ¡Más regiones estarán disponibles en el futuro! Si te gustaría cambiar tu región por favor contáctate con soporte."
},
"hosting-marketing.faq.versions-loaders": {
"message": "¿Que versiones de Minecraft y loaders se pueden usar?"
},
+336
View File
@@ -191,9 +191,21 @@
"app-marketing.hero.download-modrinth-app-for-os": {
"message": "Lataa Modrinth App alustalle {os}"
},
"app-marketing.hero.minecraft-screenshot-alt": {
"message": "Kuvakaappaus Cobblemon instanssin pää valikko näytöstä."
},
"app-marketing.hero.more-download-options": {
"message": "Lisää lataus vaihtoehtoja"
},
"app-marketing.hide-other-packages": {
"message": "Piilota muut paketit"
},
"app-marketing.not-recommended": {
"message": "Emme suosittele että käytät näitä ellet tiedä mitä teet."
},
"app-marketing.show-other-packages": {
"message": "Näytä muut paketit"
},
"auth.authorize.action.authorize": {
"message": "Valtuuta"
},
@@ -335,36 +347,171 @@
"collection.button.edit-icon": {
"message": "Muokkaa kuvaketta"
},
"collection.button.remove-icon": {
"message": "Poista kuvake"
},
"collection.button.remove-project": {
"message": "Poista projekti"
},
"collection.button.replace-icon": {
"message": "Korvaa kuvake"
},
"collection.button.select-icon": {
"message": "Valitse kuvake"
},
"collection.button.unfollow-project": {
"message": "Lopeta projektin seuraaminen"
},
"collection.delete-modal.description": {
"message": "Tämä poistaa pysyvästi tämän kokoelman. Tätä toimea ei voida peruuttaa."
},
"collection.delete-modal.title": {
"message": "Oletko varma että haluat poistaa tämän kokoelman?"
},
"collection.editing": {
"message": "Muokataan kokoelmaa"
},
"collection.error.not-found": {
"message": "Kokoelmaa ei löydy"
},
"collection.label.created-at": {
"message": "Luotu {ago}"
},
"collection.label.curated-by": {
"message": "Kuratoinut"
},
"collection.label.no-projects": {
"message": "Ei vielä projekteja kokelmassa"
},
"collection.label.projects-count": {
"message": "{count, plural, =0 {No projects yet} other {<stat>{count}</stat> {type}}}"
},
"collection.label.updated-at": {
"message": "Päivitetty {ago}"
},
"collection.return-link.dashboard-collections": {
"message": "Kokoelmasi"
},
"collection.return-link.user": {
"message": "{user}:n profiili"
},
"collection.title": {
"message": "{name} - kokoelma"
},
"conversation-thread.action.add-private-note": {
"message": "Lisää yksityinen muistiinpano"
},
"conversation-thread.action.approve": {
"message": "Hyväksy"
},
"conversation-thread.action.approve-with-reply": {
"message": "Hyväksy vastauksella"
},
"conversation-thread.action.close-thread": {
"message": "Sulje keskustelu"
},
"conversation-thread.action.close-with-reply": {
"message": "Sulje vastauksella"
},
"conversation-thread.action.reject": {
"message": "Hylkää"
},
"conversation-thread.action.reject-with-reply": {
"message": "Hylkää vastauksella"
},
"conversation-thread.action.reopen-thread": {
"message": "Avaa keskustelu uudelleen"
},
"conversation-thread.action.reply": {
"message": "Vastaa"
},
"conversation-thread.action.reply-to-thread": {
"message": "Vastaa keskusteluun"
},
"conversation-thread.action.resubmit-for-review": {
"message": "Lähetä uudelleen arvioitavaksi"
},
"conversation-thread.action.resubmit-for-review-with-reply": {
"message": "Lähetä uudelleen arvioitavaksi vastauksella"
},
"conversation-thread.action.send": {
"message": "Lähetä"
},
"conversation-thread.action.send-to-review": {
"message": "Lähetä arvioitavaksi"
},
"conversation-thread.action.send-to-review-with-reply": {
"message": "Lähetä arvioitavaksi vastauksella"
},
"conversation-thread.reply-editor.placeholder.reply": {
"message": "Vastaa keskusteluun..."
},
"conversation-thread.reply-editor.placeholder.send": {
"message": "Lähetä viesti..."
},
"conversation-thread.reply-modal.header": {
"message": "Vastaa keskusteluun"
},
"create.collection.collection-info": {
"message": "Sinun uusi kokoelma luodaan julkiseksi kokoelmaksi {count, plural, =0 {no projects} one {# project} other {# projects}}:lla. "
},
"create.collection.create-collection": {
"message": "Luo kokoelma"
},
"create.collection.name-label": {
"message": "Nimi"
},
"create.collection.name-placeholder": {
"message": "Syötä kokoelman nimi..."
},
"create.collection.summary-label": {
"message": "Yhteenveto"
},
"create.collection.title": {
"message": "Luodaan kokoelmaa"
},
"create.limit-alert.contact-support": {
"message": "Ota yhteyttä tukeen"
},
"create.limit-alert.type-collection": {
"message": "kokoelma"
},
"create.limit-alert.type-organization": {
"message": "organisaatio"
},
"create.limit-alert.type-plural-collection": {
"message": "kokoelmat"
},
"create.limit-alert.type-plural-organization": {
"message": "organisaatiot"
},
"create.limit-alert.type-plural-project": {
"message": "projektit"
},
"create.limit-alert.type-project": {
"message": "projekti"
},
"create.organization.create-organization": {
"message": "Luo organisaatio"
},
"create.organization.name-label": {
"message": "Nimi"
},
"create.organization.name-placeholder": {
"message": "Syötä organisaation nimi..."
},
"create.organization.summary-label": {
"message": "Yhteenveto"
},
"create.project.visibility-private": {
"message": "Yksityinen"
},
"create.project.visibility-public": {
"message": "Julkinen"
},
"create.project.visibility-unlisted": {
"message": "Piilotettu"
},
"dashboard.collections.button.create-new": {
"message": "Luo uusi"
},
@@ -374,18 +521,165 @@
"dashboard.collections.long-title": {
"message": "Kokoelmasi"
},
"dashboard.collections.sort.name-ascending": {
"message": "Nimi (A-Z)"
},
"dashboard.collections.sort.recently-created": {
"message": "Äskettäin luotu"
},
"dashboard.collections.sort.recently-updated": {
"message": "Äskettäin päivitetty"
},
"dashboard.creator-tax-form-modal.confirmation.download-button": {
"message": "Lataa {formType}"
},
"dashboard.creator-tax-form-modal.confirmation.title": {
"message": "Kaikki valmista! 🎉"
},
"dashboard.creator-tax-form-modal.entity.private-individual": {
"message": "Yksityishenkilö"
},
"dashboard.creator-tax-form-modal.header": {
"message": "Vero lomake"
},
"dashboard.creator-tax-form-modal.us-citizen.question": {
"message": "Oletko Yhdysvaltain kansalainen?"
},
"dashboard.creator-withdraw-modal.details-label": {
"message": "Lisätiedot"
},
"dashboard.creator-withdraw-modal.fee-breakdown-amount": {
"message": "Määrä"
},
"dashboard.creator-withdraw-modal.kyc.private-individual": {
"message": "Yksityishenkilö"
},
"dashboard.creator-withdraw-modal.muralpay-details.crypto-warning-header": {
"message": "Vahvista lompakkosi osoite"
},
"dashboard.creator-withdraw-modal.paypal-details.payment-method": {
"message": "Maksutapa"
},
"dashboard.creator-withdraw-modal.paypal-details.paypal-account": {
"message": "PayPal-tili"
},
"dashboard.creator-withdraw-modal.tremendous-details.unverified-email-header": {
"message": "Vavhistamaton sähköposti"
},
"dashboard.creator-withdraw-modal.tremendous-details.usd-paypal-warning-header": {
"message": "Matalammat maksut saatavilla"
},
"dashboard.head-title": {
"message": "Hallintapaneeli"
},
"dashboard.notifications.empty.no-unread": {
"message": "Sinulla ei ole lukemattomia ilmoituksia."
},
"dashboard.notifications.link.see-all": {
"message": "Näytä kaikki"
},
"dashboard.notifications.link.view-history": {
"message": "Näytä ilmoitushistoria"
},
"dashboard.organizations.button.create": {
"message": "Luo organisaatio"
},
"dashboard.organizations.empty.cta": {
"message": "Tee organisaatio!"
},
"dashboard.organizations.error.fetch": {
"message": "Organisaatioiden haku epäonnistui"
},
"dashboard.organizations.member-count": {
"message": "{count} {count, plural, one {jäsen} other {jäsentä}}"
},
"dashboard.projects.head-title": {
"message": "Projektit"
},
"dashboard.projects.links.and-more": {
"message": "ja {count} lisää..."
},
"dashboard.projects.links.button.clear-link": {
"message": "Tyhjennä linkki"
},
"dashboard.projects.links.button.edit": {
"message": "Muokkaa linkkejä"
},
"dashboard.projects.links.discord-invite.description": {
"message": "Kutsulinkki sinun Discord-palvelimellesi."
},
"dashboard.projects.links.discord-invite.label": {
"message": "Discord-kutsu"
},
"dashboard.projects.links.placeholder.cleared": {
"message": "Olemassa oleva linkki tyhjennetään"
},
"dashboard.projects.links.placeholder.valid-discord-url": {
"message": "Syötä kelvollinen Discord-kutsu URL"
},
"dashboard.projects.links.placeholder.valid-url": {
"message": "Syötä kelvollinen URL"
},
"dashboard.projects.links.show-all-projects": {
"message": "Näytä kaikki projektit"
},
"dashboard.projects.links.source-code.label": {
"message": "Lähdekoodi"
},
"dashboard.projects.links.wiki-page.label": {
"message": "Wikisivu"
},
"dashboard.projects.project.icon-alt": {
"message": "Kuvake projektille {title}"
},
"dashboard.projects.sort.ascending": {
"message": "Nouseva"
},
"dashboard.projects.sort.descending": {
"message": "Laskeva"
},
"dashboard.projects.sort.option.name": {
"message": "Nimi"
},
"dashboard.projects.sort.option.status": {
"message": "Tila"
},
"dashboard.projects.sort.option.type": {
"message": "Tyyppi"
},
"dashboard.projects.table.icon": {
"message": "Kuvake"
},
"dashboard.projects.table.id": {
"message": "ID"
},
"dashboard.projects.table.name": {
"message": "Nimi"
},
"dashboard.projects.table.status": {
"message": "Tila"
},
"dashboard.projects.table.type": {
"message": "Tyyppi"
},
"dashboard.report.title": {
"message": "Raportti {id}"
},
"dashboard.reports.active-title": {
"message": "Aktiiviset raportit"
},
"dashboard.reports.title": {
"message": "Raportit"
},
"dashboard.revenue.available-now": {
"message": "Saatavilla nyt"
},
"dashboard.revenue.processing": {
"message": "Prosessoidaan"
},
"dashboard.revenue.transactions.btn.download-csv": {
"message": "Lataa CSV-tiedostona"
},
"error.collection.404.list_item.1": {
"message": "Saatoit kirjoittaa kokoelman URL osoitteen väärin."
},
@@ -419,12 +713,51 @@
"error.project.404.list_title": {
"message": "Miksi?"
},
"error.project.404.title": {
"message": "Projektia ei löytynyt"
},
"error.user.404.list_title": {
"message": "Miksi?"
},
"error.user.404.title": {
"message": "Käyttäjää ei löytynyt"
},
"frog": {
"message": "Löysit sammakon! 🐸"
},
"frog.title": {
"message": "Sammakko"
},
"hosting-marketing.included.backups-included": {
"message": "Varmuuskopiot sisällyttäen"
},
"hosting-marketing.included.file-manager": {
"message": "Helppokäyttöinen tiedostoselain"
},
"hosting-marketing.included.file-manager.description": {
"message": "Etsi, hallinnoi, muokkaa ja lataa tiedostoja suoraan palvelimellesi helposti."
},
"hosting-marketing.included.help": {
"message": "Apua kun sitä tarvitset"
},
"hosting-marketing.included.help.description": {
"message": "Ota yhteyttä Modrinthin tiimiin milloin tahansa palvelinasioissa."
},
"hosting-marketing.included.sftp-access": {
"message": "SFTP pääsy"
},
"hosting-marketing.know-what-you-need": {
"message": "Tiedät jo mitä tarvitset?"
},
"hosting-marketing.medal.info": {
"message": "Kokeile ilmaista <orange>3Gt palvelinta</orange> 5 päivän ajan, jonka tarjoaa <orange>Medal</orange>"
},
"hosting-marketing.medal.learn-more": {
"message": "Lue lisää"
},
"hosting.plan.out-of-stock": {
"message": "Loppuunmyyty"
},
"landing.button.discover-mods": {
"message": "Löydä modeja"
},
@@ -512,6 +845,9 @@
"layout.action.new-project": {
"message": "Uusi projekti"
},
"layout.action.publish": {
"message": "Julkaise"
},
"layout.avatar.alt": {
"message": "Avatarisi"
},
@@ -1235,9 +1235,6 @@
"hosting-marketing.faq.location": {
"message": "Nasaan ang mga server ng Modrinth Hosting? Makapipili ba ako ng rehiyon?"
},
"hosting-marketing.faq.location.answer": {
"message": "Mayroon kaming mga server na magagamit sa Hilagang Amerika, Yuropa, at Timog-silangang Asya sa kasalukuyang sandali na maaari mong piliin pagkatapos bumili. Dadating ang higit pang rehiyon sa hinaharap! Kung nais mong magpalit ng rehiyon, mangyaring abutin ang suporta."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Ano bang bersiyon ng Minecraft at loader ang magagamit?"
},
+2 -2
View File
@@ -1014,7 +1014,7 @@
"message": "Organisations"
},
"dashboard.overview.notifications.button.mark-all-as-read": {
"message": "Mettre tout comme étant lu."
"message": "Marquer tout comme lu"
},
"dashboard.overview.notifications.button.view-history": {
"message": "Voire l'historique"
@@ -1536,7 +1536,7 @@
"message": "Où se trouvent les serveurs de Modrinth Hosting ? Puis-je choisir une région ?"
},
"hosting-marketing.faq.location.answer": {
"message": "Nous avons des serveurs disponibles en Amérique du Nord, en Europe et en Asie du Sud-Est en ce moment-même que vous pouvez choisir dès l'achat. Plus de régions à venir dans le futur ! Si vous souhaitez changer de région, veuillez contacter le support."
"message": "Nous avons des serveurs disponibles en Amérique du Nord, en Europe et en Asie du Sud-Est pour le moment que vous pouvez choisir lors de l'achat. Plus de régions sont à venir ! Si vous voulez changer de région, contactez le support."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Quelles versions de Minecraft et loaders peuvent-être utilisés ?"
@@ -1163,9 +1163,6 @@
"hosting-marketing.faq.location": {
"message": "היכן ממוקמים שרתי האחסון של Modrinth? האם ניתן לבחור אזור?"
},
"hosting-marketing.faq.location.answer": {
"message": "כרגע זמינים עבורכם שרתים בצפון אמריקה, אירופה ודרום-מזרח אסיה, אותם ניתן לבחור בעמד הרכישה. אזורים נוספים יתווספו בעתיד! אם תרצו להחליף את האזור שלכם, אנא צרו קשר עם התמיכה."
},
"hosting-marketing.faq.versions-loaders": {
"message": "באילו גרסאות של מיינקראפט ובאילו Loaders ניתן להשתמש?"
},
+20 -11
View File
@@ -291,7 +291,7 @@
"message": "Már van fiókod?"
},
"auth.sign-up.subscribe.label": {
"message": "Iratkozzon fel a Modrinth frissítéseiről szóló értesítésekre"
"message": "Feliratkozás a Modrinth híreire és frissítéseire"
},
"auth.sign-up.title": {
"message": "Regisztráció"
@@ -333,7 +333,7 @@
"message": "Email-cím hitelesítése"
},
"auth.welcome.checkbox.subscribe": {
"message": "Iratkozzon fel a Modrinth frissítéseiről szóló értesítésekre"
"message": "Feliratkozás a Modrinth híreire és frissítéseire"
},
"auth.welcome.description": {
"message": "Most már része vagy annak a fantasztikus fejlesztők és felfedezők közösségének, akik már építenek, letöltenek és naprakészek maradnak a csodálatos modokkal kapcsolatban."
@@ -416,6 +416,9 @@
"conversation-thread.action.approve-with-reply": {
"message": "Jóváhagyás és válasz"
},
"conversation-thread.action.close-thread": {
"message": "Gondolatmenet lezárása"
},
"conversation-thread.action.close-with-reply": {
"message": "Bezárás és válasz"
},
@@ -950,6 +953,9 @@
"dashboard.head-title": {
"message": "Irányítópult"
},
"dashboard.notifications.empty.no-unread": {
"message": "Nincsenek olvasatlan értesítéseid."
},
"dashboard.notifications.link.see-all": {
"message": "Összes megtekintése"
},
@@ -1478,9 +1484,6 @@
"hosting-marketing.faq.location": {
"message": "Hol találhatók a Modrinth Hosting szerverei? Kiválaszthatom a régiót?"
},
"hosting-marketing.faq.location.answer": {
"message": "Jelenleg Észak-Amerikában, Európában és Délkelet-Ázsiában állnak rendelkezésre szerverek, amelyek közül vásárláskor választhatsz. A jövőben további régiók is elérhetővé válnak! Ha szeretnéd megváltoztatni a régiót, vedd fel a kapcsolatot az ügyfélszolgálattal."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Melyik Minecraft verziók és betöltők használhatók?"
},
@@ -1749,7 +1752,7 @@
"message": "Játékosoknak"
},
"landing.section.for-players.tagline": {
"message": "Fedez fel több mint {count, number} alkotást"
"message": "Fedezz fel több mint {count, number} alkotást"
},
"landing.subheading": {
"message": "Fedezz fel, játssz és ossz meg Minecraft-tartalmakat a közösség számára létrehozott nyílt forráskódú platformunkon."
@@ -1815,7 +1818,7 @@
"message": "Mindig mellőzd"
},
"layout.banner.build-fail.description": {
"message": "A Modrinth felhasználói felületének jelenlegi telepítése nem tudta az API-ból lekérni az állapotadatokat. Ennek oka lehet egy szolgáltatáskimaradás vagy konfigurációs hiba. Kérünk, próbáld meg újra, amikor az API újra elérhetővé válik. Hibakódok: {errors}; Az API jelenlegi linkje: {url}"
"message": "A Modrinth felhasználói felületének jelenlegi telepítése nem tudta az API-ból lekérni az állapotadatokat. Ennek oka lehet egy szolgáltatáskimaradás vagy konfigurációs hiba. Kérlek próbáld meg újra, amikor az API újra elérhetővé válik. Hibakódok: {errors}; Az API jelenlegi linkje: {url}"
},
"layout.banner.build-fail.ignore": {
"message": "Mellőz"
@@ -1827,7 +1830,7 @@
"message": "Ha a hivatalos Modrinth weboldalt szeretnéd elérni, látogass el a {url} oldalra. Ezt az előzetes verziót a Modrinth munkatársai tesztelési célokra használják. A <branch-link>{owner}/{branch</branch-link> @ {commit} használatával készült."
},
"layout.banner.preview.title": {
"message": "Ez a Modrinth weboldal előzetes telepítése."
"message": "Ez a Modrinth weboldal előzetes verziója."
},
"layout.banner.staging.description": {
"message": "A tesztelési környezet teljesen elkülönül az éles Modrinth adatbázistól. Ezt tesztelési és hibakeresési célokra használják, és a Modrinth backend vagy frontend fejlesztés alatt álló, az éles példánynál újabb verzióit futtathatja."
@@ -2628,10 +2631,10 @@
"message": "Átdolgoztuk a Modrinth Környezetek rendszerét, és új lehetőségek érhetők el. Kérjük, ellenőrizd, hogy a metaadatok helyesek-e."
},
"project.environment.migration.review-button": {
"message": "Környezet beállítások Átnézése"
"message": "Környezeti beállítások áttekintése"
},
"project.environment.migration.title": {
"message": "Kérjük, tekintse át a környezet metaadatait"
"message": "Kérlek tekintsd át a környezeti metaadatokat"
},
"project.error.loading": {
"message": "Hiba a projektadatok {message} betöltése közben"
@@ -2669,12 +2672,18 @@
"project.moderation.admonition.draft.submit-for-review": {
"message": "Miután elvégezted az összes szükséges lépést, és meggyőződtél arról, hogy a projekted megfelel a Modrinth <rules-link>Tartalmi szabályainak</rules-link>, benyújthatod a projektedet felülvizsgálatra."
},
"project.moderation.admonition.rejected.header": {
"message": "Változtatások kérve lettek"
},
"project.moderation.admonition.under-review.body.5": {
"message": "<emphasis>Köszönjük türelmedet, amíg moderátoraink keményen dolgoznak a Modrinth biztonságának fenntartásán, és alig várjuk, hogy segíthessünk tartalmaid megosztásában! 💚</emphasis>"
},
"project.moderation.admonition.under-review.header": {
"message": "Projekt áttekintés alatt"
},
"project.moderation.thread.title": {
"message": "Moderációs üzenetek"
},
"project.moderation.title": {
"message": "Moderáció"
},
@@ -3489,7 +3498,7 @@
"message": "Amikor engedélyezel egy alkalmazást a Modrinth fiókoddal, hozzáférést biztosítasz számára a fiókodhoz. A fiókodhoz való hozzáférést bármikor kezelheted és ellenőrizheted itt."
},
"settings.authorizations.empty-state": {
"message": "Jelenleg nem tudjuk megjeleníteni a jogosult alkalmazásait, dolgozunk a probléma megoldásán. Kérjük, látogassa meg ezt az oldalt később!"
"message": "Jelenleg nem tudjuk megjeleníteni az azonosított alkalmazásokat, dolgozunk a probléma megoldásán. Kérlek látogasd meg ezt az oldalt később!"
},
"settings.authorizations.head-title": {
"message": "Engedélyek"
@@ -1163,9 +1163,6 @@
"hosting-marketing.faq.location": {
"message": "Di mana server Modrinth Hosting terletak? Dapatkah saya memilih wilayah?"
},
"hosting-marketing.faq.location.answer": {
"message": "Server kami tersedia di Amerika Utara, Eropa, dan Asia Tenggara dan Anda dapat memilihnya saat membeli. Wilayah-wilayah lain akan segera hadir di masa mendatang! Bila Anda ingin mengganti wilayah Anda, mohon hubungi dukungan."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Versi dan pemuat Minecraft apa sajakah yang dapat digunakan?"
},
@@ -1373,9 +1373,6 @@
"hosting-marketing.faq.location": {
"message": "Modrinthでホストするサーバーはどこにありますか?地域を選択できますか?"
},
"hosting-marketing.faq.location.answer": {
"message": "現時点では、北アメリカ、ヨーロッパ、東南アジアのサーバーを購入時に選択できます。今後、より多くの地域に対応予定です!地域を変更したい場合はサポートまでお問い合わせください。"
},
"hosting-marketing.faq.versions-loaders": {
"message": "どのMinecraftのバージョンとローダーが使えますか?"
},
+2 -2
View File
@@ -687,7 +687,7 @@
"message": "제휴 링크 검색..."
},
"dashboard.analytics.from-projects": {
"message": "프로젝트 {count}개"
"message": "{count} {count, plural, one {프로젝트} other {프로젝트}}로"
},
"dashboard.analytics.total-downloads": {
"message": "다운로드 수"
@@ -1536,7 +1536,7 @@
"message": "Modrinth 호스팅 서버는 어디에 위치해 있나요? 지역을 선택할 수 있나요?"
},
"hosting-marketing.faq.location.answer": {
"message": "현재 서버는 구매 시 북미, 유럽, 동남아 지역 중에서 선택할 수 있습니다. 향후 더 많은 지역에서 서비스를 제공할 예정입니다! 지역을 변경이 필요하면 지원팀에 문의해 주세요."
"message": "현재 북미, 유럽, 동남아시아 전역에서 구매 시 선택할 수 있는 서버가 있습니다. 앞으로 더 많은 지역이 추가될 예정입니다! 지역을 변경하고 싶으시면 지원팀에 문의해 주세요."
},
"hosting-marketing.faq.versions-loaders": {
"message": "어떤 마인크래프트 버전과 로더를 사용할 수 있나요?"
+21 -3
View File
@@ -449,6 +449,27 @@
"conversation-thread.action.send-to-review-with-reply": {
"message": "Hantar untuk semakan dengan balasan"
},
"conversation-thread.error.closing-report": {
"message": "Ralat semasa menutup laporan"
},
"conversation-thread.error.reopening-report": {
"message": "Ralat semasa membuka semula laporan"
},
"conversation-thread.error.sending-message": {
"message": "Ralat semasa menghantar mesej"
},
"conversation-thread.reply-editor.placeholder.reply": {
"message": "Balas bebenang..."
},
"conversation-thread.reply-editor.placeholder.send": {
"message": "Hantar mesej..."
},
"conversation-thread.reply-modal.confirmation.description": {
"message": "Sahkan bahawa penyederhana tidak memantau bebenang ini secara aktif"
},
"conversation-thread.reply-modal.confirmation.label": {
"message": "Saya akui bahawa penyederhana tidak memantau bebenang ini secara aktif."
},
"conversation-thread.resubmit-modal.description": {
"message": "Anda sedang menghantar <project-title>{projectTitle}</project-title> untuk disemak semula oleh penyederhana."
},
@@ -1373,9 +1394,6 @@
"hosting-marketing.faq.location": {
"message": "Di manakah pelayan Modrinth Hosting terletak? Bolehkah saya memilih rantau?"
},
"hosting-marketing.faq.location.answer": {
"message": "Kami mempunyai pelayan yang tersedia di Amerika Utara, Eropah dan Asia Tenggara pada masa ini yang boleh anda pilih semasa pembelian. Lebih banyak rantau akan datang pada masa hadapan! Jika anda ingin menukar rantau anda, sila hubungi sokongan."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Versi Minecraft dan pemuat apa yang boleh digunakan?"
},
+35 -2
View File
@@ -407,11 +407,20 @@
"collection.title": {
"message": "{name} - Collectie"
},
"conversation-thread.reply-modal.description": {
"message": "Je project is al goedgekeurd. Daarom houdt het moderatieteam deze thread niet actief in de gaten. Mocht er echter een probleem met je project zijn, dan kunnen zij je bericht wel zien."
},
"conversation-thread.reply-modal.help-center-note": {
"message": "Als je contact wilt opnemen met het moderatorteam, ga dan naar het <help-center-link>Modrinth Helpcentrum</help-center-link> en klik op het blauwe ballonnetje rechtsonder om contact op te nemen met de ondersteuning."
},
"conversation-thread.resubmit-modal.warning": {
"message": "Herhaaldelijk berichten plaatsen zonder in te gaan op de opmerkingen van de moderators kan leiden tot een opschorting van je account."
},
"create-project-version.create-modal.stage.add-files.admonition": {
"message": "Aanvullende bestanden zijn bedoeld voor ondersteunende bronnen zoals source code, niet voor alternatieve versies of varianten."
},
"create.collection.collection-info": {
"message": "Je nieuwe collectie wordt gemaakt als een publieke collectie met {{count, plural,=0 {geen projecten}one {# project}other {# projecten}}."
"message": "Je nieuwe collectie wordt gemaakt als een publieke collectie met {count, plural,=0 {geen projecten}one {# project}other {# projecten}}."
},
"create.collection.create-collection": {
"message": "Collectie aanmaken"
@@ -572,6 +581,9 @@
"dashboard.affiliate-links.search": {
"message": "Zoek affiliatelinks..."
},
"dashboard.analytics.from-projects": {
"message": "van {count} {count, plural, one {project} other {projects}}"
},
"dashboard.collections.button.create-new": {
"message": "Maak nieuwe"
},
@@ -848,6 +860,18 @@
"dashboard.creator-withdraw-modal.withdraw-limit-used": {
"message": "Je hebt je opnamelimiet <b>{withdrawLimit}</b> bereikt. Je moet een belastingformulier invullen om meer geld op te nemen."
},
"dashboard.notifications.link.view-more": {
"message": "Bekijk {extraNotifs} meer {extraNotifs, plural, one {notification} other {notifications}}"
},
"dashboard.organizations.member-count": {
"message": "{count} {count, plural, one {member} other {members}}"
},
"dashboard.projects.links.changes-applied": {
"message": "De wijzigingen worden toegepast op <strong>{count}</strong> {count, plural, one {project} other {projects}}."
},
"dashboard.projects.links.description": {
"message": "Alle links die u hieronder opgeeft, worden in elk van de geselecteerde projecten overschreven. Links die u leeg laat, worden genegeerd. U kunt een link uit alle geselecteerde projecten verwijderen met de prullenbakknop."
},
"dashboard.revenue.available-now": {
"message": "Nu beschikbaar"
},
@@ -1004,6 +1028,15 @@
"dashboard.withdraw.error.tax-form.title": {
"message": "Gelieve het belastingformulier in te vullen"
},
"discover.seo.description": {
"message": "Zoek en blader door duizenden Minecraft {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}} op Modrinth met directe, nauwkeurige zoekresultaten. Onze filters helpen je snel de beste Minecraft {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}}."
},
"discover.seo.title": {
"message": "Zoek {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}}"
},
"discover.seo.title-with-query": {
"message": "Zoek {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": "U heeft mogelijk de URL van de collectie fout ingetypt."
},
@@ -1164,7 +1197,7 @@
"message": "Waar bevinden de servers van Modrinth Hosting zich? Kan ik een regio kiezen?"
},
"hosting-marketing.faq.location.answer": {
"message": "We hebben momenteel servers beschikbaar in Noord-Amerika, Europa en Zuidoost-Azië, waaruit u bij aankoop kunt kiezen. In de toekomst zullen er meer regio's volgen! Als u uw regio wilt wijzigen, neem dan contact op met de klantenservice."
"message": "Op dit moment hebben we servers beschikbaar in Noord-Amerika, Europa en Zuidoost-Azië, waaruit u bij aankoop kunt kiezen. In de toekomst komen er nog meer regios bij! Als u van regio wilt wisselen, neem dan contact op met de klantenservice."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Welke Minecraft-versies en loaders kunnen worden gebruikt?"
@@ -1238,9 +1238,6 @@
"hosting-marketing.faq.location": {
"message": "Hvor oppholder Modrinth Hosting-serverne seg? Kan jeg velge en region?"
},
"hosting-marketing.faq.location.answer": {
"message": "Akkurat nå, så har tilgjengelige servere i Nord-Amerika, Europa, og Sørøst-Asia som du kan velge etter kjøpet ditt. Det kommer flere regioner i framtida! Viss du vil bytte regionen din, vær så snill å ta kontakt med støtte."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Hvilke Minecraft-versjoner-og-loadere kan brukes?"
},
+5 -5
View File
@@ -1362,13 +1362,13 @@
"message": "Selecionando o pacote de mods para instalar após a redefinição"
},
"discover.seo.description": {
"message": "Busque e explore milhares de {projectType, select, mod {mods} modpack {pacotes de mods} resourcepack {pacotes de recursos} shader {shaders} plugin {plugins} datapack {pacotes de dados} other {projetos}} de Minecraft no Modrinth com resultados de busca instantâneos e precisos. Nossos filtros ajudam você a encontrar rapidamente os melhores {projectType, select, mod {mods} modpack {pacotes de mods} resourcepack {pacotes de recursos} shader {shaders} plugin {plugins} datapack {pacotes de dados} other {projetos}} de Minecraft."
"message": "Busque e explore milhares de {projectType, select, mod {mods} modpack {pacotes de mods (modpacks)} resourcepack {pacotes de recursos (resourcepacks)} shader {sombreadores (shaders)} plugin {plugins} datapack {pacotes de dados (datapacks)} other {projetos}} de Minecraft no Modrinth com resultados de busca instantâneos e precisos. Nossos filtros ajudam você a encontrar rapidamente os melhores {projectType, select, mod {mods} modpack {pacotes de mods (modpacks)} resourcepack {pacotes de recursos (resourcepacks)} shader {sombreadores (shaders)} plugin {plugins} datapack {pacotes de dados (datapacks)} other {projetos}} de Minecraft."
},
"discover.seo.title": {
"message": "Buscar {projectType, select, mod {mods} modpack {pacotes de mods} resourcepack {pacotes de recursos} shader {shaders} plugin {plugins} datapack {pacotes de dados} other {projetos}}"
"message": "Buscar {projectType, select, mod {mods} modpack {pacotes de mods (modpacks)} resourcepack {pacotes de recursos (resourcepacks)} shader {sombreadores (shaders)} plugin {plugins} datapack {pacotes de dados (datapacks)} other {projetos}}"
},
"discover.seo.title-with-query": {
"message": "Buscar {projectType, select, mod {mods} modpack {pacotes de mods} resourcepack {pacotes de recursos} shader {shaders} plugin {plugins} datapack {pacotes de dados} other {projetos}} | {query}"
"message": "Buscar {projectType, select, mod {mods} modpack {pacotes de mods (modpacks)} resourcepack {pacotes de recursos (resourcepacks)} shader {sombreadores (shaders)} plugin {plugins} datapack {pacotes de dados (datapacks)} other {projetos}} | {query}"
},
"error.collection.404.list_item.1": {
"message": "Você pode ter digitado incorretamente o URL da coleção."
@@ -2565,10 +2565,10 @@
"message": "Servidor"
},
"project-type.shader.plural": {
"message": "Shaders"
"message": "Sombreadores"
},
"project-type.shader.singular": {
"message": "Shader"
"message": "Sombreador"
},
"project.about.details.created": {
"message": "Criado {date}"
@@ -1229,9 +1229,6 @@
"hosting-marketing.faq.location": {
"message": "Onde estão localizados os servidores Modrinth Hosting? Posso escolher uma região?"
},
"hosting-marketing.faq.location.answer": {
"message": "Temos servidores disponíveis na América do Norte, Europa e Ásia Sudeste, que podes escolher no momento da compra. Mais regiões serão adicionadas no futuro! Se quiseres trocar a tua região, entra em contacto com o suporte."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Que versões de Minecraft e loaders podem ser utilizados?"
},
+16 -16
View File
@@ -408,25 +408,25 @@
"message": "{name} — Коллекция"
},
"conversation-thread.action.add-private-note": {
"message": "Добавить примечание для себя"
"message": "Добавить приватную заметку"
},
"conversation-thread.action.approve": {
"message": "Одобрить"
},
"conversation-thread.action.approve-with-reply": {
"message": "Одобрить с ответом"
"message": "Одобрить с ответом"
},
"conversation-thread.action.close-thread": {
"message": "Закрыть ветку"
},
"conversation-thread.action.close-with-reply": {
"message": "Закрыть с ответом"
"message": "Закрыть с ответом"
},
"conversation-thread.action.reject": {
"message": "Отклонить"
},
"conversation-thread.action.reject-with-reply": {
"message": "Отклонить с ответом"
"message": "Отклонить с ответом"
},
"conversation-thread.action.reopen-thread": {
"message": "Возобновить ветку"
@@ -435,34 +435,34 @@
"message": "Ответить"
},
"conversation-thread.action.reply-to-thread": {
"message": "Ответить на ветку"
"message": "Ответить в ветке"
},
"conversation-thread.action.resubmit-for-review": {
"message": "Запросить перепроверку"
"message": "Отправить на перепроверку"
},
"conversation-thread.action.resubmit-for-review-with-reply": {
"message": "Запросить перепроверку с ответом"
"message": "Отправить на перепроверку с ответом"
},
"conversation-thread.action.send": {
"message": "Отправить"
},
"conversation-thread.action.send-to-review": {
"message": "Отправить на проверку"
"message": "Отправить на проверку"
},
"conversation-thread.action.send-to-review-with-reply": {
"message": "Отправить на проверку с ответом"
"message": "Отправить на проверку с ответом"
},
"conversation-thread.action.set-to-draft": {
"message": "Установить в черновик"
"message": "Установить в черновик"
},
"conversation-thread.action.set-to-draft-with-reply": {
"message": "Установить в черновик с ответом"
"message": "Установить в черновик с ответом"
},
"conversation-thread.action.withhold": {
"message": "Удержать"
},
"conversation-thread.action.withhold-with-reply": {
"message": "Удержать с ответом"
"message": "Удержать с ответом"
},
"conversation-thread.closed-thread.description": {
"message": "Эта ветка закрыта, новые сообщения в неё нельзя отправить."
@@ -477,10 +477,10 @@
"message": "Ошибка отправки сообщения"
},
"conversation-thread.reply-editor.placeholder.reply": {
"message": "Ответить в ветку..."
"message": "Ответьте в ветке..."
},
"conversation-thread.reply-editor.placeholder.send": {
"message": "Отправить сообщение..."
"message": "Отправьте сообщение..."
},
"conversation-thread.reply-modal.confirmation.description": {
"message": "Подтвердить, что модераторы не проводят активный просмотр"
@@ -492,7 +492,7 @@
"message": "Ваш проект уже одобрен. Из-за этого модерация не проводит активный просмотр этой ветки. Однако, если возникнет проблема с вашим проектом, она сможет увидеть ваше сообщение."
},
"conversation-thread.reply-modal.header": {
"message": "Ответить на ветку"
"message": "Ответ в ветке"
},
"conversation-thread.reply-modal.help-center-note": {
"message": "Если вам нужно выйти на контакт с командой модерации, используйте <help-center-link>справочный центр Modrinth</help-center-link> и нажмите на синий значок в правом нижнем углу для связи с поддержкой."
@@ -2496,7 +2496,7 @@
"message": "Запросите проверку"
},
"project-moderation-nags.submit-for-review-button": {
"message": "Отправить на проверку"
"message": "Отправить на проверку"
},
"project-moderation-nags.submit-for-review-desc": {
"message": "Проект сейчас виден только участникам. Для публикации он должен пройти проверку модераторами."
@@ -1184,9 +1184,6 @@
"hosting-marketing.faq.location": {
"message": "Var finns Modrinth Hostings servrar? Kan jag välja region?"
},
"hosting-marketing.faq.location.answer": {
"message": "Vi har för närvarande servrar tillgängliga i Nordamerika, Europa och Sydostasien som du kan välja mellan vid köpet. Fler regioner kommer att läggas till i framtiden! Om du vill byta region, vänligen kontakta supporten."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Vilka Minecraft versioner och loaders kan användas?"
},
+178 -1
View File
@@ -407,6 +407,108 @@
"collection.title": {
"message": "{name} - Koleksiyon"
},
"conversation-thread.action.add-private-note": {
"message": "Özel not ekle"
},
"conversation-thread.action.approve": {
"message": "Onaylandı"
},
"conversation-thread.action.approve-with-reply": {
"message": "Onaylandı, işlem tamamlandı"
},
"conversation-thread.action.close-thread": {
"message": "Konuyu kapat"
},
"conversation-thread.action.close-with-reply": {
"message": "Cevapla ve kapat"
},
"conversation-thread.action.reject": {
"message": "Reddet"
},
"conversation-thread.action.reject-with-reply": {
"message": "Cevap vererek reddet"
},
"conversation-thread.action.reopen-thread": {
"message": "Konuyu yeniden aç"
},
"conversation-thread.action.reply": {
"message": "Yanıtla"
},
"conversation-thread.action.reply-to-thread": {
"message": "Konuya yanıt ver"
},
"conversation-thread.action.resubmit-for-review": {
"message": "İnceleme için yeniden gönder"
},
"conversation-thread.action.resubmit-for-review-with-reply": {
"message": "Yanıtla birlikte yeniden incelemeye gönder"
},
"conversation-thread.action.send": {
"message": "Gönder"
},
"conversation-thread.action.send-to-review": {
"message": "İncelemeye gönder"
},
"conversation-thread.action.send-to-review-with-reply": {
"message": "Yanıtla birlikte incelemeye gönder"
},
"conversation-thread.action.withhold": {
"message": "Yanıtla birlikte taslağa çevir"
},
"conversation-thread.action.withhold-with-reply": {
"message": "Yanıtla birlikte beklet"
},
"conversation-thread.closed-thread.description": {
"message": "Bu konu kapatılmıştır ve yeni mesaj gönderilemez."
},
"conversation-thread.error.closing-report": {
"message": "Rapor kapatılırken hata oluştu"
},
"conversation-thread.error.sending-message": {
"message": "Mesaj gönderilirken hata oluştu"
},
"conversation-thread.reply-editor.placeholder.reply": {
"message": "Konuya yanıt ver..."
},
"conversation-thread.reply-editor.placeholder.send": {
"message": "Mesaj gönder..."
},
"conversation-thread.reply-modal.confirmation.description": {
"message": "Moderatörlerin bunu aktif olarak izlemediğini onayla"
},
"conversation-thread.reply-modal.confirmation.label": {
"message": "Moderatörlerin bu konuyu aktif olarak denetlemediğini kabul ediyorum."
},
"conversation-thread.reply-modal.description": {
"message": "Projeniz zaten onaylanmış durumda. Bu nedenle moderasyon ekibi bu konuyu aktif olarak takip etmez. Ancak projenizle ilgili bir sorun oluşursa mesajınızı yine de görebilirler."
},
"conversation-thread.reply-modal.header": {
"message": "Konuyu yanıtla"
},
"conversation-thread.reply-modal.help-center-note": {
"message": "Moderasyon ekibiyle iletişime geçmeniz gerekiyorsa, lütfen <help-center-link>Modrinth Yardım Merkezi</help-center-link> adresini kullanın ve destekle iletişime geçmek için sağ alt köşedeki mavi sohbet simgesine tıklayın."
},
"conversation-thread.resubmit-modal.confirmation.description": {
"message": "Moderatörlerin mesajlarını yanıtladığımı onaylıyorum"
},
"conversation-thread.resubmit-modal.confirmation.label": {
"message": "Moderatörlerin yorumlarını uygun şekilde yanıtladığımı onaylıyorum."
},
"conversation-thread.resubmit-modal.description": {
"message": "<project-title>{projectTitle}</project-title> projesini moderatörler tarafından yeniden incelenmek üzere gönderiyorsunuz."
},
"conversation-thread.resubmit-modal.header.resubmitting": {
"message": "İnceleme için yeniden gönderiliyor"
},
"conversation-thread.resubmit-modal.header.submitting": {
"message": "İncelemeye gönderiliyor"
},
"conversation-thread.resubmit-modal.reminder": {
"message": "Moderatör ekibinin tüm yorumlarını ele aldığınızdan emin olun."
},
"conversation-thread.resubmit-modal.warning": {
"message": "Moderatörlerin yorumlarını ele almadan tekrar tekrar gönderim yapmak, hesabınızın askıya alınmasına neden olabilir."
},
"create-project-version.create-modal.stage.add-files.admonition": {
"message": "Ek dosyalar, alternatif sürümler veya varyantlar için değil, kaynak kodu gibi destekleyici kaynaklar içindir."
},
@@ -875,6 +977,9 @@
"dashboard.head-title": {
"message": "Kontrol Paneli"
},
"dashboard.notifications.empty.no-unread": {
"message": "Okunmamış bildiriminiz yok."
},
"dashboard.notifications.link.see-all": {
"message": "Hepsi"
},
@@ -899,6 +1004,27 @@
"dashboard.organizations.title": {
"message": "Organizasyonlar"
},
"dashboard.overview.notifications.button.mark-all-as-read": {
"message": "Tümünü okundu olarak işaretle"
},
"dashboard.overview.notifications.button.view-history": {
"message": "Geçmişi görüntüle"
},
"dashboard.overview.notifications.empty.no-unread": {
"message": "Okunmamış bildiriminiz yok."
},
"dashboard.overview.notifications.error.loading": {
"message": "Bildirimler yüklenirken hata oluştu:"
},
"dashboard.overview.notifications.history.label": {
"message": "Geçmiş"
},
"dashboard.overview.notifications.history.title": {
"message": "Bildirim geçmişi"
},
"dashboard.overview.notifications.loading": {
"message": "Bildirimler yükleniyor..."
},
"dashboard.projects.bulk-edit-hint": {
"message": "Aşağıdan seçerek birden fazla projeyi düzenleyebilirsiniz."
},
@@ -1208,6 +1334,30 @@
"dashboard.withdraw.error.tax-form.title": {
"message": "Lütfen vergi formunu tamamlayınız"
},
"discover.install.back-to-setup": {
"message": "Sunucuya geri dön"
},
"discover.install.cancel-reset": {
"message": "Sıfırlamayı iptal et"
},
"discover.install.error.no-server-world": {
"message": "Yüklenebilecek hiçbir sunucu dünyası mevcut değil."
},
"discover.install.error.unsupported-content-type": {
"message": "Bu içerik türü, tarayıcıdan bir sunucuya yüklenemez."
},
"discover.install.heading.reset-modpack": {
"message": "Sıfırlama sonrası yüklenecek mod paketi seçiliyor"
},
"discover.seo.description": {
"message": "Modrinth üzerinde anında ve doğru arama sonuçlarıyla binlerce Minecraft {projectType, select, mod {mod} modpack {modpack} resourcepack {kaynak paketi} shader {shader} plugin {eklenti} datapack {veri paketi} other {proje}} arasında arama yap ve göz at. Filtrelerimiz en iyi Minecraft {projectType, select, mod {modları} modpack {mod paketlerini} resourcepack {kaynak paketlerini} shader {shaderları} plugin {eklentileri} datapack {veri paketlerini} other {projeleri}} hızlıca bulmana yardımcı olur."
},
"discover.seo.title": {
"message": "Ara {projectType, select, mod {modları} modpack {mod paketleri} resourcepack {kaynak paketleri} shader {shaderlar} plugin {eklentiler} datapack {veri paketleri} other {projeler}}"
},
"discover.seo.title-with-query": {
"message": "Ara {projectType, select, mod {modları} modpack {mod paketleri} resourcepack {kaynak paketleri} shader {shaderlar} plugin {eklentiler} datapack {veri paketleri} other {projeler}} | {query}"
},
"error.collection.404.list_item.1": {
"message": "Koleksiyonun URL'ini yanlış yazmış olabilirsin."
},
@@ -1223,6 +1373,12 @@
"error.collection.404.title": {
"message": "Koleksiyon bulunamadı"
},
"error.generic.401.signed-in-as": {
"message": "Şu anda şu hesapla oturum açtınız:"
},
"error.generic.401.title": {
"message": "Bu sayfaya erişiminiz yok"
},
"error.generic.404.subtitle": {
"message": "Aradığın sayfa yokmuş gibi duruyor."
},
@@ -1368,7 +1524,7 @@
"message": "Modrinth Hosting sunucuları nerede bulunuyor? Bölge seçebilir miyim?"
},
"hosting-marketing.faq.location.answer": {
"message": "Şu anda Kuzey Amerika, Avrupa ve Güneydoğu Asya'da satın alabileceğiniz sunucularımız bulunmaktadır. Gelecekte daha fazla bölge eklenecektir! Bölgenizi değiştirmek isterseniz, lütfen destek ekibiyle iletişime geçin."
"message": "Şu anda satın alma sırasında seçebileceğiniz sunucular Kuzey Amerika, Avrupa ve Güneydoğu Asya bölgelerinde mevcuttur. Yakında daha fazla bölge eklenecektir. Bölgenizi değiştirmek isterseniz lütfen destek ekibiyle iletişime geçin."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Hangi Minecraft sürümleri ve yükleyiciler kullanılabilir?"
@@ -1649,6 +1805,9 @@
"layout.action.create-new": {
"message": "Yeni oluştur..."
},
"layout.action.external-projects": {
"message": "Harici projeler"
},
"layout.action.file-lookup": {
"message": "Dosya bulma"
},
@@ -1697,12 +1856,21 @@
"layout.banner.add-email.description": {
"message": "Güvenlik sebebiyle Modrinth hesabınıza bir e-posta adresi kaydetmeniz gerek."
},
"layout.banner.build-fail.always-ignore": {
"message": "Her zaman yok say"
},
"layout.banner.build-fail.description": {
"message": "Modrinth'in önucunun bu dağıtımı API'den durum oluşturamadı. Bu bir kesintiden veya yapılandırmadaki bir hatadan kaynaklı olabilir. API mevcutken tekrar derleyin. Hata kodları: {errors}; şu anki API adresi: {url}"
},
"layout.banner.build-fail.ignore": {
"message": "Yok say"
},
"layout.banner.build-fail.title": {
"message": "Derleme esnasında API'den durum oluştururken hata."
},
"layout.banner.preview.description": {
"message": "Eğer resmi Modrinth web sitesine erişmek istiyorsanız, {url} adresini ziyaret edin. Bu önizleme sürümü Modrinth ekibi tarafından test amaçlı kullanılmaktadır. <branch-link>{owner}/{branch}</branch-link> @ {commit} kullanılarak derlenmiştir."
},
"layout.banner.preview.title": {
"message": "Bu, Modrinth web sitesinin önizleme dağıtımıdır."
},
@@ -1898,6 +2066,9 @@
"moderation.moderate": {
"message": "Yönet"
},
"moderation.page.external-projects": {
"message": "Harici projeler"
},
"moderation.page.projects": {
"message": "Projeler"
},
@@ -3332,6 +3503,9 @@
"settings.billing.charges.description": {
"message": "Modrinth hesabınıza geçmişte yapılan tüm ödemeleriniz burada listelenir:"
},
"settings.billing.charges.product.pyro": {
"message": "Modrinth Barındırma"
},
"settings.billing.charges.title": {
"message": "Past charges"
},
@@ -3440,6 +3614,9 @@
"settings.billing.pyro.ram": {
"message": "{gb} GB RAM"
},
"settings.billing.pyro.resubscribe.error.text": {
"message": "Modrinth sunucuna yeniden abone olurken hata oluştu."
},
"settings.billing.pyro.resubscribe.error.title": {
"message": "Yeniden abone olurken hata"
},
+76 -4
View File
@@ -407,6 +407,81 @@
"collection.title": {
"message": "{name} - Bộ sưu tập"
},
"conversation-thread.action.add-private-note": {
"message": "Thêm ghi chú bí mật"
},
"conversation-thread.action.approve": {
"message": "Chấp thuận"
},
"conversation-thread.action.approve-with-reply": {
"message": "Chấp thuận với phản hồi"
},
"conversation-thread.action.close-thread": {
"message": "Đóng luồng"
},
"conversation-thread.action.close-with-reply": {
"message": "Đóng với phản hồi"
},
"conversation-thread.action.reject": {
"message": "Từ chối"
},
"conversation-thread.action.reject-with-reply": {
"message": "Từ chối với phản hồi"
},
"conversation-thread.action.reopen-thread": {
"message": "Mở lại luồng"
},
"conversation-thread.action.reply": {
"message": "Phản hồi"
},
"conversation-thread.action.reply-to-thread": {
"message": "Phản hồi đến luồng"
},
"conversation-thread.action.resubmit-for-review": {
"message": "Gửi lại để xét duyệt"
},
"conversation-thread.action.resubmit-for-review-with-reply": {
"message": "Gửi lại để xét duyệt kèm phản hồi"
},
"conversation-thread.action.send": {
"message": "Gửi"
},
"conversation-thread.action.send-to-review": {
"message": "Gửi để xem xét"
},
"conversation-thread.action.send-to-review-with-reply": {
"message": "Gửi để xem xét với phản hồi"
},
"conversation-thread.action.set-to-draft": {
"message": "Chuyển sang bản nháp"
},
"conversation-thread.action.set-to-draft-with-reply": {
"message": "Chuyển sang bản nháp kèm phản hồi"
},
"conversation-thread.action.withhold": {
"message": "Thu hồi"
},
"conversation-thread.action.withhold-with-reply": {
"message": "Thu hồi với phản hồi"
},
"conversation-thread.closed-thread.description": {
"message": "Luồng này đã bị đóng và tin nhắn mới không thể gửi được vào."
},
"conversation-thread.error.closing-report": {
"message": "Lỗi khi đóng báo cáo"
},
"conversation-thread.error.reopening-report": {
"message": "Lỗi khi mở lại báo cáo"
},
"conversation-thread.error.sending-message": {
"message": "Lỗi khi gửi tin nhắn"
},
"conversation-thread.reply-editor.placeholder.reply": {
"message": "Đang phản hồi vào luồng..."
},
"conversation-thread.reply-editor.placeholder.send": {
"message": "Gửi một tin nhắn..."
},
"create-project-version.create-modal.stage.add-files.admonition": {
"message": "Các tệp bổ trợ dùng cho các tài nguyên hỗ trợ như mã nguồn, không dùng cho các phiên bản hoặc biến thể thay thế."
},
@@ -768,7 +843,7 @@
"message": "Mạng"
},
"dashboard.creator-withdraw-modal.nearing-threshold": {
"message": "Bạn sắp chạm đến ngưỡng rút tiền. Bạn có thể rút <b>{amountRemaining}<b> ngay bây giờ, nhưng cần có biểu mẫu thuế để rút thêm."
"message": "Bạn sắp chạm đến ngưỡng rút tiền. Bạn có thể rút <b>{amountRemaining}</b> ngay bây giờ, nhưng cần có biểu mẫu thuế để rút thêm."
},
"dashboard.creator-withdraw-modal.paypal-details.account": {
"message": "Tài khoản"
@@ -1199,9 +1274,6 @@
"hosting-marketing.faq.location": {
"message": "Những máy chủ Modrinth Hosting được nằm ở đâu? Tôi có thể chọn khu vực không?"
},
"hosting-marketing.faq.location.answer": {
"message": "Chúng tôi có các máy chủ có sẵn ở Bắc Mỹ, Châu Âu và Đông Nam Á tại thời điểm này mà bạn có thể chọn khi mua. Nhiều khu vực sẽ đến trong tương lai! Nếu bạn muốn chuyển đổi khu vực của mình, vui lòng liên hệ với bộ phận hỗ trợ."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Những phiên bản Minecraft và modloader nào có thể được sử dụng?"
},
+3 -3
View File
@@ -1391,9 +1391,6 @@
"hosting-marketing.faq.location": {
"message": "Modrinth Hosting 伺服器位於哪裡?我可以選擇區域嗎?"
},
"hosting-marketing.faq.location.answer": {
"message": "我們目前在北美、歐洲與東南亞設有伺服器,你可以在購買時選擇。未來還會推出更多區域!如果你想切換區域,請聯絡支援團隊。"
},
"hosting-marketing.faq.versions-loaders": {
"message": "可以使用哪些 Minecraft 版本與載入器?"
},
@@ -3056,6 +3053,9 @@
"search.filter.locked.server.sync": {
"message": "與伺服器同步"
},
"servers.manage.content.title": {
"message": "內容 - {serverName} - Modrinth"
},
"servers.notice.actions": {
"message": "動作"
},
@@ -1,10 +0,0 @@
const COLLECTION_PREFIX = '/collection/bLYCa4PJ'
export default defineNuxtRouteMiddleware((to) => {
if (to.path === COLLECTION_PREFIX || to.path.startsWith(`${COLLECTION_PREFIX}/`)) {
return navigateTo(`https://april-fools-2026.modrinth.com${to.fullPath}`, {
external: true,
redirectCode: 302,
})
}
})
@@ -20,8 +20,13 @@ const PROJECT_TYPES = [
export default defineNuxtRouteMiddleware(async (to) => {
// Only run this middleware on the server - it relies on server-only runtime config
if (import.meta.client) return
const routeProjectParam = to.params.project
const projectId = Array.isArray(routeProjectParam) ? routeProjectParam[0] : routeProjectParam
const routeType = Array.isArray(to.params.type) ? to.params.type[0] : to.params.type
// Only handle project routes
if (!to.params.id || !PROJECT_TYPES.includes(to.params.type as string)) {
if (!projectId || !routeType || !PROJECT_TYPES.includes(routeType)) {
return
}
@@ -29,7 +34,6 @@ export default defineNuxtRouteMiddleware(async (to) => {
const authToken = useCookie('auth-token')
const client = useServerModrinthClient({ authToken: authToken.value || undefined })
const tags = useGeneratedState()
const projectId = to.params.id as string
try {
// Fetch v2 and v3 in parallel — cache both for the page's useQuery calls
@@ -485,7 +485,7 @@
v-if="!isServerProject"
size="large"
:color="
(auth.user && currentMember) || route.name === 'type-id-version-version'
(auth.user && currentMember) || route.name === 'type-project-version-version'
? `standard`
: `brand`
"
@@ -507,7 +507,7 @@
v-else
size="large"
:color="
(auth.user && currentMember) || route.name === 'type-id-version-version'
(auth.user && currentMember) || route.name === 'type-project-version-version'
? `standard`
: `brand`
"
@@ -529,7 +529,7 @@
size="large"
circular
:color="
route.name === 'type-id-version-version' || (auth.user && currentMember)
route.name === 'type-project-version-version' || (auth.user && currentMember)
? `standard`
: `brand`
"
@@ -547,7 +547,7 @@
size="large"
circular
:color="
route.name === 'type-id-version-version' || (auth.user && currentMember)
route.name === 'type-project-version-version' || (auth.user && currentMember)
? `standard`
: `brand`
"
@@ -1136,7 +1136,7 @@ import { useModerationQueue } from '~/services/moderation-queue.ts'
import { getReportPath, reportProject } from '~/utils/report-helpers.ts'
definePageMeta({
key: (route) => `${route.params.id}`,
key: (route) => `${route.params.project}`,
})
const data = useNuxtApp()
@@ -1151,6 +1151,9 @@ const { addNotification } = notifications
const auth = await useAuth()
const user = await useUser()
// Route param for initial lookup (middleware caches by both slug and ID)
const routeProjectId = ref(useRouteId('project'))
const { createProjectDownloadUrl } = useCdnDownloadContext()
const downloadReason = ref('standalone')
@@ -1637,7 +1640,7 @@ const collections = computed(() =>
)
if (
!route.params.id ||
!routeProjectId.value ||
!(
tags.value.projectTypes.find((x) => x.id === route.params.type) ||
route.params.type === 'project'
@@ -1650,9 +1653,6 @@ if (
})
}
// Route param for initial lookup (middleware caches by both slug and ID)
const routeProjectId = computed(() => route.params.id)
// Use DI client for TanStack Query
const client = injectModrinthClient()
const queryClient = useQueryClient()
@@ -1870,7 +1870,7 @@ const { data: organizationRaw } = useQuery({
// Return null when the project no longer belongs to an organization.
const organization = computed(() => (projectRaw.value?.organization ? organizationRaw.value : null))
const isSettings = computed(() => route.name.startsWith('type-id-settings'))
const isSettings = computed(() => route.name.startsWith('type-project-settings'))
// Transform versionsV3 to be same shape as versionsV2 for compatibility in project pages
const versionsRaw = computed(() => {
@@ -1911,27 +1911,6 @@ function loadDependencies() {
// This allows showing/hiding UI elements without loading full version data
const hasVersions = computed(() => (project.value?.versions?.length ?? 0) > 0)
async function updateProjectRoute() {
if (
project.value &&
route.params.id !== project.value.slug &&
!flags.value.disablePrettyProjectUrlRedirects
) {
await navigateTo(
{
name: route.name,
params: {
...route.params,
id: project.value.slug,
},
query: route.query,
hash: route.hash,
},
{ replace: true },
)
}
}
async function invalidateProject() {
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
await queryClient.invalidateQueries({ queryKey: ['project', 'v3', routeProjectId.value] })
@@ -1947,12 +1926,27 @@ async function invalidateProject() {
const patchProjectMutation = useMutation({
mutationFn: async ({ projectId, data }) => {
await client.labrinth.projects_v2.edit(projectId, data)
if (data.slug !== undefined && data.slug !== route.params.project) {
routeProjectId.value = data.slug
await navigateTo(
{
name: route.name,
params: {
type: route.params.type,
project: data.slug,
},
query: route.query,
hash: route.hash,
},
{ replace: true },
)
}
return data
},
onMutate: async ({ projectId, data }) => {
// Cancel outgoing refetches for both slug-based and ID-based cache keys
// The query may be keyed by slug (routeProjectId) but we also have the actual UUID (projectId)
// The query may be keyed by slug (routeProjectId.value) but we also have the actual UUID (projectId)
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
if (routeProjectId.value !== projectId) {
await queryClient.cancelQueries({ queryKey: ['project', 'v2', projectId] })
@@ -2332,9 +2326,8 @@ watch(
const projectTypeDisplay = computed(() => {
if (!project.value) return ''
return formatProjectType(
data.$getProjectTypeForDisplay(project.value.project_type, project.value.loaders),
)
const projectType = isServerProject.value ? 'minecraft_java_server' : project.value.project_type
return formatProjectType(data.$getProjectTypeForDisplay(projectType, project.value.loaders))
})
const following = computed(() => {
@@ -2372,7 +2365,7 @@ useHead({
],
})
if (!route.name.startsWith('type-id-settings')) {
if (!route.name.startsWith('type-project-settings')) {
useSeoMeta({
title: () => title.value,
description: () => description.value,
@@ -2460,7 +2453,6 @@ async function patchProject(resData, quiet = false) {
{ projectId: project.value.id, data: resData },
{
onSuccess: async () => {
await updateProjectRoute()
if (!quiet) {
addNotification({
title: formatMessage(messages.projectUpdated),

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