Compare commits

..
Author SHA1 Message Date
Calum H. (IMB11) fa03a92dd0 feat: doc change to trigger ci 2026-05-07 00:02:52 +01:00
305 changed files with 6832 additions and 16721 deletions
+1 -1
View File
@@ -164,7 +164,7 @@ jobs:
with:
context: ./apps/labrinth/docker-stage
file: ./apps/labrinth/Dockerfile
push: true
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }}
annotations: ${{ steps.docker-meta.outputs.annotations }}
Generated
+24
View File
@@ -3059,6 +3059,29 @@ dependencies = [
"serde",
]
[[package]]
name = "elasticsearch"
version = "9.1.0-alpha.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12bb303aa6e1d28c0c86b6fbfe484fd0fd3f512629aeed1ac4f6b85f81d9834a"
dependencies = [
"base64 0.22.1",
"bytes",
"dyn-clone",
"flate2",
"lazy_static",
"parking_lot",
"percent-encoding",
"reqwest 0.12.24",
"rustc_version",
"serde",
"serde_json",
"serde_with",
"tokio",
"url",
"void",
]
[[package]]
name = "elliptic-curve"
version = "0.13.8"
@@ -5252,6 +5275,7 @@ dependencies = [
"dotenv-build",
"dotenvy",
"either",
"elasticsearch",
"eyre",
"futures",
"futures-util",
+1
View File
@@ -77,6 +77,7 @@ dotenv-build = "0.1.1"
dotenvy = "0.15.7"
dunce = "1.0.5"
either = "1.15.0"
elasticsearch = "9.1.0-alpha.1"
encoding_rs = "0.8.35"
enumset = "1.1.10"
eyre = "0.6.12"
+1 -1
View File
@@ -20,7 +20,7 @@
"@modrinth/utils": "workspace:*",
"@sentry/vue": "^8.27.0",
"@sfirew/minecraft-motd-parser": "^1.1.6",
"@tanstack/vue-query": "5.90.7",
"@tanstack/vue-query": "^5.90.7",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.4.5",
+4 -56
View File
@@ -2,7 +2,6 @@
import { Intercom, shutdown as shutdownIntercom } from '@intercom/messenger-js-sdk'
import {
AuthFeature,
ModrinthApiError,
NodeAuthFeature,
nodeAuthState,
PanelVersionFeature,
@@ -52,10 +51,9 @@ import {
providePageContext,
providePopupNotificationManager,
useDebugLogger,
useFormatBytes,
useVIntl,
} from '@modrinth/ui'
import { renderString } from '@modrinth/utils'
import { formatBytes, renderString } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { getVersion } from '@tauri-apps/api/app'
import { invoke } from '@tauri-apps/api/core'
@@ -99,7 +97,6 @@ import { command_listener, warning_listener } from '@/helpers/events.js'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
import { create_profile_and_install_from_file } from '@/helpers/pack'
import { list } from '@/helpers/profile.js'
import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
import { get_opening_command, initialize_state } from '@/helpers/state'
import {
@@ -109,7 +106,6 @@ import {
getUpdateSize,
isDev,
isNetworkMetered,
setRestartAfterPendingUpdate,
} from '@/helpers/utils.js'
import i18n from '@/i18n.config'
import { createContentInstall, provideContentInstall } from '@/providers/content-install'
@@ -150,9 +146,8 @@ const popupNotificationManager = new AppPopupNotificationManager()
providePopupNotificationManager(popupNotificationManager)
const { addPopupNotification } = popupNotificationManager
const appVersion = getVersion()
const tauriApiClient = new TauriModrinthClient({
userAgent: async () => `modrinth/theseus/${await appVersion} (support@modrinth.com)`,
userAgent: `modrinth/theseus/${getVersion()} (support@modrinth.com)`,
labrinthBaseUrl: config.labrinthBaseUrl,
archonBaseUrl: config.archonBaseUrl,
features: [
@@ -266,8 +261,6 @@ onUnmounted(async () => {
})
const { formatMessage } = useVIntl()
const formatBytes = useFormatBytes()
const messages = defineMessages({
updateInstalledToastTitle: {
id: 'app.update.complete-toast.title',
@@ -1026,40 +1019,11 @@ async function downloadUpdate(versionToDownload) {
async function installUpdate() {
restarting.value = true
try {
await setRestartAfterPendingUpdate(true)
} catch (e) {
restarting.value = false
handleError(e)
return
}
setTimeout(async () => {
await handleClose()
}, 250)
}
async function openModrinthProjectLinkInApp(parsed) {
const { slug, pathSuffix, url } = parsed
const loadToken = loading.begin()
try {
const { id } = await tauriApiClient.labrinth.projects_v2.check(slug)
const query = mergeUrlQuery(route.query, url)
await router.push({
path: `/project/${id}${pathSuffix}`,
query,
hash: url.hash || undefined,
})
} catch (err) {
if (err instanceof ModrinthApiError && err.statusCode === 404) {
openUrl(url.href)
} else {
handleError(err)
}
} finally {
loading.end(loadToken)
}
}
function handleClick(e) {
let target = e.target
while (target != null) {
@@ -1072,12 +1036,7 @@ function handleClick(e) {
!target.href.startsWith('https://tauri.localhost') &&
!target.href.startsWith('http://tauri.localhost')
) {
const parsed = parseModrinthLink(target.href)
if (target.target !== '_blank' && parsed) {
void openModrinthProjectLinkInApp(parsed)
} else {
openUrl(target.href)
}
openUrl(target.href)
}
e.preventDefault()
break
@@ -1608,15 +1567,11 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
.app-grid-navbar {
grid-area: nav;
position: relative;
z-index: 2;
}
.app-grid-statusbar {
grid-area: status;
padding-right: var(--window-controls-width, 0px);
position: relative;
z-index: 2;
}
[data-tauri-drag-region-exclude] {
@@ -1684,12 +1639,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
&.app-contents::before {
box-shadow: none;
}
*,
:deep(*) {
box-shadow: none !important;
--tw-drop-shadow:;
}
}
.app-sidebar::before {
@@ -1708,11 +1657,10 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
height: 100%;
overflow: auto;
overflow-x: hidden;
scrollbar-gutter: stable;
}
.app-contents::before {
z-index: 30;
z-index: 1;
content: '';
position: fixed;
left: var(--left-bar-width);
@@ -149,7 +149,7 @@ const handleOptionsClick = async (args) => {
break
case 'edit':
await router.push({
path: `/instance/${encodeURIComponent(args.item.path)}`,
path: `/instance/${encodeURIComponent(args.item.path)}/`,
})
break
case 'duplicate':
@@ -1,7 +1,6 @@
<script setup>
import { WrenchIcon, XIcon } from '@modrinth/assets'
import { PlusIcon, XIcon } from '@modrinth/assets'
import {
Accordion,
ButtonStyled,
Checkbox,
commonMessages,
@@ -10,7 +9,7 @@ import {
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { save } from '@tauri-apps/plugin-dialog'
import { open } from '@tauri-apps/plugin-dialog'
import { ref } from 'vue'
import { PackageIcon, VersionIcon } from '@/assets/icons'
@@ -41,13 +40,9 @@ const messages = defineMessages({
},
selectFilesLabel: {
id: 'app.export-modal.select-files-label',
defaultMessage: 'Configure which files are included in this export',
defaultMessage: 'Select files and folders to include in pack',
},
exportButton: { id: 'app.export-modal.export-button', defaultMessage: 'Export' },
includeFile: {
id: 'app.export-modal.include-file-accessibility-label',
defaultMessage: 'Include "{file}"?',
},
})
const props = defineProps({
@@ -70,6 +65,7 @@ const exportDescription = ref('')
const versionInput = ref('1.0.0')
const files = ref([])
const folders = ref([])
const showingFiles = ref(false)
const initFiles = async () => {
const newFolders = new Map()
@@ -91,12 +87,7 @@ const initFiles = async () => {
folder.startsWith('modrinth_logs') ||
folder.startsWith('.fabric'),
}))
.filter(
(pathData) =>
!pathData.path.includes('.DS_Store') &&
pathData.path !== 'mods/.connector' &&
!pathData.path.startsWith('mods/.connector/'),
)
.filter((pathData) => !pathData.path.includes('.DS_Store'))
.forEach((pathData) => {
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
if (parent !== '') {
@@ -130,20 +121,15 @@ const exportPack = async () => {
}
})
})
const outputPath = await save({
defaultPath: `${nameInput.value} ${versionInput.value}.mrpack`,
filters: [
{
name: 'Modrinth Modpack',
extensions: ['mrpack'],
},
],
const outputPath = await open({
directory: true,
multiple: false,
})
if (outputPath) {
export_profile_mrpack(
props.instance.path,
outputPath,
outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`,
filesToExport,
versionInput.value,
exportDescription.value,
@@ -156,91 +142,97 @@ const exportPack = async () => {
<template>
<ModalWrapper ref="exportModal" :header="formatMessage(messages.header)">
<div class="flex flex-col gap-4 w-[40rem]">
<div class="grid grid-cols-2 gap-4">
<div class="labeled_input">
<p>{{ formatMessage(messages.modpackNameLabel) }}</p>
<StyledInput
v-model="nameInput"
:icon="PackageIcon"
type="text"
:placeholder="formatMessage(messages.modpackNamePlaceholder)"
clearable
/>
</div>
<div class="labeled_input">
<p>{{ formatMessage(messages.versionNumberLabel) }}</p>
<StyledInput
v-model="versionInput"
:icon="VersionIcon"
type="text"
:placeholder="formatMessage(messages.versionNumberPlaceholder)"
clearable
/>
</div>
</div>
<div class="flex flex-col gap-2">
<p class="m-0">{{ formatMessage(commonMessages.descriptionLabel) }}</p>
<div class="modal-body">
<div class="labeled_input">
<p>{{ formatMessage(messages.modpackNameLabel) }}</p>
<StyledInput
v-model="exportDescription"
multiline
:placeholder="formatMessage(messages.descriptionPlaceholder)"
v-model="nameInput"
:icon="PackageIcon"
type="text"
:placeholder="formatMessage(messages.modpackNamePlaceholder)"
clearable
/>
</div>
<Accordion
class="w-full bg-surface-4 border border-solid border-surface-5 rounded-2xl overflow-clip"
button-class="p-4 w-full border-b border-solid border-b-surface-5 bg-surface-2 -mb-px hover:brightness-[--hover-brightness] group"
>
<template #title>
<span class="flex items-center gap-3 text-contrast group-active:scale-[0.98]">
<WrenchIcon aria-hidden="true" class="size-5 text-secondary" />
Configure which files are included in this export
</span>
</template>
<div class="flex flex-col [&>*:nth-child(even)]:bg-surface-3">
<div v-for="[path, children] in folders" :key="path.name" class="flex flex-col">
<Accordion
class="flex flex-col"
button-class="flex gap-3 pr-4 hover:bg-surface-5 group"
>
<template #title>
<div class="labeled_input">
<p>{{ formatMessage(messages.versionNumberLabel) }}</p>
<StyledInput
v-model="versionInput"
:icon="VersionIcon"
type="text"
:placeholder="formatMessage(messages.versionNumberPlaceholder)"
clearable
/>
</div>
<div class="adjacent-input">
<div class="labeled_input">
<p>{{ formatMessage(commonMessages.descriptionLabel) }}</p>
<StyledInput
v-model="exportDescription"
multiline
:placeholder="formatMessage(messages.descriptionPlaceholder)"
/>
</div>
</div>
<div class="table">
<div class="table-head">
<div class="table-cell row-wise">
{{ formatMessage(messages.selectFilesLabel) }}
<ButtonStyled circular>
<button @click="() => (showingFiles = !showingFiles)">
<PlusIcon v-if="!showingFiles" />
<XIcon v-else />
</button>
</ButtonStyled>
</div>
</div>
<div v-if="showingFiles" class="table-content">
<div v-for="[path, children] in folders" :key="path.name" class="table-row">
<div class="table-cell file-entry">
<div class="file-primary">
<Checkbox
:model-value="children.every((child) => child.selected)"
:indeterminate="
!children.every((child) => child.selected) &&
children.some((child) => child.selected)
"
:description="formatMessage(messages.includeFile, { file: path.name })"
class="pl-4 py-2"
:label="path.name"
class="select-checkbox"
:disabled="children.every((x) => x.disabled)"
@update:model-value="
(newValue) => children.forEach((child) => (child.selected = newValue))
"
@click.stop
/>
<span class="ml-2 group-active:scale-95">{{ path.name }}/</span>
</template>
<div v-for="child in children" :key="child.path">
<Checkbox
v-model="child.selected"
:label="child.name"
class="w-full px-8 py-2 hover:bg-surface-4 text-primary"
:disabled="child.disabled"
v-model="path.showingMore"
class="select-checkbox dropdown"
collapsing-toggle-style
/>
</div>
</Accordion>
<div v-if="path.showingMore" class="file-secondary">
<div v-for="child in children" :key="child.path" class="file-secondary-row">
<Checkbox
v-model="child.selected"
:label="child.name"
class="select-checkbox"
:disabled="child.disabled"
/>
</div>
</div>
</div>
</div>
<div v-for="file in files" :key="file.path" class="table-row">
<div class="table-cell file-entry">
<div class="file-primary">
<Checkbox
v-model="file.selected"
:label="file.name"
:disabled="file.disabled"
class="select-checkbox"
/>
</div>
</div>
</div>
<Checkbox
v-for="file in files"
:key="file.path"
v-model="file.selected"
:label="file.name"
:disabled="file.disabled"
class="w-full px-4 py-2 hover:bg-surface-4 text-primary"
/>
</div>
</Accordion>
<div class="flex items-center justify-end gap-2">
</div>
<div class="button-row push-right">
<ButtonStyled type="outlined">
<button @click="exportModal.hide">
<XIcon />
@@ -257,3 +249,83 @@ const exportPack = async () => {
</div>
</ModalWrapper>
</template>
<style scoped lang="scss">
.modal-body {
display: flex;
flex-direction: column;
gap: var(--gap-md);
}
.labeled_input {
display: flex;
flex-direction: column;
gap: var(--gap-sm);
p {
margin: 0;
}
}
.select-checkbox {
gap: var(--gap-sm);
button.checkbox {
border: none;
}
&.dropdown {
margin-left: auto;
}
}
.table-content {
max-height: 18rem;
overflow-y: auto;
}
.table {
border: 1px solid var(--color-bg);
}
.file-entry {
display: flex;
flex-direction: column;
gap: var(--gap-sm);
}
.file-primary {
display: flex;
align-items: center;
gap: var(--gap-sm);
}
.file-secondary {
margin-left: var(--gap-xl);
display: flex;
flex-direction: column;
gap: var(--gap-sm);
height: 100%;
vertical-align: center;
}
.file-secondary-row {
display: flex;
align-items: center;
gap: var(--gap-sm);
}
.button-row {
display: flex;
gap: var(--gap-sm);
align-items: center;
}
.row-wise {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
</style>
@@ -37,6 +37,6 @@ defineProps({
.progress-bar__fill {
height: 100%;
transition: width 0.3s ease-out;
transition: width 0.3s;
}
</style>
@@ -126,7 +126,7 @@ watch(
function fakeLoadingIncrease() {
if (loadingProgress.value < 95) {
setTimeout(() => {
loadingProgress.value += 2
loadingProgress.value += 1
fakeLoadingIncrease()
}, 5)
}
@@ -22,7 +22,7 @@
hover-color-fill="background"
circular
>
<button class="relative expanded-button close-button" @click="handleClose">
<button class="relative expanded-button" @click="handleClose">
<XIcon />
</button>
</ButtonStyled>
@@ -82,12 +82,8 @@ const handleClose = async () => {
</script>
<style scoped>
.expanded-button::before {
inset: -9px -6px;
inset: -6px;
content: '';
position: absolute;
}
.expanded-button.close-button::before {
inset: -9px -9px -9px -6px;
}
</style>
@@ -1,332 +0,0 @@
import type { Labrinth } from '@modrinth/api-client'
import { CheckIcon, PlayIcon, PlusIcon, StopCircleIcon } from '@modrinth/assets'
import type { CardAction } from '@modrinth/ui'
import { commonMessages, defineMessages, useDebugLogger, useVIntl } from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import type { ComputedRef, Ref } from 'vue'
import { onUnmounted, ref, shallowRef } from 'vue'
import type { Router } from 'vue-router'
import { process_listener } from '@/helpers/events'
import { get_by_profile_path } from '@/helpers/process'
import { kill, list as listInstances } from '@/helpers/profile.js'
import type { GameInstance } from '@/helpers/types'
import { add_server_to_profile, getServerLatency } from '@/helpers/worlds'
import { getServerAddress } from '@/store/install.js'
interface BrowseServerInstance {
name: string
path: string
}
interface ContextMenuHandle {
showMenu: (
event: MouseEvent,
result: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
options: { name: string }[],
) => void
}
interface ContextMenuOptionClick {
option: 'open_link' | 'copy_link'
item: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject
}
export interface UseAppServerBrowseOptions {
instance: Ref<BrowseServerInstance | null>
isFromWorlds: ComputedRef<boolean>
allInstalledIds: ComputedRef<Set<string>>
newlyInstalled: Ref<string[]>
installingServerProjects: Ref<string[]>
playServerProject: (projectId: string) => Promise<void>
showAddServerToInstanceModal: (serverName: string, serverAddress: string) => void
handleError: (error: unknown) => void
router: Router
}
const messages = defineMessages({
addToInstance: {
id: 'app.browse.add-to-instance',
defaultMessage: 'Add to instance',
},
addToInstanceName: {
id: 'app.browse.add-to-instance-name',
defaultMessage: 'Add to {instanceName}',
},
added: {
id: 'app.browse.added',
defaultMessage: 'Added',
},
alreadyAdded: {
id: 'app.browse.already-added',
defaultMessage: 'Already added',
},
})
export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
const { formatMessage } = useVIntl()
const debugLog = useDebugLogger('BrowseServer')
const serverPings = shallowRef<Record<string, number | undefined>>({})
const serverPingCache = new Map<string, number | undefined>()
const pendingServerPings = new Map<string, Promise<number | undefined>>()
const runningServerProjects = ref<Record<string, string>>({})
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
const contextMenuRef = ref<ContextMenuHandle | null>(null)
let serverPingCacheActive = true
let unlistenProcesses: (() => void) | null = null
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
debugLog('checkServerRunningStates', { hitCount: hits.length })
const packs = await listInstances().catch((error) => {
options.handleError(error)
return []
})
const newRunning: Record<string, string> = {}
for (const hit of hits) {
const inst = packs.find(
(pack: GameInstance) => pack.linked_data?.project_id === hit.project_id,
)
if (inst) {
const processes = await get_by_profile_path(inst.path).catch(() => [])
if (Array.isArray(processes) && processes.length > 0) {
newRunning[hit.project_id] = inst.path
}
}
}
debugLog('runningServerProjects updated', newRunning)
runningServerProjects.value = newRunning
}
async function handleStopServerProject(projectId: string) {
debugLog('handleStopServerProject', projectId)
const instancePath = runningServerProjects.value[projectId]
if (!instancePath) return
await kill(instancePath).catch(() => {})
const { [projectId]: _, ...rest } = runningServerProjects.value
runningServerProjects.value = rest
}
async function handlePlayServerProject(projectId: string) {
debugLog('handlePlayServerProject', projectId)
await options.playServerProject(projectId)
checkServerRunningStates(lastServerHits.value)
}
async function handleAddServerToInstance(project: Labrinth.Search.v3.ResultSearchProject) {
debugLog('handleAddServerToInstance', { projectId: project.project_id, name: project.name })
const address = getServerAddress(project.minecraft_java_server)
if (!address) return
if (options.instance.value) {
try {
await add_server_to_profile(
options.instance.value.path,
project.name,
address,
'prompt',
project.project_id,
project.minecraft_java_server?.content?.kind,
)
options.newlyInstalled.value.push(project.project_id)
} catch (error) {
options.handleError(error)
}
} else {
options.showAddServerToInstanceModal(project.name, address)
}
}
async function pingServerHits(hits: Labrinth.Search.v3.ResultSearchProject[]) {
debugLog('pingServerHits', { hitCount: hits.length })
const pingsToFetch = hits.flatMap((hit) => {
const address = hit.minecraft_java_server?.address
if (!address) return []
return [{ hit, address }]
})
const nextPings = { ...serverPings.value }
for (const { hit, address } of pingsToFetch) {
if (serverPingCache.has(address)) {
nextPings[hit.project_id] = serverPingCache.get(address)
}
}
serverPings.value = nextPings
await Promise.all(
pingsToFetch.map(async ({ hit, address }) => {
if (serverPingCache.has(address)) return
let pending = pendingServerPings.get(address)
if (!pending) {
pending = getServerLatency(address)
.then((latency) => {
if (serverPingCacheActive) serverPingCache.set(address, latency)
return latency
})
.catch((error) => {
console.error(`Failed to ping server ${address}:`, error)
if (serverPingCacheActive) serverPingCache.set(address, undefined)
return undefined
})
.finally(() => {
pendingServerPings.delete(address)
})
pendingServerPings.set(address, pending)
}
const latency = await pending
if (!serverPingCacheActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
}),
)
}
function updateServerHits(hits: Labrinth.Search.v3.ResultSearchProject[]) {
lastServerHits.value = hits
pingServerHits(hits)
checkServerRunningStates(hits)
}
function getServerModpackContent(project: Labrinth.Search.v3.ResultSearchProject) {
const content = project.minecraft_java_server?.content
if (content?.kind === 'modpack') {
const { project_name, project_icon, project_id } = content
if (!project_name) return undefined
return {
name: project_name,
icon: project_icon ?? undefined,
onclick:
project_id !== project.project_id
? () => {
options.router.push(`/project/${project_id}`)
}
: undefined,
showCustomModpackTooltip: project_id === project.project_id,
}
}
return undefined
}
function getServerCardActions(
serverResult: Labrinth.Search.v3.ResultSearchProject,
): CardAction[] {
const isInstalled = options.allInstalledIds.value.has(serverResult.project_id)
if (options.isFromWorlds.value && options.instance.value) {
return [
{
key: 'add-to-instance',
label: formatMessage(isInstalled ? messages.added : messages.addToInstance),
icon: isInstalled ? CheckIcon : PlusIcon,
disabled: isInstalled,
color: 'brand',
type: 'outlined',
onClick: () => handleAddServerToInstance(serverResult),
},
]
}
const actions: CardAction[] = []
actions.push({
key: 'add',
label: '',
icon: isInstalled ? CheckIcon : PlusIcon,
disabled: isInstalled,
circular: true,
tooltip: isInstalled
? formatMessage(messages.alreadyAdded)
: options.instance.value
? formatMessage(messages.addToInstanceName, {
instanceName: options.instance.value.name,
})
: formatMessage(commonMessages.addServerToInstanceButton),
onClick: () => handleAddServerToInstance(serverResult),
})
if (runningServerProjects.value[serverResult.project_id]) {
actions.push({
key: 'stop',
label: formatMessage(commonMessages.stopButton),
icon: StopCircleIcon,
color: 'red',
type: 'outlined',
onClick: () => handleStopServerProject(serverResult.project_id),
})
} else {
const isInstalling = options.installingServerProjects.value.includes(serverResult.project_id)
actions.push({
key: 'play',
label: formatMessage(
isInstalling ? commonMessages.installingLabel : commonMessages.playButton,
),
icon: PlayIcon,
disabled: isInstalling,
color: 'brand',
type: 'outlined',
onClick: () => handlePlayServerProject(serverResult.project_id),
})
}
return actions
}
function handleRightClick(
event: MouseEvent,
result: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
) {
contextMenuRef.value?.showMenu(event, result, [{ name: 'open_link' }, { name: 'copy_link' }])
}
function handleOptionsClick(args: ContextMenuOptionClick) {
const url = getProjectUrl(args.item)
switch (args.option) {
case 'open_link':
openUrl(url)
break
case 'copy_link':
navigator.clipboard.writeText(url)
break
}
}
process_listener((event: { event: string; profile_path_id: string }) => {
debugLog('process event', event)
if (event.event === 'finished') {
const projectId = Object.entries(runningServerProjects.value).find(
([, path]) => path === event.profile_path_id,
)?.[0]
if (projectId) {
const { [projectId]: _, ...rest } = runningServerProjects.value
runningServerProjects.value = rest
}
}
})
.then((unlisten) => {
unlistenProcesses = unlisten
})
.catch(options.handleError)
onUnmounted(() => {
serverPingCacheActive = false
unlistenProcesses?.()
serverPingCache.clear()
pendingServerPings.clear()
})
return {
serverPings,
contextMenuRef,
updateServerHits,
getServerModpackContent,
getServerCardActions,
handleRightClick,
handleOptionsClick,
}
}
function getProjectUrl(
item: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
) {
const projectType = 'project_types' in item ? item.project_types?.[0] : item.project_type
return `https://modrinth.com/${projectType ?? 'project'}/${item.slug ?? item.project_id}`
}
@@ -36,38 +36,18 @@ export function useIntercomPositioning({
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const defaultIntercomBubbleHorizontalPadding = computed(() =>
const intercomBubbleHorizontalPadding = 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)
@@ -113,7 +93,6 @@ export function useIntercomPositioning({
intercomBubble: {
width: ref(INTERCOM_BUBBLE_WIDTH),
horizontalPadding: intercomBubbleHorizontalPadding,
requestHorizontalPadding: requestIntercomBubbleHorizontalPadding,
requestVerticalClearance: requestIntercomBubbleVerticalClearance,
},
},
+1 -1
View File
@@ -184,7 +184,7 @@ export async function update_project(path: string, projectPath: string): Promise
// Add a project to a profile from a version
// Returns a path to the new project file
export type DownloadReason = 'standalone' | 'dependency' | 'modpack' | 'update'
export type DownloadReason = 'standalone' | 'dependency' | 'modpack'
export async function add_project_from_version(
path: string,
@@ -1,83 +0,0 @@
import type { LocationQuery, LocationQueryRaw } from 'vue-router'
const MODRINTH_HOSTNAMES = new Set(['modrinth.com', 'www.modrinth.com'])
const SUPPORTED_PROJECT_TYPES = new Set([
'mod',
'modpack',
'resourcepack',
'datapack',
'plugin',
'shader',
'server',
'project',
])
export function parseModrinthLink(
href: string,
): { slug: string; pathSuffix: string; url: URL } | null {
let url: URL
try {
url = new URL(href)
} catch {
return null
}
if (!MODRINTH_HOSTNAMES.has(url.hostname.toLowerCase())) {
return null
}
const segments = url.pathname.split('/').filter((p) => p.length > 0)
if (segments.length < 2) {
return null
}
if (SUPPORTED_PROJECT_TYPES.has(segments[0].toLowerCase())) {
const slug = segments[1]
if (!slug) {
return null
}
const rest: string[] = segments.slice(2)
const pathSuffix = toValidAppSubpath(rest)
if (pathSuffix === null) {
return null
}
return { slug, pathSuffix, url }
} else {
return null
}
}
const SUPPORTED_SUBPATHS = ['versions', 'gallery']
function toValidAppSubpath(rest: string[]): string | null {
if (rest.length === 0) {
return ''
}
const subroute = rest[0].toLowerCase()
if (rest.length === 1 && SUPPORTED_SUBPATHS.includes(subroute)) {
return `/${subroute}`
}
if (rest.length === 2 && subroute === 'version') {
return `/version/${rest[1]}`
}
return null
}
export function mergeUrlQuery(routeQuery: LocationQuery, linkUrl: URL): LocationQueryRaw {
const newQuery: LocationQueryRaw = { ...routeQuery }
const keys = new Set<string>()
linkUrl.searchParams.forEach((_value, key) => {
keys.add(key)
})
for (const key of keys) {
const values = linkUrl.searchParams.getAll(key)
newQuery[key] = values.length === 1 ? values[0] : values
}
return newQuery
}
-4
View File
@@ -22,10 +22,6 @@ export async function removeEnqueuedUpdate() {
return await invoke('remove_enqueued_update')
}
export async function setRestartAfterPendingUpdate(should_restart) {
return await invoke('set_restart_after_pending_update', { shouldRestart: should_restart })
}
// One of 'Windows', 'Linux', 'MacOS'
export async function getOS() {
return await invoke('plugin:utils|get_os')
+12 -6
View File
@@ -1,16 +1,16 @@
{
"app.action-bar.downloading-java": {
"message": "جاز تنزيل إصدار جافا {version}"
},
"app.action-bar.downloads": {
"message": "التنزيلات"
},
"app.auth-servers.unreachable.body": {
"message": "قد تكون خوادم مصادقة ماينكرافت معطلة حاليًا. تحقق من اتصالك بالإنترنت وحاول مرة أخرى لاحقًا."
},
"app.auth-servers.unreachable.header": {
"message": "تعذر الوصول إلى خوادم المصادقة"
},
"app.browse.add-server-to-instance": {
"message": "أضف الخادم للنموذج"
},
"app.browse.add-servers-to-instance": {
"message": "أضف الخوادم لنموذجك"
},
"app.browse.add-to-instance": {
"message": "أضف للنموذج"
},
@@ -29,6 +29,9 @@
"app.browse.discover-servers": {
"message": "استكشف خوادم"
},
"app.browse.install-content-to-instance": {
"message": "تثبيت محتوى لنموذجك"
},
"app.browse.server.installing": {
"message": "جاري التثبيت"
},
@@ -47,6 +50,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "إسم حزمة التعديل"
},
"app.export-modal.select-files-label": {
"message": "حدد الملفات والمجلدات المراد تضمينها في الحزمة"
},
"app.export-modal.version-number-label": {
"message": "رقم الإصدار"
},
@@ -5,6 +5,12 @@
"app.auth-servers.unreachable.header": {
"message": "Připojení k autorizačním serverům se nezdařilo"
},
"app.browse.add-server-to-instance": {
"message": "Přidat server do instance"
},
"app.browse.add-servers-to-instance": {
"message": "Přidat servery do instance"
},
"app.browse.add-to-instance": {
"message": "Přidat do instalace"
},
@@ -23,6 +29,9 @@
"app.browse.discover-servers": {
"message": "Prozkoumat servery"
},
"app.browse.install-content-to-instance": {
"message": "Nainstalovat obsah do instnce"
},
"app.export-modal.description-placeholder": {
"message": "Přidej popis modpacku..."
},
@@ -38,6 +47,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Jméno modpacku"
},
"app.export-modal.select-files-label": {
"message": "Vybrat soubory a složky co zahrnout do modpacku"
},
"app.export-modal.version-number-label": {
"message": "Číslo verze"
},
+3 -84
View File
@@ -1,88 +1,4 @@
{
"app.action-bar.downloading-java": {
"message": "Downloader Java {version}"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Gem flere kørende instances"
},
"app.action-bar.make-primary-instance": {
"message": "Gør til primære instance"
},
"app.action-bar.no-instances-running": {
"message": "Ingen instances kørende"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Primære instance"
},
"app.action-bar.show-more-running-instances": {
"message": "Vis flere kørende instances"
},
"app.action-bar.stop-instance": {
"message": "Stop instance"
},
"app.action-bar.view-active-downloads": {
"message": "Vis aktive downloads"
},
"app.action-bar.view-instance": {
"message": "Vis instance"
},
"app.action-bar.view-logs": {
"message": "Vis logs"
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Avanceret rendering"
},
"app.appearance-settings.color-theme.description": {
"message": "Vælg dit foretrukne farvetema til Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Farvetema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Skift hvilken side launcheren åbner i."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Hjem"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliotek"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Standard startside"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Slår nametagget over din spiller på skins siden fra."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Gem nametag"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Inkluderer de sidste verdener i afsnittet \"Hop tilbage\" på startsiden."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Hop tilbage ind i verdener"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimer launcheren når Minecraft starter."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimer launcher"
},
"app.appearance-settings.select-option": {
"message": "Vælg en mulighed"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Hvis du prøver at installere en Modrinth Pack fil (.mrpack) som ikke er hosted på Modrinth, så skal vi nok sikre os du forstår riskoen før du installere den."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Advar mig før jeg installere en ukendt modpacks"
},
"app.auth-servers.unreachable.body": {
"message": "Minecraft authentication servere kan måske være nede lige nu. Tjek din internet forbindelse og prøv igen senere."
},
@@ -119,6 +35,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Modpack navn"
},
"app.export-modal.select-files-label": {
"message": "Vælg filer og mapper til at inkludere i pakken"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
},
+15 -21
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Authentifizierungsserver sind nicht erreichbar"
},
"app.browse.add-servers-to-instance": {
"app.browse.add-server-to-instance": {
"message": "Server zu Instanz hinzufügen"
},
"app.browse.add-to-an-instance": {
"message": "Zu Instanz hinzufügen"
"app.browse.add-servers-to-instance": {
"message": "Server zu deiner Instanz hinzufügen"
},
"app.browse.add-to-instance": {
"message": "Zu Instanz hinzufügen"
@@ -122,9 +122,6 @@
"app.browse.already-added": {
"message": "Bereits hinzugefügt"
},
"app.browse.back-to-instance": {
"message": "Zu Instanz zurückgehen"
},
"app.browse.discover-content": {
"message": "Inhalte entdecken"
},
@@ -134,11 +131,20 @@
"app.browse.hide-added-servers": {
"message": "Bereits hinzugefügte Server ausblenden"
},
"app.browse.hide-installed-content": {
"message": "Bereits installierte Inhalte ausblenden"
},
"app.browse.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Das hinzufügen von Inhalten kann Kompatibilitätsprobleme beim beitreten eines Servers verursachen. Sämtliche hinzugefügte Inhalten gehen ausserdem beim aktualisieren der Server Instanz-Inhalte verloren."
"app.browse.server.install": {
"message": "Installieren"
},
"app.browse.server.installed": {
"message": "Installiert"
},
"app.browse.server.installing": {
"message": "Wird installiert"
@@ -158,9 +164,6 @@
"app.export-modal.header": {
"message": "Modpack exportieren"
},
"app.export-modal.include-file-accessibility-label": {
"message": "\"{file}\" einschliessen?"
},
"app.export-modal.modpack-name-label": {
"message": "Modpaketname"
},
@@ -168,7 +171,7 @@
"message": "Modpaketname"
},
"app.export-modal.select-files-label": {
"message": "Konfiguriere, welche Dateien in diesem Export miteinbezogen werden"
"message": "Wähle Dateien und Ordner zum hinzufügen im Paket aus"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
@@ -299,15 +302,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "Eine aktualisierung zum spielen von {name} ist benötigt. Bitte aktualisiere auf die neuste Version um das Spiel zu starten."
},
"app.project.install-button.already-installed": {
"message": "Dieses Projekt ist bereits installiert"
},
"app.project.install-context.back-to-browse": {
"message": "Zurück zum Durchstöbern"
},
"app.project.install-context.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
},
"app.settings.developer-mode-enabled": {
"message": "Entwicklermodus aktiviert."
},
+18 -18
View File
@@ -12,7 +12,7 @@
"message": "Zur primären Instanz machen"
},
"app.action-bar.no-instances-running": {
"message": "Keine aktiven Instanzen"
"message": "Keine Instanzen laufen"
},
"app.action-bar.offline": {
"message": "Offline"
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Authentifizierungsserver sind nicht erreichbar"
},
"app.browse.add-servers-to-instance": {
"message": "Server zu Instanz hinzufügen"
"app.browse.add-server-to-instance": {
"message": "Server zur Instanz hinzufügen"
},
"app.browse.add-to-an-instance": {
"message": "Zu Instanz hinzufügen"
"app.browse.add-servers-to-instance": {
"message": "Server zu deiner Instanz hinzufügen"
},
"app.browse.add-to-instance": {
"message": "Zur Instanz hinzufügen"
@@ -122,9 +122,6 @@
"app.browse.already-added": {
"message": "Bereits hinzugefügt"
},
"app.browse.back-to-instance": {
"message": "Zurück zur Instanz"
},
"app.browse.discover-content": {
"message": "Inhalte entdecken"
},
@@ -134,9 +131,21 @@
"app.browse.hide-added-servers": {
"message": "Bereits hinzugefügte Server ausblenden"
},
"app.browse.hide-installed-content": {
"message": "Bereits installierte Inhalte ausblenden"
},
"app.browse.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server.install": {
"message": "Installieren"
},
"app.browse.server.installed": {
"message": "Installiert"
},
"app.browse.server.installing": {
"message": "Wird installiert"
},
@@ -155,9 +164,6 @@
"app.export-modal.header": {
"message": "Modpack exportieren"
},
"app.export-modal.include-file-accessibility-label": {
"message": "\"{file}\" einschließen?"
},
"app.export-modal.modpack-name-label": {
"message": "Modpackname"
},
@@ -165,7 +171,7 @@
"message": "Modpackname"
},
"app.export-modal.select-files-label": {
"message": "Konfiguriere, welche Dateien in diesen Export enthalten sind"
"message": "Wähle Dateien und Ordner aus, die in das Paket sollen"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
@@ -296,12 +302,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "Zum Spielen von {name} ist eine Aktualisierung erforderlich. Bitte aktualisiere auf die neueste Version, um das Spiel zu starten."
},
"app.project.install-button.already-installed": {
"message": "Dieses Projekt ist bereits installiert"
},
"app.project.install-context.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
},
"app.settings.developer-mode-enabled": {
"message": "Entwicklermodus aktiviert."
},
+16 -22
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Cannot reach authentication servers"
},
"app.browse.add-servers-to-instance": {
"message": "Adding server to instance"
"app.browse.add-server-to-instance": {
"message": "Add server to instance"
},
"app.browse.add-to-an-instance": {
"message": "Add to an instance"
"app.browse.add-servers-to-instance": {
"message": "Add servers to your instance"
},
"app.browse.add-to-instance": {
"message": "Add to instance"
@@ -122,9 +122,6 @@
"app.browse.already-added": {
"message": "Already added"
},
"app.browse.back-to-instance": {
"message": "Back to instance"
},
"app.browse.discover-content": {
"message": "Discover content"
},
@@ -134,11 +131,20 @@
"app.browse.hide-added-servers": {
"message": "Hide already added servers"
},
"app.browse.hide-installed-content": {
"message": "Hide already installed content"
},
"app.browse.install-content-to-instance": {
"message": "Install content to instance"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Adding content can break compatibility when joining the server. Any added content will also be lost when you update the server instance content."
"app.browse.server.install": {
"message": "Install"
},
"app.browse.server.installed": {
"message": "Installed"
},
"app.browse.server.installing": {
"message": "Installing"
@@ -158,9 +164,6 @@
"app.export-modal.header": {
"message": "Export modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Include \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Modpack Name"
},
@@ -168,7 +171,7 @@
"message": "Modpack name"
},
"app.export-modal.select-files-label": {
"message": "Configure which files are included in this export"
"message": "Select files and folders to include in pack"
},
"app.export-modal.version-number-label": {
"message": "Version number"
@@ -299,15 +302,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "An update is required to play {name}. Please update to the latest version to launch the game."
},
"app.project.install-button.already-installed": {
"message": "This project is already installed"
},
"app.project.install-context.back-to-browse": {
"message": "Back to discover"
},
"app.project.install-context.install-content-to-instance": {
"message": "Install content to instance"
},
"app.settings.developer-mode-enabled": {
"message": "Developer mode enabled."
},
+47 -50
View File
@@ -12,43 +12,43 @@
"message": "Hacer instancia principal"
},
"app.action-bar.no-instances-running": {
"message": "No hay instancias en ejecución"
"message": "Ninguna instancia en ejecución"
},
"app.action-bar.offline": {
"message": "Desconectado"
"message": "Sin conexión"
},
"app.action-bar.primary-instance": {
"message": "Instancia principal"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar más instancias en ejecución"
"message": "Mostrar mas instancias en ejecución"
},
"app.action-bar.stop-instance": {
"message": "Detener instancia"
"message": "Finalizar instancia"
},
"app.action-bar.view-active-downloads": {
"message": "Ver descargas en curso"
"message": "Mostrar descargas activas"
},
"app.action-bar.view-instance": {
"message": "Ver instancia"
"message": "Mostrar instancia"
},
"app.action-bar.view-logs": {
"message": "Ver registros"
"message": "Mostrar registros"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Activa el renderizado avanzado, como los efectos de desenfoque, que pueden afectar el rendimiento sin aceleración por hardware."
"message": "Habilita el renderizado avanzado, como los efectos de desenfoque, que pueden causar problemas de rendimiento sin renderizado acelerado por hardware."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Renderizado avanzado"
},
"app.appearance-settings.color-theme.description": {
"message": "Selecciona el tema de color que prefieras para la Modrinth App."
"message": "Seleccione su tema de color preferido para Modrinth en este dispositivo."
},
"app.appearance-settings.color-theme.title": {
"message": "Color de la interfaz"
"message": "Tema de color"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Cambia la página en la que se abre el launcher."
"message": "Cambia la página en la que el launcher se abre en."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Inicio"
@@ -57,28 +57,28 @@
"message": "Librería"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Página de inicio predeterminada"
"message": "Página predeterminada"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Desactiva la etiqueta de nombre arriba de tu personaje en la página de skins."
"message": "Desactiva la etiqueta arriba de tu personaje en la página de skins."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Ocultar etiqueta de nombre"
"message": "Ocultar etiqueta"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Incluye los mundos recientes en la sección \"Volver a jugar\" de la página de inicio."
"message": "Incluye los mundos más recientes en la sección \"Vuelve a jugar tus mundos\" en la página de inicio."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Volver a jugar mundos"
"message": "Vuelve a jugar tus mundos"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimiza el launcher al iniciar un proceso de Minecraft."
"message": "Minimiza el launcher cuando un proceso de Minecraft empieza."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimizar launcher"
"message": "Minimiza el launcher"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usar el borde de ventana del sistema (requiere reiniciar la aplicación)."
"message": "Usa los sistemas de frame de Windows (se requiere resetear la aplicación)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas"
@@ -87,16 +87,16 @@
"message": "Selecciona una opción"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Permite mostrar u ocultar la barra lateral."
"message": "Activa la posibilidad de alternar la barra lateral."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Mostrar u ocultar la barra lateral"
"message": "Alternar barra lateral"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Si intentas instalar un paquete de Modrinth (.mrpack) que no está alojado en Modrinth, te explicaremos los riesgos antes de instalarlo."
"message": "Si intentas instalar un archivo de paquete de Modrinth (.mrpack) que no esté alojado en Modrinth, te advertiremos de los riesgos antes de instalarlo."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Advertir antes de instalar modpacks desconocidos"
"message": "Adviérteme antes de instalarme modpacks desconocidos"
},
"app.auth-servers.unreachable.body": {
"message": "Los servidores de autenticación de Minecraft pueden no estar funcionando en este momento. Verifica tu conexión a internet e inténtalo de nuevo más tarde."
@@ -104,6 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "No se puede acceder a los servidores de autenticación"
},
"app.browse.add-server-to-instance": {
"message": "Añadir servidor a instancia"
},
"app.browse.add-servers-to-instance": {
"message": "Añade servidores a tu instancia"
},
"app.browse.add-to-instance": {
"message": "Añadir a instancia"
},
@@ -125,9 +131,21 @@
"app.browse.hide-added-servers": {
"message": "Ocultar servidores ya añadidos"
},
"app.browse.hide-installed-content": {
"message": "Ocultar contenido instalado"
},
"app.browse.install-content-to-instance": {
"message": "Instalar contenido a la instancia"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server.install": {
"message": "Instalar"
},
"app.browse.server.installed": {
"message": "Instalado"
},
"app.browse.server.installing": {
"message": "Instalando"
},
@@ -135,7 +153,7 @@
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Instalando modpack..."
"message": "Instalando el modpack..."
},
"app.export-modal.description-placeholder": {
"message": "Introduce la descripción del modpack..."
@@ -146,9 +164,6 @@
"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"
},
@@ -156,7 +171,7 @@
"message": "Nombre del modpack"
},
"app.export-modal.select-files-label": {
"message": "Configura que archivos incluir en esta exportación"
"message": "Selecciona archivos y carpetas para incluirlos en el pack"
},
"app.export-modal.version-number-label": {
"message": "Número de la versión"
@@ -255,7 +270,7 @@
"message": "Contenido requerido"
},
"app.modal.install-to-play.header": {
"message": "Instalar para jugar"
"message": "Instala para jugar"
},
"app.modal.install-to-play.install-button": {
"message": "Instalar"
@@ -279,7 +294,7 @@
"message": "Ver contenidos"
},
"app.modal.update-to-play.header": {
"message": "Actualizar para jugar"
"message": "Actualiza para jugar"
},
"app.modal.update-to-play.update-required": {
"message": "Actualización requerida"
@@ -677,24 +692,6 @@
"instance.worlds.world_in_use": {
"message": "El mundo ya está en uso"
},
"minecraft-account.add-account": {
"message": "Añadir cuenta"
},
"minecraft-account.label": {
"message": "Cuenta de Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "No has inciado sesión"
},
"minecraft-account.remove-account": {
"message": "Eliminar cuenta"
},
"minecraft-account.select-account": {
"message": "Seleccionar cuenta"
},
"minecraft-account.sign-in": {
"message": "Inicia sesión en Minecraft"
},
"search.filter.locked.instance": {
"message": "Proporcionado por la instancia"
},
@@ -723,7 +720,7 @@
"message": "Un archivo solo es revisado si es subido a Modrinth, independientemente de su formato de archivo (incluyendo .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "No volver a mostrar esta advertencia"
"message": "No mostrar esta advertencia otra vez"
},
"unknown-pack-warning-modal.header": {
"message": "Confirmar instalación"
@@ -732,7 +729,7 @@
"message": "Instalar de todos modos"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "El malware a menudo es distribuido mediante modpacks compartidos en aplicaciones como Discord."
"message": "El malware es normalmente distribuido por archivos de modpack que son normalmente compartidas en aplicaciones como Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "No pudimos encontrar este archivo en Modrinth. Recomendamos solo instalar archivos de sitios de confianza."
@@ -104,6 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "No se puede conectar con los servidores de autenticación"
},
"app.browse.add-server-to-instance": {
"message": "Añadir servidor a la instancia"
},
"app.browse.add-servers-to-instance": {
"message": "Añadir servidor a tu instancia"
},
"app.browse.add-to-instance": {
"message": "Añadir a la instancia"
},
@@ -125,9 +131,21 @@
"app.browse.hide-added-servers": {
"message": "Ocultar servidores ya añadidos"
},
"app.browse.hide-installed-content": {
"message": "Ocultar contenido instalado"
},
"app.browse.install-content-to-instance": {
"message": "Instalar contenido a una instancia"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server.install": {
"message": "Instalar"
},
"app.browse.server.installed": {
"message": "Instalado"
},
"app.browse.server.installing": {
"message": "Instalando"
},
@@ -152,6 +170,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Nombre del modpack"
},
"app.export-modal.select-files-label": {
"message": "Seleccione archivos y carpetas para incluir en el paquete"
},
"app.export-modal.version-number-label": {
"message": "Número de versión"
},
@@ -5,6 +5,12 @@
"app.auth-servers.unreachable.header": {
"message": "Todennuspalvelimiin ei saada yhteyttä"
},
"app.browse.add-server-to-instance": {
"message": "Lisää palvelin instanssiin"
},
"app.browse.add-servers-to-instance": {
"message": "Lisää palvelimia instanssiisi"
},
"app.browse.add-to-instance": {
"message": "Lisää instanssiin"
},
@@ -23,6 +29,9 @@
"app.browse.discover-servers": {
"message": "Löydä palvelimia"
},
"app.browse.install-content-to-instance": {
"message": "Lataa sisältöä instanssiin"
},
"app.export-modal.description-placeholder": {
"message": "Lisää modipaketin kuvaus..."
},
@@ -38,6 +47,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Modipaketin nimi"
},
"app.export-modal.select-files-label": {
"message": "Valitse tiedostot ja kansiot pakettiin"
},
"app.export-modal.version-number-label": {
"message": "Versio numero"
},
+9 -189
View File
@@ -1,109 +1,16 @@
{
"app.action-bar.downloading-java": {
"message": "Dina-download ang Java {version}"
},
"app.action-bar.downloads": {
"message": "Mga downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Itago ang mas marami pang tumatakbong instansiya"
},
"app.action-bar.make-primary-instance": {
"message": "Gumawa ng pangunahing instansiya"
},
"app.action-bar.no-instances-running": {
"message": "Walang instansiya ang tumatakbo"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Pangunahing instansiya"
},
"app.action-bar.show-more-running-instances": {
"message": "Ipakita ang mas marami pang tumatakbong instansiya"
},
"app.action-bar.stop-instance": {
"message": "Itigil ang instansiya"
},
"app.action-bar.view-active-downloads": {
"message": "Tignan ang mga active downloads"
},
"app.action-bar.view-instance": {
"message": "Tignan ang instansiya"
},
"app.action-bar.view-logs": {
"message": "Tignan ang logs"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Pagpapagana ng masalimuot na pagrerender katulad ng paglalabo na maaaring magdudulot ng mga isyu sa performance kapag walang hardware-acceleration."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Masalimuot na pagrerender"
},
"app.appearance-settings.color-theme.description": {
"message": "Piliin ang iyong mas gustong kulay para sa Modrinth app."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema ng kulay"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Pagpapalit ng pahina na bubuksan ng launcher."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Tahanan"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Librarya"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Panimulang pahina ng paglapag"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Pagtatago ng nametag na nasa itaas ng iyong manlalaro sa loob ng pahina ng mga skin."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Itago ang nametag"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Pagsasali ng mga kamakailang mundo sa seksiyong \"Jump back in\" sa pahina ng Tahanan."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Bumalik sa mga mundo"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Liitan ang launcher kung nag-start ang isang proseso ng Minecraft."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Liitin ang launcher"
},
"app.appearance-settings.native-decorations.description": {
"message": "Gamitin ang system window frame (kailangan i-restart ang app)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Native decorations"
},
"app.appearance-settings.select-option": {
"message": "Pumili ng opsiyon"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Pagpapagana na mata-toggle ang sidebar."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "I-toggle ang sidebar"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Kung susubukan mong mag-install ng Modrinth Pack file (.mrpack) na hindi naka-host sa Modrinth, titiyakin namin na nakaaalam ka sa mga panganib bago ito ma-install."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Pakipaalala bago ko ma-install ang mga di-kilalang modpack"
},
"app.auth-servers.unreachable.body": {
"message": "Maaaring hindi maaabot ang mga authentication server ng Minecraft sa ngayon. Tingnan mo ang iyong internet connection at muling subukan mamaya."
},
"app.auth-servers.unreachable.header": {
"message": "Hindi maabot ang mga authentication server"
},
"app.browse.add-server-to-instance": {
"message": "Idagdag ang server sa instansiya"
},
"app.browse.add-servers-to-instance": {
"message": "Idagdag ang mga server sa iyong instansiya"
},
"app.browse.add-to-instance": {
"message": "Idagdag sa instansiya"
},
@@ -122,20 +29,8 @@
"app.browse.discover-servers": {
"message": "Tumuklas ng mga server"
},
"app.browse.hide-added-servers": {
"message": "Itago ang mga nailagay na mga servers"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server.installing": {
"message": "Ini-install"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Ini-install ang modpack..."
"app.browse.install-content-to-instance": {
"message": "I-install ang kontento sa instansiya"
},
"app.export-modal.description-placeholder": {
"message": "Ilagay ang paglalarawan ng modpack..."
@@ -146,9 +41,6 @@
"app.export-modal.header": {
"message": "Iluwas ang modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Salihin ang \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Pangalan ng Modpack"
},
@@ -156,7 +48,7 @@
"message": "Pangalan ng modpack"
},
"app.export-modal.select-files-label": {
"message": "Isaayos kung anong mga file ang isasali sa pagluwas"
"message": "Pumili ng mga talaksan at folder na isasali sa pack"
},
"app.export-modal.version-number-label": {
"message": "Numero ng bersiyon"
@@ -209,15 +101,6 @@
"app.instance.worlds.add-server": {
"message": "Magdagdag ng server"
},
"app.instance.worlds.browse-servers": {
"message": "Mag-browse ng servers"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' ay **permanenteng mabubura**, at walang paraan upang maibalik muli."
},
"app.instance.worlds.delete-world-title": {
"message": "Sigurado ka bang gusto mong permanenteng burahin ang world na ito?"
},
"app.instance.worlds.filter-modded": {
"message": "Modded"
},
@@ -230,18 +113,6 @@
"app.instance.worlds.filter-vanilla": {
"message": "Vanilla"
},
"app.instance.worlds.no-worlds-description": {
"message": "Maglagay ng server o mag-browse upang mag-simula"
},
"app.instance.worlds.no-worlds-heading": {
"message": "Walang servers o worlds ang nalalagay"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' ay matatangal sa iyong listahan, pati sa loob ng laro, at walang paraan upang maibalik ito."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) ay matatangal sa iyong listahan, pati sa loob ng laro, at walang paraan upang maibalik ito."
},
"app.instance.worlds.remove-server-title": {
"message": "Sigurado ka bang gusto mong tanggalin ang {name}?"
},
@@ -362,15 +233,9 @@
"app.world.world-item.incompatible-version": {
"message": "Di-magkatugmang bersiyong {version}"
},
"app.world.world-item.not-played-yet": {
"message": "Hindi pa nalalaro"
},
"app.world.world-item.offline": {
"message": "Offline"
},
"app.world.world-item.players-online": {
"message": "{count} online"
},
"friends.action.add-friend": {
"message": "Magdagdag ng kaibigan"
},
@@ -470,12 +335,6 @@
"instance.edit-world.title": {
"message": "Baguhin ang mundo"
},
"instance.files.adding-files": {
"message": "Idinadagdag ang mga file ({completed}/{total})"
},
"instance.files.save-as": {
"message": "I-save bilang..."
},
"instance.server-modal.address": {
"message": "Adres"
},
@@ -677,24 +536,6 @@
"instance.worlds.world_in_use": {
"message": "Ginagamit ang mundo"
},
"minecraft-account.add-account": {
"message": "Maglagay ng account"
},
"minecraft-account.label": {
"message": "Minecraft account"
},
"minecraft-account.not-signed-in": {
"message": "Hindi pa naka sign in"
},
"minecraft-account.remove-account": {
"message": "Itanggal ang account"
},
"minecraft-account.select-account": {
"message": "Pumili ng account"
},
"minecraft-account.sign-in": {
"message": "Mag sign in kay Minecraft"
},
"search.filter.locked.instance": {
"message": "Sagot na ng instansiya"
},
@@ -718,26 +559,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "Ang loader ay handog na ng server"
},
"unknown-pack-warning-modal.body": {
"message": "Ang file ay nasusuri lamang kapag ito ay na-upload sa Modrinth, walang pili sa file format nito (kabilang na ang .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Huwag ipakita ang babalang ito ulit"
},
"unknown-pack-warning-modal.header": {
"message": "Kumpirmahin ang installation"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "I-install parin"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Ang malware ay madalas naidadala sa mga modpack files sa pamamagitan ng pag-bigay ng mga ito sa mga platforms kagaya ng Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Hindi namin mahanap ang file na ito sa Modrinth. Mahalagang mag-install ka lamang ng files galing sa mga mapagkakatiwalang sources."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Hindi kilalang file"
}
}
+18 -24
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Impossible de contacter les serveurs d'authentification"
},
"app.browse.add-servers-to-instance": {
"app.browse.add-server-to-instance": {
"message": "Ajouter le serveur à l'instance"
},
"app.browse.add-to-an-instance": {
"message": "Ajouter à une instance"
"app.browse.add-servers-to-instance": {
"message": "Ajouter des serveurs à votre instance"
},
"app.browse.add-to-instance": {
"message": "Ajouter à l'instance"
@@ -122,9 +122,6 @@
"app.browse.already-added": {
"message": "Déjà ajouté"
},
"app.browse.back-to-instance": {
"message": "Retour à l'instance"
},
"app.browse.discover-content": {
"message": "Découvrir du contenu"
},
@@ -134,11 +131,20 @@
"app.browse.hide-added-servers": {
"message": "Masquer les serveurs déjà ajoutés"
},
"app.browse.hide-installed-content": {
"message": "Masquer le contenu déjà ajouté"
},
"app.browse.install-content-to-instance": {
"message": "Installer du contenu à l'instance"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Ajouter du contenu peut briser la comptabilité en rejoignant le serveur. Tout contenu en plus sera également perdu lorsque vous mettrez à jour le contenu de l'instance du serveur."
"app.browse.server.install": {
"message": "Installer"
},
"app.browse.server.installed": {
"message": "Installé"
},
"app.browse.server.installing": {
"message": "Installation en cours"
@@ -158,9 +164,6 @@
"app.export-modal.header": {
"message": "Exporter le modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Inclure « {file} » ?"
},
"app.export-modal.modpack-name-label": {
"message": "Nom du modpack"
},
@@ -168,7 +171,7 @@
"message": "Nom du modpack"
},
"app.export-modal.select-files-label": {
"message": "Configurez quels fichiers sont inclus dans cette exportation"
"message": "Sélectionnez des fichiers et des dossiers à inclure dans le pack"
},
"app.export-modal.version-number-label": {
"message": "Numéro de version"
@@ -189,7 +192,7 @@
"message": "Supprimer l'instance"
},
"app.instance.modpack-already-installed.body": {
"message": "Ce modpack est déjà installé sur l'instance <bold>{instanceName}</bold>. Êtes-vous sûr·e de vouloir le dupliquer ?"
"message": "Ce modpack est déjà installé sur l'instance <bold>{instanceName}</bold>. Êtes-vous sûr.e de vouloir le dupliquer ?"
},
"app.instance.modpack-already-installed.create": {
"message": "Créer"
@@ -228,7 +231,7 @@
"message": "« {name} » sera supprimé **pour toujours**, et il sera impossible de le récupérer."
},
"app.instance.worlds.delete-world-title": {
"message": "Êtes-vous sûr·e de vouloir supprimer ce monde pour toujours ?"
"message": "Êtes-vous sûr.e de vouloir supprimer ce monde pour toujours ?"
},
"app.instance.worlds.filter-modded": {
"message": "Moddé"
@@ -255,7 +258,7 @@
"message": "« {name} » ({address}) sera retiré de votre liste, y compris en jeu, et il sera impossible de le récupérer."
},
"app.instance.worlds.remove-server-title": {
"message": "Êtes-vous sûr·e de vouloir retirer {name} ?"
"message": "Êtes-vous sûr.e de vouloir retirer {name} ?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Rechercher {count} mondes..."
@@ -299,15 +302,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "Une mise à jour est requise pour jouer à {name}. Veuillez mettre à jour à la dernière version pour lancer le jeu."
},
"app.project.install-button.already-installed": {
"message": "Ce projet a déjà été installé"
},
"app.project.install-context.back-to-browse": {
"message": "Retour à la navigation"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installer du contenu à l'instance"
},
"app.settings.developer-mode-enabled": {
"message": "Mode développeur activé."
},
@@ -5,6 +5,12 @@
"app.auth-servers.unreachable.header": {
"message": "לא ניתן לגשת לשרתי האימות"
},
"app.browse.add-server-to-instance": {
"message": "הוסף שרת להתקנה"
},
"app.browse.add-servers-to-instance": {
"message": "הוסף שרתים להתקנה שלך"
},
"app.browse.add-to-instance": {
"message": "הוספה להתקנה"
},
@@ -23,6 +29,9 @@
"app.browse.discover-servers": {
"message": "גלה שרתים"
},
"app.browse.install-content-to-instance": {
"message": "הוספת התוכן להתקנה"
},
"app.export-modal.description-placeholder": {
"message": "הזן את תיאור חבילת המודים..."
},
@@ -38,6 +47,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "שם חבילת המודים"
},
"app.export-modal.select-files-label": {
"message": "בחר קבצים ותיקיות להכללה בחבילה הזו"
},
"app.export-modal.version-number-label": {
"message": "מספר גרסה"
},
+27 -45
View File
@@ -6,34 +6,19 @@
"message": "Letöltések"
},
"app.action-bar.hide-more-running-instances": {
"message": "További futó profilok elrejtése"
"message": "További futó példányok elrejtése"
},
"app.action-bar.make-primary-instance": {
"message": "Beállítás elsődleges profilként"
"message": "Elsődleges példány létrehozása"
},
"app.action-bar.no-instances-running": {
"message": "Nincsenek futó profilok"
"message": "Nincsenek futó példányok"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Elsődleges profil"
},
"app.action-bar.stop-instance": {
"message": "Profil megállítása"
},
"app.action-bar.view-logs": {
"message": "Naplók megtekintése"
},
"app.appearance-settings.color-theme.title": {
"message": "Téma"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Ha olyan Modrinth Csomagfájlt (.mrpack) próbálsz telepíteni, amely nem a Modrinth szerverén található, a telepítés előtt gondoskodunk arról, hogy tisztában legyél a kockázatokkal."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Figyelmeztess, mielőtt ismeretlen modcsomagokat telepítenék"
"message": "Elsődleges példány"
},
"app.auth-servers.unreachable.body": {
"message": "A Minecraft hitelesítő szerverek lehet, hogy nem üzemelnek. Bizonyosodj meg róla, hogy van internetkapcsolatod és próbáld meg újra."
@@ -41,6 +26,12 @@
"app.auth-servers.unreachable.header": {
"message": "Nem lehet elérni a hitelesítési kiszolgálókat"
},
"app.browse.add-server-to-instance": {
"message": "Szerver hozzáadása a profilhoz"
},
"app.browse.add-servers-to-instance": {
"message": "Szerverek hozzáadása a profilodhoz"
},
"app.browse.add-to-instance": {
"message": "Hozzáadás a profilhoz"
},
@@ -53,9 +44,6 @@
"app.browse.already-added": {
"message": "Már hozzá van adva"
},
"app.browse.back-to-instance": {
"message": "Vissza a profilhoz"
},
"app.browse.discover-content": {
"message": "Tartalom böngészése"
},
@@ -65,18 +53,24 @@
"app.browse.hide-added-servers": {
"message": "Már hozzáadott szerverek elrejtése"
},
"app.browse.hide-installed-content": {
"message": "Már telepített tartalom elrejtése"
},
"app.browse.install-content-to-instance": {
"message": "Tartalom letöltése a profilhoz"
},
"app.browse.project-type.modpacks": {
"message": "Modcsomagok"
},
"app.browse.server.install": {
"message": "Letöltés"
},
"app.browse.server.installed": {
"message": "Letöltve"
},
"app.browse.server.installing": {
"message": "Letöltés..."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Modcsomag telepítése..."
},
"app.export-modal.description-placeholder": {
"message": "Írd be a modcsomag leírását..."
},
@@ -92,6 +86,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "A modcsomag neve"
},
"app.export-modal.select-files-label": {
"message": "Válaszd ki a csomagba felveendő fájlokat és mappákat"
},
"app.export-modal.version-number-label": {
"message": "Verziószám"
},
@@ -221,12 +218,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "\nFrissítés szükséges ehhez: {name}. Kérjük, frissíts a legújabb verzióra a játék elindításához"
},
"app.project.install-context.back-to-browse": {
"message": "Vissza a böngészéshez"
},
"app.project.install-context.install-content-to-instance": {
"message": "Tartalom telepítése a profilba"
},
"app.settings.developer-mode-enabled": {
"message": "Fejlesztői mód bekapcsolva."
},
@@ -252,10 +243,10 @@
"message": "Erőforráskezelés"
},
"app.update-popup.body": {
"message": "A Modrinth App v{version} telepítésre kész! Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás a bezárásakor automatikusan frissül."
"message": "A Modrinth App v{version} telepítésre kész! Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás bezárásakor automatikusan frissül."
},
"app.update-popup.body.download-complete": {
"message": "A Modrinth App v{version} letöltése befejeződött. Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás a bezárásakor automatikusan frissül."
"message": "A Modrinth App v{version} letöltése befejeződött. Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás bezárásakor automatikusan frissül."
},
"app.update-popup.body.linux": {
"message": "A Modrinth App v{version} elérhető. Használd a csomagkezelőt a legújabb funkciók és javítások frissítéséhez!"
@@ -640,14 +631,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "A betöltőt a szerver biztosítja"
},
"unknown-pack-warning-modal.header": {
"message": "Telepítés megerősítése"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Letöltés mindenképpen"
},
"unknown-pack-warning-modal.warning.body": {
"message": "Ezt a fájlt nem találtuk meg a Modrinthon. Határozottan javasoljuk, hogy kizárólag megbízható forrásokból származó fájlokat telepíts."
}
}
+14 -80
View File
@@ -3,73 +3,25 @@
"message": "Mengunduh Java {version}"
},
"app.action-bar.downloads": {
"message": "Unduhan"
"message": "Mengunduh"
},
"app.action-bar.hide-more-running-instances": {
"message": "Sembunyikan lebih banyak instans yang sedang berjalan"
"message": "Sembunyikan lebih banyak instance yang sedang berjalan"
},
"app.action-bar.make-primary-instance": {
"message": "Jadikan sebagai instans utama"
"message": "Jadikan sebagai instance utama"
},
"app.action-bar.no-instances-running": {
"message": "Tidak ada instans yang sedang berjalan"
"message": "Tidak ada instance yang sedang berjalan"
},
"app.action-bar.offline": {
"message": "Luring"
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Instans utama"
"message": "Instance utama"
},
"app.action-bar.show-more-running-instances": {
"message": "Tampilkan lebih banyak instans yang sedang berjalan"
},
"app.action-bar.stop-instance": {
"message": "Hentikan instans"
},
"app.action-bar.view-active-downloads": {
"message": "Lihat pengunduhan berlangsung"
},
"app.action-bar.view-instance": {
"message": "Lihat instans"
},
"app.action-bar.view-logs": {
"message": "Lihat catatan"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Menghidupkan renderasi tingkat lanjut seperti efek buram yang dapat menyebabkan masalah kinerja tanpa renderasi yang dipercepat perangkat keras."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Renderasi tingkat lanjut"
},
"app.appearance-settings.color-theme.description": {
"message": "Pilih tema warna pilihan Anda untuk Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema warna"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Ubah halaman utama yang dibuka oleh peluncur."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Beranda"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Pustaka"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Mematikan label nama di atas wujud pemain Anda pada halaman rupa."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Sembunyikan label nama"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Kecilkan pluncur ketika proses Minecraft berjalan."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Kecilkan peluncur"
},
"app.appearance-settings.select-option": {
"message": "Pilih opsi"
"message": "Tampilkan lebih banyak instance yang sedang berjalan"
},
"app.auth-servers.unreachable.body": {
"message": "Server autentikasi Minecraft mungkin sedang tidak tersedia saat ini. Periksa koneksi internet Anda dan coba lagi nanti."
@@ -77,11 +29,11 @@
"app.auth-servers.unreachable.header": {
"message": "Tidak dapat terhubung ke server autentikasi"
},
"app.browse.add-servers-to-instance": {
"message": "Menambah server ke instans"
"app.browse.add-server-to-instance": {
"message": "Tambah server ke instans"
},
"app.browse.add-to-an-instance": {
"message": "Tambah ke instans"
"app.browse.add-servers-to-instance": {
"message": "Tambah server ke instans Anda"
},
"app.browse.add-to-instance": {
"message": "Tambah ke instans"
@@ -95,29 +47,14 @@
"app.browse.already-added": {
"message": "Telah ditambahkan"
},
"app.browse.back-to-instance": {
"message": "Kembali ke instans"
},
"app.browse.discover-content": {
"message": "Temukan konten"
},
"app.browse.discover-servers": {
"message": "Temukan server"
},
"app.browse.hide-added-servers": {
"message": "Sembunyikan server yang sudah ditambah"
},
"app.browse.project-type.modpacks": {
"message": "Paket Mod"
},
"app.browse.server.installing": {
"message": "Memasang"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Memasang paket mod..."
"app.browse.install-content-to-instance": {
"message": "Pasang konten ke instans"
},
"app.export-modal.description-placeholder": {
"message": "Masukkan deskripsi paket mod..."
@@ -128,9 +65,6 @@
"app.export-modal.header": {
"message": "Ekspor paket mod"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Sertakan \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nama Paket Mod"
},
@@ -138,7 +72,7 @@
"message": "Nama paket mod"
},
"app.export-modal.select-files-label": {
"message": "Konfigurasikan berkas mana yang disertakan dalam pengeksporan ini"
"message": "Pilih berkas dan folder yang ingin dimasukkan ke paket"
},
"app.export-modal.version-number-label": {
"message": "Nomor versi"
+37 -43
View File
@@ -1,12 +1,12 @@
{
"app.action-bar.downloading-java": {
"message": "Java {version}"
"message": "Scaricando Java {version}"
},
"app.action-bar.downloads": {
"message": "Download"
},
"app.action-bar.hide-more-running-instances": {
"message": "Mostra meno istanze in esecuzione"
"message": "Nascondi le istanze in esecuzione"
},
"app.action-bar.make-primary-instance": {
"message": "Rendi primaria"
@@ -21,19 +21,19 @@
"message": "Primaria"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostra più istanze in esecuzione"
"message": "Mostra le istanze in esecuzione"
},
"app.action-bar.stop-instance": {
"message": "Ferma l'istanza"
},
"app.action-bar.view-active-downloads": {
"message": "Mostra i download attivi"
"message": "Vedi i download attivi"
},
"app.action-bar.view-instance": {
"message": "Mostra l'istanza"
"message": "Vedi l'istanza"
},
"app.action-bar.view-logs": {
"message": "Mostra i log"
"message": "Vedi i log"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Abilita alcuni effetti come la sfocatura, ma può causare problemi di prestazioni senza accelerazione hardware."
@@ -63,28 +63,28 @@
"message": "Rimuove il nametag sopra la testa del giocatore nella pagina delle skin."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Nascondi il nome"
"message": "Nascondi nametag"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Elenca i mondi recenti nella sezione \"Avvio rapido\" della pagina Home."
"message": "Aggiunge i mondi recenti nella sezione \"Avvio rapido\" della pagina Home."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Avvio rapido nei mondi"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Riduci il launcher a icona quando si avvia Minecraft."
"message": "Riduci il launcher quanto si avvia Minecraft."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Riduci a icona"
"message": "Riduci il launcher"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usa la cornice di sistema (riavvio necessario)."
"message": "Usa la cornice di sistema (riavvio richiesto)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Decorazioni native"
},
"app.appearance-settings.select-option": {
"message": "Scegli una pagina"
"message": "Seleziona un'opzione"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Scegli se mostrare o nascondere la barra laterale."
@@ -93,10 +93,10 @@
"message": "Abilita la barra laterale"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Visti i rischi nell'installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth, mostriamo un avvertimento quando tenti di fare ciò."
"message": "Apparirà un avviso prima di installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth, così da conoscerne i rischi."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Avvisami se installo pacchetti sconosciuti"
"message": "Avvisami prima di installare pacchetti sconosciuti"
},
"app.auth-servers.unreachable.body": {
"message": "I server di autenticazione di Minecraft stanno riscontrando problemi. Controlla la tua connessione a Internet e riprova più tardi."
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Impossibile raggiungere i server di autenticazione"
},
"app.browse.add-servers-to-instance": {
"message": "Aggiungendo il server all'istanza"
"app.browse.add-server-to-instance": {
"message": "Aggiungi server all'istanza"
},
"app.browse.add-to-an-instance": {
"message": "Aggiungi a un'istanza"
"app.browse.add-servers-to-instance": {
"message": "Aggiungi i server alla tua istanza"
},
"app.browse.add-to-instance": {
"message": "Aggiungi all'istanza"
@@ -122,9 +122,6 @@
"app.browse.already-added": {
"message": "Già aggiunto"
},
"app.browse.back-to-instance": {
"message": "Torna all'istanza"
},
"app.browse.discover-content": {
"message": "Esplora i contenuti"
},
@@ -134,14 +131,23 @@
"app.browse.hide-added-servers": {
"message": "Nascondi server già aggiunti"
},
"app.browse.hide-installed-content": {
"message": "Nascondi contenuti già installati"
},
"app.browse.install-content-to-instance": {
"message": "Installa contenuti nell'istanza"
},
"app.browse.project-type.modpacks": {
"message": "Pacchetti di mod"
},
"app.browse.server-instance-content-warning": {
"message": "Aggiungere dei contenuti potrebbe portare problemi entrando nel server. Qualsiasi contenuto aggiunto sarà anche perso aggiornando i contenuti dell'istanza del server."
"app.browse.server.install": {
"message": "Installa"
},
"app.browse.server.installed": {
"message": "Installato"
},
"app.browse.server.installing": {
"message": "Installazione"
"message": "Installando"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
@@ -158,9 +164,6 @@
"app.export-modal.header": {
"message": "Esporta pacchetto"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Includere \"{file}\" nell'esportazione?"
},
"app.export-modal.modpack-name-label": {
"message": "Nome del pacchetto"
},
@@ -168,7 +171,7 @@
"message": "Nome del pacchetto"
},
"app.export-modal.select-files-label": {
"message": "Seleziona i file da includere in questa esportazione"
"message": "Seleziona i file e le cartelle da includere nel pacchetto"
},
"app.export-modal.version-number-label": {
"message": "Numero di versione"
@@ -299,15 +302,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "{name} richiede degli aggiornamenti. Installa l'ultima versione per poter giocare."
},
"app.project.install-button.already-installed": {
"message": "Questo progetto è già stato installato"
},
"app.project.install-context.back-to-browse": {
"message": "Torna su esplora"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installa il contenuto nell'istanza"
},
"app.settings.developer-mode-enabled": {
"message": "Modalità sviluppatore attiva."
},
@@ -447,7 +441,7 @@
"message": "Nessuna amicizia corrisponde a ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Cerca tra le amicizie..."
"message": "Cerca amicizie..."
},
"friends.section.heading": {
"message": "{title} - {count}"
@@ -486,7 +480,7 @@
"message": "Mondo di Minecraft"
},
"instance.edit-world.reset-icon": {
"message": "Ripristina icona"
"message": "Resetta icona"
},
"instance.edit-world.title": {
"message": "Modifica mondo"
@@ -546,7 +540,7 @@
"message": "Cambia icona"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Scegli un'icona"
"message": "Seleziona icona"
},
"instance.settings.tabs.general.library-groups": {
"message": "Gruppi della libreria"
@@ -741,7 +735,7 @@
"message": "Il loader è determinato dal server"
},
"unknown-pack-warning-modal.body": {
"message": "Solo i file caricati su Modrinth vengono esaminati, qualunque sia il loro formato (.mrpack inclusi)."
"message": "Un file è esaminato quando è caricato su Modrinth, indipendentemente dal suo formato (.mrpack inclusi)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Non mostrare più questo avviso"
@@ -753,10 +747,10 @@
"message": "Installa comunque"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Spesso i malware vengono nascosti nei pacchetti di mod, poi distribuiti su piattaforme come Discord."
"message": "Molto spesso i malware sono distribuiti attraverso i pacchetti di mod, distribuiti in piattaforme come Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Non è stato possibile trovare questo file su Modrinth. Consigliamo di installare file solo da fonti attendibili."
"message": "Non siamo riusciti a trovare questo file su Modrinth. Consigliamo di installare file solo da fonti di cui ti fidi."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Tipo di file sconosciuto"
+21 -18
View File
@@ -104,6 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "認証サーバーにアクセスできません"
},
"app.browse.add-server-to-instance": {
"message": "サーバーをインスタンスに追加"
},
"app.browse.add-servers-to-instance": {
"message": "サーバーを自身のインスタンスに追加"
},
"app.browse.add-to-instance": {
"message": "インスタンスに追加"
},
@@ -125,9 +131,21 @@
"app.browse.hide-added-servers": {
"message": "既に追加済みのサーバーを非表示にする"
},
"app.browse.hide-installed-content": {
"message": "既にインストールされているコンテンツを非表示にする"
},
"app.browse.install-content-to-instance": {
"message": "コンテンツをインスタンスにインストールする"
},
"app.browse.project-type.modpacks": {
"message": "Modパック"
},
"app.browse.server.install": {
"message": "インストール"
},
"app.browse.server.installed": {
"message": "インストール済み"
},
"app.browse.server.installing": {
"message": "インストール中"
},
@@ -152,6 +170,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Modパック名"
},
"app.export-modal.select-files-label": {
"message": "パックに含めるファイルとフォルダーを選択"
},
"app.export-modal.version-number-label": {
"message": "バージョン番号"
},
@@ -671,24 +692,6 @@
"instance.worlds.world_in_use": {
"message": "ワールドは使用中"
},
"minecraft-account.add-account": {
"message": "アカウントを追加"
},
"minecraft-account.label": {
"message": "Minecraftアカウント"
},
"minecraft-account.not-signed-in": {
"message": "サインインしていません"
},
"minecraft-account.remove-account": {
"message": "アカウントを一覧から削除"
},
"minecraft-account.select-account": {
"message": "アカウントを選択"
},
"minecraft-account.sign-in": {
"message": "Minecraftにサインイン"
},
"search.filter.locked.instance": {
"message": "インスタンスによる条件"
},
+19 -22
View File
@@ -104,6 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "인증 서버에 연결할 수 없습니다"
},
"app.browse.add-server-to-instance": {
"message": "인스턴스에 서버 추가"
},
"app.browse.add-servers-to-instance": {
"message": "인스턴스에 서버 추가"
},
"app.browse.add-to-instance": {
"message": "인스턴스에 추가"
},
@@ -125,9 +131,21 @@
"app.browse.hide-added-servers": {
"message": "이미 추가된 서버 숨기기"
},
"app.browse.hide-installed-content": {
"message": "이미 설치된 콘텐츠 숨기기"
},
"app.browse.install-content-to-instance": {
"message": "인스턴스에 콘텐츠 설치"
},
"app.browse.project-type.modpacks": {
"message": "모드팩"
},
"app.browse.server.install": {
"message": "설치"
},
"app.browse.server.installed": {
"message": "설치됨"
},
"app.browse.server.installing": {
"message": "설치 중"
},
@@ -146,9 +164,6 @@
"app.export-modal.header": {
"message": "모드팩 내보내기"
},
"app.export-modal.include-file-accessibility-label": {
"message": "\"{file}\"(을)를 포함할까요?"
},
"app.export-modal.modpack-name-label": {
"message": "모드팩 이름"
},
@@ -156,7 +171,7 @@
"message": "모드팩 이름"
},
"app.export-modal.select-files-label": {
"message": "내보내기에 어느 파일이 포함될지 구성"
"message": "팩에 포함할 파일과 폴더 선택"
},
"app.export-modal.version-number-label": {
"message": "버전 구분"
@@ -677,24 +692,6 @@
"instance.worlds.world_in_use": {
"message": "사용 중인 세계"
},
"minecraft-account.add-account": {
"message": "계정 추가"
},
"minecraft-account.label": {
"message": "마인크래프트 계정"
},
"minecraft-account.not-signed-in": {
"message": "로그인하지 않음"
},
"minecraft-account.remove-account": {
"message": "계정 제거"
},
"minecraft-account.select-account": {
"message": "계정 선택"
},
"minecraft-account.sign-in": {
"message": "마인크래프트 계정으로 로그인"
},
"search.filter.locked.instance": {
"message": "인스턴스에서 관리"
},
+7 -166
View File
@@ -5,110 +5,20 @@
"app.action-bar.downloads": {
"message": "Muat Turun"
},
"app.action-bar.hide-more-running-instances": {
"message": "Sembunyikan lebih banyak pemasangan yang berjalan"
},
"app.action-bar.make-primary-instance": {
"message": "Jadikan pemasangan utama"
},
"app.action-bar.no-instances-running": {
"message": "Tiada pemasangan yang berjalan"
},
"app.action-bar.offline": {
"message": "Luar Talian"
},
"app.action-bar.primary-instance": {
"message": "Pemasangan utama"
},
"app.action-bar.show-more-running-instances": {
"message": "Tunjukkan lebih banyak pemasangan yang berjalan"
},
"app.action-bar.stop-instance": {
"message": "Hentikan pemasangan"
},
"app.action-bar.view-active-downloads": {
"message": "Lihat muat turun aktif"
},
"app.action-bar.view-instance": {
"message": "Lihat pemasangan"
},
"app.action-bar.view-logs": {
"message": "Lihat log"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Mendayakan pemaparan lanjutan seperti kesan kabur yang boleh menyebabkan masalah prestasi tanpa pemaparan dipercepatkan perkakasan."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Pemaparan lanjutan"
},
"app.appearance-settings.color-theme.description": {
"message": "Pilih tema warna pilihan anda untuk Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema warna"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Tukar laman utama yang dibuka oleh pelancar."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Laman utama"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Pustaka"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Laman pendaratan lalai"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Menyahdayakan tanda nama di atas pemain anda pada halaman kekulit."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Sembunyikan tanda nama"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Sertakan dunia terkini dalam bahagian \"Lompat kembali\" pada Laman Utama."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Lompat kembali ke dalam dunia"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Meminimumkan pelancar apabila proses Minecraft bermula."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimumkan pelancar"
},
"app.appearance-settings.native-decorations.description": {
"message": "Gunakan bingkai tetingkap sistem (aplikasi perlu dimulakan semula)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Hiasan asli"
},
"app.appearance-settings.select-option": {
"message": "Pilih satu pilihan"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Membolehkan keupayaan untuk menogol bar sisi."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Togol bar sisi"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Jika anda cuba memasang fail Modrinth Pack (.mrpack) yang tidak dihoskan pada Modrinth, kami akan memastikan anda memahami risikonya sebelum memasangnya."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Beri saya amaran sebelum memasang pek mod yang tidak diketahui"
},
"app.auth-servers.unreachable.body": {
"message": "Pelayan pengesahan Minecraft mungkin sedang tergendala sekarang. Periksa sambungan internet anda dan cuba lagi kemudian."
},
"app.auth-servers.unreachable.header": {
"message": "Tidak dapat mencapai pelayan pengesahan"
},
"app.browse.add-servers-to-instance": {
"message": "Menambah pelayan ke dalam pemasangan"
"app.browse.add-server-to-instance": {
"message": "Tambahkan pelayan ke dalam pemasangan"
},
"app.browse.add-to-an-instance": {
"message": "Tambahkan ke dalam pemasangan"
"app.browse.add-servers-to-instance": {
"message": "Tambahkan pelayan ke dalam pemasangan anda"
},
"app.browse.add-to-instance": {
"message": "Tambahkan ke dalam pemasangan"
@@ -122,32 +32,14 @@
"app.browse.already-added": {
"message": "Sudah ditambah"
},
"app.browse.back-to-instance": {
"message": "Kembali ke pemasangan"
},
"app.browse.discover-content": {
"message": "Temui kandungan"
},
"app.browse.discover-servers": {
"message": "Temui pelayan"
},
"app.browse.hide-added-servers": {
"message": "Sembunyikan pelayan yang sudah ditambah"
},
"app.browse.project-type.modpacks": {
"message": "Pek Mod"
},
"app.browse.server-instance-content-warning": {
"message": "Menambah kandungan boleh merosakkan keserasian apabila menyertai pelayan. Sebarang kandungan yang ditambah juga akan hilang apabila anda mengemas kini kandungan pemasangan pelayan."
},
"app.browse.server.installing": {
"message": "Sedang memasang"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Sedang memasang pek mod..."
"app.browse.install-content-to-instance": {
"message": "Pasang kandungan ke dalam pemasangan"
},
"app.export-modal.description-placeholder": {
"message": "Masukkan keterangan pek mod..."
@@ -158,9 +50,6 @@
"app.export-modal.header": {
"message": "Eksport pek mod"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Sertakan \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nama Pek Mod"
},
@@ -168,7 +57,7 @@
"message": "Nama pek mod"
},
"app.export-modal.select-files-label": {
"message": "Konfigurasikan fail yang disertakan dalam eksport ini"
"message": "Pilih fail dan folder untuk disertakan dalam pek mod"
},
"app.export-modal.version-number-label": {
"message": "Nombor versi"
@@ -299,15 +188,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "Kemas kini diperlukan untuk memainkan {name}. Sila kemas kini kepada versi terkini untuk melancarkan permainan."
},
"app.project.install-button.already-installed": {
"message": "Projek ini sudah dipasang"
},
"app.project.install-context.back-to-browse": {
"message": "Kembali melayar"
},
"app.project.install-context.install-content-to-instance": {
"message": "Pasang kandungan ke dalam pemasangan"
},
"app.settings.developer-mode-enabled": {
"message": "Mod pembangun didayakan."
},
@@ -698,24 +578,6 @@
"instance.worlds.world_in_use": {
"message": "Dunia sedang digunakan"
},
"minecraft-account.add-account": {
"message": "Tambah akaun"
},
"minecraft-account.label": {
"message": "Akaun Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Tidak didaftar masuk"
},
"minecraft-account.remove-account": {
"message": "Alih keluar akaun"
},
"minecraft-account.select-account": {
"message": "Pilih akaun"
},
"minecraft-account.sign-in": {
"message": "Daftar masuk ke dalam Minecraft"
},
"search.filter.locked.instance": {
"message": "Disediakan oleh pemasangan ini"
},
@@ -739,26 +601,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "Pemuat adalah disediakan oleh pelayan"
},
"unknown-pack-warning-modal.body": {
"message": "Sesuatu fail hanya disemak jika ia dimuat naik ke Modrinth, tanpa mengira format failnya (termasuk .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Jangan tunjukkan amaran ini lagi"
},
"unknown-pack-warning-modal.header": {
"message": "Sahkan pemasangan"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Pasangkan juga"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Perisian hasad sering diedarkan melalui fail pek mod dengan berkongsinya di platform seperti Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Kami tidak dapat menemui fail ini di Modrinth. Kami sangat mengesyorkan anda untuk hanya memasang fail daripada sumber yang anda percayai."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Amaran fail tidak diketahui"
}
}
+7 -175
View File
@@ -1,114 +1,15 @@
{
"app.action-bar.downloading-java": {
"message": "Java {version} aan het downloaden"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Verberg meer actieve instanties"
},
"app.action-bar.make-primary-instance": {
"message": "Maak primaire instantie"
},
"app.action-bar.no-instances-running": {
"message": "Geen actieve instanties"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Primaire instantie"
},
"app.action-bar.show-more-running-instances": {
"message": "Toon meer actieve instanties"
},
"app.action-bar.stop-instance": {
"message": "Stop instantie"
},
"app.action-bar.view-active-downloads": {
"message": "Bekijk actieve downloads"
},
"app.action-bar.view-instance": {
"message": "Bekijk instantie"
},
"app.action-bar.view-logs": {
"message": "Bekijk logs"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Activeert geavanceerde weergaves zoals blur effecten die mogelijke prestatieproblemen opleveren zonder hardware-versnelde weergeving."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Geavanceerde weergave"
},
"app.appearance-settings.color-theme.description": {
"message": "Selecteer je voorkeur kleurenthema voor de Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Kleurenthema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Wijzig de pagina waarop de launcher opent."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Home"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliotheek"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Standaard openingspagina"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Schakelt het nametag boven jouw speler op de skins pagina uit."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Verberg nametag"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Inclusief recente werelden in het gedeelte \"Ga terug\" op de startpagina."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Ga terug in werelden"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimaliseer de launcher als een Minecraft proces wordt gestart."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimaliseer launcher"
},
"app.appearance-settings.native-decorations.description": {
"message": "Gebruik systeem venster frame (app opnieuw starten vereist)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Oorspronkelijke versieringen"
},
"app.appearance-settings.select-option": {
"message": "Selecteer een optie"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Schakelt de mogelijkheid om de zijbalk in- of uit te schakelen in."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Schakel zijbalk"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Als je probeert een Modrinth Pack-bestand (.mrpack) te installeren dat niet op Modrinth zelf wordt gehost, zorgen we ervoor dat je de risico's begrijpt voordat je het installeert."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Waarschuw mij voordat ik onbekende modpacks installeer"
},
"app.auth-servers.unreachable.body": {
"message": "Minecraft authenticatie servers zijn mogelijk offline. Controleer je internetverbinding en probeer opnieuw later."
},
"app.auth-servers.unreachable.header": {
"message": "Authenticatieservers kunnen niet worden bereikt"
},
"app.browse.add-servers-to-instance": {
"message": "Server toevoegen aan instantie"
"app.browse.add-server-to-instance": {
"message": "Voeg server aan instantie toe"
},
"app.browse.add-to-an-instance": {
"message": "Voeg toe aan een instantie"
"app.browse.add-servers-to-instance": {
"message": "Voeg servers aan instantie toe"
},
"app.browse.add-to-instance": {
"message": "Voeg aan instantie toe"
@@ -122,32 +23,14 @@
"app.browse.already-added": {
"message": "Al toegevoegd"
},
"app.browse.back-to-instance": {
"message": "Terug naar instantie"
},
"app.browse.discover-content": {
"message": "Ontdek content"
},
"app.browse.discover-servers": {
"message": "Ontdek servers"
},
"app.browse.hide-added-servers": {
"message": "Verberg al toegevoegde servers"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Het toevoegen van content kan de compatibiliteit verbreken bij het verbinden met de server. Alle toegevoegde inhoud gaat ook verloren wanneer u de content van de serverinstantie bijwerkt."
},
"app.browse.server.installing": {
"message": "Installeren"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Modpack aan het installeren..."
"app.browse.install-content-to-instance": {
"message": "Installeer inhoud naar instantie"
},
"app.export-modal.description-placeholder": {
"message": "Voeg modpack beschrijving in..."
@@ -158,9 +41,6 @@
"app.export-modal.header": {
"message": "Exporteer modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Betrek \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Modpack Naam"
},
@@ -168,7 +48,7 @@
"message": "Modpack naam"
},
"app.export-modal.select-files-label": {
"message": "Configureer welke bestanden bij deze export zijn betrokken"
"message": "Selecteer bestanden en mappen om toe te voegen aan pack"
},
"app.export-modal.version-number-label": {
"message": "Versie nummer"
@@ -299,15 +179,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "Een update is vereist om {name} te spelen. Update naar de laatste versie om het spel te starten."
},
"app.project.install-button.already-installed": {
"message": "Dit project is al geïnstalleerd"
},
"app.project.install-context.back-to-browse": {
"message": "Terug naar browsen"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installeer content naar instantie"
},
"app.settings.developer-mode-enabled": {
"message": "Ontwikkelaarsmodus ingeschakeld."
},
@@ -698,24 +569,6 @@
"instance.worlds.world_in_use": {
"message": "Wereld wordt gebruikt"
},
"minecraft-account.add-account": {
"message": "Voeg account toe"
},
"minecraft-account.label": {
"message": "Minecraft account"
},
"minecraft-account.not-signed-in": {
"message": "Niet ingelogd"
},
"minecraft-account.remove-account": {
"message": "Verwijder account"
},
"minecraft-account.select-account": {
"message": "Selecteer account"
},
"minecraft-account.sign-in": {
"message": "Log in bij Minecraft"
},
"search.filter.locked.instance": {
"message": "Aangeboden door de instantie"
},
@@ -739,26 +592,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "Loader is gegeven door de server"
},
"unknown-pack-warning-modal.body": {
"message": "Een bestand wordt alleen beoordeeld als het naar Modrinth is geüpload, ongeacht het bestandsformaat (inclusief .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Toon deze waarschuwing niet opnieuw"
},
"unknown-pack-warning-modal.header": {
"message": "Bevestig installatie"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Installeer toch"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Malware wordt vaak verspreid via modpack-bestanden door ze te delen op platforms zoals Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "We konden dit bestand niet vinden op Modrinth. We raden ten zeerste aan om alleen bestanden te installeren van bronnen die u vertrouwt."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Waarschuwing voor onbekend bestand"
}
}
+19 -31
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Nie udało się połączyć się z serwerami uwierzytelniania"
},
"app.browse.add-servers-to-instance": {
"message": "Dodawanie serwera do instancji"
"app.browse.add-server-to-instance": {
"message": "Dodaj serwer do instancji"
},
"app.browse.add-to-an-instance": {
"message": "Dodaj do instancji"
"app.browse.add-servers-to-instance": {
"message": "Dodaj serwery do swojej instancji"
},
"app.browse.add-to-instance": {
"message": "Dodaj do instancji"
@@ -122,9 +122,6 @@
"app.browse.already-added": {
"message": "Już dodano"
},
"app.browse.back-to-instance": {
"message": "Powrót do instancji"
},
"app.browse.discover-content": {
"message": "Odkrywaj zawartość"
},
@@ -134,9 +131,21 @@
"app.browse.hide-added-servers": {
"message": "Ukryj już dodane serwery"
},
"app.browse.hide-installed-content": {
"message": "Ukryj już dodane zasoby"
},
"app.browse.install-content-to-instance": {
"message": "Zainstaluj zasoby do instancji"
},
"app.browse.project-type.modpacks": {
"message": "Paczki modów"
},
"app.browse.server.install": {
"message": "Zainstaluj"
},
"app.browse.server.installed": {
"message": "Zainstalowano"
},
"app.browse.server.installing": {
"message": "Instalowanie"
},
@@ -161,6 +170,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Nazwa paczki modów"
},
"app.export-modal.select-files-label": {
"message": "Wybierz pliki i foldery, które mają znaleźć się w paczce"
},
"app.export-modal.version-number-label": {
"message": "Numer wersji"
},
@@ -290,15 +302,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "Aktualizacja jest wymagana, aby grać w {name}. Proszę zaktualizować do najnowszej wersji, aby uruchomić grę."
},
"app.project.install-button.already-installed": {
"message": "Ten projekt jest już zainstalowany"
},
"app.project.install-context.back-to-browse": {
"message": "Powrót do przeglądania"
},
"app.project.install-context.install-content-to-instance": {
"message": "Zainstaluj zasoby do instancji"
},
"app.settings.developer-mode-enabled": {
"message": "Tryb dewelopera włączony."
},
@@ -689,21 +692,6 @@
"instance.worlds.world_in_use": {
"message": "Świat jest w użytku"
},
"minecraft-account.add-account": {
"message": "Dodaj konto"
},
"minecraft-account.label": {
"message": "Konto Minecraft"
},
"minecraft-account.remove-account": {
"message": "Usuń konto"
},
"minecraft-account.select-account": {
"message": "Wybierz konto"
},
"minecraft-account.sign-in": {
"message": "Zaloguj się do Minecraft"
},
"search.filter.locked.instance": {
"message": "Podane przez instancję"
},
+16 -22
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Não foi possível acessar os servidores de autenticação"
},
"app.browse.add-servers-to-instance": {
"message": "Adicionando servidor à instância"
"app.browse.add-server-to-instance": {
"message": "Adicionar servidor à instância"
},
"app.browse.add-to-an-instance": {
"message": "Adicionar a uma instância"
"app.browse.add-servers-to-instance": {
"message": "Adicionar servidores à sua instância"
},
"app.browse.add-to-instance": {
"message": "Adicionar à instância"
@@ -122,9 +122,6 @@
"app.browse.already-added": {
"message": "Já adicionado"
},
"app.browse.back-to-instance": {
"message": "Voltar a instância"
},
"app.browse.discover-content": {
"message": "Descubra conteúdo"
},
@@ -134,11 +131,20 @@
"app.browse.hide-added-servers": {
"message": "Ocultar servidores já adicionados"
},
"app.browse.hide-installed-content": {
"message": "Ocultar conteúdo já adicionado"
},
"app.browse.install-content-to-instance": {
"message": "Instalar conteúdo na instância"
},
"app.browse.project-type.modpacks": {
"message": "Pacotes de mods"
},
"app.browse.server-instance-content-warning": {
"message": "Adicionar conteúdo pode quebrar a compatibilidade ao entrar no servidor. Qualquer conteúdo adicionado também será perdido ao atualizar o conteúdo da instância do servidor."
"app.browse.server.install": {
"message": "Instalar"
},
"app.browse.server.installed": {
"message": "Instalado"
},
"app.browse.server.installing": {
"message": "Instalando"
@@ -158,9 +164,6 @@
"app.export-modal.header": {
"message": "Exportar pacote de mods"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Incluir \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nome do pacote de mods"
},
@@ -168,7 +171,7 @@
"message": "Nome do pacote de mods"
},
"app.export-modal.select-files-label": {
"message": "Configure quais arquivos serão incluídos nessa exportação"
"message": "Selecione arquivos e pastas para incluir no pacote"
},
"app.export-modal.version-number-label": {
"message": "Número da versão"
@@ -299,15 +302,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "É necessária uma atualização para jogar {name}. Atualize para a versão mais recente para iniciar o jogo."
},
"app.project.install-button.already-installed": {
"message": "Este projeto já foi instalado"
},
"app.project.install-context.back-to-browse": {
"message": "Voltar ao explorar"
},
"app.project.install-context.install-content-to-instance": {
"message": "Instalar conteúdo para a instância"
},
"app.settings.developer-mode-enabled": {
"message": "Modo de desenvolvedor ativado."
},
@@ -5,6 +5,12 @@
"app.auth-servers.unreachable.header": {
"message": "Não foi possível alcançar os servidores de autenticação"
},
"app.browse.add-server-to-instance": {
"message": "Adicionar servidor à instância"
},
"app.browse.add-servers-to-instance": {
"message": "Adicionar servidores à tua instância"
},
"app.browse.add-to-instance": {
"message": "Adicionar à instância"
},
@@ -23,6 +29,9 @@
"app.browse.discover-servers": {
"message": "Descobrir servidores"
},
"app.browse.install-content-to-instance": {
"message": "Instalar conteúdo à instância"
},
"app.export-modal.description-placeholder": {
"message": "Insere a descrição do modpack..."
},
@@ -38,6 +47,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Nome do Modpack"
},
"app.export-modal.select-files-label": {
"message": "Seleciona as pastas e ficheiros a incluir no pack"
},
"app.export-modal.version-number-label": {
"message": "Número da versão"
},
+16 -40
View File
@@ -104,12 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "Нет связи с серверами аутентификации"
},
"app.browse.add-server-to-instance": {
"message": "Добавить сервер в сборку"
},
"app.browse.add-servers-to-instance": {
"message": "Добавление серверов в сборку"
},
"app.browse.add-to-an-instance": {
"message": "Добавить в сборку"
},
"app.browse.add-to-instance": {
"message": "Добавить в сборку"
},
@@ -122,9 +122,6 @@
"app.browse.already-added": {
"message": "Уже добавлено"
},
"app.browse.back-to-instance": {
"message": "Вернуться к сборке"
},
"app.browse.discover-content": {
"message": "Поиск проектов"
},
@@ -134,11 +131,20 @@
"app.browse.hide-added-servers": {
"message": "Скрывать добавленные"
},
"app.browse.hide-installed-content": {
"message": "Скрывать установленные"
},
"app.browse.install-content-to-instance": {
"message": "Установка контента в сборку"
},
"app.browse.project-type.modpacks": {
"message": "Сборки"
},
"app.browse.server-instance-content-warning": {
"message": "Добавление контента может нарушить совместимость при подключении к серверу. Также весь добавленный контент будет удалён при обновлении содержимого серверной сборки."
"app.browse.server.install": {
"message": "Установить"
},
"app.browse.server.installed": {
"message": "Установлено"
},
"app.browse.server.installing": {
"message": "Установка"
@@ -158,9 +164,6 @@
"app.export-modal.header": {
"message": "Экспорт сборки"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Добавить «{file}» в экспорт?"
},
"app.export-modal.modpack-name-label": {
"message": "Название сборки"
},
@@ -168,7 +171,7 @@
"message": "Название сборки"
},
"app.export-modal.select-files-label": {
"message": "Выберите файлы для экспорта"
"message": "Выберите файлы и папки для экспорта"
},
"app.export-modal.version-number-label": {
"message": "Номер версии"
@@ -299,15 +302,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "Обновите {name} до последней версии, чтобы запустить игру."
},
"app.project.install-button.already-installed": {
"message": "Этот проект уже установлен"
},
"app.project.install-context.back-to-browse": {
"message": "Вернуться к поиску"
},
"app.project.install-context.install-content-to-instance": {
"message": "Установка контента в сборку"
},
"app.settings.developer-mode-enabled": {
"message": "Режим разработчика включён."
},
@@ -698,24 +692,6 @@
"instance.worlds.world_in_use": {
"message": "Мир используется"
},
"minecraft-account.add-account": {
"message": "Добавить"
},
"minecraft-account.label": {
"message": "Учётная запись Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Вход не выполнен"
},
"minecraft-account.remove-account": {
"message": "Удалить"
},
"minecraft-account.select-account": {
"message": "Выберите профиль"
},
"minecraft-account.sign-in": {
"message": "Войти в Minecraft"
},
"search.filter.locked.instance": {
"message": "Управляется сборкой"
},
@@ -741,7 +717,7 @@
"message": "Загрузчик управляется сервером"
},
"unknown-pack-warning-modal.body": {
"message": "Файлы любого формата проверены на безопасность, если они загружены на Modrinth (в том числе файлы.mrpack)."
"message": "Проверяются только файлы, загруженные на Modrinth, в том числе файлы .mrpack."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Больше не предупреждать"
@@ -5,6 +5,12 @@
"app.auth-servers.unreachable.header": {
"message": "Serveri za autentifikaciju su nedostupni"
},
"app.browse.add-server-to-instance": {
"message": "Dodaj server na instancu"
},
"app.browse.add-servers-to-instance": {
"message": "Dodaj servere na tvoju instancu"
},
"app.browse.add-to-instance": {
"message": "Dodaj na instancu"
},
@@ -23,6 +29,9 @@
"app.browse.discover-servers": {
"message": "Otkrij servere"
},
"app.browse.install-content-to-instance": {
"message": "Instaliraj sadržaj na instancu"
},
"app.export-modal.description-placeholder": {
"message": "Ukucaj opis modpacka..."
},
@@ -38,6 +47,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Ime modpacka"
},
"app.export-modal.select-files-label": {
"message": "Izaberi fajlove i foldere da uključiš u pack"
},
"app.export-modal.version-number-label": {
"message": "Broj verzije"
},
+18 -12
View File
@@ -1,22 +1,16 @@
{
"app.action-bar.downloading-java": {
"message": "Laddar ner Java {version}"
},
"app.action-bar.downloads": {
"message": "Nedladdningar"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.appearance-settings.color-theme.title": {
"message": "Färgtema"
},
"app.auth-servers.unreachable.body": {
"message": "Minecrafts autentiseringsservrar kan vara nere just nu. Kontrollera din internetanslutning och försök igen senare."
},
"app.auth-servers.unreachable.header": {
"message": "Kan ej nå autentiseringsservrarna"
},
"app.browse.add-server-to-instance": {
"message": "Lägg till server till instans"
},
"app.browse.add-servers-to-instance": {
"message": "Lägg till servrar till din instans"
},
"app.browse.add-to-instance": {
"message": "Lägg till i instans"
},
@@ -35,9 +29,18 @@
"app.browse.discover-servers": {
"message": "Upptäck servrar"
},
"app.browse.install-content-to-instance": {
"message": "Installera innehåll till instans"
},
"app.browse.project-type.modpacks": {
"message": "Modpaket"
},
"app.browse.server.install": {
"message": "Installera"
},
"app.browse.server.installed": {
"message": "Installerad"
},
"app.browse.server.installing": {
"message": "Installerar"
},
@@ -56,6 +59,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Modpaketets namn"
},
"app.export-modal.select-files-label": {
"message": "Välj filer och mappar att inkludera i paketet"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
},
+78 -291
View File
@@ -1,111 +1,18 @@
{
"app.action-bar.downloading-java": {
"message": "ดาวน์โหลดเวอร์ชัน Java {version}"
},
"app.action-bar.downloads": {
"message": "ดาวน์โหลด"
},
"app.action-bar.hide-more-running-instances": {
"message": "ซ่อนการดำเนินการโปรแกรมเบื้องหลัง"
},
"app.action-bar.make-primary-instance": {
"message": "กำหนดโปรแกรมหลัก"
},
"app.action-bar.no-instances-running": {
"message": "ไม่มีโปรแกรมใดดำเนินการ"
},
"app.action-bar.offline": {
"message": "ออฟไลน์"
},
"app.action-bar.primary-instance": {
"message": "โปรแกรมหลัก"
},
"app.action-bar.show-more-running-instances": {
"message": "แสดงการดำเนินการโปรแกรมเบื้องหลังเพิ่มเติม"
},
"app.action-bar.stop-instance": {
"message": "หยุดการทำงานโปรแกรม"
},
"app.action-bar.view-active-downloads": {
"message": "ดูยอดการดาวน์โหลดที่ยังใช้งาน"
},
"app.action-bar.view-instance": {
"message": "ดูโปรแกรม"
},
"app.action-bar.view-logs": {
"message": "ดู Log"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "เปิดการใช้งานการเรนเดอร์ขั้นสูง เช่น เอฟเฟคการเบลอ อาจทำให้เกมเกิดปัญหาด้านประสิทธิภาพได้หากไม่มีอุปกรณ์ฮาร์ดแวร์ที่สามารถรองรับการเรนเดอร์ได้"
},
"app.appearance-settings.advanced-rendering.title": {
"message": "การเรนเดอร์ขั้นสูง"
},
"app.appearance-settings.color-theme.description": {
"message": "เลือกธีมสีที่คุณชื่นชอบสำหรับ Modrinth บนอุปกรณ์นี้"
},
"app.appearance-settings.color-theme.title": {
"message": "ธีมสี"
},
"app.appearance-settings.default-landing-page.description": {
"message": "เปลี่ยนหน้าไปหน้าต่างที่ลันเชอร์เปิดขึ้นมา"
},
"app.appearance-settings.default-landing-page.home": {
"message": "หน้าหลัก"
},
"app.appearance-settings.default-landing-page.library": {
"message": "รายการ"
},
"app.appearance-settings.default-landing-page.title": {
"message": "หน้าเริ่มต้น"
},
"app.appearance-settings.hide-nametag.description": {
"message": "ปิดป้ายชื่อบนศีรษะของผู้เล่นในหน้าแสดงสกิน"
},
"app.appearance-settings.hide-nametag.title": {
"message": "ซ่อนป้ายชื่อ"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "การแสดงผลโลกล่าสุดที่ผู้เล่นเล่นในส่วน \"กระโดดกลับเข้าไป\" ในหน้าหลัก"
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "กระโดดกลับเข้าไปในโลก"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "ย่อลันเชอร์เมื่อกระบวนการการรัน Minecraft เริ่มต้นแล้ว"
},
"app.appearance-settings.minimize-launcher.title": {
"message": "ย่อลันเชอร์"
},
"app.appearance-settings.native-decorations.description": {
"message": "ใช้ระบบกรอบหน้าต่าง (ต้องการการรีสตาร์ทโปรแกรมใหม่)"
},
"app.appearance-settings.native-decorations.title": {
"message": "ของตกแต่งดั้งเดิม"
},
"app.appearance-settings.select-option": {
"message": "เลือกตัวเลิือก"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "เปิดใช้งานตัวเลือกในการเลือกย่อหรือขยายแถบด้านข้าง"
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "ย่อแถบด้านข้าง"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "หากคุณกำลังติดตั้งไฟล์แพ็กของ Modrinth (.mrpack) ที่ไม่ได้โฮสต์โดยตรงบน Modrinth เราจะแจ้งให้คุณตรวจสอบให้มั่นใจก่อนที่จะติดตั้งไฟล์แพ็กดังกล่าว"
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "เตือนฉันก่อนติดตั้งแพ็กม็อดที่ไม่รู้จัก"
},
"app.auth-servers.unreachable.body": {
"message": "เซิร์ฟเวอร์ตรวจสอบสิทธิ์ของ Minecraft อาจใช้งานไม่ได้ในขณะนี้ โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณและลองใหม่อีกครั้งในภายหลัง"
},
"app.auth-servers.unreachable.header": {
"message": "ไม่สามารถเชื่อมต่อถึงเซิร์ฟเวอร์ได้"
},
"app.browse.add-server-to-instance": {
"message": "เพิ่มเซิร์ฟเวอร์ลงในอินสแตนซ์"
},
"app.browse.add-servers-to-instance": {
"message": "เพิ่มเซิร์ฟเวอร์ลงในอินสแตนซ์ของคุณ"
},
"app.browse.add-to-instance": {
"message": "เพิ่มลงในโปรแกรม"
"message": "เพิ่มลงในอินสแตนซ์"
},
"app.browse.add-to-instance-name": {
"message": "เพิ่มลงใน {instanceName}"
@@ -116,77 +23,44 @@
"app.browse.already-added": {
"message": "เพิ่มแล้ว"
},
"app.browse.discover-content": {
"message": "สำรวจเนื้อหา"
},
"app.browse.discover-servers": {
"message": "สำรวจเซิร์ฟเวอร์"
},
"app.browse.hide-added-servers": {
"message": "ซ่อนเซิร์ฟเวอร์ที่ดำเนินการเพิ่มแล้ว"
},
"app.browse.project-type.modpacks": {
"message": "แพ็กม็อด"
},
"app.browse.server.installing": {
"message": "กำลังติดตั้ง"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "กำลังติดตั้งแพ็กม็อด..."
"app.browse.install-content-to-instance": {
"message": "ติดตั้งเนื้อหาลงในอินสแตนซ์"
},
"app.export-modal.description-placeholder": {
"message": "กรอกคำอธิบายมอดแพ็ก.."
},
"app.export-modal.export-button": {
"message": "ส่งออก"
},
"app.export-modal.header": {
"message": "ส่งออกแพ็กม็อด"
},
"app.export-modal.include-file-accessibility-label": {
"message": "รวมถึง \"{file}\" ใช่หรือไม่"
},
"app.export-modal.modpack-name-label": {
"message": "ชื่อแพ็กม็อด"
"message": "ชื่อมอดแพ็ก"
},
"app.export-modal.modpack-name-placeholder": {
"message": "ชื่อแพ็กม็อด"
},
"app.export-modal.select-files-label": {
"message": "กำหนดไฟล์ที่ต้องการจะส่งออก"
},
"app.export-modal.version-number-label": {
"message": "หมายเลขเวอร์ชัน"
"message": "ชื่อ ม็อดแพ็ก"
},
"app.export-modal.version-number-placeholder": {
"message": "1.0.0"
},
"app.instance.confirm-delete.admonition-body": {
"message": "ข้อมูลทั้งหมดที่ถูกเก็บไว้ในโปรแกรมจะถูกลบออกอย่างถาวร รวมถึงโลก การตั้งค่า และเนื้อหาอื่นที่ติดตั้งไว้ทั้งหมด"
"message": "ข้อมูลทั้งหมดในอินสแตนซ์ของคุณจะถูกลบอย่างถาวร รวมถึงโลก การตั้งค่า และเนื้อหาที่ติดตั้งไว้ทั้งหมด"
},
"app.instance.confirm-delete.admonition-header": {
"message": "การดำเนินการนี้ไม่สามารถย้อนคืนได้"
},
"app.instance.confirm-delete.delete-button": {
"message": "ลบโปรแกรม"
"message": "ลบอินสแตนซ์"
},
"app.instance.confirm-delete.header": {
"message": "ลบโปรแกรม"
"message": "ลบอินสแตนซ์"
},
"app.instance.modpack-already-installed.body": {
"message": "แพ็กม็อดดังกล่าวติดตั้งไว้ใน <bold>{instanceName}</bold> อยู่แล้ว คุณแน่ใจหรือไม่ว่าต้องการจะทำซ้ำ?"
"message": "มอดแพ็กนี้ถูกติดตั้งไว้ในอินสแตนซ์ <bold>{instanceName}</bold> อยู่แล้ว คุณแน่ใจหรือไม่ว่าต้องการสร้างสำเนาซ้ำ?"
},
"app.instance.modpack-already-installed.create": {
"message": "สร้าง"
},
"app.instance.modpack-already-installed.header": {
"message": "แพ็กม็อดดังกล่าวถูกติดตั้งไปแล้ว"
"message": "ติดตั้งมอดแพ็กนี้อยู่แล้ว"
},
"app.instance.modpack-already-installed.instance": {
"message": "โปรแกรม"
"message": "อินสแตนซ์"
},
"app.instance.mods.content-type-project": {
"message": "โปรเจกต์"
@@ -198,10 +72,7 @@
"message": "เพิ่ม {count} โปรเจกต์เรียบร้อยแล้ว"
},
"app.instance.mods.share-text": {
"message": "มาตรวจดูโปรเจกต์ต่าง ๆ ที่ฉันใช้ในแพ็กม็อดนี้กัน!"
},
"app.instance.mods.share-title": {
"message": "แบ่งปันเนื้อหาแพ็กม็อด"
"message": "มาตรวจดูโปรเจกต์ต่าง ๆ ที่ฉันใช้ในมอดแพ็กนี้กัน!"
},
"app.instance.mods.successfully-uploaded": {
"message": "อัปโหลดสำเร็จ"
@@ -210,16 +81,16 @@
"message": "เพิ่มเซิร์ฟเวอร์"
},
"app.instance.worlds.browse-servers": {
"message": "ค้นหาเซิร์ฟเวอร์"
"message": "ค้นหาเซิฟเวอร์"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' จะ**ถูกลบออกอย่างถาวร** และจะไม่มีทางสามารถกู้คืนได้"
"message": "'{name}' จะถูกลบอย่างถาวร** และจะไม่สามารถกู้คืนได้**"
},
"app.instance.worlds.delete-world-title": {
"message": "คุณแน่ใจหรือไม่ว่าต้องการลบโลกนี้อย่างถาวร?"
},
"app.instance.worlds.filter-modded": {
"message": "มอด"
"message": "แบบมีมอด"
},
"app.instance.worlds.filter-offline": {
"message": "ออฟไลน์"
@@ -228,31 +99,31 @@
"message": "ออนไลน์"
},
"app.instance.worlds.filter-vanilla": {
"message": "วานิลลา"
"message": "ต้นฉบับ"
},
"app.instance.worlds.no-worlds-description": {
"message": "เพิ่มเซิร์ฟเวอร์หรือค้นหาเพื่อเริ่มต้น"
"message": "เพิ่มเซิร์ฟเวอร์หรือสำรวจเพื่อเริ่มต้น"
},
"app.instance.worlds.no-worlds-heading": {
"message": "ไม่มีเซิร์ฟเวอร์หรือโลกที่เพิ่มไว้"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' จะถูกลบออกจากรายการของคุณ ซึ่งจะรวมถึงในเกม และการลบดังกล่าวจะไม่มีทางกู้คืนได้อีก"
"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": "เซิร์ฟเวอร์นี้"
"message": "เซิร์ฟเวอร์อันนี้"
},
"app.modal.install-to-play.content-required": {
"message": "เนื้อหาที่จำเป็น"
"message": "จำเป็นต้องมีเนื้อหา"
},
"app.modal.install-to-play.header": {
"message": "ติดตั้งเพื่อเล่น"
@@ -260,20 +131,17 @@
"app.modal.install-to-play.install-button": {
"message": "ติดตั้ง"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# ม็อด}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "มอดแพ็กที่จำเป็น"
},
"app.modal.install-to-play.server-requires-mods": {
"message": "เซิร์ฟเวอร์ดังกล่าวจำเป็นต้องใช้มอดเพื่อเล่น โปรดติดตั้งและตั้งค่าไฟล์อื่นใดที่จำเป็นจาก Modrinth ก่อน จากนั้นึงจะสามารถเข้าเล่นเซิร์ฟเวอร์ได้"
"message": "เซิร์ฟเวอร์นี้จำเป็นต้องใช้มอดเพื่อเข้าเล่น คลิกติดตั้ง เพื่อตั้งค่าไฟล์ที่จำเป็นจาก Modrinth จากนั้นึงจะเข้าสู่เซิร์ฟเวอร์โดยตรงได้"
},
"app.modal.install-to-play.shared-instance": {
"message": "โปรแกรมที่มีร่วมกัน"
"message": "อินสแตนซ์ที่ใช้ร่วมกัน"
},
"app.modal.install-to-play.shared-server-instance": {
"message": "เซิร์ฟเวอร์ของโปรแกรมที่มีร่วมกัน"
"message": "อินสแตนซ์เซิร์ฟเวอร์ที่ใช้ร่วมกัน"
},
"app.modal.install-to-play.view-contents": {
"message": "ดูเนื้อหา"
@@ -291,13 +159,13 @@
"message": "โหมดนักพัฒนาเปิดอยู่"
},
"app.settings.downloading": {
"message": "ดาวน์โหลดเวอร์ชัน {version}"
"message": "ดาวน์โหลด เวอร์ชัน{version}"
},
"app.settings.tabs.appearance": {
"message": "หน้าตา"
},
"app.settings.tabs.default-instance-options": {
"message": "ตั้งค่าโปรแกรมเริ่มต้น"
"message": "ตัวเลือกอินสแตนซ์เริ่มต้น"
},
"app.settings.tabs.java-installations": {
"message": "การจัดการ Java ที่ติดตั้ง"
@@ -312,28 +180,22 @@
"message": "การจัดการทรัพยากร"
},
"app.update-popup.body": {
"message": "Modrinth v{version} พร้อมสำหรับการติดตั้งแล้ว! เปิดโปรแกรมใหม่อีกครั้งเพื่ออัปเดตตอนนี้ หรือจะรออัปเดตอัตโนมัติ ซึ่งจะเกิดขึ้นเมื่อคุณกดปิดโปรแกรม Modrinth"
"message": "แอฟ Modrinth v{version} พร้อมสำหรับการติดตั้งแล้ว! รีโหลดแอปเพื่ออัปเดต หรือ อัปเดตอัตโนมัติเมื่อคุณปิดแอฟ Modrinth"
},
"app.update-popup.body.download-complete": {
"message": "ดาวน์โหลด Modrinth v{version} สำเร็จแล้ว เปิดโปรแกรมใหม่อีกครั้งเพื่ออัปเดตตอนนี้ หรือจะรออัปเดตอัตโนมัติ ซึ่งจะเกิดขึ้นเมื่อคุณกดปิดโปรแกรม Modrinth"
"message": "แอฟ Modrinth v{version} ดาวน์โหลดเสร็จแล้ว รีโหลดแอปเพื่ออัปเดต หรือ อัปเดตอัตโนมัติเมื่อคุณปิดแอฟ Modrinth"
},
"app.update-popup.body.linux": {
"message": "Modrinth v{version} พร้อมให้อัปเดตแล้ว โปรดไปที่การจัดการแพ็กเกจของคุณสำหรับการอัปเดตคุณสมบัติใหม่และการแก้ไข่าง ๆ"
"message": "Modrinth App v{version} พร้อมให้อัปเดตแล้ว โปรดใช้ตัวจัดการแพ็กเกจ ของคุณเพื่ออัปเดตฟีเจอร์ใหม่และตัวแก้ไข่าสุด!"
},
"app.update-popup.body.metered": {
"message": "Modrinth v{version} พร้อมให้บริการแล้ว! แต่เนื่องจากคุณเปิดโหมดจำกัดปริมาณการใช้ข้อมูลเครือข่าย เราจึงไม่ได้อัปเดตเนื้อหาโดยอัตโนมัติ"
},
"app.update-popup.changelog": {
"message": "บันทึกการเปลี่ยนแปลง"
"message": "แอฟ Modrinth v{version} พร้อมแล้ว! เนื่องจากคุณเปิดโหมดจำกัดปริมาณข้อมูล เราจึงไม่ได้อัปเดตอัตโนมัติ"
},
"app.update-popup.download": {
"message": "ดาวโหลด ({size})"
},
"app.update-popup.download-complete": {
"message": "การดาวน์โหลดเสร็จสมบูรณ์"
},
"app.update-popup.reload": {
"message": "โหลดใหม่"
"message": "ดาวน์โหลดเสร็จสมบูรณ์"
},
"app.update-popup.title": {
"message": "มีการอัปเดตใหม่"
@@ -345,13 +207,13 @@
"message": "เวอร์ชั่น {version} ถูกติดตั้งแล้ว"
},
"app.update.download-update": {
"message": "ดาวน์โหลดอัเดต"
"message": "ดาวน์โหลดอัเดต"
},
"app.update.downloading-update": {
"message": "ดาวน์โหลดอัเดตไปแล้ว ({percent}%)"
"message": "ดาวน์โหลดอัเดตไปแล้ว ({percent}%)"
},
"app.update.reload-to-update": {
"message": "รีโหลดเพื่อติดตั้งอัเดต"
"message": "รีโหลดเพื่อติดตั้งอัเดต"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
@@ -360,10 +222,7 @@
"message": "เลือกตัวเลือก"
},
"app.world.world-item.incompatible-version": {
"message": "ไม่สามารถเข้ากับเวอร์ชัน {version} ได้"
},
"app.world.world-item.not-played-yet": {
"message": "ยังไม่เคยเล่น"
"message": "เวอร์ชัน {version} ไม่รองรับ"
},
"app.world.world-item.offline": {
"message": "ออฟไลน์"
@@ -374,9 +233,6 @@
"friends.action.add-friend": {
"message": "เพิ่มเพื่อน"
},
"friends.action.view-friend-requests": {
"message": "คำขอเป็นเพื่อน {count} {count, plural, other {คน}}"
},
"friends.add-friend.submit": {
"message": "ส่งคำขอเป็นเพื่อน"
},
@@ -384,13 +240,13 @@
"message": "กำลังเพิ่มเพื่อน"
},
"friends.add-friend.username.description": {
"message": "มันอาจแตกต่างจากชื่อในเกม Minecraft ของพวกเขา!"
"message": "อาจจะแตกต่างกับชื่อใน Minecraft"
},
"friends.add-friend.username.placeholder": {
"message": "กรอกชื่อผู้ใช้ Modrinth..."
"message": "ใส่ชื่อบน Modrinth"
},
"friends.add-friend.username.title": {
"message": "เพื่อนของคุณมีชื่อผู้ใช้ Modrinth ว่าอะไร?"
"message": "เพื่อนของคุณมีชื่อบน Modrinth ว่าอะไร"
},
"friends.add-friends-to-share": {
"message": "<link>เพิ่มเพื่อน</link> เพื่อเห็นว่าพวกเขากำลังเล่นอะไรอยู่!"
@@ -417,13 +273,13 @@
"message": "ออฟไลน์"
},
"friends.heading.online": {
"message": "ออไลน์"
"message": "ออไลน์"
},
"friends.heading.pending": {
"message": "กําลังดำเนินการ"
},
"friends.no-friends-match": {
"message": "ไม่มีรายชื่อเพื่อนที่ตรงกับ \"{query}\""
"message": "ไม่มีเพื่อนชื่อว่า \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "ค้นหาเพื่อน"
@@ -465,7 +321,7 @@
"message": "โลก Minecraft"
},
"instance.edit-world.reset-icon": {
"message": "รีเซ็ตรูปสัญลักษณ์"
"message": "รีเซ็ตรูปตัวอย่าง"
},
"instance.edit-world.title": {
"message": "แก้ไขโลก"
@@ -492,13 +348,13 @@
"message": "ทั่วไป"
},
"instance.settings.tabs.general.delete": {
"message": "ลบโปรแกรม"
"message": "ลบอินสแตนซ์"
},
"instance.settings.tabs.general.delete.button": {
"message": "ลบโปรแกรม"
"message": "ลบอินสแตนซ์"
},
"instance.settings.tabs.general.delete.description": {
"message": "การกระทำดังกล่าวจะลบโปรแกรมออกจากอุปกรณ์ของคุณอย่างถาวร รวมถึงโลก การตั้งค่า และเนื้อหาทั้งหมดที่ติดตั้งไว้ โปรดระมัดระวัง เนื่องจากเมื่อถูกลบแล้วจะไม่สามารถกู้คืนได้อีก"
"message": "ลบอินสแตนซ์ออกจากอุปกรณ์ของคุณอย่างถาวร รวมถึงโลก การตั้งค่า และเนื้อหาทั้งหมดที่ติดตั้งไว้ โปรดระมัดระวัง เนื่องจากเมื่อถูกลบแล้วจะไม่สามารถกู้คืนได้อีก"
},
"instance.settings.tabs.general.deleting.button": {
"message": "กำลังลบ..."
@@ -510,31 +366,31 @@
"message": "ไม่สามารถทำซ้ำได้ขณะติดตั้ง"
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "สร้างสำเนาโปรแกรม"
"message": "สร้างสำเนาอินสแตนซ์"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "สร้างสำเนาของโปรแกรมนี้ รวมถึงโลก การตั้งค่า ม็อด และอื่น ๆ"
"message": "สร้างสำเนาของอินสแตนซ์นี้ รวมถึงโลก การตั้งค่า ม็อด และอื่นๆ"
},
"instance.settings.tabs.general.edit-icon": {
"message": "แก้ไขสัญลักษณ์"
"message": "แก้ไขไอคอน"
},
"instance.settings.tabs.general.edit-icon.remove": {
"message": "ลบสัญลักษณ์"
"message": "ลบไอคอน"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "แทนที่สัญลักษณ์"
"message": "เปลี่ยนไอคอน"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "เลือกรูปสัญลักษณ์"
"message": "เลือกรูปไอคอน"
},
"instance.settings.tabs.general.library-groups": {
"message": "จัดกลุ่ม"
"message": "การจัดกลุ่ม"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "สร้างกลุ่มใหม่"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "การจัดกลุ่มจะช่วยจัดระเบียบโปรแกรมของคุณภายในหน้ารายการให้เป็นหมวดหมู่"
"message": "กลุ่มไลบรารีช่วยให้คุณจัดระเบียบอินสแตนซ์ของคุณเป็นหมวดหมู่ต่างๆ ภายในไลบรารี"
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "ใส่ชื่อกลุ่ม"
@@ -543,58 +399,43 @@
"message": "ชื่อ"
},
"instance.settings.tabs.hooks": {
"message": "Launch Hook"
"message": "ตัวดักการปล่อย"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "กำหนด Launch Hook เอง"
"message": "กำหนดเอง"
},
"instance.settings.tabs.hooks.description": {
"message": "Launch Hook จะช่วยให้ผู้เล่นที่ชำนาญสามารถใช้คำสั่งต่อระบบบางอย่างได้ ทั้งก่อนและหลังเกมถูกเปิด"
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Post-exit"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "จะรันหลังจากที่ตัวเกมถูกปิด"
"message": "ตัวดักจับช่วยให้ผู้ใช้ระดับสูงสามารถรันคำสั่งระบบบางอย่างได้ ทั้งก่อนและหลังการเปิดเกม"
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "ป้อนคำสั่ง..."
"message": "ป้อนคำสั่งหลังจบการทำงาน..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Pre-launch"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "จะรันก่อนที่โปรแกรมจะทำงาน"
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "ป้อนคำสั่ง..."
"message": "ก่อนรัน"
},
"instance.settings.tabs.hooks.title": {
"message": "Game Launch Hook"
"message": "ฮุกการเปิดเกม"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Wrapper"
"message": "ตัวห่อหุ้ม"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "คำสั่ง Wrapper สำหรับใช้ในการรันเปิด Minecraft"
"message": "คำสั่งตัวห่อหุ้มสำหรับเปิด ไมน์คราฟต์"
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "ป้อนคำสั่ง Wrapper..."
"message": "ป้อนตัวห่อหุ้มคำสั่ง..."
},
"instance.settings.tabs.installation": {
"message": "การติดตั้ง"
},
"instance.settings.tabs.installation.loader-version": {
"message": "เวอร์ชัน {loader}"
"message": "เวอร์ชัน {loader}"
},
"instance.settings.tabs.java": {
"message": "Java และ หน่วยความจำ"
},
"instance.settings.tabs.java.environment-variables": {
"message": "Environment Variables"
},
"instance.settings.tabs.java.hooks": {
"message": "Hook"
"message": "ฮุก"
},
"instance.settings.tabs.java.java-arguments": {
"message": "อาร์กิวเมนต์ Java"
@@ -608,9 +449,6 @@
"instance.settings.tabs.window": {
"message": "หน้าต่าง"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "การตั้งค่าการปรับแต่งหน้าต่าง"
},
"instance.settings.tabs.window.fullscreen": {
"message": "เต็มหน้าจอ"
},
@@ -636,7 +474,7 @@
"message": "ป้อนความกว้าง..."
},
"instance.worlds.a_minecraft_server": {
"message": "เซิร์ฟเวอร์ Minecraft"
"message": "เซิร์ฟเวอร์ ไมน์คราฟต์"
},
"instance.worlds.cant_connect": {
"message": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้"
@@ -648,7 +486,7 @@
"message": "ไม่ต้องแสดงในหน้าแรก"
},
"instance.worlds.game_already_open": {
"message": "โปรแกรมถูกเปิดอยู่แล้ว"
"message": "อินสแตนซ์ถูกเปิดอยู่แล้ว"
},
"instance.worlds.hardcore": {
"message": "โหมดฮาร์ดคอร์"
@@ -669,75 +507,24 @@
"message": "คุณสามารถเข้าสู่โลกผู้เล่นคนเดียวได้โดยตรงบน Minecraft 1.20 ขึ้นไปเท่านั้น"
},
"instance.worlds.play_instance": {
"message": "เริ่มรันโปรแกรม"
"message": "เริ่มเล่นอินสแตนซ์"
},
"instance.worlds.view_instance": {
"message": "ดูโปรแกรม"
"message": "ดูอินสแตนซ์"
},
"instance.worlds.world_in_use": {
"message": "โลกนี้กำลังถูกใช้งานอยู่"
},
"minecraft-account.add-account": {
"message": "เพิ่มบัญชี"
},
"minecraft-account.label": {
"message": "บัญชี Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "ยังไม่ได้ลงชื่อเข้าใช้"
},
"minecraft-account.remove-account": {
"message": "ลบบัญชี"
},
"minecraft-account.select-account": {
"message": "เลือกบัญชี"
},
"minecraft-account.sign-in": {
"message": "ลงชื่อเข้าใช้ Minecraft"
},
"search.filter.locked.instance": {
"message": "โปรแกรมเป็นผู้กำหนด"
},
"search.filter.locked.instance-game-version.title": {
"message": "เวอร์ชันเกมถูกกำหนดไว้โดยโปรแกรมแล้ว"
},
"search.filter.locked.instance-loader.title": {
"message": "ตัวรันถูกกำหนดไว้โดยโปรแกรมแล้ว"
"message": "อินสแตนซ์เป็นผู้จัดเตรียม"
},
"search.filter.locked.instance.sync": {
"message": "เชื่อมต่อกับโปรแกรม"
"message": "ซิงค์กับอินสแตนซ์"
},
"search.filter.locked.server": {
"message": "เซิร์ฟเวอร์เป็นผู้กำหนด"
},
"search.filter.locked.server-environment.title": {
"message": "มีเพียงม็อดสำหรับฝั่งเครื่องของผู้เล่นเท่านั้นที่สามารถเพิ่มลงในโปรแกรมเซิร์ฟเวอร์ดังกล่าวได้"
"message": "เซิร์ฟเวอร์เป็นผู้จัดเตรียม"
},
"search.filter.locked.server-game-version.title": {
"message": "เวอร์ชันเกมถูกกำหนดโดยเซิร์ฟเวอร์แล้ว"
},
"search.filter.locked.server-loader.title": {
"message": "ตัวรันถูกกำหนดโดยเซิร์ฟเวอร์แล้ว"
},
"unknown-pack-warning-modal.body": {
"message": "ไฟล์จะได้รับการตรวจสอบโดยไม่คำนึงถึงประเภทของไฟล์ (รวมทั้ง .mrpack) เมื่อไฟล์ถูกอัปโหลดขึ้น Modrinth"
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "อย่าแสดงคำเตือนนี้อีก"
},
"unknown-pack-warning-modal.header": {
"message": "ยืนยันการติดตั้ง"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "ดำเนินการติดตั้งต่อไป"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "มัลแวร์มักแฝงตัวมากับไฟล์แพ็กม็อดผ่านการแชร์ผ่านแพลตฟอร์มที่ไม่ใช่แพลตฟอร์มเฉพาะ เช่น ดิสคอร์ด"
},
"unknown-pack-warning-modal.warning.body": {
"message": "เราไม่สามารถค้นหาไฟล์ดังกล่าวได้บน Modrinth พวกเราขอแนะนำอย่างมากกว่าควรติดตั้งไฟล์จากแหล่งที่น่าเชื่อถือเท่านั้น"
},
"unknown-pack-warning-modal.warning.title": {
"message": "แจ้งเตือนไฟล์ไม่รู้จัก"
"message": "เซิร์ฟเวอร์เป็นผู้จัดเตรียมเวอร์ชันเกม"
}
}
+21 -18
View File
@@ -104,6 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "Doğrulama sunucularına erişilemedi"
},
"app.browse.add-server-to-instance": {
"message": "Sunucuyu kurulumu ekle"
},
"app.browse.add-servers-to-instance": {
"message": "Kurulumunuza sunucular ekleyin"
},
"app.browse.add-to-instance": {
"message": "Kuruluma ekle"
},
@@ -125,9 +131,21 @@
"app.browse.hide-added-servers": {
"message": "Zaten eklenmiş sunucuları gizle"
},
"app.browse.hide-installed-content": {
"message": "Zaten yüklenmiş içerikleri gizle"
},
"app.browse.install-content-to-instance": {
"message": "İçeriği kuruluma yükle"
},
"app.browse.project-type.modpacks": {
"message": "Mod paketleri"
},
"app.browse.server.install": {
"message": "Yükle"
},
"app.browse.server.installed": {
"message": "Yüklü"
},
"app.browse.server.installing": {
"message": "Yükleniyor"
},
@@ -152,6 +170,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Modpaketi adı"
},
"app.export-modal.select-files-label": {
"message": "Pakete dahil edilecek dosya ve klasörleri seçin"
},
"app.export-modal.version-number-label": {
"message": "Sürüm numarası"
},
@@ -671,24 +692,6 @@
"instance.worlds.world_in_use": {
"message": "Dünya şu anda kullanımda"
},
"minecraft-account.add-account": {
"message": "Hesap ekle"
},
"minecraft-account.label": {
"message": "Minecraft hesabı"
},
"minecraft-account.not-signed-in": {
"message": "Giriş yapılmadı"
},
"minecraft-account.remove-account": {
"message": "Hesabı kaldır"
},
"minecraft-account.select-account": {
"message": "Hesap seçin"
},
"minecraft-account.sign-in": {
"message": "Minecraft'a giriş yapın"
},
"search.filter.locked.instance": {
"message": "Kurulum tarafından sağlanan"
},
+21 -9
View File
@@ -104,6 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "Не вдається зв’язатися зі серверами автентифікації"
},
"app.browse.add-server-to-instance": {
"message": "Додати сервер до профілю"
},
"app.browse.add-servers-to-instance": {
"message": "Додати сервера до вашого профілю"
},
"app.browse.add-to-instance": {
"message": "Додати до профілю"
},
@@ -125,9 +131,21 @@
"app.browse.hide-added-servers": {
"message": "Скрити наявні сервери"
},
"app.browse.hide-installed-content": {
"message": "Скрити доданий контент"
},
"app.browse.install-content-to-instance": {
"message": "Установити вміст в профіль"
},
"app.browse.project-type.modpacks": {
"message": "Модпаки"
},
"app.browse.server.install": {
"message": "Установлено"
},
"app.browse.server.installed": {
"message": "Встановлено"
},
"app.browse.server.installing": {
"message": "Встановлення"
},
@@ -152,6 +170,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Назва збірки"
},
"app.export-modal.select-files-label": {
"message": "Виберіть файли й теки, щоб додати їх до збірки"
},
"app.export-modal.version-number-label": {
"message": "Номер версії"
},
@@ -671,15 +692,6 @@
"instance.worlds.world_in_use": {
"message": "Світ наразі використовується"
},
"minecraft-account.add-account": {
"message": "Додати обліковий запис"
},
"minecraft-account.remove-account": {
"message": "Видалити обліковий запис"
},
"minecraft-account.select-account": {
"message": "Обрати обліковий запис"
},
"search.filter.locked.instance": {
"message": "Надано профілем"
},
@@ -5,6 +5,12 @@
"app.auth-servers.unreachable.header": {
"message": "Không thể kết nối đến máy chủ xác thực"
},
"app.browse.add-server-to-instance": {
"message": "Thêm máy chủ vào hồ sơ"
},
"app.browse.add-servers-to-instance": {
"message": "Thêm máy chủ vào hồ sơ của bạn"
},
"app.browse.add-to-instance": {
"message": "Thêm vào hồ sơ"
},
@@ -23,6 +29,9 @@
"app.browse.discover-servers": {
"message": "Khám phá máy chủ"
},
"app.browse.install-content-to-instance": {
"message": "Tải nội dung này vào hồ sơ"
},
"app.export-modal.description-placeholder": {
"message": "Thêm miêu tả cho gói modpack..."
},
@@ -38,6 +47,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Tên modpack"
},
"app.export-modal.select-files-label": {
"message": "Chọn tệp và gói để thêm vào gói tài nguyên"
},
"app.export-modal.version-number-label": {
"message": "Phiên bản"
},
+21 -27
View File
@@ -12,7 +12,7 @@
"message": "设为主实例"
},
"app.action-bar.no-instances-running": {
"message": "没有运行中的实例"
"message": "没有实例正在运行"
},
"app.action-bar.offline": {
"message": "离线"
@@ -60,10 +60,10 @@
"message": "默认起始页"
},
"app.appearance-settings.hide-nametag.description": {
"message": "在皮肤页面中禁用你玩家头顶上的名。"
"message": "在皮肤页面中禁用你玩家头顶上的名称标签。"
},
"app.appearance-settings.hide-nametag.title": {
"message": "隐藏名"
"message": "隐藏名称标签"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "在主页的“快速回到”部分包含最近的世界。"
@@ -104,14 +104,14 @@
"app.auth-servers.unreachable.header": {
"message": "无法连接到身份验证服务器"
},
"app.browse.add-servers-to-instance": {
"message": "将服务器添加到实例"
"app.browse.add-server-to-instance": {
"message": "将服务器添加到实例实例"
},
"app.browse.add-to-an-instance": {
"message": "添加到实例"
"app.browse.add-servers-to-instance": {
"message": "将服务器添加到你的实例"
},
"app.browse.add-to-instance": {
"message": "添加到实例"
"message": "添加到实例实例"
},
"app.browse.add-to-instance-name": {
"message": "添加到 {instanceName}"
@@ -120,10 +120,7 @@
"message": "已添加"
},
"app.browse.already-added": {
"message": "已添加"
},
"app.browse.back-to-instance": {
"message": "返回实例"
"message": "已添加"
},
"app.browse.discover-content": {
"message": "发现内容"
@@ -134,11 +131,20 @@
"app.browse.hide-added-servers": {
"message": "隐藏已添加的服务器"
},
"app.browse.hide-installed-content": {
"message": "隐藏已安装的内容"
},
"app.browse.install-content-to-instance": {
"message": "将内容安装到实例中"
},
"app.browse.project-type.modpacks": {
"message": "整合包"
},
"app.browse.server-instance-content-warning": {
"message": "加入服务器时,添加内容可能会破坏兼容性。当更新服务器实例内容时任何添加的内容也将丢失。"
"app.browse.server.install": {
"message": "安装"
},
"app.browse.server.installed": {
"message": "已安装"
},
"app.browse.server.installing": {
"message": "安装中"
@@ -158,9 +164,6 @@
"app.export-modal.header": {
"message": "导出整合包"
},
"app.export-modal.include-file-accessibility-label": {
"message": "包含 \"{file}\""
},
"app.export-modal.modpack-name-label": {
"message": "整合包名称"
},
@@ -168,7 +171,7 @@
"message": "整合包名称"
},
"app.export-modal.select-files-label": {
"message": "配置此导出中包含哪些文件"
"message": "选择要包含在包中的文件和文件"
},
"app.export-modal.version-number-label": {
"message": "版本号"
@@ -299,15 +302,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "需要更新至最新版本才能运行 {name}。请更新后启动游戏。"
},
"app.project.install-button.already-installed": {
"message": "此项目已安装"
},
"app.project.install-context.back-to-browse": {
"message": "返回浏览器"
},
"app.project.install-context.install-content-to-instance": {
"message": "将内容安装到实例中"
},
"app.settings.developer-mode-enabled": {
"message": "开发者模式已启用。"
},
+24 -30
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "無法連線到驗證伺服器"
},
"app.browse.add-servers-to-instance": {
"message": "將伺服器新增實例"
"app.browse.add-server-to-instance": {
"message": "將伺服器新增實例"
},
"app.browse.add-to-an-instance": {
"message": "新增實例"
"app.browse.add-servers-to-instance": {
"message": "將伺服器新增實例"
},
"app.browse.add-to-instance": {
"message": "新增至實例"
@@ -120,10 +120,7 @@
"message": "已新增"
},
"app.browse.already-added": {
"message": "已新增"
},
"app.browse.back-to-instance": {
"message": "回到實例"
"message": "已新增"
},
"app.browse.discover-content": {
"message": "探索內容"
@@ -134,11 +131,20 @@
"app.browse.hide-added-servers": {
"message": "隱藏已新增的伺服器"
},
"app.browse.hide-installed-content": {
"message": "隱藏已安裝的內容"
},
"app.browse.install-content-to-instance": {
"message": "安裝至實例中"
},
"app.browse.project-type.modpacks": {
"message": "模組包"
},
"app.browse.server-instance-content-warning": {
"message": "加入內容可能會導致加入伺服器時發生相容性問題。當你更新伺服器實例內容時,任何新增的內容也會遺失。"
"app.browse.server.install": {
"message": "安裝"
},
"app.browse.server.installed": {
"message": "已安裝"
},
"app.browse.server.installing": {
"message": "安裝中"
@@ -158,9 +164,6 @@
"app.export-modal.header": {
"message": "匯出模組包"
},
"app.export-modal.include-file-accessibility-label": {
"message": "要包含「{file}」嗎?"
},
"app.export-modal.modpack-name-label": {
"message": "模組包名稱"
},
@@ -168,7 +171,7 @@
"message": "模組包名稱"
},
"app.export-modal.select-files-label": {
"message": "設定要包含在此匯出檔案中的檔案"
"message": "選擇要包含在模組包中的檔案與資料夾"
},
"app.export-modal.version-number-label": {
"message": "版本號碼"
@@ -195,7 +198,7 @@
"message": "建立"
},
"app.instance.modpack-already-installed.header": {
"message": "模組包已安裝"
"message": "模組包已安裝"
},
"app.instance.modpack-already-installed.instance": {
"message": "實例"
@@ -299,15 +302,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "需要更新才能遊玩「{name}」。請更新至最新版本以啟動遊戲。"
},
"app.project.install-button.already-installed": {
"message": "這個專案已安裝"
},
"app.project.install-context.back-to-browse": {
"message": "回到瀏覽頁面"
},
"app.project.install-context.install-content-to-instance": {
"message": "將內容安裝至實例"
},
"app.settings.developer-mode-enabled": {
"message": "開發人員模式已啟用。"
},
@@ -699,22 +693,22 @@
"message": "世界正在使用中"
},
"minecraft-account.add-account": {
"message": "新增帳號"
"message": "添加賬號"
},
"minecraft-account.label": {
"message": "Minecraft 號"
"message": "Minecraft 號"
},
"minecraft-account.not-signed-in": {
"message": "未登"
"message": "未登"
},
"minecraft-account.remove-account": {
"message": "移除號"
"message": "移除號"
},
"minecraft-account.select-account": {
"message": "選擇號"
"message": "選擇號"
},
"minecraft-account.sign-in": {
"message": "登 Minecraft"
"message": "登 Minecraft"
},
"search.filter.locked.instance": {
"message": "由該實例提供"
+334 -288
View File
@@ -5,49 +5,46 @@ import {
ClipboardCopyIcon,
ExternalIcon,
GlobeIcon,
PlayIcon,
PlusIcon,
SpinnerIcon,
StopCircleIcon,
} from '@modrinth/assets'
import type { BrowseInstallContentType, CardAction, ProjectType, Tags } from '@modrinth/ui'
import type { CardAction, ProjectType, Tags } from '@modrinth/ui'
import {
BrowsePageLayout,
BrowseSidebar,
commonMessages,
CreationFlowModal,
defineMessages,
getLatestMatchingInstallVersion,
getSelectedInstallPreferences,
getTargetInstallPreferences,
injectNotificationManager,
preferencesDiffer,
provideBrowseManager,
requestInstall,
useBrowseSearch,
useDebugLogger,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import { openUrl } from '@tauri-apps/plugin-opener'
import type { Ref } from 'vue'
import { computed, ref, watch } from 'vue'
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
import type { LocationQuery } from 'vue-router'
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { useAppServerBrowse } from '@/composables/browse/use-app-server-browse'
import {
get_project,
get_project_v3,
get_search_results_v3,
get_version_many,
} from '@/helpers/cache.js'
import { get_project_v3, get_search_results_v3 } from '@/helpers/cache.js'
import { process_listener } from '@/helpers/events'
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata'
import { get_by_profile_path } from '@/helpers/process'
import {
get as getInstance,
get_installed_project_ids as getInstalledProjectIds,
kill,
list as listInstances,
} from '@/helpers/profile.js'
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import { get_profile_worlds } from '@/helpers/worlds'
import type { GameInstance } from '@/helpers/types'
import { add_server_to_profile, get_profile_worlds, getServerLatency } from '@/helpers/worlds'
import { injectContentInstall } from '@/providers/content-install'
import { injectServerInstall } from '@/providers/server-install'
import {
@@ -55,6 +52,7 @@ import {
provideServerInstallContent,
} from '@/providers/setup/server-install-content'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { getServerAddress } from '@/store/install.js'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
@@ -78,27 +76,15 @@ const {
effectiveServerWorldId,
serverContextServerData,
serverContentProjectIds,
queuedServerInstallProjectIds,
queuedServerInstallCount,
selectedServerInstallProjects,
isInstallingQueuedServerInstalls,
queuedInstallProgress,
serverBackUrl,
serverBackLabel,
serverBrowseHeading,
clearQueuedServerInstalls,
removeQueuedServerInstall,
flushQueuedServerInstalls,
discardQueuedServerInstallsAndBack,
installQueuedServerInstallsAndBack,
initServerContext,
watchServerContextChanges,
searchServerModpacks,
getServerProjectVersions,
enforceSetupModpackRoute,
getQueuedServerInstallPlans,
setQueuedServerInstallPlans,
openServerModpackInstallFlow,
installProjectToServer,
onServerFlowBack,
handleServerModpackFlowCreate,
markServerProjectInstalled,
@@ -268,7 +254,6 @@ const instanceFilters = computed(() => {
})
const serverHideInstalled = ref(false)
const hideSelectedServerInstalls = ref(false)
if (route.query.shi) {
serverHideInstalled.value = route.query.shi === 'true'
}
@@ -306,12 +291,6 @@ const serverContextFilters = computed(() => {
filters.push({ type: 'plugin_loader', option: platform })
if (pt === 'mod') filters.push({ type: 'environment', option: 'server' })
if (hideSelectedServerInstalls.value && queuedServerInstallProjectIds.value.size > 0) {
for (const id of queuedServerInstallProjectIds.value) {
filters.push({ type: 'project_id', option: `project_id:${id}`, negative: true })
}
}
}
if (pt === 'modpack') {
@@ -334,24 +313,134 @@ const combinedProvidedFilters = computed(() =>
isServerContext.value ? serverContextFilters.value : instanceFilters.value,
)
const {
serverPings,
contextMenuRef,
updateServerHits,
getServerModpackContent,
getServerCardActions,
handleRightClick,
handleOptionsClick,
} = useAppServerBrowse({
instance,
isFromWorlds,
allInstalledIds,
newlyInstalled,
installingServerProjects,
playServerProject,
showAddServerToInstanceModal,
handleError,
router,
const serverPings = shallowRef<Record<string, number | undefined>>({})
const serverPingCache = new Map<string, number | undefined>()
const pendingServerPings = new Map<string, Promise<number | undefined>>()
let serverPingCacheActive = true
const runningServerProjects = ref<Record<string, string>>({})
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
debugLog('checkServerRunningStates', { hitCount: hits.length })
const packs = await listInstances()
const newRunning: Record<string, string> = {}
for (const hit of hits) {
const inst = packs.find((p: GameInstance) => p.linked_data?.project_id === hit.project_id)
if (inst) {
const processes = await get_by_profile_path(inst.path).catch(() => [])
if (Array.isArray(processes) && processes.length > 0) {
newRunning[hit.project_id] = inst.path
}
}
}
debugLog('runningServerProjects updated', newRunning)
runningServerProjects.value = newRunning
}
async function handleStopServerProject(projectId: string) {
debugLog('handleStopServerProject', projectId)
const instancePath = runningServerProjects.value[projectId]
if (!instancePath) return
await kill(instancePath).catch(() => {})
const { [projectId]: _, ...rest } = runningServerProjects.value
runningServerProjects.value = rest
}
async function handlePlayServerProject(projectId: string) {
debugLog('handlePlayServerProject', projectId)
await playServerProject(projectId)
checkServerRunningStates(lastServerHits.value)
}
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
async function handleAddServerToInstance(project: Labrinth.Search.v3.ResultSearchProject) {
debugLog('handleAddServerToInstance', { projectId: project.project_id, name: project.name })
const address = getServerAddress(project.minecraft_java_server)
if (!address) return
if (instance.value) {
try {
await add_server_to_profile(
instance.value.path,
project.name,
address,
'prompt',
project.project_id,
project.minecraft_java_server?.content?.kind,
)
newlyInstalled.value.push(project.project_id)
} catch (err) {
handleError(err as Error)
}
} else {
showAddServerToInstanceModal(project.name, address)
}
}
async function pingServerHits(hits: Labrinth.Search.v3.ResultSearchProject[]) {
debugLog('pingServerHits', { hitCount: hits.length })
const pingsToFetch = hits.flatMap((hit) => {
const address = hit.minecraft_java_server?.address
if (!address) return []
return [{ hit, address }]
})
const nextPings = { ...serverPings.value }
for (const { hit, address } of pingsToFetch) {
if (serverPingCache.has(address)) {
nextPings[hit.project_id] = serverPingCache.get(address)
}
}
serverPings.value = nextPings
await Promise.all(
pingsToFetch.map(async ({ hit, address }) => {
if (serverPingCache.has(address)) return
let pending = pendingServerPings.get(address)
if (!pending) {
pending = getServerLatency(address)
.then((latency) => {
if (serverPingCacheActive) serverPingCache.set(address, latency)
return latency
})
.catch((err) => {
console.error(`Failed to ping server ${address}:`, err)
if (serverPingCacheActive) serverPingCache.set(address, undefined)
return undefined
})
.finally(() => {
pendingServerPings.delete(address)
})
pendingServerPings.set(address, pending)
}
const latency = await pending
if (!serverPingCacheActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
}),
)
}
const unlistenProcesses = await process_listener(
(e: { event: string; profile_path_id: string }) => {
debugLog('process event', e)
if (e.event === 'finished') {
const projectId = Object.entries(runningServerProjects.value).find(
([, path]) => path === e.profile_path_id,
)?.[0]
if (projectId) {
const { [projectId]: _, ...rest } = runningServerProjects.value
runningServerProjects.value = rest
}
}
},
)
onUnmounted(() => {
serverPingCacheActive = false
unlistenProcesses()
serverPingCache.clear()
pendingServerPings.clear()
})
const offline = ref(!navigator.onLine)
@@ -365,13 +454,29 @@ window.addEventListener('online', () => {
})
const messages = defineMessages({
addServerToInstance: {
id: 'app.browse.add-server-to-instance',
defaultMessage: 'Add server to instance',
},
addServersToInstance: {
id: 'app.browse.add-servers-to-instance',
defaultMessage: 'Adding server to instance',
defaultMessage: 'Add servers to your instance',
},
addToAnInstance: {
id: 'app.browse.add-to-an-instance',
defaultMessage: 'Add to an instance',
addToInstance: {
id: 'app.browse.add-to-instance',
defaultMessage: 'Add to instance',
},
addToInstanceName: {
id: 'app.browse.add-to-instance-name',
defaultMessage: 'Add to {instanceName}',
},
added: {
id: 'app.browse.added',
defaultMessage: 'Added',
},
alreadyAdded: {
id: 'app.browse.already-added',
defaultMessage: 'Already added',
},
discoverContent: {
id: 'app.browse.discover-content',
@@ -397,19 +502,26 @@ const messages = defineMessages({
id: 'app.browse.hide-added-servers',
defaultMessage: 'Hide already added servers',
},
hideInstalledContent: {
id: 'app.browse.hide-installed-content',
defaultMessage: 'Hide already installed content',
},
installContentToInstance: {
id: 'app.browse.install-content-to-instance',
defaultMessage: 'Install content to instance',
},
installToServer: {
id: 'app.browse.server.install',
defaultMessage: 'Install',
},
installedToServer: {
id: 'app.browse.server.installed',
defaultMessage: 'Installed',
},
installingToServer: {
id: 'app.browse.server.installing',
defaultMessage: 'Installing',
},
backToInstance: {
id: 'app.browse.back-to-instance',
defaultMessage: 'Back to instance',
},
serverInstanceContentWarning: {
id: 'app.browse.server-instance-content-warning',
defaultMessage:
'Adding content can break compatibility when joining the server. Any added content will also be lost when you update the server instance content.',
},
modLoaderProvidedByInstance: {
id: 'search.filter.locked.instance-loader.title',
defaultMessage: 'Loader is provided by the instance',
@@ -542,6 +654,46 @@ const selectableProjectTypes = computed(() => {
]
})
const getServerModpackContent = (project: Labrinth.Search.v3.ResultSearchProject) => {
const content = project.minecraft_java_server?.content
if (content?.kind === 'modpack') {
const { project_name, project_icon, project_id } = content
if (!project_name) return undefined
return {
name: project_name,
icon: project_icon ?? undefined,
onclick:
project_id !== project.project_id
? () => {
router.push(`/project/${project_id}`)
}
: undefined,
showCustomModpackTooltip: project_id === project.project_id,
}
}
return undefined
}
const contextMenuRef = ref(null)
// @ts-expect-error - no event types
const handleRightClick = (event, result) => {
// @ts-ignore
contextMenuRef.value?.showMenu(event, result, [{ name: 'open_link' }, { name: 'copy_link' }])
}
// @ts-expect-error - no event types
const handleOptionsClick = (args) => {
switch (args.option) {
case 'open_link':
openUrl(`https://modrinth.com/${args.item.project_types?.[0] ?? 'project'}/${args.item.slug}`)
break
case 'copy_link':
navigator.clipboard.writeText(
`https://modrinth.com/${args.item.project_types?.[0] ?? 'project'}/${args.item.slug}`,
)
break
}
}
const installContext = computed(() => {
if (isServerContext.value && serverContextServerData.value) {
return {
@@ -555,15 +707,6 @@ const installContext = computed(() => {
backUrl: serverBackUrl.value,
backLabel: serverBackLabel.value,
heading: serverBrowseHeading.value,
queuedCount: queuedServerInstallCount.value,
selectedProjects: selectedServerInstallProjects.value,
isInstallingSelected: isInstallingQueuedServerInstalls.value,
installProgress: queuedInstallProgress.value,
clearQueued: clearQueuedServerInstalls,
clearSelected: clearQueuedServerInstalls,
onBack: flushQueuedServerInstalls,
discardSelectedAndBack: discardQueuedServerInstallsAndBack,
installSelected: installQueuedServerInstallsAndBack,
}
}
if (instance.value) {
@@ -573,13 +716,13 @@ const installContext = computed(() => {
gameVersion: instance.value.game_version,
iconSrc: instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : null,
backUrl: `/instance/${encodeURIComponent(instance.value.path)}${isFromWorlds.value ? '/worlds' : ''}`,
backLabel: formatMessage(messages.backToInstance),
backLabel: 'Back to instance',
heading: formatMessage(
isFromWorlds.value ? messages.addServersToInstance : commonMessages.installingContentLabel,
isFromWorlds.value ? messages.addServersToInstance : messages.installContentToInstance,
),
warning:
isServerInstance.value && !isFromWorlds.value
? formatMessage(messages.serverInstanceContentWarning)
? 'Adding content can break compatibility when joining the server. Any added content will also be lost when you update the server instance content.'
: undefined,
}
}
@@ -598,82 +741,71 @@ function setProjectInstalling(projectId: string, installing: boolean) {
installingProjectIds.value = next
}
const serverInstallQueue = {
get: getQueuedServerInstallPlans,
set: setQueuedServerInstallPlans,
}
function getCurrentSelectedInstallPreferences(projectTypeValue: string) {
return getSelectedInstallPreferences({
contentType: projectTypeValue,
selectedFilters: searchState.currentFilters.value,
providedFilters: combinedProvidedFilters.value,
overriddenProvidedFilterTypes: searchState.overriddenProvidedFilterTypes.value,
})
}
function getServerInstallTargetPreferences(contentType: BrowseInstallContentType) {
return getTargetInstallPreferences(
{
gameVersion: serverContextServerData.value?.mc_version,
loader: serverContextServerData.value?.loader,
},
contentType,
)
}
function getInstanceInstallTargetPreferences(projectTypeValue: string) {
return getTargetInstallPreferences(
{
gameVersion: instance.value?.game_version,
loader: instance.value?.loader,
},
projectTypeValue,
)
}
async function getInstallProjectVersions(projectId: string) {
const project = await get_project(projectId, 'must_revalidate')
return (await get_version_many(
project.versions,
'must_revalidate',
)) as Labrinth.Versions.v2.Version[]
}
async function chooseInstanceInstallVersion(
project: Labrinth.Search.v2.ResultSearchProject & Labrinth.Search.v3.ResultSearchProject,
projectTypeValue: string,
) {
const targetInstance = instance.value
if (!targetInstance) {
return { versionId: null as string | null }
}
const selectedPreferences = getCurrentSelectedInstallPreferences(projectTypeValue)
const targetPreferences = getInstanceInstallTargetPreferences(projectTypeValue)
if (!preferencesDiffer(selectedPreferences, targetPreferences)) {
return { versionId: null as string | null }
}
const selectedVersion = getLatestMatchingInstallVersion(
await getInstallProjectVersions(project.project_id),
selectedPreferences,
projectTypeValue,
)
if (!selectedVersion) {
return { versionId: null as string | null }
}
return { versionId: selectedVersion.id }
}
function getCardActions(
result: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
currentProjectType: string,
): CardAction[] {
if (currentProjectType === 'server') {
return getServerCardActions(result as Labrinth.Search.v3.ResultSearchProject)
const serverResult = result as Labrinth.Search.v3.ResultSearchProject
const isInstalled = allInstalledIds.value.has(serverResult.project_id)
if (isFromWorlds.value && instance.value) {
return [
{
key: 'add-to-instance',
label: formatMessage(isInstalled ? messages.added : messages.addToInstance),
icon: isInstalled ? CheckIcon : PlusIcon,
disabled: isInstalled,
color: 'brand',
type: 'outlined',
onClick: () => handleAddServerToInstance(serverResult),
},
]
}
const actions: CardAction[] = []
actions.push({
key: 'add',
label: '',
icon: isInstalled ? CheckIcon : PlusIcon,
disabled: isInstalled,
circular: true,
tooltip: isInstalled
? formatMessage(messages.alreadyAdded)
: instance.value
? formatMessage(messages.addToInstanceName, { instanceName: instance.value.name })
: formatMessage(messages.addServerToInstance),
onClick: () => handleAddServerToInstance(serverResult),
})
if (runningServerProjects.value[serverResult.project_id]) {
actions.push({
key: 'stop',
label: formatMessage(commonMessages.stopButton),
icon: StopCircleIcon,
color: 'red',
type: 'outlined',
onClick: () => handleStopServerProject(serverResult.project_id),
})
} else {
const isInstalling = (installingServerProjects.value as string[]).includes(
serverResult.project_id,
)
actions.push({
key: 'play',
label: formatMessage(
isInstalling ? commonMessages.installingLabel : commonMessages.playButton,
),
icon: PlayIcon,
disabled: isInstalling,
color: 'brand',
type: 'outlined',
onClick: () => handlePlayServerProject(serverResult.project_id),
})
}
return actions
}
// Non-server project actions
@@ -685,84 +817,39 @@ function getCardActions(
const isInstalled =
projectResult.installed ||
allInstalledIds.value.has(projectResult.project_id || '') ||
serverContentProjectIds.value.has(projectResult.project_id || '') ||
serverContextServerData.value?.upstream?.project_id === projectResult.project_id
serverContentProjectIds.value.has(projectResult.project_id || '')
const isInstalling = installingProjectIds.value.has(projectResult.project_id)
if (
isServerContext.value &&
['modpack', 'mod', 'plugin', 'datapack'].includes(currentProjectType)
) {
const isQueued = queuedServerInstallProjectIds.value.has(projectResult.project_id)
const isInstallingSelection = isInstallingQueuedServerInstalls.value
const validatingInstall =
isInstalling && currentProjectType !== 'modpack' && !isInstallingSelection
const installLabel = isInstalled
? commonMessages.installedLabel
: isQueued
? isInstalling || isInstallingSelection
? validatingInstall
? commonMessages.validatingLabel
: messages.installingToServer
: commonMessages.selectedLabel
: isInstalling || isInstallingSelection
? validatingInstall
? commonMessages.validatingLabel
: messages.installingToServer
: commonMessages.installButton
return [
{
key: 'install',
label: formatMessage(installLabel),
icon:
isInstalling || isInstallingSelection
? SpinnerIcon
: isQueued || isInstalled
? CheckIcon
: PlusIcon,
iconClass: isInstalling || isInstallingSelection ? 'animate-spin' : undefined,
disabled: isInstalled || isInstalling || isInstallingSelection,
color: isQueued && !isInstalling && !isInstallingSelection ? 'green' : 'brand',
label: formatMessage(
isInstalling
? messages.installingToServer
: isInstalled
? messages.installedToServer
: messages.installToServer,
),
icon: isInstalled ? CheckIcon : PlusIcon,
iconClass: isInstalling ? 'animate-spin' : undefined,
disabled: isInstalled || isInstalling,
color: 'brand',
type: 'outlined',
onClick: async () => {
if (isQueued) {
removeQueuedServerInstall(projectResult.project_id)
return
}
const contentType = currentProjectType as BrowseInstallContentType
const isModpack = contentType === 'modpack'
const shouldShowInstalling = isModpack || !isQueued
if (shouldShowInstalling) {
setProjectInstalling(projectResult.project_id, true)
}
setProjectInstalling(projectResult.project_id, true)
try {
await requestInstall({
project: projectResult,
contentType,
mode: isModpack ? 'immediate' : 'queue',
selectedFilters: isModpack ? [] : searchState.currentFilters.value,
providedFilters: isModpack ? [] : combinedProvidedFilters.value,
overriddenProvidedFilterTypes: isModpack
? []
: searchState.overriddenProvidedFilterTypes.value,
targetPreferences: getServerInstallTargetPreferences(contentType),
getProjectVersions: getInstallProjectVersions,
queue: serverInstallQueue,
install: (plan) =>
openServerModpackInstallFlow({
projectId: plan.projectId,
versionId: plan.versionId,
name: plan.project.name,
iconUrl: plan.project.icon_url ?? undefined,
}),
})
const didInstall = await installProjectToServer(projectResult)
if (didInstall !== false) {
onSearchResultInstalled(projectResult.project_id)
}
} catch (err) {
handleError(err as Error)
} finally {
if (shouldShowInstalling) {
setProjectInstalling(projectResult.project_id, false)
}
setProjectInstalling(projectResult.project_id, false)
}
},
},
@@ -775,15 +862,13 @@ function getCardActions(
return [
{
key: 'install',
label: formatMessage(
isInstalling
? messages.installingToServer
: isInstalled
? commonMessages.installedLabel
: shouldUseInstallIcon
? commonMessages.installButton
: messages.addToAnInstance,
),
label: isInstalling
? 'Installing'
: isInstalled
? 'Installed'
: shouldUseInstallIcon
? 'Install'
: 'Add to an instance',
icon: isInstalling ? SpinnerIcon : isInstalled ? CheckIcon : PlusIcon,
iconClass: isInstalling ? 'animate-spin' : undefined,
disabled: isInstalled || isInstalling,
@@ -791,39 +876,28 @@ function getCardActions(
type: 'outlined',
onClick: async () => {
setProjectInstalling(projectResult.project_id, true)
try {
const selectedInstall = instance.value
? await chooseInstanceInstallVersion(projectResult, currentProjectType)
: { versionId: null as string | null }
if (selectedInstall === null) {
await installVersion(
projectResult.project_id,
null,
instance.value ? instance.value.path : null,
'SearchCard',
(versionId) => {
setProjectInstalling(projectResult.project_id, false)
return
}
const selectedPreferences = getCurrentSelectedInstallPreferences(currentProjectType)
await installVersion(
projectResult.project_id,
selectedInstall.versionId,
instance.value ? instance.value.path : null,
'SearchCard',
(versionId) => {
setProjectInstalling(projectResult.project_id, false)
if (versionId) {
onSearchResultInstalled(projectResult.project_id)
}
},
(profile) => {
router.push(`/instance/${profile}`)
},
{
preferredLoader: instance.value?.loader ?? selectedPreferences.loaders?.[0],
preferredGameVersion:
instance.value?.game_version ?? selectedPreferences.gameVersions?.[0],
},
)
} catch (err) {
if (versionId) {
onSearchResultInstalled(projectResult.project_id)
}
},
(profile) => {
router.push(`/instance/${profile}`)
},
{
preferredLoader: instance.value?.loader ?? undefined,
preferredGameVersion: instance.value?.game_version ?? undefined,
},
).catch((err) => {
setProjectInstalling(projectResult.project_id, false)
handleError(err)
}
})
},
},
]
@@ -863,7 +937,9 @@ async function search(requestParams: string) {
if (isServer) {
const hits = rawResults.result.hits ?? []
updateServerHits(hits)
lastServerHits.value = hits
pingServerHits(hits)
checkServerRunningStates(hits)
return {
projectHits: [],
serverHits: hits,
@@ -948,12 +1024,6 @@ watch(
{ deep: true },
)
watch(queuedServerInstallCount, (count) => {
if (count === 0) {
hideSelectedServerInstalls.value = false
}
})
if (instance.value?.game_version) {
const gv = instance.value.game_version
const alreadyHasGv = searchState.serverCurrentFilters.value.some(
@@ -966,26 +1036,16 @@ if (instance.value?.game_version) {
await searchState.refreshSearch()
function getProjectBrowseQuery() {
if (!installContext.value) return undefined
return {
...route.query,
b: route.fullPath,
}
}
provideBrowseManager({
tags,
projectType,
...searchState,
getProjectLink: (result: Labrinth.Search.v2.ResultSearchProject) => ({
path: `/project/${result.project_id ?? result.slug}`,
query: getProjectBrowseQuery(),
}),
getServerProjectLink: (result: Labrinth.Search.v3.ResultSearchProject) => ({
path: `/project/${result.slug ?? result.project_id}`,
query: getProjectBrowseQuery(),
query: instance.value ? { i: instance.value.path } : undefined,
}),
getServerProjectLink: (result: Labrinth.Search.v3.ResultSearchProject) =>
`/project/${result.slug ?? result.project_id}`,
selectableProjectTypes,
showProjectTypeTabs: computed(() => !isServerContext.value),
variant: 'app',
@@ -1008,18 +1068,8 @@ provideBrowseManager({
() => (isServerContext.value && projectType.value !== 'modpack') || !!instance.value,
),
hideInstalledLabel: computed(() =>
formatMessage(
isFromWorlds.value ? messages.hideAddedServers : commonMessages.hideInstalledContentLabel,
),
formatMessage(isFromWorlds.value ? messages.hideAddedServers : messages.hideInstalledContent),
),
hideSelected: hideSelectedServerInstalls,
showHideSelected: computed(
() =>
isServerContext.value &&
projectType.value !== 'modpack' &&
queuedServerInstallCount.value > 0,
),
hideSelectedLabel: computed(() => formatMessage(commonMessages.hideSelectedContentLabel)),
onInstalled: onSearchResultInstalled,
serverPings,
getServerModpackContent,
@@ -1034,12 +1084,8 @@ provideBrowseManager({
<BrowsePageLayout>
<template #after>
<ContextMenu ref="contextMenuRef" @option-clicked="handleOptionsClick">
<template #open_link>
<GlobeIcon /> {{ formatMessage(commonMessages.openInModrinthButton) }} <ExternalIcon />
</template>
<template #copy_link>
<ClipboardCopyIcon /> {{ formatMessage(commonMessages.copyLinkButton) }}
</template>
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
</ContextMenu>
</template>
</BrowsePageLayout>
@@ -311,7 +311,7 @@ async function updateProject(mod: ContentItem) {
const profile = await get(props.instance.path).catch(handleError)
if (profile) {
await installVersionDependencies(profile, versionData, 'update').catch(handleError)
await installVersionDependencies(profile, versionData).catch(handleError)
}
}
}
@@ -347,7 +347,7 @@ async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions
const profile = await get(props.instance.path).catch(handleError)
if (profile) {
await installVersionDependencies(profile, version, 'update').catch(handleError)
await installVersionDependencies(profile, version).catch(handleError)
}
mod.file_path = newPath
+19 -239
View File
@@ -45,13 +45,7 @@
/>
</Teleport>
<div class="flex flex-col gap-4 p-6">
<div
v-if="projectInstallContext"
class="sticky top-0 z-20 -mx-6 -mt-6 rounded-tl-[--radius-xl] border-0 border-b border-solid bg-surface-1 p-3 border-surface-5"
>
<BrowseInstallHeader :install-context="projectInstallContext" />
</div>
<InstanceIndicator v-if="instance && !projectInstallContext" :instance="instance" />
<InstanceIndicator v-if="instance" :instance="instance" />
<template v-if="data">
<Teleport
v-if="themeStore.featureFlags.project_background"
@@ -70,7 +64,7 @@
<ButtonStyled v-if="serverPlaying" size="large" color="red">
<button @click="handleStopServer">
<StopCircleIcon />
{{ formatMessage(commonMessages.stopButton) }}
Stop
</button>
</ButtonStyled>
<ButtonStyled v-else size="large" color="brand">
@@ -79,18 +73,11 @@
@click="handleClickPlay"
>
<PlayIcon />
{{
data && installingServerProjects.includes(data.id)
? formatMessage(commonMessages.installingLabel)
: formatMessage(commonMessages.playButton)
}}
{{ data && installingServerProjects.includes(data.id) ? 'Installing...' : 'Play' }}
</button>
</ButtonStyled>
<ButtonStyled size="large" circular>
<button
v-tooltip="formatMessage(commonMessages.addServerToInstanceButton)"
@click="handleAddServerToInstance"
>
<button v-tooltip="'Add server to instance'" @click="handleAddServerToInstance">
<PlusIcon />
</button>
</ButtonStyled>
@@ -124,17 +111,13 @@
<template v-else #actions>
<ButtonStyled size="large" color="brand">
<button
v-tooltip="installButtonTooltip"
:disabled="installButtonDisabled"
v-tooltip="installed ? `This project is already installed` : null"
:disabled="installed || installing"
@click="install(null)"
>
<SpinnerIcon
v-if="installButtonLoading && !installButtonInstalled"
class="animate-spin"
/>
<DownloadIcon v-else-if="!installButtonInstalled && !serverProjectSelected" />
<CheckIcon v-else />
{{ installButtonLabel }}
<DownloadIcon v-if="!installed && !installing" />
<CheckIcon v-else-if="installed" />
{{ installing ? 'Installing...' : installed ? 'Installed' : 'Install' }}
</button>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
@@ -183,7 +166,7 @@
:links="[
{
label: 'Description',
href: projectDescriptionHref,
href: `/project/${$route.params.id}`,
},
{
label: 'Versions',
@@ -193,7 +176,7 @@
},
{
label: 'Gallery',
href: projectGalleryHref,
href: `/project/${$route.params.id}/gallery`,
shown: data.gallery.length > 0,
},
]"
@@ -212,39 +195,11 @@
</template>
<template v-else> Project data couldn't not be loaded. </template>
</div>
<SelectedProjectsFloatingBar
v-if="projectInstallContext"
:install-context="projectInstallContext"
/>
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
<template #install>
<DownloadIcon /> {{ formatMessage(commonMessages.installButton) }}
</template>
<template #open_link>
<GlobeIcon /> {{ formatMessage(commonMessages.openInModrinthButton) }} <ExternalIcon />
</template>
<template #copy_link>
<ClipboardCopyIcon /> {{ formatMessage(commonMessages.copyLinkButton) }}
</template>
<template #install> <DownloadIcon /> Install </template>
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
</ContextMenu>
<CreationFlowModal
v-if="serverInstallContent.isServerContext.value && data?.project_type === 'modpack'"
ref="serverSetupModalRef"
:type="
serverInstallContent.serverFlowFrom.value === 'reset-server'
? 'reset-server'
: 'server-onboarding'
"
:available-loaders="['vanilla', 'fabric', 'neoforge', 'forge', 'quilt', 'paper', 'purpur']"
:show-snapshot-toggle="true"
:on-back="serverInstallContent.onServerFlowBack"
:search-modpacks="serverInstallContent.searchServerModpacks"
:get-project-versions="serverInstallContent.getServerProjectVersions"
:get-loader-manifest="getLoaderManifest"
@hide="() => {}"
@browse-modpacks="() => {}"
@create="serverInstallContent.handleServerModpackFlowCreate"
/>
</div>
</template>
@@ -261,16 +216,10 @@ import {
PlayIcon,
PlusIcon,
ReportIcon,
SpinnerIcon,
StopCircleIcon,
} from '@modrinth/assets'
import {
BrowseInstallHeader,
ButtonStyled,
commonMessages,
CreationFlowModal,
defineMessages,
getTargetInstallPreferences,
injectNotificationManager,
NavTabs,
OverflowMenu,
@@ -282,11 +231,7 @@ import {
ProjectSidebarLinks,
ProjectSidebarServerInfo,
ProjectSidebarTags,
requestInstall,
SelectedProjectsFloatingBar,
useVIntl,
} from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { openUrl } from '@tauri-apps/plugin-opener'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
@@ -304,7 +249,6 @@ import {
get_version_many,
} from '@/helpers/cache.js'
import { process_listener } from '@/helpers/events'
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata'
import { get_by_profile_path } from '@/helpers/process'
import {
get as getInstance,
@@ -316,7 +260,6 @@ import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import { getServerLatency } from '@/helpers/worlds'
import { injectContentInstall } from '@/providers/content-install'
import { injectServerInstall } from '@/providers/server-install'
import { createServerInstallContent } from '@/providers/setup/server-install-content'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { getServerAddress } from '@/store/install.js'
import { useTheming } from '@/store/state.js'
@@ -329,22 +272,6 @@ const route = useRoute()
const router = useRouter()
const breadcrumbs = useBreadcrumbs()
const themeStore = useTheming()
const { formatMessage } = useVIntl()
const messages = defineMessages({
backToBrowse: {
id: 'app.project.install-context.back-to-browse',
defaultMessage: 'Back to discover',
},
installContentToInstance: {
id: 'app.project.install-context.install-content-to-instance',
defaultMessage: 'Install content to instance',
},
alreadyInstalled: {
id: 'app.project.install-button.already-installed',
defaultMessage: 'This project is already installed',
},
})
const { installingServerProjects, playServerProject, showAddServerToInstanceModal } =
injectServerInstall()
@@ -369,11 +296,6 @@ const serverPing = ref(undefined)
const serverStatusOnline = ref(false)
const serverInstancePath = ref(null)
const serverPlaying = ref(false)
const serverSetupModalRef = ref(null)
const serverInstallContent = createServerInstallContent({ serverSetupModalRef })
serverInstallContent.watchServerContextChanges()
await serverInstallContent.initServerContext()
const instanceFilters = computed(() => {
if (!instance.value) {
@@ -393,9 +315,11 @@ const instanceFilters = computed(() => {
return { l: loaders, g: instance.value.game_version }
})
function buildProjectHref(path, extraQuery = {}) {
const versionsHref = computed(() => {
const base = `/project/${route.params.id}/versions`
const filters = instanceFilters.value
const params = new URLSearchParams()
for (const [key, val] of Object.entries({ ...route.query, ...extraQuery })) {
for (const [key, val] of Object.entries(filters)) {
if (Array.isArray(val)) {
for (const v of val) params.append(key, v)
} else if (val) {
@@ -403,102 +327,7 @@ function buildProjectHref(path, extraQuery = {}) {
}
}
const qs = params.toString()
return qs ? `${path}?${qs}` : path
}
const projectDescriptionHref = computed(() => buildProjectHref(`/project/${route.params.id}`))
const versionsHref = computed(() =>
buildProjectHref(`/project/${route.params.id}/versions`, instanceFilters.value),
)
const projectGalleryHref = computed(() => buildProjectHref(`/project/${route.params.id}/gallery`))
const projectBrowseBackUrl = computed(() => {
const browsePath = route.query.b
if (typeof browsePath === 'string' && browsePath.startsWith('/browse/')) return browsePath
const type = data.value?.project_type ? `${data.value.project_type}` : 'mod'
return `/browse/${type}`
})
const projectInstallContext = computed(() => {
const serverData = serverInstallContent.serverContextServerData.value
if (serverData) {
return {
name: serverData.name,
loader: serverData.loader ?? '',
gameVersion: serverData.mc_version ?? '',
serverId: serverInstallContent.serverIdQuery.value,
upstream: serverData.upstream,
iconSrc: null,
isMedal: serverData.is_medal,
backUrl: projectBrowseBackUrl.value,
backLabel: formatMessage(messages.backToBrowse),
heading: serverInstallContent.serverBrowseHeading.value,
queuedCount: serverInstallContent.queuedServerInstallCount.value,
selectedProjects: serverInstallContent.selectedServerInstallProjects.value,
isInstallingSelected: serverInstallContent.isInstallingQueuedServerInstalls.value,
installProgress: serverInstallContent.queuedInstallProgress.value,
clearQueued: serverInstallContent.clearQueuedServerInstalls,
clearSelected: serverInstallContent.clearQueuedServerInstalls,
discardSelectedAndBack: serverInstallContent.discardQueuedServerInstallsAndBack,
installSelected: serverInstallContent.installQueuedServerInstallsAndBack,
}
}
if (instance.value) {
return {
name: instance.value.name,
loader: instance.value.loader,
gameVersion: instance.value.game_version,
iconSrc: instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : null,
backUrl: projectBrowseBackUrl.value,
backLabel: formatMessage(messages.backToBrowse),
heading: formatMessage(messages.installContentToInstance),
}
}
return null
})
const serverProjectInstallContext = computed(
() =>
!!serverInstallContent.serverContextServerData.value &&
['modpack', 'mod', 'plugin', 'datapack'].includes(data.value?.project_type),
)
const serverProjectSelected = computed(
() => !!data.value && serverInstallContent.queuedServerInstallProjectIds.value.has(data.value.id),
)
const serverProjectInstalled = computed(
() =>
!!data.value &&
(serverInstallContent.serverContentProjectIds.value.has(data.value.id) ||
serverInstallContent.serverContextServerData.value?.upstream?.project_id === data.value.id),
)
const installButtonLoading = computed(
() => installing.value || serverInstallContent.isInstallingQueuedServerInstalls.value,
)
const installButtonValidating = computed(
() =>
serverProjectInstallContext.value &&
installing.value &&
data.value?.project_type !== 'modpack' &&
!serverInstallContent.isInstallingQueuedServerInstalls.value,
)
const installButtonInstalled = computed(() =>
serverProjectInstallContext.value ? serverProjectInstalled.value : installed.value,
)
const installButtonDisabled = computed(
() => installButtonInstalled.value || installButtonLoading.value,
)
const installButtonLabel = computed(() => {
if (installButtonInstalled.value) return formatMessage(commonMessages.installedLabel)
if (installButtonValidating.value) return formatMessage(commonMessages.validatingLabel)
if (installButtonLoading.value) return formatMessage(commonMessages.installingLabel)
if (serverProjectSelected.value) return formatMessage(commonMessages.selectedLabel)
return formatMessage(commonMessages.installButton)
})
const installButtonTooltip = computed(() => {
if (installButtonInstalled.value) return formatMessage(messages.alreadyInstalled)
return null
return qs ? `${base}?${qs}` : base
})
const [allLoaders, allGameVersions] = await Promise.all([
@@ -670,55 +499,6 @@ watch(
)
async function install(version) {
if (serverProjectInstallContext.value && data.value) {
if (serverProjectSelected.value) {
serverInstallContent.removeQueuedServerInstall(data.value.id)
return
}
if (installButtonDisabled.value) return
installing.value = true
try {
const contentType = data.value.project_type
await requestInstall({
project: {
...data.value,
project_id: data.value.id,
icon_url: data.value.icon_url,
},
contentType,
mode: contentType === 'modpack' ? 'immediate' : 'queue',
selectedFilters: [],
providedFilters: [],
overriddenProvidedFilterTypes: [],
targetPreferences: getTargetInstallPreferences(
{
gameVersion: serverInstallContent.serverContextServerData.value?.mc_version,
loader: serverInstallContent.serverContextServerData.value?.loader,
},
contentType,
),
getProjectVersions: async () => versions.value,
queue: {
get: serverInstallContent.getQueuedServerInstallPlans,
set: serverInstallContent.setQueuedServerInstallPlans,
},
install: (plan) =>
serverInstallContent.openServerModpackInstallFlow({
projectId: plan.projectId,
versionId: plan.versionId,
name: plan.project.title ?? plan.project.name ?? data.value.title,
iconUrl: plan.project.icon_url ?? undefined,
}),
})
} catch (err) {
handleError(err)
} finally {
installing.value = false
}
return
}
installing.value = true
await installVersion(
data.value.id,
@@ -176,10 +176,9 @@ import {
ButtonStyled,
Card,
CopyCode,
useFormatBytes,
useFormatDateTime,
} from '@modrinth/ui'
import { renderString } from '@modrinth/utils'
import { formatBytes, renderString } from '@modrinth/utils'
import { computed, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
@@ -192,7 +191,6 @@ const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const formatBytes = useFormatBytes()
const breadcrumbs = useBreadcrumbs()
@@ -430,7 +430,6 @@ export function createContentInstall(opts: {
await installVersionDependencies(
profile,
version,
'dependency',
(depProject: Labrinth.Projects.v2.Project, depVersion?: Labrinth.Versions.v2.Version) => {
addInstallingItem(instance.id, depProject, depVersion)
installedProjectIds.push(depProject.id)
@@ -486,10 +485,10 @@ export function createContentInstall(opts: {
if (!id) return
await add_project_from_version(id, version.id, 'standalone')
await opts.router.push(`/instance/${encodeURIComponent(id)}`)
await opts.router.push(`/instance/${encodeURIComponent(id)}/`)
const instance = await get(id)
await installVersionDependencies(instance, version, 'dependency')
await installVersionDependencies(instance, version)
trackEvent('InstanceCreate', {
source: 'ProjectInstallModal',
@@ -513,7 +512,7 @@ export function createContentInstall(opts: {
function handleNavigate(instance: ContentInstallInstance) {
modalRef?.hide()
opts.router.push(`/instance/${encodeURIComponent(instance.id)}`)
opts.router.push(`/instance/${encodeURIComponent(instance.id)}/`)
}
function handleCancel() {
@@ -590,7 +589,6 @@ export function createContentInstall(opts: {
await installVersionDependencies(
instance,
version,
'dependency',
(
depProject: Labrinth.Projects.v2.Project,
depVersion?: Labrinth.Versions.v2.Version,
@@ -666,7 +664,7 @@ export function createContentInstall(opts: {
},
handleModpackDuplicateGoToInstance(instancePath: string) {
pendingModpackInstall = null
opts.router.push(`/instance/${encodeURIComponent(instancePath)}`)
opts.router.push(`/instance/${encodeURIComponent(instancePath)}/`)
},
setIncompatibilityWarningModal(ref: IncompatibilityWarningModalRef) {
incompatibilityWarningModalRef = ref
@@ -1,17 +1,6 @@
import { provideFilePicker } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-dialog'
import { readFile } from '@tauri-apps/plugin-fs'
function getFileName(path: string, fallback: string) {
return path.split(/[\\/]/).pop() || fallback
}
async function createFileFromPath(path: string, fallbackName: string, type?: string) {
const bytes = await readFile(path)
const name = getFileName(path, fallbackName)
return new File([bytes], name, type ? { type } : undefined)
}
export function setupFilePickerProvider() {
provideFilePicker({
@@ -23,7 +12,8 @@ export function setupFilePickerProvider() {
if (!result) return null
const path = result.path ?? result
if (!path) return null
const file = await createFileFromPath(path, 'icon')
const name = path.split(/[\\/]/).pop() || 'icon'
const file = new File([], name)
return { file, path, previewUrl: convertFileSrc(path) }
},
async pickModpackFile() {
@@ -34,11 +24,8 @@ export function setupFilePickerProvider() {
if (!result) return null
const path = result.path ?? result
if (!path) return null
const file = await createFileFromPath(
path,
'modpack.mrpack',
'application/x-modrinth-modpack+zip',
)
const name = path.split(/[\\/]/).pop() || 'modpack.mrpack'
const file = new File([], name)
return { file, path, previewUrl: '' }
},
})
@@ -1,35 +1,22 @@
import type { AbstractModrinthClient, Archon, Labrinth } from '@modrinth/api-client'
import type { Archon, Labrinth } from '@modrinth/api-client'
import {
addPendingServerContentInstalls,
type BrowseInstallPlan,
type BrowseSelectedProject,
createContext,
type CreationFlowContextValue,
flushStoredServerAddonInstallQueue,
getStoredServerAddonInstallQueue,
injectModrinthClient,
injectNotificationManager,
type PendingServerContentInstall,
type PendingServerContentInstallType,
readPendingServerContentInstalls,
readStoredServerInstallQueue,
removePendingServerContentInstall,
writePendingServerContentInstallBaseline,
writeStoredServerInstallQueue,
} from '@modrinth/ui'
import { computed, type ComputedRef, nextTick, type Ref, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
type ServerFlowFrom = 'onboarding' | 'reset-server'
type ServerInstallableType = 'modpack' | 'mod' | 'plugin' | 'datapack'
type InstallableSearchResult = Labrinth.Search.v3.ResultSearchProject & {
title?: string
installing?: boolean
installed?: boolean
}
type PendingServerContentInstallInput = Omit<PendingServerContentInstall, 'createdAt'>
export interface ServerModpackSelectionRequest {
interface ServerModpackSelectionRequest {
projectId: string
versionId: string
name: string
@@ -53,19 +40,9 @@ export interface ServerInstallContentContext {
effectiveServerWorldId: ComputedRef<string | null>
serverContextServerData: Ref<Archon.Servers.v0.Server | null>
serverContentProjectIds: Ref<Set<string>>
queuedServerInstallProjectIds: ComputedRef<Set<string>>
queuedServerInstallCount: ComputedRef<number>
selectedServerInstallProjects: ComputedRef<BrowseSelectedProject[]>
isInstallingQueuedServerInstalls: Ref<boolean>
queuedInstallProgress: Ref<{ completed: number; total: number }>
serverBackUrl: ComputedRef<string>
serverBackLabel: ComputedRef<string>
serverBrowseHeading: ComputedRef<string>
clearQueuedServerInstalls: () => void
removeQueuedServerInstall: (projectId: string) => void
flushQueuedServerInstalls: () => Promise<boolean>
discardQueuedServerInstallsAndBack: () => Promise<void>
installQueuedServerInstallsAndBack: () => Promise<boolean>
initServerContext: () => Promise<void>
watchServerContextChanges: () => void
searchServerModpacks: (
@@ -74,11 +51,7 @@ export interface ServerInstallContentContext {
) => Promise<Labrinth.Projects.v2.SearchResult>
getServerProjectVersions: (projectId: string) => Promise<{ id: string }[]>
enforceSetupModpackRoute: (currentProjectType: string | undefined) => void
getQueuedServerInstallPlans: () => Map<string, BrowseInstallPlan<InstallableSearchResult>>
setQueuedServerInstallPlans: (
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
) => void
openServerModpackInstallFlow: (request: ServerModpackSelectionRequest) => Promise<void>
installProjectToServer: (project: InstallableSearchResult) => Promise<boolean>
onServerFlowBack: () => void
handleServerModpackFlowCreate: (config: CreationFlowContextValue) => Promise<void>
markServerProjectInstalled: (id: string) => void
@@ -92,114 +65,6 @@ function readQueryString(value: unknown): string | null {
return typeof value === 'string' && value.length > 0 ? value : null
}
function getQueuedInstallOwnerFallback(project: InstallableSearchResult) {
if (project.organization) {
const ownerId = project.organization_id ?? project.organization
return {
id: ownerId,
name: project.organization,
type: 'organization' as const,
link: `https://modrinth.com/organization/${ownerId}`,
}
}
if (!project.author) return null
const ownerId = project.author_id ?? project.author
return {
id: ownerId,
name: project.author,
type: 'user' as const,
link: `https://modrinth.com/user/${ownerId}`,
}
}
async function getQueuedInstallOwner(
client: AbstractModrinthClient,
project: InstallableSearchResult,
) {
const fallback = getQueuedInstallOwnerFallback(project)
try {
if (project.organization) {
const organization = await client.labrinth.projects_v3.getOrganization(project.project_id)
if (organization) {
return {
id: organization.id,
name: organization.name,
type: 'organization' as const,
avatar_url: organization.icon_url ?? undefined,
link: `https://modrinth.com/organization/${organization.slug}`,
}
}
}
const members = await client.labrinth.projects_v3.getMembers(project.project_id)
const owner =
members.find((member) => member.user.id === project.author_id)?.user ??
members.find((member) => member.is_owner || member.role === 'Owner')?.user ??
members[0]?.user
if (owner) {
return {
id: owner.id,
name: owner.username,
type: 'user' as const,
avatar_url: owner.avatar_url,
link: `https://modrinth.com/user/${owner.username}`,
}
}
} catch {
return fallback
}
return fallback
}
function getQueuedAddonInstallPlans(
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
) {
return Array.from(plans.values()).filter((plan) => plan.contentType !== 'modpack')
}
function getQueuedInstallPlaceholder(
plan: BrowseInstallPlan<InstallableSearchResult>,
owner: PendingServerContentInstallInput['owner'],
): PendingServerContentInstallInput {
const project = plan.project as InstallableSearchResult & { slug?: string | null }
return {
projectId: plan.projectId,
versionId: plan.versionId,
contentType: plan.contentType as PendingServerContentInstallType,
title: project.title ?? project.name ?? 'Project',
versionName: plan.versionName ?? null,
versionNumber: plan.versionNumber ?? null,
fileName: plan.fileName ?? null,
owner,
slug: project.slug ?? plan.projectId,
iconUrl: project.icon_url ?? null,
}
}
function getQueuedInstallPlaceholderFallbacks(
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
) {
return getQueuedAddonInstallPlans(plans).map((plan) =>
getQueuedInstallPlaceholder(plan, getQueuedInstallOwnerFallback(plan.project)),
)
}
async function getQueuedInstallPlaceholders(
client: AbstractModrinthClient,
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
) {
return Promise.all(
getQueuedAddonInstallPlans(plans).map(async (plan) =>
getQueuedInstallPlaceholder(plan, await getQueuedInstallOwner(client, plan.project)),
),
)
}
export function createServerInstallContent(opts: {
serverSetupModalRef: Ref<ServerSetupModalHandle | null>
}) {
@@ -225,22 +90,8 @@ export function createServerInstallContent(opts: {
const serverContextWorldId = ref<string | null>(worldIdQuery.value)
const serverContextServerData = ref<Archon.Servers.v0.Server | null>(null)
const serverContentProjectIds = ref<Set<string>>(new Set())
const serverContentInstallKeys = ref<Set<string>>(new Set())
const queuedServerInstalls = ref<Map<string, BrowseInstallPlan<InstallableSearchResult>>>(
new Map(),
)
const queuedServerInstallProjectIds = computed(() => new Set(queuedServerInstalls.value.keys()))
const queuedServerInstallCount = computed(() => queuedServerInstalls.value.size)
const selectedServerInstallProjects = computed<BrowseSelectedProject[]>(() =>
Array.from(queuedServerInstalls.value.values()).map((plan) => ({
id: plan.projectId,
name: plan.project.title ?? plan.project.name ?? 'Project',
iconUrl: plan.project.icon_url ?? null,
})),
)
const isInstallingQueuedServerInstalls = ref(false)
const queuedInstallProgress = ref({ completed: 0, total: 0 })
const effectiveServerWorldId = computed(() => worldIdQuery.value ?? serverContextWorldId.value)
const serverBackUrl = computed(() => {
const sid = serverIdQuery.value
if (!sid) return '/hosting/manage'
@@ -259,9 +110,9 @@ export function createServerInstallContent(opts: {
})
const serverBrowseHeading = computed(() => {
if (serverFlowFrom.value === 'reset-server') {
return 'Selecting modpack to install after reset'
return 'Select modpack to install after reset'
}
return 'Installing content'
return 'Install content to server'
})
async function resolveServerContextWorldId(serverId: string) {
@@ -283,11 +134,7 @@ export function createServerInstallContent(opts: {
.map((addon) => addon.project_id)
.filter((projectId): projectId is string => !!projectId),
)
const keys = new Set(
(content.addons ?? []).map((addon) => addon.project_id ?? addon.filename),
)
serverContentProjectIds.value = ids
serverContentInstallKeys.value = keys
} catch (err) {
handleError(err as Error)
}
@@ -312,7 +159,6 @@ export function createServerInstallContent(opts: {
}
if (resolvedWorldId) {
queuedServerInstalls.value = readStoredServerInstallQueue(sid, resolvedWorldId)
await refreshServerInstalledContent(sid, resolvedWorldId)
}
}
@@ -322,15 +168,11 @@ export function createServerInstallContent(opts: {
if (!sid) {
serverContextServerData.value = null
serverContentProjectIds.value = new Set()
serverContentInstallKeys.value = new Set()
setQueuedServerInstallPlans(new Map())
return
}
if (sid !== prevSid) {
serverContentProjectIds.value = new Set()
serverContentInstallKeys.value = new Set()
queuedServerInstalls.value = readStoredServerInstallQueue(sid, wid)
try {
serverContextServerData.value = await client.archon.servers_v0.get(sid)
} catch (err) {
@@ -338,16 +180,28 @@ export function createServerInstallContent(opts: {
}
}
if (wid !== prevWid) {
queuedServerInstalls.value = readStoredServerInstallQueue(sid, wid)
}
if (wid && (sid !== prevSid || wid !== prevWid)) {
await refreshServerInstalledContent(sid, wid)
}
})
}
function normalizeLoader(loader: string) {
return loader.toLowerCase().replaceAll('_', '').replaceAll('-', '').replaceAll(' ', '')
}
function getCompatibleLoaders(loader: string) {
const normalized = normalizeLoader(loader)
if (!normalized) return new Set<string>()
if (normalized === 'paper' || normalized === 'purpur' || normalized === 'spigot') {
return new Set(['paper', 'purpur', 'spigot', 'bukkit'])
}
if (normalized === 'neoforge' || normalized === 'neo') {
return new Set(['neoforge', 'neo'])
}
return new Set([normalized])
}
function enforceSetupModpackRoute(currentProjectType: string | undefined) {
if (!isSetupServerContext.value || currentProjectType === 'modpack') return
router.replace({
@@ -394,135 +248,82 @@ export function createServerInstallContent(opts: {
ctx.modal.value?.setStage('final-config')
}
function clearQueuedServerInstalls() {
setQueuedServerInstallPlans(new Map())
}
function removeQueuedServerInstall(projectId: string) {
const nextPlans = new Map(queuedServerInstalls.value)
nextPlans.delete(projectId)
setQueuedServerInstallPlans(nextPlans)
}
function setStoredServerInstallPlans(
serverId: string,
worldId: string,
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
) {
if (serverId === serverIdQuery.value && worldId === effectiveServerWorldId.value) {
queuedServerInstalls.value = plans
function getCurrentServerInstallType(): ServerInstallableType {
const raw = Array.isArray(route.params.projectType)
? route.params.projectType[0]
: route.params.projectType
if (raw === 'modpack' || raw === 'mod' || raw === 'plugin' || raw === 'datapack') {
return raw
}
writeStoredServerInstallQueue(serverId, worldId, plans)
throw new Error('This content type cannot be installed to a server from browse.')
}
async function flushQueuedServerInstalls(
serverId: string | null = serverIdQuery.value,
worldId: string | null = effectiveServerWorldId.value,
) {
if (isInstallingQueuedServerInstalls.value) return false
async function installProjectToServer(project: InstallableSearchResult) {
const contentType = getCurrentServerInstallType()
const sid = serverIdQuery.value
const wid = effectiveServerWorldId.value
if (!sid || !wid) {
throw new Error('No server world is available for install.')
}
if (!serverId || !worldId) {
handleError(new Error('No server world is available for install.'))
if (contentType === 'modpack') {
const versions = await client.labrinth.versions_v2.getProjectVersions(project.project_id, {
include_changelog: false,
})
const versionId = versions[0]?.id ?? project.version_id
if (!versionId) {
throw new Error('No version found for this modpack')
}
await openServerModpackInstallFlow({
projectId: project.project_id,
versionId,
name: project.name,
iconUrl: project.icon_url ?? undefined,
})
return false
}
const queuedPlans = getStoredServerAddonInstallQueue<InstallableSearchResult>(serverId, worldId)
if (queuedPlans.size === 0) return true
const versions = await client.labrinth.versions_v2.getProjectVersions(project.project_id, {
include_changelog: false,
})
const serverLoader = (serverContextServerData.value?.loader ?? '').toLowerCase()
const serverGameVersion = (serverContextServerData.value?.mc_version ?? '').trim()
const compatibleLoaders = getCompatibleLoaders(serverLoader)
isInstallingQueuedServerInstalls.value = true
queuedInstallProgress.value = {
completed: 0,
total: queuedPlans.size,
const hasGameVersionMatch = (version: Labrinth.Versions.v2.Version) =>
!serverGameVersion || version.game_versions.includes(serverGameVersion)
const hasLoaderMatch = (version: Labrinth.Versions.v2.Version) => {
if (contentType === 'datapack') return true
if (compatibleLoaders.size === 0) return true
return version.loaders.some((loader) => compatibleLoaders.has(normalizeLoader(loader)))
}
try {
const result = await flushStoredServerAddonInstallQueue({
serverId,
worldId,
install: (plans) =>
client.archon.content_v1.addAddons(
serverId,
worldId,
plans.map((plan) => ({
project_id: plan.projectId,
version_id: plan.versionId,
})),
),
onQueueChange: (plans) => setStoredServerInstallPlans(serverId, worldId, plans),
})
if (!result.ok) {
for (const plan of result.attemptedPlans) {
removePendingServerContentInstall(serverId, worldId, plan.projectId)
}
handleError(result.error as Error)
return false
}
queuedInstallProgress.value = {
completed: result.flushedPlans.length,
total: result.flushedPlans.length,
}
serverContentProjectIds.value = new Set([
...serverContentProjectIds.value,
...result.flushedPlans.map((plan) => plan.projectId),
])
serverContentInstallKeys.value = new Set([
...serverContentInstallKeys.value,
...result.flushedPlans.map((plan) => plan.projectId),
])
return true
} finally {
isInstallingQueuedServerInstalls.value = false
queuedInstallProgress.value = { completed: 0, total: 0 }
let matchingVersion = versions.find(
(version) => hasGameVersionMatch(version) && hasLoaderMatch(version),
)
if (!matchingVersion) {
matchingVersion = versions.find((version) => hasLoaderMatch(version))
}
}
async function discardQueuedServerInstallsAndBack() {
clearQueuedServerInstalls()
await router.push(serverBackUrl.value)
}
async function installQueuedServerInstallsAndBack() {
const sid = serverIdQuery.value
const wid = effectiveServerWorldId.value
const backUrl = serverBackUrl.value
const plans = new Map(queuedServerInstalls.value)
if (sid && wid) {
writePendingServerContentInstallBaseline(sid, wid, serverContentInstallKeys.value)
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
void getQueuedInstallPlaceholders(client, plans)
.then((items) => {
const pendingProjectIds = new Set(
readPendingServerContentInstalls(sid, wid).map((item) => item.projectId),
)
addPendingServerContentInstalls(
sid,
wid,
items.filter((item) => pendingProjectIds.has(item.projectId)),
)
})
.catch((err) => handleError(err as Error))
if (!matchingVersion) {
matchingVersion = versions.find((version) => hasGameVersionMatch(version))
}
if (!matchingVersion) {
matchingVersion = versions[0]
}
if (!matchingVersion) {
throw new Error('No installable version was found for this project.')
}
await router.push(backUrl)
void flushQueuedServerInstalls(sid, wid)
await client.archon.content_v1.addAddon(sid, wid, {
project_id: matchingVersion.project_id,
version_id: matchingVersion.id,
})
serverContentProjectIds.value = new Set([...serverContentProjectIds.value, project.project_id])
return true
}
function getQueuedServerInstallPlans() {
return queuedServerInstalls.value
}
function setQueuedServerInstallPlans(
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
) {
queuedServerInstalls.value = plans
writeStoredServerInstallQueue(serverIdQuery.value, effectiveServerWorldId.value, plans)
}
function onServerFlowBack() {
serverSetupModalRef.value?.hide()
}
@@ -576,27 +377,15 @@ export function createServerInstallContent(opts: {
effectiveServerWorldId,
serverContextServerData,
serverContentProjectIds,
queuedServerInstallProjectIds,
queuedServerInstallCount,
selectedServerInstallProjects,
isInstallingQueuedServerInstalls,
queuedInstallProgress,
serverBackUrl,
serverBackLabel,
serverBrowseHeading,
clearQueuedServerInstalls,
removeQueuedServerInstall,
flushQueuedServerInstalls,
discardQueuedServerInstallsAndBack,
installQueuedServerInstallsAndBack,
initServerContext,
watchServerContextChanges,
searchServerModpacks,
getServerProjectVersions,
enforceSetupModpackRoute,
getQueuedServerInstallPlans,
setQueuedServerInstallPlans,
openServerModpackInstallFlow,
installProjectToServer,
onServerFlowBack,
handleServerModpackFlowCreate,
markServerProjectInstalled,
+2 -2
View File
@@ -48,7 +48,7 @@ export const isVersionCompatible = (version, project, instance) => {
)
}
export const installVersionDependencies = async (profile, version, reason, onDepInstalling) => {
export const installVersionDependencies = async (profile, version, onDepInstalling) => {
const projectNames = new Map()
const storeProjectName = (p) => {
if (p?.id && p.title) projectNames.set(p.id, p.title)
@@ -177,7 +177,7 @@ export const installVersionDependencies = async (profile, version, reason, onDep
const batch = queuedInstalls.slice(i, i + batchSize)
await Promise.all(
batch.map(async ({ versionId }) => {
await add_project_from_version(profile.path, versionId, reason)
await add_project_from_version(profile.path, versionId, 'dependency')
}),
)
}
+19 -261
View File
@@ -1,6 +1,4 @@
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use tauri::plugin::TauriPlugin;
use tauri::{Manager, PhysicalPosition, PhysicalSize, Runtime};
@@ -16,156 +14,6 @@ pub struct AdsState {
}
const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie";
#[cfg(not(target_os = "linux"))]
const ADS_USER_AGENT: &str = concat!(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ",
"(KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ",
"ModrinthApp/",
env!("CARGO_PKG_VERSION"),
" (Modrinth App)",
);
#[cfg(windows)]
fn ads_user_agent_override_params() -> String {
serde_json::json!({
"userAgent": ADS_USER_AGENT,
"platform": "Win32",
"userAgentMetadata": {
"brands": [
{ "brand": "Chromium", "version": "128" },
{ "brand": "Google Chrome", "version": "128" },
{ "brand": "Modrinth App", "version": env!("CARGO_PKG_VERSION") },
{ "brand": "Not=A?Brand", "version": "99" },
],
"fullVersion": "128.0.0.0",
"fullVersionList": [
{ "brand": "Chromium", "version": "128.0.0.0" },
{ "brand": "Google Chrome", "version": "128.0.0.0" },
{ "brand": "Modrinth App", "version": env!("CARGO_PKG_VERSION") },
{ "brand": "Not=A?Brand", "version": "99.0.0.0" },
],
"platform": "Windows",
"platformVersion": "10.0.0",
"architecture": "x86",
"bitness": "64",
"model": "",
"mobile": false,
},
})
.to_string()
}
#[cfg(windows)]
fn configure_ads_cookie_settings(
core_webview2: &webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2,
) {
use webview2_com::Microsoft::Web::WebView2::Win32::{
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2,
ICoreWebView2_13, ICoreWebView2Profile3,
};
use windows_core::Interface;
match core_webview2
.cast::<ICoreWebView2_13>()
.and_then(|core_webview2| unsafe { core_webview2.Profile() })
.and_then(|profile| profile.cast::<ICoreWebView2Profile3>())
{
Ok(profile) => {
if let Err(error) = unsafe {
profile.SetPreferredTrackingPreventionLevel(
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE,
)
} {
tracing::warn!(
?error,
"Failed to disable ads WebView2 tracking prevention"
);
}
}
Err(error) => {
tracing::warn!(
?error,
"Failed to access ads WebView2 profile tracking prevention settings"
);
}
}
}
fn set_webview_visible<R: Runtime>(
webview: &tauri::Webview<R>,
_visible: bool,
) {
webview
.with_webview(
#[allow(unused_variables)]
move |wv| {
#[cfg(windows)]
{
let controller = wv.controller();
unsafe { controller.SetIsVisible(_visible) }.ok();
}
},
)
.ok();
}
fn set_webview_visible_for_window<R: Runtime>(
app: &tauri::AppHandle<R>,
webview: &tauri::Webview<R>,
visible: bool,
) {
let is_minimized = app
.get_window("main")
.and_then(|window| window.is_minimized().ok())
.unwrap_or(false);
set_webview_visible(webview, visible && !is_minimized);
}
fn sync_webview_visibility_for_main_window<R: Runtime>(
app: &tauri::AppHandle<R>,
main_window: &tauri::Window<R>,
was_minimized: &AtomicBool,
) {
let is_minimized = main_window.is_minimized().unwrap_or(false);
let was = was_minimized.load(Ordering::SeqCst);
if is_minimized == was {
return;
}
was_minimized.store(is_minimized, Ordering::SeqCst);
let ads_visible = if is_minimized {
false
} else {
match app.state::<RwLock<AdsState>>().try_read() {
Ok(state) => state.shown && !state.modal_shown,
Err(_) => false,
}
};
let mut webviews = Vec::new();
let mut seen_webviews = HashSet::new();
for webview in main_window.webviews() {
seen_webviews.insert(webview.label().to_string());
webviews.push(webview);
}
for webview in app.webviews().into_values() {
if seen_webviews.insert(webview.label().to_string()) {
webviews.push(webview);
}
}
for webview in webviews {
let visible =
!is_minimized && (webview.label() != "ads-window" || ads_visible);
set_webview_visible(&webview, visible);
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::<R>::new("ads")
@@ -182,11 +30,10 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
// visible when we refresh, the Aditude wrapper will not make any ad requests
// unless Chromium reports the page as visible. The refresh does not reset the
// visibility state.
let refresh_app = app.clone();
let app = app.clone();
tauri::async_runtime::spawn(async move {
loop {
if let Some(webview) =
refresh_app.webviews().get_mut("ads-window")
if let Some(webview) = app.webviews().get_mut("ads-window")
{
let _ = webview.navigate(AD_LINK.parse().unwrap());
}
@@ -196,34 +43,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
}
});
if let Some(main_window) = app.get_window("main") {
let app_handle = app.clone();
let event_window = main_window.clone();
let was_minimized = Arc::new(AtomicBool::new(false));
main_window.on_window_event(move |_| {
sync_webview_visibility_for_main_window(
&app_handle,
&event_window,
&was_minimized,
);
let delayed_app_handle = app_handle.clone();
let delayed_event_window = event_window.clone();
let delayed_was_minimized = was_minimized.clone();
tauri::async_runtime::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
sync_webview_visibility_for_main_window(
&delayed_app_handle,
&delayed_event_window,
&delayed_was_minimized,
);
});
});
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -284,38 +103,31 @@ pub async fn init_ads_window<R: Runtime>(
webview.show().ok();
webview.set_position(position).ok();
webview.set_size(size).ok();
set_webview_visible_for_window(&app, webview, true);
} else {
webview.hide().ok();
webview
.set_position(PhysicalPosition::new(-1000, -1000))
.ok();
set_webview_visible(webview, false);
}
Some(webview.clone())
} else if let Some(window) = app.get_window("main") {
#[cfg(windows)]
let webview_url =
WebviewUrl::External("about:blank".parse().unwrap());
#[cfg(not(windows))]
let webview_url = WebviewUrl::External(AD_LINK.parse().unwrap());
let webview = window.add_child(
tauri::webview::WebviewBuilder::new("ads-window", webview_url)
.initialization_script_for_all_frames(include_str!(
"ads-init.js"
))
// We use a standard Chrome user agent for compatibility with our ad provider,
// since Tauri is not recognized by ad providers by default.
// Aditude has separately informed SSPs and IVT vendors that this traffic
// originates from a desktop app.
.user_agent(ADS_USER_AGENT)
.zoom_hotkeys_enabled(false)
.transparent(true)
.on_new_window(|_, _| {
tauri::webview::NewWindowResponse::Deny
}),
tauri::webview::WebviewBuilder::new(
"ads-window",
WebviewUrl::External(
AD_LINK.parse().unwrap(),
),
)
.initialization_script_for_all_frames(include_str!("ads-init.js"))
// We use a standard Chrome user agent for compatibility with our ad provider,
// since Tauri is not recognized by ad providers by default.
// Aditude has separately informed SSPs and IVT vendors that this traffic
// originates from a desktop app.
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36")
.zoom_hotkeys_enabled(false)
.transparent(true)
.on_new_window(|_, _| tauri::webview::NewWindowResponse::Deny),
// set both the `hide`/`show` state and `position`,
// to ensure that the webview is actually shown/hidden
if state.shown {
@@ -328,68 +140,15 @@ pub async fn init_ads_window<R: Runtime>(
if state.shown {
webview.show().ok();
set_webview_visible_for_window(&app, &webview, true);
} else {
webview.hide().ok();
set_webview_visible(&webview, false);
}
webview.with_webview(#[allow(unused_variables)] |webview2| {
#[cfg(windows)]
{
use webview2_com::CallDevToolsProtocolMethodCompletedHandler;
use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2_8;
use windows_core::Interface;
use windows_core::HSTRING;
let core_webview2 =
unsafe { webview2.controller().CoreWebView2() };
if let Ok(core_webview2) = core_webview2 {
configure_ads_cookie_settings(&core_webview2);
let navigate_webview = core_webview2.clone();
let handler =
CallDevToolsProtocolMethodCompletedHandler::create(
Box::new(move |result: windows_core::Result<()>, _| {
if let Err(error) = result {
tracing::error!(
?error,
"Failed to override ads user-agent client hints"
);
}
unsafe {
navigate_webview
.Navigate(&HSTRING::from(AD_LINK))
.ok();
}
Ok(())
}) as Box<_>,
);
unsafe {
if let Err(error) = core_webview2
.CallDevToolsProtocolMethod(
&HSTRING::from(
"Emulation.setUserAgentOverride",
),
&HSTRING::from(
ads_user_agent_override_params(),
),
&handler,
)
{
tracing::error!(
?error,
"Failed to install ads user-agent client hints override"
);
core_webview2.Navigate(&HSTRING::from(AD_LINK)).ok();
}
}
}
let webview2_controller = webview2.controller();
let Ok(webview2_8) = unsafe { webview2_controller.CoreWebView2() }
@@ -407,9 +166,9 @@ pub async fn init_ads_window<R: Runtime>(
None
};
if webview.is_none() {
let Some(webview) = webview.clone() else {
return Ok(());
}
};
// tauri::async_runtime::spawn(async move {
// loop {
@@ -490,7 +249,6 @@ pub async fn show_ads_window<R: Runtime>(
webview.set_size(size).ok();
webview.set_position(position).ok();
webview.show().ok();
set_webview_visible_for_window(&app, webview, true);
}
}
+14 -52
View File
@@ -6,7 +6,6 @@
use native_dialog::{DialogBuilder, MessageLevel};
use std::env;
use std::sync::atomic::Ordering;
use tauri::{Listener, Manager};
use tauri_plugin_fs::FsExt;
use theseus::prelude::*;
@@ -97,17 +96,6 @@ fn restart_app(app: tauri::AppHandle) {
app.restart();
}
#[tauri::command]
async fn set_restart_after_pending_update(
should_restart: bool,
) -> api::Result<()> {
let state = State::get().await?;
state
.restart_after_pending_update
.store(should_restart, Ordering::Relaxed);
Ok(())
}
// if Tauri app is called with arguments, then those arguments will be treated as commands
// ie: deep links or filepaths for .mrpacks
fn main() {
@@ -257,7 +245,6 @@ fn main() {
get_update_size,
enqueue_update_for_installation,
remove_enqueued_update,
set_restart_after_pending_update,
toggle_decorations,
show_window,
restart_app,
@@ -275,13 +262,7 @@ fn main() {
#[cfg(feature = "updater")]
if matches!(event, tauri::RunEvent::Exit) {
let update_data = app.state::<PendingUpdateData>().inner();
let should_restart = State::get_if_initialized()
.map(|s| {
s.restart_after_pending_update.load(Ordering::Relaxed)
})
.unwrap_or(false);
if let Some((update, data)) = &*update_data.0.lock().unwrap()
{
if let Some((update, data)) = &*update_data.0.lock().unwrap() {
fn set_changelog_toast(version: Option<String>) {
let toast_result: theseus::Result<()> = tauri::async_runtime::block_on(async move {
let mut settings = settings::get().await?;
@@ -290,46 +271,27 @@ fn main() {
Ok(())
});
if let Err(e) = toast_result {
tracing::warn!(
"Failed to set pending_update_toast: {e}"
)
tracing::warn!("Failed to set pending_update_toast: {e}")
}
}
set_changelog_toast(Some(update.version.clone()));
match update.install(data) {
Ok(()) => {
if should_restart {
tracing::info!(
"Pending update installed successfully (version {}); restarting because user requested reload",
update.version
);
app.restart();
} else {
tracing::info!(
"Pending update installed successfully (version {}); exiting without relaunch (user did not request reload)",
update.version
);
}
}
Err(e) => {
tracing::error!(
"Pending update install failed (version {}): {e}",
update.version
);
set_changelog_toast(None);
if let Err(e) = update.install(data) {
tracing::error!("Error while updating: {e}");
set_changelog_toast(None);
DialogBuilder::message()
.set_level(MessageLevel::Error)
.set_title("Update error")
.set_text(format!("Failed to install update due to an error:\n{e}"))
.alert()
.show()
.unwrap();
}
DialogBuilder::message()
.set_level(MessageLevel::Error)
.set_title("Update error")
.set_text(format!("Failed to install update due to an error:\n{e}"))
.alert()
.show()
.unwrap();
}
app.restart();
}
}
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Opened { urls } = event {
tracing::info!("Handling webview open {urls:?}");
+1 -1
View File
@@ -47,7 +47,7 @@
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@sentry/nuxt": "^10.33.0",
"@tanstack/vue-query": "5.90.7",
"@tanstack/vue-query": "^5.90.7",
"@types/three": "^0.172.0",
"@vitejs/plugin-vue": "^6.0.3",
"@vue-email/components": "^0.0.21",
+61 -47
View File
@@ -17,60 +17,74 @@
</label>
</template>
<script setup lang="ts">
import { useFormatBytes } from '@modrinth/ui'
import { fileIsValid } from '@modrinth/utils'
import { ref } from 'vue'
<script>
import { fileIsValid } from '~/helpers/fileUtils.js'
const props = withDefaults(
defineProps<{
prompt?: string
multiple?: boolean
accept?: string
export default {
components: {},
props: {
prompt: {
type: String,
default: 'Select file',
},
multiple: {
type: Boolean,
default: false,
},
accept: {
type: String,
default: null,
},
/**
* The max file size in bytes
*/
maxSize?: number | null
showIcon?: boolean
shouldAlwaysReset?: boolean
longStyle?: boolean
disabled?: boolean
}>(),
{
prompt: 'Select file',
multiple: false,
showIcon: true,
shouldAlwaysReset: false,
longStyle: false,
disabled: false,
maxSize: {
type: Number,
default: null,
},
showIcon: {
type: Boolean,
default: true,
},
shouldAlwaysReset: {
type: Boolean,
default: false,
},
longStyle: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
)
emits: ['change'],
data() {
return {
files: [],
}
},
methods: {
addFiles(files, shouldNotReset) {
if (!shouldNotReset || this.shouldAlwaysReset) {
this.files = files
}
const emit = defineEmits<{ change: [files: File[]] }>()
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions))
const formatBytes = useFormatBytes()
const files = ref<File[]>([])
function addFiles(incoming: FileList, shouldNotReset = false) {
if (!shouldNotReset || props.shouldAlwaysReset) {
files.value = Array.from(incoming)
}
const validationOptions = { maxSize: props.maxSize, alertOnInvalid: true }
files.value = files.value.filter((file) => fileIsValid(file, validationOptions, formatBytes))
if (files.value.length > 0) {
emit('change', files.value)
}
}
function handleDrop(e: DragEvent) {
addFiles(e.dataTransfer!.files)
}
function handleChange(e: Event) {
const input = e.target as HTMLInputElement
if (!input.files) return
addFiles(input.files)
if (this.files.length > 0) {
this.$emit('change', this.files)
}
},
handleDrop(e) {
this.addFiles(e.dataTransfer.files)
},
handleChange(e) {
this.addFiles(e.target.files)
},
},
}
</script>
@@ -1,12 +1,5 @@
<script setup lang="ts">
import {
BlueskyIcon,
DiscordIcon,
GithubIcon,
MastodonIcon,
ToggleRightIcon,
TwitterIcon,
} from '@modrinth/assets'
import { BlueskyIcon, DiscordIcon, GithubIcon, MastodonIcon, TwitterIcon } from '@modrinth/assets'
import {
AutoLink,
ButtonStyled,
@@ -17,7 +10,6 @@ import {
type MessageDescriptor,
useVIntl,
} from '@modrinth/ui'
import { commonSettingsMessages } from '@modrinth/ui/src/utils/common-messages.js'
import TextLogo from '~/components/brand/TextLogo.vue'
@@ -241,21 +233,11 @@ function developerModeIncrement() {
role="region"
:aria-label="formatMessage(messages.modrinthInformation)"
>
<div class="flex items-center gap-2">
<TextLogo
aria-hidden="true"
class="text-logo button-base h-6 w-auto text-contrast lg:h-8"
@click="developerModeIncrement()"
/>
<ButtonStyled v-if="flags.developerMode" circular type="transparent" color="brand">
<nuxt-link
v-tooltip="formatMessage(commonSettingsMessages.featureFlags)"
to="/settings/flags"
>
<ToggleRightIcon />
</nuxt-link>
</ButtonStyled>
</div>
<TextLogo
aria-hidden="true"
class="text-logo button-base h-6 w-auto text-contrast lg:h-8"
@click="developerModeIncrement()"
/>
<div class="flex flex-wrap justify-center gap-px sm:-mx-2">
<ButtonStyled
v-for="(social, index) in socialLinks"
@@ -1,11 +1,7 @@
<script setup lang="ts">
import { XCircleIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, defineMessages, PagewideBanner, useVIntl } from '@modrinth/ui'
import { defineMessages, PagewideBanner, useVIntl } from '@modrinth/ui'
const { formatMessage } = useVIntl()
const flags = useFeatureFlags()
const tempIgnored = ref(false)
const messages = defineMessages({
title: {
@@ -17,34 +13,16 @@ const messages = defineMessages({
defaultMessage:
"This deploy of Modrinth's frontend failed to generate state from the API. This may be due to an outage or an error in configuration. Rebuild when the API is available. Error codes: {errors}; Current API URL is: {url}",
},
ignoreErrors: {
id: 'layout.banner.build-fail.ignore',
defaultMessage: 'Ignore',
},
alwaysIgnore: {
id: 'layout.banner.build-fail.always-ignore',
defaultMessage: 'Always ignore',
},
})
defineProps<{
errors: any[] | undefined
apiUrl: string
}>()
function alwaysIgnoreBanner() {
flags.value.alwaysIgnoreErrorBanner = true
saveFeatureFlags()
}
</script>
<template>
<PagewideBanner
v-if="
flags.showAllBanners || (errors?.length && !tempIgnored && !flags.alwaysIgnoreErrorBanner)
"
variant="error"
>
<PagewideBanner v-if="errors?.length" variant="error">
<template #title>
<span>{{ formatMessage(messages.title) }}</span>
</template>
@@ -56,19 +34,5 @@ function alwaysIgnoreBanner() {
})
}}
</template>
<template #actions_right>
<ButtonStyled color="red" type="transparent" hover-color-fill="background">
<button @click="alwaysIgnoreBanner">
<XCircleIcon />
{{ formatMessage(messages.alwaysIgnore) }}
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="tempIgnored = true">
<XIcon />
{{ formatMessage(messages.ignoreErrors) }}
</button>
</ButtonStyled>
</template>
</PagewideBanner>
</template>
@@ -13,7 +13,6 @@ import {
const { formatMessage } = useVIntl()
const flags = useFeatureFlags()
const config = useRuntimeConfig()
const route = useRoute()
const messages = defineMessages({
title: {
@@ -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 <link>https://modrinth.com</link>. This preview deploy is used by Modrinth staff for testing purposes. It was built using <branch-link>{owner}/{branch}</branch-link> @ {commit}.`,
},
})
@@ -30,12 +29,10 @@ function hidePreviewBanner() {
flags.value.hidePreviewBanner = true
saveFeatureFlags()
}
const url = computed(() => `https://modrinth.com${route.fullPath}`)
</script>
<template>
<PagewideBanner v-if="!flags.hidePreviewBanner || flags.showAllBanners" variant="info">
<PagewideBanner v-if="!flags.hidePreviewBanner" variant="info">
<template #title>
<span>{{ formatMessage(messages.title) }}</span>
</template>
@@ -48,9 +45,9 @@ const url = computed(() => `https://modrinth.com${route.fullPath}`)
branch: config.public.branch,
}"
>
<template #url>
<a :href="url" target="_blank" rel="noopener" class="text-link">
{{ url }}
<template #link="{ children }">
<a href="https://modrinth.com" target="_blank" rel="noopener" class="text-link">
<component :is="() => normalizeChildren(children)" />
</a>
</template>
<template #branch-link="{ children }">
@@ -78,7 +75,7 @@ const url = computed(() => `https://modrinth.com${route.fullPath}`)
</IntlFormatted>
</span>
</template>
<template #actions_top_right>
<template #actions_right>
<ButtonStyled type="transparent" circular>
<button :aria-label="formatMessage(commonMessages.closeButton)" @click="hidePreviewBanner">
<XIcon aria-hidden="true" />
@@ -47,7 +47,7 @@ function hideRussiaCensorshipBanner() {
<span class="text-xs font-medium">(Перевод на русский)</span>
</nuxt-link>
</ButtonStyled>
<ButtonStyled type="transparent" hover-color-fill="background">
<ButtonStyled>
<nuxt-link to="/news/article/standing-by-our-values">
<BookTextIcon /> Read our full statement
<span class="text-xs font-medium">(English)</span>
@@ -55,7 +55,7 @@ function hideRussiaCensorshipBanner() {
</ButtonStyled>
</div>
</template>
<template #actions_top_right>
<template #actions_right>
<ButtonStyled circular type="transparent">
<button
v-tooltip="formatMessage(commonMessages.closeButton)"
@@ -10,7 +10,6 @@ import {
const { formatMessage } = useVIntl()
const cosmetics = useCosmetics()
const flags = useFeatureFlags()
const messages = defineMessages({
title: {
@@ -30,14 +29,14 @@ function hideStagingBanner() {
</script>
<template>
<PagewideBanner v-if="flags.showAllBanners || !cosmetics.hideStagingBanner" variant="warning">
<PagewideBanner v-if="!cosmetics.hideStagingBanner" variant="warning">
<template #title>
<span>{{ formatMessage(messages.title) }}</span>
</template>
<template #description>
{{ formatMessage(messages.description) }}
</template>
<template #actions_top_right>
<template #actions_right>
<ButtonStyled type="transparent" circular>
<button :aria-label="formatMessage(commonMessages.closeButton)" @click="hideStagingBanner">
<XIcon aria-hidden="true" />
@@ -29,8 +29,8 @@ const messages = defineMessages({
<template #description>
<span>{{ formatMessage(messages.description) }}</span>
</template>
<template #actions_right>
<ButtonStyled color="red">
<template #actions>
<ButtonStyled>
<nuxt-link to="/settings/billing">
<SettingsIcon aria-hidden="true" />
{{ formatMessage(messages.action) }}
@@ -55,7 +55,7 @@ function openTaxForm(e: MouseEvent) {
formatMessage(messages.description, { threshold: formatMoney(taxThreshold) })
}}</span>
</template>
<template #actions_right>
<template #actions>
<ButtonStyled color="orange">
<button @click="openTaxForm"><FileTextIcon /> {{ formatMessage(messages.action) }}</button>
</ButtonStyled>
@@ -29,7 +29,7 @@ const messages = defineMessages({
<template #description>
<span>{{ formatMessage(messages.description) }}</span>
</template>
<template #actions_right>
<template #actions>
<div class="flex w-fit flex-row">
<ButtonStyled color="red">
<nuxt-link to="https://support.modrinth.com" target="_blank" rel="noopener">
@@ -96,12 +96,14 @@ async function handleResendEmailVerification() {
}}
</span>
</template>
<template #actions_right>
<ButtonStyled color="orange">
<button v-if="hasEmail" @click="handleResendEmailVerification">
<template #actions>
<ButtonStyled v-if="hasEmail">
<button @click="handleResendEmailVerification">
{{ formatMessage(verifyEmailBannerMessages.action) }}
</button>
<nuxt-link v-else to="/settings/account">
</ButtonStyled>
<ButtonStyled v-else>
<nuxt-link to="/settings/account">
<SettingsIcon aria-hidden="true" />
{{ formatMessage(addEmailBannerMessages.action) }}
</nuxt-link>
@@ -1,41 +0,0 @@
<script setup lang="ts">
import { PagewideBanner } from '@modrinth/ui'
const flags = useFeatureFlags()
const route = useRoute()
const url = computed(() => `https://modrinth.com${route.fullPath}`)
const bannerRoot = ref<HTMLElement | null>(null)
function onProdLinkClick(e: MouseEvent) {
e.preventDefault()
const el = bannerRoot.value
if (el) {
const { height } = el.getBoundingClientRect()
window.scrollBy({ top: Math.ceil(height), behavior: 'auto' })
}
window.open(url.value, '_blank', 'noopener,noreferrer')
}
</script>
<template>
<div v-if="flags.showViewProdRouteBanner || flags.showAllBanners" ref="bannerRoot">
<PagewideBanner variant="info" slim>
<template #description>
<span>
View route on production:
<a
:href="url"
target="_blank"
rel="noopener noreferrer"
class="text-link"
@click="onProdLinkClick"
>
{{ url }}
</a>
</span>
</template>
</PagewideBanner>
</div>
</template>
@@ -2,29 +2,28 @@
<div
class="flex h-11 items-center justify-between gap-2 rounded-xl bg-button-bg px-4 py-1 text-button-text"
>
<div class="flex min-w-0 flex-1 items-center justify-start gap-2">
<div class="grid max-w-[75%] grid-cols-[auto_1fr_auto] items-center gap-2">
<Avatar v-if="icon" :src="icon" alt="dependency-icon" size="20px" :no-shadow="true" />
<span
v-tooltip="name || projectId"
class="min-w-0 max-w-fit flex-1 truncate font-semibold text-contrast"
>
<span v-tooltip="name || projectId" class="truncate font-semibold text-contrast">
{{ name || 'Unknown Project' }}
</span>
<span
v-if="versionNumber"
v-tooltip="versionNumber"
class="min-w-0 max-w-fit flex-1 truncate whitespace-nowrap text-sm font-medium"
>
{{ versionNumber }}
</span>
<TagItem class="shrink-0 border !border-solid border-surface-5 capitalize">
{{ dependencyType }}
</TagItem>
</div>
<div v-if="!hideRemove" class="flex shrink-0 items-center justify-end gap-1">
<span
v-if="versionName"
v-tooltip="versionName"
class="truncate whitespace-nowrap font-medium"
:class="!hideRemove ? 'max-w-[35%]' : 'max-w-[50%]'"
>
{{ versionName }}
</span>
<div v-if="!hideRemove" class="flex items-center justify-end gap-1">
<ButtonStyled size="standard" :circular="true">
<button aria-label="Remove file" class="-mr-2 !shadow-none" @click="emitRemove">
<XIcon aria-hidden="true" />
@@ -44,12 +43,12 @@ const emit = defineEmits<{
(e: 'remove'): void
}>()
const { projectId, name, icon, dependencyType, versionNumber, hideRemove } = defineProps<{
const { projectId, name, icon, dependencyType, versionName, hideRemove } = defineProps<{
projectId: string
name?: string
icon?: string
dependencyType: Labrinth.Versions.v2.DependencyType
versionNumber?: string
versionName?: string
hideRemove?: boolean
}>()
@@ -8,7 +8,7 @@
:name="dependency.name"
:icon="dependency.icon"
:dependency-type="dependency.dependencyType"
:version-number="dependency.versionNumber"
:version-name="dependency.versionName"
:hide-remove="disableRemove"
@remove="() => removeDependency(index)"
/>
@@ -35,7 +35,7 @@ const addedDependencies = computed(() =>
if (!dep.project_id) return null
const dependencyProject = dependencyProjects.value[dep.project_id]
const versionNumber = dependencyVersions.value[dep.version_id || '']?.version_number ?? ''
const versionName = dependencyVersions.value[dep.version_id || '']?.name ?? ''
if (!dependencyProject && projectsFetchLoading.value) return null
@@ -44,7 +44,7 @@ const addedDependencies = computed(() =>
name: dependencyProject?.name,
icon: dependencyProject?.icon_url,
dependencyType: dep.dependency_type,
versionNumber,
versionName,
}
})
.filter(Boolean),
@@ -3,16 +3,11 @@
v-model="projectId"
placeholder="Select project"
:options="options"
:search-value="selectedProjectOption?.label"
:searchable="true"
search-placeholder="Search by name or paste ID..."
:no-options-message="searchLoading ? 'Loading...' : 'No results found'"
searchable
disable-search-filter
select-search-text-on-focus
:show-chevron="false"
:disable-search-filter="true"
@search-input="(query) => handleSearch(query)"
@search-blur="handleSearchBlur"
@select="handleSelect"
/>
</template>
@@ -20,37 +15,15 @@
import type { ComboboxOption } from '@modrinth/ui'
import { Combobox, injectModrinthClient, injectNotificationManager } from '@modrinth/ui'
import { useDebounceFn } from '@vueuse/core'
import { defineAsyncComponent, h, markRaw, ref, watch } from 'vue'
import { defineAsyncComponent, h } from 'vue'
const { addNotification } = injectNotificationManager()
const projectId = defineModel<string>()
const searchLoading = ref(false)
const options = ref<ComboboxOption<string>[]>([])
const selectedProjectOption = ref<ComboboxOption<string>>()
const selectedProjectSearchQuery = ref('')
const { labrinth } = injectModrinthClient()
let latestSearchQuery = ''
function hitToOption(hit: { title: string; project_id: string; icon_url?: string | null }) {
return {
label: hit.title,
value: hit.project_id,
icon: markRaw(
defineAsyncComponent(() =>
Promise.resolve({
setup: () => () =>
h('img', {
src: hit.icon_url,
alt: hit.title,
class: 'h-5 w-5 rounded',
}),
}),
),
),
}
}
const search = async (query: string) => {
query = query.trim()
@@ -80,77 +53,36 @@ const search = async (query: string) => {
facets: [[`project_id:${query.replace(/[^a-zA-Z0-9]/g, '')}`]], // remove any non-alphanumeric characters
})
if (query !== latestSearchQuery) return
options.value = [...resultsByProjectId.hits, ...results.hits].map(hitToOption)
options.value = [...resultsByProjectId.hits, ...results.hits].map((hit) => ({
label: hit.title,
value: hit.project_id,
icon: markRaw(
defineAsyncComponent(() =>
Promise.resolve({
setup: () => () =>
h('img', {
src: hit.icon_url,
alt: hit.title,
class: 'h-5 w-5 rounded',
}),
}),
),
),
}))
} catch (error: any) {
if (query !== latestSearchQuery) return
addNotification({
title: 'An error occurred',
text: error.data ? error.data.description : error,
type: 'error',
})
}
if (query === latestSearchQuery) {
searchLoading.value = false
}
searchLoading.value = false
}
const throttledSearch = useDebounceFn(search, 250)
const runSearch = async (query: string, debounce: boolean) => {
query = query.trim()
latestSearchQuery = query
if (!query) {
searchLoading.value = false
options.value = []
await throttledSearch(query)
return
}
searchLoading.value = true
await (debounce ? throttledSearch(query) : search(query))
}
const throttledSearch = useDebounceFn(search, 500)
const handleSearch = async (query: string) => {
await runSearch(query, true)
searchLoading.value = true
await throttledSearch(query)
}
const handleSelect = (option: ComboboxOption<string>) => {
selectedProjectOption.value = option
selectedProjectSearchQuery.value = latestSearchQuery
}
const handleSearchBlur = async () => {
if (!projectId.value) return
const selectedOption =
options.value.find((option) => option.value === projectId.value) ??
(selectedProjectOption.value?.value === projectId.value
? selectedProjectOption.value
: undefined)
if (!selectedOption) return
await runSearch(selectedProjectSearchQuery.value || selectedOption.label, false)
if (!options.value.some((option) => option.value === selectedOption.value)) {
options.value = [selectedOption, ...options.value]
}
}
watch(projectId, (value) => {
if (!value) {
selectedProjectOption.value = undefined
selectedProjectSearchQuery.value = ''
return
}
const option = options.value.find((option) => option.value === value)
if (option) {
selectedProjectOption.value = option
}
})
</script>
@@ -8,7 +8,7 @@
:name="dependency.name"
:icon="dependency.icon"
:dependency-type="dependency.dependency_type"
:version-number="dependency.versionNumber"
:version-name="dependency.versionName"
@on-add-suggestion="
() =>
handleAddSuggestion({
@@ -1,31 +1,28 @@
<template>
<div
class="flex h-11 items-center justify-between gap-2 rounded-xl border-2 border-dashed border-surface-5 px-4 py-1 text-button-text"
class="flex items-center justify-between gap-2 rounded-xl border-2 border-dashed border-surface-5 px-4 py-1 text-button-text"
>
<div class="flex min-w-0 flex-1 items-center justify-start gap-2">
<div class="grid max-w-[75%] grid-cols-[auto_1fr_auto] items-center gap-2">
<Avatar v-if="icon" :src="icon" alt="dependency-icon" size="20px" :no-shadow="true" />
<span
v-tooltip="name || 'Unknown Project'"
class="min-w-0 max-w-fit flex-1 truncate font-semibold text-contrast"
>
<span v-tooltip="name || 'Unknown Project'" class="truncate font-semibold text-contrast">
{{ name || 'Unknown Project' }}
</span>
<span
v-if="versionNumber"
v-tooltip="versionNumber"
class="min-w-0 max-w-fit flex-1 truncate whitespace-nowrap text-sm font-medium"
>
{{ versionNumber }}
</span>
<TagItem class="shrink-0 border !border-solid border-surface-5 capitalize">
{{ dependencyType }}
</TagItem>
</div>
<div class="flex shrink-0 items-center justify-end gap-1">
<span
v-if="versionName"
v-tooltip="versionName"
class="max-w-[35%] truncate whitespace-nowrap font-medium"
>
{{ versionName }}
</span>
<div class="flex items-center justify-end gap-1">
<ButtonStyled size="standard" :circular="true" type="transparent">
<button aria-label="Add dependency" class="!shadow-none" @click="emitAddSuggestion">
<PlusIcon aria-hidden="true" />
@@ -44,11 +41,11 @@ const emit = defineEmits<{
(e: 'onAddSuggestion'): void
}>()
const { name, icon, dependencyType, versionNumber } = defineProps<{
const { name, icon, dependencyType, versionName } = defineProps<{
name?: string
icon?: string
dependencyType: Labrinth.Versions.v2.DependencyType
versionNumber?: string
versionName?: string
}>()
function emitAddSuggestion() {
@@ -1,10 +1,11 @@
<template>
<Tabs
<NavTabs
v-if="editingVersion"
value="add-files"
:tabs="editTabs"
class="mb-5 border border-solid border-surface-5 !shadow-none !drop-shadow-none"
@change="setEditTab"
mode="local"
:links="editTabLinks"
:active-index="2"
class="mb-4 border border-solid border-surface-5 shadow-none drop-shadow-none"
@tab-click="setEditTab"
/>
<div class="flex w-full flex-col gap-4">
<template
@@ -98,8 +99,7 @@ import {
defineMessages,
DropzoneFileInput,
injectProjectPageContext,
Tabs,
type TabsTab,
NavTabs,
useVIntl,
} from '@modrinth/ui'
import { acceptFileFromProjectType } from '@modrinth/utils'
@@ -124,14 +124,18 @@ const {
handleNewFiles,
} = injectManageVersionContext()
const editTabs: TabsTab[] = [
{ label: 'Metadata', value: 'metadata' },
{ label: 'Details', value: 'add-details' },
{ label: 'Files', value: 'add-files' },
]
const editTabs = [
{ label: 'Metadata', href: 'metadata', stage: 'metadata' },
{ label: 'Details', href: 'details', stage: 'add-details' },
{ label: 'Files', href: 'files', stage: 'add-files' },
] as const
function setEditTab(tab: TabsTab) {
modal.value?.setStage(tab.value)
const editTabLinks = editTabs.map(({ label, href }) => ({ label, href }))
function setEditTab(index: number) {
const tab = editTabs[index]
if (!tab) return
modal.value?.setStage(tab.stage)
}
function handleRemoveFile(index: number) {
@@ -1,58 +1,182 @@
<template>
<div class="flex w-full max-w-full flex-col gap-3">
<div class="grid gap-2.5">
<span class="font-semibold text-contrast">Project</span>
<DependencySelect v-model="newDependencyProjectId" />
<div class="flex w-full max-w-full flex-col gap-6">
<div class="flex flex-col gap-4">
<span class="font-semibold text-contrast">Add dependency</span>
<div class="flex flex-col gap-3 rounded-2xl border border-solid border-surface-5 p-4">
<div class="grid gap-2.5">
<span class="font-semibold text-contrast">Project</span>
<DependencySelect v-model="newDependencyProjectId" />
</div>
<template v-if="newDependencyProjectId">
<div class="grid gap-2.5">
<span class="font-semibold text-contrast"> Version </span>
<Combobox
v-model="newDependencyVersionId"
placeholder="Select version"
:options="[{ label: 'Any version', value: null }, ...newDependencyVersions]"
:searchable="true"
/>
</div>
<div class="grid gap-2.5">
<span class="font-semibold text-contrast"> Dependency relation </span>
<Combobox
v-model="newDependencyType"
placeholder="Select dependency type"
:options="[
{ label: 'Required', value: 'required' },
{ label: 'Optional', value: 'optional' },
{ label: 'Incompatible', value: 'incompatible' },
{ label: 'Embedded', value: 'embedded' },
]"
/>
</div>
<ButtonStyled color="green">
<button
class="self-start"
:disabled="!newDependencyProjectId"
@click="
() =>
addDependency(
toRaw({
project_id: newDependencyProjectId,
version_id: newDependencyVersionId || undefined,
dependency_type: newDependencyType,
}),
)
"
>
Add Dependency
</button>
</ButtonStyled>
</template>
</div>
</div>
<template v-if="newDependencyProjectId">
<div class="grid gap-2.5">
<span class="font-semibold text-contrast"> Version </span>
<Combobox
v-model="newDependencyVersionId"
placeholder="Select version"
:options="newDependencyVersionOptions"
:search-value="selectedNewDependencyVersionLabel"
:searchable="true"
:select-search-text-on-focus="true"
/>
</div>
<div v-if="visibleSuggestedDependencies.length" class="flex flex-col gap-4">
<span class="font-semibold text-contrast">Suggested dependencies</span>
<SuggestedDependencies @on-add-suggestion="handleAddSuggestedDependency" />
</div>
<div class="grid gap-2.5">
<span class="font-semibold text-contrast"> Dependency relation </span>
<Combobox
v-model="newDependencyType"
placeholder="Select dependency type"
:options="[
{ label: 'Required', value: 'required' },
{ label: 'Optional', value: 'optional' },
{ label: 'Incompatible', value: 'incompatible' },
{ label: 'Embedded', value: 'embedded' },
]"
/>
</div>
</template>
<div v-if="addedDependencies.length" class="flex flex-col gap-4">
<span class="font-semibold text-contrast">Added dependencies</span>
<DependenciesList />
</div>
</div>
</template>
<script lang="ts" setup>
import { Combobox } from '@modrinth/ui'
import { computed } from 'vue'
import type { Labrinth } from '@modrinth/api-client'
import {
ButtonStyled,
Combobox,
injectModrinthClient,
injectNotificationManager,
} from '@modrinth/ui'
import type { ComboboxOption } from '@modrinth/ui/src/components/base/Combobox.vue'
import DependencySelect from '~/components/ui/create-project-version/components/DependencySelect.vue'
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
const { newDependencyProjectId, newDependencyType, newDependencyVersionId, newDependencyVersions } =
injectManageVersionContext()
import DependenciesList from '../components/DependenciesList.vue'
import SuggestedDependencies from '../components/SuggestedDependencies/SuggestedDependencies.vue'
const newDependencyVersionOptions = computed(() => [
{ label: 'Any version', value: null },
...newDependencyVersions.value,
])
const selectedNewDependencyVersionLabel = computed(
() =>
newDependencyVersionOptions.value.find(
(option) => option.value === newDependencyVersionId.value,
)?.label,
const { addNotification } = injectNotificationManager()
const { labrinth } = injectModrinthClient()
const {
draftVersion,
dependencyProjects,
dependencyVersions,
projectsFetchLoading,
visibleSuggestedDependencies,
} = injectManageVersionContext()
const errorNotification = (err: any) => {
addNotification({
title: 'An error occurred',
text: err.data ? err.data.description : err,
type: 'error',
})
}
const newDependencyProjectId = ref<string>()
const newDependencyType = ref<Labrinth.Versions.v2.DependencyType>('required')
const newDependencyVersionId = ref<string | null>(null)
const newDependencyVersions = ref<ComboboxOption<string>[]>([])
// reset to defaults when select different project
watch(newDependencyProjectId, async () => {
newDependencyVersionId.value = null
newDependencyType.value = 'required'
if (!newDependencyProjectId.value) {
newDependencyVersions.value = []
} else {
try {
const versions = await labrinth.versions_v3.getProjectVersions(newDependencyProjectId.value)
newDependencyVersions.value = versions.map((version) => ({
label: version.name,
value: version.id,
}))
} catch (error: any) {
errorNotification(error)
}
}
})
const addedDependencies = computed(() =>
(draftVersion.value.dependencies || [])
.map((dep) => {
if (!dep.project_id) return null
const dependencyProject = dependencyProjects.value[dep.project_id]
const versionName = dependencyVersions.value[dep.version_id || '']?.name ?? ''
if (!dependencyProject && projectsFetchLoading.value) return null
return {
projectId: dep.project_id,
name: dependencyProject?.name,
icon: dependencyProject?.icon_url,
dependencyType: dep.dependency_type,
versionName,
}
})
.filter(Boolean),
)
const addDependency = (dependency: Labrinth.Versions.v3.Dependency) => {
if (!draftVersion.value.dependencies) draftVersion.value.dependencies = []
const alreadyAdded = draftVersion.value.dependencies.some((existing) => {
if (existing.project_id !== dependency.project_id) return false
if (!existing.version_id && !dependency.version_id) return true
return existing.version_id === dependency.version_id
})
if (alreadyAdded) {
addNotification({
title: 'Dependency already added',
text: 'You cannot add the same dependency twice.',
type: 'error',
})
return
}
projectsFetchLoading.value = true
draftVersion.value.dependencies.push(dependency)
newDependencyProjectId.value = undefined
}
const handleAddSuggestedDependency = (dependency: Labrinth.Versions.v3.Dependency) => {
draftVersion.value.dependencies?.push({
project_id: dependency.project_id,
version_id: dependency.version_id,
dependency_type: dependency.dependency_type,
})
}
</script>
@@ -1,10 +1,11 @@
<template>
<Tabs
<NavTabs
v-if="editingVersion"
value="add-details"
:tabs="editTabs"
class="mb-5 border border-solid border-surface-5 !shadow-none !drop-shadow-none"
@change="setEditTab"
mode="local"
:links="editTabLinks"
:active-index="1"
class="mb-4 border border-solid border-surface-5 shadow-none drop-shadow-none"
@tab-click="setEditTab"
/>
<div class="flex w-full flex-col gap-6">
<div class="flex flex-col gap-2">
@@ -17,7 +18,6 @@
:never-empty="true"
:capitalize="true"
:disabled="isUploading"
hide-checkmark-icon
/>
</div>
<div class="flex flex-col gap-2">
@@ -61,21 +61,25 @@
</template>
<script lang="ts" setup>
import { Chips, MarkdownEditor, StyledInput, Tabs, type TabsTab } from '@modrinth/ui'
import { Chips, MarkdownEditor, NavTabs, StyledInput } from '@modrinth/ui'
import { useImageUpload } from '~/composables/image-upload.ts'
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
const { draftVersion, isUploading, editingVersion, modal } = injectManageVersionContext()
const editTabs: TabsTab[] = [
{ label: 'Metadata', value: 'metadata' },
{ label: 'Details', value: 'add-details' },
{ label: 'Files', value: 'add-files' },
]
const editTabs = [
{ label: 'Metadata', href: 'metadata', stage: 'metadata' },
{ label: 'Details', href: 'details', stage: 'add-details' },
{ label: 'Files', href: 'files', stage: 'add-files' },
] as const
function setEditTab(tab: TabsTab) {
modal.value?.setStage(tab.value)
const editTabLinks = editTabs.map(({ label, href }) => ({ label, href }))
function setEditTab(index: number) {
const tab = editTabs[index]
if (!tab) return
modal.value?.setStage(tab.stage)
}
async function onImageUpload(file: File) {
@@ -1,10 +1,11 @@
<template>
<Tabs
<NavTabs
v-if="editingVersion"
value="metadata"
:tabs="editTabs"
class="mb-3 border border-solid border-surface-5 !shadow-none !drop-shadow-none"
@change="setEditTab"
mode="local"
:links="editTabLinks"
:active-index="0"
class="mb-2 border border-solid border-surface-5 shadow-none drop-shadow-none"
@tab-click="setEditTab"
/>
<div class="flex flex-col gap-6">
<div v-if="!editingVersion" class="flex flex-col gap-1">
@@ -160,32 +161,40 @@
</template>
<template v-if="!noDependenciesProject">
<div class="flex flex-col gap-2.5">
<div class="flex flex-col gap-1">
<div class="flex items-center justify-between">
<span class="font-semibold text-contrast"> Dependencies </span>
<div v-if="visibleSuggestedDependencies.length" class="flex flex-col gap-1">
<div class="flex items-center justify-between">
<span class="font-semibold text-contrast"> Suggested dependencies </span>
<ButtonStyled type="transparent" size="standard">
<button @click="addDependency">
<PlusIcon />
Add dependency
</button>
</ButtonStyled>
</div>
<ButtonStyled type="transparent" size="standard">
<button @click="editDependencies">
<EditIcon />
Edit
</button>
</ButtonStyled>
</div>
<SuggestedDependencies @on-add-suggestion="handleAddSuggestedDependency" />
</div>
<div v-if="draftVersion.dependencies?.length" class="flex flex-col gap-4">
<DependenciesList />
</div>
<div v-else class="flex flex-col gap-1.5 gap-y-4 rounded-xl bg-surface-2 p-3 py-4">
<span class="text-sm font-medium">No dependencies added.</span>
</div>
<div
v-if="!visibleSuggestedDependencies.length || draftVersion.dependencies?.length"
class="flex flex-col gap-1"
>
<div class="flex items-center justify-between">
<span class="font-semibold text-contrast"> Dependencies </span>
<ButtonStyled type="transparent" size="standard">
<button @click="editDependencies">
<EditIcon />
Edit
</button>
</ButtonStyled>
</div>
<div v-if="visibleSuggestedDependencies.length" class="flex flex-col gap-2.5">
<div class="flex items-center justify-between">
<span class="font-medium"> Suggested </span>
</div>
<SuggestedDependencies @on-add-suggestion="handleAddSuggestedDependency" />
<div v-if="draftVersion.dependencies?.length" class="flex flex-col gap-4">
<DependenciesList disable-remove />
</div>
<div v-else class="flex flex-col gap-1.5 gap-y-4 rounded-xl bg-surface-2 p-3 py-4">
<span class="text-sm font-medium">No dependencies added.</span>
</div>
</div>
</template>
@@ -194,15 +203,14 @@
<script lang="ts" setup>
import type { Labrinth } from '@modrinth/api-client'
import { EditIcon, getLoaderIcon, PlusIcon, UnknownIcon } from '@modrinth/assets'
import { EditIcon, getLoaderIcon, UnknownIcon } from '@modrinth/assets'
import {
ButtonStyled,
defineMessages,
ENVIRONMENTS_COPY,
FormattedTag,
injectProjectPageContext,
Tabs,
type TabsTab,
NavTabs,
TagItem,
useVIntl,
} from '@modrinth/ui'
@@ -231,14 +239,18 @@ const { projectV2 } = injectProjectPageContext()
const generatedState = useGeneratedState()
const loaders = computed(() => generatedState.value.loaders)
const editTabs: TabsTab[] = [
{ label: 'Metadata', value: 'metadata' },
{ label: 'Details', value: 'add-details' },
{ label: 'Files', value: 'add-files' },
]
const editTabs = [
{ label: 'Metadata', href: 'metadata', stage: 'metadata' },
{ label: 'Details', href: 'details', stage: 'add-details' },
{ label: 'Files', href: 'files', stage: 'add-files' },
] as const
function setEditTab(tab: TabsTab) {
modal.value?.setStage(tab.value)
const editTabLinks = editTabs.map(({ label, href }) => ({ label, href }))
function setEditTab(index: number) {
const tab = editTabs[index]
if (!tab) return
modal.value?.setStage(tab.stage)
}
const isModpack = computed(() => projectType.value === 'modpack')
@@ -267,7 +279,7 @@ const editEnvironment = () => {
const editFiles = () => {
modal.value?.setStage('from-details-files')
}
const addDependency = () => {
const editDependencies = () => {
modal.value?.setStage('from-details-dependencies')
}
@@ -1,19 +1,12 @@
<template>
<div class="shadow-card rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4">
<div class="shadow-card rounded-2xl border border-surface-5 bg-surface-3 p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<NuxtLink
:to="`/project/${queueEntry.project.slug}`"
target="_blank"
tabindex="-1"
class="flex"
>
<Avatar
:src="queueEntry.project.icon_url"
size="4rem"
class="rounded-2xl border border-surface-5 bg-surface-4 !shadow-none"
/>
</NuxtLink>
<Avatar
:src="queueEntry.project.icon_url"
size="4rem"
class="rounded-2xl border border-surface-5 bg-surface-4 !shadow-none"
/>
<div class="flex flex-col gap-1.5">
<div class="flex items-center gap-2">
<NuxtLink
@@ -29,15 +22,11 @@
<component
:is="getProjectTypeIcon(queueEntry.project.project_types[0] as any)"
aria-hidden="true"
class="size-4"
class="h-4 w-4"
/>
<span class="text-sm font-medium text-secondary">
{{
queueEntry.project.project_types.length === 0
? '???'
: queueEntry.project.project_types
.map((t) => formatProjectType(t, true))
.join(', ')
queueEntry.project.project_types.map((t) => formatProjectType(t, true)).join(', ')
}}
</span>
</div>
@@ -46,39 +35,37 @@
class="flex items-center gap-2 rounded-full border border-solid border-surface-5 bg-surface-4 px-2.5 py-1"
>
<span class="text-sm text-secondary">Requesting</span>
<Badge :type="queueEntry.project.requested_status" class="text-sm" />
<Badge :type="queueEntry.project.requested_status" class="status" />
</div>
</div>
<div v-if="queueEntry.ownership?.kind === 'user'">
<NuxtLink
:to="`/user/${queueEntry.ownership.id}`"
target="_blank"
class="flex w-fit min-w-40 items-center gap-1 text-sm font-medium text-secondary hover:underline"
>
<Avatar
:src="queueEntry.ownership.icon_url"
size="1.5rem"
circle
class="border border-surface-5 bg-surface-4 !shadow-none"
/>
{{ queueEntry.ownership.name }}
</NuxtLink>
</div>
<div
v-else-if="queueEntry.ownership?.kind === 'organization'"
class="flex items-center gap-1"
>
<div v-if="queueEntry.owner" class="flex items-center gap-1">
<Avatar
:src="queueEntry.ownership.icon_url"
:src="queueEntry.owner.user.avatar_url"
size="1.5rem"
circle
class="border border-surface-5 bg-surface-4 !shadow-none"
/>
<NuxtLink
:to="`/organization/${queueEntry.ownership.id}`"
:to="`/user/${queueEntry.owner.user.username}`"
target="_blank"
class="text-sm font-medium text-secondary hover:underline"
>
{{ queueEntry.ownership.name }}
{{ queueEntry.owner.user.username }}
</NuxtLink>
</div>
<div v-else-if="queueEntry.org" class="flex items-center gap-1">
<Avatar
:src="queueEntry.org.icon_url"
size="1.5rem"
circle
class="border border-surface-5 bg-surface-4 !shadow-none"
/>
<NuxtLink
:to="`/organization/${queueEntry.org.slug}`"
target="_blank"
class="text-sm font-medium text-secondary hover:underline"
>
{{ queueEntry.org.name }}
</NuxtLink>
</div>
</div>
@@ -97,21 +84,26 @@
</span>
<div class="flex items-center gap-2">
<ButtonStyled circular>
<button v-tooltip="'Copy ID'" @click="copyId">
<ClipboardCopyIcon />
</button>
</ButtonStyled>
<ButtonStyled circular>
<button v-tooltip="'Copy link'" @click="copyLink">
<LinkIcon />
</button>
</ButtonStyled>
<ButtonStyled circular color="orange">
<button v-tooltip="'Begin review'" @click="openProjectForReview">
<ScaleIcon />
<button @click="openProjectForReview">
<ScaleIcon class="size-5" />
</button>
</ButtonStyled>
<ButtonStyled circular>
<OverflowMenu :options="quickActions" :dropdown-id="`${baseId}-quick-actions`">
<template #default>
<EllipsisVerticalIcon class="size-4" />
</template>
<template #copy-id>
<ClipboardCopyIcon />
<span class="hidden sm:inline">Copy ID</span>
</template>
<template #copy-link>
<LinkIcon />
<span class="hidden sm:inline">Copy link</span>
</template>
</OverflowMenu>
</ButtonStyled>
</div>
</div>
</div>
@@ -119,13 +111,15 @@
</template>
<script setup lang="ts">
import { ClipboardCopyIcon, LinkIcon, ScaleIcon } from '@modrinth/assets'
import { ClipboardCopyIcon, EllipsisVerticalIcon, LinkIcon, ScaleIcon } from '@modrinth/assets'
import {
Avatar,
Badge,
ButtonStyled,
getProjectTypeIcon,
injectNotificationManager,
OverflowMenu,
type OverflowMenuOption,
useFormatDateTime,
useRelativeTime,
} from '@modrinth/ui'
@@ -149,6 +143,8 @@ const formatDateTimeFull = useFormatDateTime({
timeZone: 'UTC',
})
const baseId = useId()
const props = defineProps<{
queueEntry: ModerationProject
}>()
@@ -189,27 +185,34 @@ const formattedDate = computed(() => {
}
})
function copyLink() {
const base = window.location.origin
const projectUrl = `${base}/project/${props.queueEntry.project.slug}`
navigator.clipboard.writeText(projectUrl).then(() => {
addNotification({
type: 'success',
title: 'Project link copied',
text: 'The link to this project has been copied to your clipboard.',
})
})
}
function copyId() {
navigator.clipboard.writeText(props.queueEntry.project.id).then(() => {
addNotification({
type: 'success',
title: 'Project ID copied',
text: 'The ID of this project has been copied to your clipboard.',
})
})
}
const quickActions: OverflowMenuOption[] = [
{
id: 'copy-link',
action: () => {
const base = window.location.origin
const projectUrl = `${base}/project/${props.queueEntry.project.slug}`
navigator.clipboard.writeText(projectUrl).then(() => {
addNotification({
type: 'success',
title: 'Project link copied',
text: 'The link to this project has been copied to your clipboard.',
})
})
},
},
{
id: 'copy-id',
action: () => {
navigator.clipboard.writeText(props.queueEntry.project.id).then(() => {
addNotification({
type: 'success',
title: 'Project ID copied',
text: 'The ID of this project has been copied to your clipboard.',
})
})
},
},
]
function openProjectForReview() {
emit('startFromProject', props.queueEntry.project.id)
@@ -143,7 +143,7 @@
:expand-text="expandText"
collapse-text="Collapse thread"
>
<div class="bg-surface-2 pt-2">
<div class="bg-surface-2 p-4 pt-2">
<ThreadView
v-if="threadWithReportBody"
ref="reportThread"
@@ -28,7 +28,6 @@ import {
injectNotificationManager,
OverflowMenu,
type OverflowMenuOption,
useFormatBytes,
useFormatDateTime,
} from '@modrinth/ui'
import { NavTabs } from '@modrinth/ui'
@@ -57,7 +56,6 @@ const formatDateTimeUtc = useFormatDateTime({
timeZoneName: 'short',
timeZone: 'UTC',
})
const formatBytes = useFormatBytes()
type FlattenedFileReport = Labrinth.TechReview.Internal.FileReport & {
id: string
@@ -364,6 +362,12 @@ const formattedDate = computed(() => {
return `${diffDays} days ago`
})
function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KiB`
return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`
}
function viewFileFlags(file: FlattenedFileReport) {
selectedFileId.value = file.id
currentTab.value = 'File'
@@ -847,7 +851,7 @@ const reviewSummaryPreview = computed(() => {
const fileVerdict = fileUnsafe > 0 ? 'Unsafe' : 'Safe'
markdown += `### ${fileData.fileName}\n`
markdown += `> ${formatBytes(fileData.fileSize)}${fileData.decisions.length} issues • Max severity: ${fileData.maxSeverity} • **Verdict:** ${fileVerdict}\n\n`
markdown += `> ${formatFileSize(fileData.fileSize)}${fileData.decisions.length} issues • Max severity: ${fileData.maxSeverity} • **Verdict:** ${fileVerdict}\n\n`
markdown += `<details>\n<summary>Issues (${fileSafe} safe, ${fileUnsafe} unsafe)</summary>\n\n`
markdown += `| Class | Issue Type | Severity | Decision |\n`
markdown += `|-------|------------|----------|----------|\n`
@@ -1075,7 +1079,6 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
mode="local"
:links="navTabsLinks"
:active-index="activeTabIndex"
class="bg-surface-3! shadow-none!"
@tab-click="handleTabClick"
/>
</div>
@@ -1088,7 +1091,7 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
collapse-text="Collapse thread"
class="border-x border-b border-solid border-surface-3"
>
<div class="bg-surface-2 pt-0">
<div class="bg-surface-2 p-4 pt-0">
<!-- DEV-531 -->
<!-- @vue-expect-error TODO: will convert ThreadView to use api-client types at a later date -->
<ThreadView
@@ -1147,7 +1150,7 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
</span>
<div class="rounded-full border border-solid border-surface-5 bg-surface-3 px-2.5 py-1">
<span class="text-sm font-medium text-secondary">{{
formatBytes(file.file_size)
formatFileSize(file.file_size)
}}</span>
</div>
<div
@@ -358,44 +358,26 @@
<div v-else-if="generatedMessage" class="flex items-center gap-2">
<ButtonStyled>
<button :disabled="loadingModerationDecision" @click="goBackToStages">
<button @click="goBackToStages">
<LeftArrowIcon aria-hidden="true" />
Edit
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button :disabled="loadingModerationDecision" @click="sendMessage('rejected')">
<SpinnerIcon
v-if="moderationDecision === 'rejected'"
class="animate-spin"
aria-hidden="true"
/>
<XIcon v-else aria-hidden="true" />
<button @click="sendMessage('rejected')">
<XIcon aria-hidden="true" />
Reject
</button>
</ButtonStyled>
<ButtonStyled color="orange">
<button :disabled="loadingModerationDecision" @click="sendMessage('withheld')">
<SpinnerIcon
v-if="moderationDecision === 'withheld'"
class="animate-spin"
aria-hidden="true"
/>
<LinkIcon v-else aria-hidden="true" />
<button @click="sendMessage('withheld')">
<EyeOffIcon aria-hidden="true" />
Withhold
</button>
</ButtonStyled>
<ButtonStyled color="green">
<button
:disabled="loadingModerationDecision"
@click="sendMessage(approveSendStatus)"
>
<SpinnerIcon
v-if="moderationDecision === approveSendStatus"
class="animate-spin"
aria-hidden="true"
/>
<CheckIcon v-else aria-hidden="true" />
<button @click="sendMessage(projectV2.requested_status ?? 'approved')">
<CheckIcon aria-hidden="true" />
Approve
</button>
</ButtonStyled>
@@ -446,15 +428,14 @@ import {
BrushCleaningIcon,
CheckIcon,
DropdownIcon,
EyeOffIcon,
FileTextIcon,
KeyboardIcon,
LeftArrowIcon,
LinkIcon,
ListBulletedIcon,
LockIcon,
RightArrowIcon,
ScaleIcon,
SpinnerIcon,
ToggleLeftIcon,
ToggleRightIcon,
XIcon,
@@ -779,12 +760,6 @@ const message = ref(
)
const generatedMessage = ref(persistedGeneratedMessage.generated === true)
const loadingMessage = ref(false)
const moderationDecision = ref<ProjectStatus | null>(null)
const loadingModerationDecision = computed(() => moderationDecision.value !== null)
const approveSendStatus = computed<ProjectStatus>(() => {
const requested = projectV2.value.requested_status
return requested ?? 'approved'
})
const done = ref(false)
function persistGeneratedMessageState() {
@@ -1099,7 +1074,6 @@ function resetProgress() {
done.value = false
clearGeneratedMessageState()
loadingMessage.value = false
moderationDecision.value = null
localStorage.removeItem(`modpack-permissions-${projectV2.value.id}`)
localStorage.removeItem(`modpack-permissions-index-${projectV2.value.id}`)
@@ -1216,7 +1190,7 @@ function handleKeybinds(event: KeyboardEvent) {
tryResetProgress: resetProgress,
tryExitModeration: handleExit,
tryApprove: () => sendMessage(approveSendStatus.value),
tryApprove: () => sendMessage(projectV2.value.requested_status ?? 'approved'),
tryReject: () => sendMessage('rejected'),
tryWithhold: () => sendMessage('withheld'),
tryEditMessage: goBackToStages,
@@ -2003,7 +1977,6 @@ async function sendMessage(status: ProjectStatus) {
return
}
moderationDecision.value = status
try {
await useBaseFetch(`project/${projectId}`, {
method: 'PATCH',
@@ -2053,8 +2026,6 @@ async function sendMessage(status: ProjectStatus) {
text: 'Failed to submit moderation decision. Please try again.',
type: 'error',
})
} finally {
moderationDecision.value = null
}
}
@@ -1,31 +1,17 @@
<template>
<div>
<section>
<section class="universal-card">
<Breadcrumbs
v-if="breadcrumbsStack"
:current-title="`Report ${reportId}`"
:link-stack="breadcrumbsStack"
/>
<h2>Report details</h2>
<ReportInfo
:report="report"
:show-thread="false"
:show-message="false"
:auth="auth"
class="card-shadow mb-4 rounded-2xl border border-solid border-surface-4 bg-surface-2 p-4"
/>
<ReportInfo :report="report" :show-thread="false" :show-message="false" :auth="auth" />
</section>
<section
v-if="report && thread"
class="card-shadow rounded-2xl border border-solid border-surface-4 bg-surface-3"
>
<h2 class="m-4 mb-2 text-xl font-semibold text-contrast">Messages with the moderators</h2>
<p class="mx-4 mt-0">
Make sure to include evidence of all claims you make, or your report may be closed without
action.
</p>
<section v-if="report && thread" class="universal-card">
<h2>Messages</h2>
<ConversationThread
class="overflow-clip rounded-b-2xl border-0 border-t border-solid border-surface-4 bg-surface-2"
:thread="thread"
:report="report"
:auth="auth"
@@ -1,410 +1,277 @@
<template>
<div>
<NewModal
<Modal
ref="modalSubmit"
:header="
formatMessage(
isRejected(project)
? messages.resubmitModalHeaderResubmitting
: messages.resubmitModalHeaderSubmitting,
)
"
:header="isRejected(project) ? 'Resubmit for review' : 'Submit for review'"
>
<div class="flex max-w-[35rem] flex-col gap-3">
<p class="m-0">
<IntlFormatted
:message-id="messages.resubmitModalDescription"
:message-values="{ projectTitle: project.title }"
>
<template #project-title="{ children }">
<span class="font-semibold text-contrast">
<component :is="() => children" />
</span>
</template>
</IntlFormatted>
</p>
<p class="m-0">{{ formatMessage(messages.resubmitModalReminder) }}</p>
<p class="m-0 font-semibold text-red">
{{ formatMessage(messages.resubmitModalWarning) }}
</p>
<div class="modal-submit universal-body">
<span>
You're submitting <span class="project-title">{{ project.title }}</span> to be reviewed
again by the moderators.
</span>
<span>
Make sure you have addressed the comments from the moderation team.
<span class="known-errors">
Repeated submissions without addressing the moderators' comments may result in an
account suspension.
</span>
</span>
<Checkbox
v-model="submissionConfirmation"
:description="formatMessage(messages.resubmitModalConfirmationDescription)"
description="Confirm I have addressed the messages from the moderators"
>
{{ formatMessage(messages.resubmitModalConfirmationLabel) }}
I confirm that I have properly addressed the moderators' comments.
</Checkbox>
<div class="flex flex-wrap items-center justify-end gap-2">
<ButtonStyled type="outlined">
<button @click="modalSubmit.hide()">
<XIcon aria-hidden="true" />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<div class="input-group push-right">
<ButtonStyled color="orange">
<button
:disabled="!submissionConfirmation || isLoading"
@click="runBlockingAction('resubmit-modal', resubmit)"
>
<SpinnerIcon
v-if="loadingAction === 'resubmit-modal'"
class="animate-spin"
aria-hidden="true"
/>
<ScaleIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionResubmitForReview) }}
<button :disabled="!submissionConfirmation" @click="resubmit()">
<ScaleIcon aria-hidden="true" />
Resubmit for review
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<NewModal ref="modalReply" :header="formatMessage(messages.replyModalHeader)">
<div class="flex max-w-[45rem] flex-col gap-3">
<p class="m-0">{{ formatMessage(messages.replyModalDescription) }}</p>
<p class="m-0">
<IntlFormatted :message-id="messages.replyModalHelpCenterNote">
<template #help-center-link="{ children }">
<a class="text-link" href="https://support.modrinth.com" target="_blank">
<component :is="() => children" />
</a>
</template>
</IntlFormatted>
</p>
</Modal>
<Modal ref="modalReply" header="Reply to thread">
<div class="modal-submit universal-body">
<span>
Your project is already approved. As such, the moderation team does not actively monitor
this thread. However, they may still see your message if there is a problem with your
project.
</span>
<span>
If you need to get in contact with the moderation team, please use the
<a class="text-link" href="https://support.modrinth.com" target="_blank">
Modrinth Help Center
</a>
and click the green bubble to contact support.
</span>
<Checkbox
v-model="replyConfirmation"
:description="formatMessage(messages.replyModalConfirmationDescription)"
description="Confirm moderators do not actively monitor this"
>
{{ formatMessage(messages.replyModalConfirmationLabel) }}
I acknowledge that the moderators do not actively monitor the thread.
</Checkbox>
<div class="flex flex-wrap items-center justify-end gap-2">
<ButtonStyled type="outlined">
<button @click="modalReply.hide()">
<XIcon aria-hidden="true" />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<div class="input-group push-right">
<ButtonStyled color="brand">
<button
:disabled="!replyConfirmation || isLoading"
@click="runBlockingAction('reply-modal', () => sendReplyFromModal())"
>
<SpinnerIcon
v-if="loadingAction === 'reply-modal'"
class="animate-spin"
aria-hidden="true"
/>
<ReplyIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionReplyToThread) }}
<button :disabled="!replyConfirmation" @click="sendReplyFromModal()">
<ReplyIcon aria-hidden="true" />
Reply to thread
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<div v-if="flags.developerMode" class="mx-4 mb-3 font-semibold">
</Modal>
<div v-if="flags.developerMode" class="thread-id">
Thread ID:
<CopyCode :text="thread.id" />
</div>
<div v-bind="$attrs" class="flex flex-col">
<div v-if="sortedMessages.length > 0" class="flex flex-col pt-2">
<ThreadMessage
v-for="message in sortedMessages"
:key="'message-' + message.id"
:thread="thread"
:message="message"
:members="members"
:report="report"
:auth="auth"
raised
@update-thread="() => updateThreadLocal()"
<div v-if="sortedMessages.length > 0" class="messages universal-card recessed">
<ThreadMessage
v-for="message in sortedMessages"
:key="'message-' + message.id"
:thread="thread"
:message="message"
:members="members"
:report="report"
:auth="auth"
raised
@update-thread="() => updateThreadLocal()"
/>
</div>
<template v-if="report && report.closed">
<p>This thread is closed and new messages cannot be sent to it.</p>
<ButtonStyled v-if="isStaff(auth.user)">
<button @click="reopenReport()">
<CheckCircleIcon aria-hidden="true" />
Reopen thread
</button>
</ButtonStyled>
</template>
<template v-else-if="!report || !report.closed">
<div class="markdown-editor-spacing">
<MarkdownEditor
v-model="replyBody"
:placeholder="sortedMessages.length > 0 ? 'Reply to thread...' : 'Send a message...'"
:on-image-upload="onUploadImage"
/>
</div>
<template v-if="report && report.closed">
<p>{{ formatMessage(messages.closedThreadDescription) }}</p>
<ButtonStyled v-if="isStaff(auth.user)">
<button :disabled="isLoading" @click="runBlockingAction('reopen', () => reopenReport())">
<SpinnerIcon
v-if="loadingAction === 'reopen'"
class="animate-spin"
aria-hidden="true"
/>
<CheckCircleIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionReopenThread) }}
<div class="input-group">
<ButtonStyled color="brand">
<button
v-if="sortedMessages.length > 0"
:disabled="!replyBody"
@click="isApproved(project) && !isStaff(auth.user) ? openReplyModal() : sendReply()"
>
<ReplyIcon aria-hidden="true" />
Reply
</button>
<button
v-else
:disabled="!replyBody"
@click="isApproved(project) && !isStaff(auth.user) ? openReplyModal() : sendReply()"
>
<SendIcon aria-hidden="true" />
Send
</button>
</ButtonStyled>
</template>
<template v-else-if="!report || !report.closed">
<div class="mx-4 mb-2 mt-2">
<MarkdownEditor
v-model="replyBody"
:placeholder="
formatMessage(
sortedMessages.length > 0
? messages.replyEditorPlaceholderReply
: messages.replyEditorPlaceholderSend,
)
"
:on-image-upload="onUploadImage"
/>
</div>
<div class="m-4 mt-3 flex flex-wrap items-center justify-between gap-4">
<div class="flex flex-wrap items-center gap-2">
<ButtonStyled color="brand">
<button
v-if="sortedMessages.length > 0"
:disabled="!replyBody || isLoading"
@click="
isApproved(project)
? openReplyModal()
: runBlockingAction('reply', () => sendReply())
"
>
<SpinnerIcon
v-if="loadingAction === 'reply'"
class="animate-spin"
aria-hidden="true"
/>
<ReplyIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionReply) }}
<ButtonStyled v-if="isStaff(auth.user)">
<button :disabled="!replyBody" @click="sendReply(null, true)">
<ScaleIcon aria-hidden="true" />
Add private note
</button>
</ButtonStyled>
<template v-if="currentMember && !isStaff(auth.user)">
<template v-if="isRejected(project)">
<ButtonStyled color="orange">
<button v-if="replyBody" @click="openResubmitModal(true)">
<ScaleIcon aria-hidden="true" />
Resubmit for review with reply
</button>
<button
v-else
:disabled="!replyBody || isLoading"
@click="
isApproved(project)
? openReplyModal()
: runBlockingAction('send', () => sendReply())
"
>
<SpinnerIcon
v-if="loadingAction === 'send'"
class="animate-spin"
aria-hidden="true"
/>
<SendIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionSend) }}
<button v-else @click="openResubmitModal(false)">
<ScaleIcon aria-hidden="true" />
Resubmit for review
</button>
</ButtonStyled>
<ButtonStyled v-if="isStaff(auth.user)">
<button
:disabled="!replyBody || isLoading"
@click="runBlockingAction('private-note', () => sendReply(null, true))"
>
<SpinnerIcon
v-if="loadingAction === 'private-note'"
class="animate-spin"
aria-hidden="true"
/>
<ScaleIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionAddPrivateNote) }}
</button>
</ButtonStyled>
<template v-if="currentMember && !currentMember.staffOnly">
<template v-if="isRejected(project)">
<ButtonStyled color="orange">
<button v-if="replyBody" :disabled="isLoading" @click="openResubmitModal(true)">
<ScaleIcon aria-hidden="true" />
{{ formatMessage(messages.actionResubmitForReviewWithReply) }}
</template>
</template>
<div class="spacer"></div>
<div class="input-group extra-options">
<template v-if="report">
<template v-if="isStaff(auth.user)">
<ButtonStyled color="red">
<button v-if="replyBody" @click="closeReport(true)">
<CheckCircleIcon aria-hidden="true" />
Close with reply
</button>
<button v-else @click="closeReport()">
<CheckCircleIcon aria-hidden="true" />
Close thread
</button>
</ButtonStyled>
</template>
</template>
<template v-if="project">
<template v-if="isStaff(auth.user)">
<ButtonStyled v-if="replyBody" color="green">
<button :disabled="isApproved(project)" @click="sendReply(requestedStatus)">
<CheckIcon aria-hidden="true" />
Approve with reply
</button>
</ButtonStyled>
<ButtonStyled v-else color="green">
<button :disabled="isApproved(project)" @click="setStatus(requestedStatus)">
<CheckIcon aria-hidden="true" />
Approve
</button>
</ButtonStyled>
<div class="joined-buttons">
<ButtonStyled v-if="replyBody" color="red">
<button :disabled="project.status === 'rejected'" @click="sendReply('rejected')">
<XIcon aria-hidden="true" />
Reject with reply
</button>
<button v-else :disabled="isLoading" @click="openResubmitModal(false)">
<ScaleIcon aria-hidden="true" />
{{ formatMessage(messages.actionResubmitForReview) }}
</ButtonStyled>
<ButtonStyled v-else color="red">
<button :disabled="project.status === 'rejected'" @click="setStatus('rejected')">
<XIcon aria-hidden="true" />
Reject
</button>
</ButtonStyled>
</template>
</template>
</div>
<div class="flex flex-wrap items-center gap-2">
<template v-if="report">
<template v-if="isStaff(auth.user)">
<ButtonStyled color="red">
<button
v-if="replyBody"
:disabled="isLoading"
@click="runBlockingAction('close-with-reply', () => closeReport(true))"
>
<SpinnerIcon
v-if="loadingAction === 'close-with-reply'"
class="animate-spin"
aria-hidden="true"
/>
<CheckCircleIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionCloseWithReply) }}
</button>
<button
v-else
:disabled="isLoading"
@click="runBlockingAction('close', () => closeReport())"
>
<SpinnerIcon
v-if="loadingAction === 'close'"
class="animate-spin"
aria-hidden="true"
/>
<CheckCircleIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionCloseThread) }}
</button>
</ButtonStyled>
</template>
</template>
<template v-if="project">
<template v-if="isStaff(auth.user)">
<ButtonStyled v-if="replyBody" color="green">
<button
:disabled="isApproved(project) || isLoading"
@click="
runBlockingAction('approve-with-reply', () => sendReply(requestedStatus))
<OverflowMenu
class="btn-dropdown-animation"
:options="
replyBody
? [
{
id: 'withhold-reply',
color: 'danger',
action: () => {
sendReply('withheld')
},
hoverFilled: true,
disabled: project.status === 'withheld',
},
{
id: 'set-to-draft-reply',
action: () => {
sendReply('draft')
},
hoverFilled: true,
disabled: project.status === 'draft',
},
{
id: 'send-to-review-reply',
action: () => {
sendReply('processing', true)
},
hoverFilled: true,
disabled: project.status === 'processing',
},
]
: [
{
id: 'withhold',
color: 'danger',
action: () => {
setStatus('withheld')
},
hoverFilled: true,
disabled: project.status === 'withheld',
},
{
id: 'set-to-draft',
action: () => {
setStatus('draft')
},
hoverFilled: true,
disabled: project.status === 'draft',
},
{
id: 'send-to-review',
action: () => {
setStatus('processing')
},
hoverFilled: true,
disabled: project.status === 'processing',
},
]
"
>
<SpinnerIcon
v-if="loadingAction === 'approve-with-reply'"
class="animate-spin"
aria-hidden="true"
/>
<CheckIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionApproveWithReply) }}
</button>
<DropdownIcon aria-hidden="true" />
<template #withhold-reply>
<EyeOffIcon aria-hidden="true" />
Withhold with reply
</template>
<template #withhold>
<EyeOffIcon aria-hidden="true" />
Withhold
</template>
<template #set-to-draft-reply>
<FileTextIcon aria-hidden="true" />
Set to draft with reply
</template>
<template #set-to-draft>
<FileTextIcon aria-hidden="true" />
Set to draft
</template>
<template #send-to-review-reply>
<ScaleIcon aria-hidden="true" />
Send to review with reply
</template>
<template #send-to-review>
<ScaleIcon aria-hidden="true" />
Send to review
</template>
</OverflowMenu>
</ButtonStyled>
<ButtonStyled v-else color="green">
<button
:disabled="isApproved(project) || isLoading"
@click="runBlockingAction('approve', () => setStatus(requestedStatus))"
>
<SpinnerIcon
v-if="loadingAction === 'approve'"
class="animate-spin"
aria-hidden="true"
/>
<CheckIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionApprove) }}
</button>
</ButtonStyled>
<div class="joined-buttons">
<ButtonStyled v-if="replyBody" color="red">
<button
:disabled="project.status === 'rejected' || isLoading"
@click="runBlockingAction('reject-with-reply', () => sendReply('rejected'))"
>
<SpinnerIcon
v-if="loadingAction === 'reject-with-reply'"
class="animate-spin"
aria-hidden="true"
/>
<XIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionRejectWithReply) }}
</button>
</ButtonStyled>
<ButtonStyled v-else color="red">
<button
:disabled="project.status === 'rejected' || isLoading"
@click="runBlockingAction('reject', () => setStatus('rejected'))"
>
<SpinnerIcon
v-if="loadingAction === 'reject'"
class="animate-spin"
aria-hidden="true"
/>
<XIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionReject) }}
</button>
</ButtonStyled>
<ButtonStyled color="red">
<OverflowMenu
class="btn-dropdown-animation"
:disabled="isLoading"
:options="
replyBody
? [
{
id: 'withhold-reply',
color: 'danger',
action: () =>
runBlockingAction('withhold-reply', () => sendReply('withheld')),
hoverFilled: true,
disabled: project.status === 'withheld' || isLoading,
},
{
id: 'set-to-draft-reply',
action: () =>
runBlockingAction('set-to-draft-reply', () => sendReply('draft')),
hoverFilled: true,
disabled: project.status === 'draft' || isLoading,
},
{
id: 'send-to-review-reply',
action: () =>
runBlockingAction('send-to-review-reply', () =>
sendReply('processing', true),
),
hoverFilled: true,
disabled: project.status === 'processing' || isLoading,
},
]
: [
{
id: 'withhold',
color: 'danger',
action: () =>
runBlockingAction('withhold', () => setStatus('withheld')),
hoverFilled: true,
disabled: project.status === 'withheld' || isLoading,
},
{
id: 'set-to-draft',
action: () =>
runBlockingAction('set-to-draft', () => setStatus('draft')),
hoverFilled: true,
disabled: project.status === 'draft' || isLoading,
},
{
id: 'send-to-review',
action: () =>
runBlockingAction('send-to-review', () =>
setStatus('processing'),
),
hoverFilled: true,
disabled: project.status === 'processing' || isLoading,
},
]
"
>
<SpinnerIcon
v-if="isDropdownLoading"
class="animate-spin"
aria-hidden="true"
/>
<DropdownIcon v-else aria-hidden="true" />
<template #withhold-reply>
<EyeOffIcon aria-hidden="true" />
{{ formatMessage(messages.actionWithholdWithReply) }}
</template>
<template #withhold>
<EyeOffIcon aria-hidden="true" />
{{ formatMessage(messages.actionWithhold) }}
</template>
<template #set-to-draft-reply>
<FileTextIcon aria-hidden="true" />
{{ formatMessage(messages.actionSetToDraftWithReply) }}
</template>
<template #set-to-draft>
<FileTextIcon aria-hidden="true" />
{{ formatMessage(messages.actionSetToDraft) }}
</template>
<template #send-to-review-reply>
<ScaleIcon aria-hidden="true" />
{{ formatMessage(messages.actionSendToReviewWithReply) }}
</template>
<template #send-to-review>
<ScaleIcon aria-hidden="true" />
{{ formatMessage(messages.actionSendToReview) }}
</template>
</OverflowMenu>
</ButtonStyled>
</div>
</template>
</div>
</template>
</div>
</template>
</div>
</template>
</div>
</div>
</template>
</div>
</template>
@@ -418,185 +285,24 @@ import {
ReplyIcon,
ScaleIcon,
SendIcon,
SpinnerIcon,
XIcon,
} from '@modrinth/assets'
import {
ButtonStyled,
Checkbox,
commonMessages,
CopyCode,
defineMessages,
injectNotificationManager,
IntlFormatted,
MarkdownEditor,
NewModal,
OverflowMenu,
useVIntl,
} from '@modrinth/ui'
import Modal from '~/components/ui/Modal.vue'
import ThreadMessage from '~/components/ui/thread/ThreadMessage.vue'
import { useImageUpload } from '~/composables/image-upload.ts'
import { isApproved, isRejected } from '~/helpers/projects.js'
import { isStaff } from '~/helpers/users.js'
const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl()
const messages = defineMessages({
resubmitModalHeaderResubmitting: {
id: 'conversation-thread.resubmit-modal.header.resubmitting',
defaultMessage: 'Resubmitting for review',
},
resubmitModalHeaderSubmitting: {
id: 'conversation-thread.resubmit-modal.header.submitting',
defaultMessage: 'Submitting for review',
},
resubmitModalDescription: {
id: 'conversation-thread.resubmit-modal.description',
defaultMessage:
"You're submitting <project-title>{projectTitle}</project-title> to be reviewed again by the moderators.",
},
resubmitModalReminder: {
id: 'conversation-thread.resubmit-modal.reminder',
defaultMessage: 'Make sure you have addressed all the comments from the moderation team.',
},
resubmitModalWarning: {
id: 'conversation-thread.resubmit-modal.warning',
defaultMessage:
"Repeated submissions without addressing the moderators' comments may result in an account suspension.",
},
resubmitModalConfirmationDescription: {
id: 'conversation-thread.resubmit-modal.confirmation.description',
defaultMessage: 'Confirm I have addressed the messages from the moderators',
},
resubmitModalConfirmationLabel: {
id: 'conversation-thread.resubmit-modal.confirmation.label',
defaultMessage: "I confirm that I have properly addressed the moderators' comments.",
},
replyModalHeader: {
id: 'conversation-thread.reply-modal.header',
defaultMessage: 'Reply to thread',
},
replyModalDescription: {
id: 'conversation-thread.reply-modal.description',
defaultMessage:
'Your project is already approved. As such, the moderation team does not actively monitor this thread. However, they may still see your message if there is a problem with your project.',
},
replyModalHelpCenterNote: {
id: 'conversation-thread.reply-modal.help-center-note',
defaultMessage:
'If you need to get in contact with the moderation team, please use the <help-center-link>Modrinth Help Center</help-center-link> and click the blue bubble in the bottom right corner to contact support.',
},
replyModalConfirmationDescription: {
id: 'conversation-thread.reply-modal.confirmation.description',
defaultMessage: 'Confirm moderators do not actively monitor this',
},
replyModalConfirmationLabel: {
id: 'conversation-thread.reply-modal.confirmation.label',
defaultMessage: 'I acknowledge that the moderators do not actively monitor the thread.',
},
closedThreadDescription: {
id: 'conversation-thread.closed-thread.description',
defaultMessage: 'This thread is closed and new messages cannot be sent to it.',
},
replyEditorPlaceholderReply: {
id: 'conversation-thread.reply-editor.placeholder.reply',
defaultMessage: 'Reply to thread...',
},
replyEditorPlaceholderSend: {
id: 'conversation-thread.reply-editor.placeholder.send',
defaultMessage: 'Send a message...',
},
actionResubmitForReview: {
id: 'conversation-thread.action.resubmit-for-review',
defaultMessage: 'Resubmit for review',
},
actionReplyToThread: {
id: 'conversation-thread.action.reply-to-thread',
defaultMessage: 'Reply to thread',
},
actionReopenThread: {
id: 'conversation-thread.action.reopen-thread',
defaultMessage: 'Reopen thread',
},
actionReply: {
id: 'conversation-thread.action.reply',
defaultMessage: 'Reply',
},
actionSend: {
id: 'conversation-thread.action.send',
defaultMessage: 'Send',
},
actionAddPrivateNote: {
id: 'conversation-thread.action.add-private-note',
defaultMessage: 'Add private note',
},
actionResubmitForReviewWithReply: {
id: 'conversation-thread.action.resubmit-for-review-with-reply',
defaultMessage: 'Resubmit for review with reply',
},
actionCloseWithReply: {
id: 'conversation-thread.action.close-with-reply',
defaultMessage: 'Close with reply',
},
actionCloseThread: {
id: 'conversation-thread.action.close-thread',
defaultMessage: 'Close thread',
},
actionApproveWithReply: {
id: 'conversation-thread.action.approve-with-reply',
defaultMessage: 'Approve with reply',
},
actionApprove: {
id: 'conversation-thread.action.approve',
defaultMessage: 'Approve',
},
actionRejectWithReply: {
id: 'conversation-thread.action.reject-with-reply',
defaultMessage: 'Reject with reply',
},
actionReject: {
id: 'conversation-thread.action.reject',
defaultMessage: 'Reject',
},
actionWithholdWithReply: {
id: 'conversation-thread.action.withhold-with-reply',
defaultMessage: 'Withhold with reply',
},
actionWithhold: {
id: 'conversation-thread.action.withhold',
defaultMessage: 'Withhold',
},
actionSetToDraftWithReply: {
id: 'conversation-thread.action.set-to-draft-with-reply',
defaultMessage: 'Set to draft with reply',
},
actionSetToDraft: {
id: 'conversation-thread.action.set-to-draft',
defaultMessage: 'Set to draft',
},
actionSendToReviewWithReply: {
id: 'conversation-thread.action.send-to-review-with-reply',
defaultMessage: 'Send to review with reply',
},
actionSendToReview: {
id: 'conversation-thread.action.send-to-review',
defaultMessage: 'Send to review',
},
errorSendingMessage: {
id: 'conversation-thread.error.sending-message',
defaultMessage: 'Error sending message',
},
errorClosingReport: {
id: 'conversation-thread.error.closing-report',
defaultMessage: 'Error closing report',
},
errorReopeningReport: {
id: 'conversation-thread.error.reopening-report',
defaultMessage: 'Error reopening report',
},
})
const props = defineProps({
thread: {
@@ -657,30 +363,6 @@ const sortedMessages = computed(() => {
const modalSubmit = ref(null)
const modalReply = ref(null)
const loadingAction = ref(null)
const isLoading = computed(() => loadingAction.value !== null)
const dropdownActionIds = [
'withhold',
'withhold-reply',
'set-to-draft',
'set-to-draft-reply',
'send-to-review',
'send-to-review-reply',
]
const isDropdownLoading = computed(() => dropdownActionIds.includes(loadingAction.value))
async function runBlockingAction(actionId, action) {
if (loadingAction.value !== null) {
return
}
loadingAction.value = actionId
try {
await action()
} finally {
loadingAction.value = null
}
}
async function updateThreadLocal() {
let threadId = null
if (props.project) {
@@ -708,8 +390,8 @@ async function onUploadImage(file) {
}
async function sendReplyFromModal(status = null, privateMessage = false) {
await sendReply(status, privateMessage)
modalReply.value.hide()
await sendReply(status, privateMessage)
}
async function sendReply(status = null, privateMessage = false) {
@@ -742,7 +424,7 @@ async function sendReply(status = null, privateMessage = false) {
}
} catch (err) {
addNotification({
title: formatMessage(messages.errorSendingMessage),
title: 'Error sending message',
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -764,7 +446,7 @@ async function closeReport(reply) {
await updateThreadLocal()
} catch (err) {
addNotification({
title: formatMessage(messages.errorClosingReport),
title: 'Error closing report',
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -782,7 +464,7 @@ async function reopenReport() {
await updateThreadLocal()
} catch (err) {
addNotification({
title: formatMessage(messages.errorReopeningReport),
title: 'Error reopening report',
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -814,8 +496,44 @@ async function resubmit() {
}
const requestedStatus = computed(() => props.project.requested_status ?? 'approved')
defineOptions({
inheritAttrs: false,
})
</script>
<style lang="scss" scoped>
.markdown-editor-spacing {
margin-bottom: var(--gap-md);
}
.messages {
display: flex;
flex-direction: column;
padding: var(--spacing-card-md);
}
.thread-id {
margin-bottom: var(--spacing-card-md);
font-weight: bold;
color: var(--color-heading);
}
.input-group {
.spacer {
flex-grow: 1;
flex-shrink: 1;
}
.extra-options {
flex-basis: fit-content;
}
}
.modal-submit {
padding: var(--spacing-card-bg);
display: flex;
flex-direction: column;
gap: var(--spacing-card-lg);
.project-title {
font-weight: bold;
}
}
</style>
@@ -1,11 +1,10 @@
<template>
<div
class="message px-4 py-3"
class="message"
:class="{
'has-body': message.body.type === 'text' && !forceCompact,
'no-actions': noLinks,
private: isPrivateMessage,
'show-private-bg': flags.showModeratorPrivateMessageHighlight,
}"
>
<template v-if="members[message.author_id]">
@@ -23,6 +22,11 @@
/>
</AutoLink>
<span :class="`message__author role-${members[message.author_id].role}`">
<LockIcon
v-if="isPrivateMessage"
v-tooltip="'Only visible to moderators'"
class="private-icon"
/>
<AutoLink :to="noLinks ? '' : `/user/${members[message.author_id].username}`">
{{ members[message.author_id].username }}
</AutoLink>
@@ -31,11 +35,6 @@
v-else-if="members[message.author_id].role === 'admin'"
v-tooltip="'Modrinth Team'"
/>
<EyeOffIcon
v-if="isPrivateMessage"
v-tooltip="'Only visible to moderators'"
class="ml-1 text-orange"
/>
<MicrophoneIcon
v-if="report && message.author_id === report.reporter_user?.id"
v-tooltip="'Reporter'"
@@ -80,12 +79,6 @@
<span v-if="message.body.new_status === 'processing'">
submitted the project for review.
</span>
<span v-else-if="message.body.old_status === 'processing'">
reviewed the project and set its status to <Badge :type="message.body.new_status" />.
</span>
<span v-else-if="message.body.new_status === 'draft'">
reverted this project back to a <Badge :type="message.body.new_status" />.
</span>
<span v-else>
changed the project's status from <Badge :type="message.body.old_status" /> to
<Badge :type="message.body.new_status" />.
@@ -133,7 +126,7 @@
<script setup>
import {
EyeOffIcon,
LockIcon,
MicrophoneIcon,
ModrinthIcon,
MoreHorizontalIcon,
@@ -185,7 +178,6 @@ const props = defineProps({
})
const emit = defineEmits(['update-thread'])
const flags = useFeatureFlags()
const formattedMessage = computed(() => {
const body = renderString(props.message.body.body)
@@ -230,13 +222,15 @@ async function deleteMessage() {
<style lang="scss" scoped>
.message {
--gap-size: var(--spacing-card-xs);
display: flex;
flex-direction: row;
gap: var(--spacing-card-sm);
gap: var(--gap-size);
flex-wrap: wrap;
align-items: center;
border-radius: var(--size-rounded-card);
padding: var(--spacing-card-md);
word-break: break-word;
position: relative;
.avatar,
.backed-svg {
@@ -244,12 +238,14 @@ async function deleteMessage() {
}
&.has-body {
--gap-size: var(--spacing-card-sm);
display: grid;
grid-template:
'icon author actions'
'icon body actions'
'date date date';
grid-template-columns: min-content auto 1fr;
column-gap: var(--gap-size);
row-gap: var(--spacing-card-xs);
.message__icon {
@@ -264,22 +260,13 @@ async function deleteMessage() {
&:not(.no-actions):hover,
&:not(.no-actions):focus-within {
background-color: var(--surface-2-5);
background-color: var(--color-table-alternate-row);
.message__actions {
opacity: 1;
}
}
&.private.show-private-bg::before {
content: '';
inset: 0;
position: absolute;
background-color: var(--color-orange);
opacity: 0.05;
pointer-events: none;
}
&.no-actions {
padding: 0;
@@ -359,6 +346,10 @@ a:active + .message__author a,
color: var(--color-purple);
}
.private-icon {
color: var(--color-gray);
}
@media screen and (min-width: 600px) {
.message {
//grid-template:
@@ -372,7 +363,6 @@ a:active + .message__author a,
'icon body actions'
'date date date';
grid-template-columns: min-content auto 1fr;
grid-template-rows: min-content 1fr auto;
}
}
}
@@ -387,7 +377,7 @@ a:active + .message__author a,
'icon author date actions'
'icon body body actions';
grid-template-columns: min-content auto 1fr;
grid-template-rows: min-content 1fr;
grid-template-rows: min-content 1fr auto;
}
}
}
@@ -1,11 +1,11 @@
<template>
<div>
<div v-if="flags.developerMode" class="m-4 font-bold text-heading">
<div v-if="flags.developerMode" class="mt-4 font-bold text-heading">
Thread ID:
<CopyCode :text="thread.id" />
</div>
<div v-if="sortedMessages.length > 0" class="flex flex-col rounded-xl">
<div v-if="sortedMessages.length > 0" class="flex flex-col space-y-4 rounded-xl p-3 sm:p-4">
<ThreadMessage
v-for="message in sortedMessages"
:key="'message-' + message.id"
@@ -28,7 +28,7 @@
</template>
<template v-else>
<div class="px-4 py-2">
<div>
<MarkdownEditor
v-model="replyBody"
:placeholder="sortedMessages.length > 0 ? 'Reply to thread...' : 'Send a message...'"
@@ -37,7 +37,7 @@
</div>
<div
class="mt-4 flex flex-col items-stretch justify-between gap-3 px-4 pb-4 sm:flex-row sm:items-center sm:gap-2"
class="mt-4 flex flex-col items-stretch justify-between gap-3 sm:flex-row sm:items-center sm:gap-2"
>
<div class="flex flex-col items-stretch gap-2 sm:flex-row sm:items-center">
<ButtonStyled v-if="sortedMessages.length > 0" color="brand">
@@ -49,11 +49,6 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
labrinthApiCanary: false,
dismissedExternalProjectsInfo: false,
modpackPermissionsPage: false,
showAllBanners: false,
alwaysIgnoreErrorBanner: false,
showViewProdRouteBanner: false,
showModeratorProjectMemberUi: false,
showModeratorPrivateMessageHighlight: true,
} as const)
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
@@ -1,746 +0,0 @@
import type { Archon, Labrinth } from '@modrinth/api-client'
import type {
BrowseInstallContentType,
BrowseInstallPlan,
BrowseSearchState,
CreationFlowContextValue,
FilterValue,
PendingServerContentInstall,
PendingServerContentInstallType,
} from '@modrinth/ui'
import {
addPendingServerContentInstalls,
commonMessages,
defineMessages,
flushStoredServerAddonInstallQueue,
getStoredServerAddonInstallQueue,
getTargetInstallPreferences,
injectModrinthClient,
injectNotificationManager,
readPendingServerContentInstalls,
readStoredServerInstallQueue,
removePendingServerContentInstall,
requestInstall,
useVIntl,
writePendingServerContentInstallBaseline,
writeStoredServerInstallQueue,
} from '@modrinth/ui'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import type { ComputedRef, Ref } from 'vue'
import { computed, nextTick, ref, watch } from 'vue'
import { navigateTo, useRoute } from '#app'
import { queryAsString } from '~/utils/router'
type PendingServerContentInstallInput = Omit<PendingServerContentInstall, 'createdAt'>
type ServerInstallBrowseSearchState = Pick<
BrowseSearchState,
'currentFilters' | 'overriddenProvidedFilterTypes'
>
type ServerInstallProjectType = {
id?: string
}
type ServerInstallDebug = (...args: unknown[]) => void
export interface ServerInstallModalHandle {
show: () => void | Promise<void>
hide: () => void
ctx?: CreationFlowContextValue | null
}
export interface ServerInstallSearchResult extends Labrinth.Search.v2.ResultSearchProject {
installed?: boolean
}
export interface UseServerInstallContentOptions {
projectType: ComputedRef<ServerInstallProjectType | undefined>
onboardingModalRef: Ref<ServerInstallModalHandle | null>
debug?: ServerInstallDebug
}
const messages = defineMessages({
unsupportedContentType: {
id: 'discover.install.error.unsupported-content-type',
defaultMessage: 'This content type cannot be installed to a server from browse.',
},
noServerWorld: {
id: 'discover.install.error.no-server-world',
defaultMessage: 'No server world is available for install.',
},
backToSetup: {
id: 'discover.install.back-to-setup',
defaultMessage: 'Back to setup',
},
cancelReset: {
id: 'discover.install.cancel-reset',
defaultMessage: 'Cancel reset',
},
backToServer: {
id: 'discover.install.back-to-server',
defaultMessage: 'Back to server',
},
resetModpackHeading: {
id: 'discover.install.heading.reset-modpack',
defaultMessage: 'Selecting modpack to install after reset',
},
})
function getQueuedInstallOwnerFallback(project: ServerInstallSearchResult) {
if (project.organization) {
const ownerId = project.organization_id ?? project.organization
return {
id: ownerId,
name: project.organization,
type: 'organization' as const,
link: `/organization/${ownerId}`,
}
}
if (!project.author) return null
const ownerId = project.author_id ?? project.author
return {
id: ownerId,
name: project.author,
type: 'user' as const,
link: `/user/${ownerId}`,
}
}
function getQueuedAddonInstallPlans(
plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>,
) {
return Array.from(plans.values()).filter((plan) => plan.contentType !== 'modpack')
}
export function useServerInstallContent({
projectType,
onboardingModalRef,
debug = () => {},
}: UseServerInstallContentOptions) {
const { formatMessage } = useVIntl()
const client = injectModrinthClient()
const queryClient = useQueryClient()
const route = useRoute()
const { handleError } = injectNotificationManager()
let browseSearchState: ServerInstallBrowseSearchState | null = null
const currentServerId = computed(() => queryAsString(route.query.sid) || null)
const fromContext = computed(() => queryAsString(route.query.from) || null)
const currentWorldId = computed(() => queryAsString(route.query.wid) || null)
const {
data: serverData,
isLoading: serverDataLoading,
error: serverDataError,
} = useQuery({
queryKey: computed(() => ['servers', 'detail', currentServerId.value] as const),
queryFn: () => {
debug('serverData queryFn firing for:', currentServerId.value)
return client.archon.servers_v0.get(currentServerId.value!)
},
enabled: computed(() => {
const enabled = !!currentServerId.value
debug('serverData enabled:', enabled)
return enabled
}),
})
watch(serverData, (val) =>
debug('serverData changed:', val?.server_id, val?.name, val?.loader, val?.mc_version),
)
watch(serverDataLoading, (val) => debug('serverData loading:', val))
watch(serverDataError, (val) => {
if (val) debug('serverData error:', val)
})
const serverIcon = computed(() => {
if (!currentServerId.value || !import.meta.client) return null
return localStorage.getItem(`server-icon-${currentServerId.value}`)
})
const serverHideInstalled = ref(false)
const hideSelectedServerInstalls = ref(false)
const installingProjectIds = ref<Set<string>>(new Set())
const optimisticallyInstalledProjectIds = ref<Set<string>>(new Set())
const hiddenInstalledProjectIds = ref<Set<string>>(new Set())
const hiddenInstalledProjectIdsInitialized = ref(false)
const queuedServerInstalls = ref<Map<string, BrowseInstallPlan<ServerInstallSearchResult>>>(
readStoredServerInstallQueue(currentServerId.value, currentWorldId.value),
)
const queuedServerInstallProjectIds = computed(() => new Set(queuedServerInstalls.value.keys()))
const queuedServerInstallCount = computed(() => queuedServerInstalls.value.size)
const selectedServerInstallProjects = computed(() =>
Array.from(queuedServerInstalls.value.values()).map((plan) => ({
id: plan.projectId,
name: plan.project.title ?? formatMessage(commonMessages.projectLabel),
iconUrl: plan.project.icon_url ?? null,
})),
)
const isInstallingQueuedServerInstalls = ref(false)
const queuedInstallProgress = ref({ completed: 0, total: 0 })
const serverInstallQueue = {
get: () => queuedServerInstalls.value,
set: (plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>) => {
queuedServerInstalls.value = plans
writeStoredServerInstallQueue(currentServerId.value, currentWorldId.value, plans)
},
}
const contentQueryKey = computed(() => ['content', 'list', currentServerId.value ?? ''] as const)
const { data: serverContentData, error: serverContentError } = useQuery({
queryKey: contentQueryKey,
queryFn: () =>
client.archon.content_v1.getAddons(currentServerId.value!, currentWorldId.value!),
enabled: computed(() => !!currentServerId.value && !!currentWorldId.value),
})
function setBrowseSearchState(state: ServerInstallBrowseSearchState) {
browseSearchState = state
}
function setStoredServerInstallPlans(
serverId: string,
worldId: string,
plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>,
) {
if (serverId === currentServerId.value && worldId === currentWorldId.value) {
queuedServerInstalls.value = plans
}
writeStoredServerInstallQueue(serverId, worldId, plans)
}
async function getQueuedInstallOwner(project: ServerInstallSearchResult) {
const fallback = getQueuedInstallOwnerFallback(project)
try {
if (project.organization) {
const organization = await client.labrinth.projects_v3.getOrganization(project.project_id)
if (organization) {
return {
id: organization.id,
name: organization.name,
type: 'organization' as const,
avatar_url: organization.icon_url ?? undefined,
link: `/organization/${organization.slug}`,
}
}
}
const members = await client.labrinth.projects_v3.getMembers(project.project_id)
const owner =
members.find((member) => member.user.id === project.author_id)?.user ??
members.find((member) => member.is_owner || member.role === 'Owner')?.user ??
members[0]?.user
if (owner) {
return {
id: owner.id,
name: owner.username,
type: 'user' as const,
avatar_url: owner.avatar_url,
link: `/user/${owner.username}`,
}
}
} catch {
return fallback
}
return fallback
}
function getQueuedInstallPlaceholder(
plan: BrowseInstallPlan<ServerInstallSearchResult>,
owner: PendingServerContentInstallInput['owner'],
): PendingServerContentInstallInput {
return {
projectId: plan.projectId,
versionId: plan.versionId,
contentType: plan.contentType as PendingServerContentInstallType,
title: plan.project.title ?? formatMessage(commonMessages.projectLabel),
versionName: plan.versionName ?? null,
versionNumber: plan.versionNumber ?? null,
fileName: plan.fileName ?? null,
owner,
slug: plan.project.slug ?? plan.projectId,
iconUrl: plan.project.icon_url ?? null,
}
}
function getQueuedInstallPlaceholderFallbacks(
plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>,
) {
return getQueuedAddonInstallPlans(plans).map((plan) =>
getQueuedInstallPlaceholder(plan, getQueuedInstallOwnerFallback(plan.project)),
)
}
async function getQueuedInstallPlaceholders(
plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>,
) {
return Promise.all(
getQueuedAddonInstallPlans(plans).map(async (plan) =>
getQueuedInstallPlaceholder(plan, await getQueuedInstallOwner(plan.project)),
),
)
}
function setProjectInstalling(projectId: string, installing: boolean) {
const next = new Set(installingProjectIds.value)
if (installing) {
next.add(projectId)
} else {
next.delete(projectId)
}
installingProjectIds.value = next
}
function markProjectInstalled(projectId: string) {
optimisticallyInstalledProjectIds.value = new Set([
...optimisticallyInstalledProjectIds.value,
projectId,
])
}
function getServerInstalledProjectIds(data = serverContentData.value) {
return new Set(
(data?.addons ?? [])
.map((addon) => addon.project_id)
.filter((projectId): projectId is string => !!projectId),
)
}
function getServerInstalledContentKeys(data = serverContentData.value) {
return new Set((data?.addons ?? []).map((addon) => addon.project_id ?? addon.filename))
}
function syncHiddenInstalledProjectIds() {
hiddenInstalledProjectIds.value = new Set([
...getServerInstalledProjectIds(),
...optimisticallyInstalledProjectIds.value,
])
hiddenInstalledProjectIdsInitialized.value = true
}
const serverFilters = computed<FilterValue[]>(() => {
debug(
'serverFilters recomputing, serverData:',
!!serverData.value,
'projectType:',
projectType.value?.id,
)
const filters: FilterValue[] = []
if (serverData.value && projectType.value?.id !== 'modpack') {
const gameVersion = serverData.value.mc_version
if (gameVersion) {
filters.push({ type: 'game_version', option: gameVersion })
}
const platform = serverData.value.loader?.toLowerCase()
const modLoaders = ['fabric', 'forge', 'quilt', 'neoforge']
if (platform && modLoaders.includes(platform)) {
filters.push({ type: 'mod_loader', option: platform })
}
const pluginLoaders = ['paper', 'purpur']
if (platform && pluginLoaders.includes(platform)) {
filters.push({ type: 'plugin_loader', option: platform })
}
if (projectType.value?.id === 'mod') {
filters.push({ type: 'environment', option: 'server' })
}
if (serverHideInstalled.value && hiddenInstalledProjectIds.value.size > 0) {
for (const x of hiddenInstalledProjectIds.value) {
filters.push({
type: 'project_id',
option: `project_id:${x}`,
negative: true,
})
}
}
if (hideSelectedServerInstalls.value && queuedServerInstallProjectIds.value.size > 0) {
for (const id of queuedServerInstallProjectIds.value) {
filters.push({
type: 'project_id',
option: `project_id:${id}`,
negative: true,
})
}
}
}
if (currentServerId.value && projectType.value?.id === 'modpack') {
filters.push(
{ type: 'environment', option: 'client' },
{ type: 'environment', option: 'server' },
)
}
debug('serverFilters result:', filters)
return filters
})
function getCurrentServerInstallType(): BrowseInstallContentType {
const type = projectType.value?.id
if (type === 'modpack' || type === 'mod' || type === 'plugin' || type === 'datapack') {
return type
}
throw new Error(formatMessage(messages.unsupportedContentType))
}
function getServerInstallTargetPreferences(contentType: BrowseInstallContentType) {
return getTargetInstallPreferences(
{
gameVersion: serverData.value?.mc_version,
loader: serverData.value?.loader,
},
contentType,
)
}
function getInstallProjectVersions(projectId: string) {
return client.labrinth.versions_v2.getProjectVersions(projectId, {
include_changelog: false,
})
}
function clearQueuedServerInstalls() {
serverInstallQueue.set(new Map())
}
function removeQueuedServerInstall(projectId: string) {
const nextPlans = new Map(queuedServerInstalls.value)
nextPlans.delete(projectId)
serverInstallQueue.set(nextPlans)
}
async function flushQueuedServerInstalls(
serverId: string | null = currentServerId.value,
worldId: string | null = currentWorldId.value,
) {
if (isInstallingQueuedServerInstalls.value) return false
if (!serverId || !worldId) {
handleError(new Error(formatMessage(messages.noServerWorld)))
return false
}
const queuedPlans = getStoredServerAddonInstallQueue<ServerInstallSearchResult>(
serverId,
worldId,
)
if (queuedPlans.size === 0) return true
isInstallingQueuedServerInstalls.value = true
queuedInstallProgress.value = {
completed: 0,
total: queuedPlans.size,
}
try {
const result = await flushStoredServerAddonInstallQueue({
serverId,
worldId,
install: (plans) =>
client.archon.content_v1.addAddons(
serverId,
worldId,
plans.map((plan) => ({
project_id: plan.projectId,
version_id: plan.versionId,
})),
),
onQueueChange: (plans) => setStoredServerInstallPlans(serverId, worldId, plans),
})
if (!result.ok) {
for (const plan of result.attemptedPlans) {
removePendingServerContentInstall(serverId, worldId, plan.projectId)
}
handleError(result.error as Error)
return false
}
for (const plan of result.flushedPlans) {
markProjectInstalled(plan.projectId)
}
queuedInstallProgress.value = {
completed: result.flushedPlans.length,
total: result.flushedPlans.length,
}
if (result.flushedPlans.length > 0) {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', serverId] }),
queryClient.invalidateQueries({ queryKey: ['content', 'list'] }),
])
}
return true
} finally {
isInstallingQueuedServerInstalls.value = false
queuedInstallProgress.value = { completed: 0, total: 0 }
}
}
async function discardQueuedServerInstallsAndBack() {
clearQueuedServerInstalls()
await navigateTo(serverBackUrl.value)
}
async function installQueuedServerInstallsAndBack() {
const sid = currentServerId.value
const wid = currentWorldId.value
const backUrl = serverBackUrl.value
const plans = new Map(queuedServerInstalls.value)
if (sid && wid) {
writeStoredServerInstallQueue(sid, wid, plans)
writePendingServerContentInstallBaseline(sid, wid, [
...getServerInstalledContentKeys(),
...optimisticallyInstalledProjectIds.value,
])
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
void getQueuedInstallPlaceholders(plans)
.then((items) => {
const pendingProjectIds = new Set(
readPendingServerContentInstalls(sid, wid).map((item) => item.projectId),
)
addPendingServerContentInstalls(
sid,
wid,
items.filter((item) => pendingProjectIds.has(item.projectId)),
)
})
.catch((err) => handleError(err as Error))
}
await navigateTo(backUrl)
void flushQueuedServerInstalls(sid, wid)
return true
}
async function serverInstall(project: ServerInstallSearchResult) {
if (!serverData.value || !currentServerId.value || !currentWorldId.value) {
handleError(new Error('No server to install to.'))
return
}
if (!browseSearchState) {
handleError(new Error('Search state is not ready.'))
return
}
const contentType = getCurrentServerInstallType()
const isModpack = contentType === 'modpack'
try {
if (!isModpack && queuedServerInstallProjectIds.value.has(project.project_id)) {
removeQueuedServerInstall(project.project_id)
return
}
if (isModpack || !queuedServerInstallProjectIds.value.has(project.project_id)) {
setProjectInstalling(project.project_id, true)
}
await requestInstall({
project,
contentType,
mode: isModpack ? 'immediate' : 'queue',
selectedFilters: isModpack ? [] : browseSearchState.currentFilters.value,
providedFilters: isModpack ? [] : serverFilters.value,
overriddenProvidedFilterTypes: isModpack
? []
: browseSearchState.overriddenProvidedFilterTypes.value,
targetPreferences: getServerInstallTargetPreferences(contentType),
getProjectVersions: getInstallProjectVersions,
queue: serverInstallQueue,
install: async (plan) => {
const modalInstance = onboardingModalRef.value
if (!modalInstance) {
setProjectInstalling(plan.projectId, false)
return
}
onboardingInstallingProject.value = plan.project
modalInstance.show()
await nextTick()
const ctx = modalInstance.ctx
if (!ctx) return
ctx.setupType.value = 'modpack'
ctx.modpackSelection.value = {
projectId: plan.projectId,
versionId: plan.versionId,
name: plan.project.title,
iconUrl: plan.project.icon_url ?? undefined,
}
ctx.modal.value?.setStage('final-config')
},
})
} catch (e) {
console.error(e)
if (isModpack) {
setProjectInstalling(project.project_id, false)
}
handleError(e instanceof Error ? e : new Error(`Error installing content ${e}`))
} finally {
if (!isModpack) {
setProjectInstalling(project.project_id, false)
}
}
}
const onboardingInstallingProject = ref<ServerInstallSearchResult | null>(null)
function onOnboardingHide() {
if (onboardingInstallingProject.value) {
setProjectInstalling(onboardingInstallingProject.value.project_id, false)
onboardingInstallingProject.value = null
}
}
function onOnboardingBack() {
onboardingModalRef.value?.hide()
}
async function onModpackFlowCreate(config: CreationFlowContextValue) {
if (!currentServerId.value || !currentWorldId.value || !config.modpackSelection.value) return
try {
await client.archon.content_v1.installContent(currentServerId.value, currentWorldId.value, {
content_variant: 'modpack',
spec: {
platform: 'modrinth',
project_id: config.modpackSelection.value.projectId,
version_id: config.modpackSelection.value.versionId,
},
soft_override: false,
properties: config.buildProperties(),
} satisfies Archon.Content.v1.InstallWorldContent)
if (fromContext.value === 'onboarding') {
await client.archon.servers_v1.endIntro(currentServerId.value)
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', currentServerId.value] })
navigateTo(`/hosting/manage/${currentServerId.value}/content`)
} else {
navigateTo(`/hosting/manage/${currentServerId.value}?openSettings=installation`)
}
} catch (e) {
handleError(new Error(`Error installing modpack: ${e}`))
config.loading.value = false
}
}
const serverBackUrl = computed(() => {
if (!serverData.value) return ''
const id = serverData.value.server_id
if (fromContext.value === 'onboarding') return `/hosting/manage/${id}?resumeModal=setup-type`
if (fromContext.value === 'reset-server')
return `/hosting/manage/${id}?openSettings=installation`
return `/hosting/manage/${id}/content`
})
const serverBackLabel = computed(() => {
if (fromContext.value === 'onboarding') return formatMessage(messages.backToSetup)
if (fromContext.value === 'reset-server') return formatMessage(messages.cancelReset)
return formatMessage(messages.backToServer)
})
const serverBrowseHeading = computed(() =>
fromContext.value === 'reset-server'
? formatMessage(messages.resetModpackHeading)
: formatMessage(commonMessages.installingContentLabel),
)
const installContext = computed(() => {
if (!serverData.value) return null
return {
name: serverData.value.name,
loader: serverData.value.loader ?? '',
gameVersion: serverData.value.mc_version ?? '',
serverId: currentServerId.value,
upstream: serverData.value.upstream,
iconSrc: serverIcon.value,
isMedal: serverData.value.is_medal,
backUrl: serverBackUrl.value,
backLabel: serverBackLabel.value,
heading: serverBrowseHeading.value,
queuedCount: queuedServerInstallCount.value,
selectedProjects: selectedServerInstallProjects.value,
isInstallingSelected: isInstallingQueuedServerInstalls.value,
installProgress: queuedInstallProgress.value,
clearQueued: clearQueuedServerInstalls,
clearSelected: clearQueuedServerInstalls,
onBack: flushQueuedServerInstalls,
discardSelectedAndBack: discardQueuedServerInstallsAndBack,
installSelected: installQueuedServerInstallsAndBack,
}
})
watch(serverContentError, (error) => {
if (error) {
console.error('Failed to load server content:', error)
handleError(error)
}
})
watch(
serverContentData,
(data) => {
if (!data) return
if (!hiddenInstalledProjectIdsInitialized.value) {
syncHiddenInstalledProjectIds()
}
},
{ immediate: true },
)
if (route.query.shi && projectType.value?.id !== 'modpack') {
serverHideInstalled.value = route.query.shi === 'true'
}
watch(serverHideInstalled, (hideInstalled) => {
if (hideInstalled) {
syncHiddenInstalledProjectIds()
}
})
watch([currentServerId, currentWorldId], ([serverId, worldId], [prevServerId, prevWorldId]) => {
if (serverId !== prevServerId || worldId !== prevWorldId) {
queuedServerInstalls.value = readStoredServerInstallQueue(serverId, worldId)
}
})
watch(queuedServerInstallCount, (count) => {
if (count === 0) {
hideSelectedServerInstalls.value = false
}
})
return {
currentServerId,
fromContext,
currentWorldId,
serverData,
serverContentData,
serverFilters,
serverHideInstalled,
hideSelectedServerInstalls,
installingProjectIds,
optimisticallyInstalledProjectIds,
queuedServerInstallProjectIds,
queuedServerInstallCount,
isInstallingQueuedServerInstalls,
installContext,
setBrowseSearchState,
syncHiddenInstalledProjectIds,
serverInstall,
onOnboardingHide,
onOnboardingBack,
onModpackFlowCreate,
}
}
@@ -1,140 +0,0 @@
import type { FilterValue } from '@modrinth/ui'
import { LOADER_FILTER_TYPES } from '@modrinth/ui'
const TEN_MINUTES = 600
export type DownloadContext = {
gameVersion?: string
loader?: string
reason?: 'standalone' | 'dependency' | 'modpack' | 'update'
}
export type FilterSelection = {
gameVersion?: string
loader?: string
}
const cookieDefaults = {
maxAge: TEN_MINUTES,
sameSite: 'lax' as const,
secure: true,
path: '/',
httpOnly: false,
}
function readCookieValue(value: string | null | undefined): string | undefined {
if (typeof value !== 'string' || !value) {
return undefined
}
return value
}
function newFilterSelection(
gameVersion: string | undefined,
loader: string | undefined,
): FilterSelection | null {
if (!gameVersion && !loader) {
return null
} else if (!gameVersion) {
return {
loader,
}
} else if (!loader) {
return {
gameVersion,
}
} else {
return {
gameVersion,
loader,
}
}
}
export function useCdnDownloadContext() {
const filterGameVersionCookie = useCookie<string | null>('mr_download_filter_game_version', {
...cookieDefaults,
default: () => null,
})
const filterLoaderCookie = useCookie<string | null>('mr_download_filter_loader', {
...cookieDefaults,
default: () => null,
})
function createProjectDownloadUrl(originalUrl: string, context?: DownloadContext): string {
if (!originalUrl.startsWith('https://cdn.modrinth.com')) {
return originalUrl
}
const reason = context?.reason
const gameVersion = context?.gameVersion ?? readCookieValue(filterGameVersionCookie.value)
const loader = context?.loader ?? readCookieValue(filterLoaderCookie.value)
try {
const url = new URL(originalUrl)
if (reason) {
url.searchParams.set('mr_download_reason', reason)
}
if (gameVersion) {
url.searchParams.set('mr_game_version', gameVersion)
} else {
url.searchParams.delete('mr_game_version')
}
if (loader) {
url.searchParams.set('mr_loader', loader)
} else {
url.searchParams.delete('mr_loader')
}
return url.toString()
} catch {
return originalUrl
}
}
function persistFilterSelection(selection: FilterSelection | null) {
if (!selection) {
filterGameVersionCookie.value = null
filterLoaderCookie.value = null
return
}
filterGameVersionCookie.value = selection.gameVersion ?? null
filterLoaderCookie.value = selection.loader ?? null
}
function updateDiscoverFilterContext(filters: FilterValue[]) {
if (!import.meta.client) {
return
}
const versionFilters = [
...new Set(filters.filter((f) => f.type === 'game_version').map((f) => f.option)),
]
const loaderFilters = [
...new Set(
filters
.filter((f) => (LOADER_FILTER_TYPES as readonly string[]).includes(f.type))
.map((f) => f.option),
),
]
const gameVersion = versionFilters.length === 1 ? versionFilters[0] : undefined
const loader = loaderFilters.length === 1 ? loaderFilters[0] : undefined
persistFilterSelection(newFilterSelection(gameVersion, loader))
}
function updateVersionsFilterContext(gameVersions: string[], loaders: string[]) {
if (!import.meta.client) {
return
}
const gameVersion = gameVersions.length === 1 ? gameVersions[0] : undefined
const loader = loaders.length === 1 ? loaders[0] : undefined
persistFilterSelection(newFilterSelection(gameVersion, loader))
}
return {
createProjectDownloadUrl,
updateDiscoverFilterContext,
updateVersionsFilterContext,
}
}
+32
View File
@@ -0,0 +1,32 @@
import { formatBytes } from '@modrinth/utils'
export const fileIsValid = (file, validationOptions) => {
const { maxSize, alertOnInvalid } = validationOptions
if (maxSize !== null && maxSize !== undefined && file.size > maxSize) {
if (alertOnInvalid) {
alert(`File ${file.name} is too big! Must be less than ${formatBytes(maxSize)}`)
}
return false
}
return true
}
export const acceptFileFromProjectType = (projectType) => {
switch (projectType) {
case 'mod':
return '.jar,.zip,.litemod,application/java-archive,application/x-java-archive,application/zip'
case 'plugin':
return '.jar,.zip,application/java-archive,application/x-java-archive,application/zip'
case 'resourcepack':
return '.zip,application/zip'
case 'shader':
return '.zip,application/zip'
case 'datapack':
return '.zip,application/zip'
case 'modpack':
return '.mrpack,application/x-modrinth-modpack+zip,application/zip'
default:
return '*'
}
}
@@ -28,44 +28,26 @@ export function createLoaderParsers(
if (metadata.dependencies) {
const neoForgeDependency = Object.values(metadata.dependencies)
.flat()
.filter((dependency: any) => dependency.modId === 'neoforge')
.map((dependency: any) => dependency.versionRange)
.find((range) => range)
const minecraftDependency = Object.values(metadata.dependencies)
.flat()
.filter((dependency: any) => dependency.modId === 'minecraft')
.map((dependency: any) => dependency.versionRange)
.find((range) => range)
.find((dependency: any) => dependency.modId === 'neoforge')
if (minecraftDependency) {
newGameVersions = getGameVersionsMatchingMavenRange(
minecraftDependency,
simplifiedGameVersions,
)
} else if (neoForgeDependency) {
// https://docs.neoforged.net/docs/gettingstarted/versioning/#neoforge
// NeoForge's versioning changed after Mojang changed from 1.<major>.<minor> to <year>.<drop>.<patch>, so both cases need to be handled
const neoPre26Regex = /^(?<mc_major>\d+)(?:\.(?<mc_minor>\d+))?(?:\.(?<neo>\d+)?)?$/
const neoPost26Regex =
/^(?<mc_year>\d+)(?:\.(?<mc_drop>\d+))?(?:\.(?<mc_patch>\d+)?)?(?:\.(\d+))?$/
newGameVersions = getGameVersionsMatchingMavenRange(
neoForgeDependency.replace('-beta', ''),
simplifiedGameVersions,
(version) => {
const matchPre26 = version.match(neoPre26Regex)
if (matchPre26 && matchPre26.groups) {
const { mc_major, mc_minor } = matchPre26.groups
return mc_minor ? `1.${mc_major}.${mc_minor}` : `1.${mc_major}`
}
const matchPost26 = version.match(neoPost26Regex)
if (matchPost26 && matchPost26.groups) {
const { mc_year, mc_drop, mc_patch } = matchPost26.groups
return `${mc_year}${mc_drop ? `.${mc_drop}` : ''}${mc_patch ? `.${mc_patch}` : ''}`
}
return version
},
)
if (neoForgeDependency) {
try {
// https://docs.neoforged.net/docs/gettingstarted/versioning/#neoforge
const mcVersionRange = (neoForgeDependency as any).versionRange
.replace('-beta', '')
.replace(
/(\d+)(?:\.(\d+))?(?:\.(\d+)?)?/g,
(_match: string, major: string, minor: string) => {
return `1.${major}${minor ? '.' + minor : ''}`
},
)
newGameVersions = getGameVersionsMatchingMavenRange(
mcVersionRange,
simplifiedGameVersions,
)
} catch {
// Ignore parsing errors, just leave game_versions empty
}
}
}
@@ -25,7 +25,6 @@ export function getGameVersionsMatchingSemverRange(
export function getGameVersionsMatchingMavenRange(
range: string | undefined,
gameVersions: string[],
processor: (version: string) => string | null = (v) => v,
): string[] {
if (!range) {
return []
@@ -64,42 +63,24 @@ export function getGameVersionsMatchingMavenRange(
for (const range of ranges) {
let result
let version1
let version2
if ((result = range.match(LESS_THAN_EQUAL)) && (version1 = processor(result[1]))) {
semverRanges.push(`<=${version1}`)
} else if ((result = range.match(LESS_THAN)) && (version1 = processor(result[1]))) {
semverRanges.push(`<${version1}`)
} else if ((result = range.match(EQUAL)) && (version1 = processor(result[1]))) {
semverRanges.push(`${version1}`)
} else if ((result = range.match(GREATER_THAN_EQUAL)) && (version1 = processor(result[1]))) {
semverRanges.push(`>=${version1}`)
} else if ((result = range.match(GREATER_THAN)) && (version1 = processor(result[1]))) {
semverRanges.push(`>${version1}`)
} else if (
(result = range.match(BETWEEN)) &&
(version1 = processor(result[1])) &&
(version2 = processor(result[2]))
) {
semverRanges.push(`>${version1} <${version2}`)
} else if (
(result = range.match(BETWEEN_EQUAL)) &&
(version1 = processor(result[1])) &&
(version2 = processor(result[2]))
) {
semverRanges.push(`>=${version1} <=${version2}`)
} else if (
(result = range.match(BETWEEN_LESS_THAN_EQUAL)) &&
(version1 = processor(result[1])) &&
(version2 = processor(result[2]))
) {
semverRanges.push(`>${version1} <=${version2}`)
} else if (
(result = range.match(BETWEEN_GREATER_THAN_EQUAL)) &&
(version1 = processor(result[1])) &&
(version2 = processor(result[2]))
) {
semverRanges.push(`>=${version1} <${version2}`)
if ((result = range.match(LESS_THAN_EQUAL))) {
semverRanges.push(`<=${result[1]}`)
} else if ((result = range.match(LESS_THAN))) {
semverRanges.push(`<${result[1]}`)
} else if ((result = range.match(EQUAL))) {
semverRanges.push(`${result[1]}`)
} else if ((result = range.match(GREATER_THAN_EQUAL))) {
semverRanges.push(`>=${result[1]}`)
} else if ((result = range.match(GREATER_THAN))) {
semverRanges.push(`>${result[1]}`)
} else if ((result = range.match(BETWEEN))) {
semverRanges.push(`>${result[1]} <${result[2]}`)
} else if ((result = range.match(BETWEEN_EQUAL))) {
semverRanges.push(`>=${result[1]} <=${result[2]}`)
} else if ((result = range.match(BETWEEN_LESS_THAN_EQUAL))) {
semverRanges.push(`>${result[1]} <=${result[2]}`)
} else if ((result = range.match(BETWEEN_GREATER_THAN_EQUAL))) {
semverRanges.push(`>=${result[1]} <${result[2]}`)
}
}
return getGameVersionsMatchingSemverRange(semverRanges, gameVersions)
+48 -27
View File
@@ -181,35 +181,56 @@ export async function enrichReportBatch(reports: Report[]): Promise<ExtendedRepo
}
// Doesn't need to be in @modrinth/moderation because it is specific to the frontend.
export interface ModerationOwnershipUser {
kind: 'user'
id: string
name: string
icon_url: string | null
}
export interface ModerationOwnershipOrganization {
kind: 'organization'
id: string
name: string
icon_url: string | null
}
export type ModerationOwnership = ModerationOwnershipUser | ModerationOwnershipOrganization
export interface ProjectWithOwnership {
ownership: ModerationOwnership
[key: string]: any
}
export interface ModerationProject {
project: any
ownership: ModerationOwnership | null
owner: TeamMember | null
org: Organization | null
}
export function toModerationProjects(projects: ProjectWithOwnership[]): ModerationProject[] {
return projects.map(({ ownership, ...project }) => ({
project,
ownership: ownership ?? null,
}))
export async function enrichProjectBatch(projects: any[]): Promise<ModerationProject[]> {
const teamIds = [...new Set(projects.map((p) => p.team_id).filter(Boolean))]
const orgIds = [...new Set(projects.map((p) => p.organization).filter(Boolean))]
const [teamsData, orgsData]: [TeamMember[][], Organization[]] = await Promise.all([
teamIds.length > 0
? fetchSegmented(teamIds, (ids) => `teams?ids=${asEncodedJsonArray(ids)}`)
: Promise.resolve([]),
orgIds.length > 0
? fetchSegmented(orgIds, (ids) => `organizations?ids=${asEncodedJsonArray(ids)}`, {
apiVersion: 3,
})
: Promise.resolve([]),
])
const cache = useModerationCache()
teamsData.forEach((team) => {
if (team.length > 0) cache.teams.value.set(team[0].team_id, team)
})
orgsData.forEach((org: Organization) => {
cache.orgs.value.set(org.id, org)
})
return projects.map((project) => {
let owner: TeamMember | null = null
let org: Organization | null = null
if (project.team_id) {
const teamMembers = cache.teams.value.get(project.team_id)
if (teamMembers) {
owner = teamMembers.find((member) => member.role === 'Owner') || null
}
}
if (project.organization) {
org = cache.orgs.value.get(project.organization) || null
}
return {
project,
owner,
org,
} as ModerationProject
})
}
+8 -23
View File
@@ -34,39 +34,25 @@
'modrinth-parent__no-modal-blurs': !cosmetics.advancedRendering,
}"
>
<RussiaBanner v-if="flags.showAllBanners || isRussia" />
<TaxIdMismatchBanner v-if="flags.showAllBanners || showTinMismatchBanner" />
<TaxComplianceBanner v-if="flags.showAllBanners || showTaxComplianceBanner" />
<RussiaBanner v-if="isRussia" />
<TaxIdMismatchBanner v-if="showTinMismatchBanner" />
<TaxComplianceBanner v-if="showTaxComplianceBanner" />
<VerifyEmailBanner
v-if="
flags.showAllBanners ||
(auth.user && !auth.user.email_verified && route.path !== '/auth/verify-email')
"
v-if="auth.user && !auth.user.email_verified && route.path !== '/auth/verify-email'"
:has-email="!!auth?.user?.email"
/>
<SubscriptionPaymentFailedBanner
v-if="
flags.showAllBanners ||
(user.subscriptions.some((x) => x.status === 'payment-failed') &&
route.path !== '/settings/billing')
"
/>
<PreviewBanner
v-if="
flags.showAllBanners || (config.public.buildEnv === 'production' && config.public.preview)
"
/>
<StagingBanner
v-if="
flags.showAllBanners ||
config.public.apiBaseUrl.startsWith('https://staging-api.modrinth.com')
user.subscriptions.some((x) => x.status === 'payment-failed') &&
route.path !== '/settings/billing'
"
/>
<PreviewBanner v-if="config.public.buildEnv === 'production' && config.public.preview" />
<StagingBanner v-if="config.public.apiBaseUrl.startsWith('https://staging-api.modrinth.com')" />
<GeneratedStateErrorsBanner
:errors="generatedStateErrors"
:api-url="config.public.apiBaseUrl"
/>
<ViewOnModrinthBanner />
<header
class="desktop-only relative z-[5] mx-auto grid max-w-[1280px] grid-cols-[1fr_auto] items-center gap-2 px-6 py-4 lg:grid-cols-[auto_1fr_auto]"
>
@@ -781,7 +767,6 @@ import SubscriptionPaymentFailedBanner from '~/components/ui/banner/Subscription
import TaxComplianceBanner from '~/components/ui/banner/TaxComplianceBanner.vue'
import TaxIdMismatchBanner from '~/components/ui/banner/TaxIdMismatchBanner.vue'
import VerifyEmailBanner from '~/components/ui/banner/VerifyEmailBanner.vue'
import ViewOnModrinthBanner from '~/components/ui/banner/ViewOnModrinthBanner.vue'
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue'
import ProjectCreateModal from '~/components/ui/create/ProjectCreateModal.vue'
+3 -6
View File
@@ -506,12 +506,6 @@
"create.project.title": {
"message": "Vytvořit projekt"
},
"create.project.type-project": {
"message": "Projekt"
},
"create.project.type-server": {
"message": "Server"
},
"create.project.url-label": {
"message": "URL"
},
@@ -950,6 +944,9 @@
"moderation.page.reports": {
"message": "Hlášení"
},
"moderation.page.technicalReview": {
"message": "Technické hodnocení"
},
"muralpay.country.at": {
"message": "Rakousko"
},
@@ -1229,33 +1229,6 @@
"dashboard.withdraw.error.tax-form.title": {
"message": "Bitte Steuerformular ausfüllen"
},
"discover.install.back-to-server": {
"message": "Zurück zum Server"
},
"discover.install.back-to-setup": {
"message": "Zurück zur Einrichtung"
},
"discover.install.cancel-reset": {
"message": "Reset abbrechen"
},
"discover.install.error.no-server-world": {
"message": "Keine Serverwelt ist zur installation verfügbar."
},
"discover.install.error.unsupported-content-type": {
"message": "Diese Art von Inhalt kann nicht vom durchstöbern zu einem Server hinzugefügt werden."
},
"discover.install.heading.reset-modpack": {
"message": "Modpack zum installieren nach dem Reset auswählen"
},
"discover.seo.description": {
"message": "Suche und durchstöbere tausende von Minecraft {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}} auf Modrinth sofortigen, genauen Suchergebnissen. Unsere Filter helfen dir beim schnellen finden der besten Minecraft {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}}."
},
"discover.seo.title": {
"message": "Durchsuche {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}}"
},
"discover.seo.title-with-query": {
"message": "Durchsuche {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}} | {query}"
},
"error.collection.404.list_item.1": {
"message": "Du hast eventuell die Kollektions-URL falsch geschrieben."
},
@@ -1271,12 +1244,6 @@
"error.collection.404.title": {
"message": "Kollektion nicht gefunden"
},
"error.generic.401.signed-in-as": {
"message": "Angemeldet als:"
},
"error.generic.401.title": {
"message": "Du hast keinen Zugriff auf diese Seite"
},
"error.generic.404.subtitle": {
"message": "Die Seite nach welcher du suchst, scheint nicht zu existieren."
},
@@ -1703,9 +1670,6 @@
"layout.action.create-new": {
"message": "Erstellen..."
},
"layout.action.external-projects": {
"message": "Externe Projekte"
},
"layout.action.file-lookup": {
"message": "Datei ansehen"
},
@@ -1958,9 +1922,6 @@
"moderation.moderate": {
"message": "Moderieren"
},
"moderation.page.external-projects": {
"message": "Externe Projekte"
},
"moderation.page.projects": {
"message": "Projekte"
},
@@ -2654,45 +2615,6 @@
"project.settings.general.url.title": {
"message": "URL"
},
"project.settings.permissions.attention-needed.description.proj-approved": {
"message": "Bitte lege Nachweise vor, dass du die Erlaubnis zur Weiterverbreitung fgolgender Dateien hast; alle zurückgehaltenen Versionen werden automatisch veröffentlicht."
},
"project.settings.permissions.attention-needed.description.proj-draft": {
"message": "Bitte lege Nachweise vor, dass du die Erlaubnis zur Weiterverbreitung fgolgender Dateien hast, bevor du dein Projekt zur überprüfung einreichen kannst."
},
"project.settings.permissions.attention-needed.title": {
"message": "Unbekannter eingebetteter Inhalt"
},
"project.settings.permissions.completed.description": {
"message": "Alle externen Inhalte haben Quellenangaben."
},
"project.settings.permissions.completed.title": {
"message": "Zuordnungen abgschlossen!"
},
"project.settings.permissions.empty-state.description": {
"message": "Keine deiner Versionen enrhalten externe Inhalte, also musst du dir keine Sorgen über die Einholung von Berechtigungen machen."
},
"project.settings.permissions.empty-state.heading": {
"message": "Du bist startklar!"
},
"project.settings.permissions.fail.description": {
"message": "Du hast keine Berechtigung, einige der von dir hinzugefügten externen Inhalte weiterzuverbreiten. Um auf Modrinth veröffentlichen zu können, entferne bitte die urheberrechtswidrigen Inhalte."
},
"project.settings.permissions.fail.title": {
"message": "Einige Inhalte können nicht enthalten sein"
},
"project.settings.permissions.info-banner.description": {
"message": "Wenn du Inhalte einbindest, welche nicht auf Modrinth gehostet werden, musst du uns deren Quelle mitteilen und bestätigen, dass du die Berechtigung zur Verbreitung der Dateien hast. In <link>unserer Anleitung (Englisch)</link> lernst du, wie du dies am besten machst!"
},
"project.settings.permissions.info-banner.title": {
"message": "Lerne wie Zuschreibungen funktionieren"
},
"project.settings.permissions.learn-more": {
"message": "Mehr erfahren"
},
"project.settings.permissions.search-placeholder": {
"message": "Durchsuche {count} {count, plural, one {externes Projekt} other {externe Projekte}}..."
},
"project.settings.title": {
"message": "Einstellungen"
},
@@ -2711,15 +2633,6 @@
"project.versions.title": {
"message": "Versionen"
},
"project.versions.withheld-versions-warning.description": {
"message": "{count, plural, one {Diese Version ist} other {Diese Versionen sind}} derzeit zurückgehalten und nicht öffentlich gelistet. Bitte stelle einen Nachweis bereit, dass du die Erlaubnis hast, bestimmte Dateien weiterzuverbreiten, die in {count, plural, one {der Modpack-Version} other {den Modpack-Versionen}} enthalten sind."
},
"project.versions.withheld-versions-warning.resolve-button": {
"message": "Beheben"
},
"project.versions.withheld-versions-warning.title": {
"message": "{count, plural, one {Version {version_name}} other {Versionen}} zurückgehalten aufgrund von unbekannten eingebetteten Inhalten"
},
"report.already-reported": {
"message": "Du hast bereits {title} gemeldet"
},
@@ -3089,9 +3002,6 @@
"search.filter.locked.server.sync": {
"message": "Mit Server synchronisieren"
},
"servers.manage.content.title": {
"message": "Inhalt - {serverName} - Modrinth"
},
"servers.notice.actions": {
"message": "Aktionen"
},
@@ -1229,27 +1229,6 @@
"dashboard.withdraw.error.tax-form.title": {
"message": "Bitte Steuerformular ausfüllen"
},
"discover.install.back-to-server": {
"message": "Zurück zum Server"
},
"discover.install.back-to-setup": {
"message": "Zurück zur Einrichtung"
},
"discover.install.cancel-reset": {
"message": "Zurücksetzung abbrechen"
},
"discover.install.error.no-server-world": {
"message": "Keine Serverwelt ist zur Installation verfügbar."
},
"discover.seo.description": {
"message": "Durchsuche und entdecke tausende Minecraft {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}} auf Modrinth mit sofortigen und präzisen Suchergebnissen. Unsere Filter helfen dir dabei, schnell die besten Minecraft {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}} zu finden."
},
"discover.seo.title": {
"message": "Durchsuche {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}}"
},
"discover.seo.title-with-query": {
"message": "Durchsuche {projectType, select, mod {Mods} modpack {Modpacks} resourcepack {Ressourcenpakete} shader {Shader} plugin {Plugins} datapack {Datenpakete} other {Projekte}} | {query}"
},
"error.collection.404.list_item.1": {
"message": "Eventuell hast du die URL der Kollektion falsch eingegeben."
},
@@ -1265,12 +1244,6 @@
"error.collection.404.title": {
"message": "Kollektion nicht gefunden"
},
"error.generic.401.signed-in-as": {
"message": "Du bist aktuell angemeldet als:"
},
"error.generic.401.title": {
"message": "Du hast keinen Zugriff auf diese Seite"
},
"error.generic.404.subtitle": {
"message": "Die von dir gesuchte Seite scheint nicht zu existieren."
},
@@ -1697,9 +1670,6 @@
"layout.action.create-new": {
"message": "Erstellen..."
},
"layout.action.external-projects": {
"message": "Externe Projekte"
},
"layout.action.file-lookup": {
"message": "Dateisuche"
},
@@ -1952,9 +1922,6 @@
"moderation.moderate": {
"message": "Moderieren"
},
"moderation.page.external-projects": {
"message": "Externe Projekte"
},
"moderation.page.projects": {
"message": "Projekte"
},
@@ -2648,45 +2615,6 @@
"project.settings.general.url.title": {
"message": "URL"
},
"project.settings.permissions.attention-needed.description.proj-approved": {
"message": "Bitte lege Nachweise vor, dass du die Erlaubnis zur Weiterverbreitung folgender Dateien hast; alle zurückgehaltenen Versionen werden automatisch veröffentlicht."
},
"project.settings.permissions.attention-needed.description.proj-draft": {
"message": "Bitte lege Nachweise vor, dass du die Erlaubnis zur Weiterverbreitung aller folgenden Dateien hast, bevor du dein Projekt zur Überprüfung einreichen kannst."
},
"project.settings.permissions.attention-needed.title": {
"message": "Unbekannter eingebetteter Inhalt"
},
"project.settings.permissions.completed.description": {
"message": "Alle externen Inhalte haben Quellenangaben."
},
"project.settings.permissions.completed.title": {
"message": "Quellenangaben vollständig!"
},
"project.settings.permissions.empty-state.description": {
"message": "Keine deiner Versionen enthält externe Inhalte, du musst du dir keine Gedanken über die Einholung von Genehmigungen machen."
},
"project.settings.permissions.empty-state.heading": {
"message": "Du bist startklar!"
},
"project.settings.permissions.fail.description": {
"message": "Du hast keine Berechtigung, einige der von dir hinzugefügten externen Inhalte weiterzuverbreiten. Entferne die urheberrechtsverletzenden Inhalte, um auf Modrinth veröffentlichen zu können."
},
"project.settings.permissions.fail.title": {
"message": "Bestimmte Inhalte können nicht enthalten sein"
},
"project.settings.permissions.info-banner.description": {
"message": "Wenn du Inhalte einbindest, welche nicht auf Modrinth gehostet werden, musst du uns deren Quelle mitteilen und bestätigen, dass du die Berechtigung zur Verbreitung der Dateien hast. In <link>unserer Anleitung</link> lernst du, wie du dies am besten machst!"
},
"project.settings.permissions.info-banner.title": {
"message": "So funktionieren Quellenangaben"
},
"project.settings.permissions.learn-more": {
"message": "Mehr erfahren"
},
"project.settings.permissions.search-placeholder": {
"message": "Durchsuche {count} {count, plural, one {externes Projekt} other {externe Projekte}}..."
},
"project.settings.title": {
"message": "Einstellungen"
},
@@ -2705,15 +2633,6 @@
"project.versions.title": {
"message": "Versionen"
},
"project.versions.withheld-versions-warning.description": {
"message": "{count, plural, one {Diese Version ist} other {Diese Versionen sind}} derzeit zurückgehalten und nicht öffentlich gelistet. Bitte stelle einen Nachweis bereit, dass du die Erlaubnis hast, bestimmte Dateien weiterzuverbreiten, die im Modpack in {count, plural, one {Version} other {Versionen}} enthalten sind."
},
"project.versions.withheld-versions-warning.resolve-button": {
"message": "Beheben"
},
"project.versions.withheld-versions-warning.title": {
"message": "{count, plural, one {Version {version_name} wurde} other {Versionen wurden}} aufgrund unbekannter eingebetteter Inhalte zurückgehalten"
},
"report.already-reported": {
"message": "Du hast {title} bereits gemeldet"
},
@@ -3083,9 +3002,6 @@
"search.filter.locked.server.sync": {
"message": "Mit Server synchronisieren"
},
"servers.manage.content.title": {
"message": "Inhalt - {serverName} - Modrinth"
},
"servers.notice.actions": {
"message": "Aktionen"
},
+20 -248
View File
@@ -407,117 +407,6 @@
"collection.title": {
"message": "{name} - Collection"
},
"conversation-thread.action.add-private-note": {
"message": "Add private note"
},
"conversation-thread.action.approve": {
"message": "Approve"
},
"conversation-thread.action.approve-with-reply": {
"message": "Approve with reply"
},
"conversation-thread.action.close-thread": {
"message": "Close thread"
},
"conversation-thread.action.close-with-reply": {
"message": "Close with reply"
},
"conversation-thread.action.reject": {
"message": "Reject"
},
"conversation-thread.action.reject-with-reply": {
"message": "Reject with reply"
},
"conversation-thread.action.reopen-thread": {
"message": "Reopen thread"
},
"conversation-thread.action.reply": {
"message": "Reply"
},
"conversation-thread.action.reply-to-thread": {
"message": "Reply to thread"
},
"conversation-thread.action.resubmit-for-review": {
"message": "Resubmit for review"
},
"conversation-thread.action.resubmit-for-review-with-reply": {
"message": "Resubmit for review with reply"
},
"conversation-thread.action.send": {
"message": "Send"
},
"conversation-thread.action.send-to-review": {
"message": "Send to review"
},
"conversation-thread.action.send-to-review-with-reply": {
"message": "Send to review with reply"
},
"conversation-thread.action.set-to-draft": {
"message": "Set to draft"
},
"conversation-thread.action.set-to-draft-with-reply": {
"message": "Set to draft with reply"
},
"conversation-thread.action.withhold": {
"message": "Withhold"
},
"conversation-thread.action.withhold-with-reply": {
"message": "Withhold with reply"
},
"conversation-thread.closed-thread.description": {
"message": "This thread is closed and new messages cannot be sent to it."
},
"conversation-thread.error.closing-report": {
"message": "Error closing report"
},
"conversation-thread.error.reopening-report": {
"message": "Error reopening report"
},
"conversation-thread.error.sending-message": {
"message": "Error sending message"
},
"conversation-thread.reply-editor.placeholder.reply": {
"message": "Reply to thread..."
},
"conversation-thread.reply-editor.placeholder.send": {
"message": "Send a message..."
},
"conversation-thread.reply-modal.confirmation.description": {
"message": "Confirm moderators do not actively monitor this"
},
"conversation-thread.reply-modal.confirmation.label": {
"message": "I acknowledge that the moderators do not actively monitor the thread."
},
"conversation-thread.reply-modal.description": {
"message": "Your project is already approved. As such, the moderation team does not actively monitor this thread. However, they may still see your message if there is a problem with your project."
},
"conversation-thread.reply-modal.header": {
"message": "Reply to thread"
},
"conversation-thread.reply-modal.help-center-note": {
"message": "If you need to get in contact with the moderation team, please use the <help-center-link>Modrinth Help Center</help-center-link> and click the blue bubble in the bottom right corner to contact support."
},
"conversation-thread.resubmit-modal.confirmation.description": {
"message": "Confirm I have addressed the messages from the moderators"
},
"conversation-thread.resubmit-modal.confirmation.label": {
"message": "I confirm that I have properly addressed the moderators' comments."
},
"conversation-thread.resubmit-modal.description": {
"message": "You're submitting <project-title>{projectTitle}</project-title> to be reviewed again by the moderators."
},
"conversation-thread.resubmit-modal.header.resubmitting": {
"message": "Resubmitting for review"
},
"conversation-thread.resubmit-modal.header.submitting": {
"message": "Submitting for review"
},
"conversation-thread.resubmit-modal.reminder": {
"message": "Make sure you have addressed all the comments from the moderation team."
},
"conversation-thread.resubmit-modal.warning": {
"message": "Repeated submissions without addressing the moderators' comments may result in an account suspension."
},
"create-project-version.create-modal.stage.add-files.admonition": {
"message": "Supplementary files are for supporting resources like source code, not for alternative versions or variants."
},
@@ -986,8 +875,23 @@
"dashboard.head-title": {
"message": "Dashboard"
},
"dashboard.notifications.button.mark-all-as-read": {
"message": "Mark all as read"
},
"dashboard.notifications.button.view-history": {
"message": "View history"
},
"dashboard.notifications.empty.no-unread": {
"message": "You have no unread notifications."
"message": "You don't have any unread notifications."
},
"dashboard.notifications.error.loading": {
"message": "Error loading notifications:"
},
"dashboard.notifications.history.label": {
"message": "History"
},
"dashboard.notifications.history.title": {
"message": "Notification history"
},
"dashboard.notifications.link.see-all": {
"message": "See all"
@@ -998,6 +902,9 @@
"dashboard.notifications.link.view-more": {
"message": "View {extraNotifs} more {extraNotifs, plural, one {notification} other {notifications}}"
},
"dashboard.notifications.loading": {
"message": "Loading notifications..."
},
"dashboard.organizations.button.create": {
"message": "Create organization"
},
@@ -1013,27 +920,6 @@
"dashboard.organizations.title": {
"message": "Organizations"
},
"dashboard.overview.notifications.button.mark-all-as-read": {
"message": "Mark all as read"
},
"dashboard.overview.notifications.button.view-history": {
"message": "View history"
},
"dashboard.overview.notifications.empty.no-unread": {
"message": "You don't have any unread notifications."
},
"dashboard.overview.notifications.error.loading": {
"message": "Error loading notifications:"
},
"dashboard.overview.notifications.history.label": {
"message": "History"
},
"dashboard.overview.notifications.history.title": {
"message": "Notification history"
},
"dashboard.overview.notifications.loading": {
"message": "Loading notifications..."
},
"dashboard.projects.bulk-edit-hint": {
"message": "You can edit multiple projects at once by selecting them below."
},
@@ -1343,33 +1229,6 @@
"dashboard.withdraw.error.tax-form.title": {
"message": "Please complete tax form"
},
"discover.install.back-to-server": {
"message": "Back to server"
},
"discover.install.back-to-setup": {
"message": "Back to setup"
},
"discover.install.cancel-reset": {
"message": "Cancel reset"
},
"discover.install.error.no-server-world": {
"message": "No server world is available for install."
},
"discover.install.error.unsupported-content-type": {
"message": "This content type cannot be installed to a server from browse."
},
"discover.install.heading.reset-modpack": {
"message": "Selecting modpack to install after reset"
},
"discover.seo.description": {
"message": "Search and browse thousands of Minecraft {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}} on Modrinth with instant, accurate search results. Our filters help you quickly find the best Minecraft {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}}."
},
"discover.seo.title": {
"message": "Search {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}}"
},
"discover.seo.title-with-query": {
"message": "Search {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}} | {query}"
},
"error.collection.404.list_item.1": {
"message": "You may have mistyped the collection's URL."
},
@@ -1868,20 +1727,14 @@
"layout.banner.add-email.description": {
"message": "For security reasons, Modrinth needs you to register an email address to your account."
},
"layout.banner.build-fail.always-ignore": {
"message": "Always ignore"
},
"layout.banner.build-fail.description": {
"message": "This deploy of Modrinth's frontend failed to generate state from the API. This may be due to an outage or an error in configuration. Rebuild when the API is available. Error codes: {errors}; Current API URL is: {url}"
},
"layout.banner.build-fail.ignore": {
"message": "Ignore"
},
"layout.banner.build-fail.title": {
"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 <link>https://modrinth.com</link>. This preview deploy is used by Modrinth staff for testing purposes. It was built using <branch-link>{owner}/{branch}</branch-link> @ {commit}."
},
"layout.banner.preview.title": {
"message": "This is a preview deploy of the Modrinth website."
@@ -2711,84 +2564,6 @@
"project.license.title": {
"message": "License"
},
"project.moderation.admonition.approved.body.private": {
"message": "Your project is private, meaning it can only be accessed by you and people you invite."
},
"project.moderation.admonition.approved.body.public": {
"message": "Your project is published and discoverable on Modrinth."
},
"project.moderation.admonition.approved.body.unlisted": {
"message": "Your project is unlisted, meaning it can only be accessed with a direct link and is not discoverable on Modrinth."
},
"project.moderation.admonition.approved.body.visibility-message": {
"message": "You can change the visibility of your project in your project's <visibility-settings-link>visibility settings</visibility-settings-link>."
},
"project.moderation.admonition.approved.header": {
"message": "Project approved"
},
"project.moderation.admonition.draft.body": {
"message": "This is a draft project that cannot be seen by others until submitted for review and approved by Modrinth's moderation team."
},
"project.moderation.admonition.draft.header": {
"message": "Draft project"
},
"project.moderation.admonition.draft.submit-for-review": {
"message": "Once you have completed all required steps and ensured your project complies with Modrinth's <rules-link>Content Rules</rules-link> you can submit your project for review."
},
"project.moderation.admonition.rejected.address-all-concerns": {
"message": "Please address all moderation concerns, including any issues listed in messages below, before resubmitting this project."
},
"project.moderation.admonition.rejected.header": {
"message": "Changes requested"
},
"project.moderation.admonition.rejected.spam-notice": {
"message": "Repeatedly submitting your project without addressing all moderation concerns first may result in account suspension."
},
"project.moderation.admonition.under-review.body.1": {
"message": "Your project is in queue to be reviewed by Modrinth's moderation team."
},
"project.moderation.admonition.under-review.body.2": {
"message": "Your project will be scanned and then reviewed by human moderators to ensure it meets Modrinth's <rules-link>Content Rules</rules-link> and <terms-link>Terms of Use</terms-link>."
},
"project.moderation.admonition.under-review.body.3": {
"message": "You can still modify your project, it won't affect your position in the queue."
},
"project.moderation.admonition.under-review.body.4": {
"message": "We aim to review submissions in 24-48 hours, but some projects may face delays. This does not reflect an issue with your submission."
},
"project.moderation.admonition.under-review.body.5": {
"message": "<emphasis>We appreciate your patience while our moderators work hard to keep Modrinth safe, and look forward to helping you share your content! 💚</emphasis>"
},
"project.moderation.admonition.under-review.header": {
"message": "Project under review"
},
"project.moderation.admonition.withheld.body": {
"message": "Your project will not appear publicly and can only be accessed with a direct link.{requestedStatus, select, unlisted { Based on your selected <visibility-settings-link>visibility settings</visibility-settings-link>, most likely no action is necessary.} other { Please address all moderation concerns, including any issues listed in messages below before resubmitting this project.}}"
},
"project.moderation.admonition.withheld.header": {
"message": "Unlisted by staff"
},
"project.moderation.error.unauthorized": {
"message": "Unauthorized"
},
"project.moderation.thread.approved-warning": {
"message": "This thread is not actively monitored, but may be reviewed for information about your project if needed."
},
"project.moderation.thread.help-center-note.1": {
"message": "Content moderators cannot provide support for most issues and messages to this thread do not notify staff."
},
"project.moderation.thread.help-center-note.2": {
"message": "If you need assistance or have additional inquiries, please visit the <help-center-link>Modrinth Help Center</help-center-link> and click the blue bubble to contact support."
},
"project.moderation.thread.moderator-see-user-ui-toggle": {
"message": "Show member UI"
},
"project.moderation.thread.private-description": {
"message": "This is a private conversation thread with the Modrinth moderators. They may message you with issues concerning this project."
},
"project.moderation.thread.title": {
"message": "Moderation messages"
},
"project.moderation.title": {
"message": "Moderation"
},
@@ -3287,9 +3062,6 @@
"search.filter.locked.server.sync": {
"message": "Sync with server"
},
"servers.manage.content.title": {
"message": "Content - {serverName} - Modrinth"
},
"servers.notice.actions": {
"message": "Actions"
},
+9 -69
View File
@@ -924,7 +924,7 @@
"message": "Puedes editar varios proyectos a la vez seleccionándolos a continuación."
},
"dashboard.projects.bulk-edit.server-disabled": {
"message": "Los proyectos no son compatible con la edición masiva de servidores"
"message": "Proyectos de servidor no soportan edición masiva"
},
"dashboard.projects.empty": {
"message": "Aún no tienes ningún proyecto. Haz clic en el botón verde de arriba para empezar."
@@ -942,19 +942,19 @@
"message": "Editar enlaces"
},
"dashboard.projects.links.changes-applied": {
"message": "Los cambios se aplicaran en <strong>{count}</strong>{count, plural,one {proyecto}other {proyectos}}."
"message": "Cambios seran a aplicados a {count} {count, plural,one {proyecto} other {proyectos}}."
},
"dashboard.projects.links.description": {
"message": "Cualquier enlace escrito más abajo se modificará en los proyectos seleccionados. Los que dejes en blanco serán ignorados. Puedes quitar un enlace de todos los proyectos seleccionados usando el botón de basurero."
"message": "Los enlaces que especifique a continuación se sobrescribin en cada uno de los proyectos seleccionados. Los que deje en blanco se ignorarán. Puede eliminar un enlace de todos los proyectos seleccionados mediante el botón de la papelera."
},
"dashboard.projects.links.discord-invite.description": {
"message": "Una invitación a tu servidor de Discord."
"message": "Un link de invitación a tu servidor de Discord."
},
"dashboard.projects.links.discord-invite.label": {
"message": "Invitación de Discord"
},
"dashboard.projects.links.issue-tracker.description": {
"message": "Un lugar donde los usuarios reporten errores, problemas y preocupaciones sobre tu proyecto."
"message": "Un lugar para usuarios para reportar bugs, errores, y preocupaciones sobre tu proyecto."
},
"dashboard.projects.links.issue-tracker.label": {
"message": "Registro de errores"
@@ -984,7 +984,7 @@
"message": "Página wiki"
},
"dashboard.projects.notification.bulk-edit-success": {
"message": "Se editaron los enlaces de los proyectos seleccionados."
"message": "Edición masiva seleccionada de tus links de proyecto."
},
"dashboard.projects.project.icon-alt": {
"message": "Icono de {title}"
@@ -1244,12 +1244,6 @@
"error.collection.404.title": {
"message": "Colección no encontrada"
},
"error.generic.401.signed-in-as": {
"message": "Has iniciado sesión como:"
},
"error.generic.401.title": {
"message": "No tienes acceso a esta página"
},
"error.generic.404.subtitle": {
"message": "Parece que la página que estabas buscando no existe."
},
@@ -1665,7 +1659,7 @@
"message": "Para jugadores"
},
"landing.section.for-players.tagline": {
"message": "Descubre s de {count, number} creaciones"
"message": "Descubre sobre las creaciones de {count, number}"
},
"landing.subheading": {
"message": "Descubre, juega y comparte contenido de Minecraft a través de nuestra plataforma de código abierto creada para la comunidad."
@@ -1676,9 +1670,6 @@
"layout.action.create-new": {
"message": "Crear nuevo..."
},
"layout.action.external-projects": {
"message": "Proyectos externos"
},
"layout.action.file-lookup": {
"message": "Buscar archivo"
},
@@ -1931,9 +1922,6 @@
"moderation.moderate": {
"message": "Moderar"
},
"moderation.page.external-projects": {
"message": "Proyectos externos"
},
"moderation.page.projects": {
"message": "Proyectos"
},
@@ -2627,45 +2615,6 @@
"project.settings.general.url.title": {
"message": "URL"
},
"project.settings.permissions.attention-needed.description.proj-approved": {
"message": "Por favor proporciona pruebas de que tienes permiso para redistribuir todos estos archivos y cualquier versión retenida se publicará automáticamente."
},
"project.settings.permissions.attention-needed.description.proj-draft": {
"message": "Por favor proporciona pruebas de que tienes permiso para redistribuir todos estos archivos antes de enviar tu proyecto a revisión."
},
"project.settings.permissions.attention-needed.title": {
"message": "Incluye contenido desconocido"
},
"project.settings.permissions.completed.description": {
"message": "Todo el contenido externo proporciona atribuciones."
},
"project.settings.permissions.completed.title": {
"message": "¡Atribuciones completadas!"
},
"project.settings.permissions.empty-state.description": {
"message": "Ninguna de tus versiones presenta contenido externo, por lo que no necesitas preocuparte de obtener permisos."
},
"project.settings.permissions.empty-state.heading": {
"message": "¡Ya estás listo!"
},
"project.settings.permissions.fail.description": {
"message": "No tienes permiso de redistribuir parte del contenido externo que añadiste. Para poder publicar en Modrinth, quite el contenido infractor."
},
"project.settings.permissions.fail.title": {
"message": "No se puede incluir parte del contenido"
},
"project.settings.permissions.info-banner.description": {
"message": "Si incluyes contenido que no está en Modrinth, debes dejarnos saber de donde es y verificar que tienes permiso de distribuir los archivos. ¡Mira <link>nuestra guía</link> para aprender como hacerlo!"
},
"project.settings.permissions.info-banner.title": {
"message": "Aprende cómo funcionan las atribuciones"
},
"project.settings.permissions.learn-more": {
"message": "Aprende más"
},
"project.settings.permissions.search-placeholder": {
"message": "Busca {count} {count, plural, one {proyecto externo} other {proyectos externos}}..."
},
"project.settings.title": {
"message": "Configuración"
},
@@ -2684,23 +2633,14 @@
"project.versions.title": {
"message": "Versiones"
},
"project.versions.withheld-versions-warning.description": {
"message": "{count, plural, one {Esta versión se encuentra retenida y no listada} other {Estas versiones se encuentran retenidas y no listadas}} publicamente. Por favor proporcione pruebas que tiene permiso de redistribuir algunos de los archivos presentes en {count, plural, one {la versión} other {las versiones}} del modpack."
},
"project.versions.withheld-versions-warning.resolve-button": {
"message": "Resolver"
},
"project.versions.withheld-versions-warning.title": {
"message": "{count, plural, one {Versión {version_name} retenida} other {Versiones retenidas}} debido a que incluye contenido desconocido"
},
"report.already-reported": {
"message": "Ya has reportado {title}"
},
"report.already-reported-description": {
"message": "Ya tienes un reporte abierto para {item, select, project {este proyecto} version {esta versión} user {este usuario} other {este contenido}}. Puedes añadir más detalles a tu reporte si tienes más información que agregar."
"message": "Ya tienes un reporte abierto para este {item, select, project {project} version {version} user {user} other {content}}. Puedes añadir más detalles a tu reporte si tienes más información que agregar."
},
"report.back-to-item": {
"message": "Volver {item, select, project {al proyecto} version {a la versión} user {al usuario} other {al contenido}}"
"message": "Volver a {item, select, project {proyecto} version {version} user {usuario} other {contenido}}"
},
"report.body.description": {
"message": "Incluye enlaces e imágenes si es posible y relevante. Los reportes vacíos o insuficientes serán cerrados e ignorados."
@@ -1928,6 +1928,9 @@
"moderation.page.reports": {
"message": "Informes"
},
"moderation.page.technicalReview": {
"message": "Revisión Técnica"
},
"muralpay.account-type.checking": {
"message": "Cheque"
},
+10 -130
View File
@@ -393,7 +393,7 @@
"message": "Wala pang proyekto sa koleksiyon"
},
"collection.label.projects-count": {
"message": "{count, plural, =0 {Wala pang proyekto} other {<stat>{count}</stat> {type}}}"
"message": "{count, plural, =0 {Wala pang proyekto} one {<stat>{count}</stat> {type}} other {<stat>{count}</stat> na {type}}}"
},
"collection.label.updated-at": {
"message": "In-update {ago}"
@@ -554,9 +554,6 @@
"dashboard.affiliate-links.create.button": {
"message": "Lumikha ng affiliate link"
},
"dashboard.affiliate-links.empty.no-codes": {
"message": "Walang mahanap na affiliate code."
},
"dashboard.affiliate-links.error.title": {
"message": "Bigong ma-load ang mga affiliate link"
},
@@ -575,15 +572,6 @@
"dashboard.affiliate-links.search": {
"message": "Maghanap ng affiliate link..."
},
"dashboard.analytics.from-projects": {
"message": "galing sa {count} {count, plural, one {proyekto} other {na proyekto}}"
},
"dashboard.analytics.total-downloads": {
"message": "Kabuuang pag-download"
},
"dashboard.analytics.total-followers": {
"message": "Kabuuang tagasunod"
},
"dashboard.collections.button.create-new": {
"message": "Lumikha ng bago"
},
@@ -603,23 +591,11 @@
"message": "{count} {countPlural, plural, one {proyekto} other {na proyekto}}"
},
"dashboard.collections.label.search-input": {
"message": "Maghanap sa iyong koleksiyon"
"message": "Hanapin ang iyong mga koleksiyon"
},
"dashboard.collections.long-title": {
"message": "Mga koleksiyon mo"
},
"dashboard.collections.placeholder.search": {
"message": "Maghanap ng koleksiyon..."
},
"dashboard.collections.sort.name-ascending": {
"message": "Pangalan (A-Z)"
},
"dashboard.collections.sort.recently-created": {
"message": "Kamakailang Linikha"
},
"dashboard.collections.sort.recently-updated": {
"message": "Kamakailang In-update"
},
"dashboard.creator-tax-form-modal.confirmation.download-button": {
"message": "Download {formType}"
},
@@ -872,66 +848,6 @@
"dashboard.creator-withdraw-modal.withdraw-limit-used": {
"message": "Naubos mo na ang iyong <b>{withdrawLimit}</b> hangganan sa pag-withdraw. Mangangailangan kang kumumpleto ng pormularyo sa buwis upang makapag-withdraw ngayon higit pa."
},
"dashboard.head-title": {
"message": "Dashboard"
},
"dashboard.notifications.button.mark-all-as-read": {
"message": "Tratuhing binasa na"
},
"dashboard.notifications.button.view-history": {
"message": "Tingnan ang kasaysayan"
},
"dashboard.notifications.empty.no-unread": {
"message": "Wala kang di-nababasang notipikasyon."
},
"dashboard.notifications.error.loading": {
"message": "Error sa paglo-load ng mga notipikasyon:"
},
"dashboard.notifications.history.label": {
"message": "Kasaysayan"
},
"dashboard.notifications.history.title": {
"message": "Kasaysayan ng notipikasyon"
},
"dashboard.notifications.link.see-all": {
"message": "Tingnan lahat"
},
"dashboard.notifications.link.view-history": {
"message": "Tingnan ang kasaysayan ng notipikasyon"
},
"dashboard.notifications.link.view-more": {
"message": "Tingnan ang {extraNotifs} pang {extraNotifs, plural, other {notipikasyon}}"
},
"dashboard.notifications.loading": {
"message": "Naglo-load ang mga notipikasyon..."
},
"dashboard.organizations.button.create": {
"message": "Lumikha ng organisasyon"
},
"dashboard.organizations.empty.cta": {
"message": "Gumawa ng organisasyon!"
},
"dashboard.organizations.member-count": {
"message": "{count} {count, plural, one {miyembro} other {na miyembro}}"
},
"dashboard.organizations.title": {
"message": "Organisasyon"
},
"dashboard.projects.head-title": {
"message": "Mga proyekto"
},
"dashboard.projects.links.and-more": {
"message": "at {count} pa..."
},
"dashboard.projects.project.review-environment-metadata": {
"message": "Pakisuri sa metadata sa environment"
},
"dashboard.reports.active-title": {
"message": "Aktibong sumbong"
},
"dashboard.reports.title": {
"message": "Mga sumbong"
},
"dashboard.revenue.available-now": {
"message": "Magagamit ngayon"
},
@@ -968,9 +884,6 @@
"dashboard.revenue.transactions.btn.download-csv": {
"message": "I-download bilang CSV"
},
"dashboard.revenue.transactions.head-title": {
"message": "Kasaysayan ng transaksiyon"
},
"dashboard.revenue.transactions.header": {
"message": "Mga transaksiyon"
},
@@ -983,9 +896,6 @@
"dashboard.revenue.transactions.see-all": {
"message": "Tingnan lahat"
},
"dashboard.revenue.transactions.year.all": {
"message": "Lahat ng taon"
},
"dashboard.revenue.withdraw.blocked-tin-mismatch": {
"message": "Ang iyong pag-withdraw ay pansamantalang pinatigilan dahil hindi tumugma ang iyong TIN o SSN sa talaan ng IRS. Mangyaring abutin ang suporta upang ma-reset at muling sumitehin ang iyong pormularyo sa buwis."
},
@@ -998,9 +908,6 @@
"dashboard.revenue.withdraw.header": {
"message": "Mag-withdraw"
},
"dashboard.sidebar.label.activeReports": {
"message": "Aktibong sumbong"
},
"dashboard.withdraw.completion.account": {
"message": "Account"
},
@@ -1050,7 +957,7 @@
"message": "Kailangang mapatunayan ang email"
},
"dashboard.withdraw.error.generic.text": {
"message": "Hindi namin maisumite ang iyong hiling na mag-withdraw, mangyaring suriin ang iyong mga detalye o abutin ang suporta. {error}"
"message": "Hindi namin masumite ang iyong hiling na mag-withdraw, mangyaring suriin ang iyong mga detalye o abutin ang suporta. {error}"
},
"dashboard.withdraw.error.generic.title": {
"message": "Hindi makapag-withdraw"
@@ -1083,7 +990,7 @@
"message": "Ang halaga ng pag-withdraw (matapos ang singil) hindi umabot sa pinakamaliit na pinapayagang kailangan. Mangyaring taasan ang iyong halagang ma-withdraw."
},
"dashboard.withdraw.error.minimum-not-met.title": {
"message": "Labis na mababa ang balyu"
"message": "Labis na mababa ang halaga"
},
"dashboard.withdraw.error.paypal-country-mismatch.text": {
"message": "Mangyaring pillin ang wastong PayPal transfer option para sa iyong rehiyon (US o internasyunal)."
@@ -1590,7 +1497,7 @@
"message": "Itong pinakalat na frontend ng Modrinth ay bigong makabuo ng estado galing sa API. Maaaring dahil ito sa isang kawalan o kamalian sa kumpigurasyon. Muling magpatayo kapag magagamit na muli ang API. Mga code ng kamalian: {errors}; Ang kasalukuyang URL ng API ay: {url}"
},
"layout.banner.build-fail.title": {
"message": "Error sa pagbubuo ng estado galing sa API habang nagpapatayo."
"message": "May error sa pagbuo ng estado galing sa API habang nagpapatayo."
},
"layout.banner.preview.description": {
"message": "Kung ibig mong maakses ang opisyal na website ng Modrinth, bisitahin ang <link>https://modrinth.com</link>. Ito ay isang pinakalat na pasilip na ginagamit lamang ng kawani ng Modrinth sa pagsusubok nito. Ito ay pinatayo gamit ang <branch-link>{owner}/{branch}</branch-link> @ {commit}."
@@ -1797,7 +1704,7 @@
"message": "Mga Sumbong"
},
"moderation.page.technicalReview": {
"message": "Tech review"
"message": "Teknikal na Pagsusuri"
},
"muralpay.account-type.checking": {
"message": "Sinusuri"
@@ -2334,7 +2241,7 @@
"message": "Bersiyon ng laro: {version}"
},
"project.download.game-version-error": {
"message": "Error: walang mahanap na mga bersiyon ng laro"
"message": "Kamalian: walang mahanap na mga bersiyon ng laro"
},
"project.download.game-version-tooltip": {
"message": "Ang {title} ay magagamit lamang sa {version}"
@@ -2355,7 +2262,7 @@
"message": "Plataporma: {platform}"
},
"project.download.platform-error": {
"message": "Error: walang mahanap na platform"
"message": "Kamalian: walang mahanap na platform"
},
"project.download.platform-tooltip": {
"message": "Ang {title} ay magagamit lamang sa {platform}"
@@ -2400,7 +2307,7 @@
"message": "Pakisuri sa metadata sa environment"
},
"project.error.loading": {
"message": "Error sa paglo-load ng datos ng proyekto{message}"
"message": "Nagkamali sa pag-load ng project data{message}"
},
"project.error.page-not-found": {
"message": "Hindi mahanap na pahina"
@@ -2483,9 +2390,6 @@
"project.settings.general.url.title": {
"message": "URL"
},
"project.settings.permissions.info-banner.description": {
"message": "Kung magsasama ka ng mga kontentong hindi naka-host sa Modrinth, kailangan niyong ipaalam sa amin sa pinanggalingan nito at matiyak na pinahintulutang mapakalat ang mga file. Tingnan ang <link>aming gabay</link> upang matuto kung papaano ito magawa nang maayos!"
},
"project.settings.title": {
"message": "Mga Setting"
},
@@ -2912,9 +2816,6 @@
"settings.account.email.modal.header.add": {
"message": "Magdagdag ng email"
},
"settings.account.password.modal.header.add": {
"message": "Magdagdag ng password"
},
"settings.account.providers.action.add": {
"message": "Idagdag"
},
@@ -2930,9 +2831,6 @@
"settings.account.security.email.title": {
"message": "Email"
},
"settings.account.security.password.action.add": {
"message": "Magdagdag ng password"
},
"settings.account.security.password.title": {
"message": "Password"
},
@@ -3020,9 +2918,6 @@
"settings.applications.field.url.placeholder": {
"message": "https://example.com"
},
"settings.applications.head-title": {
"message": "Mga aplikasyon"
},
"settings.applications.modal.header": {
"message": "Impormasyon ng aplikasyon"
},
@@ -3045,7 +2940,7 @@
"message": "Kasalukuyang hindi namin mapapakita ang mga pinahintulutang app, kinukumpuni pa namin ito. Mangyaring bisitahin ang pahinang ito sa ibang pagkataon!"
},
"settings.authorizations.head-title": {
"message": "Mga pagpapahintulot"
"message": "Mga Pagpapahintulot"
},
"settings.authorizations.revoke.action": {
"message": "Bawiin"
@@ -3119,15 +3014,6 @@
"settings.billing.payment_method.title": {
"message": "Mga paraan ng pagbabayad"
},
"settings.billing.price.slash-interval": {
"message": "/{interval}"
},
"settings.billing.pyro.resubscribe.error.text": {
"message": "Isang error ang naganap habang pagre-resubscribe sa iyong Modrinth server."
},
"settings.billing.pyro.resubscribe.error.title": {
"message": "Error sa muling pag-subscribe"
},
"settings.billing.pyro_subscription.description": {
"message": "Pamahalaan ang iyong mga Modrinth Server subscription."
},
@@ -3239,9 +3125,6 @@
"settings.display.theme.title": {
"message": "Paksang kulay"
},
"settings.head-title": {
"message": "Settings ng display"
},
"settings.pats.action.create": {
"message": "Lumikha ng PAT"
},
@@ -3302,9 +3185,6 @@
"settings.profile.description": {
"message": "Makikita ng publiko ang impormasyon ng iyong profile at sa <docs-link>API ng Modrinth</docs-link>."
},
"settings.profile.head-title": {
"message": "Settings ng profile"
},
"settings.profile.profile-info": {
"message": "Impormasyon ng profile"
},

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